diff --git a/CHANGELOG.md b/CHANGELOG.md index 09d482bc5b..bdb931eb2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +- Allow to set value using asterisks as a wildcard in `@inject` https://github.com/nuwave/lighthouse/pull/2280 + ### Changed - Pass resolver arguments to `FieldBuilderDirective::handleFieldBuilder()` https://github.com/nuwave/lighthouse/pull/2234 diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index 3ab68ac9da..247067ea61 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -1729,6 +1729,7 @@ directive @inject( The target name of the argument into which the value is injected. You can use dot notation to set the value at arbitrary depth within the incoming argument. + Use an asterisk `*` as a path segment where the value of the argument is a list to be traversed. """ name: String! ) repeatable on FIELD_DEFINITION @@ -1756,6 +1757,29 @@ type Mutation { } ``` +If you have an array you need to inject a value into, you can use an asterisk `*`. + +```graphql +type Mutation { + updateUser(input: UpdateUserInput!): Task + @update + @inject(context: "user.id", name: "input.tasks.create.*.user_id") +} + +input UpdateUserInput { + id: ID! + tasks: UpdateTasksHasManyInput +} + +input UpdateTasksHasManyInput { + create: [CreateTaskInput!] +} + +input CreateTaskInput { + title: String! +} +``` + ## @interface ```graphql diff --git a/src/Execution/Arguments/Argument.php b/src/Execution/Arguments/Argument.php index 6ded839a13..b1c1720e12 100644 --- a/src/Execution/Arguments/Argument.php +++ b/src/Execution/Arguments/Argument.php @@ -4,7 +4,7 @@ use Illuminate\Support\Collection; -class Argument +class Argument implements \ArrayAccess, \IteratorAggregate { /** * The value given by the client. @@ -85,4 +85,42 @@ protected static function toPlainRecursive($value) return $value; } + + public function offsetExists(mixed $offset): bool + { + $argument = $this; + + return isset($argument->value[$offset]); + } + + public function &offsetGet(mixed $offset): mixed + { + $argument = $this; + + return $argument->value[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $argument = $this; + + $argumentSet = new ArgumentSet(); + $argumentSet[(string) $offset] = $value; + + $argument->value = $argumentSet; + } + + public function offsetUnset(mixed $offset): void + { + $argument = $this; + + unset($argument[$offset]); + } + + public function getIterator(): \ArrayIterator + { + $value = $this->value; + + return new \ArrayIterator($value); + } } diff --git a/src/Execution/Arguments/ArgumentSet.php b/src/Execution/Arguments/ArgumentSet.php index 298f26296e..20e48b35ba 100644 --- a/src/Execution/Arguments/ArgumentSet.php +++ b/src/Execution/Arguments/ArgumentSet.php @@ -2,7 +2,7 @@ namespace Nuwave\Lighthouse\Execution\Arguments; -class ArgumentSet +class ArgumentSet implements \ArrayAccess, \IteratorAggregate { /** * An associative array from argument names to arguments. @@ -61,34 +61,15 @@ public function has(string $key): bool /** * Add a value at the dot-separated path. - * - * Works just like @see \Illuminate\Support\Arr::add(). + * Asterisks may be used to indicate wildcards. * * @param mixed $value any value to inject */ - public function addValue(string $path, $value): self + public function addValue(string $path, mixed $value): self { $argumentSet = $this; - $keys = explode('.', $path); - - while (count($keys) > 1) { - $key = array_shift($keys); - // If the key doesn't exist at this depth, we will just create an empty ArgumentSet - // to hold the next value, allowing us to create the ArgumentSet to hold a final - // value at the correct depth. Then we'll keep digging into the ArgumentSet. - if (! isset($argumentSet->arguments[$key])) { - $argument = new Argument(); - $argument->value = new self(); - $argumentSet->arguments[$key] = $argument; - } - - $argumentSet = $argumentSet->arguments[$key]->value; - } - - $argument = new Argument(); - $argument->value = $value; - $argumentSet->arguments[array_shift($keys)] = $argument; + data_set($argumentSet, $path, $value); return $this; } @@ -102,4 +83,41 @@ public function argumentsWithUndefined(): array { return array_merge($this->arguments, $this->undefined); } + + public function offsetExists(mixed $offset): bool + { + $argumentSet = $this; + + return isset($argumentSet->arguments[$offset]); + } + + public function &offsetGet(mixed $offset): Argument + { + $argumentSet = $this; + + return $argumentSet->arguments[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $argumentSet = $this; + + $argument = new Argument(); + $argument->value = $value; + $argumentSet->arguments[(string) $offset] = $argument; + } + + public function offsetUnset(mixed $offset): void + { + $argumentSet = $this; + + unset($argumentSet->arguments[$offset]); + } + + public function getIterator(): \ArrayIterator + { + $arguments = $this->arguments; + + return new \ArrayIterator($arguments); + } } diff --git a/src/Schema/Directives/InjectDirective.php b/src/Schema/Directives/InjectDirective.php index 5a7f9d73d8..dbe52b6ed2 100644 --- a/src/Schema/Directives/InjectDirective.php +++ b/src/Schema/Directives/InjectDirective.php @@ -28,6 +28,7 @@ public static function definition(): string The target name of the argument into which the value is injected. You can use dot notation to set the value at arbitrary depth within the incoming argument. + Use an asterisk `*` as a path segment where the value of the argument is a list to be traversed. """ name: String! ) repeatable on FIELD_DEFINITION diff --git a/tests/Integration/Schema/Directives/InjectDirectiveTest.php b/tests/Integration/Schema/Directives/InjectDirectiveTest.php index 017386cf87..a2f8a12e3d 100644 --- a/tests/Integration/Schema/Directives/InjectDirectiveTest.php +++ b/tests/Integration/Schema/Directives/InjectDirectiveTest.php @@ -12,7 +12,7 @@ public function testCreateFromInputObjectWithDeepInjection(): void $user = factory(User::class)->create(); $this->be($user); - $this->schema .= ' + $this->schema .= /* @lang GraphQL */ ' type Task { id: ID! name: String! @@ -56,4 +56,152 @@ public function testCreateFromInputObjectWithDeepInjection(): void ], ]); } + + public function testCreateFromInputObjectWithWildcardInjection(): void + { + $user = factory(User::class)->create(); + $this->be($user); + + $this->schema .= /* @lang GraphQL */ ' + type Task { + id: ID! + name: String! + user: User @belongsTo + } + + type User { + id: ID + tasks: [Task!] @hasmany + } + + type Mutation { + updateUser(input: UpdateUserInput @spread): User + @update + @inject(context: "user.id", name: "tasks.create.*.user_id") + } + + input UpdateUserInput { + id: ID! + tasks: CreateTaskInputMany + } + + input CreateTaskInputMany { + create: [CreateTaskInput!] + } + + input CreateTaskInput { + name: String + } + '; + + $this->graphQL(' + mutation ($input: UpdateUserInput!) { + updateUser(input: $input) { + tasks { + id + name + user { + id + } + } + } + } + ', [ + 'input' => [ + 'id' => $user->getKey(), + 'tasks' => [ + 'create' => [ + [ 'name' => 'foo' ], + [ 'name' => 'bar' ], + ], + ], + ], + ])->assertJson([ + 'data' => [ + 'updateUser' => [ + 'tasks' => [ + [ + 'id' => '1', + 'name' => 'foo', + 'user' => [ + 'id' => '1', + ], + ], + [ + 'id' => '2', + 'name' => 'bar', + 'user' => [ + 'id' => '1', + ], + ], + ], + ], + ], + ]); + } + + + public function testWillRejectValuesNotPlacedAtArrayWithWildcardInjection(): void + { + $user = factory(User::class)->create(); + $this->be($user); + + $this->schema .= /* @lang GraphQL */ ' + type Task { + id: ID! + name: String! + user: User @belongsTo + } + + type User { + id: ID + tasks: [Task!] @hasmany + } + + type Mutation { + updateUser(input: UpdateUserInput @spread): User + @update + @inject(context: "user.id", name: "tasks.create.*.user_id") + } + + input UpdateUserInput { + id: ID! + tasks: CreateTaskInputMany + } + + input CreateTaskInputMany { + create: [CreateTaskInput!] + } + + input CreateTaskInput { + name: String + } + '; + + $this->graphQL(' + mutation ($input: UpdateUserInput!) { + updateUser(input: $input) { + id + tasks { + id + name + user { + id + } + } + } + } + ', [ + 'input' => [ + 'id' => $user->getKey(), + ], + ])->assertJson([ + 'data' => [ + 'updateUser' => [ + 'id' => $user->getKey(), + 'tasks' => [], + ], + ], + ]); + } }