Skip to content

Commit

Permalink
Allow customizing the definition of the root Query field node add…
Browse files Browse the repository at this point in the history
…ed by `@node` (#2449)
  • Loading branch information
spawnia authored Sep 20, 2023
1 parent b30a632 commit 24178ba
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 34 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 16 additions & 3 deletions docs/6/api-reference/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`.

Expand All @@ -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'],
Expand All @@ -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

Expand Down
8 changes: 4 additions & 4 deletions docs/6/digging-deeper/relay.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 16 additions & 3 deletions docs/master/api-reference/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`.

Expand All @@ -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'],
Expand All @@ -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

Expand Down
8 changes: 4 additions & 4 deletions docs/master/digging-deeper/relay.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 7 additions & 4 deletions src/GlobalId/NodeDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 */ <<<GRAPHQL
node(id: ID! @globalId): Node @field(resolver: "{$nodeRegistryClass}@resolve")
GRAPHQL
);
if (! ASTHelper::hasNode($queryType->fields, 'node')) {
$queryType->fields[] = Parser::fieldDefinition(/** @lang GraphQL */ <<<GRAPHQL
node(id: ID! @globalId): Node @field(resolver: "{$nodeRegistryClass}@resolve")
GRAPHQL
);
}
}
}
}
8 changes: 4 additions & 4 deletions src/GlobalId/NodeRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use GraphQL\Error\Error;
use GraphQL\Type\Definition\Type;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Execution\ResolveInfo;
use Nuwave\Lighthouse\Schema\TypeRegistry;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
Expand All @@ -19,7 +18,7 @@ class NodeRegistry
*
* @var array<string, NodeResolverFn>
*/
protected array $nodeResolver = [];
protected array $nodeResolverFns = [];

/**
* The stashed current type.
Expand All @@ -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;
}
Expand All @@ -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.");
}

Expand Down
26 changes: 18 additions & 8 deletions src/Schema/AST/ASTHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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.");
Expand All @@ -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)
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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
{
Expand All @@ -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
Expand All @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions tests/Integration/GlobalId/NodeDirectiveDBTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -30,7 +30,7 @@ public function testExplicitNull(): void
';

$this
->graphQL(/** @lang GraphQL */'
->graphQL(/** @lang GraphQL */ '
{
users(createdBetween: null) {
id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -30,7 +30,7 @@ public function testExplicitNull(): void
';

$this
->graphQL(/** @lang GraphQL */'
->graphQL(/** @lang GraphQL */ '
{
users(notCreatedBetween: null) {
id
Expand Down

0 comments on commit 24178ba

Please sign in to comment.