diff --git a/src/Language/AST/NodeKind.php b/src/Language/AST/NodeKind.php index d321cdc..7c0f300 100644 --- a/src/Language/AST/NodeKind.php +++ b/src/Language/AST/NodeKind.php @@ -69,6 +69,9 @@ class NodeKind const DIRECTIVE_DEFINITION = 'DirectiveDefinition'; + // Type System Extensions + const SCHEMA_EXTENSION = 'SchemaExtension'; + /** @var string[] */ public static $classMap = [ self::NAME => NameNode::class, diff --git a/src/Language/AST/SchemaTypeExtensionNode.php b/src/Language/AST/SchemaTypeExtensionNode.php new file mode 100644 index 0000000..e86177d --- /dev/null +++ b/src/Language/AST/SchemaTypeExtensionNode.php @@ -0,0 +1,17 @@ +kind === Token::NAME) { switch ($keywordToken->value) { + case 'schema': + return $this->parseSchemaTypeExtension(); case 'scalar': return $this->parseScalarTypeExtension(); case 'type': @@ -1464,6 +1467,33 @@ class Parser throw $this->unexpected($keywordToken); } + /** + * @return SchemaTypeExtensionNode + * @throws SyntaxError + */ + private function parseSchemaTypeExtension() + { + $start = $this->lexer->token; + $this->expectKeyword('extend'); + $this->expectKeyword('schema'); + $directives = $this->parseDirectives(true); + $operationTypes = $this->peek(Token::BRACE_L) + ? $this->many( + Token::BRACE_L, + [$this, 'parseOperationTypeDefinition'], + Token::BRACE_R + ) : []; + if (count($directives) === 0 && count($operationTypes) === 0) { + $this->unexpected(); + } + + return new SchemaTypeExtensionNode([ + 'directives' => $directives, + 'operationTypes' => $operationTypes, + 'loc' => $this->loc($start), + ]); + } + /** * @return ScalarTypeExtensionNode * @throws SyntaxError diff --git a/src/Language/Printer.php b/src/Language/Printer.php index f5110f1..f3148ed 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -41,6 +41,7 @@ use GraphQL\Language\AST\OperationTypeDefinitionNode; use GraphQL\Language\AST\ScalarTypeDefinitionNode; use GraphQL\Language\AST\ScalarTypeExtensionNode; use GraphQL\Language\AST\SchemaDefinitionNode; +use GraphQL\Language\AST\SchemaTypeExtensionNode; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Language\AST\UnionTypeDefinitionNode; @@ -335,6 +336,17 @@ class Printer ); }), + NodeKind::SCHEMA_EXTENSION => function (SchemaTypeExtensionNode $def) { + return $this->join( + [ + 'extend schema', + $this->join($def->directives, ' '), + $this->block($def->operationTypes), + ], + ' ' + ); + }, + NodeKind::SCALAR_TYPE_EXTENSION => function (ScalarTypeExtensionNode $def) { return $this->join( [ diff --git a/src/Language/Visitor.php b/src/Language/Visitor.php index 94c83e7..1a27f5a 100644 --- a/src/Language/Visitor.php +++ b/src/Language/Visitor.php @@ -165,6 +165,8 @@ class Visitor NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'], NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations'], + + NodeKind::SCHEMA_EXTENSION => ['directives', 'operationTypes'], ]; /** diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index f837db7..b572948 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -50,7 +50,7 @@ class Directive $args = []; foreach ($config['args'] as $name => $arg) { if (is_array($arg)) { - $args[] = FieldDefinition::create($arg + ['name' => $name]); + $args[] = new FieldArgument($arg + ['name' => $name]); } else { $args[] = $arg; } diff --git a/src/Type/Definition/EnumType.php b/src/Type/Definition/EnumType.php index 86e6c94..638a6ef 100644 --- a/src/Type/Definition/EnumType.php +++ b/src/Type/Definition/EnumType.php @@ -7,6 +7,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\EnumTypeDefinitionNode; +use GraphQL\Language\AST\EnumTypeExtensionNode; use GraphQL\Language\AST\EnumValueNode; use GraphQL\Language\AST\Node; use GraphQL\Utils\MixedStore; @@ -33,6 +34,9 @@ class EnumType extends Type implements InputType, OutputType, LeafType, NamedTyp /** @var \ArrayObject */ private $nameLookup; + /** @var EnumTypeExtensionNode[] */ + public $extensionASTNodes; + public function __construct($config) { if (! isset($config['name'])) { @@ -41,10 +45,11 @@ class EnumType extends Type implements InputType, OutputType, LeafType, NamedTyp Utils::invariant(is_string($config['name']), 'Must provide name.'); - $this->name = $config['name']; - $this->description = $config['description'] ?? null; - $this->astNode = $config['astNode'] ?? null; - $this->config = $config; + $this->name = $config['name']; + $this->description = $config['description'] ?? null; + $this->astNode = $config['astNode'] ?? null; + $this->extensionASTNodes = $config['extensionASTNodes'] ?? null; + $this->config = $config; } /** diff --git a/src/Type/Definition/InputObjectType.php b/src/Type/Definition/InputObjectType.php index 4b4bb52..542a0d1 100644 --- a/src/Type/Definition/InputObjectType.php +++ b/src/Type/Definition/InputObjectType.php @@ -6,6 +6,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\InputObjectTypeDefinitionNode; +use GraphQL\Language\AST\InputObjectTypeExtensionNode; use GraphQL\Utils\Utils; use function call_user_func; use function is_array; @@ -24,6 +25,9 @@ class InputObjectType extends Type implements InputType, NamedType /** @var InputObjectField[] */ private $fields; + /** @var InputObjectTypeExtensionNode[] */ + public $extensionASTNodes; + /** * * @param mixed[] $config @@ -36,10 +40,11 @@ class InputObjectType extends Type implements InputType, NamedType Utils::invariant(is_string($config['name']), 'Must provide name.'); - $this->config = $config; - $this->name = $config['name']; - $this->astNode = $config['astNode'] ?? null; - $this->description = $config['description'] ?? null; + $this->config = $config; + $this->name = $config['name']; + $this->astNode = $config['astNode'] ?? null; + $this->description = $config['description'] ?? null; + $this->extensionASTNodes = $config['extensionASTNodes'] ?? null; } /** diff --git a/src/Type/Definition/ScalarType.php b/src/Type/Definition/ScalarType.php index d93b2e7..db4e63f 100644 --- a/src/Type/Definition/ScalarType.php +++ b/src/Type/Definition/ScalarType.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace GraphQL\Type\Definition; use GraphQL\Language\AST\ScalarTypeDefinitionNode; +use GraphQL\Language\AST\ScalarTypeExtensionNode; use GraphQL\Utils\Utils; use function is_string; @@ -31,15 +32,19 @@ abstract class ScalarType extends Type implements OutputType, InputType, LeafTyp /** @var ScalarTypeDefinitionNode|null */ public $astNode; + /** @var ScalarTypeExtensionNode[] */ + public $extensionASTNodes; + /** * @param mixed[] $config */ public function __construct(array $config = []) { - $this->name = $config['name'] ?? $this->tryInferName(); - $this->description = $config['description'] ?? $this->description; - $this->astNode = $config['astNode'] ?? null; - $this->config = $config; + $this->name = $config['name'] ?? $this->tryInferName(); + $this->description = $config['description'] ?? $this->description; + $this->astNode = $config['astNode'] ?? null; + $this->extensionASTNodes = $config['extensionASTNodes'] ?? null; + $this->config = $config; Utils::invariant(is_string($this->name), 'Must provide name.'); } diff --git a/src/Type/Definition/UnionType.php b/src/Type/Definition/UnionType.php index 26ba005..ea3ec64 100644 --- a/src/Type/Definition/UnionType.php +++ b/src/Type/Definition/UnionType.php @@ -6,6 +6,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\UnionTypeDefinitionNode; +use GraphQL\Language\AST\UnionTypeExtensionNode; use GraphQL\Utils\Utils; use function call_user_func; use function is_array; @@ -27,6 +28,9 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType, /** @var ObjectType[] */ private $possibleTypeNames; + /** @var UnionTypeExtensionNode[] */ + public $extensionASTNodes; + public function __construct($config) { if (! isset($config['name'])) { @@ -40,10 +44,11 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType, * the default implemenation will call `isTypeOf` on each implementing * Object type. */ - $this->name = $config['name']; - $this->description = $config['description'] ?? null; - $this->astNode = $config['astNode'] ?? null; - $this->config = $config; + $this->name = $config['name']; + $this->description = $config['description'] ?? null; + $this->astNode = $config['astNode'] ?? null; + $this->extensionASTNodes = $config['extensionASTNodes'] ?? null; + $this->config = $config; } /** diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 4d86298..9d37c71 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -8,6 +8,7 @@ use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use GraphQL\GraphQL; use GraphQL\Language\AST\SchemaDefinitionNode; +use GraphQL\Language\AST\SchemaTypeExtensionNode; use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\InterfaceType; @@ -67,6 +68,9 @@ class Schema /** @var InvariantViolation[]|null */ private $validationErrors; + /** @var SchemaTypeExtensionNode[] */ + public $extensionASTNodes; + /** * @api * @param mixed[]|SchemaConfig $config @@ -110,7 +114,9 @@ class Schema ); } - $this->config = $config; + $this->config = $config; + $this->extensionASTNodes = $config->extensionASTNodes; + if ($config->query) { $this->resolvedTypes[$config->query->name] = $config->query; } diff --git a/src/Type/SchemaConfig.php b/src/Type/SchemaConfig.php index 3be1c98..f2e7138 100644 --- a/src/Type/SchemaConfig.php +++ b/src/Type/SchemaConfig.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace GraphQL\Type; use GraphQL\Language\AST\SchemaDefinitionNode; +use GraphQL\Language\AST\SchemaTypeExtensionNode; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; @@ -51,6 +52,9 @@ class SchemaConfig /** @var bool */ public $assumeValid; + /** @var SchemaTypeExtensionNode[] */ + public $extensionASTNodes; + /** * Converts an array of options to instance of SchemaConfig * (or just returns empty config when array is not passed). @@ -100,6 +104,10 @@ class SchemaConfig if (isset($options['assumeValid'])) { $config->setAssumeValid((bool) $options['assumeValid']); } + + if (isset($options['extensionASTNodes'])) { + $config->setExtensionASTNodes($options['extensionASTNodes']); + } } return $config; @@ -266,4 +274,20 @@ class SchemaConfig return $this; } + + /** + * @return SchemaTypeExtensionNode[] + */ + public function getExtensionASTNodes() + { + return $this->extensionASTNodes; + } + + /** + * @param SchemaTypeExtensionNode[] $extensionASTNodes + */ + public function setExtensionASTNodes(array $extensionASTNodes) + { + $this->extensionASTNodes = $extensionASTNodes; + } } diff --git a/src/Validator/Rules/KnownDirectives.php b/src/Validator/Rules/KnownDirectives.php index e9a0b8c..c0c220b 100644 --- a/src/Validator/Rules/KnownDirectives.php +++ b/src/Validator/Rules/KnownDirectives.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace GraphQL\Validator\Rules; use GraphQL\Error\Error; +use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DirectiveNode; use GraphQL\Language\AST\InputObjectTypeDefinitionNode; use GraphQL\Language\AST\Node; @@ -12,6 +13,7 @@ use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeList; use GraphQL\Language\DirectiveLocation; use GraphQL\Validator\ValidationContext; +use function array_map; use function count; use function in_array; use function sprintf; @@ -20,37 +22,49 @@ class KnownDirectives extends ValidationRule { public function getVisitor(ValidationContext $context) { - return [ - NodeKind::DIRECTIVE => function (DirectiveNode $node, $key, $parent, $path, $ancestors) use ($context) { - $directiveDef = null; - foreach ($context->getSchema()->getDirectives() as $def) { - if ($def->name === $node->name->value) { - $directiveDef = $def; - break; - } - } + $locationsMap = []; + $schema = $context->getSchema(); + $definedDirectives = $schema->getDirectives(); - if (! $directiveDef) { + foreach ($definedDirectives as $directive) { + $locationsMap[$directive->name] = $directive->locations; + } + + $astDefinition = $context->getDocument()->definitions; + + foreach ($astDefinition as $def) { + if (! ($def instanceof DirectiveDefinitionNode)) { + continue; + } + + $locationsMap[$def->name->value] = array_map(function ($name) { + return $name->value; + }, $def->locations); + } + return [ + NodeKind::DIRECTIVE => function (DirectiveNode $node, $key, $parent, $path, $ancestors) use ($context, $locationsMap) { + $name = $node->name->value; + $locations = $locationsMap[$name] ?? null; + + if (! $locations) { $context->reportError(new Error( - self::unknownDirectiveMessage($node->name->value), + self::unknownDirectiveMessage($name), [$node] )); - return; } + $candidateLocation = $this->getDirectiveLocationForASTPath($ancestors); - if (! $candidateLocation) { - $context->reportError(new Error( - self::misplacedDirectiveMessage($node->name->value, $node->type), - [$node] - )); - } elseif (! in_array($candidateLocation, $directiveDef->locations)) { - $context->reportError(new Error( - self::misplacedDirectiveMessage($node->name->value, $candidateLocation), - [$node] - )); + if (! $candidateLocation || in_array($candidateLocation, $locations)) { + return; } + $context->reportError( + new Error( + self::misplacedDirectiveMessage($name, $candidateLocation), + [$node] + ) + ); }, ]; } @@ -88,6 +102,7 @@ class KnownDirectives extends ValidationRule case NodeKind::FRAGMENT_DEFINITION: return DirectiveLocation::FRAGMENT_DEFINITION; case NodeKind::SCHEMA_DEFINITION: + case NodeKind::SCHEMA_EXTENSION: return DirectiveLocation::SCHEMA; case NodeKind::SCALAR_TYPE_DEFINITION: case NodeKind::SCALAR_TYPE_EXTENSION: