mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-22 04:46:04 +03:00
Move schema validation into separate step (type constructors)
This is the second step of moving work from type constructors to the schema validation function. ref: graphql/graphql-js#1132
This commit is contained in:
parent
6d08c342c9
commit
97e8a9e200
@ -171,7 +171,7 @@ static function float()
|
||||
```php
|
||||
/**
|
||||
* @api
|
||||
* @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
|
||||
* @param Type|ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
|
||||
* @return ListOfType
|
||||
*/
|
||||
static function listOf($wrappedType)
|
||||
@ -1345,7 +1345,6 @@ Also it is possible to override warning handler (which is **trigger_error()** by
|
||||
|
||||
**Class Constants:**
|
||||
```php
|
||||
const WARNING_NAME = 1;
|
||||
const WARNING_ASSIGN = 2;
|
||||
const WARNING_CONFIG = 4;
|
||||
const WARNING_FULL_SCHEMA_SCAN = 8;
|
||||
|
@ -9,7 +9,6 @@ namespace GraphQL\Error;
|
||||
*/
|
||||
final class Warning
|
||||
{
|
||||
const WARNING_NAME = 1;
|
||||
const WARNING_ASSIGN = 2;
|
||||
const WARNING_CONFIG = 4;
|
||||
const WARNING_FULL_SCHEMA_SCAN = 8;
|
||||
|
@ -19,7 +19,7 @@ class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var EnumValueDefinitionNode[]|null
|
||||
* @var EnumValueDefinitionNode[]|null|NodeList
|
||||
*/
|
||||
public $values;
|
||||
|
||||
|
@ -14,7 +14,7 @@ class FieldDefinitionNode extends Node
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var InputValueDefinitionNode[]
|
||||
* @var InputValueDefinitionNode[]|NodeList
|
||||
*/
|
||||
public $arguments;
|
||||
|
||||
@ -24,7 +24,7 @@ class FieldDefinitionNode extends Node
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var DirectiveNode[]
|
||||
* @var DirectiveNode[]|NodeList
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
|
@ -980,6 +980,7 @@ class Parser
|
||||
|
||||
/**
|
||||
* @return OperationTypeDefinitionNode
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
function parseOperationTypeDefinition()
|
||||
{
|
||||
@ -1095,11 +1096,12 @@ class Parser
|
||||
|
||||
/**
|
||||
* @return InputValueDefinitionNode[]|NodeList
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
function parseArgumentDefs()
|
||||
{
|
||||
if (!$this->peek(Token::PAREN_L)) {
|
||||
return [];
|
||||
return new NodeList([]);
|
||||
}
|
||||
return $this->many(Token::PAREN_L, [$this, 'parseInputValueDef'], Token::PAREN_R);
|
||||
}
|
||||
@ -1357,7 +1359,7 @@ class Parser
|
||||
$fields = $this->parseFieldsDefinition();
|
||||
|
||||
if (
|
||||
count($interfaces) === 0 &&
|
||||
!$interfaces &&
|
||||
count($directives) === 0 &&
|
||||
count($fields) === 0
|
||||
) {
|
||||
@ -1412,7 +1414,7 @@ class Parser
|
||||
$types = $this->parseMemberTypesDefinition();
|
||||
if (
|
||||
count($directives) === 0 &&
|
||||
count($types) === 0
|
||||
!$types
|
||||
) {
|
||||
throw $this->unexpected();
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
use GraphQL\Language\DirectiveLocation;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class Directive
|
||||
@ -159,6 +160,9 @@ class Directive
|
||||
foreach ($config as $key => $value) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
|
||||
Utils::invariant($this->name, 'Directive must be named.');
|
||||
Utils::invariant(is_array($this->locations), 'Must provide locations for directive.');
|
||||
$this->config = $config;
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ use GraphQL\Utils\Utils;
|
||||
* Class EnumType
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class EnumType extends Type implements InputType, OutputType, LeafType
|
||||
class EnumType extends Type implements InputType, OutputType, LeafType, NamedType
|
||||
{
|
||||
/**
|
||||
* @var EnumTypeDefinitionNode|null
|
||||
@ -39,7 +39,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType
|
||||
$config['name'] = $this->tryInferName();
|
||||
}
|
||||
|
||||
Utils::assertValidName($config['name'], !empty($config['isIntrospection']));
|
||||
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||
|
||||
Config::validate($config, [
|
||||
'name' => Config::NAME | Config::REQUIRED,
|
||||
@ -188,24 +188,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType
|
||||
);
|
||||
|
||||
$values = $this->getValues();
|
||||
|
||||
Utils::invariant(
|
||||
!empty($values),
|
||||
"{$this->name} values must be not empty."
|
||||
);
|
||||
foreach ($values as $value) {
|
||||
try {
|
||||
Utils::assertValidName($value->name);
|
||||
} catch (InvariantViolation $e) {
|
||||
throw new InvariantViolation(
|
||||
"{$this->name} has value with invalid name: " .
|
||||
Utils::printSafe($value->name) . " ({$e->getMessage()})"
|
||||
);
|
||||
}
|
||||
Utils::invariant(
|
||||
!in_array($value->name, ['true', 'false', 'null']),
|
||||
"{$this->name}: \"{$value->name}\" can not be used as an Enum value."
|
||||
);
|
||||
Utils::invariant(
|
||||
!isset($value->config['isDeprecated']),
|
||||
"{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"."
|
||||
|
@ -9,7 +9,7 @@ use GraphQL\Utils\Utils;
|
||||
* Class InputObjectType
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class InputObjectType extends Type implements InputType
|
||||
class InputObjectType extends Type implements InputType, NamedType
|
||||
{
|
||||
/**
|
||||
* @var InputObjectField[]
|
||||
@ -31,7 +31,7 @@ class InputObjectType extends Type implements InputType
|
||||
$config['name'] = $this->tryInferName();
|
||||
}
|
||||
|
||||
Utils::assertValidName($config['name']);
|
||||
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||
|
||||
Config::validate($config, [
|
||||
'name' => Config::NAME | Config::REQUIRED,
|
||||
@ -91,41 +91,4 @@ class InputObjectType extends Type implements InputType
|
||||
Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name);
|
||||
return $this->fields[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public function assertValid()
|
||||
{
|
||||
parent::assertValid();
|
||||
|
||||
$fields = $this->getFields();
|
||||
|
||||
Utils::invariant(
|
||||
!empty($fields),
|
||||
"{$this->name} fields must not be empty"
|
||||
);
|
||||
|
||||
foreach ($fields as $field) {
|
||||
try {
|
||||
Utils::assertValidName($field->name);
|
||||
} catch (InvariantViolation $e) {
|
||||
throw new InvariantViolation("{$this->name}.{$field->name}: {$e->getMessage()}");
|
||||
}
|
||||
|
||||
$fieldType = $field->type;
|
||||
if ($fieldType instanceof WrappingType) {
|
||||
$fieldType = $fieldType->getWrappedType(true);
|
||||
}
|
||||
Utils::invariant(
|
||||
$fieldType instanceof InputType,
|
||||
"{$this->name}.{$field->name} field type must be Input Type but got: %s.",
|
||||
Utils::printSafe($field->type)
|
||||
);
|
||||
Utils::invariant(
|
||||
!isset($field->config['resolve']),
|
||||
"{$this->name}.{$field->name} field type has a resolve property, but Input Types cannot define resolvers."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,16 @@ namespace GraphQL\Type\Definition;
|
||||
|
||||
/*
|
||||
export type GraphQLInputType =
|
||||
GraphQLScalarType |
|
||||
GraphQLEnumType |
|
||||
GraphQLInputObjectType |
|
||||
GraphQLList |
|
||||
GraphQLNonNull;
|
||||
| GraphQLScalarType
|
||||
| GraphQLEnumType
|
||||
| GraphQLInputObjectType
|
||||
| GraphQLList<GraphQLInputType>
|
||||
| GraphQLNonNull<
|
||||
| GraphQLScalarType
|
||||
| GraphQLEnumType
|
||||
| GraphQLInputObjectType
|
||||
| GraphQLList<GraphQLInputType>,
|
||||
>;
|
||||
*/
|
||||
interface InputType
|
||||
{
|
||||
|
@ -10,7 +10,7 @@ use GraphQL\Utils\Utils;
|
||||
* Class InterfaceType
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType
|
||||
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NamedType
|
||||
{
|
||||
/**
|
||||
* @param mixed $type
|
||||
@ -51,7 +51,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
||||
$config['name'] = $this->tryInferName();
|
||||
}
|
||||
|
||||
Utils::assertValidName($config['name']);
|
||||
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||
|
||||
Config::validate($config, [
|
||||
'name' => Config::NAME,
|
||||
@ -120,23 +120,9 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
||||
{
|
||||
parent::assertValid();
|
||||
|
||||
$fields = $this->getFields();
|
||||
|
||||
Utils::invariant(
|
||||
!isset($this->config['resolveType']) || is_callable($this->config['resolveType']),
|
||||
"{$this->name} must provide \"resolveType\" as a function."
|
||||
);
|
||||
|
||||
Utils::invariant(
|
||||
!empty($fields),
|
||||
"{$this->name} fields must not be empty"
|
||||
);
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$field->assertValid($this);
|
||||
foreach ($field->args as $arg) {
|
||||
$arg->assertValid($field, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
src/Type/Definition/NamedType.php
Normal file
15
src/Type/Definition/NamedType.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
/*
|
||||
export type GraphQLNamedType =
|
||||
| GraphQLScalarType
|
||||
| GraphQLObjectType
|
||||
| GraphQLInterfaceType
|
||||
| GraphQLUnionType
|
||||
| GraphQLEnumType
|
||||
| GraphQLInputObjectType;
|
||||
*/
|
||||
interface NamedType
|
||||
{
|
||||
}
|
@ -47,7 +47,7 @@ use GraphQL\Utils\Utils;
|
||||
* ]);
|
||||
*
|
||||
*/
|
||||
class ObjectType extends Type implements OutputType, CompositeType
|
||||
class ObjectType extends Type implements OutputType, CompositeType, NamedType
|
||||
{
|
||||
/**
|
||||
* @param mixed $type
|
||||
@ -103,7 +103,7 @@ class ObjectType extends Type implements OutputType, CompositeType
|
||||
$config['name'] = $this->tryInferName();
|
||||
}
|
||||
|
||||
Utils::assertValidName($config['name'], !empty($config['isIntrospection']));
|
||||
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||
|
||||
// Note: this validation is disabled by default, because it is resource-consuming
|
||||
// TODO: add bin/validate script to check if schema is valid during development
|
||||
@ -228,18 +228,5 @@ class ObjectType extends Type implements OutputType, CompositeType
|
||||
!isset($this->config['isTypeOf']) || is_callable($this->config['isTypeOf']),
|
||||
"{$this->name} must provide 'isTypeOf' as a function"
|
||||
);
|
||||
|
||||
// getFields() and getInterfaceMap() will do structural validation
|
||||
$fields = $this->getFields();
|
||||
Utils::invariant(
|
||||
!empty($fields),
|
||||
"{$this->name} fields must not be empty"
|
||||
);
|
||||
foreach ($fields as $field) {
|
||||
$field->assertValid($this);
|
||||
foreach ($field->args as $arg) {
|
||||
$arg->assertValid($field, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ use GraphQL\Utils\Utils;
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
abstract class ScalarType extends Type implements OutputType, InputType, LeafType
|
||||
abstract class ScalarType extends Type implements OutputType, InputType, LeafType, NamedType
|
||||
{
|
||||
/**
|
||||
* @var ScalarTypeDefinitionNode|null
|
||||
@ -36,6 +36,6 @@ abstract class ScalarType extends Type implements OutputType, InputType, LeafTyp
|
||||
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
|
||||
$this->config = $config;
|
||||
|
||||
Utils::assertValidName($this->name);
|
||||
Utils::invariant(is_string($this->name), 'Must provide name.');
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ abstract class Type implements \JsonSerializable
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
|
||||
* @param Type|ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
|
||||
* @return ListOfType
|
||||
*/
|
||||
public static function listOf($wrappedType)
|
||||
@ -161,8 +161,11 @@ abstract class Type implements \JsonSerializable
|
||||
*/
|
||||
public static function isInputType($type)
|
||||
{
|
||||
$nakedType = self::getNamedType($type);
|
||||
return $nakedType instanceof InputType;
|
||||
return $type instanceof InputType &&
|
||||
(
|
||||
!$type instanceof WrappingType ||
|
||||
self::getNamedType($type) instanceof InputType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -172,8 +175,11 @@ abstract class Type implements \JsonSerializable
|
||||
*/
|
||||
public static function isOutputType($type)
|
||||
{
|
||||
$nakedType = self::getNamedType($type);
|
||||
return $nakedType instanceof OutputType;
|
||||
return $type instanceof OutputType &&
|
||||
(
|
||||
!$type instanceof WrappingType ||
|
||||
self::getNamedType($type) instanceof OutputType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -311,6 +317,7 @@ abstract class Type implements \JsonSerializable
|
||||
*/
|
||||
public function assertValid()
|
||||
{
|
||||
Utils::assertValidName($this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -9,7 +9,7 @@ use GraphQL\Utils\Utils;
|
||||
* Class UnionType
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
||||
class UnionType extends Type implements AbstractType, OutputType, CompositeType, NamedType
|
||||
{
|
||||
/**
|
||||
* @var UnionTypeDefinitionNode
|
||||
@ -36,7 +36,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
||||
$config['name'] = $this->tryInferName();
|
||||
}
|
||||
|
||||
Utils::assertValidName($config['name']);
|
||||
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||
|
||||
Config::validate($config, [
|
||||
'name' => Config::NAME | Config::REQUIRED,
|
||||
@ -81,7 +81,8 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
||||
|
||||
if (!is_array($types)) {
|
||||
throw new InvariantViolation(
|
||||
"{$this->name} types must be an Array or a callable which returns an Array."
|
||||
"Must provide Array of types or a callable which returns " .
|
||||
"such an array for Union {$this->name}"
|
||||
);
|
||||
}
|
||||
|
||||
@ -133,31 +134,11 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
||||
{
|
||||
parent::assertValid();
|
||||
|
||||
$types = $this->getTypes();
|
||||
Utils::invariant(
|
||||
!empty($types),
|
||||
"{$this->name} types must not be empty"
|
||||
);
|
||||
|
||||
if (isset($this->config['resolveType'])) {
|
||||
Utils::invariant(
|
||||
is_callable($this->config['resolveType']),
|
||||
"{$this->name} must provide \"resolveType\" as a function."
|
||||
);
|
||||
}
|
||||
|
||||
$includedTypeNames = [];
|
||||
foreach ($types as $objType) {
|
||||
Utils::invariant(
|
||||
$objType instanceof ObjectType,
|
||||
"{$this->name} may only contain Object types, it cannot contain: %s.",
|
||||
Utils::printSafe($objType)
|
||||
);
|
||||
Utils::invariant(
|
||||
!isset($includedTypeNames[$objType->name]),
|
||||
"{$this->name} can include {$objType->name} type only once."
|
||||
);
|
||||
$includedTypeNames[$objType->name] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -168,7 +168,7 @@ EOD;
|
||||
* @param Type $type
|
||||
* @return bool
|
||||
*/
|
||||
public static function isIntrospectionType(Type $type)
|
||||
public static function isIntrospectionType($type)
|
||||
{
|
||||
return in_array($type->name, array_keys(self::getTypes()));
|
||||
}
|
||||
|
@ -2,9 +2,11 @@
|
||||
namespace GraphQL\Type;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
|
||||
use GraphQL\Language\AST\NamedTypeNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||
@ -13,20 +15,24 @@ use GraphQL\Language\AST\SchemaDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeNode;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\EnumValueDefinition;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InputObjectField;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
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\Utils\TypeComparators;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class SchemaValidationContext
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
* @var Error[]
|
||||
*/
|
||||
private $errors = [];
|
||||
|
||||
@ -56,7 +62,7 @@ class SchemaValidationContext
|
||||
);
|
||||
} else if (!$queryType instanceof ObjectType) {
|
||||
$this->reportError(
|
||||
'Query root type must be Object type but got: ' . Utils::getVariableType($queryType) . '.',
|
||||
'Query root type must be Object type, it cannot be ' . Utils::printSafe($queryType) . '.',
|
||||
$this->getOperationTypeNode($queryType, 'query')
|
||||
);
|
||||
}
|
||||
@ -64,7 +70,7 @@ class SchemaValidationContext
|
||||
$mutationType = $this->schema->getMutationType();
|
||||
if ($mutationType && !$mutationType instanceof ObjectType) {
|
||||
$this->reportError(
|
||||
'Mutation root type must be Object type if provided but got: ' . Utils::getVariableType($mutationType) . '.',
|
||||
'Mutation root type must be Object type if provided, it cannot be ' . Utils::printSafe($mutationType) . '.',
|
||||
$this->getOperationTypeNode($mutationType, 'mutation')
|
||||
);
|
||||
}
|
||||
@ -72,282 +78,12 @@ class SchemaValidationContext
|
||||
$subscriptionType = $this->schema->getSubscriptionType();
|
||||
if ($subscriptionType && !$subscriptionType instanceof ObjectType) {
|
||||
$this->reportError(
|
||||
'Subscription root type must be Object type if provided but got: ' . Utils::getVariableType($subscriptionType) . '.',
|
||||
'Subscription root type must be Object type if provided, it cannot be ' . Utils::printSafe($subscriptionType) . '.',
|
||||
$this->getOperationTypeNode($subscriptionType, 'subscription')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function validateDirectives()
|
||||
{
|
||||
$directives = $this->schema->getDirectives();
|
||||
foreach($directives as $directive) {
|
||||
if (!$directive instanceof Directive) {
|
||||
$this->reportError(
|
||||
"Expected directive but got: " . $directive,
|
||||
is_object($directive) ? $directive->astNode : null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function validateTypes()
|
||||
{
|
||||
$typeMap = $this->schema->getTypeMap();
|
||||
foreach($typeMap as $typeName => $type) {
|
||||
// Ensure all provided types are in fact GraphQL type.
|
||||
if (!Type::isType($type)) {
|
||||
$this->reportError(
|
||||
"Expected GraphQL type but got: " . Utils::getVariableType($type),
|
||||
is_object($type) ? $type->astNode : null
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure objects implement the interfaces they claim to.
|
||||
if ($type instanceof ObjectType) {
|
||||
$implementedTypeNames = [];
|
||||
|
||||
foreach($type->getInterfaces() as $iface) {
|
||||
if (isset($implementedTypeNames[$iface->name])) {
|
||||
$this->reportError(
|
||||
"{$type->name} must declare it implements {$iface->name} only once.",
|
||||
$this->getAllImplementsInterfaceNode($type, $iface)
|
||||
);
|
||||
}
|
||||
$implementedTypeNames[$iface->name] = true;
|
||||
$this->validateObjectImplementsInterface($type, $iface);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $object
|
||||
* @param InterfaceType $iface
|
||||
*/
|
||||
private function validateObjectImplementsInterface(ObjectType $object, $iface)
|
||||
{
|
||||
if (!$iface instanceof InterfaceType) {
|
||||
$this->reportError(
|
||||
$object .
|
||||
" must only implement Interface types, it cannot implement " .
|
||||
$iface . ".",
|
||||
$this->getImplementsInterfaceNode($object, $iface)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$objectFieldMap = $object->getFields();
|
||||
$ifaceFieldMap = $iface->getFields();
|
||||
|
||||
// Assert each interface field is implemented.
|
||||
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
|
||||
$objectField = array_key_exists($fieldName, $objectFieldMap)
|
||||
? $objectFieldMap[$fieldName]
|
||||
: null;
|
||||
|
||||
// Assert interface field exists on object.
|
||||
if (!$objectField) {
|
||||
$this->reportError(
|
||||
"\"{$iface->name}\" expects field \"{$fieldName}\" but \"{$object->name}\" does not provide it.",
|
||||
[$this->getFieldNode($iface, $fieldName), $object->astNode]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assert interface field type is satisfied by object field type, by being
|
||||
// a valid subtype. (covariant)
|
||||
if (
|
||||
!TypeComparators::isTypeSubTypeOf(
|
||||
$this->schema,
|
||||
$objectField->getType(),
|
||||
$ifaceField->getType()
|
||||
)
|
||||
) {
|
||||
$this->reportError(
|
||||
"{$iface->name}.{$fieldName} expects type ".
|
||||
"\"{$ifaceField->getType()}\"" .
|
||||
" but {$object->name}.{$fieldName} is type " .
|
||||
"\"{$objectField->getType()}\".",
|
||||
[
|
||||
$this->getFieldTypeNode($iface, $fieldName),
|
||||
$this->getFieldTypeNode($object, $fieldName),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Assert each interface field arg is implemented.
|
||||
foreach($ifaceField->args as $ifaceArg) {
|
||||
$argName = $ifaceArg->name;
|
||||
$objectArg = null;
|
||||
|
||||
foreach($objectField->args as $arg) {
|
||||
if ($arg->name === $argName) {
|
||||
$objectArg = $arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Assert interface field arg exists on object field.
|
||||
if (!$objectArg) {
|
||||
$this->reportError(
|
||||
"{$iface->name}.{$fieldName} expects argument \"{$argName}\" but ".
|
||||
"{$object->name}.{$fieldName} does not provide it.",
|
||||
[
|
||||
$this->getFieldArgNode($iface, $fieldName, $argName),
|
||||
$this->getFieldNode($object, $fieldName),
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assert interface field arg type matches object field arg type.
|
||||
// (invariant)
|
||||
// TODO: change to contravariant?
|
||||
if (!TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType())) {
|
||||
$this->reportError(
|
||||
"{$iface->name}.{$fieldName}({$argName}:) expects type ".
|
||||
"\"{$ifaceArg->getType()}\"" .
|
||||
" but {$object->name}.{$fieldName}({$argName}:) is type " .
|
||||
"\"{$objectArg->getType()}\".",
|
||||
[
|
||||
$this->getFieldArgTypeNode($iface, $fieldName, $argName),
|
||||
$this->getFieldArgTypeNode($object, $fieldName, $argName),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: validate default values?
|
||||
}
|
||||
|
||||
// Assert additional arguments must not be required.
|
||||
foreach($objectField->args as $objectArg) {
|
||||
$argName = $objectArg->name;
|
||||
$ifaceArg = null;
|
||||
|
||||
foreach($ifaceField->args as $arg) {
|
||||
if ($arg->name === $argName) {
|
||||
$ifaceArg = $arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$ifaceArg && $objectArg->getType() instanceof NonNull) {
|
||||
$this->reportError(
|
||||
"{$object->name}.{$fieldName}({$argName}:) is of required type " .
|
||||
"\"{$objectArg->getType()}\"" .
|
||||
" but is not also provided by the interface {$iface->name}.{$fieldName}.",
|
||||
[
|
||||
$this->getFieldArgTypeNode($object, $fieldName, $argName),
|
||||
$this->getFieldNode($iface, $fieldName),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $type
|
||||
* @param InterfaceType|null $iface
|
||||
* @return NamedTypeNode|null
|
||||
*/
|
||||
private function getImplementsInterfaceNode(ObjectType $type, $iface)
|
||||
{
|
||||
$nodes = $this->getAllImplementsInterfaceNode($type, $iface);
|
||||
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $type
|
||||
* @param InterfaceType|null $iface
|
||||
* @return NamedTypeNode[]
|
||||
*/
|
||||
private function getAllImplementsInterfaceNode(ObjectType $type, $iface)
|
||||
{
|
||||
$implementsNodes = [];
|
||||
/** @var ObjectTypeDefinitionNode|ObjectTypeExtensionNode[] $astNodes */
|
||||
$astNodes = array_merge([$type->astNode], $type->extensionASTNodes ?: []);
|
||||
|
||||
foreach($astNodes as $astNode) {
|
||||
if ($astNode && $astNode->interfaces) {
|
||||
foreach($astNode->interfaces as $node) {
|
||||
if ($node->name->value === $iface->name) {
|
||||
$implementsNodes[] = $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $implementsNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @param string $fieldName
|
||||
* @return FieldDefinitionNode|null
|
||||
*/
|
||||
private function getFieldNode($type, $fieldName)
|
||||
{
|
||||
/** @var ObjectTypeDefinitionNode|ObjectTypeExtensionNode[] $astNodes */
|
||||
$astNodes = array_merge([$type->astNode], $type->extensionASTNodes ?: []);
|
||||
|
||||
foreach($astNodes as $astNode) {
|
||||
if ($astNode && $astNode->fields) {
|
||||
foreach($astNode->fields as $node) {
|
||||
if ($node->name->value === $fieldName) {
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @param string $fieldName
|
||||
* @return TypeNode|null
|
||||
*/
|
||||
private function getFieldTypeNode($type, $fieldName)
|
||||
{
|
||||
$fieldNode = $this->getFieldNode($type, $fieldName);
|
||||
if ($fieldNode) {
|
||||
return $fieldNode->type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @param string $fieldName
|
||||
* @param string $argName
|
||||
* @return InputValueDefinitionNode|null
|
||||
*/
|
||||
private function getFieldArgNode($type, $fieldName, $argName)
|
||||
{
|
||||
$fieldNode = $this->getFieldNode($type, $fieldName);
|
||||
if ($fieldNode && $fieldNode->arguments) {
|
||||
foreach ($fieldNode->arguments as $node) {
|
||||
if ($node->name->value === $argName) {
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @param string $fieldName
|
||||
* @param string $argName
|
||||
* @return TypeNode|null
|
||||
*/
|
||||
private function getFieldArgTypeNode($type, $fieldName, $argName)
|
||||
{
|
||||
$fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName);
|
||||
if ($fieldArgNode) {
|
||||
return $fieldArgNode->type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type $type
|
||||
* @param string $operation
|
||||
@ -373,12 +109,620 @@ class SchemaValidationContext
|
||||
return $operationTypeNode ? $operationTypeNode->type : ($type ? $type->astNode : null);
|
||||
}
|
||||
|
||||
public function validateDirectives()
|
||||
{
|
||||
$directives = $this->schema->getDirectives();
|
||||
foreach($directives as $directive) {
|
||||
// Ensure all directives are in fact GraphQL directives.
|
||||
if (!$directive instanceof Directive) {
|
||||
$this->reportError(
|
||||
"Expected directive but got: " . Utils::printSafe($directive) . '.',
|
||||
is_object($directive) ? $directive->astNode : null
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure they are named correctly.
|
||||
$this->validateName($directive);
|
||||
|
||||
// TODO: Ensure proper locations.
|
||||
|
||||
$argNames = [];
|
||||
foreach ($directive->args as $arg) {
|
||||
$argName = $arg->name;
|
||||
|
||||
// Ensure they are named correctly.
|
||||
$this->validateName($directive);
|
||||
|
||||
if (isset($argNames[$argName])) {
|
||||
$this->reportError(
|
||||
"Argument @{$directive->name}({$argName}:) can only be defined once.",
|
||||
$this->getAllDirectiveArgNodes($directive, $argName)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$argNames[$argName] = true;
|
||||
|
||||
// Ensure the type is an input type.
|
||||
if (!Type::isInputType($arg->getType())) {
|
||||
$this->reportError(
|
||||
"The type of @{$directive->name}({$argName}:) must be Input Type " .
|
||||
'but got: ' . Utils::printSafe($arg->getType()) . '.',
|
||||
$this->getDirectiveArgTypeNode($directive, $argName)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type|Directive|FieldDefinition|EnumValueDefinition|InputObjectField $node
|
||||
*/
|
||||
private function validateName($node)
|
||||
{
|
||||
// Ensure names are valid, however introspection types opt out.
|
||||
$error = Utils::isValidNameError($node->name, $node->astNode);
|
||||
if ($error && !Introspection::isIntrospectionType($node)) {
|
||||
$this->addError($error);
|
||||
}
|
||||
}
|
||||
|
||||
public function validateTypes()
|
||||
{
|
||||
$typeMap = $this->schema->getTypeMap();
|
||||
foreach($typeMap as $typeName => $type) {
|
||||
// Ensure all provided types are in fact GraphQL type.
|
||||
if (!$type instanceof NamedType) {
|
||||
$this->reportError(
|
||||
"Expected GraphQL named type but got: " . Utils::printSafe($type) . '.',
|
||||
is_object($type) ? $type->astNode : null
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->validateName($type);
|
||||
|
||||
if ($type instanceof ObjectType) {
|
||||
// Ensure fields are valid
|
||||
$this->validateFields($type);
|
||||
|
||||
// Ensure objects implement the interfaces they claim to.
|
||||
$this->validateObjectInterfaces($type);
|
||||
} else if ($type instanceof InterfaceType) {
|
||||
// Ensure fields are valid.
|
||||
$this->validateFields($type);
|
||||
} else if ($type instanceof UnionType) {
|
||||
// Ensure Unions include valid member types.
|
||||
$this->validateUnionMembers($type);
|
||||
} else if ($type instanceof EnumType) {
|
||||
// Ensure Enums have valid values.
|
||||
$this->validateEnumValues($type);
|
||||
} else if ($type instanceof InputObjectType) {
|
||||
// Ensure Input Object fields are valid.
|
||||
$this->validateInputFields($type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
*/
|
||||
private function validateFields($type) {
|
||||
$fieldMap = $type->getFields();
|
||||
|
||||
// Objects and Interfaces both must define one or more fields.
|
||||
if (!$fieldMap) {
|
||||
$this->reportError(
|
||||
"Type {$type->name} must define one or more fields.",
|
||||
$this->getAllObjectOrInterfaceNodes($type)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($fieldMap as $fieldName => $field) {
|
||||
// Ensure they are named correctly.
|
||||
$this->validateName($field);
|
||||
|
||||
// Ensure they were defined at most once.
|
||||
$fieldNodes = $this->getAllFieldNodes($type, $fieldName);
|
||||
if ($fieldNodes && count($fieldNodes) > 1) {
|
||||
$this->reportError(
|
||||
"Field {$type->name}.{$fieldName} can only be defined once.",
|
||||
$fieldNodes
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure the type is an output type
|
||||
if (!Type::isOutputType($field->getType())) {
|
||||
$this->reportError(
|
||||
"The type of {$type->name}.{$fieldName} must be Output Type " .
|
||||
'but got: ' . Utils::printSafe($field->getType()) . '.',
|
||||
$this->getFieldTypeNode($type, $fieldName)
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure the arguments are valid
|
||||
$argNames = [];
|
||||
foreach($field->args as $arg) {
|
||||
$argName = $arg->name;
|
||||
|
||||
// Ensure they are named correctly.
|
||||
$this->validateName($arg);
|
||||
|
||||
if (isset($argNames[$argName])) {
|
||||
$this->reportError(
|
||||
"Field argument {$type->name}.{$fieldName}({$argName}:) can only " .
|
||||
'be defined once.',
|
||||
$this->getAllFieldArgNodes($type, $fieldName, $argName)
|
||||
);
|
||||
}
|
||||
$argNames[$argName] = true;
|
||||
|
||||
// Ensure the type is an input type
|
||||
if (!Type::isInputType($arg->getType())) {
|
||||
$this->reportError(
|
||||
"The type of {$type->name}.{$fieldName}({$argName}:) must be Input " .
|
||||
'Type but got: '. Utils::printSafe($arg->getType()) . '.',
|
||||
$this->getFieldArgTypeNode($type, $fieldName, $argName)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function validateObjectInterfaces(ObjectType $object) {
|
||||
$implementedTypeNames = [];
|
||||
foreach($object->getInterfaces() as $iface) {
|
||||
if (isset($implementedTypeNames[$iface->name])) {
|
||||
$this->reportError(
|
||||
"Type {$object->name} can only implement {$iface->name} once.",
|
||||
$this->getAllImplementsInterfaceNodes($object, $iface)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$implementedTypeNames[$iface->name] = true;
|
||||
$this->validateObjectImplementsInterface($object, $iface);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $object
|
||||
* @param InterfaceType $iface
|
||||
*/
|
||||
private function validateObjectImplementsInterface(ObjectType $object, $iface)
|
||||
{
|
||||
if (!$iface instanceof InterfaceType) {
|
||||
$this->reportError(
|
||||
"Type {$object->name} must only implement Interface types, " .
|
||||
"it cannot implement ". Utils::printSafe($iface) . ".",
|
||||
$this->getImplementsInterfaceNode($object, $iface)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$objectFieldMap = $object->getFields();
|
||||
$ifaceFieldMap = $iface->getFields();
|
||||
|
||||
// Assert each interface field is implemented.
|
||||
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
|
||||
$objectField = array_key_exists($fieldName, $objectFieldMap)
|
||||
? $objectFieldMap[$fieldName]
|
||||
: null;
|
||||
|
||||
// Assert interface field exists on object.
|
||||
if (!$objectField) {
|
||||
$this->reportError(
|
||||
"Interface field {$iface->name}.{$fieldName} expected but " .
|
||||
"{$object->name} does not provide it.",
|
||||
[$this->getFieldNode($iface, $fieldName), $object->astNode]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assert interface field type is satisfied by object field type, by being
|
||||
// a valid subtype. (covariant)
|
||||
if (
|
||||
!TypeComparators::isTypeSubTypeOf(
|
||||
$this->schema,
|
||||
$objectField->getType(),
|
||||
$ifaceField->getType()
|
||||
)
|
||||
) {
|
||||
$this->reportError(
|
||||
"Interface field {$iface->name}.{$fieldName} expects type ".
|
||||
"{$ifaceField->getType()} but {$object->name}.{$fieldName} " .
|
||||
"is type " . Utils::printSafe($objectField->getType()) . ".",
|
||||
[
|
||||
$this->getFieldTypeNode($iface, $fieldName),
|
||||
$this->getFieldTypeNode($object, $fieldName),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Assert each interface field arg is implemented.
|
||||
foreach($ifaceField->args as $ifaceArg) {
|
||||
$argName = $ifaceArg->name;
|
||||
$objectArg = null;
|
||||
|
||||
foreach($objectField->args as $arg) {
|
||||
if ($arg->name === $argName) {
|
||||
$objectArg = $arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Assert interface field arg exists on object field.
|
||||
if (!$objectArg) {
|
||||
$this->reportError(
|
||||
"Interface field argument {$iface->name}.{$fieldName}({$argName}:) " .
|
||||
"expected but {$object->name}.{$fieldName} does not provide it.",
|
||||
[
|
||||
$this->getFieldArgNode($iface, $fieldName, $argName),
|
||||
$this->getFieldNode($object, $fieldName),
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assert interface field arg type matches object field arg type.
|
||||
// (invariant)
|
||||
// TODO: change to contravariant?
|
||||
if (!TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType())) {
|
||||
$this->reportError(
|
||||
"Interface field argument {$iface->name}.{$fieldName}({$argName}:) ".
|
||||
"expects type " . Utils::printSafe($ifaceArg->getType()) . " but " .
|
||||
"{$object->name}.{$fieldName}({$argName}:) is type " .
|
||||
Utils::printSafe($objectArg->getType()) . ".",
|
||||
[
|
||||
$this->getFieldArgTypeNode($iface, $fieldName, $argName),
|
||||
$this->getFieldArgTypeNode($object, $fieldName, $argName),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: validate default values?
|
||||
}
|
||||
|
||||
// Assert additional arguments must not be required.
|
||||
foreach($objectField->args as $objectArg) {
|
||||
$argName = $objectArg->name;
|
||||
$ifaceArg = null;
|
||||
|
||||
foreach($ifaceField->args as $arg) {
|
||||
if ($arg->name === $argName) {
|
||||
$ifaceArg = $arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$ifaceArg && $objectArg->getType() instanceof NonNull) {
|
||||
$this->reportError(
|
||||
"Object field argument {$object->name}.{$fieldName}({$argName}:) " .
|
||||
"is of required type " . Utils::printSafe($objectArg->getType()) . " but is not also " .
|
||||
"provided by the Interface field {$iface->name}.{$fieldName}.",
|
||||
[
|
||||
$this->getFieldArgTypeNode($object, $fieldName, $argName),
|
||||
$this->getFieldNode($iface, $fieldName),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function validateUnionMembers(UnionType $union)
|
||||
{
|
||||
$memberTypes = $union->getTypes();
|
||||
|
||||
if (!$memberTypes) {
|
||||
$this->reportError(
|
||||
"Union type {$union->name} must define one or more member types.",
|
||||
$union->astNode
|
||||
);
|
||||
}
|
||||
|
||||
$includedTypeNames = [];
|
||||
|
||||
foreach($memberTypes as $memberType) {
|
||||
if (isset($includedTypeNames[$memberType->name])) {
|
||||
$this->reportError(
|
||||
"Union type {$union->name} can only include type ".
|
||||
"{$memberType->name} once.",
|
||||
$this->getUnionMemberTypeNodes($union, $memberType->name)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$includedTypeNames[$memberType->name] = true;
|
||||
if (!$memberType instanceof ObjectType) {
|
||||
$this->reportError(
|
||||
"Union type {$union->name} can only include Object types, ".
|
||||
"it cannot include " . Utils::printSafe($memberType) . ".",
|
||||
$this->getUnionMemberTypeNodes($union, Utils::printSafe($memberType))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function validateEnumValues(EnumType $enumType)
|
||||
{
|
||||
$enumValues = $enumType->getValues();
|
||||
|
||||
if (!$enumValues) {
|
||||
$this->reportError(
|
||||
"Enum type {$enumType->name} must define one or more values.",
|
||||
$enumType->astNode
|
||||
);
|
||||
}
|
||||
|
||||
foreach($enumValues as $enumValue) {
|
||||
$valueName = $enumValue->name;
|
||||
|
||||
// Ensure no duplicates
|
||||
$allNodes = $this->getEnumValueNodes($enumType, $valueName);
|
||||
if ($allNodes && count($allNodes) > 1) {
|
||||
$this->reportError(
|
||||
"Enum type {$enumType->name} can include value {$valueName} only once.",
|
||||
$allNodes
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure valid name.
|
||||
$this->validateName($enumValue);
|
||||
if ($valueName === 'true' || $valueName === 'false' || $valueName === 'null') {
|
||||
$this->reportError(
|
||||
"Enum type {$enumType->name} cannot include value: {$valueName}.",
|
||||
$enumValue->astNode
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function validateInputFields(InputObjectType $inputObj)
|
||||
{
|
||||
$fieldMap = $inputObj->getFields();
|
||||
|
||||
if (!$fieldMap) {
|
||||
$this->reportError(
|
||||
"Input Object type {$inputObj->name} must define one or more fields.",
|
||||
$inputObj->astNode
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure the arguments are valid
|
||||
foreach ($fieldMap as $fieldName => $field) {
|
||||
// Ensure they are named correctly.
|
||||
$this->validateName($field);
|
||||
|
||||
// TODO: Ensure they are unique per field.
|
||||
|
||||
// Ensure the type is an input type
|
||||
if (!Type::isInputType($field->getType())) {
|
||||
$this->reportError(
|
||||
"The type of {$inputObj->name}.{$fieldName} must be Input Type " .
|
||||
"but got: " . Utils::printSafe($field->getType()) . ".",
|
||||
$field->astNode ? $field->astNode->type : null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @return ObjectTypeDefinitionNode[]|ObjectTypeExtensionNode[]|InterfaceTypeDefinitionNode[]|InterfaceTypeExtensionNode[]
|
||||
*/
|
||||
private function getAllObjectOrInterfaceNodes($type)
|
||||
{
|
||||
return $type->astNode
|
||||
? ($type->extensionASTNodes
|
||||
? array_merge([$type->astNode], $type->extensionASTNodes)
|
||||
: [$type->astNode])
|
||||
: ($type->extensionASTNodes ?: []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $type
|
||||
* @param InterfaceType $iface
|
||||
* @return NamedTypeNode|null
|
||||
*/
|
||||
private function getImplementsInterfaceNode(ObjectType $type, $iface)
|
||||
{
|
||||
$nodes = $this->getAllImplementsInterfaceNodes($type, $iface);
|
||||
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $type
|
||||
* @param InterfaceType $iface
|
||||
* @return NamedTypeNode[]
|
||||
*/
|
||||
private function getAllImplementsInterfaceNodes(ObjectType $type, $iface)
|
||||
{
|
||||
$implementsNodes = [];
|
||||
$astNodes = $this->getAllObjectOrInterfaceNodes($type);
|
||||
|
||||
foreach($astNodes as $astNode) {
|
||||
if ($astNode && $astNode->interfaces) {
|
||||
foreach($astNode->interfaces as $node) {
|
||||
if ($node->name->value === $iface->name) {
|
||||
$implementsNodes[] = $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $implementsNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @param string $fieldName
|
||||
* @return FieldDefinitionNode|null
|
||||
*/
|
||||
private function getFieldNode($type, $fieldName)
|
||||
{
|
||||
$nodes = $this->getAllFieldNodes($type, $fieldName);
|
||||
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @param string $fieldName
|
||||
* @return FieldDefinitionNode[]
|
||||
*/
|
||||
private function getAllFieldNodes($type, $fieldName)
|
||||
{
|
||||
$fieldNodes = [];
|
||||
$astNodes = $this->getAllObjectOrInterfaceNodes($type);
|
||||
foreach($astNodes as $astNode) {
|
||||
if ($astNode && $astNode->fields) {
|
||||
foreach($astNode->fields as $node) {
|
||||
if ($node->name->value === $fieldName) {
|
||||
$fieldNodes[] = $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $fieldNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @param string $fieldName
|
||||
* @return TypeNode|null
|
||||
*/
|
||||
private function getFieldTypeNode($type, $fieldName)
|
||||
{
|
||||
$fieldNode = $this->getFieldNode($type, $fieldName);
|
||||
return $fieldNode ? $fieldNode->type : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @param string $fieldName
|
||||
* @param string $argName
|
||||
* @return InputValueDefinitionNode|null
|
||||
*/
|
||||
private function getFieldArgNode($type, $fieldName, $argName)
|
||||
{
|
||||
$nodes = $this->getAllFieldArgNodes($type, $fieldName, $argName);
|
||||
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @param string $fieldName
|
||||
* @param string $argName
|
||||
* @return InputValueDefinitionNode[]
|
||||
*/
|
||||
private function getAllFieldArgNodes($type, $fieldName, $argName)
|
||||
{
|
||||
$argNodes = [];
|
||||
$fieldNode = $this->getFieldNode($type, $fieldName);
|
||||
if ($fieldNode && $fieldNode->arguments) {
|
||||
foreach ($fieldNode->arguments as $node) {
|
||||
if ($node->name->value === $argName) {
|
||||
$argNodes[] = $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $argNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
* @param string $fieldName
|
||||
* @param string $argName
|
||||
* @return TypeNode|null
|
||||
*/
|
||||
private function getFieldArgTypeNode($type, $fieldName, $argName)
|
||||
{
|
||||
$fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName);
|
||||
return $fieldArgNode ? $fieldArgNode->type : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Directive $directive
|
||||
* @param string $argName
|
||||
* @return InputValueDefinitionNode[]
|
||||
*/
|
||||
private function getAllDirectiveArgNodes(Directive $directive, $argName)
|
||||
{
|
||||
$argNodes = [];
|
||||
$directiveNode = $directive->astNode;
|
||||
if ($directiveNode && $directiveNode->arguments) {
|
||||
foreach($directiveNode->arguments as $node) {
|
||||
if ($node->name->value === $argName) {
|
||||
$argNodes[] = $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $argNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Directive $directive
|
||||
* @param string $argName
|
||||
* @return TypeNode|null
|
||||
*/
|
||||
private function getDirectiveArgTypeNode(Directive $directive, $argName)
|
||||
{
|
||||
$argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0];
|
||||
return $argNode ? $argNode->type : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UnionType $union
|
||||
* @param string $typeName
|
||||
* @return NamedTypeNode[]
|
||||
*/
|
||||
private function getUnionMemberTypeNodes(UnionType $union, $typeName)
|
||||
{
|
||||
if ($union->astNode && $union->astNode->types) {
|
||||
return array_filter(
|
||||
$union->astNode->types,
|
||||
function (NamedTypeNode $value) use ($typeName) {
|
||||
return $value->name->value === $typeName;
|
||||
}
|
||||
);
|
||||
}
|
||||
return $union->astNode ?
|
||||
$union->astNode->types : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EnumType $enum
|
||||
* @param string $valueName
|
||||
* @return EnumValueDefinitionNode[]
|
||||
*/
|
||||
private function getEnumValueNodes(EnumType $enum, $valueName)
|
||||
{
|
||||
if ($enum->astNode && $enum->astNode->values) {
|
||||
return array_filter(
|
||||
iterator_to_array($enum->astNode->values),
|
||||
function (EnumValueDefinitionNode $value) use ($valueName) {
|
||||
return $value->name->value === $valueName;
|
||||
}
|
||||
);
|
||||
}
|
||||
return $enum->astNode ?
|
||||
$enum->astNode->values : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param array|Node|TypeNode|TypeDefinitionNode $nodes
|
||||
*/
|
||||
private function reportError($message, $nodes = null) {
|
||||
$nodes = array_filter($nodes && is_array($nodes) ? $nodes : [$nodes]);
|
||||
$this->errors[] = new Error($message, $nodes);
|
||||
$this->addError(new Error($message, $nodes));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Error $error
|
||||
*/
|
||||
private function addError($error) {
|
||||
$this->errors[] = $error;
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ use GraphQL\Type\Definition\CustomScalarType;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\OutputType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
@ -128,53 +127,7 @@ class ASTDefinitionBuilder
|
||||
|
||||
/**
|
||||
* @param TypeNode $typeNode
|
||||
* @return InputType|Type
|
||||
* @throws Error
|
||||
*/
|
||||
public function buildInputType(TypeNode $typeNode)
|
||||
{
|
||||
$type = $this->internalBuildWrappedType($typeNode);
|
||||
Utils::invariant(Type::isInputType($type), 'Expected Input type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TypeNode $typeNode
|
||||
* @return OutputType|Type
|
||||
* @throws Error
|
||||
*/
|
||||
public function buildOutputType(TypeNode $typeNode)
|
||||
{
|
||||
$type = $this->internalBuildWrappedType($typeNode);
|
||||
Utils::invariant(Type::isOutputType($type), 'Expected Output type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TypeNode|string $typeNode
|
||||
* @return ObjectType|Type
|
||||
* @throws Error
|
||||
*/
|
||||
public function buildObjectType($typeNode)
|
||||
{
|
||||
$type = $this->buildType($typeNode);
|
||||
return ObjectType::assertObjectType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TypeNode|string $typeNode
|
||||
* @return InterfaceType|Type
|
||||
* @throws Error
|
||||
*/
|
||||
public function buildInterfaceType($typeNode)
|
||||
{
|
||||
$type = $this->buildType($typeNode);
|
||||
return InterfaceType::assertInterfaceType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TypeNode $typeNode
|
||||
* @return Type
|
||||
* @return Type|InputType
|
||||
* @throws Error
|
||||
*/
|
||||
private function internalBuildWrappedType(TypeNode $typeNode)
|
||||
@ -199,7 +152,10 @@ class ASTDefinitionBuilder
|
||||
public function buildField(FieldDefinitionNode $field)
|
||||
{
|
||||
return [
|
||||
'type' => $this->buildOutputType($field->type),
|
||||
// Note: While this could make assertions to get the correctly typed
|
||||
// value, that would throw immediately while type system validation
|
||||
// with validateSchema() will produce more actionable results.
|
||||
'type' => $this->internalBuildWrappedType($field->type),
|
||||
'description' => $this->getDescription($field),
|
||||
'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null,
|
||||
'deprecationReason' => $this->getDeprecationReason($field),
|
||||
@ -282,7 +238,10 @@ class ASTDefinitionBuilder
|
||||
return $value->name->value;
|
||||
},
|
||||
function ($value) {
|
||||
$type = $this->buildInputType($value->type);
|
||||
// Note: While this could make assertions to get the correctly typed
|
||||
// value, that would throw immediately while type system validation
|
||||
// with validateSchema() will produce more actionable results.
|
||||
$type = $this->internalBuildWrappedType($value->type);
|
||||
$config = [
|
||||
'name' => $value->name->value,
|
||||
'type' => $type,
|
||||
@ -339,9 +298,12 @@ class ASTDefinitionBuilder
|
||||
return new UnionType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
// Note: While this could make assertions to get the correctly typed
|
||||
// values below, that would throw immediately while type system
|
||||
// validation with validateSchema() will produce more actionable results.
|
||||
'types' => $def->types
|
||||
? Utils::map($def->types, function ($typeNode) {
|
||||
return $this->buildObjectType($typeNode);
|
||||
return $this->buildType($typeNode);
|
||||
}):
|
||||
[],
|
||||
'astNode' => $def,
|
||||
@ -409,7 +371,7 @@ class ASTDefinitionBuilder
|
||||
{
|
||||
$loc = $node->loc;
|
||||
if (!$loc || !$loc->startToken) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
$comments = [];
|
||||
$token = $loc->startToken->prev;
|
||||
|
@ -221,7 +221,7 @@ class SchemaPrinter
|
||||
|
||||
private static function printArgs($options, $args, $indentation = '')
|
||||
{
|
||||
if (count($args) === 0) {
|
||||
if (!$args) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Error\Warning;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\WrappingType;
|
||||
use \Traversable, \InvalidArgumentException;
|
||||
@ -229,7 +231,7 @@ class Utils
|
||||
* @param string $message
|
||||
* @param mixed $sprintfParam1
|
||||
* @param mixed $sprintfParam2 ...
|
||||
* @throws InvariantViolation
|
||||
* @throws Error
|
||||
*/
|
||||
public static function invariant($test, $message = '')
|
||||
{
|
||||
@ -239,6 +241,7 @@ class Utils
|
||||
array_shift($args);
|
||||
$message = call_user_func_array('sprintf', $args);
|
||||
}
|
||||
// TODO switch to Error here
|
||||
throw new InvariantViolation($message);
|
||||
}
|
||||
}
|
||||
@ -302,8 +305,12 @@ class Utils
|
||||
return $var->toString();
|
||||
}
|
||||
if (is_object($var)) {
|
||||
if (method_exists($var, '__toString')) {
|
||||
return (string) $var;
|
||||
} else {
|
||||
return 'instance of ' . get_class($var);
|
||||
}
|
||||
}
|
||||
if (is_array($var)) {
|
||||
return json_encode($var);
|
||||
}
|
||||
@ -399,34 +406,46 @@ class Utils
|
||||
}
|
||||
|
||||
/**
|
||||
* Upholds the spec rules about naming.
|
||||
*
|
||||
* @param $name
|
||||
* @param bool $isIntrospection
|
||||
* @throws InvariantViolation
|
||||
* @throws Error
|
||||
*/
|
||||
public static function assertValidName($name, $isIntrospection = false)
|
||||
public static function assertValidName($name)
|
||||
{
|
||||
$regex = '/^[_a-zA-Z][_a-zA-Z0-9]*$/';
|
||||
$error = self::isValidNameError($name);
|
||||
if ($error) {
|
||||
throw $error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$name || !is_string($name)) {
|
||||
throw new InvariantViolation(
|
||||
"Must be named. Unexpected name: " . self::printSafe($name)
|
||||
/**
|
||||
* Returns an Error if a name is invalid.
|
||||
*
|
||||
* @param string $name
|
||||
* @param Node|null $node
|
||||
* @return Error|null
|
||||
*/
|
||||
public static function isValidNameError($name, $node = null)
|
||||
{
|
||||
Utils::invariant(is_string($name), 'Expected string');
|
||||
|
||||
if (isset($name[1]) && $name[0] === '_' && $name[1] === '_') {
|
||||
return new Error(
|
||||
"Name \"{$name}\" must not begin with \"__\", which is reserved by " .
|
||||
"GraphQL introspection.",
|
||||
$node
|
||||
);
|
||||
}
|
||||
|
||||
if (!$isIntrospection && isset($name[1]) && $name[0] === '_' && $name[1] === '_') {
|
||||
Warning::warnOnce(
|
||||
'Name "'.$name.'" must not begin with "__", which is reserved by ' .
|
||||
'GraphQL introspection. In a future release of graphql this will ' .
|
||||
'become an exception',
|
||||
Warning::WARNING_NAME
|
||||
if (!preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name)) {
|
||||
return new Error(
|
||||
"Names must match /^[_a-zA-Z][_a-zA-Z0-9]*\$/ but \"{$name}\" does not.",
|
||||
$node
|
||||
);
|
||||
}
|
||||
|
||||
if (!preg_match($regex, $name)) {
|
||||
throw new InvariantViolation(
|
||||
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "'.$name.'" does not.'
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -468,37 +468,6 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @it prohibits putting non-Object types in unions
|
||||
*/
|
||||
public function testProhibitsPuttingNonObjectTypesInUnions()
|
||||
{
|
||||
$int = Type::int();
|
||||
|
||||
$badUnionTypes = [
|
||||
$int,
|
||||
new NonNull($int),
|
||||
new ListOfType($int),
|
||||
$this->interfaceType,
|
||||
$this->unionType,
|
||||
$this->enumType,
|
||||
$this->inputObjectType
|
||||
];
|
||||
|
||||
foreach ($badUnionTypes as $type) {
|
||||
try {
|
||||
$union = new UnionType(['name' => 'BadUnion', 'types' => [$type]]);
|
||||
$union->assertValid();
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (\Exception $e) {
|
||||
$this->assertSame(
|
||||
'BadUnion may only contain Object types, it cannot contain: ' . Utils::printSafe($type) . '.',
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows a thunk for Union\'s types
|
||||
*/
|
||||
|
File diff suppressed because it is too large
Load Diff
47
tests/Utils/AssertValidNameTest.php
Normal file
47
tests/Utils/AssertValidNameTest.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace GraphQL\Tests\Utils;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
class AssertValidNameTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// Describe: assertValidName()
|
||||
|
||||
/**
|
||||
* @it throws for use of leading double underscores
|
||||
*/
|
||||
public function testThrowsForUseOfLeadingDoubleUnderscores()
|
||||
{
|
||||
$this->setExpectedException(
|
||||
Error::class,
|
||||
'"__bad" must not begin with "__", which is reserved by GraphQL introspection.'
|
||||
);
|
||||
Utils::assertValidName('__bad');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it throws for non-strings
|
||||
*/
|
||||
public function testThrowsForNonStrings()
|
||||
{
|
||||
$this->setExpectedException(
|
||||
InvariantViolation::class,
|
||||
'Expected string'
|
||||
);
|
||||
Utils::assertValidName([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it throws for names with invalid characters
|
||||
*/
|
||||
public function testThrowsForNamesWithInvalidCharacters()
|
||||
{
|
||||
$this->setExpectedException(
|
||||
Error::class,
|
||||
'Names must match'
|
||||
);
|
||||
Utils::assertValidName('>--()-->');
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user