From 06c6c4bd975190cba572d341e8929ea1ed1b9825 Mon Sep 17 00:00:00 2001 From: Daniel Tschinder Date: Mon, 12 Feb 2018 22:41:52 +0100 Subject: [PATCH] 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 --- docs/reference.md | 27 +- src/Executor/Executor.php | 16 +- src/Server.php | 1 + src/Type/Definition/InterfaceType.php | 7 + src/Type/Definition/ObjectType.php | 18 +- src/Type/Definition/Type.php | 20 + src/Type/Schema.php | 189 ++--- src/Type/SchemaConfig.php | 63 +- src/Type/SchemaValidationContext.php | 384 +++++++++ src/Utils.php | 3 + src/Utils/ASTDefinitionBuilder.php | 7 +- src/Utils/BuildSchema.php | 24 +- src/Utils/TypeComparators.php | 2 +- tests/ServerTest.php | 34 +- tests/Type/ValidationTest.php | 1059 +++++++++++++------------ tests/Utils/BuildSchemaTest.php | 34 - 16 files changed, 1136 insertions(+), 752 deletions(-) create mode 100644 src/Type/SchemaValidationContext.php diff --git a/docs/reference.md b/docs/reference.md index 789eb5f..7ee6f21 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -231,6 +231,15 @@ static function isCompositeType($type) static function isAbstractType($type) ``` +```php +/** + * @api + * @param Type $type + * @return bool + */ +static function isType($type) +``` + ```php /** * @api @@ -431,7 +440,7 @@ static function create(array $options = []) * @param ObjectType $query * @return SchemaConfig */ -function setQuery(GraphQL\Type\Definition\ObjectType $query) +function setQuery($query) ``` ```php @@ -440,7 +449,7 @@ function setQuery(GraphQL\Type\Definition\ObjectType $query) * @param ObjectType $mutation * @return SchemaConfig */ -function setMutation(GraphQL\Type\Definition\ObjectType $mutation) +function setMutation($mutation) ``` ```php @@ -449,7 +458,7 @@ function setMutation(GraphQL\Type\Definition\ObjectType $mutation) * @param ObjectType $subscription * @return SchemaConfig */ -function setSubscription(GraphQL\Type\Definition\ObjectType $subscription) +function setSubscription($subscription) ``` ```php @@ -670,6 +679,18 @@ function getDirectives() 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 /** * Validates schema. diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index fab555a..8ce9baa 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -333,7 +333,6 @@ class Executor } } - /** * Extracts the root type of the operation from the schema. * @@ -346,12 +345,19 @@ class Executor { switch ($operation->operation) { 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': $mutationType = $schema->getMutationType(); if (!$mutationType) { throw new Error( - 'Schema is not configured for mutations', + 'Schema is not configured for mutations.', [$operation] ); } @@ -360,14 +366,14 @@ class Executor $subscriptionType = $schema->getSubscriptionType(); if (!$subscriptionType) { throw new Error( - 'Schema is not configured for subscriptions', + 'Schema is not configured for subscriptions.', [ $operation ] ); } return $subscriptionType; default: throw new Error( - 'Can only execute queries, mutations and subscriptions', + 'Can only execute queries, mutations and subscriptions.', [$operation] ); } diff --git a/src/Server.php b/src/Server.php index 3350b1f..16d1f05 100644 --- a/src/Server.php +++ b/src/Server.php @@ -472,6 +472,7 @@ class Server { try { $schema = $this->getSchema(); + $schema->assertValid(); } catch (InvariantViolation $e) { throw new InvariantViolation("Cannot validate, schema contains errors: {$e->getMessage()}", null, $e); } diff --git a/src/Type/Definition/InterfaceType.php b/src/Type/Definition/InterfaceType.php index 9d532ec..a92be48 100644 --- a/src/Type/Definition/InterfaceType.php +++ b/src/Type/Definition/InterfaceType.php @@ -3,6 +3,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\InterfaceTypeDefinitionNode; +use GraphQL\Language\AST\InterfaceTypeExtensionNode; use GraphQL\Utils\Utils; /** @@ -21,6 +22,11 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT */ public $astNode; + /** + * @var InterfaceTypeExtensionNode[] + */ + public $extensionASTNodes; + /** * InterfaceType constructor. * @param array $config @@ -46,6 +52,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT $this->name = $config['name']; $this->description = isset($config['description']) ? $config['description'] : null; $this->astNode = isset($config['astNode']) ? $config['astNode'] : null; + $this->extensionASTNodes = isset($config['extensionASTNodes']) ? $config['extensionASTNodes'] : null; $this->config = $config; } diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index 717942f..5f2d3e1 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -152,13 +152,13 @@ class ObjectType extends Type implements OutputType, CompositeType $interfaces = isset($this->config['interfaces']) ? $this->config['interfaces'] : []; $interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces; - if (!is_array($interfaces)) { + if ($interfaces && !is_array($interfaces)) { throw new InvariantViolation( "{$this->name} interfaces must be an Array or a callable which returns an Array." ); } - $this->interfaces = $interfaces; + $this->interfaces = $interfaces ?: []; } return $this->interfaces; } @@ -227,19 +227,5 @@ class ObjectType extends Type implements OutputType, CompositeType $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; - } } } diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 89bac9b..5afceb8 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -2,6 +2,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Error\InvariantViolation; +use GraphQL\Language\AST\ListType; use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\TypeDefinitionNode; use GraphQL\Type\Introspection; @@ -203,6 +204,25 @@ abstract class Type implements \JsonSerializable 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 * @param Type $type diff --git a/src/Type/Schema.php b/src/Type/Schema.php index b4ec795..b68ef12 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -1,18 +1,16 @@ $subscriptionType ]; } + if (is_array($config)) { $config = SchemaConfig::create($config); } - Utils::invariant( - $config instanceof SchemaConfig, - 'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s', - implode(', ', [ - 'query', - 'mutation', - 'subscription', - 'types', - 'directives', - 'typeLoader' - ]), - Utils::getVariableType($config) - ); - - Utils::invariant( - $config->query instanceof ObjectType, - "Schema query must be Object Type but got: " . Utils::getVariableType($config->query) - ); + // If this schema was built from a source known to be valid, then it may be + // marked with assumeValid to avoid an additional type system validation. + if ($config->getAssumeValid()) { + $this->validationErrors = []; + } else { + // Otherwise check for common mistakes during construction to produce + // clear and early error messages. + Utils::invariant( + $config instanceof SchemaConfig, + 'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s', + implode(', ', [ + 'query', + 'mutation', + 'subscription', + 'types', + 'directives', + '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->resolvedTypes[$config->query->name] = $config->query; - + if ($config->query) { + $this->resolvedTypes[$config->query->name] = $config->query; + } if ($config->mutation) { $this->resolvedTypes[$config->mutation->name] = $config->mutation; } if ($config->subscription) { $this->resolvedTypes[$config->subscription->name] = $config->subscription; } - if (is_array($this->config->types)) { + if ($this->config->types) { foreach ($this->resolveAdditionalTypes() as $type) { if (isset($this->resolvedTypes[$type->name])) { Utils::invariant( @@ -393,6 +409,32 @@ class Schema 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. * @@ -403,18 +445,13 @@ class Schema */ public function assertValid() { - foreach ($this->config->getDirectives() as $index => $directive) { - Utils::invariant( - $directive instanceof Directive, - "Each entry of \"directives\" option of Schema config must be an instance of %s but entry at position %d is %s.", - Directive::class, - $index, - Utils::printSafe($directive) - ); + $errors = $this->validate(); + + if ($errors) { + throw new InvariantViolation(implode("\n\n", $this->validationErrors)); } $internalTypes = Type::getInternalTypes() + Introspection::getTypes(); - foreach ($this->getTypeMap() as $name => $type) { if (isset($internalTypes[$name])) { continue ; @@ -422,22 +459,6 @@ class Schema $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 if ($this->config->typeLoader) { 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}." - ); - } - } - } - } - } } diff --git a/src/Type/SchemaConfig.php b/src/Type/SchemaConfig.php index 2b03c37..5905a94 100644 --- a/src/Type/SchemaConfig.php +++ b/src/Type/SchemaConfig.php @@ -58,6 +58,11 @@ class SchemaConfig */ public $astNode; + /** + * @var bool + */ + public $assumeValid; + /** * Converts an array of options to instance of SchemaConfig * (or just returns empty config when array is not passed). @@ -72,47 +77,22 @@ class SchemaConfig if (!empty($options)) { 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']); } 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']); } 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']); } 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']); } 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']); } @@ -140,13 +120,12 @@ class SchemaConfig } 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']); } + + if (isset($options['assumeValid'])) { + $config->setAssumeValid((bool) $options['assumeValid']); + } } return $config; @@ -175,7 +154,7 @@ class SchemaConfig * @param ObjectType $query * @return SchemaConfig */ - public function setQuery(ObjectType $query) + public function setQuery($query) { $this->query = $query; return $this; @@ -186,7 +165,7 @@ class SchemaConfig * @param ObjectType $mutation * @return SchemaConfig */ - public function setMutation(ObjectType $mutation) + public function setMutation($mutation) { $this->mutation = $mutation; return $this; @@ -197,7 +176,7 @@ class SchemaConfig * @param ObjectType $subscription * @return SchemaConfig */ - public function setSubscription(ObjectType $subscription) + public function setSubscription($subscription) { $this->subscription = $subscription; return $this; @@ -236,6 +215,16 @@ class SchemaConfig return $this; } + /** + * @param bool $assumeValid + * @return SchemaConfig + */ + public function setAssumeValid($assumeValid) + { + $this->assumeValid = $assumeValid; + return $this; + } + /** * @api * @return ObjectType @@ -289,4 +278,12 @@ class SchemaConfig { return $this->typeLoader; } + + /** + * @return bool + */ + public function getAssumeValid() + { + return $this->assumeValid; + } } diff --git a/src/Type/SchemaValidationContext.php b/src/Type/SchemaValidationContext.php new file mode 100644 index 0000000..a0a4312 --- /dev/null +++ b/src/Type/SchemaValidationContext.php @@ -0,0 +1,384 @@ +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); + } +} diff --git a/src/Utils.php b/src/Utils.php index b73186f..feabcf0 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -6,6 +6,9 @@ trigger_error( E_USER_DEPRECATED ); +/** + * @deprecated Use GraphQL\Utils\Utils + */ class Utils extends \GraphQL\Utils\Utils { } diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index d38310b..073e773 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -266,9 +266,12 @@ class ASTDefinitionBuilder 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 $this->buildInterfaceType($iface); + return $this->buildType($iface); }); } return null; diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index 9d764cd..0b8ae31 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -130,20 +130,20 @@ class BuildSchema $directives[] = Directive::deprecatedDirective(); } - if (!isset($operationTypes['query'])) { - throw new Error( - 'Must provide schema definition with query type or a type named Query.' - ); - } + // Note: While this could make early assertions to get the correctly + // typed values below, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. $schema = new Schema([ - 'query' => $defintionBuilder->buildObjectType($operationTypes['query']), - 'mutation' => isset($operationTypes['mutation']) ? - $defintionBuilder->buildObjectType($operationTypes['mutation']) : - null, - 'subscription' => isset($operationTypes['subscription']) ? - $defintionBuilder->buildObjectType($operationTypes['subscription']) : - null, + 'query' => isset($operationTypes['query']) + ? $defintionBuilder->buildType($operationTypes['query']) + : null, + 'mutation' => isset($operationTypes['mutation']) + ? $defintionBuilder->buildType($operationTypes['mutation']) + : null, + 'subscription' => isset($operationTypes['subscription']) + ? $defintionBuilder->buildType($operationTypes['subscription']) + : null, 'typeLoader' => function ($name) use ($defintionBuilder) { return $defintionBuilder->buildType($name); }, diff --git a/src/Utils/TypeComparators.php b/src/Utils/TypeComparators.php index 64639b4..0ddcbaf 100644 --- a/src/Utils/TypeComparators.php +++ b/src/Utils/TypeComparators.php @@ -48,7 +48,7 @@ class TypeComparators * @param Type $superType * @return bool */ - static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType) + static function isTypeSubTypeOf(Schema $schema, $maybeSubType, $superType) { // Equivalent type is a valid subtype if ($maybeSubType === $superType) { diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 03c6895..db7df22 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -39,8 +39,9 @@ class ServerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(500, $server->getUnexpectedErrorStatus()); $this->assertEquals(DocumentValidator::allRules(), $server->getValidationRules()); - $this->setExpectedException(InvariantViolation::class, 'Schema query must be Object Type but got: NULL'); - $server->getSchema(); + $schema = $server->getSchema(); + $this->setExpectedException(InvariantViolation::class, 'Query root type must be provided.'); + $schema->assertValid(); } public function testCannotUseSetQueryTypeAndSetSchema() @@ -328,8 +329,8 @@ class ServerTest extends \PHPUnit_Framework_TestCase $this->assertInternalType('array', $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(); + $this->setExpectedException(InvariantViolation::class, 'Cannot validate, schema contains errors: Query root type must be provided.'); $server->validate($ast); } @@ -538,15 +539,14 @@ class ServerTest extends \PHPUnit_Framework_TestCase { $mock = $this->getMockBuilder('GraphQL\Server') ->setMethods(['readInput', 'produceOutput']) - ->getMock() - ; + ->getMock(); $mock->method('readInput') ->will($this->returnValue(json_encode(['query' => '{err}']))); $output = null; $mock->method('produceOutput') - ->will($this->returnCallback(function($a1, $a2) use (&$output) { + ->will($this->returnCallback(function ($a1, $a2) use (&$output) { $output = func_get_args(); })); @@ -554,17 +554,35 @@ class ServerTest extends \PHPUnit_Framework_TestCase $mock->handleRequest(); $this->assertInternalType('array', $output); - $this->assertArraySubset(['errors' => [['message' => 'Unexpected Error']]], $output[0]); - $this->assertEquals(500, $output[1]); + $this->assertArraySubset(['errors' => [['message' => 'Schema does not define the required query root type.']]], $output[0]); + $this->assertEquals(200, $output[1]); $output = null; $mock->setUnexpectedErrorMessage($newErr = 'Hey! Something went wrong!'); $mock->setUnexpectedErrorStatus(501); + $mock->method('readInput') + ->will($this->throwException(new \Exception('test'))); $mock->handleRequest(); $this->assertInternalType('array', $output); $this->assertEquals(['errors' => [['message' => $newErr]]], $output[0]); $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([ 'name' => 'Query', diff --git a/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index 6d689c6..26626ad 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -1,6 +1,7 @@ getMessage() === $message) { + if ($error instanceof Error) { + $errorLocations = []; + foreach ($error->getLocations() as $location) { + $errorLocations[] = $location->toArray(); + } + $this->assertEquals($locations, $errorLocations ?: null); + } + return; + } + } + + $this->fail( + 'Failed asserting that the array of validation messages contains ' . + 'the message "' . $message . '"' . "\n" . + 'Found the following messages in the array:' . "\n" . + join("\n", array_map(function($error) { return "\"{$error->getMessage()}\""; }, $array)) + ); + } + public function testRejectsTypesWithoutNames() { $this->assertEachCallableThrows([ @@ -213,11 +242,22 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsASchemaWhoseQueryTypeIsAnObjectType() { - // Must not throw: - $schema = new Schema([ - 'query' => $this->SomeObjectType - ]); - $schema->assertValid(); + $schema = BuildSchema::build(' + type Query { + test: String + } + '); + $this->assertEquals([], $schema->validate()); + + $schemaWithDef = BuildSchema::build(' + schema { + query: QueryRoot + } + type QueryRoot { + test: String + } + '); + $this->assertEquals([], $schemaWithDef->validate()); } /** @@ -225,17 +265,32 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsASchemaWhoseQueryAndMutationTypesAreObjectTypes() { - $mutationType = new ObjectType([ - 'name' => 'Mutation', - 'fields' => [ - 'edit' => ['type' => Type::string()] - ] - ]); - $schema = new Schema([ - 'query' => $this->SomeObjectType, - 'mutation' => $mutationType - ]); - $schema->assertValid(); + $schema = BuildSchema::build(' + type Query { + test: String + } + + type Mutation { + test: String + } + '); + $this->assertEquals([], $schema->validate()); + + $schema = BuildSchema::build(' + schema { + query: QueryRoot + mutation: MutationRoot + } + + type QueryRoot { + test: String + } + + type MutationRoot { + test: String + } + '); + $this->assertEquals([], $schema->validate()); } /** @@ -243,17 +298,32 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsASchemaWhoseQueryAndSubscriptionTypesAreObjectTypes() { - $subscriptionType = new ObjectType([ - 'name' => 'Subscription', - 'fields' => [ - 'subscribe' => ['type' => Type::string()] - ] - ]); - $schema = new Schema([ - 'query' => $this->SomeObjectType, - 'subscription' => $subscriptionType - ]); - $schema->assertValid(); + $schema = BuildSchema::build(' + type Query { + test: String + } + + type Subscription { + test: String + } + '); + $this->assertEquals([], $schema->validate()); + + $schema = BuildSchema::build(' + schema { + query: QueryRoot + subscription: SubscriptionRoot + } + + type QueryRoot { + test: String + } + + type SubscriptionRoot { + test: String + } + '); + $this->assertEquals([], $schema->validate()); } /** @@ -261,22 +331,68 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsASchemaWithoutAQueryType() { - $this->setExpectedException(InvariantViolation::class, 'Schema query must be Object Type but got: NULL'); - new Schema([]); + $schema = BuildSchema::build(' + type Mutation { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'Query root type must be provided.' + ); + + + $schemaWithDef = BuildSchema::build(' + schema { + mutation: MutationRoot + } + + type MutationRoot { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schemaWithDef->validate(), + 'Query root type must be provided.', + [['line' => 2, 'column' => 7]] + ); } /** - * @it rejects a Schema whose query type is an input type + * @it rejects a Schema whose query root type is not an Object type */ - public function testRejectsASchemaWhoseQueryTypeIsAnInputType() + public function testRejectsASchemaWhoseQueryTypeIsNotAnObjectType() { - $this->setExpectedException( - InvariantViolation::class, - 'Schema query must be Object Type if provided but got: SomeInputObject' + $schema = BuildSchema::build(' + input Query { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'Query root type must be Object type but got: Query.', + [['line' => 2, 'column' => 7]] + ); + + + $schemaWithDef = BuildSchema::build(' + schema { + query: SomeInputObject + } + + input SomeInputObject { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schemaWithDef->validate(), + 'Query root type must be Object type but got: SomeInputObject.', + [['line' => 3, 'column' => 16]] ); - new Schema([ - 'query' => $this->SomeInputObjectType - ]); } /** @@ -284,14 +400,43 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsASchemaWhoseMutationTypeIsAnInputType() { - $this->setExpectedException( - InvariantViolation::class, - 'Schema mutation must be Object Type if provided but got: SomeInputObject' + $schema = BuildSchema::build(' + type Query { + field: String + } + + input Mutation { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'Mutation root type must be Object type if provided but got: Mutation.', + [['line' => 6, 'column' => 7]] + ); + + + $schemaWithDef = BuildSchema::build(' + schema { + query: Query + mutation: SomeInputObject + } + + type Query { + field: String + } + + input SomeInputObject { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schemaWithDef->validate(), + 'Mutation root type must be Object type if provided but got: SomeInputObject.', + [['line' => 4, 'column' => 19]] ); - new Schema([ - 'query' => $this->SomeObjectType, - 'mutation' => $this->SomeInputObjectType - ]); } /** @@ -299,14 +444,45 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsASchemaWhoseSubscriptionTypeIsAnInputType() { - $this->setExpectedException( - InvariantViolation::class, - 'Schema subscription must be Object Type if provided but got: SomeInputObject' + $schema = BuildSchema::build(' + type Query { + field: String + } + + input Subscription { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'Subscription root type must be Object type if provided but got: Subscription.', + [['line' => 6, 'column' => 7]] ); - new Schema([ - 'query' => $this->SomeObjectType, - 'subscription' => $this->SomeInputObjectType - ]); + + + $schemaWithDef = BuildSchema::build(' + schema { + query: Query + subscription: SomeInputObject + } + + type Query { + field: String + } + + input SomeInputObject { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schemaWithDef->validate(), + 'Subscription root type must be Object type if provided but got: SomeInputObject.', + [['line' => 4, 'column' => 23]] + ); + + } /** @@ -319,12 +495,10 @@ class ValidationTest extends \PHPUnit_Framework_TestCase 'directives' => ['somedirective'] ]); - $this->setExpectedException( - InvariantViolation::class, - 'Each entry of "directives" option of Schema config must be an instance of GraphQL\Type\Definition\Directive but entry at position 0 is "somedirective".' + $this->assertContainsValidationMessage( + $schema->validate(), + 'Expected directive but got: somedirective' ); - - $schema->assertValid(); } // DESCRIBE: Type System: A Schema must contain uniquely named types @@ -774,39 +948,6 @@ class ValidationTest extends \PHPUnit_Framework_TestCase $schema->assertValid(); } - /** - * @it rejects an Object that declare it implements same interface more than once - */ - public function testRejectsAnObjectThatDeclareItImplementsSameInterfaceMoreThanOnce() - { - $NonUniqInterface = new InterfaceType([ - 'name' => 'NonUniqInterface', - 'fields' => ['f' => ['type' => Type::string()]], - ]); - - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => ['f' => ['type' => Type::string()]], - ]); - - $schema = $this->schemaWithFieldType(new ObjectType([ - 'name' => 'SomeObject', - 'interfaces' => function () use ($NonUniqInterface, $AnotherInterface) { - return [$NonUniqInterface, $AnotherInterface, $NonUniqInterface]; - }, - 'fields' => ['f' => ['type' => Type::string()]] - ])); - - $this->setExpectedException( - InvariantViolation::class, - 'SomeObject may declare it implements NonUniqInterface only once.' - ); - - $schema->assertValid(); - } - - // TODO: rejects an Object type with interfaces as a function returning an incorrect type - /** * @it rejects an Object type with interfaces as a function returning an incorrect type */ @@ -1654,52 +1795,6 @@ class ValidationTest extends \PHPUnit_Framework_TestCase $schema->assertValid(); } - - - // DESCRIBE: Type System: Objects can only implement interfaces - - /** - * @it accepts an Object implementing an Interface - */ - public function testAcceptsAnObjectImplementingAnInterface() - { - $AnotherInterfaceType = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => ['f' => ['type' => Type::string()]] - ]); - - $schema = $this->schemaWithObjectImplementingType($AnotherInterfaceType); - $schema->assertValid(); - } - - /** - * @it rejects an Object implementing a non-Interface type - */ - public function testRejectsAnObjectImplementingANonInterfaceType() - { - $notInterfaceTypes = $this->withModifiers([ - $this->SomeScalarType, - $this->SomeEnumType, - $this->SomeObjectType, - $this->SomeUnionType, - $this->SomeInputObjectType, - ]); - foreach ($notInterfaceTypes as $type) { - $schema = $this->schemaWithObjectImplementingType($type); - - try { - $schema->assertValid(); - $this->fail('Exepected exception not thrown for type ' . $type); - } catch (InvariantViolation $e) { - $this->assertEquals( - 'BadObject may only implement Interface types, it cannot implement ' . $type . '.', - $e->getMessage() - ); - } - } - } - - // DESCRIBE: Type System: Unions must represent Object types /** @@ -1991,7 +2086,6 @@ class ValidationTest extends \PHPUnit_Framework_TestCase } } - // DESCRIBE: Objects must adhere to Interface they implement /** @@ -1999,33 +2093,24 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWhichImplementsAnInterface() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()] - ] - ] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } + + interface AnotherInterface { + field(input: String): String + } + + type AnotherObject implements AnotherInterface { + field(input: String): String + } + '); - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()] - ] - ] - ] - ]); - - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); + $this->assertEquals( + [], + $schema->validate() + ); } /** @@ -2033,34 +2118,25 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWhichImplementsAnInterfaceAlongWithMoreFields() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ], - 'anotherfield' => ['type' => Type::string()] - ] - ]); + interface AnotherInterface { + field(input: String): String + } - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); + type AnotherObject implements AnotherInterface { + field(input: String): String + anotherField: String + } + '); + + $this->assertEquals( + [], + $schema->validate() + ); } /** @@ -2068,75 +2144,24 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalOptionalArguments() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - 'anotherInput' => ['type' => Type::string()], - ] - ] - ] - ]); + interface AnotherInterface { + field(input: String): String + } - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); - } + type AnotherObject implements AnotherInterface { + field(input: String, anotherInput: String): String + } + '); - /** - * @it rejects an Object which implements an Interface field along with additional required arguments - */ - public function testRejectsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalRequiredArguments() - { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ] - ] - ]); - - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - 'anotherInput' => ['type' => Type::nonNull(Type::string())], - ] - ] - ] - ]); - - $schema = $this->schemaWithFieldType($AnotherObject); - - $this->setExpectedException( - InvariantViolation::class, - 'AnotherObject.field(anotherInput:) is of required type "String!" but is not also provided by the interface AnotherInterface.field.' + $this->assertEquals( + [], + $schema->validate() ); - - $schema->assertValid(); } /** @@ -2144,33 +2169,26 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectMissingAnInterfaceField() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'anotherfield' => ['type' => Type::string()] - ] - ]); + interface AnotherInterface { + field(input: String): String + } - $schema = $this->schemaWithFieldType($AnotherObject); + type AnotherObject implements AnotherInterface { + anotherField: String + } + '); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface expects field "field" but AnotherObject does not provide it' + $this->assertContainsValidationMessage( + $schema->validate(), + '"AnotherInterface" expects field "field" but ' . + '"AnotherObject" does not provide it.', + [['line' => 7, 'column' => 9], ['line' => 10, 'column' => 7]] ); - $schema->assertValid(); } /** @@ -2178,27 +2196,26 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectWithAnIncorrectlyTypedInterfaceField() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => Type::string()] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => $this->SomeScalarType] - ] - ]); + interface AnotherInterface { + field(input: String): String + } - $schema = $this->schemaWithFieldType($AnotherObject); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field expects type "String" but AnotherObject.field provides type "SomeScalar"' + type AnotherObject implements AnotherInterface { + field(input: String): Int + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects type "String" but ' . + 'AnotherObject.field is type "Int".', + [['line' => 7, 'column' => 31], ['line' => 11, 'column' => 31]] ); - $schema->assertValid(); } /** @@ -2206,43 +2223,29 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectWithADifferentlyTypedInterfaceField() { - $TypeA = new ObjectType([ - 'name' => 'A', - 'fields' => [ - 'foo' => ['type' => Type::string()] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $TypeB = new ObjectType([ - 'name' => 'B', - 'fields' => [ - 'foo' => ['type' => Type::string()] - ] - ]); + type A { foo: String } + type B { foo: String } - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => $TypeA] - ] - ]); + interface AnotherInterface { + field: A + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => $TypeB] - ] - ]); + type AnotherObject implements AnotherInterface { + field: B + } + '); - $schema = $this->schemaWithFieldType($AnotherObject); - - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field expects type "A" but AnotherObject.field provides type "B"' + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects type "A" but ' . + 'AnotherObject.field is type "B".', + [['line' => 10, 'column' => 16], ['line' => 14, 'column' => 16]] ); - - $schema->assertValid(); } /** @@ -2250,27 +2253,24 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWithASubtypedInterfaceFieldForInterface() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => function () use (&$AnotherInterface) { - return [ - 'field' => ['type' => $AnotherInterface] - ]; - } - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => function () use (&$AnotherObject) { - return [ - 'field' => ['type' => $AnotherObject] - ]; - } - ]); + interface AnotherInterface { + field: AnotherInterface + } - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); + type AnotherObject implements AnotherInterface { + field: AnotherObject + } + '); + + $this->assertEquals( + [], + $schema->validate() + ); } /** @@ -2278,23 +2278,30 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWithASubtypedInterfaceFieldForUnion() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => $this->SomeUnionType] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => $this->SomeObjectType] - ] - ]); + type SomeObject { + field: String + } - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); + union SomeUnionType = SomeObject + + interface AnotherInterface { + field: SomeUnionType + } + + type AnotherObject implements AnotherInterface { + field: SomeObject + } + '); + + $this->assertEquals( + [], + $schema->validate() + ); } /** @@ -2302,36 +2309,26 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectMissingAnInterfaceArgument() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - ] - ] - ]); + interface AnotherInterface { + field(input: String): String + } - $schema = $this->schemaWithFieldType($AnotherObject); + type AnotherObject implements AnotherInterface { + field: String + } + '); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field expects argument "input" but AnotherObject.field does not provide it.' + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects argument "input" but ' . + 'AnotherObject.field does not provide it.', + [['line' => 7, 'column' => 15], ['line' => 11, 'column' => 9]] ); - - $schema->assertValid(); } /** @@ -2339,39 +2336,87 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectWithAnIncorrectlyTypedInterfaceArgument() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => $this->SomeScalarType], - ] - ] - ] - ]); + interface AnotherInterface { + field(input: String): String + } - $schema = $this->schemaWithFieldType($AnotherObject); + type AnotherObject implements AnotherInterface { + field(input: Int): String + } + '); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field(input:) expects type "String" but AnotherObject.field(input:) provides type "SomeScalar".' + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field(input:) expects type "String" but ' . + 'AnotherObject.field(input:) is type "Int".', + [['line' => 7, 'column' => 22], ['line' => 11, 'column' => 22]] ); + } - $schema->assertValid(); + /** + * @it rejects an Object with both an incorrectly typed field and argument + */ + public function testRejectsAnObjectWithBothAnIncorrectlyTypedFieldAndArgument() + { + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } + + interface AnotherInterface { + field(input: String): String + } + + type AnotherObject implements AnotherInterface { + field(input: Int): Int + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects type "String" but ' . + 'AnotherObject.field is type "Int".', + [['line' => 7, 'column' => 31], ['line' => 11, 'column' => 28]] + ); + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field(input:) expects type "String" but ' . + 'AnotherObject.field(input:) is type "Int".', + [['line' => 7, 'column' => 22], ['line' => 11, 'column' => 22]] + ); + } + + /** + * @it rejects an Object which implements an Interface field along with additional required arguments + */ + public function testRejectsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalRequiredArguments() + { + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } + + interface AnotherInterface { + field(input: String): String + } + + type AnotherObject implements AnotherInterface { + field(input: String, anotherInput: String!): String + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherObject.field(anotherInput:) is of required type ' . + '"String!" but is not also provided by the interface ' . + 'AnotherInterface.field.', + [['line' => 11, 'column' => 44], ['line' => 7, 'column' => 9]] + ); } /** @@ -2379,23 +2424,24 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWithAnEquivalentlyModifiedInterfaceFieldType() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => Type::nonNull(Type::listOf(Type::string()))] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => Type::nonNull(Type::listOf(Type::string()))] - ] - ]); + interface AnotherInterface { + field: [String]! + } - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); + type AnotherObject implements AnotherInterface { + field: [String]! + } + '); + + $this->assertEquals( + [], + $schema->validate() + ); } /** @@ -2403,29 +2449,26 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectWithANonListInterfaceFieldListType() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => Type::listOf(Type::string())] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => Type::string()] - ] - ]); + interface AnotherInterface { + field: [String] + } - $schema = $this->schemaWithFieldType($AnotherObject); + type AnotherObject implements AnotherInterface { + field: String + } + '); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field expects type "[String]" but AnotherObject.field provides type "String"' + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects type "[String]" but ' . + 'AnotherObject.field is type "String".', + [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]] ); - - $schema->assertValid(); } /** @@ -2433,27 +2476,26 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectWithAListInterfaceFieldNonListType() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => Type::string()] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => Type::listOf(Type::string())] - ] - ]); + interface AnotherInterface { + field: String + } - $schema = $this->schemaWithFieldType($AnotherObject); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field expects type "String" but AnotherObject.field provides type "[String]"' + type AnotherObject implements AnotherInterface { + field: [String] + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects type "String" but ' . + 'AnotherObject.field is type "[String]".', + [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]] ); - $schema->assertValid(); } /** @@ -2461,23 +2503,24 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWithASubsetNonNullInterfaceFieldType() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => Type::string()] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => Type::nonNull(Type::string())] - ] - ]); + interface AnotherInterface { + field: String + } - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); + type AnotherObject implements AnotherInterface { + field: String! + } + '); + + $this->assertEquals( + [], + $schema->validate() + ); } /** @@ -2485,29 +2528,26 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectWithASupersetNullableInterfaceFieldType() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => Type::nonNull(Type::string())] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => Type::string()] - ] - ]); + interface AnotherInterface { + field: String! + } - $schema = $this->schemaWithFieldType($AnotherObject); + type AnotherObject implements AnotherInterface { + field: String + } + '); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field expects type "String!" but AnotherObject.field provides type "String"' + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects type "String!" but ' . + 'AnotherObject.field is type "String".', + [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]] ); - - $schema->assertValid(); } /** @@ -2665,25 +2705,6 @@ class ValidationTest extends \PHPUnit_Framework_TestCase ]); } - private function schemaWithObjectImplementingType($implementedType) - { - $BadObjectType = new ObjectType([ - 'name' => 'BadObject', - 'interfaces' => [$implementedType], - 'fields' => ['f' => ['type' => Type::string()]] - ]); - - return new Schema([ - 'query' => new ObjectType([ - 'name' => 'Query', - 'fields' => [ - 'f' => ['type' => $BadObjectType] - ] - ]), - 'types' => [$BadObjectType] - ]); - } - private function withModifiers($types) { return array_merge( diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index 6c4eaa6..cfd21f7 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -863,21 +863,6 @@ type Query { // 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 */ @@ -893,25 +878,6 @@ schema { 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 { bar: Bar }