mirror of
https://github.com/retailcrm/graphql-php.git
synced 2025-02-06 07:49:24 +03:00
Validate schema root types and directives
This moves validation out of GraphQLSchema's constructor (but not yet from other type constructors), which is responsible for root type validation and interface implementation checking. Reduces time to construct GraphQLSchema significantly, shifting the time to validation. This also allows for much looser rules within the schema builders, which implicitly validate while trying to adhere to flow types. Instead we use any casts to loosen the rules to defer that to validation where errors can be richer. This also loosens the rule that a schema can only be constructed if it has a query type, moving that to validation as well. That makes flow typing slightly less nice, but allows for incremental schema building which is valuable ref: graphql/graphql-js#1124
This commit is contained in:
parent
15374a31dd
commit
06c6c4bd97
@ -231,6 +231,15 @@ static function isCompositeType($type)
|
|||||||
static function isAbstractType($type)
|
static function isAbstractType($type)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
* @param Type $type
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
static function isType($type)
|
||||||
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
/**
|
/**
|
||||||
* @api
|
* @api
|
||||||
@ -431,7 +440,7 @@ static function create(array $options = [])
|
|||||||
* @param ObjectType $query
|
* @param ObjectType $query
|
||||||
* @return SchemaConfig
|
* @return SchemaConfig
|
||||||
*/
|
*/
|
||||||
function setQuery(GraphQL\Type\Definition\ObjectType $query)
|
function setQuery($query)
|
||||||
```
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
@ -440,7 +449,7 @@ function setQuery(GraphQL\Type\Definition\ObjectType $query)
|
|||||||
* @param ObjectType $mutation
|
* @param ObjectType $mutation
|
||||||
* @return SchemaConfig
|
* @return SchemaConfig
|
||||||
*/
|
*/
|
||||||
function setMutation(GraphQL\Type\Definition\ObjectType $mutation)
|
function setMutation($mutation)
|
||||||
```
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
@ -449,7 +458,7 @@ function setMutation(GraphQL\Type\Definition\ObjectType $mutation)
|
|||||||
* @param ObjectType $subscription
|
* @param ObjectType $subscription
|
||||||
* @return SchemaConfig
|
* @return SchemaConfig
|
||||||
*/
|
*/
|
||||||
function setSubscription(GraphQL\Type\Definition\ObjectType $subscription)
|
function setSubscription($subscription)
|
||||||
```
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
@ -670,6 +679,18 @@ function getDirectives()
|
|||||||
function getDirective($name)
|
function getDirective($name)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* Validates schema.
|
||||||
|
*
|
||||||
|
* This operation requires full schema scan. Do not use in production environment.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @return InvariantViolation[]|Error[]
|
||||||
|
*/
|
||||||
|
function validate()
|
||||||
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
/**
|
/**
|
||||||
* Validates schema.
|
* Validates schema.
|
||||||
|
@ -333,7 +333,6 @@ class Executor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the root type of the operation from the schema.
|
* Extracts the root type of the operation from the schema.
|
||||||
*
|
*
|
||||||
@ -346,12 +345,19 @@ class Executor
|
|||||||
{
|
{
|
||||||
switch ($operation->operation) {
|
switch ($operation->operation) {
|
||||||
case 'query':
|
case 'query':
|
||||||
return $schema->getQueryType();
|
$queryType = $schema->getQueryType();
|
||||||
|
if (!$queryType) {
|
||||||
|
throw new Error(
|
||||||
|
'Schema does not define the required query root type.',
|
||||||
|
[$operation]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $queryType;
|
||||||
case 'mutation':
|
case 'mutation':
|
||||||
$mutationType = $schema->getMutationType();
|
$mutationType = $schema->getMutationType();
|
||||||
if (!$mutationType) {
|
if (!$mutationType) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Schema is not configured for mutations',
|
'Schema is not configured for mutations.',
|
||||||
[$operation]
|
[$operation]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -360,14 +366,14 @@ class Executor
|
|||||||
$subscriptionType = $schema->getSubscriptionType();
|
$subscriptionType = $schema->getSubscriptionType();
|
||||||
if (!$subscriptionType) {
|
if (!$subscriptionType) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Schema is not configured for subscriptions',
|
'Schema is not configured for subscriptions.',
|
||||||
[ $operation ]
|
[ $operation ]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return $subscriptionType;
|
return $subscriptionType;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Can only execute queries, mutations and subscriptions',
|
'Can only execute queries, mutations and subscriptions.',
|
||||||
[$operation]
|
[$operation]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -472,6 +472,7 @@ class Server
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$schema = $this->getSchema();
|
$schema = $this->getSchema();
|
||||||
|
$schema->assertValid();
|
||||||
} catch (InvariantViolation $e) {
|
} catch (InvariantViolation $e) {
|
||||||
throw new InvariantViolation("Cannot validate, schema contains errors: {$e->getMessage()}", null, $e);
|
throw new InvariantViolation("Cannot validate, schema contains errors: {$e->getMessage()}", null, $e);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ namespace GraphQL\Type\Definition;
|
|||||||
|
|
||||||
use GraphQL\Error\InvariantViolation;
|
use GraphQL\Error\InvariantViolation;
|
||||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,6 +22,11 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
|||||||
*/
|
*/
|
||||||
public $astNode;
|
public $astNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var InterfaceTypeExtensionNode[]
|
||||||
|
*/
|
||||||
|
public $extensionASTNodes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* InterfaceType constructor.
|
* InterfaceType constructor.
|
||||||
* @param array $config
|
* @param array $config
|
||||||
@ -46,6 +52,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
|||||||
$this->name = $config['name'];
|
$this->name = $config['name'];
|
||||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||||
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
|
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
|
||||||
|
$this->extensionASTNodes = isset($config['extensionASTNodes']) ? $config['extensionASTNodes'] : null;
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,13 +152,13 @@ class ObjectType extends Type implements OutputType, CompositeType
|
|||||||
$interfaces = isset($this->config['interfaces']) ? $this->config['interfaces'] : [];
|
$interfaces = isset($this->config['interfaces']) ? $this->config['interfaces'] : [];
|
||||||
$interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces;
|
$interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces;
|
||||||
|
|
||||||
if (!is_array($interfaces)) {
|
if ($interfaces && !is_array($interfaces)) {
|
||||||
throw new InvariantViolation(
|
throw new InvariantViolation(
|
||||||
"{$this->name} interfaces must be an Array or a callable which returns an Array."
|
"{$this->name} interfaces must be an Array or a callable which returns an Array."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->interfaces = $interfaces;
|
$this->interfaces = $interfaces ?: [];
|
||||||
}
|
}
|
||||||
return $this->interfaces;
|
return $this->interfaces;
|
||||||
}
|
}
|
||||||
@ -227,19 +227,5 @@ class ObjectType extends Type implements OutputType, CompositeType
|
|||||||
$arg->assertValid($field, $this);
|
$arg->assertValid($field, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$implemented = [];
|
|
||||||
foreach ($this->getInterfaces() as $iface) {
|
|
||||||
Utils::invariant(
|
|
||||||
$iface instanceof InterfaceType,
|
|
||||||
"{$this->name} may only implement Interface types, it cannot implement %s.",
|
|
||||||
Utils::printSafe($iface)
|
|
||||||
);
|
|
||||||
Utils::invariant(
|
|
||||||
!isset($implemented[$iface->name]),
|
|
||||||
"{$this->name} may declare it implements {$iface->name} only once."
|
|
||||||
);
|
|
||||||
$implemented[$iface->name] = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
namespace GraphQL\Type\Definition;
|
namespace GraphQL\Type\Definition;
|
||||||
|
|
||||||
use GraphQL\Error\InvariantViolation;
|
use GraphQL\Error\InvariantViolation;
|
||||||
|
use GraphQL\Language\AST\ListType;
|
||||||
use GraphQL\Language\AST\NamedType;
|
use GraphQL\Language\AST\NamedType;
|
||||||
use GraphQL\Language\AST\TypeDefinitionNode;
|
use GraphQL\Language\AST\TypeDefinitionNode;
|
||||||
use GraphQL\Type\Introspection;
|
use GraphQL\Type\Introspection;
|
||||||
@ -203,6 +204,25 @@ abstract class Type implements \JsonSerializable
|
|||||||
return $type instanceof AbstractType;
|
return $type instanceof AbstractType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
* @param Type $type
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isType($type)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
$type instanceof ScalarType ||
|
||||||
|
$type instanceof ObjectType ||
|
||||||
|
$type instanceof InterfaceType ||
|
||||||
|
$type instanceof UnionType ||
|
||||||
|
$type instanceof EnumType ||
|
||||||
|
$type instanceof InputObjectType ||
|
||||||
|
$type instanceof ListType ||
|
||||||
|
$type instanceof NonNull
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api
|
* @api
|
||||||
* @param Type $type
|
* @param Type $type
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Type;
|
namespace GraphQL\Type;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Error\InvariantViolation;
|
use GraphQL\Error\InvariantViolation;
|
||||||
use GraphQL\GraphQL;
|
use GraphQL\GraphQL;
|
||||||
use GraphQL\Language\AST\SchemaDefinitionNode;
|
use GraphQL\Language\AST\SchemaDefinitionNode;
|
||||||
use GraphQL\Type\Definition\FieldArgument;
|
|
||||||
use GraphQL\Type\Definition\NonNull;
|
|
||||||
use GraphQL\Type\Definition\AbstractType;
|
use GraphQL\Type\Definition\AbstractType;
|
||||||
use GraphQL\Type\Definition\Directive;
|
use GraphQL\Type\Definition\Directive;
|
||||||
use GraphQL\Type\Definition\InterfaceType;
|
use GraphQL\Type\Definition\InterfaceType;
|
||||||
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\Type\Definition\UnionType;
|
||||||
use GraphQL\Utils\TypeComparators;
|
|
||||||
use GraphQL\Utils\TypeInfo;
|
use GraphQL\Utils\TypeInfo;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
@ -64,6 +62,11 @@ class Schema
|
|||||||
*/
|
*/
|
||||||
private $fullyLoaded = false;
|
private $fullyLoaded = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var InvariantViolation[]|null
|
||||||
|
*/
|
||||||
|
private $validationErrors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema constructor.
|
* Schema constructor.
|
||||||
*
|
*
|
||||||
@ -86,39 +89,52 @@ class Schema
|
|||||||
'subscription' => $subscriptionType
|
'subscription' => $subscriptionType
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_array($config)) {
|
if (is_array($config)) {
|
||||||
$config = SchemaConfig::create($config);
|
$config = SchemaConfig::create($config);
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::invariant(
|
// If this schema was built from a source known to be valid, then it may be
|
||||||
$config instanceof SchemaConfig,
|
// marked with assumeValid to avoid an additional type system validation.
|
||||||
'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s',
|
if ($config->getAssumeValid()) {
|
||||||
implode(', ', [
|
$this->validationErrors = [];
|
||||||
'query',
|
} else {
|
||||||
'mutation',
|
// Otherwise check for common mistakes during construction to produce
|
||||||
'subscription',
|
// clear and early error messages.
|
||||||
'types',
|
Utils::invariant(
|
||||||
'directives',
|
$config instanceof SchemaConfig,
|
||||||
'typeLoader'
|
'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s',
|
||||||
]),
|
implode(', ', [
|
||||||
Utils::getVariableType($config)
|
'query',
|
||||||
);
|
'mutation',
|
||||||
|
'subscription',
|
||||||
Utils::invariant(
|
'types',
|
||||||
$config->query instanceof ObjectType,
|
'directives',
|
||||||
"Schema query must be Object Type but got: " . Utils::getVariableType($config->query)
|
'typeLoader'
|
||||||
);
|
]),
|
||||||
|
Utils::getVariableType($config)
|
||||||
|
);
|
||||||
|
Utils::invariant(
|
||||||
|
!$config->types || is_array($config->types) || is_callable($config->types),
|
||||||
|
"\"types\" must be array or callable if provided but got: " . Utils::getVariableType($config->types)
|
||||||
|
);
|
||||||
|
Utils::invariant(
|
||||||
|
!$config->directives || is_array($config->directives),
|
||||||
|
"\"directives\" must be Array if provided but got: " . Utils::getVariableType($config->directives)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
$this->resolvedTypes[$config->query->name] = $config->query;
|
if ($config->query) {
|
||||||
|
$this->resolvedTypes[$config->query->name] = $config->query;
|
||||||
|
}
|
||||||
if ($config->mutation) {
|
if ($config->mutation) {
|
||||||
$this->resolvedTypes[$config->mutation->name] = $config->mutation;
|
$this->resolvedTypes[$config->mutation->name] = $config->mutation;
|
||||||
}
|
}
|
||||||
if ($config->subscription) {
|
if ($config->subscription) {
|
||||||
$this->resolvedTypes[$config->subscription->name] = $config->subscription;
|
$this->resolvedTypes[$config->subscription->name] = $config->subscription;
|
||||||
}
|
}
|
||||||
if (is_array($this->config->types)) {
|
if ($this->config->types) {
|
||||||
foreach ($this->resolveAdditionalTypes() as $type) {
|
foreach ($this->resolveAdditionalTypes() as $type) {
|
||||||
if (isset($this->resolvedTypes[$type->name])) {
|
if (isset($this->resolvedTypes[$type->name])) {
|
||||||
Utils::invariant(
|
Utils::invariant(
|
||||||
@ -393,6 +409,32 @@ class Schema
|
|||||||
return isset($typeMap[$typeName]) ? $typeMap[$typeName] : null;
|
return isset($typeMap[$typeName]) ? $typeMap[$typeName] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates schema.
|
||||||
|
*
|
||||||
|
* This operation requires full schema scan. Do not use in production environment.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @return InvariantViolation[]|Error[]
|
||||||
|
*/
|
||||||
|
public function validate() {
|
||||||
|
// If this Schema has already been validated, return the previous results.
|
||||||
|
if ($this->validationErrors !== null) {
|
||||||
|
return $this->validationErrors;
|
||||||
|
}
|
||||||
|
// Validate the schema, producing a list of errors.
|
||||||
|
$context = new SchemaValidationContext($this);
|
||||||
|
$context->validateRootTypes();
|
||||||
|
$context->validateDirectives();
|
||||||
|
$context->validateTypes();
|
||||||
|
|
||||||
|
// Persist the results of validation before returning to ensure validation
|
||||||
|
// does not run multiple times for this schema.
|
||||||
|
$this->validationErrors = $context->getErrors();
|
||||||
|
|
||||||
|
return $this->validationErrors;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates schema.
|
* Validates schema.
|
||||||
*
|
*
|
||||||
@ -403,18 +445,13 @@ class Schema
|
|||||||
*/
|
*/
|
||||||
public function assertValid()
|
public function assertValid()
|
||||||
{
|
{
|
||||||
foreach ($this->config->getDirectives() as $index => $directive) {
|
$errors = $this->validate();
|
||||||
Utils::invariant(
|
|
||||||
$directive instanceof Directive,
|
if ($errors) {
|
||||||
"Each entry of \"directives\" option of Schema config must be an instance of %s but entry at position %d is %s.",
|
throw new InvariantViolation(implode("\n\n", $this->validationErrors));
|
||||||
Directive::class,
|
|
||||||
$index,
|
|
||||||
Utils::printSafe($directive)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$internalTypes = Type::getInternalTypes() + Introspection::getTypes();
|
$internalTypes = Type::getInternalTypes() + Introspection::getTypes();
|
||||||
|
|
||||||
foreach ($this->getTypeMap() as $name => $type) {
|
foreach ($this->getTypeMap() as $name => $type) {
|
||||||
if (isset($internalTypes[$name])) {
|
if (isset($internalTypes[$name])) {
|
||||||
continue ;
|
continue ;
|
||||||
@ -422,22 +459,6 @@ class Schema
|
|||||||
|
|
||||||
$type->assertValid();
|
$type->assertValid();
|
||||||
|
|
||||||
if ($type instanceof AbstractType) {
|
|
||||||
$possibleTypes = $this->getPossibleTypes($type);
|
|
||||||
|
|
||||||
Utils::invariant(
|
|
||||||
!empty($possibleTypes),
|
|
||||||
"Could not find possible implementing types for {$type->name} " .
|
|
||||||
'in schema. Check that schema.types is defined and is an array of ' .
|
|
||||||
'all possible types in the schema.'
|
|
||||||
);
|
|
||||||
|
|
||||||
} else if ($type instanceof ObjectType) {
|
|
||||||
foreach ($type->getInterfaces() as $iface) {
|
|
||||||
$this->assertImplementsIntarface($type, $iface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure type loader returns the same instance as registered in other places of schema
|
// Make sure type loader returns the same instance as registered in other places of schema
|
||||||
if ($this->config->typeLoader) {
|
if ($this->config->typeLoader) {
|
||||||
Utils::invariant(
|
Utils::invariant(
|
||||||
@ -448,74 +469,4 @@ class Schema
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertImplementsIntarface(ObjectType $object, InterfaceType $iface)
|
|
||||||
{
|
|
||||||
$objectFieldMap = $object->getFields();
|
|
||||||
$ifaceFieldMap = $iface->getFields();
|
|
||||||
|
|
||||||
// Assert each interface field is implemented.
|
|
||||||
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
|
|
||||||
|
|
||||||
// Assert interface field exists on object.
|
|
||||||
Utils::invariant(
|
|
||||||
isset($objectFieldMap[$fieldName]),
|
|
||||||
"{$iface->name} expects field \"{$fieldName}\" but {$object->name} does not provide it"
|
|
||||||
);
|
|
||||||
|
|
||||||
$objectField = $objectFieldMap[$fieldName];
|
|
||||||
|
|
||||||
// Assert interface field type is satisfied by object field type, by being
|
|
||||||
// a valid subtype. (covariant)
|
|
||||||
Utils::invariant(
|
|
||||||
TypeComparators::isTypeSubTypeOf($this, $objectField->getType(), $ifaceField->getType()),
|
|
||||||
"{$iface->name}.{$fieldName} expects type \"{$ifaceField->getType()}\" " .
|
|
||||||
"but " .
|
|
||||||
"{$object->name}.${fieldName} provides type \"{$objectField->getType()}\""
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert each interface field arg is implemented.
|
|
||||||
foreach ($ifaceField->args as $ifaceArg) {
|
|
||||||
$argName = $ifaceArg->name;
|
|
||||||
|
|
||||||
/** @var FieldArgument $objectArg */
|
|
||||||
$objectArg = Utils::find($objectField->args, function(FieldArgument $arg) use ($argName) {
|
|
||||||
return $arg->name === $argName;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert interface field arg exists on object field.
|
|
||||||
Utils::invariant(
|
|
||||||
$objectArg,
|
|
||||||
"{$iface->name}.{$fieldName} expects argument \"{$argName}\" but ".
|
|
||||||
"{$object->name}.{$fieldName} does not provide it."
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert interface field arg type matches object field arg type.
|
|
||||||
// (invariant)
|
|
||||||
Utils::invariant(
|
|
||||||
TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType()),
|
|
||||||
"{$iface->name}.{$fieldName}({$argName}:) expects type " .
|
|
||||||
"\"{$ifaceArg->getType()->name}\" but " .
|
|
||||||
"{$object->name}.{$fieldName}({$argName}:) provides type " .
|
|
||||||
"\"{$objectArg->getType()->name}\"."
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert additional arguments must not be required.
|
|
||||||
foreach ($objectField->args as $objectArg) {
|
|
||||||
$argName = $objectArg->name;
|
|
||||||
$ifaceArg = Utils::find($ifaceField->args, function(FieldArgument $arg) use ($argName) {
|
|
||||||
return $arg->name === $argName;
|
|
||||||
});
|
|
||||||
if (!$ifaceArg) {
|
|
||||||
Utils::invariant(
|
|
||||||
!($objectArg->getType() instanceof NonNull),
|
|
||||||
"{$object->name}.{$fieldName}({$argName}:) is of required type " .
|
|
||||||
"\"{$objectArg->getType()}\" but is not also provided by the " .
|
|
||||||
"interface {$iface->name}.{$fieldName}."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,11 @@ class SchemaConfig
|
|||||||
*/
|
*/
|
||||||
public $astNode;
|
public $astNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $assumeValid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an array of options to instance of SchemaConfig
|
* Converts an array of options to instance of SchemaConfig
|
||||||
* (or just returns empty config when array is not passed).
|
* (or just returns empty config when array is not passed).
|
||||||
@ -72,47 +77,22 @@ class SchemaConfig
|
|||||||
|
|
||||||
if (!empty($options)) {
|
if (!empty($options)) {
|
||||||
if (isset($options['query'])) {
|
if (isset($options['query'])) {
|
||||||
Utils::invariant(
|
|
||||||
$options['query'] instanceof ObjectType,
|
|
||||||
'Schema query must be Object Type if provided but got: %s',
|
|
||||||
Utils::printSafe($options['query'])
|
|
||||||
);
|
|
||||||
$config->setQuery($options['query']);
|
$config->setQuery($options['query']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($options['mutation'])) {
|
if (isset($options['mutation'])) {
|
||||||
Utils::invariant(
|
|
||||||
$options['mutation'] instanceof ObjectType,
|
|
||||||
'Schema mutation must be Object Type if provided but got: %s',
|
|
||||||
Utils::printSafe($options['mutation'])
|
|
||||||
);
|
|
||||||
$config->setMutation($options['mutation']);
|
$config->setMutation($options['mutation']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($options['subscription'])) {
|
if (isset($options['subscription'])) {
|
||||||
Utils::invariant(
|
|
||||||
$options['subscription'] instanceof ObjectType,
|
|
||||||
'Schema subscription must be Object Type if provided but got: %s',
|
|
||||||
Utils::printSafe($options['subscription'])
|
|
||||||
);
|
|
||||||
$config->setSubscription($options['subscription']);
|
$config->setSubscription($options['subscription']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($options['types'])) {
|
if (isset($options['types'])) {
|
||||||
Utils::invariant(
|
|
||||||
is_array($options['types']) || is_callable($options['types']),
|
|
||||||
'Schema types must be array or callable if provided but got: %s',
|
|
||||||
Utils::printSafe($options['types'])
|
|
||||||
);
|
|
||||||
$config->setTypes($options['types']);
|
$config->setTypes($options['types']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($options['directives'])) {
|
if (isset($options['directives'])) {
|
||||||
Utils::invariant(
|
|
||||||
is_array($options['directives']),
|
|
||||||
'Schema directives must be array if provided but got: %s',
|
|
||||||
Utils::printSafe($options['directives'])
|
|
||||||
);
|
|
||||||
$config->setDirectives($options['directives']);
|
$config->setDirectives($options['directives']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,13 +120,12 @@ class SchemaConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($options['astNode'])) {
|
if (isset($options['astNode'])) {
|
||||||
Utils::invariant(
|
|
||||||
$options['astNode'] instanceof SchemaDefinitionNode,
|
|
||||||
'Schema astNode must be an instance of SchemaDefinitionNode but got: %s',
|
|
||||||
Utils::printSafe($options['typeLoader'])
|
|
||||||
);
|
|
||||||
$config->setAstNode($options['astNode']);
|
$config->setAstNode($options['astNode']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($options['assumeValid'])) {
|
||||||
|
$config->setAssumeValid((bool) $options['assumeValid']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $config;
|
return $config;
|
||||||
@ -175,7 +154,7 @@ class SchemaConfig
|
|||||||
* @param ObjectType $query
|
* @param ObjectType $query
|
||||||
* @return SchemaConfig
|
* @return SchemaConfig
|
||||||
*/
|
*/
|
||||||
public function setQuery(ObjectType $query)
|
public function setQuery($query)
|
||||||
{
|
{
|
||||||
$this->query = $query;
|
$this->query = $query;
|
||||||
return $this;
|
return $this;
|
||||||
@ -186,7 +165,7 @@ class SchemaConfig
|
|||||||
* @param ObjectType $mutation
|
* @param ObjectType $mutation
|
||||||
* @return SchemaConfig
|
* @return SchemaConfig
|
||||||
*/
|
*/
|
||||||
public function setMutation(ObjectType $mutation)
|
public function setMutation($mutation)
|
||||||
{
|
{
|
||||||
$this->mutation = $mutation;
|
$this->mutation = $mutation;
|
||||||
return $this;
|
return $this;
|
||||||
@ -197,7 +176,7 @@ class SchemaConfig
|
|||||||
* @param ObjectType $subscription
|
* @param ObjectType $subscription
|
||||||
* @return SchemaConfig
|
* @return SchemaConfig
|
||||||
*/
|
*/
|
||||||
public function setSubscription(ObjectType $subscription)
|
public function setSubscription($subscription)
|
||||||
{
|
{
|
||||||
$this->subscription = $subscription;
|
$this->subscription = $subscription;
|
||||||
return $this;
|
return $this;
|
||||||
@ -236,6 +215,16 @@ class SchemaConfig
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bool $assumeValid
|
||||||
|
* @return SchemaConfig
|
||||||
|
*/
|
||||||
|
public function setAssumeValid($assumeValid)
|
||||||
|
{
|
||||||
|
$this->assumeValid = $assumeValid;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api
|
* @api
|
||||||
* @return ObjectType
|
* @return ObjectType
|
||||||
@ -289,4 +278,12 @@ class SchemaConfig
|
|||||||
{
|
{
|
||||||
return $this->typeLoader;
|
return $this->typeLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function getAssumeValid()
|
||||||
|
{
|
||||||
|
return $this->assumeValid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
384
src/Type/SchemaValidationContext.php
Normal file
384
src/Type/SchemaValidationContext.php
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Type;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
|
use GraphQL\Error\InvariantViolation;
|
||||||
|
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\NamedTypeNode;
|
||||||
|
use GraphQL\Language\AST\Node;
|
||||||
|
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\ObjectTypeExtensionNode;
|
||||||
|
use GraphQL\Language\AST\SchemaDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\TypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\TypeNode;
|
||||||
|
use GraphQL\Type\Definition\Directive;
|
||||||
|
use GraphQL\Type\Definition\InterfaceType;
|
||||||
|
use GraphQL\Type\Definition\NonNull;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use GraphQL\Utils\TypeComparators;
|
||||||
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class SchemaValidationContext
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $errors = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Schema
|
||||||
|
*/
|
||||||
|
private $schema;
|
||||||
|
|
||||||
|
public function __construct(Schema $schema)
|
||||||
|
{
|
||||||
|
$this->schema = $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Error[]
|
||||||
|
*/
|
||||||
|
public function getErrors() {
|
||||||
|
return $this->errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateRootTypes() {
|
||||||
|
$queryType = $this->schema->getQueryType();
|
||||||
|
if (!$queryType) {
|
||||||
|
$this->reportError(
|
||||||
|
'Query root type must be provided.',
|
||||||
|
$this->schema->getAstNode()
|
||||||
|
);
|
||||||
|
} else if (!$queryType instanceof ObjectType) {
|
||||||
|
$this->reportError(
|
||||||
|
'Query root type must be Object type but got: ' . Utils::getVariableType($queryType) . '.',
|
||||||
|
$this->getOperationTypeNode($queryType, 'query')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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) . '.',
|
||||||
|
$this->getOperationTypeNode($mutationType, 'mutation')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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) . '.',
|
||||||
|
$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
|
||||||
|
*
|
||||||
|
* @return TypeNode|TypeDefinitionNode
|
||||||
|
*/
|
||||||
|
private function getOperationTypeNode($type, $operation)
|
||||||
|
{
|
||||||
|
$astNode = $this->schema->getAstNode();
|
||||||
|
|
||||||
|
$operationTypeNode = null;
|
||||||
|
if ($astNode instanceof SchemaDefinitionNode) {
|
||||||
|
$operationTypeNode = null;
|
||||||
|
|
||||||
|
foreach($astNode->operationTypes as $operationType) {
|
||||||
|
if ($operationType->operation === $operation) {
|
||||||
|
$operationTypeNode = $operationType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $operationTypeNode ? $operationTypeNode->type : ($type ? $type->astNode : 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);
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,9 @@ trigger_error(
|
|||||||
E_USER_DEPRECATED
|
E_USER_DEPRECATED
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use GraphQL\Utils\Utils
|
||||||
|
*/
|
||||||
class Utils extends \GraphQL\Utils\Utils
|
class Utils extends \GraphQL\Utils\Utils
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -266,9 +266,12 @@ class ASTDefinitionBuilder
|
|||||||
|
|
||||||
private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
|
private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
|
||||||
{
|
{
|
||||||
if (isset($def->interfaces)) {
|
if ($def->interfaces) {
|
||||||
|
// Note: While this could make early assertions to get the correctly
|
||||||
|
// typed values, that would throw immediately while type system
|
||||||
|
// validation with validateSchema() will produce more actionable results.
|
||||||
return Utils::map($def->interfaces, function ($iface) {
|
return Utils::map($def->interfaces, function ($iface) {
|
||||||
return $this->buildInterfaceType($iface);
|
return $this->buildType($iface);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -130,20 +130,20 @@ class BuildSchema
|
|||||||
$directives[] = Directive::deprecatedDirective();
|
$directives[] = Directive::deprecatedDirective();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($operationTypes['query'])) {
|
// Note: While this could make early assertions to get the correctly
|
||||||
throw new Error(
|
// typed values below, that would throw immediately while type system
|
||||||
'Must provide schema definition with query type or a type named Query.'
|
// validation with validateSchema() will produce more actionable results.
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$schema = new Schema([
|
$schema = new Schema([
|
||||||
'query' => $defintionBuilder->buildObjectType($operationTypes['query']),
|
'query' => isset($operationTypes['query'])
|
||||||
'mutation' => isset($operationTypes['mutation']) ?
|
? $defintionBuilder->buildType($operationTypes['query'])
|
||||||
$defintionBuilder->buildObjectType($operationTypes['mutation']) :
|
: null,
|
||||||
null,
|
'mutation' => isset($operationTypes['mutation'])
|
||||||
'subscription' => isset($operationTypes['subscription']) ?
|
? $defintionBuilder->buildType($operationTypes['mutation'])
|
||||||
$defintionBuilder->buildObjectType($operationTypes['subscription']) :
|
: null,
|
||||||
null,
|
'subscription' => isset($operationTypes['subscription'])
|
||||||
|
? $defintionBuilder->buildType($operationTypes['subscription'])
|
||||||
|
: null,
|
||||||
'typeLoader' => function ($name) use ($defintionBuilder) {
|
'typeLoader' => function ($name) use ($defintionBuilder) {
|
||||||
return $defintionBuilder->buildType($name);
|
return $defintionBuilder->buildType($name);
|
||||||
},
|
},
|
||||||
|
@ -48,7 +48,7 @@ class TypeComparators
|
|||||||
* @param Type $superType
|
* @param Type $superType
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType)
|
static function isTypeSubTypeOf(Schema $schema, $maybeSubType, $superType)
|
||||||
{
|
{
|
||||||
// Equivalent type is a valid subtype
|
// Equivalent type is a valid subtype
|
||||||
if ($maybeSubType === $superType) {
|
if ($maybeSubType === $superType) {
|
||||||
|
@ -39,8 +39,9 @@ class ServerTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals(500, $server->getUnexpectedErrorStatus());
|
$this->assertEquals(500, $server->getUnexpectedErrorStatus());
|
||||||
$this->assertEquals(DocumentValidator::allRules(), $server->getValidationRules());
|
$this->assertEquals(DocumentValidator::allRules(), $server->getValidationRules());
|
||||||
|
|
||||||
$this->setExpectedException(InvariantViolation::class, 'Schema query must be Object Type but got: NULL');
|
$schema = $server->getSchema();
|
||||||
$server->getSchema();
|
$this->setExpectedException(InvariantViolation::class, 'Query root type must be provided.');
|
||||||
|
$schema->assertValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCannotUseSetQueryTypeAndSetSchema()
|
public function testCannotUseSetQueryTypeAndSetSchema()
|
||||||
@ -328,8 +329,8 @@ class ServerTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertInternalType('array', $errors);
|
$this->assertInternalType('array', $errors);
|
||||||
$this->assertNotEmpty($errors);
|
$this->assertNotEmpty($errors);
|
||||||
|
|
||||||
$this->setExpectedException(InvariantViolation::class, 'Cannot validate, schema contains errors: Schema query must be Object Type but got: NULL');
|
|
||||||
$server = Server::create();
|
$server = Server::create();
|
||||||
|
$this->setExpectedException(InvariantViolation::class, 'Cannot validate, schema contains errors: Query root type must be provided.');
|
||||||
$server->validate($ast);
|
$server->validate($ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -538,15 +539,14 @@ class ServerTest extends \PHPUnit_Framework_TestCase
|
|||||||
{
|
{
|
||||||
$mock = $this->getMockBuilder('GraphQL\Server')
|
$mock = $this->getMockBuilder('GraphQL\Server')
|
||||||
->setMethods(['readInput', 'produceOutput'])
|
->setMethods(['readInput', 'produceOutput'])
|
||||||
->getMock()
|
->getMock();
|
||||||
;
|
|
||||||
|
|
||||||
$mock->method('readInput')
|
$mock->method('readInput')
|
||||||
->will($this->returnValue(json_encode(['query' => '{err}'])));
|
->will($this->returnValue(json_encode(['query' => '{err}'])));
|
||||||
|
|
||||||
$output = null;
|
$output = null;
|
||||||
$mock->method('produceOutput')
|
$mock->method('produceOutput')
|
||||||
->will($this->returnCallback(function($a1, $a2) use (&$output) {
|
->will($this->returnCallback(function ($a1, $a2) use (&$output) {
|
||||||
$output = func_get_args();
|
$output = func_get_args();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -554,17 +554,35 @@ class ServerTest extends \PHPUnit_Framework_TestCase
|
|||||||
$mock->handleRequest();
|
$mock->handleRequest();
|
||||||
|
|
||||||
$this->assertInternalType('array', $output);
|
$this->assertInternalType('array', $output);
|
||||||
$this->assertArraySubset(['errors' => [['message' => 'Unexpected Error']]], $output[0]);
|
$this->assertArraySubset(['errors' => [['message' => 'Schema does not define the required query root type.']]], $output[0]);
|
||||||
$this->assertEquals(500, $output[1]);
|
$this->assertEquals(200, $output[1]);
|
||||||
|
|
||||||
$output = null;
|
$output = null;
|
||||||
$mock->setUnexpectedErrorMessage($newErr = 'Hey! Something went wrong!');
|
$mock->setUnexpectedErrorMessage($newErr = 'Hey! Something went wrong!');
|
||||||
$mock->setUnexpectedErrorStatus(501);
|
$mock->setUnexpectedErrorStatus(501);
|
||||||
|
$mock->method('readInput')
|
||||||
|
->will($this->throwException(new \Exception('test')));
|
||||||
$mock->handleRequest();
|
$mock->handleRequest();
|
||||||
|
|
||||||
$this->assertInternalType('array', $output);
|
$this->assertInternalType('array', $output);
|
||||||
$this->assertEquals(['errors' => [['message' => $newErr]]], $output[0]);
|
$this->assertEquals(['errors' => [['message' => $newErr]]], $output[0]);
|
||||||
$this->assertEquals(501, $output[1]);
|
$this->assertEquals(501, $output[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleRequest2()
|
||||||
|
{
|
||||||
|
$mock = $this->getMockBuilder('GraphQL\Server')
|
||||||
|
->setMethods(['readInput', 'produceOutput'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$mock->method('readInput')
|
||||||
|
->will($this->returnValue(json_encode(['query' => '{err}'])));
|
||||||
|
|
||||||
|
$output = null;
|
||||||
|
$mock->method('produceOutput')
|
||||||
|
->will($this->returnCallback(function ($a1, $a2) use (&$output) {
|
||||||
|
$output = func_get_args();
|
||||||
|
}));
|
||||||
|
|
||||||
$mock->setQueryType(new ObjectType([
|
$mock->setQueryType(new ObjectType([
|
||||||
'name' => 'Query',
|
'name' => 'Query',
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -863,21 +863,6 @@ type Query {
|
|||||||
|
|
||||||
// Describe: Failures
|
// Describe: Failures
|
||||||
|
|
||||||
/**
|
|
||||||
* @it Requires a schema definition or Query type
|
|
||||||
*/
|
|
||||||
public function testRequiresSchemaDefinitionOrQueryType()
|
|
||||||
{
|
|
||||||
$this->setExpectedException('GraphQL\Error\Error', 'Must provide schema definition with query type or a type named Query.');
|
|
||||||
$body = '
|
|
||||||
type Hello {
|
|
||||||
bar: Bar
|
|
||||||
}
|
|
||||||
';
|
|
||||||
$doc = Parser::parse($body);
|
|
||||||
BuildSchema::buildAST($doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @it Allows only a single schema definition
|
* @it Allows only a single schema definition
|
||||||
*/
|
*/
|
||||||
@ -893,25 +878,6 @@ schema {
|
|||||||
query: Hello
|
query: Hello
|
||||||
}
|
}
|
||||||
|
|
||||||
type Hello {
|
|
||||||
bar: Bar
|
|
||||||
}
|
|
||||||
';
|
|
||||||
$doc = Parser::parse($body);
|
|
||||||
BuildSchema::buildAST($doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @it Requires a query type
|
|
||||||
*/
|
|
||||||
public function testRequiresQueryType()
|
|
||||||
{
|
|
||||||
$this->setExpectedException('GraphQL\Error\Error', 'Must provide schema definition with query type or a type named Query.');
|
|
||||||
$body = '
|
|
||||||
schema {
|
|
||||||
mutation: Hello
|
|
||||||
}
|
|
||||||
|
|
||||||
type Hello {
|
type Hello {
|
||||||
bar: Bar
|
bar: Bar
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user