Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Directives from Scalar/Enum/Input/Type/Interface extensions #2512

Merged
merged 6 commits into from
Mar 1, 2024
Merged
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

- Merge directives from Scalar/Enum/Type/Input/Interface extension node into target node https://github.com/nuwave/lighthouse/pull/2512

## v6.33.4

### Fixed
Expand Down
25 changes: 23 additions & 2 deletions src/Schema/AST/ASTBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
];
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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().');
Expand All @@ -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]
Expand All @@ -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,
Expand All @@ -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}.");
Expand Down
167 changes: 167 additions & 0 deletions tests/Unit/Schema/AST/ASTBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 */ '
Expand Down Expand Up @@ -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 */ '
Expand All @@ -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 */ '
Expand All @@ -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 */ '
Expand All @@ -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 */ '
Expand Down
Loading