diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ac775de22..0eea753fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +## v6.19.0 + +### Added + +- Allow customizing the definition of the root `Query` field `node` added by `@node` https://github.com/nuwave/lighthouse/pull/2449 + ## v6.18.2 ### Fixed diff --git a/docs/6/api-reference/directives.md b/docs/6/api-reference/directives.md index f947a9497c..48f67bbe29 100644 --- a/docs/6/api-reference/directives.md +++ b/docs/6/api-reference/directives.md @@ -2360,6 +2360,18 @@ directive @node( ) on OBJECT ``` +When you use `@node` on a type, Lighthouse will add a field `node` to the root Query type. +If you want to customize its description, change the resolver or add middleware, you can add it yourself like this: + +```graphql +type Query { + "This description is up to you." + node(id: ID! @globalId): Node @field(resolver: "Nuwave\\Lighthouse\\GlobalId\\NodeRegistry@resolve") + @someMiddlewareDirective + @maybeAuthorization +} +``` + Lighthouse defaults to resolving types through the underlying model, for example by calling `User::find($id)`. @@ -2381,7 +2393,8 @@ The `resolver` argument has to specify a function which will be passed the decoded `id` and resolves to a result. ```php -public function byId($id): array { +public function byId($id): array +{ return [ 'DE' => ['name' => 'Germany'], 'MY' => ['name' => 'Malaysia'], @@ -2391,8 +2404,8 @@ public function byId($id): array { [Read more](../digging-deeper/relay.md#global-object-identification). -Behind the scenes, Lighthouse will decode the global id sent from the client -to find the model by it's primary id in the database. +Behind the scenes, Lighthouse will decode the global ID sent from the client +to find the model by its primary key in the database. ## @notIn diff --git a/docs/6/digging-deeper/relay.md b/docs/6/digging-deeper/relay.md index 4dce4838c7..c6b7f467b4 100644 --- a/docs/6/digging-deeper/relay.md +++ b/docs/6/digging-deeper/relay.md @@ -33,11 +33,11 @@ type User { You may rebind the `\Nuwave\Lighthouse\Support\Contracts\GlobalId` interface to add your own mechanism of encoding/decoding global ids. -[Global Object Identification](https://facebook.github.io/relay/graphql/objectidentification.htm) +[Global Object Identification](https://relay.dev/graphql/objectidentification.htm) -[@node](../api-reference/directives.md#node) - -[@globalId](../api-reference/directives.md#globalid) +Directives: +- [@node](../api-reference/directives.md#node) +- [@globalId](../api-reference/directives.md#globalid) ## Input Object Mutations diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index f947a9497c..48f67bbe29 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -2360,6 +2360,18 @@ directive @node( ) on OBJECT ``` +When you use `@node` on a type, Lighthouse will add a field `node` to the root Query type. +If you want to customize its description, change the resolver or add middleware, you can add it yourself like this: + +```graphql +type Query { + "This description is up to you." + node(id: ID! @globalId): Node @field(resolver: "Nuwave\\Lighthouse\\GlobalId\\NodeRegistry@resolve") + @someMiddlewareDirective + @maybeAuthorization +} +``` + Lighthouse defaults to resolving types through the underlying model, for example by calling `User::find($id)`. @@ -2381,7 +2393,8 @@ The `resolver` argument has to specify a function which will be passed the decoded `id` and resolves to a result. ```php -public function byId($id): array { +public function byId($id): array +{ return [ 'DE' => ['name' => 'Germany'], 'MY' => ['name' => 'Malaysia'], @@ -2391,8 +2404,8 @@ public function byId($id): array { [Read more](../digging-deeper/relay.md#global-object-identification). -Behind the scenes, Lighthouse will decode the global id sent from the client -to find the model by it's primary id in the database. +Behind the scenes, Lighthouse will decode the global ID sent from the client +to find the model by its primary key in the database. ## @notIn diff --git a/docs/master/digging-deeper/relay.md b/docs/master/digging-deeper/relay.md index 4dce4838c7..c6b7f467b4 100644 --- a/docs/master/digging-deeper/relay.md +++ b/docs/master/digging-deeper/relay.md @@ -33,11 +33,11 @@ type User { You may rebind the `\Nuwave\Lighthouse\Support\Contracts\GlobalId` interface to add your own mechanism of encoding/decoding global ids. -[Global Object Identification](https://facebook.github.io/relay/graphql/objectidentification.htm) +[Global Object Identification](https://relay.dev/graphql/objectidentification.htm) -[@node](../api-reference/directives.md#node) - -[@globalId](../api-reference/directives.md#globalid) +Directives: +- [@node](../api-reference/directives.md#node) +- [@globalId](../api-reference/directives.md#globalid) ## Input Object Mutations diff --git a/src/GlobalId/NodeDirective.php b/src/GlobalId/NodeDirective.php index 45f3b42c86..c0c7fba5e9 100644 --- a/src/GlobalId/NodeDirective.php +++ b/src/GlobalId/NodeDirective.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Database\Eloquent\Model; use Nuwave\Lighthouse\Exceptions\DefinitionException; +use Nuwave\Lighthouse\Schema\AST\ASTHelper; use Nuwave\Lighthouse\Schema\AST\DocumentAST; use Nuwave\Lighthouse\Schema\Directives\BaseDirective; use Nuwave\Lighthouse\Schema\RootType; @@ -104,10 +105,12 @@ interface {$nodeInterfaceName} @interface(resolveType: "{$nodeRegistryClass}@res $queryType = $documentAST->types[RootType::QUERY]; assert($queryType instanceof ObjectTypeDefinitionNode); - $queryType->fields[] = Parser::fieldDefinition(/** @lang GraphQL */ <<fields, 'node')) { + $queryType->fields[] = Parser::fieldDefinition(/** @lang GraphQL */ << */ - protected array $nodeResolver = []; + protected array $nodeResolverFns = []; /** * The stashed current type. @@ -43,7 +42,7 @@ public function __construct( */ public function registerNode(string $typeName, callable $resolver): self { - $this->nodeResolver[$typeName] = $resolver; + $this->nodeResolverFns[$typeName] = $resolver; return $this; } @@ -65,8 +64,9 @@ public function resolve(mixed $root, array $args, GraphQLContext $context, Resol throw new Error("[{$decodedType}] is not a type and cannot be resolved."); } + $resolver = $this->nodeResolverFns[$decodedType] ?? null; // Check if we have a resolver registered for the given type - if (! $resolver = Arr::get($this->nodeResolver, $decodedType)) { + if (! $resolver) { throw new Error("[{$decodedType}] is not a registered node and cannot be resolved."); } diff --git a/src/Schema/AST/ASTHelper.php b/src/Schema/AST/ASTHelper.php index 0a2c5b00ac..71f729da4f 100644 --- a/src/Schema/AST/ASTHelper.php +++ b/src/Schema/AST/ASTHelper.php @@ -112,7 +112,7 @@ public static function duplicateDefinition(string $oldName): string /** Unwrap lists and non-nulls and get the name of the contained type. */ public static function getUnderlyingTypeName(Node $definition): string { - $namedType = self::getUnderlyingNamedTypeNode($definition); + $namedType = static::getUnderlyingNamedTypeNode($definition); return $namedType->name->value; } @@ -130,7 +130,7 @@ public static function getUnderlyingNamedTypeNode(Node $node): NamedTypeNode || $node instanceof FieldDefinitionNode || $node instanceof InputValueDefinitionNode ) { - return self::getUnderlyingNamedTypeNode($node->type); + return static::getUnderlyingNamedTypeNode($node->type); } throw new DefinitionException("The node '{$node->kind}' does not have a type associated with it."); @@ -143,7 +143,7 @@ public static function getUnderlyingNamedTypeNode(Node $node): NamedTypeNode */ public static function directiveArgValue(DirectiveNode $directive, string $name, mixed $default = null): mixed { - $arg = self::firstByName($directive->arguments, $name); + $arg = static::firstByName($directive->arguments, $name); return $arg !== null ? AST::valueFromASTUntyped($arg->value) @@ -186,13 +186,13 @@ public static function directiveDefinition(Node $definitionNode, string $name): /** @var \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode> $directives */ $directives = $definitionNode->directives; - return self::firstByName($directives, $name); + return static::firstByName($directives, $name); } /** Check if a node has a directive with the given name on it. */ public static function hasDirective(Node $definitionNode, string $name): bool { - return self::directiveDefinition($definitionNode, $name) !== null; + return static::directiveDefinition($definitionNode, $name) !== null; } /** @@ -219,6 +219,16 @@ public static function firstByName(iterable $nodes, string $name): ?Node return null; } + /** + * Does the given list of nodes contain a node with the given name? + * + * @param iterable<\GraphQL\Language\AST\Node> $nodes + */ + public static function hasNode(iterable $nodes, string $name): bool + { + return static::firstByName($nodes, $name) !== null; + } + /** Directives might have an additional namespace associated with them, @see \Nuwave\Lighthouse\Schema\Directives\NamespaceDirective. */ public static function namespaceForDirective(Node $definitionNode, string $directiveName): ?string { @@ -244,7 +254,7 @@ public static function attachDirectiveToObjectTypeFields(DocumentAST $documentAS /** Checks the given type to see whether it implements the given interface. */ public static function typeImplementsInterface(ObjectTypeDefinitionNode $type, string $interfaceName): bool { - return self::firstByName($type->interfaces, $interfaceName) !== null; + return static::hasNode($type->interfaces, $interfaceName); } public static function addDirectiveToFields(DirectiveNode $directiveNode, ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode &$typeWithFields): void @@ -253,14 +263,14 @@ public static function addDirectiveToFields(DirectiveNode $directiveNode, Object $directiveLocator = Container::getInstance()->make(DirectiveLocator::class); $directive = $directiveLocator->resolve($name); - $directiveDefinition = self::extractDirectiveDefinition($directive::definition()); + $directiveDefinition = static::extractDirectiveDefinition($directive::definition()); foreach ($typeWithFields->fields as $fieldDefinition) { // If the field already has the same directive defined, and it is not // a repeatable directive, skip over it. // Field directives are more specific than those defined on a type. if ( - self::hasDirective($fieldDefinition, $name) + static::hasDirective($fieldDefinition, $name) && ! $directiveDefinition->repeatable ) { continue; diff --git a/tests/Integration/GlobalId/NodeDirectiveDBTest.php b/tests/Integration/GlobalId/NodeDirectiveDBTest.php index 0b96445bab..b69120b332 100644 --- a/tests/Integration/GlobalId/NodeDirectiveDBTest.php +++ b/tests/Integration/GlobalId/NodeDirectiveDBTest.php @@ -217,4 +217,32 @@ public function testThrowsWhenNodeDirectiveIsDefinedOnNonObjectType(): void } '); } + + public function testPreservesCustomNodeField(): void + { + $result = 42; + $this->mockResolver($result); + + $this->schema .= /** @lang GraphQL */ ' + type Query { + # Nonsensical example, just done this way for ease of testing. + # Usually customization would have the purpose of adding middleware. + node: Int! @mock + } + + type User @node { + name: String! + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + { + node + } + ')->assertExactJson([ + 'data' => [ + 'node' => $result, + ], + ]); + } } diff --git a/tests/Integration/Schema/Directives/WhereBetweenDirectiveTest.php b/tests/Integration/Schema/Directives/WhereBetweenDirectiveTest.php index 13aa3b91cf..3e7807fd87 100644 --- a/tests/Integration/Schema/Directives/WhereBetweenDirectiveTest.php +++ b/tests/Integration/Schema/Directives/WhereBetweenDirectiveTest.php @@ -11,7 +11,7 @@ public function testExplicitNull(): void { $users = factory(User::class, 2)->create(); - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' scalar DateTime @scalar(class: "Nuwave\\\Lighthouse\\\Schema\\\Types\\\Scalars\\\DateTime") type User { @@ -30,7 +30,7 @@ public function testExplicitNull(): void '; $this - ->graphQL(/** @lang GraphQL */' + ->graphQL(/** @lang GraphQL */ ' { users(createdBetween: null) { id diff --git a/tests/Integration/Schema/Directives/WhereNotBetweenDirectiveTest.php b/tests/Integration/Schema/Directives/WhereNotBetweenDirectiveTest.php index 512224de1b..da493b6976 100644 --- a/tests/Integration/Schema/Directives/WhereNotBetweenDirectiveTest.php +++ b/tests/Integration/Schema/Directives/WhereNotBetweenDirectiveTest.php @@ -11,7 +11,7 @@ public function testExplicitNull(): void { $users = factory(User::class, 2)->create(); - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' scalar DateTime @scalar(class: "Nuwave\\\Lighthouse\\\Schema\\\Types\\\Scalars\\\DateTime") type User { @@ -30,7 +30,7 @@ public function testExplicitNull(): void '; $this - ->graphQL(/** @lang GraphQL */' + ->graphQL(/** @lang GraphQL */ ' { users(notCreatedBetween: null) { id