From 624c7daedc208aa7f553c3adc2d7c36f748f0db7 Mon Sep 17 00:00:00 2001 From: bpteam Date: Thu, 1 Jun 2023 00:36:44 +0300 Subject: [PATCH 1/2] fix federation directive definition --- src/Utils/FederationV23SchemaExtender.php | 166 ++++++++++++++ ...ationV23SchemaExtenderCommonSchemaTest.php | 208 ++++++++++++++++++ ...23SchemaExtenderCustomImportSchemaTest.php | 169 ++++++++++++++ 3 files changed, 543 insertions(+) create mode 100644 src/Utils/FederationV23SchemaExtender.php create mode 100644 tests/Utils/FederationSchemaExtender/FederationV23SchemaExtenderCommonSchemaTest.php create mode 100644 tests/Utils/FederationSchemaExtender/FederationV23SchemaExtenderCustomImportSchemaTest.php diff --git a/src/Utils/FederationV23SchemaExtender.php b/src/Utils/FederationV23SchemaExtender.php new file mode 100644 index 0000000..e310735 --- /dev/null +++ b/src/Utils/FederationV23SchemaExtender.php @@ -0,0 +1,166 @@ + self::TAG, + 'shareable' => self::SHAREABLE, + 'inaccessible' => self::INACCESSIBLE, + 'override' => self::OVERRIDE, + 'external' => self::EXTERNAL, + 'requires' => self::REQUIRES, + 'provides' => self::PROVIDES, + 'key' => self::KEY, + 'extends' => self::EXTENDS, + 'composeDirective' => self::COMPOSE_DIRECTIVE, + 'interfaceObject' => self::INTERFACE_OBJECT, + ]; + + public static function build(Schema $schema, DocumentNode $ast): Schema + { + $schema = parent::build($schema, $ast); + + $schema = self::addDirectiveIfNotExists( + $schema, + 'link', + << $directive) { + $schema = self::addDirectiveIfNotExists( + $schema, + $directiveName, + sprintf($directive, $directiveName) + ); + } + $alias = 'federation'; + foreach (static::DIRECTIVE_MAP as $directiveName => $directive) { + $schema = self::addDirectiveIfNotExists( + $schema, + $alias . '__' . $directiveName, + sprintf($directive, $alias . '__' . $directiveName) + ); + } + + $hasLinkExtension = false; + /** @var SchemaExtensionNode|Node $node */ + foreach ($ast->definitions as $node) { + if ($node->kind === 'SchemaExtension') { + /** @var SchemaExtensionNode $node */ + /** @var DirectiveNode $directive */ + foreach ($node->directives->getIterator() as $directive) { + if ($directive->name->value === 'link') { + $hasLinkExtension = true; + /** @var ArgumentNode $argument */ + foreach ($directive->arguments->getIterator() as $argument) { + if ($argument->name->value === 'as') { + $schema = self::addFederationDirectivesWithAliases( + $schema, + self::trimDirectivePrefix($argument->value->value), + ); + } elseif ($argument->name->value === 'import') { + /** @var ObjectValueNode|StringValueNode $value */ + foreach ($argument->value->values->getIterator() as $value) { + if ($value instanceof StringValueNode) { + $directiveName = self::trimDirectivePrefix($value->value); + if (empty(static::DIRECTIVE_MAP[$directiveName])) { + continue; + } + $schema = self::addDirectiveIfNotExists( + $schema, + $directiveName, + sprintf(static::DIRECTIVE_MAP[$directiveName], $directiveName) + ); + } elseif ($value instanceof ObjectValueNode) { + $name = null; + $as = null; + /** @var ArgumentNode $argument */ + foreach ($value->fields->getIterator() as $argument) { + if ($argument->name->value === 'name') { + $name = self::trimDirectivePrefix($argument->value->value); + } + if ($argument->name->value === 'as') { + $as = self::trimDirectivePrefix($argument->value->value); + } + } + if (empty(static::DIRECTIVE_MAP[$name])) { + continue; + } + if ($name !== null && $as !== null) { + $schema = self::addDirectiveIfNotExists( + $schema, + $as, + sprintf(static::DIRECTIVE_MAP[$name], $as) + ); + } elseif ($name !== null) { + $schema = self::addDirectiveIfNotExists( + $schema, + $name, + sprintf(static::DIRECTIVE_MAP[$name], $name) + ); + } + } + } + } + } + } + } + } + } + + if (!$hasLinkExtension) { + $documentAST = Parser::parse( + 'extend schema @link(url: "https://specs.apollo.dev/federation/v2.3")' + ); + $schema = SchemaExtender::extend($schema, $documentAST); + } + + return $schema; + } + + private static function trimDirectivePrefix(string $value): string + { + return ltrim($value, '@'); + } +} \ No newline at end of file diff --git a/tests/Utils/FederationSchemaExtender/FederationV23SchemaExtenderCommonSchemaTest.php b/tests/Utils/FederationSchemaExtender/FederationV23SchemaExtenderCommonSchemaTest.php new file mode 100644 index 0000000..d565339 --- /dev/null +++ b/tests/Utils/FederationSchemaExtender/FederationV23SchemaExtenderCommonSchemaTest.php @@ -0,0 +1,208 @@ +assertFalse($schemaExtended->getQueryType()->hasField('_entities'), '_entity found'); + $this->assertTrue($schemaExtended->getQueryType()->hasField('_service'), '_service found'); + $this->assertNotNull($schemaExtended->getDirective('external'), 'external not found'); + $this->assertNotNull($schemaExtended->getDirective('requires'), 'requires not found'); + $this->assertNotNull($schemaExtended->getDirective('provides'), 'provides not found'); + $this->assertNotNull($schemaExtended->getDirective('extends'), 'extends not found'); + $this->assertNotNull($schemaExtended->getDirective('link'), 'link not found'); + $this->assertNotNull($schemaExtended->getDirective('shareable'), 'shareable not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__shareable'), 'federation__shareable not found'); + $this->assertNotNull($schemaExtended->getDirective('inaccessible'), 'inaccessible not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__inaccessible'), 'federation__inaccessible not found'); + $this->assertNotNull($schemaExtended->getDirective('tag'), 'tag not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__tag'), 'federation__tag not found'); + $this->assertNotNull($schemaExtended->getDirective('override'), 'override not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__override'), 'federation__override not found'); + $this->assertNotNull($schemaExtended->getDirective('composeDirective'), 'composeDirective not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__composeDirective'), 'federation__composeDirective not found'); + $this->assertNotNull($schemaExtended->getDirective('interfaceObject'), 'interfaceObject not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__interfaceObject'), 'federation__interfaceObject not found'); + } + + /** + * @param string $sdl + * @return void + * @dataProvider dataProviderNotFederatedSchema + * @throws SyntaxError + */ + public function testNotExtendSchemaWithoutKeyDirective_Entity(string $sdl) + { + $ast = Parser::parse($sdl); + $schema = BuildSchema::build($ast); + $schemaExtended = FederationV23SchemaExtender::build($schema, $ast); + + $this->assertFalse($schemaExtended->hasType('_Entity'), '_Entity found'); + } + + /** + * @param string $sdl + * @return void + * @dataProvider dataProviderNotFederatedSchema + * @throws SyntaxError + */ + public function testNotExtendSchemaWithoutKeyDirective_Any(string $sdl) + { + $ast = Parser::parse($sdl); + $schema = BuildSchema::build($ast); + $schemaExtended = FederationV23SchemaExtender::build($schema, $ast); + + $this->assertFalse($schemaExtended->hasType('_Any'), '_Any found'); + } + + /** + * @param string $sdl + * @return void + * @dataProvider dataProviderNotFederatedSchema + * @throws SyntaxError + */ + public function testNotExtendSchemaWithoutKeyDirective_Service(string $sdl) + { + $ast = Parser::parse($sdl); + $schema = BuildSchema::build($ast); + $schemaExtended = FederationV23SchemaExtender::build($schema, $ast); + + $this->assertTrue($schemaExtended->hasType('_Service'), '_Service not found'); + $this->assertTrue($schemaExtended->hasType('Query'), 'Query not found'); + /** @var ObjectType $query */ + $query = $schemaExtended->getType('Query'); + $this->assertTrue((bool) $query->getField('_service'), '_service not found'); + } + + public function dataProviderNotFederatedSchema(): iterable + { + yield [<<<'SDL' +scalar _FieldSet +directive @external on OBJECT | FIELD_DEFINITION +directive @requires(fields: _FieldSet!) on FIELD_DEFINITION +directive @provides(fields: _FieldSet!) on FIELD_DEFINITION +directive @key(fields: _FieldSet!) on OBJECT | INTERFACE +directive @extends on OBJECT | INTERFACE +directive @isAuthenticated on FIELD | FIELD_DEFINITION +directive @hasRole(role: String) on FIELD | FIELD_DEFINITION +directive @pow(ex: Int!) on FIELD | FIELD_DEFINITION +directive @uppercase on FIELD | FIELD_DEFINITION + +type Query { + hero: Character +} + +type Character { + name: String + friends: [Character] + homeWorld: Planet + species: Species +} + +type Planet { + name: String + climate: String +} + +type Species { + name: String + lifespan: Int + origin: Planet +} +SDL]; + yield [<<<'SDL' +scalar _FieldSet +directive @isAuthenticated on FIELD | FIELD_DEFINITION +directive @hasRole(role: String) on FIELD | FIELD_DEFINITION +directive @pow(ex: Int!) on FIELD | FIELD_DEFINITION +directive @uppercase on FIELD | FIELD_DEFINITION + +type Query @extends { + hero: Character +} + +type Character { + name: String + friends: [Character] + homeWorld: Planet @external + species: Species @requires(fields: "homeWorld") +} + +type Planet @key(fields: "name") { + name: String + climate: String +} + +type Species { + name: String + lifespan: Int + origin: Planet +} +SDL]; + yield [<<<'SDL' +extend schema +@link( + url: "https://specs.apollo.dev/federation/v2.3", + import: [ + "@composeDirective", + "@extends", + "@external", + "@inaccessible", + "@interfaceObject", + "@key", + "@override", + "@provides", + "@requires", + "@shareable", + "@tag" + ] +) +directive @isAuthenticated on FIELD | FIELD_DEFINITION +directive @hasRole(role: String) on FIELD | FIELD_DEFINITION +directive @pow(ex: Int!) on FIELD | FIELD_DEFINITION +directive @uppercase on FIELD | FIELD_DEFINITION + +type Query @extends { + hero: Character +} + +type Character { + name: String + friends: [Character] + homeWorld: Planet @external + species: Species @requires(fields: "homeWorld") +} + +type Planet @key(fields: "name") { + name: String + climate: String +} + +type Species { + name: String + lifespan: Int + origin: Planet +} +SDL]; + + } +} \ No newline at end of file diff --git a/tests/Utils/FederationSchemaExtender/FederationV23SchemaExtenderCustomImportSchemaTest.php b/tests/Utils/FederationSchemaExtender/FederationV23SchemaExtenderCustomImportSchemaTest.php new file mode 100644 index 0000000..66cfc57 --- /dev/null +++ b/tests/Utils/FederationSchemaExtender/FederationV23SchemaExtenderCustomImportSchemaTest.php @@ -0,0 +1,169 @@ +assertFalse($schemaExtended->getQueryType()->hasField('_entities'), '_entity found'); + $this->assertTrue($schemaExtended->getQueryType()->hasField('_service'), '_service found'); + $this->assertNotNull($schemaExtended->getDirective('external'), 'external not found'); + $this->assertNotNull($schemaExtended->getDirective('requires'), 'requires not found'); + $this->assertNotNull($schemaExtended->getDirective('provides'), 'provides not found'); + $this->assertNotNull($schemaExtended->getDirective('extends'), 'extends not found'); + $this->assertNotNull($schemaExtended->getDirective('link'), 'link not found'); + $this->assertNotNull($schemaExtended->getDirective('shareable'), 'shareable not found'); + $this->assertNotNull($schemaExtended->getDirective('omg__shareable'), 'omg__shareable not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__shareable'), 'federation__shareable not found'); + $this->assertNotNull($schemaExtended->getDirective('inaccessible'), 'inaccessible not found'); + $this->assertNotNull($schemaExtended->getDirective('omg__inaccessible'), 'omg__inaccessible not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__inaccessible'), 'federation__inaccessible not found'); + $this->assertNotNull($schemaExtended->getDirective('tag'), 'tag not found'); + $this->assertNotNull($schemaExtended->getDirective('omg__tag'), 'omg__tag not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__tag'), 'federation__tag not found'); + $this->assertNotNull($schemaExtended->getDirective('override'), 'override not found'); + $this->assertNotNull($schemaExtended->getDirective('omg__override'), 'omg__override not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__override'), 'federation__override not found'); + } + + /** + * @return void + * @throws SyntaxError + */ + public function testForAliasImport() + { + $sdl = <<<'SDL' +scalar _FieldSet +directive @link( + url: String, + as: String, + for: link__Purpose, + import: [link__Import] +) repeatable on SCHEMA +enum link__Purpose { + "`SECURITY` features provide metadata necessary to securely resolve fields." + SECURITY + "`EXECUTION` features provide metadata necessary for operation execution." + EXECUTION +} +scalar link__Import +directive @isAuthenticated on FIELD | FIELD_DEFINITION +directive @hasRole(role: String) on FIELD | FIELD_DEFINITION +directive @pow(ex: Int!) on FIELD | FIELD_DEFINITION +directive @uppercase on FIELD | FIELD_DEFINITION + +extend schema @link( +url: "http://localhost:8080/graphql", +import: [ + { name: "@shareable", as: "@omgs" }, + { name: "@inaccessible", as: "@iomg" } + ] +) + +type Query { + hero: Character +} + +type Character { + name: String + friends: [Character] + homeWorld: Planet + species: Species +} + +type Planet { + name: String + climate: String +} + +type Species { + name: String + lifespan: Int + origin: Planet +} +SDL; + + $ast = Parser::parse($sdl); + $schema = BuildSchema::build($ast); + $schemaExtended = FederationV23SchemaExtender::build($schema, $ast); + + $this->assertFalse($schemaExtended->getQueryType()->hasField('_entities'), '_entity found'); + $this->assertTrue($schemaExtended->getQueryType()->hasField('_service'), '_service found'); + $this->assertNotNull($schemaExtended->getDirective('external'), 'external not found'); + $this->assertNotNull($schemaExtended->getDirective('requires'), 'requires not found'); + $this->assertNotNull($schemaExtended->getDirective('provides'), 'provides not found'); + $this->assertNotNull($schemaExtended->getDirective('extends'), 'extends not found'); + $this->assertNotNull($schemaExtended->getDirective('link'), 'link not found'); + $this->assertNotNull($schemaExtended->getDirective('shareable'), 'shareable not found'); + $this->assertNotNull($schemaExtended->getDirective('omgs'), 'omgs not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__shareable'), 'federation__shareable not found'); + $this->assertNotNull($schemaExtended->getDirective('inaccessible'), 'inaccessible not found'); + $this->assertNotNull($schemaExtended->getDirective('iomg'), 'iomg not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__inaccessible'), 'federation__inaccessible not found'); + $this->assertNotNull($schemaExtended->getDirective('tag'), 'tag not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__tag'), 'federation__tag not found'); + $this->assertNotNull($schemaExtended->getDirective('override'), 'override not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__override'), 'federation__override not found'); + } + +} \ No newline at end of file From 5f2d1aabda0f00295e78455c96cf513719728b60 Mon Sep 17 00:00:00 2001 From: bpteam Date: Thu, 1 Jun 2023 00:47:43 +0300 Subject: [PATCH 2/2] fix federation directive definition --- src/Utils/FederationV22SchemaExtender.php | 2 + ...ationV22SchemaExtenderCommonSchemaTest.php | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/Utils/FederationV22SchemaExtender.php b/src/Utils/FederationV22SchemaExtender.php index 5e8ca17..d1a07bf 100644 --- a/src/Utils/FederationV22SchemaExtender.php +++ b/src/Utils/FederationV22SchemaExtender.php @@ -26,6 +26,7 @@ class FederationV22SchemaExtender extends FederationV1SchemaExtender private const PROVIDES = 'directive @%s(fields: FieldSet!) on FIELD_DEFINITION'; private const KEY = 'directive @%s(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE'; private const EXTENDS = 'directive @%s on OBJECT | INTERFACE'; + private const COMPOSE_DIRECTIVE = 'directive @%s(name: String!) repeatable on SCHEMA'; private const DIRECTIVE_MAP = [ 'tag' => self::TAG, @@ -37,6 +38,7 @@ class FederationV22SchemaExtender extends FederationV1SchemaExtender 'provides' => self::PROVIDES, 'key' => self::KEY, 'extends' => self::EXTENDS, + 'composeDirective' => self::COMPOSE_DIRECTIVE, ]; public static function build(Schema $schema, DocumentNode $ast): Schema diff --git a/tests/Utils/FederationSchemaExtender/FederationV22SchemaExtenderCommonSchemaTest.php b/tests/Utils/FederationSchemaExtender/FederationV22SchemaExtenderCommonSchemaTest.php index 964e31b..17948a8 100644 --- a/tests/Utils/FederationSchemaExtender/FederationV22SchemaExtenderCommonSchemaTest.php +++ b/tests/Utils/FederationSchemaExtender/FederationV22SchemaExtenderCommonSchemaTest.php @@ -39,6 +39,8 @@ public function testNotExtendSchemaWithoutKeyDirectiveQuery(string $sdl) $this->assertNotNull($schemaExtended->getDirective('federation__tag'), 'federation__tag not found'); $this->assertNotNull($schemaExtended->getDirective('override'), 'override not found'); $this->assertNotNull($schemaExtended->getDirective('federation__override'), 'federation__override not found'); + $this->assertNotNull($schemaExtended->getDirective('composeDirective'), 'composeDirective not found'); + $this->assertNotNull($schemaExtended->getDirective('federation__composeDirective'), 'federation__composeDirective not found'); } /** @@ -120,6 +122,50 @@ public function dataProviderNotFederatedSchema(): iterable climate: String } +type Species { + name: String + lifespan: Int + origin: Planet +} +SDL]; + yield [<<<'SDL' +extend schema +@link( + url: "https://specs.apollo.dev/federation/v2.2", + import: [ + "@composeDirective", + "@extends", + "@external", + "@inaccessible", + "@key", + "@override", + "@provides", + "@requires", + "@shareable", + "@tag" + ] +) +directive @isAuthenticated on FIELD | FIELD_DEFINITION +directive @hasRole(role: String) on FIELD | FIELD_DEFINITION +directive @pow(ex: Int!) on FIELD | FIELD_DEFINITION +directive @uppercase on FIELD | FIELD_DEFINITION + +type Query @extends { + hero: Character +} + +type Character { + name: String + friends: [Character] + homeWorld: Planet @external + species: Species @requires(fields: "homeWorld") +} + +type Planet @key(fields: "name") { + name: String + climate: String +} + type Species { name: String lifespan: Int