Skip to content

Commit 94e1e9a

Browse files
authored
Add Node interface as soon as a type uses it (#2387)
1 parent d4ea07d commit 94e1e9a

File tree

13 files changed

+122
-144
lines changed

13 files changed

+122
-144
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
<!-- Link to related issues this PR resolves, e.g. "Resolves #236"-->
2+
13
- [ ] Added or updated tests
24
- [ ] Documented user facing changes
35
- [ ] Updated CHANGELOG.md
46

5-
<!-- Link to related issues this PR resolves, e.g. "Resolves #236"-->
6-
77
**Changes**
88

99
<!-- Detail the changes in behaviour this PR introduces. -->

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ You can find and compare releases at the [GitHub release page](https://github.co
99

1010
## Unreleased
1111

12+
## v6.6.1
13+
14+
### Fixed
15+
16+
- Add `Node` interface as soon as a type uses it with `@node` https://github.com/nuwave/lighthouse/pull/2387
17+
1218
## v6.6.0
1319

1420
### Fixed

docs/4/subscriptions/client-implementations.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ class PusherLink extends ApolloLink {
2525
});
2626

2727
// Capture the super method
28-
const prevSubscribe = subscribeObservable.subscribe.bind(
29-
subscribeObservable
30-
);
28+
const prevSubscribe =
29+
subscribeObservable.subscribe.bind(subscribeObservable);
3130

3231
// Override subscribe to return an `unsubscribe` object, see
3332
// https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts#L182-L212

docs/5/subscriptions/client-implementations.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ class PusherLink extends ApolloLink {
2525
});
2626

2727
// Capture the super method
28-
const prevSubscribe = subscribeObservable.subscribe.bind(
29-
subscribeObservable
30-
);
28+
const prevSubscribe =
29+
subscribeObservable.subscribe.bind(subscribeObservable);
3130

3231
// Override subscribe to return an `unsubscribe` object, see
3332
// https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts#L182-L212

docs/6/subscriptions/client-implementations.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ class PusherLink extends ApolloLink {
2525
});
2626

2727
// Capture the super method
28-
const prevSubscribe = subscribeObservable.subscribe.bind(
29-
subscribeObservable
30-
);
28+
const prevSubscribe =
29+
subscribeObservable.subscribe.bind(subscribeObservable);
3130

3231
// Override subscribe to return an `unsubscribe` object, see
3332
// https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts#L182-L212

docs/master/subscriptions/client-implementations.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ class PusherLink extends ApolloLink {
2525
});
2626

2727
// Capture the super method
28-
const prevSubscribe = subscribeObservable.subscribe.bind(
29-
subscribeObservable
30-
);
28+
const prevSubscribe =
29+
subscribeObservable.subscribe.bind(subscribeObservable);
3130

3231
// Override subscribe to return an `unsubscribe` object, see
3332
// https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts#L182-L212

src/Federation/ASTManipulator.php

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
66
use GraphQL\Language\Parser;
77
use Nuwave\Lighthouse\Events\ManipulateAST;
8+
use Nuwave\Lighthouse\Federation\Resolvers\Entities;
9+
use Nuwave\Lighthouse\Federation\Resolvers\Service;
10+
use Nuwave\Lighthouse\Federation\Types\Any;
11+
use Nuwave\Lighthouse\Federation\Types\FieldSet;
812
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
913
use Nuwave\Lighthouse\Schema\RootType;
1014

@@ -22,17 +26,15 @@ public function handle(ManipulateAST $manipulateAST): void
2226

2327
protected function addScalars(DocumentAST &$documentAST): void
2428
{
25-
$documentAST->setTypeDefinition(
26-
Parser::scalarTypeDefinition(/** @lang GraphQL */ '
27-
scalar _Any @scalar(class: "Nuwave\\\Lighthouse\\\Federation\\\Types\\\Any")
28-
'),
29-
);
30-
31-
$documentAST->setTypeDefinition(
32-
Parser::scalarTypeDefinition(/** @lang GraphQL */ '
33-
scalar _FieldSet @scalar(class: "Nuwave\\\Lighthouse\\\Federation\\\Types\\\FieldSet")
34-
'),
35-
);
29+
$anyClass = addslashes(Any::class);
30+
$documentAST->setTypeDefinition(Parser::scalarTypeDefinition(/** @lang GraphQL */ <<<GRAPHQL
31+
scalar _Any @scalar(class: "{$anyClass}")
32+
GRAPHQL));
33+
34+
$fieldSetClass = addslashes(FieldSet::class);
35+
$documentAST->setTypeDefinition(Parser::scalarTypeDefinition(/** @lang GraphQL */ <<<GRAPHQL
36+
scalar _FieldSet @scalar(class: "{$fieldSetClass}")
37+
GRAPHQL));
3638
}
3739

3840
/** Combine object types with @key into the _Entity union. */
@@ -68,24 +70,24 @@ protected function addEntityUnion(DocumentAST &$documentAST): void
6870

6971
protected function addRootFields(DocumentAST &$documentAST): void
7072
{
71-
// In federation it is fine for a schema to not have a user-defined root query type,
73+
// In federation, it is fine for a schema to not have a user-defined root query type,
7274
// since we add two federation related fields to it here.
73-
if (! isset($documentAST->types[RootType::QUERY])) {
74-
$documentAST->types[RootType::QUERY] = Parser::objectTypeDefinition(/** @lang GraphQL */ 'type Query');
75-
}
75+
$documentAST->types[RootType::QUERY] ??= Parser::objectTypeDefinition(/** @lang GraphQL */ 'type Query');
7676

7777
$queryType = $documentAST->types[RootType::QUERY];
7878
assert($queryType instanceof ObjectTypeDefinitionNode);
7979

80-
$queryType->fields[] = Parser::fieldDefinition(/** @lang GraphQL */ '
81-
_entities(
82-
representations: [_Any!]!
83-
): [_Entity]! @field(resolver: "Nuwave\\\Lighthouse\\\Federation\\\Resolvers\\\Entities")
84-
');
85-
86-
$queryType->fields[] = Parser::fieldDefinition(/** @lang GraphQL */ '
87-
_service: _Service! @field(resolver: "Nuwave\\\Lighthouse\\\Federation\\\Resolvers\\\Service")
88-
');
80+
$entitiesClass = addslashes(Entities::class);
81+
$queryType->fields[] = Parser::fieldDefinition(/** @lang GraphQL */ <<<GRAPHQL
82+
_entities(
83+
representations: [_Any!]!
84+
): [_Entity]! @field(resolver: "{$entitiesClass}")
85+
GRAPHQL);
86+
87+
$serviceClass = addslashes(Service::class);
88+
$queryType->fields[] = Parser::fieldDefinition(/** @lang GraphQL */ <<<GRAPHQL
89+
_service: _Service! @field(resolver: "{$serviceClass}")
90+
GRAPHQL);
8991
}
9092

9193
protected function addServiceType(DocumentAST &$documentAST): void

src/GlobalId/GlobalIdServiceProvider.php

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,12 @@
22

33
namespace Nuwave\Lighthouse\GlobalId;
44

5-
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
6-
use GraphQL\Language\Parser;
75
use Illuminate\Contracts\Events\Dispatcher;
86
use Illuminate\Support\ServiceProvider;
9-
use Nuwave\Lighthouse\Events\ManipulateAST;
107
use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces;
11-
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
12-
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
13-
use Nuwave\Lighthouse\Schema\RootType;
148

159
class GlobalIdServiceProvider extends ServiceProvider
1610
{
17-
public const NODE = 'Node';
18-
1911
public function register(): void
2012
{
2113
$this->app->bind(GlobalId::class, Base64GlobalId::class);
@@ -25,55 +17,5 @@ public function register(): void
2517
public function boot(Dispatcher $dispatcher): void
2618
{
2719
$dispatcher->listen(RegisterDirectiveNamespaces::class, static fn (): string => __NAMESPACE__);
28-
$dispatcher->listen(ManipulateAST::class, function (ManipulateAST $manipulateAST): void {
29-
$documentAST = $manipulateAST->documentAST;
30-
31-
// Only add the node type and node field if a type actually implements them.
32-
// If we were to add it regardless, a validation error is thrown because an
33-
// interface without implementations is pointless to have in the schema.
34-
if ($this->hasTypeImplementingNodeInterface($documentAST)) {
35-
$this->addNodeSupport($documentAST);
36-
}
37-
});
38-
}
39-
40-
protected function addNodeSupport(DocumentAST $documentAST): void
41-
{
42-
$node = self::NODE;
43-
$globalId = config('lighthouse.global_id_field');
44-
45-
// Double slashes to escape the slashes in the namespace.
46-
$documentAST->setTypeDefinition(
47-
Parser::interfaceTypeDefinition(/** @lang GraphQL */ <<<GRAPHQL
48-
"Any object implementing this type can be found by ID through `Query.node`."
49-
interface {$node} @interface(resolveType: "Nuwave\\\Lighthouse\\\GlobalId\\\NodeRegistry@resolveType") {
50-
"Global identifier that can be used to resolve any Node implementation."
51-
{$globalId}: ID!
52-
}
53-
GRAPHQL
54-
),
55-
);
56-
57-
$queryType = $documentAST->types[RootType::QUERY];
58-
assert($queryType instanceof ObjectTypeDefinitionNode);
59-
60-
$queryType->fields[] = Parser::fieldDefinition(/** @lang GraphQL */ <<<'GRAPHQL'
61-
node(id: ID! @globalId): Node @field(resolver: "Nuwave\\Lighthouse\\GlobalId\\NodeRegistry@resolve")
62-
GRAPHQL
63-
);
64-
}
65-
66-
protected function hasTypeImplementingNodeInterface(DocumentAST $documentAST): bool
67-
{
68-
foreach ($documentAST->types as $typeDefinition) {
69-
if (
70-
$typeDefinition instanceof ObjectTypeDefinitionNode
71-
&& ASTHelper::typeImplementsInterface($typeDefinition, self::NODE)
72-
) {
73-
return true;
74-
}
75-
}
76-
77-
return false;
7820
}
7921
}

src/GlobalId/NodeDirective.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,23 @@
66
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
77
use GraphQL\Language\AST\TypeDefinitionNode;
88
use GraphQL\Language\Parser;
9+
use Illuminate\Contracts\Config\Repository as ConfigRepository;
910
use Illuminate\Database\Eloquent\Model;
1011
use Nuwave\Lighthouse\Exceptions\DefinitionException;
1112
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
1213
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
14+
use Nuwave\Lighthouse\Schema\RootType;
1315
use Nuwave\Lighthouse\Schema\Values\TypeValue;
1416
use Nuwave\Lighthouse\Support\Contracts\TypeManipulator;
1517
use Nuwave\Lighthouse\Support\Contracts\TypeMiddleware;
1618

1719
class NodeDirective extends BaseDirective implements TypeMiddleware, TypeManipulator
1820
{
21+
public const NODE_INTERFACE_NAME = 'Node';
22+
1923
public function __construct(
2024
protected NodeRegistry $nodeRegistry,
25+
protected ConfigRepository $config,
2126
) {}
2227

2328
public static function definition(): string
@@ -74,11 +79,35 @@ public function manipulateTypeDefinition(DocumentAST &$documentAST, TypeDefiniti
7479
throw new DefinitionException("The {$this->name()} directive must only be used on object type definitions, not on {$typeDefinition->kind} {$typeDefinition->getName()->value}.");
7580
}
7681

77-
$namedTypeNode = Parser::parseType(GlobalIdServiceProvider::NODE, ['noLocation' => true]);
82+
$namedTypeNode = Parser::parseType(self::NODE_INTERFACE_NAME, ['noLocation' => true]);
7883
assert($namedTypeNode instanceof NamedTypeNode);
7984
$typeDefinition->interfaces[] = $namedTypeNode;
8085

81-
$globalIdFieldName = config('lighthouse.global_id_field');
86+
$globalIdFieldName = $this->config->get('lighthouse.global_id_field');
8287
$typeDefinition->fields[] = Parser::fieldDefinition(/** @lang GraphQL */ "{$globalIdFieldName}: ID! @globalId");
88+
89+
if (! isset($documentAST->types[self::NODE_INTERFACE_NAME])) {
90+
$nodeInterfaceName = self::NODE_INTERFACE_NAME;
91+
$nodeRegistryClass = addslashes(NodeRegistry::class);
92+
93+
$documentAST->setTypeDefinition(
94+
Parser::interfaceTypeDefinition(/** @lang GraphQL */ <<<GRAPHQL
95+
"Any object implementing this type can be found by ID through `Query.node`."
96+
interface {$nodeInterfaceName} @interface(resolveType: "{$nodeRegistryClass}@resolveType") {
97+
"Global identifier that can be used to resolve any Node implementation."
98+
{$globalIdFieldName}: ID!
99+
}
100+
GRAPHQL
101+
),
102+
);
103+
104+
$queryType = $documentAST->types[RootType::QUERY];
105+
assert($queryType instanceof ObjectTypeDefinitionNode);
106+
107+
$queryType->fields[] = Parser::fieldDefinition(/** @lang GraphQL */ <<<GRAPHQL
108+
node(id: ID! @globalId): Node @field(resolver: "{$nodeRegistryClass}@resolve")
109+
GRAPHQL
110+
);
111+
}
83112
}
84113
}

src/Pagination/PaginationManipulator.php

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,15 @@ protected function registerConnection(
8282
$connectionTypeName = "{$fieldTypeName}Connection";
8383
}
8484

85-
$connectionFieldName = addslashes(ConnectionField::class);
86-
85+
$connectionFieldClass = addslashes(ConnectionField::class);
8786
$connectionType = Parser::objectTypeDefinition(/** @lang GraphQL */ <<<GRAPHQL
8887
"A paginated list of {$fieldTypeName} edges."
8988
type {$connectionTypeName} {
9089
"Pagination information about the list of edges."
91-
{$paginationType->infoFieldName()}: PageInfo! @field(resolver: "{$connectionFieldName}@pageInfoResolver")
90+
{$paginationType->infoFieldName()}: PageInfo! @field(resolver: "{$connectionFieldClass}@pageInfoResolver")
9291
9392
"A list of {$fieldTypeName} edges."
94-
edges: [{$connectionEdgeName}!]! @field(resolver: "{$connectionFieldName}@edgeResolver")
93+
edges: [{$connectionEdgeName}!]! @field(resolver: "{$connectionFieldClass}@edgeResolver")
9594
}
9695
GRAPHQL
9796
);
@@ -133,9 +132,7 @@ protected function addPaginationWrapperType(ObjectTypeDefinitionNode $objectType
133132
$existingType = $this->documentAST->types[$typeName] ?? null;
134133
if ($existingType !== null) {
135134
if (! $existingType instanceof ObjectTypeDefinitionNode) {
136-
throw new DefinitionException(
137-
"Expected object type for pagination wrapper {$typeName}, found {$objectType->kind} instead.",
138-
);
135+
throw new DefinitionException("Expected object type for pagination wrapper {$typeName}, found {$objectType->kind} instead.");
139136
}
140137

141138
$objectType = $existingType;
@@ -145,7 +142,10 @@ protected function addPaginationWrapperType(ObjectTypeDefinitionNode $objectType
145142
isset($this->modelClass)
146143
&& ! ASTHelper::hasDirective($objectType, ModelDirective::NAME)
147144
) {
148-
$objectType->directives[] = Parser::constDirective(/** @lang GraphQL */ '@model(class: "' . addslashes($this->modelClass) . '")');
145+
$modelClassEscaped = addslashes($this->modelClass);
146+
$objectType->directives[] = Parser::constDirective(/** @lang GraphQL */ <<<GRAPHQL
147+
@model(class: "{$modelClassEscaped}")
148+
GRAPHQL);
149149
}
150150

151151
$this->documentAST->setTypeDefinition($objectType);
@@ -171,18 +171,16 @@ protected function registerPaginator(
171171
"A list of {$fieldTypeName} items."
172172
data: [{$fieldTypeName}!]! @field(resolver: "{$paginatorFieldClassName}@dataResolver")
173173
}
174-
GRAPHQL
175-
);
174+
GRAPHQL);
176175
$this->addPaginationWrapperType($paginatorType);
177176

178177
$fieldDefinition->arguments[] = Parser::inputValueDefinition(
179178
self::countArgument($defaultCount, $maxCount),
180179
);
181180
$fieldDefinition->arguments[] = Parser::inputValueDefinition(/** @lang GraphQL */ <<<'GRAPHQL'
182-
"The offset from which items are returned."
183-
page: Int
184-
GRAPHQL
185-
);
181+
"The offset from which items are returned."
182+
page: Int
183+
GRAPHQL);
186184

187185
$fieldDefinition->type = $this->paginationResultType($paginatorTypeName);
188186
$parentType->fields = ASTHelper::mergeUniqueNodeList($parentType->fields, [$fieldDefinition], true);
@@ -208,18 +206,16 @@ protected function registerSimplePaginator(
208206
"A list of {$fieldTypeName} items."
209207
data: [{$fieldTypeName}!]! @field(resolver: "{$paginatorFieldClassName}@dataResolver")
210208
}
211-
GRAPHQL
212-
);
209+
GRAPHQL);
213210
$this->addPaginationWrapperType($paginatorType);
214211

215212
$fieldDefinition->arguments[] = Parser::inputValueDefinition(
216213
self::countArgument($defaultCount, $maxCount),
217214
);
218215
$fieldDefinition->arguments[] = Parser::inputValueDefinition(/** @lang GraphQL */ <<<'GRAPHQL'
219-
"The offset from which items are returned."
220-
page: Int
221-
GRAPHQL
222-
);
216+
"The offset from which items are returned."
217+
page: Int
218+
GRAPHQL);
223219

224220
$fieldDefinition->type = $this->paginationResultType($paginatorTypeName);
225221
$parentType->fields = ASTHelper::mergeUniqueNodeList($parentType->fields, [$fieldDefinition], true);

src/Validation/ValidatorDirective.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,12 @@ protected function setFullClassnameOnDirective(Node &$definition, string $classC
120120
// @phpstan-ignore-next-line The passed in Node types all have the property $directives
121121
foreach ($definition->directives as $directive) {
122122
if ($directive->name->value === $this->name()) {
123+
$validatorClassEscaped = addslashes($validatorClass);
123124
$directive->arguments = ASTHelper::mergeUniqueNodeList(
124125
$directive->arguments,
125-
[Parser::argument('class: "' . addslashes($validatorClass) . '"')],
126+
[Parser::argument(/** @lang GraphQL */ <<<GRAPHQL
127+
class: "{$validatorClassEscaped}"
128+
GRAPHQL)],
126129
true,
127130
);
128131
}

0 commit comments

Comments
 (0)