Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

### Added

- Allow injecting into every element of a list using an asterisk `*` wildcard path segment in `@inject` https://github.com/nuwave/lighthouse/pull/2280

## v6.69.0

### Added
Expand Down
26 changes: 26 additions & 0 deletions docs/master/api-reference/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,8 @@ 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 to inject the value into every
element of a list found at that position.
"""
name: String!
) repeatable on FIELD_DEFINITION
Expand All @@ -2086,6 +2088,30 @@ type Mutation {
}
```

If you need to inject a value into every element of a list, such as a nested `create` mutation
for a `HasMany` relation, use an asterisk `*` as a wildcard path segment.

```graphql
type Mutation {
updateUser(input: UpdateUserInput!): User
@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
Expand Down
80 changes: 60 additions & 20 deletions src/Execution/Arguments/ArgumentSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Nuwave\Lighthouse\Execution\Arguments;

use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Exceptions\DefinitionException;

class ArgumentSet
{
Expand Down Expand Up @@ -68,33 +69,72 @@ public function exists(string $key): bool
/**
* Add a value at the dot-separated path.
*
* Works just like @see \Illuminate\Support\Arr::add().
* Works just like @see \Illuminate\Support\Arr::add(), but a path segment
* of `*` injects the value into every element of the list found at that
* position instead of a single key.
*/
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 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;
}
$this->addValueAtKeys(explode('.', $path), $value);

return $this;
}

/** @param non-empty-array<int, string> $keys */
private function addValueAtKeys(array $keys, mixed $value): void
{
$key = array_shift($keys);

$argumentSet = $argumentSet->arguments[$key]->value;
if ($keys === []) {
$argument = new Argument();
$argument->value = $value;
$this->arguments[$key] = $argument;

return;
}

$argument = new Argument();
$argument->value = $value;
$argumentSet->arguments[array_shift($keys)] = $argument;
$throughWildcard = $keys[0] === '*';

return $this;
// The branch leading up to a `*` might not have been given by the client
// at all, e.g. an optional nested input that was omitted. In that case,
// there is nothing to inject into, so we just do nothing.
if ($throughWildcard && ! isset($this->arguments[$key])) {
return;
}

// If the key doesn't exist at this depth, we will 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($this->arguments[$key])) {
$argument = new Argument();
$argument->value = new self();
$this->arguments[$key] = $argument;
}

$argument = $this->arguments[$key];

if (! $throughWildcard) {
$argument->value->addValueAtKeys($keys, $value);

return;
}

array_shift($keys);
if ($keys === []) {
throw new DefinitionException("Can not use `*` as the final path segment of the `name` argument of `@inject`, a field to inject into must follow, got: {$key}.*");
}

if (! is_array($argument->value)) {
throw new DefinitionException("Expected the value at `{$key}` to be a list because of the `*` wildcard used in the `name` argument of `@inject`, got: " . get_debug_type($argument->value) . '.');
}

foreach ($argument->value as $element) {
if (! $element instanceof self) {
throw new DefinitionException("Expected the elements of the list at `{$key}` to be inputs because of the `*` wildcard used in the `name` argument of `@inject`, got: " . get_debug_type($element) . '.');
}

$element->addValueAtKeys($keys, $value);
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/Schema/Directives/InjectDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ 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 to inject the value into every
element of a list found at that position.
"""
name: String!
) repeatable on FIELD_DEFINITION
Expand Down
177 changes: 177 additions & 0 deletions tests/Integration/Schema/Directives/InjectDirectiveTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tests\Integration\Schema\Directives;

use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Tests\DBTestCase;
use Tests\Utils\Models\User;

Expand Down Expand Up @@ -56,4 +57,180 @@ public function testCreateFromInputObjectWithDeepInjection(): void
],
]);
}

public function testInjectsIntoEveryElementOfANestedListWithWildcard(): void
{
$user = factory(User::class)->create();
$this->be($user);

$this->schema .= /** @lang GraphQL */ <<<'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: UpdateTasksHasManyInput
}

input UpdateTasksHasManyInput {
create: [CreateTaskInput!]
}

input CreateTaskInput {
name: String
}
GRAPHQL;

$this->graphQL(/** @lang GraphQL */ <<<'GRAPHQL'
mutation ($input: UpdateUserInput!) {
updateUser(input: $input) {
tasks {
name
user {
id
}
}
}
}
GRAPHQL, [
'input' => [
'id' => $user->getKey(),
'tasks' => [
'create' => [
['name' => 'foo'],
['name' => 'bar'],
],
],
],
])->assertJson([
'data' => [
'updateUser' => [
'tasks' => [
[
'name' => 'foo',
'user' => ['id' => (string) $user->getKey()],
],
[
'name' => 'bar',
'user' => ['id' => (string) $user->getKey()],
],
],
],
],
]);
}

public function testSkipsWildcardInjectionWhenTheTargetedListIsNotGiven(): void
{
$user = factory(User::class)->create();
$this->be($user);

$this->schema .= /** @lang GraphQL */ <<<'GRAPHQL'
type Task {
id: ID!
name: String!
}

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: UpdateTasksHasManyInput
}

input UpdateTasksHasManyInput {
create: [CreateTaskInput!]
}

input CreateTaskInput {
name: String
}
GRAPHQL;

$this->graphQL(/** @lang GraphQL */ <<<'GRAPHQL'
mutation ($input: UpdateUserInput!) {
updateUser(input: $input) {
id
tasks {
id
}
}
}
GRAPHQL, [
'input' => [
'id' => $user->getKey(),
],
])->assertJson([
'data' => [
'updateUser' => [
'id' => (string) $user->getKey(),
'tasks' => [],
],
],
]);
}

public function testThrowsWhenWildcardIsUsedOnAValueThatIsNotAList(): void
{
$user = factory(User::class)->create();
$this->be($user);

$this->schema .= /** @lang GraphQL */ <<<'GRAPHQL'
type Task {
id: ID!
name: String!
user: User @belongsTo
}

type User {
id: ID
}

type Mutation {
createTask(input: CreateTaskInput! @spread): Task
@create
@inject(context: "user.id", name: "name.*.user_id")
}

input CreateTaskInput {
name: String
}
GRAPHQL;

$this->expectException(DefinitionException::class);
$this->expectExceptionMessage('Expected the value at `name` to be a list because of the `*` wildcard used in the `name` argument of `@inject`, got: string.');

$this->graphQL(/** @lang GraphQL */ <<<'GRAPHQL'
mutation {
createTask(input: {
name: "foo"
}) {
id
}
}
GRAPHQL);
}
}
Loading
Loading