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

fix federation directive definition #23

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions src/Utils/FederationV22SchemaExtender.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
166 changes: 166 additions & 0 deletions src/Utils/FederationV23SchemaExtender.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

declare(strict_types=1);

namespace Axtiva\FlexibleGraphql\Utils;

use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Language\AST\SchemaExtensionNode;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Language\Parser;
use GraphQL\Type\Schema;
use GraphQL\Utils\SchemaExtender;

class FederationV23SchemaExtender extends FederationV1SchemaExtender
{
private const SHAREABLE = 'directive @%s on OBJECT | FIELD_DEFINITION';
private const INACCESSIBLE = 'directive @%s on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION';
private const OVERRIDE = 'directive @%s(from: String!) on FIELD_DEFINITION';
private const TAG = 'directive @%s(name: String!) repeatable on | FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION';
private const EXTERNAL = 'directive @%s on FIELD_DEFINITION';
private const REQUIRES = 'directive @%s(fields: FieldSet!) on FIELD_DEFINITION';
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 INTERFACE_OBJECT = 'directive @%s on OBJECT | INTERFACE';

private const DIRECTIVE_MAP = [
'tag' => 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',
<<<GQL
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
GQL
);

foreach (static::DIRECTIVE_MAP as $directiveName => $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, '@');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

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