mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-25 06:16:05 +03:00
parent
7ff3e9399f
commit
e7de069bd5
@ -9,6 +9,6 @@ class DocumentNode extends Node
|
||||
/** @var string */
|
||||
public $kind = NodeKind::DOCUMENT;
|
||||
|
||||
/** @var DefinitionNode[] */
|
||||
/** @var NodeList|DefinitionNode[] */
|
||||
public $definitions;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ namespace GraphQL\Type\Definition;
|
||||
use Exception;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\TypeDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeExtensionNode;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Utils\Utils;
|
||||
use JsonSerializable;
|
||||
@ -47,6 +48,9 @@ abstract class Type implements JsonSerializable
|
||||
/** @var mixed[] */
|
||||
public $config;
|
||||
|
||||
/** @var TypeExtensionNode[] */
|
||||
public $extensionASTNodes;
|
||||
|
||||
/**
|
||||
* @return IDType
|
||||
*
|
||||
|
@ -12,6 +12,7 @@ use GraphQL\Language\AST\EnumTypeExtensionNode;
|
||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\ListTypeNode;
|
||||
use GraphQL\Language\AST\NamedTypeNode;
|
||||
@ -492,4 +493,37 @@ class ASTDefinitionBuilder
|
||||
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
622
src/Utils/SchemaExtender.php
Normal file
622
src/Utils/SchemaExtender.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
@ -16,10 +16,12 @@ use GraphQL\Validator\Rules\ExecutableDefinitions;
|
||||
use GraphQL\Validator\Rules\FieldsOnCorrectType;
|
||||
use GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
|
||||
use GraphQL\Validator\Rules\KnownArgumentNames;
|
||||
use GraphQL\Validator\Rules\KnownArgumentNamesOnDirectives;
|
||||
use GraphQL\Validator\Rules\KnownDirectives;
|
||||
use GraphQL\Validator\Rules\KnownFragmentNames;
|
||||
use GraphQL\Validator\Rules\KnownTypeNames;
|
||||
use GraphQL\Validator\Rules\LoneAnonymousOperation;
|
||||
use GraphQL\Validator\Rules\LoneSchemaDefinition;
|
||||
use GraphQL\Validator\Rules\NoFragmentCycles;
|
||||
use GraphQL\Validator\Rules\NoUndefinedVariables;
|
||||
use GraphQL\Validator\Rules\NoUnusedFragments;
|
||||
@ -27,6 +29,7 @@ use GraphQL\Validator\Rules\NoUnusedVariables;
|
||||
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
|
||||
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
|
||||
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
|
||||
use GraphQL\Validator\Rules\ProvidedRequiredArgumentsOnDirectives;
|
||||
use GraphQL\Validator\Rules\QueryComplexity;
|
||||
use GraphQL\Validator\Rules\QueryDepth;
|
||||
use GraphQL\Validator\Rules\QuerySecurityRule;
|
||||
@ -44,10 +47,13 @@ use GraphQL\Validator\Rules\VariablesDefaultValueAllowed;
|
||||
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
|
||||
use Throwable;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function is_array;
|
||||
use function sprintf;
|
||||
use const PHP_EOL;
|
||||
|
||||
/**
|
||||
* Implements the "Validation" section of the spec.
|
||||
@ -78,6 +84,9 @@ class DocumentValidator
|
||||
/** @var QuerySecurityRule[]|null */
|
||||
private static $securityRules;
|
||||
|
||||
/** @var ValidationRule[]|null */
|
||||
private static $sdlRules;
|
||||
|
||||
/** @var bool */
|
||||
private static $initRules = false;
|
||||
|
||||
@ -183,6 +192,23 @@ class DocumentValidator
|
||||
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,
|
||||
* 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.
|
||||
*
|
||||
* Deprecated. Rely on validation for documents containing literal values.
|
||||
* Deprecated. Rely on validation for documents co
|
||||
* ntaining literal values.
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
@ -282,4 +309,19 @@ class DocumentValidator
|
||||
|
||||
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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
93
src/Validator/Rules/KnownArgumentNamesOnDirectives.php
Normal file
93
src/Validator/Rules/KnownArgumentNamesOnDirectives.php
Normal 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]
|
||||
));
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
46
src/Validator/Rules/LoneSchemaDefinition.php
Normal file
46
src/Validator/Rules/LoneSchemaDefinition.php
Normal 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;
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
110
src/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php
Normal file
110
src/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php
Normal 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])
|
||||
);
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
1931
tests/Utils/SchemaExtenderTest.php
Normal file
1931
tests/Utils/SchemaExtenderTest.php
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user