diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b2e54b48..777313732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +### Added + +- Merge directives from Scalar/Enum/Type/Input/Interface extension node into target node https://github.com/nuwave/lighthouse/pull/2512 + ## v6.33.4 ### Fixed diff --git a/src/Schema/AST/ASTBuilder.php b/src/Schema/AST/ASTBuilder.php index 61bde930f..848068616 100644 --- a/src/Schema/AST/ASTBuilder.php +++ b/src/Schema/AST/ASTBuilder.php @@ -10,6 +10,8 @@ use GraphQL\Language\AST\InterfaceTypeExtensionNode; use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\AST\ObjectTypeExtensionNode; +use GraphQL\Language\AST\ScalarTypeDefinitionNode; +use GraphQL\Language\AST\ScalarTypeExtensionNode; use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\AST\UnionTypeExtensionNode; use GraphQL\Language\Parser; @@ -33,6 +35,7 @@ class ASTBuilder ObjectTypeExtensionNode::class => ObjectTypeDefinitionNode::class, InputObjectTypeExtensionNode::class => InputObjectTypeDefinitionNode::class, InterfaceTypeExtensionNode::class => InterfaceTypeDefinitionNode::class, + ScalarTypeExtensionNode::class => ScalarTypeDefinitionNode::class, EnumTypeExtensionNode::class => EnumTypeDefinitionNode::class, UnionTypeExtensionNode::class => UnionTypeDefinitionNode::class, ]; @@ -122,6 +125,8 @@ protected function applyTypeExtensionManipulators(): void || $typeExtension instanceof InterfaceTypeExtensionNode ) { $this->extendObjectLikeType($typeName, $typeExtension); + } elseif ($typeExtension instanceof ScalarTypeExtensionNode) { + $this->extendScalarType($typeName, $typeExtension); } elseif ($typeExtension instanceof EnumTypeExtensionNode) { $this->extendEnumType($typeName, $typeExtension); } elseif ($typeExtension instanceof UnionTypeExtensionNode) { @@ -156,6 +161,7 @@ protected function extendObjectLikeType(string $typeName, ObjectTypeExtensionNod // @phpstan-ignore-next-line $typeExtension->fields, ); + $extendedObjectLikeType->directives = $extendedObjectLikeType->directives->merge($typeExtension->directives); if ($extendedObjectLikeType instanceof ObjectTypeDefinitionNode) { assert($typeExtension instanceof ObjectTypeExtensionNode, 'We know this because we passed assertExtensionMatchesDefinition().'); @@ -166,6 +172,17 @@ protected function extendObjectLikeType(string $typeName, ObjectTypeExtensionNod } } + protected function extendScalarType(string $typeName, ScalarTypeExtensionNode $typeExtension): void + { + $extendedScalar = $this->documentAST->types[$typeName] + ?? throw new DefinitionException($this->missingBaseDefinition($typeName, $typeExtension)); + assert($extendedScalar instanceof ScalarTypeDefinitionNode); + + $this->assertExtensionMatchesDefinition($typeExtension, $extendedScalar); + + $extendedScalar->directives = $extendedScalar->directives->merge($typeExtension->directives); + } + protected function extendEnumType(string $typeName, EnumTypeExtensionNode $typeExtension): void { $extendedEnum = $this->documentAST->types[$typeName] @@ -174,6 +191,7 @@ protected function extendEnumType(string $typeName, EnumTypeExtensionNode $typeE $this->assertExtensionMatchesDefinition($typeExtension, $extendedEnum); + $extendedEnum->directives = $extendedEnum->directives->merge($typeExtension->directives); $extendedEnum->values = ASTHelper::mergeUniqueNodeList( $extendedEnum->values, $typeExtension->values, @@ -194,12 +212,15 @@ protected function extendUnionType(string $typeName, UnionTypeExtensionNode $typ ); } - protected function missingBaseDefinition(string $typeName, ObjectTypeExtensionNode|InputObjectTypeExtensionNode|InterfaceTypeExtensionNode|EnumTypeExtensionNode|UnionTypeExtensionNode $typeExtension): string + protected function missingBaseDefinition(string $typeName, ObjectTypeExtensionNode|InputObjectTypeExtensionNode|InterfaceTypeExtensionNode|ScalarTypeExtensionNode|EnumTypeExtensionNode|UnionTypeExtensionNode $typeExtension): string { return "Could not find a base definition {$typeName} of kind {$typeExtension->kind} to extend."; } - protected function assertExtensionMatchesDefinition(ObjectTypeExtensionNode|InputObjectTypeExtensionNode|InterfaceTypeExtensionNode|EnumTypeExtensionNode|UnionTypeExtensionNode $extension, ObjectTypeDefinitionNode|InputObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeDefinitionNode|UnionTypeDefinitionNode $definition): void + protected function assertExtensionMatchesDefinition( + ObjectTypeExtensionNode|InputObjectTypeExtensionNode|InterfaceTypeExtensionNode|ScalarTypeExtensionNode|EnumTypeExtensionNode|UnionTypeExtensionNode $extension, + ObjectTypeDefinitionNode|InputObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|ScalarTypeDefinitionNode|EnumTypeDefinitionNode|UnionTypeDefinitionNode $definition, + ): void { if (static::EXTENSION_TO_DEFINITION_CLASS[$extension::class] !== $definition::class) { throw new DefinitionException("The type extension {$extension->name->value} of kind {$extension->kind} can not extend a definition of kind {$definition->kind}."); diff --git a/tests/Unit/Schema/AST/ASTBuilderTest.php b/tests/Unit/Schema/AST/ASTBuilderTest.php index b897d9b33..046774bc0 100644 --- a/tests/Unit/Schema/AST/ASTBuilderTest.php +++ b/tests/Unit/Schema/AST/ASTBuilderTest.php @@ -7,11 +7,14 @@ use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\ObjectTypeDefinitionNode; +use GraphQL\Language\AST\ScalarTypeDefinitionNode; use GraphQL\Language\AST\UnionTypeDefinitionNode; use Illuminate\Support\Collection; use Nuwave\Lighthouse\Exceptions\DefinitionException; use Nuwave\Lighthouse\Schema\AST\ASTBuilder; use Nuwave\Lighthouse\Schema\AST\ASTHelper; +use Nuwave\Lighthouse\Schema\DirectiveLocator; +use Nuwave\Lighthouse\Schema\Directives\BaseDirective; use Nuwave\Lighthouse\Schema\RootType; use Tests\TestCase; @@ -49,6 +52,35 @@ public function testMergeTypeExtensionFields(): void $this->assertCount(3, $queryType->fields); } + public function testMergeTypeExtensionDirectives(): void + { + $directive = new class() extends BaseDirective { + public static function definition(): string + { + return /** @lang GraphQL */ 'directive @foo repeatable on OBJECT'; + } + }; + + $directiveLocator = $this->app->make(DirectiveLocator::class); + $directiveLocator->setResolved('foo', $directive::class); + + $this->schema = /** @lang GraphQL */ ' + type MyType { + field: String + } + + extend type MyType @foo + + extend type MyType @foo + '; + $documentAST = $this->astBuilder->documentAST(); + + $myType = $documentAST->types['MyType']; + assert($myType instanceof ObjectTypeDefinitionNode); + + $this->assertCount(2, $myType->directives); + } + public function testAllowsExtendingUndefinedRootTypes(): void { $this->schema = /** @lang GraphQL */ ' @@ -105,6 +137,35 @@ public function testMergeInputExtensionFields(): void $this->assertCount(3, $inputs->fields); } + public function testMergeInputExtensionDirectives(): void + { + $directive = new class() extends BaseDirective { + public static function definition(): string + { + return /** @lang GraphQL */ 'directive @foo repeatable on INPUT_OBJECT'; + } + }; + + $directiveLocator = $this->app->make(DirectiveLocator::class); + $directiveLocator->setResolved('foo', $directive::class); + + $this->schema = /** @lang GraphQL */ ' + input MyInput { + field: String + } + + extend input MyInput @foo + + extend input MyInput @foo + '; + $documentAST = $this->astBuilder->documentAST(); + + $myInput = $documentAST->types['MyInput']; + assert($myInput instanceof InputObjectTypeDefinitionNode); + + $this->assertCount(2, $myInput->directives); + } + public function testMergeInterfaceExtensionFields(): void { $this->schema = /** @lang GraphQL */ ' @@ -128,6 +189,62 @@ interface Named { $this->assertCount(3, $named->fields); } + public function testMergeInterfaceExtensionDirectives(): void + { + $directive = new class() extends BaseDirective { + public static function definition(): string + { + return /** @lang GraphQL */ 'directive @foo repeatable on INTERFACE'; + } + }; + + $directiveLocator = $this->app->make(DirectiveLocator::class); + $directiveLocator->setResolved('foo', $directive::class); + + $this->schema = /** @lang GraphQL */ ' + interface MyInterface { + field: String + } + + extend interface MyInterface @foo + + extend interface MyInterface @foo + '; + $documentAST = $this->astBuilder->documentAST(); + + $myInterface = $documentAST->types['MyInterface']; + assert($myInterface instanceof InterfaceTypeDefinitionNode); + + $this->assertCount(2, $myInterface->directives); + } + + public function testMergeScalarExtensionDirectives(): void + { + $directive = new class() extends BaseDirective { + public static function definition(): string + { + return /** @lang GraphQL */ 'directive @foo repeatable on SCALAR'; + } + }; + + $directiveLocator = $this->app->make(DirectiveLocator::class); + $directiveLocator->setResolved('foo', $directive::class); + + $this->schema = /** @lang GraphQL */ ' + scalar MyScalar + + extend scalar MyScalar @foo + + extend scalar MyScalar @foo + '; + $documentAST = $this->astBuilder->documentAST(); + + $myScalar = $documentAST->types['MyScalar']; + assert($myScalar instanceof ScalarTypeDefinitionNode); + + $this->assertCount(2, $myScalar->directives); + } + public function testMergeEnumExtensionFields(): void { $this->schema = /** @lang GraphQL */ ' @@ -152,6 +269,36 @@ enum MyEnum { $this->assertCount(4, $myEnum->values); } + public function testMergeEnumExtensionDirectives(): void + { + $directive = new class() extends BaseDirective { + public static function definition(): string + { + return /** @lang GraphQL */ 'directive @foo repeatable on ENUM'; + } + }; + + $directiveLocator = $this->app->make(DirectiveLocator::class); + $directiveLocator->setResolved('foo', $directive::class); + + $this->schema = /** @lang GraphQL */ ' + enum MyEnum { + ONE + TWO + } + + extend enum MyEnum @foo + + extend enum MyEnum @foo + '; + $documentAST = $this->astBuilder->documentAST(); + + $myEnum = $documentAST->types['MyEnum']; + assert($myEnum instanceof EnumTypeDefinitionNode); + + $this->assertCount(2, $myEnum->directives); + } + public function testMergeUnionExtensionFields(): void { $this->schema = /** @lang GraphQL */ ' @@ -173,6 +320,26 @@ public function testMergeUnionExtensionFields(): void $this->assertCount(3, $myUnion->types); } + public function testDoesNotAllowExtendingUndefinedScalar(): void + { + $directive = new class() extends BaseDirective { + public static function definition(): string + { + return /** @lang GraphQL */ 'directive @foo repeatable on SCALAR'; + } + }; + + $directiveLocator = $this->app->make(DirectiveLocator::class); + $directiveLocator->setResolved('foo', $directive::class); + + $this->schema = /** @lang GraphQL */ ' + extend scalar MyScalar @foo + '; + + $this->expectExceptionObject(new DefinitionException('Could not find a base definition MyScalar of kind ' . NodeKind::SCALAR_TYPE_EXTENSION . ' to extend.')); + $this->astBuilder->documentAST(); + } + public function testDoesNotAllowExtendingUndefinedTypes(): void { $this->schema = /** @lang GraphQL */ '