mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-25 22:36:02 +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
|
```php
|
||||||
/**
|
/**
|
||||||
* @api
|
* @api
|
||||||
* @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
|
* @param Type|ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
|
||||||
* @return ListOfType
|
* @return ListOfType
|
||||||
*/
|
*/
|
||||||
static function listOf($wrappedType)
|
static function listOf($wrappedType)
|
||||||
@ -1345,7 +1345,6 @@ Also it is possible to override warning handler (which is **trigger_error()** by
|
|||||||
|
|
||||||
**Class Constants:**
|
**Class Constants:**
|
||||||
```php
|
```php
|
||||||
const WARNING_NAME = 1;
|
|
||||||
const WARNING_ASSIGN = 2;
|
const WARNING_ASSIGN = 2;
|
||||||
const WARNING_CONFIG = 4;
|
const WARNING_CONFIG = 4;
|
||||||
const WARNING_FULL_SCHEMA_SCAN = 8;
|
const WARNING_FULL_SCHEMA_SCAN = 8;
|
||||||
|
@ -9,7 +9,6 @@ namespace GraphQL\Error;
|
|||||||
*/
|
*/
|
||||||
final class Warning
|
final class Warning
|
||||||
{
|
{
|
||||||
const WARNING_NAME = 1;
|
|
||||||
const WARNING_ASSIGN = 2;
|
const WARNING_ASSIGN = 2;
|
||||||
const WARNING_CONFIG = 4;
|
const WARNING_CONFIG = 4;
|
||||||
const WARNING_FULL_SCHEMA_SCAN = 8;
|
const WARNING_FULL_SCHEMA_SCAN = 8;
|
||||||
|
@ -19,7 +19,7 @@ class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode
|
|||||||
public $directives;
|
public $directives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var EnumValueDefinitionNode[]|null
|
* @var EnumValueDefinitionNode[]|null|NodeList
|
||||||
*/
|
*/
|
||||||
public $values;
|
public $values;
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ class FieldDefinitionNode extends Node
|
|||||||
public $name;
|
public $name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var InputValueDefinitionNode[]
|
* @var InputValueDefinitionNode[]|NodeList
|
||||||
*/
|
*/
|
||||||
public $arguments;
|
public $arguments;
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ class FieldDefinitionNode extends Node
|
|||||||
public $type;
|
public $type;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DirectiveNode[]
|
* @var DirectiveNode[]|NodeList
|
||||||
*/
|
*/
|
||||||
public $directives;
|
public $directives;
|
||||||
|
|
||||||
|
@ -980,6 +980,7 @@ class Parser
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @return OperationTypeDefinitionNode
|
* @return OperationTypeDefinitionNode
|
||||||
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
function parseOperationTypeDefinition()
|
function parseOperationTypeDefinition()
|
||||||
{
|
{
|
||||||
@ -1095,11 +1096,12 @@ class Parser
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @return InputValueDefinitionNode[]|NodeList
|
* @return InputValueDefinitionNode[]|NodeList
|
||||||
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
function parseArgumentDefs()
|
function parseArgumentDefs()
|
||||||
{
|
{
|
||||||
if (!$this->peek(Token::PAREN_L)) {
|
if (!$this->peek(Token::PAREN_L)) {
|
||||||
return [];
|
return new NodeList([]);
|
||||||
}
|
}
|
||||||
return $this->many(Token::PAREN_L, [$this, 'parseInputValueDef'], Token::PAREN_R);
|
return $this->many(Token::PAREN_L, [$this, 'parseInputValueDef'], Token::PAREN_R);
|
||||||
}
|
}
|
||||||
@ -1357,7 +1359,7 @@ class Parser
|
|||||||
$fields = $this->parseFieldsDefinition();
|
$fields = $this->parseFieldsDefinition();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
count($interfaces) === 0 &&
|
!$interfaces &&
|
||||||
count($directives) === 0 &&
|
count($directives) === 0 &&
|
||||||
count($fields) === 0
|
count($fields) === 0
|
||||||
) {
|
) {
|
||||||
@ -1412,7 +1414,7 @@ class Parser
|
|||||||
$types = $this->parseMemberTypesDefinition();
|
$types = $this->parseMemberTypesDefinition();
|
||||||
if (
|
if (
|
||||||
count($directives) === 0 &&
|
count($directives) === 0 &&
|
||||||
count($types) === 0
|
!$types
|
||||||
) {
|
) {
|
||||||
throw $this->unexpected();
|
throw $this->unexpected();
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ namespace GraphQL\Type\Definition;
|
|||||||
|
|
||||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||||
use GraphQL\Language\DirectiveLocation;
|
use GraphQL\Language\DirectiveLocation;
|
||||||
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Directive
|
* Class Directive
|
||||||
@ -159,6 +160,9 @@ class Directive
|
|||||||
foreach ($config as $key => $value) {
|
foreach ($config as $key => $value) {
|
||||||
$this->{$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;
|
$this->config = $config;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ use GraphQL\Utils\Utils;
|
|||||||
* Class EnumType
|
* Class EnumType
|
||||||
* @package GraphQL\Type\Definition
|
* @package GraphQL\Type\Definition
|
||||||
*/
|
*/
|
||||||
class EnumType extends Type implements InputType, OutputType, LeafType
|
class EnumType extends Type implements InputType, OutputType, LeafType, NamedType
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var EnumTypeDefinitionNode|null
|
* @var EnumTypeDefinitionNode|null
|
||||||
@ -39,7 +39,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType
|
|||||||
$config['name'] = $this->tryInferName();
|
$config['name'] = $this->tryInferName();
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::assertValidName($config['name'], !empty($config['isIntrospection']));
|
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||||
|
|
||||||
Config::validate($config, [
|
Config::validate($config, [
|
||||||
'name' => Config::NAME | Config::REQUIRED,
|
'name' => Config::NAME | Config::REQUIRED,
|
||||||
@ -188,24 +188,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType
|
|||||||
);
|
);
|
||||||
|
|
||||||
$values = $this->getValues();
|
$values = $this->getValues();
|
||||||
|
|
||||||
Utils::invariant(
|
|
||||||
!empty($values),
|
|
||||||
"{$this->name} values must be not empty."
|
|
||||||
);
|
|
||||||
foreach ($values as $value) {
|
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(
|
Utils::invariant(
|
||||||
!isset($value->config['isDeprecated']),
|
!isset($value->config['isDeprecated']),
|
||||||
"{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"."
|
"{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"."
|
||||||
|
@ -9,7 +9,7 @@ use GraphQL\Utils\Utils;
|
|||||||
* Class InputObjectType
|
* Class InputObjectType
|
||||||
* @package GraphQL\Type\Definition
|
* @package GraphQL\Type\Definition
|
||||||
*/
|
*/
|
||||||
class InputObjectType extends Type implements InputType
|
class InputObjectType extends Type implements InputType, NamedType
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var InputObjectField[]
|
* @var InputObjectField[]
|
||||||
@ -31,7 +31,7 @@ class InputObjectType extends Type implements InputType
|
|||||||
$config['name'] = $this->tryInferName();
|
$config['name'] = $this->tryInferName();
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::assertValidName($config['name']);
|
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||||
|
|
||||||
Config::validate($config, [
|
Config::validate($config, [
|
||||||
'name' => Config::NAME | Config::REQUIRED,
|
'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);
|
Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name);
|
||||||
return $this->fields[$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 =
|
export type GraphQLInputType =
|
||||||
GraphQLScalarType |
|
| GraphQLScalarType
|
||||||
GraphQLEnumType |
|
| GraphQLEnumType
|
||||||
GraphQLInputObjectType |
|
| GraphQLInputObjectType
|
||||||
GraphQLList |
|
| GraphQLList<GraphQLInputType>
|
||||||
GraphQLNonNull;
|
| GraphQLNonNull<
|
||||||
|
| GraphQLScalarType
|
||||||
|
| GraphQLEnumType
|
||||||
|
| GraphQLInputObjectType
|
||||||
|
| GraphQLList<GraphQLInputType>,
|
||||||
|
>;
|
||||||
*/
|
*/
|
||||||
interface InputType
|
interface InputType
|
||||||
{
|
{
|
||||||
|
@ -10,7 +10,7 @@ use GraphQL\Utils\Utils;
|
|||||||
* Class InterfaceType
|
* Class InterfaceType
|
||||||
* @package GraphQL\Type\Definition
|
* @package GraphQL\Type\Definition
|
||||||
*/
|
*/
|
||||||
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType
|
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NamedType
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param mixed $type
|
* @param mixed $type
|
||||||
@ -51,7 +51,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
|||||||
$config['name'] = $this->tryInferName();
|
$config['name'] = $this->tryInferName();
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::assertValidName($config['name']);
|
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||||
|
|
||||||
Config::validate($config, [
|
Config::validate($config, [
|
||||||
'name' => Config::NAME,
|
'name' => Config::NAME,
|
||||||
@ -120,23 +120,9 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
|||||||
{
|
{
|
||||||
parent::assertValid();
|
parent::assertValid();
|
||||||
|
|
||||||
$fields = $this->getFields();
|
|
||||||
|
|
||||||
Utils::invariant(
|
Utils::invariant(
|
||||||
!isset($this->config['resolveType']) || is_callable($this->config['resolveType']),
|
!isset($this->config['resolveType']) || is_callable($this->config['resolveType']),
|
||||||
"{$this->name} must provide \"resolveType\" as a function."
|
"{$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
|
* @param mixed $type
|
||||||
@ -103,7 +103,7 @@ class ObjectType extends Type implements OutputType, CompositeType
|
|||||||
$config['name'] = $this->tryInferName();
|
$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
|
// 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
|
// 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']),
|
!isset($this->config['isTypeOf']) || is_callable($this->config['isTypeOf']),
|
||||||
"{$this->name} must provide 'isTypeOf' as a function"
|
"{$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
|
* @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->astNode = isset($config['astNode']) ? $config['astNode'] : null;
|
||||||
$this->config = $config;
|
$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
|
* @api
|
||||||
* @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
|
* @param Type|ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
|
||||||
* @return ListOfType
|
* @return ListOfType
|
||||||
*/
|
*/
|
||||||
public static function listOf($wrappedType)
|
public static function listOf($wrappedType)
|
||||||
@ -161,8 +161,11 @@ abstract class Type implements \JsonSerializable
|
|||||||
*/
|
*/
|
||||||
public static function isInputType($type)
|
public static function isInputType($type)
|
||||||
{
|
{
|
||||||
$nakedType = self::getNamedType($type);
|
return $type instanceof InputType &&
|
||||||
return $nakedType instanceof InputType;
|
(
|
||||||
|
!$type instanceof WrappingType ||
|
||||||
|
self::getNamedType($type) instanceof InputType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -172,8 +175,11 @@ abstract class Type implements \JsonSerializable
|
|||||||
*/
|
*/
|
||||||
public static function isOutputType($type)
|
public static function isOutputType($type)
|
||||||
{
|
{
|
||||||
$nakedType = self::getNamedType($type);
|
return $type instanceof OutputType &&
|
||||||
return $nakedType instanceof OutputType;
|
(
|
||||||
|
!$type instanceof WrappingType ||
|
||||||
|
self::getNamedType($type) instanceof OutputType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -311,6 +317,7 @@ abstract class Type implements \JsonSerializable
|
|||||||
*/
|
*/
|
||||||
public function assertValid()
|
public function assertValid()
|
||||||
{
|
{
|
||||||
|
Utils::assertValidName($this->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -9,7 +9,7 @@ use GraphQL\Utils\Utils;
|
|||||||
* Class UnionType
|
* Class UnionType
|
||||||
* @package GraphQL\Type\Definition
|
* @package GraphQL\Type\Definition
|
||||||
*/
|
*/
|
||||||
class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
class UnionType extends Type implements AbstractType, OutputType, CompositeType, NamedType
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var UnionTypeDefinitionNode
|
* @var UnionTypeDefinitionNode
|
||||||
@ -36,7 +36,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
|||||||
$config['name'] = $this->tryInferName();
|
$config['name'] = $this->tryInferName();
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::assertValidName($config['name']);
|
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||||
|
|
||||||
Config::validate($config, [
|
Config::validate($config, [
|
||||||
'name' => Config::NAME | Config::REQUIRED,
|
'name' => Config::NAME | Config::REQUIRED,
|
||||||
@ -81,7 +81,8 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
|||||||
|
|
||||||
if (!is_array($types)) {
|
if (!is_array($types)) {
|
||||||
throw new InvariantViolation(
|
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();
|
parent::assertValid();
|
||||||
|
|
||||||
$types = $this->getTypes();
|
|
||||||
Utils::invariant(
|
|
||||||
!empty($types),
|
|
||||||
"{$this->name} types must not be empty"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isset($this->config['resolveType'])) {
|
if (isset($this->config['resolveType'])) {
|
||||||
Utils::invariant(
|
Utils::invariant(
|
||||||
is_callable($this->config['resolveType']),
|
is_callable($this->config['resolveType']),
|
||||||
"{$this->name} must provide \"resolveType\" as a function."
|
"{$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
|
* @param Type $type
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function isIntrospectionType(Type $type)
|
public static function isIntrospectionType($type)
|
||||||
{
|
{
|
||||||
return in_array($type->name, array_keys(self::getTypes()));
|
return in_array($type->name, array_keys(self::getTypes()));
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
namespace GraphQL\Type;
|
namespace GraphQL\Type;
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Error\InvariantViolation;
|
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
|
||||||
use GraphQL\Language\AST\NamedTypeNode;
|
use GraphQL\Language\AST\NamedTypeNode;
|
||||||
use GraphQL\Language\AST\Node;
|
use GraphQL\Language\AST\Node;
|
||||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||||
@ -13,20 +15,24 @@ use GraphQL\Language\AST\SchemaDefinitionNode;
|
|||||||
use GraphQL\Language\AST\TypeDefinitionNode;
|
use GraphQL\Language\AST\TypeDefinitionNode;
|
||||||
use GraphQL\Language\AST\TypeNode;
|
use GraphQL\Language\AST\TypeNode;
|
||||||
use GraphQL\Type\Definition\Directive;
|
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\InterfaceType;
|
||||||
|
use GraphQL\Type\Definition\NamedType;
|
||||||
use GraphQL\Type\Definition\NonNull;
|
use GraphQL\Type\Definition\NonNull;
|
||||||
use GraphQL\Type\Definition\ObjectType;
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use GraphQL\Type\Definition\UnionType;
|
||||||
use GraphQL\Utils\TypeComparators;
|
use GraphQL\Utils\TypeComparators;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class SchemaValidationContext
|
class SchemaValidationContext
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var Error[]
|
||||||
*/
|
*/
|
||||||
private $errors = [];
|
private $errors = [];
|
||||||
|
|
||||||
@ -56,7 +62,7 @@ class SchemaValidationContext
|
|||||||
);
|
);
|
||||||
} else if (!$queryType instanceof ObjectType) {
|
} else if (!$queryType instanceof ObjectType) {
|
||||||
$this->reportError(
|
$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')
|
$this->getOperationTypeNode($queryType, 'query')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -64,7 +70,7 @@ class SchemaValidationContext
|
|||||||
$mutationType = $this->schema->getMutationType();
|
$mutationType = $this->schema->getMutationType();
|
||||||
if ($mutationType && !$mutationType instanceof ObjectType) {
|
if ($mutationType && !$mutationType instanceof ObjectType) {
|
||||||
$this->reportError(
|
$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')
|
$this->getOperationTypeNode($mutationType, 'mutation')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -72,282 +78,12 @@ class SchemaValidationContext
|
|||||||
$subscriptionType = $this->schema->getSubscriptionType();
|
$subscriptionType = $this->schema->getSubscriptionType();
|
||||||
if ($subscriptionType && !$subscriptionType instanceof ObjectType) {
|
if ($subscriptionType && !$subscriptionType instanceof ObjectType) {
|
||||||
$this->reportError(
|
$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')
|
$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 Type $type
|
||||||
* @param string $operation
|
* @param string $operation
|
||||||
@ -373,12 +109,620 @@ class SchemaValidationContext
|
|||||||
return $operationTypeNode ? $operationTypeNode->type : ($type ? $type->astNode : null);
|
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 string $message
|
||||||
* @param array|Node|TypeNode|TypeDefinitionNode $nodes
|
* @param array|Node|TypeNode|TypeDefinitionNode $nodes
|
||||||
*/
|
*/
|
||||||
private function reportError($message, $nodes = null) {
|
private function reportError($message, $nodes = null) {
|
||||||
$nodes = array_filter($nodes && is_array($nodes) ? $nodes : [$nodes]);
|
$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\EnumType;
|
||||||
use GraphQL\Type\Definition\InputObjectType;
|
use GraphQL\Type\Definition\InputObjectType;
|
||||||
use GraphQL\Type\Definition\InputType;
|
use GraphQL\Type\Definition\InputType;
|
||||||
use GraphQL\Type\Definition\OutputType;
|
|
||||||
use GraphQL\Type\Definition\Directive;
|
use GraphQL\Type\Definition\Directive;
|
||||||
use GraphQL\Type\Definition\InterfaceType;
|
use GraphQL\Type\Definition\InterfaceType;
|
||||||
use GraphQL\Type\Definition\FieldArgument;
|
use GraphQL\Type\Definition\FieldArgument;
|
||||||
@ -128,53 +127,7 @@ class ASTDefinitionBuilder
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param TypeNode $typeNode
|
* @param TypeNode $typeNode
|
||||||
* @return InputType|Type
|
* @return Type|InputType
|
||||||
* @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
|
|
||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
private function internalBuildWrappedType(TypeNode $typeNode)
|
private function internalBuildWrappedType(TypeNode $typeNode)
|
||||||
@ -199,7 +152,10 @@ class ASTDefinitionBuilder
|
|||||||
public function buildField(FieldDefinitionNode $field)
|
public function buildField(FieldDefinitionNode $field)
|
||||||
{
|
{
|
||||||
return [
|
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),
|
'description' => $this->getDescription($field),
|
||||||
'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null,
|
'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null,
|
||||||
'deprecationReason' => $this->getDeprecationReason($field),
|
'deprecationReason' => $this->getDeprecationReason($field),
|
||||||
@ -282,7 +238,10 @@ class ASTDefinitionBuilder
|
|||||||
return $value->name->value;
|
return $value->name->value;
|
||||||
},
|
},
|
||||||
function ($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 = [
|
$config = [
|
||||||
'name' => $value->name->value,
|
'name' => $value->name->value,
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
@ -339,9 +298,12 @@ class ASTDefinitionBuilder
|
|||||||
return new UnionType([
|
return new UnionType([
|
||||||
'name' => $def->name->value,
|
'name' => $def->name->value,
|
||||||
'description' => $this->getDescription($def),
|
'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
|
'types' => $def->types
|
||||||
? Utils::map($def->types, function ($typeNode) {
|
? Utils::map($def->types, function ($typeNode) {
|
||||||
return $this->buildObjectType($typeNode);
|
return $this->buildType($typeNode);
|
||||||
}):
|
}):
|
||||||
[],
|
[],
|
||||||
'astNode' => $def,
|
'astNode' => $def,
|
||||||
@ -409,7 +371,7 @@ class ASTDefinitionBuilder
|
|||||||
{
|
{
|
||||||
$loc = $node->loc;
|
$loc = $node->loc;
|
||||||
if (!$loc || !$loc->startToken) {
|
if (!$loc || !$loc->startToken) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
$comments = [];
|
$comments = [];
|
||||||
$token = $loc->startToken->prev;
|
$token = $loc->startToken->prev;
|
||||||
|
@ -221,7 +221,7 @@ class SchemaPrinter
|
|||||||
|
|
||||||
private static function printArgs($options, $args, $indentation = '')
|
private static function printArgs($options, $args, $indentation = '')
|
||||||
{
|
{
|
||||||
if (count($args) === 0) {
|
if (!$args) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Utils;
|
namespace GraphQL\Utils;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Error\InvariantViolation;
|
use GraphQL\Error\InvariantViolation;
|
||||||
use GraphQL\Error\Warning;
|
use GraphQL\Error\Warning;
|
||||||
|
use GraphQL\Language\AST\Node;
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
use GraphQL\Type\Definition\WrappingType;
|
use GraphQL\Type\Definition\WrappingType;
|
||||||
use \Traversable, \InvalidArgumentException;
|
use \Traversable, \InvalidArgumentException;
|
||||||
@ -229,7 +231,7 @@ class Utils
|
|||||||
* @param string $message
|
* @param string $message
|
||||||
* @param mixed $sprintfParam1
|
* @param mixed $sprintfParam1
|
||||||
* @param mixed $sprintfParam2 ...
|
* @param mixed $sprintfParam2 ...
|
||||||
* @throws InvariantViolation
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public static function invariant($test, $message = '')
|
public static function invariant($test, $message = '')
|
||||||
{
|
{
|
||||||
@ -239,6 +241,7 @@ class Utils
|
|||||||
array_shift($args);
|
array_shift($args);
|
||||||
$message = call_user_func_array('sprintf', $args);
|
$message = call_user_func_array('sprintf', $args);
|
||||||
}
|
}
|
||||||
|
// TODO switch to Error here
|
||||||
throw new InvariantViolation($message);
|
throw new InvariantViolation($message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -302,7 +305,11 @@ class Utils
|
|||||||
return $var->toString();
|
return $var->toString();
|
||||||
}
|
}
|
||||||
if (is_object($var)) {
|
if (is_object($var)) {
|
||||||
return 'instance of ' . get_class($var);
|
if (method_exists($var, '__toString')) {
|
||||||
|
return (string) $var;
|
||||||
|
} else {
|
||||||
|
return 'instance of ' . get_class($var);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (is_array($var)) {
|
if (is_array($var)) {
|
||||||
return json_encode($var);
|
return json_encode($var);
|
||||||
@ -399,34 +406,46 @@ class Utils
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Upholds the spec rules about naming.
|
||||||
|
*
|
||||||
* @param $name
|
* @param $name
|
||||||
* @param bool $isIntrospection
|
* @throws Error
|
||||||
* @throws InvariantViolation
|
|
||||||
*/
|
*/
|
||||||
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(
|
* Returns an Error if a name is invalid.
|
||||||
"Must be named. Unexpected name: " . self::printSafe($name)
|
*
|
||||||
|
* @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] === '_') {
|
if (!preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name)) {
|
||||||
Warning::warnOnce(
|
return new Error(
|
||||||
'Name "'.$name.'" must not begin with "__", which is reserved by ' .
|
"Names must match /^[_a-zA-Z][_a-zA-Z0-9]*\$/ but \"{$name}\" does not.",
|
||||||
'GraphQL introspection. In a future release of graphql this will ' .
|
$node
|
||||||
'become an exception',
|
|
||||||
Warning::WARNING_NAME
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preg_match($regex, $name)) {
|
return null;
|
||||||
throw new InvariantViolation(
|
|
||||||
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "'.$name.'" does not.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
* @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