Skip to content

Commit

Permalink
Merge directives from Scalar/Enum/Type/Input/Interface extension node…
Browse files Browse the repository at this point in the history
… into target node
  • Loading branch information
LastDragon-ru authored Mar 1, 2024
1 parent a66df10 commit 9e46ce6
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 2 deletions.
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

0 comments on commit 9e46ce6

Please sign in to comment.