FEATURE: SchemaExtender (#362)

FEATURE: SchemaExtender (#362)
This commit is contained in:
Torsten Blindert 2018-10-08 15:55:20 +02:00 committed by Vladimir Razuvaev
parent 7ff3e9399f
commit e7de069bd5
9 changed files with 2884 additions and 2 deletions

View File

@ -9,6 +9,6 @@ class DocumentNode extends Node
/** @var string */ /** @var string */
public $kind = NodeKind::DOCUMENT; public $kind = NodeKind::DOCUMENT;
/** @var DefinitionNode[] */ /** @var NodeList|DefinitionNode[] */
public $definitions; public $definitions;
} }

View File

@ -7,6 +7,7 @@ namespace GraphQL\Type\Definition;
use Exception; use Exception;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\TypeDefinitionNode; use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\AST\TypeExtensionNode;
use GraphQL\Type\Introspection; use GraphQL\Type\Introspection;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use JsonSerializable; use JsonSerializable;
@ -47,6 +48,9 @@ abstract class Type implements JsonSerializable
/** @var mixed[] */ /** @var mixed[] */
public $config; public $config;
/** @var TypeExtensionNode[] */
public $extensionASTNodes;
/** /**
* @return IDType * @return IDType
* *

View File

@ -12,6 +12,7 @@ use GraphQL\Language\AST\EnumTypeExtensionNode;
use GraphQL\Language\AST\EnumValueDefinitionNode; use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode; use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode; use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\ListTypeNode; use GraphQL\Language\AST\ListTypeNode;
use GraphQL\Language\AST\NamedTypeNode; use GraphQL\Language\AST\NamedTypeNode;
@ -492,4 +493,37 @@ class ASTDefinitionBuilder
return $innerType; return $innerType;
} }
/**
* @return mixed[]
*/
public function buildInputField(InputValueDefinitionNode $value) : array
{
$type = $this->internalBuildWrappedType($value->type);
$config = [
'name' => $value->name->value,
'type' => $type,
'description' => $this->getDescription($value),
'astNode' => $value,
];
if ($value->defaultValue) {
$config['defaultValue'] = $value->defaultValue;
}
return $config;
}
/**
* @return mixed[]
*/
public function buildEnumValue(EnumValueDefinitionNode $value) : array
{
return [
'description' => $this->getDescription($value),
'deprecationReason' => $this->getDeprecationReason($value),
'astNode' => $value,
];
}
} }

View File

@ -0,0 +1,622 @@
<?php
declare(strict_types=1);
namespace GraphQL\Utils;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\AST\SchemaTypeExtensionNode;
use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\AST\TypeExtensionNode;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\EnumValueDefinition;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Type\Schema;
use GraphQL\Validator\DocumentValidator;
use function array_keys;
use function array_map;
use function array_merge;
use function array_values;
use function count;
class SchemaExtender
{
const SCHEMA_EXTENSION = 'SchemaExtension';
/** @var Type[] */
protected static $extendTypeCache;
/** @var mixed[] */
protected static $typeExtensionsMap;
/** @var ASTDefinitionBuilder */
protected static $astBuilder;
/**
* @return TypeExtensionNode[]|null
*/
protected static function getExtensionASTNodes(NamedType $type) : ?array
{
if (! $type instanceof Type) {
return null;
}
$name = $type->name;
if ($type->extensionASTNodes !== null) {
if (isset(static::$typeExtensionsMap[$name])) {
return array_merge($type->extensionASTNodes, static::$typeExtensionsMap[$name]);
}
return $type->extensionASTNodes;
}
return static::$typeExtensionsMap[$name] ?? null;
}
/**
* @throws Error
*/
protected static function checkExtensionNode(Type $type, Node $node) : void
{
switch ($node->kind ?? null) {
case NodeKind::OBJECT_TYPE_EXTENSION:
if (! ($type instanceof ObjectType)) {
throw new Error(
'Cannot extend non-object type "' . $type->name . '".',
[$node]
);
}
break;
case NodeKind::INTERFACE_TYPE_EXTENSION:
if (! ($type instanceof InterfaceType)) {
throw new Error(
'Cannot extend non-interface type "' . $type->name . '".',
[$node]
);
}
break;
case NodeKind::ENUM_TYPE_EXTENSION:
if (! ($type instanceof EnumType)) {
throw new Error(
'Cannot extend non-enum type "' . $type->name . '".',
[$node]
);
}
break;
case NodeKind::UNION_TYPE_EXTENSION:
if (! ($type instanceof UnionType)) {
throw new Error(
'Cannot extend non-union type "' . $type->name . '".',
[$node]
);
}
break;
case NodeKind::INPUT_OBJECT_TYPE_EXTENSION:
if (! ($type instanceof InputObjectType)) {
throw new Error(
'Cannot extend non-input object type "' . $type->name . '".',
[$node]
);
}
break;
}
}
protected static function extendCustomScalarType(CustomScalarType $type) : CustomScalarType
{
return new CustomScalarType([
'name' => $type->name,
'description' => $type->description,
'astNode' => $type->astNode,
'serialize' => $type->config['serialize'] ?? null,
'parseValue' => $type->config['parseValue'] ?? null,
'parseLiteral' => $type->config['parseLiteral'] ?? null,
'extensionASTNodes' => static::getExtensionASTNodes($type),
]);
}
protected static function extendUnionType(UnionType $type) : UnionType
{
return new UnionType([
'name' => $type->name,
'description' => $type->description,
'types' => static function () use ($type) {
return static::extendPossibleTypes($type);
},
'astNode' => $type->astNode,
'resolveType' => $type->config['resolveType'] ?? null,
'extensionASTNodes' => static::getExtensionASTNodes($type),
]);
}
protected static function extendEnumType(EnumType $type) : EnumType
{
return new EnumType([
'name' => $type->name,
'description' => $type->description,
'values' => static::extendValueMap($type),
'astNode' => $type->astNode,
'extensionASTNodes' => static::getExtensionASTNodes($type),
]);
}
protected static function extendInputObjectType(InputObjectType $type) : InputObjectType
{
return new InputObjectType([
'name' => $type->name,
'description' => $type->description,
'fields' => static function () use ($type) {
return static::extendInputFieldMap($type);
},
'astNode' => $type->astNode,
'extensionASTNodes' => static::getExtensionASTNodes($type),
]);
}
/**
* @return mixed[]
*/
protected static function extendInputFieldMap(InputObjectType $type) : array
{
$newFieldMap = [];
$oldFieldMap = $type->getFields();
foreach ($oldFieldMap as $fieldName => $field) {
$newFieldMap[$fieldName] = [
'description' => $field->description,
'type' => static::extendType($field->type),
'astNode' => $field->astNode,
];
if (! $field->defaultValueExists()) {
continue;
}
$newFieldMap[$fieldName]['defaultValue'] = $field->defaultValue;
}
$extensions = static::$typeExtensionsMap[$type->name] ?? null;
if ($extensions !== null) {
foreach ($extensions as $extension) {
foreach ($extension->fields as $field) {
$fieldName = $field->name->value;
if (isset($oldFieldMap[$fieldName])) {
throw new Error('Field "' . $type->name . '.' . $fieldName . '" already exists in the schema. It cannot also be defined in this type extension.', [$field]);
}
$newFieldMap[$fieldName] = static::$astBuilder->buildInputField($field);
}
}
}
return $newFieldMap;
}
/**
* @return mixed[]
*/
protected static function extendValueMap(EnumType $type) : array
{
$newValueMap = [];
/** @var EnumValueDefinition[] $oldValueMap */
$oldValueMap = [];
foreach ($type->getValues() as $value) {
$oldValueMap[$value->name] = $value;
}
foreach ($oldValueMap as $key => $value) {
$newValueMap[$key] = [
'name' => $value->name,
'description' => $value->description,
'value' => $value->value,
'deprecationReason' => $value->deprecationReason,
'astNode' => $value->astNode,
];
}
$extensions = static::$typeExtensionsMap[$type->name] ?? null;
if ($extensions !== null) {
foreach ($extensions as $extension) {
foreach ($extension->values as $value) {
$valueName = $value->name->value;
if (isset($oldValueMap[$valueName])) {
throw new Error('Enum value "' . $type->name . '.' . $valueName . '" already exists in the schema. It cannot also be defined in this type extension.', [$value]);
}
$newValueMap[$valueName] = static::$astBuilder->buildEnumValue($value);
}
}
}
return $newValueMap;
}
/**
* @return ObjectType[]
*/
protected static function extendPossibleTypes(UnionType $type) : array
{
$possibleTypes = array_map(static function ($type) {
return static::extendNamedType($type);
}, $type->getTypes());
$extensions = static::$typeExtensionsMap[$type->name] ?? null;
if ($extensions !== null) {
foreach ($extensions as $extension) {
foreach ($extension->types as $namedType) {
$possibleTypes[] = static::$astBuilder->buildType($namedType);
}
}
}
return $possibleTypes;
}
/**
* @return InterfaceType[]
*/
protected static function extendImplementedInterfaces(ObjectType $type) : array
{
$interfaces = array_map(static function (InterfaceType $interfaceType) {
return static::extendNamedType($interfaceType);
}, $type->getInterfaces());
$extensions = static::$typeExtensionsMap[$type->name] ?? null;
if ($extensions !== null) {
/** @var ObjectTypeExtensionNode $extension */
foreach ($extensions as $extension) {
foreach ($extension->interfaces as $namedType) {
$interfaces[] = static::$astBuilder->buildType($namedType);
}
}
}
return $interfaces;
}
protected static function extendType($typeDef)
{
if ($typeDef instanceof ListOfType) {
return Type::listOf(static::extendType($typeDef->ofType));
}
if ($typeDef instanceof NonNull) {
return Type::nonNull(static::extendType($typeDef->getWrappedType()));
}
return static::extendNamedType($typeDef);
}
/**
* @param FieldArgument[] $args
*
* @return mixed[]
*/
protected static function extendArgs(array $args) : array
{
return Utils::keyValMap(
$args,
static function (FieldArgument $arg) {
return $arg->name;
},
static function (FieldArgument $arg) {
$def = [
'type' => static::extendType($arg->getType()),
'description' => $arg->description,
'astNode' => $arg->astNode,
];
if ($arg->defaultValueExists()) {
$def['defaultValue'] = $arg->defaultValue;
}
return $def;
}
);
}
/**
* @param InterfaceType|ObjectType $type
*
* @return mixed[]
*
* @throws Error
*/
protected static function extendFieldMap($type) : array
{
$newFieldMap = [];
$oldFieldMap = $type->getFields();
foreach (array_keys($oldFieldMap) as $fieldName) {
$field = $oldFieldMap[$fieldName];
$newFieldMap[$fieldName] = [
'name' => $fieldName,
'description' => $field->description,
'deprecationReason' => $field->deprecationReason,
'type' => static::extendType($field->getType()),
'args' => static::extendArgs($field->args),
'astNode' => $field->astNode,
'resolveFn' => $field->resolveFn,
];
}
$extensions = static::$typeExtensionsMap[$type->name] ?? null;
if ($extensions !== null) {
foreach ($extensions as $extension) {
foreach ($extension->fields as $field) {
$fieldName = $field->name->value;
if (isset($oldFieldMap[$fieldName])) {
throw new Error('Field "' . $type->name . '.' . $fieldName . '" already exists in the schema. It cannot also be defined in this type extension.', [$field]);
}
$newFieldMap[$fieldName] = static::$astBuilder->buildField($field);
}
}
}
return $newFieldMap;
}
protected static function extendObjectType(ObjectType $type) : ObjectType
{
return new ObjectType([
'name' => $type->name,
'description' => $type->description,
'interfaces' => static function () use ($type) {
return static::extendImplementedInterfaces($type);
},
'fields' => static function () use ($type) {
return static::extendFieldMap($type);
},
'astNode' => $type->astNode,
'extensionASTNodes' => static::getExtensionASTNodes($type),
'isTypeOf' => $type->config['isTypeOf'] ?? null,
]);
}
protected static function extendInterfaceType(InterfaceType $type) : InterfaceType
{
return new InterfaceType([
'name' => $type->name,
'description' => $type->description,
'fields' => static function () use ($type) {
return static::extendFieldMap($type);
},
'astNode' => $type->astNode,
'extensionASTNodes' => static::getExtensionASTNodes($type),
'resolveType' => $type->config['resolveType'] ?? null,
]);
}
protected static function isSpecifiedScalarType(Type $type) : bool
{
return $type instanceof NamedType &&
(
$type->name === Type::STRING ||
$type->name === Type::INT ||
$type->name === Type::FLOAT ||
$type->name === Type::BOOLEAN ||
$type->name === Type::ID
);
}
protected static function extendNamedType(Type $type)
{
if (Introspection::isIntrospectionType($type) || static::isSpecifiedScalarType($type)) {
return $type;
}
$name = $type->name;
if (! isset(static::$extendTypeCache[$name])) {
if ($type instanceof CustomScalarType) {
static::$extendTypeCache[$name] = static::extendCustomScalarType($type);
} elseif ($type instanceof ObjectType) {
static::$extendTypeCache[$name] = static::extendObjectType($type);
} elseif ($type instanceof InterfaceType) {
static::$extendTypeCache[$name] = static::extendInterfaceType($type);
} elseif ($type instanceof UnionType) {
static::$extendTypeCache[$name] = static::extendUnionType($type);
} elseif ($type instanceof EnumType) {
static::$extendTypeCache[$name] = static::extendEnumType($type);
} elseif ($type instanceof InputObjectType) {
static::$extendTypeCache[$name] = static::extendInputObjectType($type);
}
}
return static::$extendTypeCache[$name];
}
/**
* @return mixed|null
*/
protected static function extendMaybeNamedType(?NamedType $type = null)
{
if ($type !== null) {
return static::extendNamedType($type);
}
return null;
}
/**
* @param DirectiveDefinitionNode[] $directiveDefinitions
*
* @return Directive[]
*/
protected static function getMergedDirectives(Schema $schema, array $directiveDefinitions) : array
{
$existingDirectives = array_map(static function (Directive $directive) {
return static::extendDirective($directive);
}, $schema->getDirectives());
Utils::invariant(count($existingDirectives) > 0, 'schema must have default directives');
return array_merge(
$existingDirectives,
array_map(static function (DirectiveDefinitionNode $directive) {
return static::$astBuilder->buildDirective($directive);
}, $directiveDefinitions)
);
}
protected static function extendDirective(Directive $directive) : Directive
{
return new Directive([
'name' => $directive->name,
'description' => $directive->description,
'locations' => $directive->locations,
'args' => static::extendArgs($directive->args),
'astNode' => $directive->astNode,
]);
}
/**
* @param mixed[]|null $options
*/
public static function extend(Schema $schema, DocumentNode $documentAST, ?array $options = null) : Schema
{
if ($options === null || ! (isset($options['assumeValid']) || isset($options['assumeValidSDL']))) {
DocumentValidator::assertValidSDLExtension($documentAST, $schema);
}
$typeDefinitionMap = [];
static::$typeExtensionsMap = [];
$directiveDefinitions = [];
/** @var SchemaDefinitionNode|null $schemaDef */
$schemaDef = null;
/** @var SchemaTypeExtensionNode[] $schemaExtensions */
$schemaExtensions = [];
$definitionsCount = count($documentAST->definitions);
for ($i = 0; $i < $definitionsCount; $i++) {
/** @var Node $def */
$def = $documentAST->definitions[$i];
if ($def instanceof SchemaDefinitionNode) {
$schemaDef = $def;
} elseif ($def instanceof SchemaTypeExtensionNode) {
$schemaExtensions[] = $def;
} elseif ($def instanceof TypeDefinitionNode) {
$typeName = isset($def->name) ? $def->name->value : null;
if ($schema->getType($typeName)) {
throw new Error('Type "' . $typeName . '" already exists in the schema. It cannot also be defined in this type definition.', [$def]);
}
$typeDefinitionMap[$typeName] = $def;
} elseif ($def instanceof TypeExtensionNode) {
$extendedTypeName = isset($def->name) ? $def->name->value : null;
$existingType = $schema->getType($extendedTypeName);
if ($existingType === null) {
throw new Error('Cannot extend type "' . $extendedTypeName . '" because it does not exist in the existing schema.', [$def]);
}
static::checkExtensionNode($existingType, $def);
$existingTypeExtensions = static::$typeExtensionsMap[$extendedTypeName] ?? null;
static::$typeExtensionsMap[$extendedTypeName] = $existingTypeExtensions !== null ? array_merge($existingTypeExtensions, [$def]) : [$def];
} elseif ($def instanceof DirectiveDefinitionNode) {
$directiveName = $def->name->value;
$existingDirective = $schema->getDirective($directiveName);
if ($existingDirective !== null) {
throw new Error('Directive "' . $directiveName . '" already exists in the schema. It cannot be redefined.', [$def]);
}
$directiveDefinitions[] = $def;
}
}
if (count(static::$typeExtensionsMap) === 0 &&
count($typeDefinitionMap) === 0 &&
count($directiveDefinitions) === 0 &&
count($schemaExtensions) === 0 &&
$schemaDef === null
) {
return $schema;
}
static::$astBuilder = new ASTDefinitionBuilder(
$typeDefinitionMap,
$options,
static function (string $typeName) use ($schema) {
/** @var NamedType $existingType */
$existingType = $schema->getType($typeName);
if ($existingType !== null) {
return static::extendNamedType($existingType);
}
throw new Error('Unknown type: "' . $typeName . '". Ensure that this type exists either in the original schema, or is added in a type definition.', [$typeName]);
}
);
static::$extendTypeCache = [];
$operationTypes = [
'query' => static::extendMaybeNamedType($schema->getQueryType()),
'mutation' => static::extendMaybeNamedType($schema->getMutationType()),
'subscription' => static::extendMaybeNamedType($schema->getSubscriptionType()),
];
if ($schemaDef) {
foreach ($schemaDef->operationTypes as $operationType) {
$operation = $operationType->operation;
$type = $operationType->type;
if (isset($operationTypes[$operation])) {
throw new Error('Must provide only one ' . $operation . ' type in schema.');
}
$operationTypes[$operation] = static::$astBuilder->buildType($type);
}
}
foreach ($schemaExtensions as $schemaExtension) {
if (! $schemaExtension->operationTypes) {
continue;
}
foreach ($schemaExtension->operationTypes as $operationType) {
$operation = $operationType->operation;
if ($operationTypes[$operation]) {
throw new Error('Must provide only one ' . $operation . ' type in schema.');
}
$operationTypes[$operation] = static::$astBuilder->buildType($operationType->type);
}
}
$schemaExtensionASTNodes = count($schemaExtensions) > 0
? ($schema->extensionASTNodes ? array_merge($schema->extensionASTNodes, $schemaExtensions) : $schemaExtensions)
: $schema->extensionASTNodes;
$types = array_merge(
array_map(static function ($type) {
return static::extendType($type);
}, array_values($schema->getTypeMap())),
array_map(static function ($type) {
return static::$astBuilder->buildType($type);
}, array_values($typeDefinitionMap))
);
return new Schema([
'query' => $operationTypes['query'],
'mutation' => $operationTypes['mutation'],
'subscription' => $operationTypes['subscription'],
'types' => $types,
'directives' => static::getMergedDirectives($schema, $directiveDefinitions),
'astNode' => $schema->getAstNode(),
'extensionASTNodes' => $schemaExtensionASTNodes,
]);
}
}

View File

@ -16,10 +16,12 @@ use GraphQL\Validator\Rules\ExecutableDefinitions;
use GraphQL\Validator\Rules\FieldsOnCorrectType; use GraphQL\Validator\Rules\FieldsOnCorrectType;
use GraphQL\Validator\Rules\FragmentsOnCompositeTypes; use GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
use GraphQL\Validator\Rules\KnownArgumentNames; use GraphQL\Validator\Rules\KnownArgumentNames;
use GraphQL\Validator\Rules\KnownArgumentNamesOnDirectives;
use GraphQL\Validator\Rules\KnownDirectives; use GraphQL\Validator\Rules\KnownDirectives;
use GraphQL\Validator\Rules\KnownFragmentNames; use GraphQL\Validator\Rules\KnownFragmentNames;
use GraphQL\Validator\Rules\KnownTypeNames; use GraphQL\Validator\Rules\KnownTypeNames;
use GraphQL\Validator\Rules\LoneAnonymousOperation; use GraphQL\Validator\Rules\LoneAnonymousOperation;
use GraphQL\Validator\Rules\LoneSchemaDefinition;
use GraphQL\Validator\Rules\NoFragmentCycles; use GraphQL\Validator\Rules\NoFragmentCycles;
use GraphQL\Validator\Rules\NoUndefinedVariables; use GraphQL\Validator\Rules\NoUndefinedVariables;
use GraphQL\Validator\Rules\NoUnusedFragments; use GraphQL\Validator\Rules\NoUnusedFragments;
@ -27,6 +29,7 @@ use GraphQL\Validator\Rules\NoUnusedVariables;
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged; use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
use GraphQL\Validator\Rules\PossibleFragmentSpreads; use GraphQL\Validator\Rules\PossibleFragmentSpreads;
use GraphQL\Validator\Rules\ProvidedNonNullArguments; use GraphQL\Validator\Rules\ProvidedNonNullArguments;
use GraphQL\Validator\Rules\ProvidedRequiredArgumentsOnDirectives;
use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth; use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\QuerySecurityRule; use GraphQL\Validator\Rules\QuerySecurityRule;
@ -44,10 +47,13 @@ use GraphQL\Validator\Rules\VariablesDefaultValueAllowed;
use GraphQL\Validator\Rules\VariablesInAllowedPosition; use GraphQL\Validator\Rules\VariablesInAllowedPosition;
use Throwable; use Throwable;
use function array_filter; use function array_filter;
use function array_map;
use function array_merge; use function array_merge;
use function count; use function count;
use function implode;
use function is_array; use function is_array;
use function sprintf; use function sprintf;
use const PHP_EOL;
/** /**
* Implements the "Validation" section of the spec. * Implements the "Validation" section of the spec.
@ -78,6 +84,9 @@ class DocumentValidator
/** @var QuerySecurityRule[]|null */ /** @var QuerySecurityRule[]|null */
private static $securityRules; private static $securityRules;
/** @var ValidationRule[]|null */
private static $sdlRules;
/** @var bool */ /** @var bool */
private static $initRules = false; private static $initRules = false;
@ -183,6 +192,23 @@ class DocumentValidator
return self::$securityRules; return self::$securityRules;
} }
public static function sdlRules()
{
if (self::$sdlRules === null) {
self::$sdlRules = [
LoneSchemaDefinition::class => new LoneSchemaDefinition(),
KnownDirectives::class => new KnownDirectives(),
KnownArgumentNamesOnDirectives::class => new KnownArgumentNamesOnDirectives(),
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
UniqueArgumentNames::class => new UniqueArgumentNames(),
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
ProvidedRequiredArgumentsOnDirectives::class => new ProvidedRequiredArgumentsOnDirectives(),
];
}
return self::$sdlRules;
}
/** /**
* This uses a specialized visitor which runs multiple visitors in parallel, * This uses a specialized visitor which runs multiple visitors in parallel,
* while maintaining the visitor skip and break API. * while maintaining the visitor skip and break API.
@ -264,7 +290,8 @@ class DocumentValidator
/** /**
* Utility which determines if a value literal node is valid for an input type. * Utility which determines if a value literal node is valid for an input type.
* *
* Deprecated. Rely on validation for documents containing literal values. * Deprecated. Rely on validation for documents co
* ntaining literal values.
* *
* @deprecated * @deprecated
* *
@ -282,4 +309,19 @@ class DocumentValidator
return $context->getErrors(); return $context->getErrors();
} }
public static function assertValidSDLExtension(DocumentNode $documentAST, Schema $schema)
{
$errors = self::visitUsingRules($schema, new TypeInfo($schema), $documentAST, self::sdlRules());
if (count($errors) !== 0) {
throw new Error(
implode(
PHP_EOL . PHP_EOL,
array_map(static function (Error $error) : string {
return $error->message;
}, $errors)
)
);
}
}
} }

View File

@ -0,0 +1,93 @@
<?php
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\InputValueDefinitionNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NodeList;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Validator\ValidationContext;
use function array_map;
use function in_array;
use function iterator_to_array;
/**
* Known argument names on directives
*
* A GraphQL directive is only valid if all supplied arguments are defined by
* that field.
*/
class KnownArgumentNamesOnDirectives extends ValidationRule
{
protected static function unknownDirectiveArgMessage(string $argName, string $directionName)
{
return 'Unknown argument "' . $argName . '" on directive "@' . $directionName . '".';
}
public function getVisitor(ValidationContext $context)
{
$directiveArgs = [];
$schema = $context->getSchema();
$definedDirectives = $schema !== null ? $schema->getDirectives() : Directive::getInternalDirectives();
foreach ($definedDirectives as $directive) {
$directiveArgs[$directive->name] = array_map(
static function (FieldArgument $arg) : string {
return $arg->name;
},
$directive->args
);
}
$astDefinitions = $context->getDocument()->definitions;
foreach ($astDefinitions as $def) {
if (! ($def instanceof DirectiveDefinitionNode)) {
continue;
}
$name = $def->name->value;
if ($def->arguments !== null) {
$arguments = $def->arguments;
if ($arguments instanceof NodeList) {
$arguments = iterator_to_array($arguments->getIterator());
}
$directiveArgs[$name] = array_map(static function (InputValueDefinitionNode $arg) : string {
return $arg->name->value;
}, $arguments);
} else {
$directiveArgs[$name] = [];
}
}
return [
NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($directiveArgs, $context) {
$directiveName = $directiveNode->name->value;
$knownArgs = $directiveArgs[$directiveName] ?? null;
if ($directiveNode->arguments === null || ! $knownArgs) {
return;
}
foreach ($directiveNode->arguments as $argNode) {
$argName = $argNode->name->value;
if (in_array($argName, $knownArgs)) {
continue;
}
$context->reportError(new Error(
self::unknownDirectiveArgMessage($argName, $directiveName),
[$argNode]
));
}
},
];
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Validator\ValidationContext;
/**
* Lone Schema definition
*
* A GraphQL document is only valid if it contains only one schema definition.
*/
class LoneSchemaDefinition extends ValidationRule
{
public function getVisitor(ValidationContext $context)
{
$oldSchema = $context->getSchema();
$alreadyDefined = $oldSchema !== null ? (
$oldSchema->getAstNode() ||
$oldSchema->getQueryType() ||
$oldSchema->getMutationType() ||
$oldSchema->getSubscriptionType()
) : false;
$schemaDefinitionsCount = 0;
return [
NodeKind::SCHEMA_DEFINITION => static function (SchemaDefinitionNode $node) use ($alreadyDefined, $context, &$schemaDefinitionsCount) {
if ($alreadyDefined !== false) {
$context->reportError(new Error('Cannot define a new schema within a schema extension.', $node));
return;
}
if ($schemaDefinitionsCount > 0) {
$context->reportError(new Error('Must provide only one schema definition.', $node));
}
++$schemaDefinitionsCount;
},
];
}
}

View File

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext;
use function array_filter;
use function is_array;
use function iterator_to_array;
/**
* Provided required arguments on directives
*
* A directive is only valid if all required (non-null without a
* default value) field arguments have been provided.
*/
class ProvidedRequiredArgumentsOnDirectives extends ValidationRule
{
protected static function missingDirectiveArgMessage(string $directiveName, string $argName)
{
return 'Directive "' . $directiveName . '" argument "' . $argName . '" is required but ont provided.';
}
public function getVisitor(ValidationContext $context)
{
$requiredArgsMap = [];
$schema = $context->getSchema();
$definedDirectives = $schema->getDirectives();
foreach ($definedDirectives as $directive) {
$requiredArgsMap[$directive->name] = Utils::keyMap(
array_filter($directive->args, static function (FieldArgument $arg) : bool {
return $arg->getType() instanceof NonNull && ! isset($arg->defaultValue);
}),
static function (FieldArgument $arg) : string {
return $arg->name;
}
);
}
$astDefinition = $context->getDocument()->definitions;
foreach ($astDefinition as $def) {
if (! ($def instanceof DirectiveDefinitionNode)) {
continue;
}
if (is_array($def->arguments)) {
$arguments = $def->arguments;
} elseif ($def->arguments instanceof NodeList) {
$arguments = iterator_to_array($def->arguments->getIterator());
} else {
$arguments = null;
}
$requiredArgsMap[$def->name->value] = Utils::keyMap(
$arguments ? array_filter($arguments, static function (Node $argument) : bool {
return $argument instanceof NonNullTypeNode &&
(
! isset($argument->defaultValue) ||
$argument->defaultValue === null
);
}) : [],
static function (NamedTypeNode $argument) : string {
return $argument->name->value;
}
);
}
return [
NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($requiredArgsMap, $context) {
$directiveName = $directiveNode->name->value;
$requiredArgs = $requiredArgsMap[$directiveName] ?? null;
if (! $requiredArgs) {
return;
}
$argNodes = $directiveNode->arguments ?: [];
$argNodeMap = Utils::keyMap(
$argNodes,
static function (ArgumentNode $arg) : string {
return $arg->name->value;
}
);
foreach ($requiredArgs as $argName => $arg) {
if (isset($argNodeMap[$argName])) {
continue;
}
$context->reportError(
new Error(static::missingDirectiveArgMessage($directiveName, $argName), [$directiveNode])
);
}
},
];
}
}

File diff suppressed because it is too large Load Diff