mirror of
https://github.com/retailcrm/graphql-php.git
synced 2025-02-06 07:49:24 +03:00
Merge pull request #248 from danez/012
Port 0.12.3 changes from graphql-js
This commit is contained in:
commit
45baa5f185
47
UPGRADE.md
47
UPGRADE.md
@ -1,4 +1,49 @@
|
|||||||
## Upgrade v0.10.x > dev-master
|
## Upgrade v0.11.x > dev-master
|
||||||
|
|
||||||
|
### Breaking: Descriptions in comments are not used as descriptions by default anymore
|
||||||
|
Descriptions now need to be inside Strings or BlockStrings in order to be picked up as
|
||||||
|
description. If you want to keep the old behaviour you can supply the option `commentDescriptions`
|
||||||
|
to BuildSchema::buildAST(), BuildSchema::build() or Printer::doPrint().
|
||||||
|
|
||||||
|
Here is the official way now to define descriptions in the graphQL language:
|
||||||
|
|
||||||
|
Old:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
# Description
|
||||||
|
type Dog {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
New:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
"Description"
|
||||||
|
type Dog {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Long Description
|
||||||
|
"""
|
||||||
|
type Dog {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Breaking: Custom types need to return `Utils::undefined()` or throw on invalid value
|
||||||
|
As null might be a valid value custom types need to return now `Utils::undefined()` or throw an
|
||||||
|
Exception inside `parseLiteral()`, `parseValue()` and `serialize()`.
|
||||||
|
|
||||||
|
Returning null from any of these methods will now be treated as valid result.
|
||||||
|
|
||||||
|
### Breaking: TypeConfigDecorator was removed from BuildSchema
|
||||||
|
TypeConfigDecorator was used as second argument in `BuildSchema::build()` and `BuildSchema::buildAST()` to
|
||||||
|
enable generated schemas with Unions or Interfaces to be used for resolving. This was fixed in a more
|
||||||
|
generalised approach so that the TypeConfigDecorator is not needed anymore and can be removed.
|
||||||
|
|
||||||
|
The concrete Types are now resolved based on the `__typename` field.
|
||||||
|
|
||||||
### Possibly Breaking: AST to array serialization excludes nulls
|
### Possibly Breaking: AST to array serialization excludes nulls
|
||||||
Most users won't be affected. It *may* affect you only if you do your own manipulations
|
Most users won't be affected. It *may* affect you only if you do your own manipulations
|
||||||
|
@ -171,7 +171,7 @@ static function float()
|
|||||||
```php
|
```php
|
||||||
/**
|
/**
|
||||||
* @api
|
* @api
|
||||||
* @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
|
* @param Type|ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
|
||||||
* @return ListOfType
|
* @return ListOfType
|
||||||
*/
|
*/
|
||||||
static function listOf($wrappedType)
|
static function listOf($wrappedType)
|
||||||
@ -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
|
||||||
@ -374,28 +383,28 @@ public $variableValues;
|
|||||||
*/
|
*/
|
||||||
function getFieldSelection($depth = 0)
|
function getFieldSelection($depth = 0)
|
||||||
```
|
```
|
||||||
# GraphQL\Type\Definition\DirectiveLocation
|
# GraphQL\Language\DirectiveLocation
|
||||||
List of available directive locations
|
List of available directive locations
|
||||||
|
|
||||||
**Class Constants:**
|
**Class Constants:**
|
||||||
```php
|
```php
|
||||||
const IFACE = "INTERFACE";
|
|
||||||
const SUBSCRIPTION = "SUBSCRIPTION";
|
|
||||||
const FRAGMENT_SPREAD = "FRAGMENT_SPREAD";
|
|
||||||
const QUERY = "QUERY";
|
const QUERY = "QUERY";
|
||||||
const MUTATION = "MUTATION";
|
const MUTATION = "MUTATION";
|
||||||
|
const SUBSCRIPTION = "SUBSCRIPTION";
|
||||||
|
const FIELD = "FIELD";
|
||||||
const FRAGMENT_DEFINITION = "FRAGMENT_DEFINITION";
|
const FRAGMENT_DEFINITION = "FRAGMENT_DEFINITION";
|
||||||
const INPUT_OBJECT = "INPUT_OBJECT";
|
const FRAGMENT_SPREAD = "FRAGMENT_SPREAD";
|
||||||
const INLINE_FRAGMENT = "INLINE_FRAGMENT";
|
const INLINE_FRAGMENT = "INLINE_FRAGMENT";
|
||||||
const UNION = "UNION";
|
const SCHEMA = "SCHEMA";
|
||||||
const SCALAR = "SCALAR";
|
const SCALAR = "SCALAR";
|
||||||
|
const OBJECT = "OBJECT";
|
||||||
const FIELD_DEFINITION = "FIELD_DEFINITION";
|
const FIELD_DEFINITION = "FIELD_DEFINITION";
|
||||||
const ARGUMENT_DEFINITION = "ARGUMENT_DEFINITION";
|
const ARGUMENT_DEFINITION = "ARGUMENT_DEFINITION";
|
||||||
|
const IFACE = "INTERFACE";
|
||||||
|
const UNION = "UNION";
|
||||||
const ENUM = "ENUM";
|
const ENUM = "ENUM";
|
||||||
const OBJECT = "OBJECT";
|
|
||||||
const ENUM_VALUE = "ENUM_VALUE";
|
const ENUM_VALUE = "ENUM_VALUE";
|
||||||
const FIELD = "FIELD";
|
const INPUT_OBJECT = "INPUT_OBJECT";
|
||||||
const SCHEMA = "SCHEMA";
|
|
||||||
const INPUT_FIELD_DEFINITION = "INPUT_FIELD_DEFINITION";
|
const INPUT_FIELD_DEFINITION = "INPUT_FIELD_DEFINITION";
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -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.
|
||||||
@ -697,10 +718,25 @@ Parses string containing GraphQL query or [type definition](type-system/type-lan
|
|||||||
* in the source that they correspond to. This configuration flag
|
* in the source that they correspond to. This configuration flag
|
||||||
* disables that behavior for performance or testing.)
|
* disables that behavior for performance or testing.)
|
||||||
*
|
*
|
||||||
|
* experimentalFragmentVariables: boolean,
|
||||||
|
* (If enabled, the parser will understand and parse variable definitions
|
||||||
|
* contained in a fragment definition. They'll be represented in the
|
||||||
|
* `variableDefinitions` field of the FragmentDefinitionNode.
|
||||||
|
*
|
||||||
|
* The syntax is identical to normal, query-defined variables. For example:
|
||||||
|
*
|
||||||
|
* fragment A($var: Boolean = false) on T {
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Note: this feature is experimental and may change or be removed in the
|
||||||
|
* future.)
|
||||||
|
*
|
||||||
* @api
|
* @api
|
||||||
* @param Source|string $source
|
* @param Source|string $source
|
||||||
* @param array $options
|
* @param array $options
|
||||||
* @return DocumentNode
|
* @return DocumentNode
|
||||||
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
static function parse($source, array $options = [])
|
static function parse($source, array $options = [])
|
||||||
```
|
```
|
||||||
@ -936,7 +972,12 @@ const UNION_TYPE_DEFINITION = "UnionTypeDefinition";
|
|||||||
const ENUM_TYPE_DEFINITION = "EnumTypeDefinition";
|
const ENUM_TYPE_DEFINITION = "EnumTypeDefinition";
|
||||||
const ENUM_VALUE_DEFINITION = "EnumValueDefinition";
|
const ENUM_VALUE_DEFINITION = "EnumValueDefinition";
|
||||||
const INPUT_OBJECT_TYPE_DEFINITION = "InputObjectTypeDefinition";
|
const INPUT_OBJECT_TYPE_DEFINITION = "InputObjectTypeDefinition";
|
||||||
const TYPE_EXTENSION_DEFINITION = "TypeExtensionDefinition";
|
const SCALAR_TYPE_EXTENSION = "ScalarTypeExtension";
|
||||||
|
const OBJECT_TYPE_EXTENSION = "ObjectTypeExtension";
|
||||||
|
const INTERFACE_TYPE_EXTENSION = "InterfaceTypeExtension";
|
||||||
|
const UNION_TYPE_EXTENSION = "UnionTypeExtension";
|
||||||
|
const ENUM_TYPE_EXTENSION = "EnumTypeExtension";
|
||||||
|
const INPUT_OBJECT_TYPE_EXTENSION = "InputObjectTypeExtension";
|
||||||
const DIRECTIVE_DEFINITION = "DirectiveDefinition";
|
const DIRECTIVE_DEFINITION = "DirectiveDefinition";
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -1319,7 +1360,6 @@ Also it is possible to override warning handler (which is **trigger_error()** by
|
|||||||
|
|
||||||
**Class Constants:**
|
**Class Constants:**
|
||||||
```php
|
```php
|
||||||
const WARNING_NAME = 1;
|
|
||||||
const WARNING_ASSIGN = 2;
|
const WARNING_ASSIGN = 2;
|
||||||
const WARNING_CONFIG = 4;
|
const WARNING_CONFIG = 4;
|
||||||
const WARNING_FULL_SCHEMA_SCAN = 8;
|
const WARNING_FULL_SCHEMA_SCAN = 8;
|
||||||
@ -1352,7 +1392,7 @@ static function setWarningHandler(callable $warningHandler = null)
|
|||||||
* @api
|
* @api
|
||||||
* @param bool|int $suppress
|
* @param bool|int $suppress
|
||||||
*/
|
*/
|
||||||
static function suppress($suppress = false)
|
static function suppress($suppress = true)
|
||||||
```
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
@ -1367,7 +1407,7 @@ static function suppress($suppress = false)
|
|||||||
* @api
|
* @api
|
||||||
* @param bool|int $enable
|
* @param bool|int $enable
|
||||||
*/
|
*/
|
||||||
static function enable($enable = false)
|
static function enable($enable = true)
|
||||||
```
|
```
|
||||||
# GraphQL\Error\ClientAware
|
# GraphQL\Error\ClientAware
|
||||||
This interface is used for [default error formatting](error-handling.md).
|
This interface is used for [default error formatting](error-handling.md).
|
||||||
@ -1697,7 +1737,7 @@ function setPersistentQueryLoader(callable $persistentQueryLoader)
|
|||||||
* @param bool|int $set
|
* @param bool|int $set
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
function setDebug($set = false)
|
function setDebug($set = true)
|
||||||
```
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
@ -1927,13 +1967,19 @@ See [section in docs](type-system/type-language.md) for details.
|
|||||||
* Given that AST it constructs a GraphQL\Type\Schema. The resulting schema
|
* Given that AST it constructs a GraphQL\Type\Schema. The resulting schema
|
||||||
* has no resolve methods, so execution will use default resolvers.
|
* has no resolve methods, so execution will use default resolvers.
|
||||||
*
|
*
|
||||||
|
* Accepts options as a second argument:
|
||||||
|
*
|
||||||
|
* - commentDescriptions:
|
||||||
|
* Provide true to use preceding comments as the description.
|
||||||
|
*
|
||||||
|
*
|
||||||
* @api
|
* @api
|
||||||
* @param DocumentNode $ast
|
* @param DocumentNode $ast
|
||||||
* @param callable $typeConfigDecorator
|
* @param array $options
|
||||||
* @return Schema
|
* @return Schema
|
||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
static function buildAST(GraphQL\Language\AST\DocumentNode $ast, callable $typeConfigDecorator = null)
|
static function buildAST(GraphQL\Language\AST\DocumentNode $ast, array $options = [])
|
||||||
```
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
@ -1943,10 +1989,10 @@ static function buildAST(GraphQL\Language\AST\DocumentNode $ast, callable $typeC
|
|||||||
*
|
*
|
||||||
* @api
|
* @api
|
||||||
* @param DocumentNode|Source|string $source
|
* @param DocumentNode|Source|string $source
|
||||||
* @param callable $typeConfigDecorator
|
* @param array $options
|
||||||
* @return Schema
|
* @return Schema
|
||||||
*/
|
*/
|
||||||
static function build($source, callable $typeConfigDecorator = null)
|
static function build($source, array $options = [])
|
||||||
```
|
```
|
||||||
# GraphQL\Utils\AST
|
# GraphQL\Utils\AST
|
||||||
Various utilities dealing with AST
|
Various utilities dealing with AST
|
||||||
@ -2049,6 +2095,32 @@ static function astFromValue($value, GraphQL\Type\Definition\InputType $type)
|
|||||||
static function valueFromAST($valueNode, GraphQL\Type\Definition\InputType $type, $variables = null)
|
static function valueFromAST($valueNode, GraphQL\Type\Definition\InputType $type, $variables = null)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* Produces a PHP value given a GraphQL Value AST.
|
||||||
|
*
|
||||||
|
* Unlike `valueFromAST()`, no type is provided. The resulting JavaScript value
|
||||||
|
* will reflect the provided GraphQL value AST.
|
||||||
|
*
|
||||||
|
* | GraphQL Value | PHP Value |
|
||||||
|
* | -------------------- | ------------- |
|
||||||
|
* | Input Object | Assoc Array |
|
||||||
|
* | List | Array |
|
||||||
|
* | Boolean | Boolean |
|
||||||
|
* | String | String |
|
||||||
|
* | Int / Float | Int / Float |
|
||||||
|
* | Enum | Mixed |
|
||||||
|
* | Null | null |
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @param Node $valueNode
|
||||||
|
* @param array|null $variables
|
||||||
|
* @return mixed
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
static function valueFromASTUntyped($valueNode, array $variables = null)
|
||||||
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
/**
|
/**
|
||||||
* Returns type definition for given AST Type node
|
* Returns type definition for given AST Type node
|
||||||
@ -2057,7 +2129,7 @@ static function valueFromAST($valueNode, GraphQL\Type\Definition\InputType $type
|
|||||||
* @param Schema $schema
|
* @param Schema $schema
|
||||||
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
|
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
|
||||||
* @return Type
|
* @return Type
|
||||||
* @throws InvariantViolation
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
static function typeFromAST(GraphQL\Type\Schema $schema, $inputTypeNode)
|
static function typeFromAST(GraphQL\Type\Schema $schema, $inputTypeNode)
|
||||||
```
|
```
|
||||||
@ -2079,11 +2151,15 @@ Given an instance of Schema, prints it in GraphQL type language.
|
|||||||
**Class Methods:**
|
**Class Methods:**
|
||||||
```php
|
```php
|
||||||
/**
|
/**
|
||||||
|
* Accepts options as a second argument:
|
||||||
|
*
|
||||||
|
* - commentDescriptions:
|
||||||
|
* Provide true to use preceding comments as the description.
|
||||||
* @api
|
* @api
|
||||||
* @param Schema $schema
|
* @param Schema $schema
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
static function doPrint(GraphQL\Type\Schema $schema)
|
static function doPrint(GraphQL\Type\Schema $schema, array $options = [])
|
||||||
```
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
@ -2092,5 +2168,5 @@ static function doPrint(GraphQL\Type\Schema $schema)
|
|||||||
* @param Schema $schema
|
* @param Schema $schema
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
static function printIntrosepctionSchema(GraphQL\Type\Schema $schema)
|
static function printIntrosepctionSchema(GraphQL\Type\Schema $schema, array $options = [])
|
||||||
```
|
```
|
||||||
|
@ -35,9 +35,9 @@ In **graphql-php** custom directive is an instance of `GraphQL\Type\Definition\D
|
|||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php
|
||||||
|
use GraphQL\Language\DirectiveLocation;
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
use GraphQL\Type\Definition\Directive;
|
use GraphQL\Type\Definition\Directive;
|
||||||
use GraphQL\Type\Definition\DirectiveLocation;
|
|
||||||
use GraphQL\Type\Definition\FieldArgument;
|
use GraphQL\Type\Definition\FieldArgument;
|
||||||
|
|
||||||
$trackDirective = new Directive([
|
$trackDirective = new Directive([
|
||||||
|
@ -33,36 +33,11 @@ $contents = file_get_contents('schema.graphql');
|
|||||||
$schema = BuildSchema::build($contents);
|
$schema = BuildSchema::build($contents);
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, such schema is created without any resolvers. As a result, it doesn't support **Interfaces** and **Unions**
|
By default, such schema is created without any resolvers.
|
||||||
because it is impossible to resolve actual implementations during execution.
|
|
||||||
|
|
||||||
Also, we have to rely on [default field resolver](../data-fetching.md#default-field-resolver) and **root value** in
|
We have to rely on [default field resolver](../data-fetching.md#default-field-resolver) and **root value** in
|
||||||
order to execute a query against this schema.
|
order to execute a query against this schema.
|
||||||
|
|
||||||
# Defining resolvers
|
|
||||||
Since 0.10.0
|
|
||||||
|
|
||||||
In order to enable **Interfaces**, **Unions** and custom field resolvers you can pass the second argument:
|
|
||||||
**type config decorator** to schema builder.
|
|
||||||
|
|
||||||
It accepts default type config produced by the builder and is expected to add missing options like
|
|
||||||
[**resolveType**](interfaces.md#configuration-options) for interface types or
|
|
||||||
[**resolveField**](object-types.md#configuration-options) for object types.
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
use GraphQL\Utils\BuildSchema;
|
|
||||||
|
|
||||||
$typeConfigDecorator = function($typeConfig, $typeDefinitionNode) {
|
|
||||||
$name = $typeConfig['name'];
|
|
||||||
// ... add missing options to $typeConfig based on type $name
|
|
||||||
return $typeConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
$contents = file_get_contents('schema.graphql');
|
|
||||||
$schema = BuildSchema::build($contents, $typeConfigDecorator);
|
|
||||||
```
|
|
||||||
|
|
||||||
# Performance considerations
|
# Performance considerations
|
||||||
Since 0.10.0
|
Since 0.10.0
|
||||||
|
|
||||||
|
@ -42,20 +42,21 @@ class UrlType extends ScalarType
|
|||||||
/**
|
/**
|
||||||
* Parses an externally provided literal value to use as an input (e.g. in Query AST)
|
* Parses an externally provided literal value to use as an input (e.g. in Query AST)
|
||||||
*
|
*
|
||||||
* @param $ast Node
|
* @param Node $valueNode
|
||||||
|
* @param array|null $variables
|
||||||
* @return null|string
|
* @return null|string
|
||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public function parseLiteral($ast)
|
public function parseLiteral($valueNode, array $variables = null)
|
||||||
{
|
{
|
||||||
// Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
|
// Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
|
||||||
// error location in query:
|
// error location in query:
|
||||||
if (!($ast instanceof StringValueNode)) {
|
if (!($valueNode instanceof StringValueNode)) {
|
||||||
throw new Error('Query error: Can only parse strings got: ' . $ast->kind, [$ast]);
|
throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]);
|
||||||
}
|
}
|
||||||
if (!is_string($ast->value) || !filter_var($ast->value, FILTER_VALIDATE_URL)) {
|
if (!is_string($valueNode->value) || !filter_var($valueNode->value, FILTER_VALIDATE_URL)) {
|
||||||
throw new Error('Query error: Not a valid URL', [$ast]);
|
throw new Error('Query error: Not a valid URL', [$valueNode]);
|
||||||
}
|
}
|
||||||
return $ast->value;
|
return $valueNode->value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Error;
|
namespace GraphQL\Error;
|
||||||
|
|
||||||
|
use GraphQL\Language\AST\Node;
|
||||||
use GraphQL\Language\Source;
|
use GraphQL\Language\Source;
|
||||||
use GraphQL\Language\SourceLocation;
|
use GraphQL\Language\SourceLocation;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
@ -52,7 +53,10 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||||||
public $nodes;
|
public $nodes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The source GraphQL document corresponding to this error.
|
* The source GraphQL document for the first location of this error.
|
||||||
|
*
|
||||||
|
* Note that if this Error represents more than one node, the source may not
|
||||||
|
* represent nodes after the first node.
|
||||||
*
|
*
|
||||||
* @var Source|null
|
* @var Source|null
|
||||||
*/
|
*/
|
||||||
@ -73,6 +77,11 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||||||
*/
|
*/
|
||||||
protected $category;
|
protected $category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $extensions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an arbitrary Error, presumably thrown while attempting to execute a
|
* Given an arbitrary Error, presumably thrown while attempting to execute a
|
||||||
* GraphQL operation, produce a new GraphQLError aware of the location in the
|
* GraphQL operation, produce a new GraphQLError aware of the location in the
|
||||||
@ -95,6 +104,7 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||||||
}
|
}
|
||||||
|
|
||||||
$source = $positions = $originalError = null;
|
$source = $positions = $originalError = null;
|
||||||
|
$extensions = [];
|
||||||
|
|
||||||
if ($error instanceof self) {
|
if ($error instanceof self) {
|
||||||
$message = $error->getMessage();
|
$message = $error->getMessage();
|
||||||
@ -102,6 +112,7 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||||||
$nodes = $error->nodes ?: $nodes;
|
$nodes = $error->nodes ?: $nodes;
|
||||||
$source = $error->source;
|
$source = $error->source;
|
||||||
$positions = $error->positions;
|
$positions = $error->positions;
|
||||||
|
$extensions = $error->extensions;
|
||||||
} else if ($error instanceof \Exception || $error instanceof \Throwable) {
|
} else if ($error instanceof \Exception || $error instanceof \Throwable) {
|
||||||
$message = $error->getMessage();
|
$message = $error->getMessage();
|
||||||
$originalError = $error;
|
$originalError = $error;
|
||||||
@ -115,7 +126,8 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||||||
$source,
|
$source,
|
||||||
$positions,
|
$positions,
|
||||||
$path,
|
$path,
|
||||||
$originalError
|
$originalError,
|
||||||
|
$extensions
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,11 +143,12 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $message
|
* @param string $message
|
||||||
* @param array|null $nodes
|
* @param array|Node|null $nodes
|
||||||
* @param Source $source
|
* @param Source $source
|
||||||
* @param array|null $positions
|
* @param array|null $positions
|
||||||
* @param array|null $path
|
* @param array|null $path
|
||||||
* @param \Throwable $previous
|
* @param \Throwable $previous
|
||||||
|
* @param array $extensions
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
$message,
|
$message,
|
||||||
@ -143,19 +156,28 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||||||
Source $source = null,
|
Source $source = null,
|
||||||
$positions = null,
|
$positions = null,
|
||||||
$path = null,
|
$path = null,
|
||||||
$previous = null
|
$previous = null,
|
||||||
|
array $extensions = []
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
parent::__construct($message, 0, $previous);
|
parent::__construct($message, 0, $previous);
|
||||||
|
|
||||||
|
// Compute list of blame nodes.
|
||||||
if ($nodes instanceof \Traversable) {
|
if ($nodes instanceof \Traversable) {
|
||||||
$nodes = iterator_to_array($nodes);
|
$nodes = iterator_to_array($nodes);
|
||||||
|
} else if ($nodes && !is_array($nodes)) {
|
||||||
|
$nodes = [$nodes];
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->nodes = $nodes;
|
$this->nodes = $nodes;
|
||||||
$this->source = $source;
|
$this->source = $source;
|
||||||
$this->positions = $positions;
|
$this->positions = $positions;
|
||||||
$this->path = $path;
|
$this->path = $path;
|
||||||
|
$this->extensions = $extensions ?: (
|
||||||
|
$previous && $previous instanceof self
|
||||||
|
? $previous->extensions
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
if ($previous instanceof ClientAware) {
|
if ($previous instanceof ClientAware) {
|
||||||
$this->isClientSafe = $previous->isClientSafe();
|
$this->isClientSafe = $previous->isClientSafe();
|
||||||
@ -235,11 +257,18 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||||||
if (null === $this->locations) {
|
if (null === $this->locations) {
|
||||||
$positions = $this->getPositions();
|
$positions = $this->getPositions();
|
||||||
$source = $this->getSource();
|
$source = $this->getSource();
|
||||||
|
$nodes = $this->nodes;
|
||||||
|
|
||||||
if ($positions && $source) {
|
if ($positions && $source) {
|
||||||
$this->locations = array_map(function ($pos) use ($source) {
|
$this->locations = array_map(function ($pos) use ($source) {
|
||||||
return $source->getLocation($pos);
|
return $source->getLocation($pos);
|
||||||
}, $positions);
|
}, $positions);
|
||||||
|
} else if ($nodes) {
|
||||||
|
$this->locations = array_filter(array_map(function ($node) {
|
||||||
|
if ($node->loc) {
|
||||||
|
return $node->loc->source->getLocation($node->loc->start);
|
||||||
|
}
|
||||||
|
}, $nodes));
|
||||||
} else {
|
} else {
|
||||||
$this->locations = [];
|
$this->locations = [];
|
||||||
}
|
}
|
||||||
@ -248,6 +277,14 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||||||
return $this->locations;
|
return $this->locations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|Node[]|null
|
||||||
|
*/
|
||||||
|
public function getNodes()
|
||||||
|
{
|
||||||
|
return $this->nodes;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array describing the path from the root value to the field which produced this error.
|
* Returns an array describing the path from the root value to the field which produced this error.
|
||||||
* Only included for execution errors.
|
* Only included for execution errors.
|
||||||
@ -260,6 +297,14 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||||||
return $this->path;
|
return $this->path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getExtensions()
|
||||||
|
{
|
||||||
|
return $this->extensions;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns array representation of error suitable for serialization
|
* Returns array representation of error suitable for serialization
|
||||||
*
|
*
|
||||||
@ -272,6 +317,10 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||||||
'message' => $this->getMessage()
|
'message' => $this->getMessage()
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($this->getExtensions()) {
|
||||||
|
$arr = array_merge($this->getExtensions(), $arr);
|
||||||
|
}
|
||||||
|
|
||||||
$locations = Utils::map($this->getLocations(), function(SourceLocation $loc) {
|
$locations = Utils::map($this->getLocations(), function(SourceLocation $loc) {
|
||||||
return $loc->toSerializableArray();
|
return $loc->toSerializableArray();
|
||||||
});
|
});
|
||||||
@ -297,4 +346,12 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||||||
{
|
{
|
||||||
return $this->toSerializableArray();
|
return $this->toSerializableArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return FormattedError::printError($this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Error;
|
namespace GraphQL\Error;
|
||||||
|
|
||||||
|
use GraphQL\Language\AST\Node;
|
||||||
|
use GraphQL\Language\Source;
|
||||||
use GraphQL\Language\SourceLocation;
|
use GraphQL\Language\SourceLocation;
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
use GraphQL\Type\Definition\WrappingType;
|
use GraphQL\Type\Definition\WrappingType;
|
||||||
@ -27,6 +29,98 @@ class FormattedError
|
|||||||
self::$internalErrorMessage = $msg;
|
self::$internalErrorMessage = $msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints a GraphQLError to a string, representing useful location information
|
||||||
|
* about the error's position in the source.
|
||||||
|
*
|
||||||
|
* @param Error $error
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function printError(Error $error)
|
||||||
|
{
|
||||||
|
$printedLocations = [];
|
||||||
|
if ($error->nodes) {
|
||||||
|
/** @var Node $node */
|
||||||
|
foreach($error->nodes as $node) {
|
||||||
|
if ($node->loc) {
|
||||||
|
$printedLocations[] = self::highlightSourceAtLocation(
|
||||||
|
$node->loc->source,
|
||||||
|
$node->loc->source->getLocation($node->loc->start)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ($error->getSource() && $error->getLocations()) {
|
||||||
|
$source = $error->getSource();
|
||||||
|
foreach($error->getLocations() as $location) {
|
||||||
|
$printedLocations[] = self::highlightSourceAtLocation($source, $location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !$printedLocations
|
||||||
|
? $error->getMessage()
|
||||||
|
: join("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a helpful description of the location of the error in the GraphQL
|
||||||
|
* Source document.
|
||||||
|
*
|
||||||
|
* @param Source $source
|
||||||
|
* @param SourceLocation $location
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function highlightSourceAtLocation(Source $source, SourceLocation $location)
|
||||||
|
{
|
||||||
|
$line = $location->line;
|
||||||
|
$lineOffset = $source->locationOffset->line - 1;
|
||||||
|
$columnOffset = self::getColumnOffset($source, $location);
|
||||||
|
$contextLine = $line + $lineOffset;
|
||||||
|
$contextColumn = $location->column + $columnOffset;
|
||||||
|
$prevLineNum = (string) ($contextLine - 1);
|
||||||
|
$lineNum = (string) $contextLine;
|
||||||
|
$nextLineNum = (string) ($contextLine + 1);
|
||||||
|
$padLen = strlen($nextLineNum);
|
||||||
|
$lines = preg_split('/\r\n|[\n\r]/', $source->body);
|
||||||
|
|
||||||
|
$lines[0] = self::whitespace($source->locationOffset->column - 1) . $lines[0];
|
||||||
|
|
||||||
|
$outputLines = [
|
||||||
|
"{$source->name} ($contextLine:$contextColumn)",
|
||||||
|
$line >= 2 ? (self::lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null,
|
||||||
|
self::lpad($padLen, $lineNum) . ': ' . $lines[$line - 1],
|
||||||
|
self::whitespace(2 + $padLen + $contextColumn - 1) . '^',
|
||||||
|
$line < count($lines)? self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] : null
|
||||||
|
];
|
||||||
|
|
||||||
|
return join("\n", array_filter($outputLines));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Source $source
|
||||||
|
* @param SourceLocation $location
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function getColumnOffset(Source $source, SourceLocation $location)
|
||||||
|
{
|
||||||
|
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $len
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function whitespace($len) {
|
||||||
|
return str_repeat(' ', $len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $len
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function lpad($len, $str) {
|
||||||
|
return self::whitespace($len - mb_strlen($str)) . $str;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard GraphQL error formatter. Converts any exception to array
|
* Standard GraphQL error formatter. Converts any exception to array
|
||||||
* conforming to GraphQL spec.
|
* conforming to GraphQL spec.
|
||||||
@ -66,6 +160,10 @@ class FormattedError
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($e instanceof Error) {
|
if ($e instanceof Error) {
|
||||||
|
if ($e->getExtensions()) {
|
||||||
|
$formattedError = array_merge($e->getExtensions(), $formattedError);
|
||||||
|
}
|
||||||
|
|
||||||
$locations = Utils::map($e->getLocations(), function(SourceLocation $loc) {
|
$locations = Utils::map($e->getLocations(), function(SourceLocation $loc) {
|
||||||
return $loc->toSerializableArray();
|
return $loc->toSerializableArray();
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
namespace GraphQL\Error;
|
namespace GraphQL\Error;
|
||||||
|
|
||||||
use GraphQL\Language\Source;
|
use GraphQL\Language\Source;
|
||||||
use GraphQL\Language\SourceLocation;
|
|
||||||
|
|
||||||
class SyntaxError extends Error
|
class SyntaxError extends Error
|
||||||
{
|
{
|
||||||
@ -13,59 +12,11 @@ class SyntaxError extends Error
|
|||||||
*/
|
*/
|
||||||
public function __construct(Source $source, $position, $description)
|
public function __construct(Source $source, $position, $description)
|
||||||
{
|
{
|
||||||
$location = $source->getLocation($position);
|
parent::__construct(
|
||||||
$line = $location->line + $source->locationOffset->line - 1;
|
"Syntax Error: $description",
|
||||||
$columnOffset = self::getColumnOffset($source, $location);
|
null,
|
||||||
$column = $location->column + $columnOffset;
|
$source,
|
||||||
|
[$position]
|
||||||
$syntaxError =
|
);
|
||||||
"Syntax Error {$source->name} ({$line}:{$column}) $description\n" .
|
|
||||||
"\n".
|
|
||||||
self::highlightSourceAtLocation($source, $location);
|
|
||||||
|
|
||||||
parent::__construct($syntaxError, null, $source, [$position]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Source $source
|
|
||||||
* @param SourceLocation $location
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function highlightSourceAtLocation(Source $source, SourceLocation $location)
|
|
||||||
{
|
|
||||||
$line = $location->line;
|
|
||||||
$lineOffset = $source->locationOffset->line - 1;
|
|
||||||
$columnOffset = self::getColumnOffset($source, $location);
|
|
||||||
|
|
||||||
$contextLine = $line + $lineOffset;
|
|
||||||
$prevLineNum = (string) ($contextLine - 1);
|
|
||||||
$lineNum = (string) $contextLine;
|
|
||||||
$nextLineNum = (string) ($contextLine + 1);
|
|
||||||
$padLen = mb_strlen($nextLineNum, 'UTF-8');
|
|
||||||
|
|
||||||
$unicodeChars = json_decode('"\u2028\u2029"'); // Quick hack to get js-compatible representation of these chars
|
|
||||||
$lines = preg_split('/\r\n|[\n\r' . $unicodeChars . ']/su', $source->body);
|
|
||||||
|
|
||||||
$whitespace = function ($len) {
|
|
||||||
return str_repeat(' ', $len);
|
|
||||||
};
|
|
||||||
|
|
||||||
$lpad = function ($len, $str) {
|
|
||||||
return str_pad($str, $len - mb_strlen($str, 'UTF-8') + 1, ' ', STR_PAD_LEFT);
|
|
||||||
};
|
|
||||||
|
|
||||||
$lines[0] = $whitespace($source->locationOffset->column - 1) . $lines[0];
|
|
||||||
|
|
||||||
return
|
|
||||||
($line >= 2 ? $lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2] . "\n" : '') .
|
|
||||||
($lpad($padLen, $lineNum) . ': ' . $lines[$line - 1] . "\n") .
|
|
||||||
($whitespace(2 + $padLen + $location->column - 1 + $columnOffset) . "^\n") .
|
|
||||||
($line < count($lines) ? $lpad($padLen, $nextLineNum) . ': ' . $lines[$line] . "\n" : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getColumnOffset(Source $source, SourceLocation $location)
|
|
||||||
{
|
|
||||||
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ namespace GraphQL\Error;
|
|||||||
*/
|
*/
|
||||||
final class Warning
|
final class Warning
|
||||||
{
|
{
|
||||||
const WARNING_NAME = 1;
|
|
||||||
const WARNING_ASSIGN = 2;
|
const WARNING_ASSIGN = 2;
|
||||||
const WARNING_CONFIG = 4;
|
const WARNING_CONFIG = 4;
|
||||||
const WARNING_FULL_SCHEMA_SCAN = 8;
|
const WARNING_FULL_SCHEMA_SCAN = 8;
|
||||||
|
@ -100,9 +100,16 @@ class Executor
|
|||||||
{
|
{
|
||||||
// TODO: deprecate (just always use SyncAdapter here) and have `promiseToExecute()` for other cases
|
// TODO: deprecate (just always use SyncAdapter here) and have `promiseToExecute()` for other cases
|
||||||
$promiseAdapter = self::getPromiseAdapter();
|
$promiseAdapter = self::getPromiseAdapter();
|
||||||
|
$result = self::promiseToExecute(
|
||||||
$result = self::promiseToExecute($promiseAdapter, $schema, $ast, $rootValue, $contextValue,
|
$promiseAdapter,
|
||||||
$variableValues, $operationName, $fieldResolver);
|
$schema,
|
||||||
|
$ast,
|
||||||
|
$rootValue,
|
||||||
|
$contextValue,
|
||||||
|
$variableValues,
|
||||||
|
$operationName,
|
||||||
|
$fieldResolver
|
||||||
|
);
|
||||||
|
|
||||||
// Wait for promised results when using sync promises
|
// Wait for promised results when using sync promises
|
||||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||||
@ -140,11 +147,19 @@ class Executor
|
|||||||
callable $fieldResolver = null
|
callable $fieldResolver = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
try {
|
$exeContext = self::buildExecutionContext(
|
||||||
$exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $contextValue, $variableValues,
|
$schema,
|
||||||
$operationName, $fieldResolver, $promiseAdapter);
|
$ast,
|
||||||
} catch (Error $e) {
|
$rootValue,
|
||||||
return $promiseAdapter->createFulfilled(new ExecutionResult(null, [$e]));
|
$contextValue,
|
||||||
|
$variableValues,
|
||||||
|
$operationName,
|
||||||
|
$fieldResolver,
|
||||||
|
$promiseAdapter
|
||||||
|
);
|
||||||
|
|
||||||
|
if (is_array($exeContext)) {
|
||||||
|
return $promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
$executor = new self($exeContext);
|
$executor = new self($exeContext);
|
||||||
@ -159,13 +174,12 @@ class Executor
|
|||||||
* @param DocumentNode $documentNode
|
* @param DocumentNode $documentNode
|
||||||
* @param $rootValue
|
* @param $rootValue
|
||||||
* @param $contextValue
|
* @param $contextValue
|
||||||
* @param $rawVariableValues
|
* @param array|\Traversable $rawVariableValues
|
||||||
* @param string $operationName
|
* @param string $operationName
|
||||||
* @param callable $fieldResolver
|
* @param callable $fieldResolver
|
||||||
* @param PromiseAdapter $promiseAdapter
|
* @param PromiseAdapter $promiseAdapter
|
||||||
*
|
*
|
||||||
* @return ExecutionContext
|
* @return ExecutionContext|Error[]
|
||||||
* @throws Error
|
|
||||||
*/
|
*/
|
||||||
private static function buildExecutionContext(
|
private static function buildExecutionContext(
|
||||||
Schema $schema,
|
Schema $schema,
|
||||||
@ -178,30 +192,17 @@ class Executor
|
|||||||
PromiseAdapter $promiseAdapter = null
|
PromiseAdapter $promiseAdapter = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (null !== $rawVariableValues) {
|
|
||||||
Utils::invariant(
|
|
||||||
is_array($rawVariableValues) || $rawVariableValues instanceof \ArrayAccess,
|
|
||||||
"Variable values are expected to be array or instance of ArrayAccess, got " . Utils::getVariableType($rawVariableValues)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (null !== $operationName) {
|
|
||||||
Utils::invariant(
|
|
||||||
is_string($operationName),
|
|
||||||
"Operation name is supposed to be string, got " . Utils::getVariableType($operationName)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$fragments = [];
|
$fragments = [];
|
||||||
|
/** @var OperationDefinitionNode $operation */
|
||||||
$operation = null;
|
$operation = null;
|
||||||
|
$hasMultipleAssumedOperations = false;
|
||||||
|
|
||||||
foreach ($documentNode->definitions as $definition) {
|
foreach ($documentNode->definitions as $definition) {
|
||||||
switch ($definition->kind) {
|
switch ($definition->kind) {
|
||||||
case NodeKind::OPERATION_DEFINITION:
|
case NodeKind::OPERATION_DEFINITION:
|
||||||
if (!$operationName && $operation) {
|
if (!$operationName && $operation) {
|
||||||
throw new Error(
|
$hasMultipleAssumedOperations = true;
|
||||||
'Must provide operation name if query contains multiple operations.'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (!$operationName ||
|
if (!$operationName ||
|
||||||
(isset($definition->name) && $definition->name->value === $operationName)) {
|
(isset($definition->name) && $definition->name->value === $operationName)) {
|
||||||
@ -211,29 +212,45 @@ class Executor
|
|||||||
case NodeKind::FRAGMENT_DEFINITION:
|
case NodeKind::FRAGMENT_DEFINITION:
|
||||||
$fragments[$definition->name->value] = $definition;
|
$fragments[$definition->name->value] = $definition;
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
"GraphQL cannot execute a request containing a {$definition->kind}.",
|
|
||||||
[$definition]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$operation) {
|
if (!$operation) {
|
||||||
if ($operationName) {
|
if ($operationName) {
|
||||||
throw new Error("Unknown operation named \"$operationName\".");
|
$errors[] = new Error("Unknown operation named \"$operationName\".");
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Must provide an operation.');
|
$errors[] = new Error('Must provide an operation.');
|
||||||
}
|
}
|
||||||
|
} else if ($hasMultipleAssumedOperations) {
|
||||||
|
$errors[] = new Error(
|
||||||
|
'Must provide operation name if query contains multiple operations.'
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$variableValues = Values::getVariableValues(
|
$variableValues = null;
|
||||||
|
if ($operation) {
|
||||||
|
$coercedVariableValues = Values::getVariableValues(
|
||||||
$schema,
|
$schema,
|
||||||
$operation->variableDefinitions ?: [],
|
$operation->variableDefinitions ?: [],
|
||||||
$rawVariableValues ?: []
|
$rawVariableValues ?: []
|
||||||
);
|
);
|
||||||
|
|
||||||
$exeContext = new ExecutionContext(
|
if ($coercedVariableValues['errors']) {
|
||||||
|
$errors = array_merge($errors, $coercedVariableValues['errors']);
|
||||||
|
} else {
|
||||||
|
$variableValues = $coercedVariableValues['coerced'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($errors) {
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils::invariant($operation, 'Has operation if no errors.');
|
||||||
|
Utils::invariant($variableValues !== null, 'Has variables if no errors.');
|
||||||
|
|
||||||
|
return new ExecutionContext(
|
||||||
$schema,
|
$schema,
|
||||||
$fragments,
|
$fragments,
|
||||||
$rootValue,
|
$rootValue,
|
||||||
@ -244,7 +261,6 @@ class Executor
|
|||||||
$fieldResolver ?: self::$defaultFieldResolver,
|
$fieldResolver ?: self::$defaultFieldResolver,
|
||||||
$promiseAdapter ?: self::getPromiseAdapter()
|
$promiseAdapter ?: self::getPromiseAdapter()
|
||||||
);
|
);
|
||||||
return $exeContext;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -338,7 +354,6 @@ class Executor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the root type of the operation from the schema.
|
* Extracts the root type of the operation from the schema.
|
||||||
*
|
*
|
||||||
@ -351,12 +366,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]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -365,14 +387,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]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1053,15 +1075,6 @@ class Executor
|
|||||||
$runtimeType = $returnType->resolveType($result, $exeContext->contextValue, $info);
|
$runtimeType = $returnType->resolveType($result, $exeContext->contextValue, $info);
|
||||||
|
|
||||||
if (null === $runtimeType) {
|
if (null === $runtimeType) {
|
||||||
if ($returnType instanceof InterfaceType && $info->schema->getConfig()->typeLoader) {
|
|
||||||
Warning::warnOnce(
|
|
||||||
"GraphQL Interface Type `{$returnType->name}` returned `null` from it`s `resolveType` function ".
|
|
||||||
'for value: ' . Utils::printSafe($result) . '. Switching to slow resolution method using `isTypeOf` ' .
|
|
||||||
'of all possible implementations. It requires full schema scan and degrades query performance significantly. '.
|
|
||||||
' Make sure your `resolveType` always returns valid implementation or throws.',
|
|
||||||
Warning::WARNING_FULL_SCHEMA_SCAN
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
|
$runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1122,9 +1135,11 @@ class Executor
|
|||||||
|
|
||||||
if (!$runtimeType instanceof ObjectType) {
|
if (!$runtimeType instanceof ObjectType) {
|
||||||
throw new InvariantViolation(
|
throw new InvariantViolation(
|
||||||
"Abstract type {$returnType} must resolve to an Object type at runtime " .
|
"Abstract type {$returnType} must resolve to an Object type at " .
|
||||||
"for field {$info->parentType}.{$info->fieldName} with " .
|
"runtime for field {$info->parentType}.{$info->fieldName} with " .
|
||||||
'value "' . Utils::printSafe($result) . '", received "'. Utils::printSafe($runtimeType) . '".'
|
'value "' . Utils::printSafe($result) . '", received "'. Utils::printSafe($runtimeType) . '".' .
|
||||||
|
'Either the ' . $returnType . ' type should provide a "resolveType" ' .
|
||||||
|
'function or each possible types should provide an "isTypeOf" function.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1194,7 +1209,7 @@ class Executor
|
|||||||
{
|
{
|
||||||
$serializedResult = $returnType->serialize($result);
|
$serializedResult = $returnType->serialize($result);
|
||||||
|
|
||||||
if ($serializedResult === null) {
|
if (Utils::isInvalid($serializedResult)) {
|
||||||
throw new InvariantViolation(
|
throw new InvariantViolation(
|
||||||
'Expected a value of type "'. Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result)
|
'Expected a value of type "'. Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result)
|
||||||
);
|
);
|
||||||
@ -1307,7 +1322,12 @@ class Executor
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* If a resolveType function is not given, then a default resolve behavior is
|
* If a resolveType function is not given, then a default resolve behavior is
|
||||||
* used which tests each possible type for the abstract type by calling
|
* used which attempts two strategies:
|
||||||
|
*
|
||||||
|
* First, See if the provided value has a `__typename` field defined, if so, use
|
||||||
|
* that value as name of the resolved type.
|
||||||
|
*
|
||||||
|
* Otherwise, test each possible type for the abstract type by calling
|
||||||
* isTypeOf for the object being coerced, returning the first type that matches.
|
* isTypeOf for the object being coerced, returning the first type that matches.
|
||||||
*
|
*
|
||||||
* @param $value
|
* @param $value
|
||||||
@ -1318,6 +1338,27 @@ class Executor
|
|||||||
*/
|
*/
|
||||||
private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractType $abstractType)
|
private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractType $abstractType)
|
||||||
{
|
{
|
||||||
|
// First, look for `__typename`.
|
||||||
|
if (
|
||||||
|
$value !== null &&
|
||||||
|
is_array($value) &&
|
||||||
|
isset($value['__typename']) &&
|
||||||
|
is_string($value['__typename'])
|
||||||
|
) {
|
||||||
|
return $value['__typename'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader) {
|
||||||
|
Warning::warnOnce(
|
||||||
|
"GraphQL Interface Type `{$abstractType->name}` returned `null` from it`s `resolveType` function ".
|
||||||
|
'for value: ' . Utils::printSafe($value) . '. Switching to slow resolution method using `isTypeOf` ' .
|
||||||
|
'of all possible implementations. It requires full schema scan and degrades query performance significantly. '.
|
||||||
|
' Make sure your `resolveType` always returns valid implementation or throws.',
|
||||||
|
Warning::WARNING_FULL_SCHEMA_SCAN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, test each possible type.
|
||||||
$possibleTypes = $info->schema->getPossibleTypes($abstractType);
|
$possibleTypes = $info->schema->getPossibleTypes($abstractType);
|
||||||
$promisedIsTypeOfResults = [];
|
$promisedIsTypeOfResults = [];
|
||||||
|
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Executor;
|
namespace GraphQL\Executor;
|
||||||
|
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Error\InvariantViolation;
|
|
||||||
use GraphQL\Language\AST\ArgumentNode;
|
use GraphQL\Language\AST\ArgumentNode;
|
||||||
use GraphQL\Language\AST\DirectiveNode;
|
use GraphQL\Language\AST\DirectiveNode;
|
||||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||||
@ -15,18 +13,20 @@ use GraphQL\Language\AST\NodeList;
|
|||||||
use GraphQL\Language\AST\VariableNode;
|
use GraphQL\Language\AST\VariableNode;
|
||||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||||
use GraphQL\Language\Printer;
|
use GraphQL\Language\Printer;
|
||||||
|
use GraphQL\Type\Definition\EnumType;
|
||||||
|
use GraphQL\Type\Definition\ScalarType;
|
||||||
use GraphQL\Type\Schema;
|
use GraphQL\Type\Schema;
|
||||||
use GraphQL\Type\Definition\Directive;
|
use GraphQL\Type\Definition\Directive;
|
||||||
use GraphQL\Type\Definition\FieldDefinition;
|
use GraphQL\Type\Definition\FieldDefinition;
|
||||||
use GraphQL\Type\Definition\InputObjectType;
|
use GraphQL\Type\Definition\InputObjectType;
|
||||||
use GraphQL\Type\Definition\InputType;
|
use GraphQL\Type\Definition\InputType;
|
||||||
use GraphQL\Type\Definition\LeafType;
|
|
||||||
use GraphQL\Type\Definition\ListOfType;
|
use GraphQL\Type\Definition\ListOfType;
|
||||||
use GraphQL\Type\Definition\NonNull;
|
use GraphQL\Type\Definition\NonNull;
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
use GraphQL\Utils\AST;
|
use GraphQL\Utils\AST;
|
||||||
use GraphQL\Utils\TypeInfo;
|
use GraphQL\Utils\TypeInfo;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
|
use GraphQL\Utils\Value;
|
||||||
use GraphQL\Validator\DocumentValidator;
|
use GraphQL\Validator\DocumentValidator;
|
||||||
|
|
||||||
class Values
|
class Values
|
||||||
@ -37,56 +37,62 @@ class Values
|
|||||||
* to match the variable definitions, a Error will be thrown.
|
* to match the variable definitions, a Error will be thrown.
|
||||||
*
|
*
|
||||||
* @param Schema $schema
|
* @param Schema $schema
|
||||||
* @param VariableDefinitionNode[] $definitionNodes
|
* @param VariableDefinitionNode[] $varDefNodes
|
||||||
* @param array $inputs
|
* @param array $inputs
|
||||||
* @return array
|
* @return array
|
||||||
* @throws Error
|
|
||||||
*/
|
*/
|
||||||
public static function getVariableValues(Schema $schema, $definitionNodes, array $inputs)
|
public static function getVariableValues(Schema $schema, $varDefNodes, array $inputs)
|
||||||
{
|
{
|
||||||
|
$errors = [];
|
||||||
$coercedValues = [];
|
$coercedValues = [];
|
||||||
foreach ($definitionNodes as $definitionNode) {
|
foreach ($varDefNodes as $varDefNode) {
|
||||||
$varName = $definitionNode->variable->name->value;
|
$varName = $varDefNode->variable->name->value;
|
||||||
$varType = TypeInfo::typeFromAST($schema, $definitionNode->type);
|
/** @var InputType|Type $varType */
|
||||||
|
$varType = TypeInfo::typeFromAST($schema, $varDefNode->type);
|
||||||
|
|
||||||
if (!Type::isInputType($varType)) {
|
if (!Type::isInputType($varType)) {
|
||||||
throw new Error(
|
$errors[] = new Error(
|
||||||
'Variable "$'.$varName.'" expected value of type ' .
|
"Variable \"\$$varName\" expected value of type " .
|
||||||
'"' . Printer::doPrint($definitionNode->type) . '" which cannot be used as an input type.',
|
'"' . Printer::doPrint($varDefNode->type) . '" which cannot be used as an input type.',
|
||||||
[$definitionNode->type]
|
[$varDefNode->type]
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (!array_key_exists($varName, $inputs)) {
|
if (!array_key_exists($varName, $inputs)) {
|
||||||
$defaultValue = $definitionNode->defaultValue;
|
|
||||||
if ($defaultValue) {
|
|
||||||
$coercedValues[$varName] = AST::valueFromAST($defaultValue, $varType);
|
|
||||||
}
|
|
||||||
if ($varType instanceof NonNull) {
|
if ($varType instanceof NonNull) {
|
||||||
throw new Error(
|
$errors[] = new Error(
|
||||||
'Variable "$'.$varName .'" of required type ' .
|
"Variable \"\$$varName\" of required type " .
|
||||||
'"'. Utils::printSafe($varType) . '" was not provided.',
|
"\"{$varType}\" was not provided.",
|
||||||
[$definitionNode]
|
[$varDefNode]
|
||||||
);
|
);
|
||||||
|
} else if ($varDefNode->defaultValue) {
|
||||||
|
$coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$value = $inputs[$varName];
|
$value = $inputs[$varName];
|
||||||
$errors = self::isValidPHPValue($value, $varType);
|
$coerced = Value::coerceValue($value, $varType, $varDefNode);
|
||||||
if (!empty($errors)) {
|
/** @var Error[] $coercionErrors */
|
||||||
$message = "\n" . implode("\n", $errors);
|
$coercionErrors = $coerced['errors'];
|
||||||
throw new Error(
|
if ($coercionErrors) {
|
||||||
'Variable "$' . $varName . '" got invalid value ' .
|
$messagePrelude = "Variable \"\$$varName\" got invalid value " . Utils::printSafeJson($value) . '; ';
|
||||||
json_encode($value) . '.' . $message,
|
|
||||||
[$definitionNode]
|
foreach($coercionErrors as $error) {
|
||||||
|
$errors[] = new Error(
|
||||||
|
$messagePrelude . $error->getMessage(),
|
||||||
|
$error->getNodes(),
|
||||||
|
$error->getSource(),
|
||||||
|
$error->getPositions(),
|
||||||
|
$error->getPath(),
|
||||||
|
$error,
|
||||||
|
$error->getExtensions()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
$coercedValue = self::coerceValue($varType, $value);
|
$coercedValues[$varName] = $coerced['value'];
|
||||||
Utils::invariant($coercedValue !== Utils::undefined(), 'Should have reported error.');
|
|
||||||
$coercedValues[$varName] = $coercedValue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $coercedValues;
|
}
|
||||||
|
}
|
||||||
|
return ['errors' => $errors, 'coerced' => $errors ? null : $coercedValues];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,7 +115,6 @@ class Values
|
|||||||
}
|
}
|
||||||
|
|
||||||
$coercedValues = [];
|
$coercedValues = [];
|
||||||
$undefined = Utils::undefined();
|
|
||||||
|
|
||||||
/** @var ArgumentNode[] $argNodeMap */
|
/** @var ArgumentNode[] $argNodeMap */
|
||||||
$argNodeMap = $argNodes ? Utils::keyMap($argNodes, function (ArgumentNode $arg) {
|
$argNodeMap = $argNodes ? Utils::keyMap($argNodes, function (ArgumentNode $arg) {
|
||||||
@ -152,11 +157,12 @@ class Values
|
|||||||
} else {
|
} else {
|
||||||
$valueNode = $argumentNode->value;
|
$valueNode = $argumentNode->value;
|
||||||
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
|
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
|
||||||
if ($coercedValue === $undefined) {
|
if (Utils::isInvalid($coercedValue)) {
|
||||||
$errors = DocumentValidator::isValidLiteralValue($argType, $valueNode);
|
// Note: ValuesOfCorrectType validation should catch this before
|
||||||
$message = !empty($errors) ? ("\n" . implode("\n", $errors)) : '';
|
// execution. This is a runtime check to ensure execution does not
|
||||||
|
// continue with an invalid argument value.
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Argument "' . $name . '" got invalid value ' . Printer::doPrint($valueNode) . '.' . $message,
|
'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.',
|
||||||
[ $argumentNode->value ]
|
[ $argumentNode->value ]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -207,179 +213,16 @@ class Values
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a PHP value and a GraphQL type, determine if the value will be
|
* @deprecated as of 0.12 (Use coerceValue() directly for richer information)
|
||||||
* accepted for that type. This is primarily useful for validating the
|
|
||||||
* runtime values of query variables.
|
|
||||||
*
|
|
||||||
* @param $value
|
* @param $value
|
||||||
* @param InputType $type
|
* @param InputType $type
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function isValidPHPValue($value, InputType $type)
|
public static function isValidPHPValue($value, InputType $type)
|
||||||
{
|
{
|
||||||
// A value must be provided if the type is non-null.
|
$errors = Value::coerceValue($value, $type)['errors'];
|
||||||
if ($type instanceof NonNull) {
|
return $errors
|
||||||
if (null === $value) {
|
? array_map(function(/*\Throwable */$error) { return $error->getMessage(); }, $errors)
|
||||||
return ['Expected "' . Utils::printSafe($type) . '", found null.'];
|
: [];
|
||||||
}
|
|
||||||
return self::isValidPHPValue($value, $type->getWrappedType());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $value) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lists accept a non-list value as a list of one.
|
|
||||||
if ($type instanceof ListOfType) {
|
|
||||||
$itemType = $type->getWrappedType();
|
|
||||||
if (is_array($value)) {
|
|
||||||
$tmp = [];
|
|
||||||
foreach ($value as $index => $item) {
|
|
||||||
$errors = self::isValidPHPValue($item, $itemType);
|
|
||||||
$tmp = array_merge($tmp, Utils::map($errors, function ($error) use ($index) {
|
|
||||||
return "In element #$index: $error";
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return $tmp;
|
|
||||||
}
|
|
||||||
return self::isValidPHPValue($value, $itemType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input objects check each defined field.
|
|
||||||
if ($type instanceof InputObjectType) {
|
|
||||||
if (!is_object($value) && !is_array($value)) {
|
|
||||||
return ["Expected \"{$type->name}\", found not an object."];
|
|
||||||
}
|
|
||||||
$fields = $type->getFields();
|
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
// Ensure every provided field is defined.
|
|
||||||
$props = is_object($value) ? get_object_vars($value) : $value;
|
|
||||||
foreach ($props as $providedField => $tmp) {
|
|
||||||
if (!isset($fields[$providedField])) {
|
|
||||||
$errors[] = "In field \"{$providedField}\": Unknown field.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure every defined field is valid.
|
|
||||||
foreach ($fields as $fieldName => $tmp) {
|
|
||||||
$newErrors = self::isValidPHPValue(isset($value[$fieldName]) ? $value[$fieldName] : null, $fields[$fieldName]->getType());
|
|
||||||
$errors = array_merge(
|
|
||||||
$errors,
|
|
||||||
Utils::map($newErrors, function ($error) use ($fieldName) {
|
|
||||||
return "In field \"{$fieldName}\": {$error}";
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return $errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type instanceof LeafType) {
|
|
||||||
try {
|
|
||||||
// Scalar/Enum input checks to ensure the type can parse the value to
|
|
||||||
// a non-null value.
|
|
||||||
$parseResult = $type->parseValue($value);
|
|
||||||
if (null === $parseResult && !$type->isValidValue($value)) {
|
|
||||||
$v = Utils::printSafeJson($value);
|
|
||||||
return [
|
|
||||||
"Expected type \"{$type->name}\", found $v."
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return [
|
|
||||||
"Expected type \"{$type->name}\", found " . Utils::printSafeJson($value) . ': ' .
|
|
||||||
$e->getMessage()
|
|
||||||
];
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return [
|
|
||||||
"Expected type \"{$type->name}\", found " . Utils::printSafeJson($value) . ': ' .
|
|
||||||
$e->getMessage()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvariantViolation('Must be input type');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a type and any value, return a runtime value coerced to match the type.
|
|
||||||
*/
|
|
||||||
private static function coerceValue(Type $type, $value)
|
|
||||||
{
|
|
||||||
$undefined = Utils::undefined();
|
|
||||||
if ($value === $undefined) {
|
|
||||||
return $undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type instanceof NonNull) {
|
|
||||||
if ($value === null) {
|
|
||||||
// Intentionally return no value.
|
|
||||||
return $undefined;
|
|
||||||
}
|
|
||||||
return self::coerceValue($type->getWrappedType(), $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type instanceof ListOfType) {
|
|
||||||
$itemType = $type->getWrappedType();
|
|
||||||
if (is_array($value) || $value instanceof \Traversable) {
|
|
||||||
$coercedValues = [];
|
|
||||||
foreach ($value as $item) {
|
|
||||||
$itemValue = self::coerceValue($itemType, $item);
|
|
||||||
if ($undefined === $itemValue) {
|
|
||||||
// Intentionally return no value.
|
|
||||||
return $undefined;
|
|
||||||
}
|
|
||||||
$coercedValues[] = $itemValue;
|
|
||||||
}
|
|
||||||
return $coercedValues;
|
|
||||||
} else {
|
|
||||||
$coercedValue = self::coerceValue($itemType, $value);
|
|
||||||
if ($coercedValue === $undefined) {
|
|
||||||
// Intentionally return no value.
|
|
||||||
return $undefined;
|
|
||||||
}
|
|
||||||
return [$coercedValue];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type instanceof InputObjectType) {
|
|
||||||
$coercedObj = [];
|
|
||||||
$fields = $type->getFields();
|
|
||||||
foreach ($fields as $fieldName => $field) {
|
|
||||||
if (!array_key_exists($fieldName, $value)) {
|
|
||||||
if ($field->defaultValueExists()) {
|
|
||||||
$coercedObj[$fieldName] = $field->defaultValue;
|
|
||||||
} else if ($field->getType() instanceof NonNull) {
|
|
||||||
// Intentionally return no value.
|
|
||||||
return $undefined;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$fieldValue = self::coerceValue($field->getType(), $value[$fieldName]);
|
|
||||||
if ($fieldValue === $undefined) {
|
|
||||||
// Intentionally return no value.
|
|
||||||
return $undefined;
|
|
||||||
}
|
|
||||||
$coercedObj[$fieldName] = $fieldValue;
|
|
||||||
}
|
|
||||||
return $coercedObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type instanceof LeafType) {
|
|
||||||
$parsed = $type->parseValue($value);
|
|
||||||
if (null === $parsed) {
|
|
||||||
// null or invalid values represent a failure to parse correctly,
|
|
||||||
// in which case no value is returned.
|
|
||||||
return $undefined;
|
|
||||||
}
|
|
||||||
return $parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvariantViolation('Must be input type');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@ namespace GraphQL\Language\AST;
|
|||||||
interface DefinitionNode
|
interface DefinitionNode
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* export type DefinitionNode = OperationDefinitionNode
|
* export type DefinitionNode =
|
||||||
* | FragmentDefinitionNode
|
* | ExecutableDefinitionNode
|
||||||
* | TypeSystemDefinitionNode // experimental non-spec addition.
|
* | TypeSystemDefinitionNode; // experimental non-spec addition.
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
@ -22,4 +22,9 @@ class DirectiveDefinitionNode extends Node implements TypeSystemDefinitionNode
|
|||||||
* @var NameNode[]
|
* @var NameNode[]
|
||||||
*/
|
*/
|
||||||
public $locations;
|
public $locations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var StringValueNode|null
|
||||||
|
*/
|
||||||
|
public $description;
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,12 @@ class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode
|
|||||||
public $directives;
|
public $directives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var EnumValueDefinitionNode[]
|
* @var EnumValueDefinitionNode[]|null|NodeList
|
||||||
*/
|
*/
|
||||||
public $values;
|
public $values;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var StringValueNode|null
|
||||||
*/
|
*/
|
||||||
public $description;
|
public $description;
|
||||||
}
|
}
|
||||||
|
25
src/Language/AST/EnumTypeExtensionNode.php
Normal file
25
src/Language/AST/EnumTypeExtensionNode.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Language\AST;
|
||||||
|
|
||||||
|
class EnumTypeExtensionNode extends Node implements TypeExtensionNode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $kind = NodeKind::ENUM_TYPE_EXTENSION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var NameNode
|
||||||
|
*/
|
||||||
|
public $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DirectiveNode[]|null
|
||||||
|
*/
|
||||||
|
public $directives;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var EnumValueDefinitionNode[]|null
|
||||||
|
*/
|
||||||
|
public $values;
|
||||||
|
}
|
@ -19,7 +19,7 @@ class EnumValueDefinitionNode extends Node
|
|||||||
public $directives;
|
public $directives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var StringValueNode|null
|
||||||
*/
|
*/
|
||||||
public $description;
|
public $description;
|
||||||
}
|
}
|
||||||
|
11
src/Language/AST/ExecutableDefinitionNode.php
Normal file
11
src/Language/AST/ExecutableDefinitionNode.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Language\AST;
|
||||||
|
|
||||||
|
interface ExecutableDefinitionNode extends DefinitionNode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* export type ExecutableDefinitionNode =
|
||||||
|
* | OperationDefinitionNode
|
||||||
|
* | FragmentDefinitionNode;
|
||||||
|
*/
|
||||||
|
}
|
@ -14,7 +14,7 @@ class FieldDefinitionNode extends Node
|
|||||||
public $name;
|
public $name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var InputValueDefinitionNode[]
|
* @var InputValueDefinitionNode[]|NodeList
|
||||||
*/
|
*/
|
||||||
public $arguments;
|
public $arguments;
|
||||||
|
|
||||||
@ -24,12 +24,12 @@ class FieldDefinitionNode extends Node
|
|||||||
public $type;
|
public $type;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DirectiveNode[]
|
* @var DirectiveNode[]|NodeList
|
||||||
*/
|
*/
|
||||||
public $directives;
|
public $directives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var StringValueNode|null
|
||||||
*/
|
*/
|
||||||
public $description;
|
public $description;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Language\AST;
|
namespace GraphQL\Language\AST;
|
||||||
|
|
||||||
class FragmentDefinitionNode extends Node implements DefinitionNode, HasSelectionSet
|
class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
|
||||||
{
|
{
|
||||||
public $kind = NodeKind::FRAGMENT_DEFINITION;
|
public $kind = NodeKind::FRAGMENT_DEFINITION;
|
||||||
|
|
||||||
@ -10,13 +10,21 @@ class FragmentDefinitionNode extends Node implements DefinitionNode, HasSelectio
|
|||||||
*/
|
*/
|
||||||
public $name;
|
public $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: fragment variable definitions are experimental and may be changed
|
||||||
|
* or removed in the future.
|
||||||
|
*
|
||||||
|
* @var VariableDefinitionNode[]|NodeList
|
||||||
|
*/
|
||||||
|
public $variableDefinitions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var NamedTypeNode
|
* @var NamedTypeNode
|
||||||
*/
|
*/
|
||||||
public $typeCondition;
|
public $typeCondition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DirectiveNode[]
|
* @var DirectiveNode[]|NodeList
|
||||||
*/
|
*/
|
||||||
public $directives;
|
public $directives;
|
||||||
|
|
||||||
|
@ -14,17 +14,17 @@ class InputObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
|
|||||||
public $name;
|
public $name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DirectiveNode[]
|
* @var DirectiveNode[]|null
|
||||||
*/
|
*/
|
||||||
public $directives;
|
public $directives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var InputValueDefinitionNode[]
|
* @var InputValueDefinitionNode[]|null
|
||||||
*/
|
*/
|
||||||
public $fields;
|
public $fields;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var StringValueNode|null
|
||||||
*/
|
*/
|
||||||
public $description;
|
public $description;
|
||||||
}
|
}
|
||||||
|
25
src/Language/AST/InputObjectTypeExtensionNode.php
Normal file
25
src/Language/AST/InputObjectTypeExtensionNode.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Language\AST;
|
||||||
|
|
||||||
|
class InputObjectTypeExtensionNode extends Node implements TypeExtensionNode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $kind = NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var NameNode
|
||||||
|
*/
|
||||||
|
public $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DirectiveNode[]|null
|
||||||
|
*/
|
||||||
|
public $directives;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var InputValueDefinitionNode[]|null
|
||||||
|
*/
|
||||||
|
public $fields;
|
||||||
|
}
|
@ -29,7 +29,7 @@ class InputValueDefinitionNode extends Node
|
|||||||
public $directives;
|
public $directives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var StringValueNode|null
|
||||||
*/
|
*/
|
||||||
public $description;
|
public $description;
|
||||||
}
|
}
|
||||||
|
@ -14,17 +14,17 @@ class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode
|
|||||||
public $name;
|
public $name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DirectiveNode[]
|
* @var DirectiveNode[]|null
|
||||||
*/
|
*/
|
||||||
public $directives;
|
public $directives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var FieldDefinitionNode[]
|
* @var FieldDefinitionNode[]|null
|
||||||
*/
|
*/
|
||||||
public $fields = [];
|
public $fields;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var StringValueNode|null
|
||||||
*/
|
*/
|
||||||
public $description;
|
public $description;
|
||||||
}
|
}
|
||||||
|
25
src/Language/AST/InterfaceTypeExtensionNode.php
Normal file
25
src/Language/AST/InterfaceTypeExtensionNode.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Language\AST;
|
||||||
|
|
||||||
|
class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $kind = NodeKind::INTERFACE_TYPE_EXTENSION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var NameNode
|
||||||
|
*/
|
||||||
|
public $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DirectiveNode[]|null
|
||||||
|
*/
|
||||||
|
public $directives;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var FieldDefinitionNode[]|null
|
||||||
|
*/
|
||||||
|
public $fields;
|
||||||
|
}
|
@ -7,7 +7,7 @@ class ListValueNode extends Node implements ValueNode
|
|||||||
public $kind = NodeKind::LST;
|
public $kind = NodeKind::LST;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ValueNode[]
|
* @var ValueNode[]|NodeList
|
||||||
*/
|
*/
|
||||||
public $values;
|
public $values;
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,12 @@ class NodeKind
|
|||||||
|
|
||||||
// Type Extensions
|
// Type Extensions
|
||||||
|
|
||||||
const TYPE_EXTENSION_DEFINITION = 'TypeExtensionDefinition';
|
const SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension';
|
||||||
|
const OBJECT_TYPE_EXTENSION = 'ObjectTypeExtension';
|
||||||
|
const INTERFACE_TYPE_EXTENSION = 'InterfaceTypeExtension';
|
||||||
|
const UNION_TYPE_EXTENSION = 'UnionTypeExtension';
|
||||||
|
const ENUM_TYPE_EXTENSION = 'EnumTypeExtension';
|
||||||
|
const INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension';
|
||||||
|
|
||||||
// Directive Definitions
|
// Directive Definitions
|
||||||
|
|
||||||
@ -127,7 +132,12 @@ class NodeKind
|
|||||||
NodeKind::INPUT_OBJECT_TYPE_DEFINITION =>InputObjectTypeDefinitionNode::class,
|
NodeKind::INPUT_OBJECT_TYPE_DEFINITION =>InputObjectTypeDefinitionNode::class,
|
||||||
|
|
||||||
// Type Extensions
|
// Type Extensions
|
||||||
NodeKind::TYPE_EXTENSION_DEFINITION => TypeExtensionDefinitionNode::class,
|
NodeKind::SCALAR_TYPE_EXTENSION => ScalarTypeExtensionNode::class,
|
||||||
|
NodeKind::OBJECT_TYPE_EXTENSION => ObjectTypeExtensionNode::class,
|
||||||
|
NodeKind::INTERFACE_TYPE_EXTENSION => InterfaceTypeExtensionNode::class,
|
||||||
|
NodeKind::UNION_TYPE_EXTENSION => UnionTypeExtensionNode::class,
|
||||||
|
NodeKind::ENUM_TYPE_EXTENSION => EnumTypeExtensionNode::class,
|
||||||
|
NodeKind::INPUT_OBJECT_TYPE_EXTENSION => InputObjectTypeExtensionNode::class,
|
||||||
|
|
||||||
// Directive Definitions
|
// Directive Definitions
|
||||||
NodeKind::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class
|
NodeKind::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class
|
||||||
|
@ -19,17 +19,17 @@ class ObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
|
|||||||
public $interfaces = [];
|
public $interfaces = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DirectiveNode[]
|
* @var DirectiveNode[]|null
|
||||||
*/
|
*/
|
||||||
public $directives;
|
public $directives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var FieldDefinitionNode[]
|
* @var FieldDefinitionNode[]|null
|
||||||
*/
|
*/
|
||||||
public $fields;
|
public $fields;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var StringValueNode|null
|
||||||
*/
|
*/
|
||||||
public $description;
|
public $description;
|
||||||
}
|
}
|
||||||
|
30
src/Language/AST/ObjectTypeExtensionNode.php
Normal file
30
src/Language/AST/ObjectTypeExtensionNode.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Language\AST;
|
||||||
|
|
||||||
|
class ObjectTypeExtensionNode extends Node implements TypeExtensionNode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $kind = NodeKind::OBJECT_TYPE_EXTENSION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var NameNode
|
||||||
|
*/
|
||||||
|
public $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var NamedTypeNode[]
|
||||||
|
*/
|
||||||
|
public $interfaces = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DirectiveNode[]
|
||||||
|
*/
|
||||||
|
public $directives;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var FieldDefinitionNode[]
|
||||||
|
*/
|
||||||
|
public $fields;
|
||||||
|
}
|
@ -6,7 +6,7 @@ class ObjectValueNode extends Node implements ValueNode
|
|||||||
public $kind = NodeKind::OBJECT;
|
public $kind = NodeKind::OBJECT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ObjectFieldNode[]
|
* @var ObjectFieldNode[]|NodeList
|
||||||
*/
|
*/
|
||||||
public $fields;
|
public $fields;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Language\AST;
|
namespace GraphQL\Language\AST;
|
||||||
|
|
||||||
class OperationDefinitionNode extends Node implements DefinitionNode, HasSelectionSet
|
class OperationDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
|
@ -19,7 +19,7 @@ class ScalarTypeDefinitionNode extends Node implements TypeDefinitionNode
|
|||||||
public $directives;
|
public $directives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var StringValueNode|null
|
||||||
*/
|
*/
|
||||||
public $description;
|
public $description;
|
||||||
}
|
}
|
||||||
|
20
src/Language/AST/ScalarTypeExtensionNode.php
Normal file
20
src/Language/AST/ScalarTypeExtensionNode.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Language\AST;
|
||||||
|
|
||||||
|
class ScalarTypeExtensionNode extends Node implements TypeExtensionNode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $kind = NodeKind::SCALAR_TYPE_EXTENSION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var NameNode
|
||||||
|
*/
|
||||||
|
public $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DirectiveNode[]|null
|
||||||
|
*/
|
||||||
|
public $directives;
|
||||||
|
}
|
@ -9,4 +9,9 @@ class StringValueNode extends Node implements ValueNode
|
|||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
public $value;
|
public $value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var boolean|null
|
||||||
|
*/
|
||||||
|
public $block;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace GraphQL\Language\AST;
|
|
||||||
|
|
||||||
class TypeExtensionDefinitionNode extends Node implements TypeSystemDefinitionNode
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public $kind = NodeKind::TYPE_EXTENSION_DEFINITION;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var ObjectTypeDefinitionNode
|
|
||||||
*/
|
|
||||||
public $definition;
|
|
||||||
}
|
|
15
src/Language/AST/TypeExtensionNode.php
Normal file
15
src/Language/AST/TypeExtensionNode.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Language\AST;
|
||||||
|
|
||||||
|
interface TypeExtensionNode extends TypeSystemDefinitionNode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
export type TypeExtensionNode =
|
||||||
|
| ScalarTypeExtensionNode
|
||||||
|
| ObjectTypeExtensionNode
|
||||||
|
| InterfaceTypeExtensionNode
|
||||||
|
| UnionTypeExtensionNode
|
||||||
|
| EnumTypeExtensionNode
|
||||||
|
| InputObjectTypeExtensionNode;
|
||||||
|
*/
|
||||||
|
}
|
@ -4,9 +4,10 @@ namespace GraphQL\Language\AST;
|
|||||||
interface TypeSystemDefinitionNode extends DefinitionNode
|
interface TypeSystemDefinitionNode extends DefinitionNode
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
export type TypeSystemDefinitionNode = SchemaDefinitionNode
|
export type TypeSystemDefinitionNode =
|
||||||
|
| SchemaDefinitionNode
|
||||||
| TypeDefinitionNode
|
| TypeDefinitionNode
|
||||||
| TypeExtensionDefinitionNode
|
| TypeExtensionNode
|
||||||
| DirectiveDefinitionNode
|
| DirectiveDefinitionNode
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,12 @@ class UnionTypeDefinitionNode extends Node implements TypeDefinitionNode
|
|||||||
public $directives;
|
public $directives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var NamedTypeNode[]
|
* @var NamedTypeNode[]|null
|
||||||
*/
|
*/
|
||||||
public $types = [];
|
public $types;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var StringValueNode|null
|
||||||
*/
|
*/
|
||||||
public $description;
|
public $description;
|
||||||
}
|
}
|
||||||
|
25
src/Language/AST/UnionTypeExtensionNode.php
Normal file
25
src/Language/AST/UnionTypeExtensionNode.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Language\AST;
|
||||||
|
|
||||||
|
class UnionTypeExtensionNode extends Node implements TypeExtensionNode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $kind = NodeKind::UNION_TYPE_EXTENSION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var NameNode
|
||||||
|
*/
|
||||||
|
public $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DirectiveNode[]|null
|
||||||
|
*/
|
||||||
|
public $directives;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var NamedTypeNode[]|null
|
||||||
|
*/
|
||||||
|
public $types;
|
||||||
|
}
|
60
src/Language/DirectiveLocation.php
Normal file
60
src/Language/DirectiveLocation.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Language;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of available directive locations
|
||||||
|
*/
|
||||||
|
class DirectiveLocation
|
||||||
|
{
|
||||||
|
// Request Definitions
|
||||||
|
const QUERY = 'QUERY';
|
||||||
|
const MUTATION = 'MUTATION';
|
||||||
|
const SUBSCRIPTION = 'SUBSCRIPTION';
|
||||||
|
const FIELD = 'FIELD';
|
||||||
|
const FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION';
|
||||||
|
const FRAGMENT_SPREAD = 'FRAGMENT_SPREAD';
|
||||||
|
const INLINE_FRAGMENT = 'INLINE_FRAGMENT';
|
||||||
|
|
||||||
|
// Type System Definitions
|
||||||
|
const SCHEMA = 'SCHEMA';
|
||||||
|
const SCALAR = 'SCALAR';
|
||||||
|
const OBJECT = 'OBJECT';
|
||||||
|
const FIELD_DEFINITION = 'FIELD_DEFINITION';
|
||||||
|
const ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION';
|
||||||
|
const IFACE = 'INTERFACE';
|
||||||
|
const UNION = 'UNION';
|
||||||
|
const ENUM = 'ENUM';
|
||||||
|
const ENUM_VALUE = 'ENUM_VALUE';
|
||||||
|
const INPUT_OBJECT = 'INPUT_OBJECT';
|
||||||
|
const INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION';
|
||||||
|
|
||||||
|
private static $locations = [
|
||||||
|
self::QUERY => self::QUERY,
|
||||||
|
self::MUTATION => self::MUTATION,
|
||||||
|
self::SUBSCRIPTION => self::SUBSCRIPTION,
|
||||||
|
self::FIELD => self::FIELD,
|
||||||
|
self::FRAGMENT_DEFINITION => self::FRAGMENT_DEFINITION,
|
||||||
|
self::FRAGMENT_SPREAD => self::FRAGMENT_SPREAD,
|
||||||
|
self::INLINE_FRAGMENT => self::INLINE_FRAGMENT,
|
||||||
|
self::SCHEMA => self::SCHEMA,
|
||||||
|
self::SCALAR => self::SCALAR,
|
||||||
|
self::OBJECT => self::OBJECT,
|
||||||
|
self::FIELD_DEFINITION => self::FIELD_DEFINITION,
|
||||||
|
self::ARGUMENT_DEFINITION => self::ARGUMENT_DEFINITION,
|
||||||
|
self::IFACE => self::IFACE,
|
||||||
|
self::UNION => self::UNION,
|
||||||
|
self::ENUM => self::ENUM,
|
||||||
|
self::ENUM_VALUE => self::ENUM_VALUE,
|
||||||
|
self::INPUT_OBJECT => self::INPUT_OBJECT,
|
||||||
|
self::INPUT_FIELD_DEFINITION => self::INPUT_FIELD_DEFINITION,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function has($name)
|
||||||
|
{
|
||||||
|
return isset(self::$locations[$name]);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ namespace GraphQL\Language;
|
|||||||
|
|
||||||
use GraphQL\Error\SyntaxError;
|
use GraphQL\Error\SyntaxError;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
|
use GraphQL\Utils\BlockString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Lexer is a stateful stream generator in that every time
|
* A Lexer is a stateful stream generator in that every time
|
||||||
@ -91,13 +92,18 @@ class Lexer
|
|||||||
*/
|
*/
|
||||||
public function advance()
|
public function advance()
|
||||||
{
|
{
|
||||||
$token = $this->lastToken = $this->token;
|
$this->lastToken = $this->token;
|
||||||
|
$token = $this->token = $this->lookahead();
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lookahead()
|
||||||
|
{
|
||||||
|
$token = $this->token;
|
||||||
if ($token->kind !== Token::EOF) {
|
if ($token->kind !== Token::EOF) {
|
||||||
do {
|
do {
|
||||||
$token = $token->next = $this->readToken($token);
|
$token = $token->next ?: ($token->next = $this->readToken($token));
|
||||||
} while ($token->kind === Token::COMMENT);
|
} while ($token->kind === Token::COMMENT);
|
||||||
$this->token = $token;
|
|
||||||
}
|
}
|
||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
@ -201,7 +207,15 @@ class Lexer
|
|||||||
->readNumber($line, $col, $prev);
|
->readNumber($line, $col, $prev);
|
||||||
// "
|
// "
|
||||||
case 34:
|
case 34:
|
||||||
return $this->moveStringCursor(-1, -1 * $bytes)
|
list(,$nextCode) = $this->readChar();
|
||||||
|
list(,$nextNextCode) = $this->moveStringCursor(1, 1)->readChar();
|
||||||
|
|
||||||
|
if ($nextCode === 34 && $nextNextCode === 34) {
|
||||||
|
return $this->moveStringCursor(-2, (-1 * $bytes) - 1)
|
||||||
|
->readBlockString($line, $col, $prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->moveStringCursor(-2, (-1 * $bytes) - 1)
|
||||||
->readString($line, $col, $prev);
|
->readString($line, $col, $prev);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,12 +384,28 @@ class Lexer
|
|||||||
$value = '';
|
$value = '';
|
||||||
|
|
||||||
while (
|
while (
|
||||||
$code &&
|
$code !== null &&
|
||||||
// not LineTerminator
|
// not LineTerminator
|
||||||
$code !== 10 && $code !== 13 &&
|
$code !== 10 && $code !== 13
|
||||||
// not Quote (")
|
|
||||||
$code !== 34
|
|
||||||
) {
|
) {
|
||||||
|
// Closing Quote (")
|
||||||
|
if ($code === 34) {
|
||||||
|
$value .= $chunk;
|
||||||
|
|
||||||
|
// Skip quote
|
||||||
|
$this->moveStringCursor(1, 1);
|
||||||
|
|
||||||
|
return new Token(
|
||||||
|
Token::STRING,
|
||||||
|
$start,
|
||||||
|
$this->position,
|
||||||
|
$line,
|
||||||
|
$col,
|
||||||
|
$prev,
|
||||||
|
$value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$this->assertValidStringCharacterCode($code, $this->position);
|
$this->assertValidStringCharacterCode($code, $this->position);
|
||||||
$this->moveStringCursor(1, $bytes);
|
$this->moveStringCursor(1, $bytes);
|
||||||
|
|
||||||
@ -421,7 +451,6 @@ class Lexer
|
|||||||
list ($char, $code, $bytes) = $this->readChar();
|
list ($char, $code, $bytes) = $this->readChar();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($code !== 34) {
|
|
||||||
throw new SyntaxError(
|
throw new SyntaxError(
|
||||||
$this->source,
|
$this->source,
|
||||||
$this->position,
|
$this->position,
|
||||||
@ -429,19 +458,76 @@ class Lexer
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a block string token from the source file.
|
||||||
|
*
|
||||||
|
* """("?"?(\\"""|\\(?!=""")|[^"\\]))*"""
|
||||||
|
*/
|
||||||
|
private function readBlockString($line, $col, Token $prev)
|
||||||
|
{
|
||||||
|
$start = $this->position;
|
||||||
|
|
||||||
|
// Skip leading quotes and read first string char:
|
||||||
|
list ($char, $code, $bytes) = $this->moveStringCursor(3, 3)->readChar();
|
||||||
|
|
||||||
|
$chunk = '';
|
||||||
|
$value = '';
|
||||||
|
|
||||||
|
while ($code !== null) {
|
||||||
|
// Closing Triple-Quote (""")
|
||||||
|
if ($code === 34) {
|
||||||
|
// Move 2 quotes
|
||||||
|
list(,$nextCode) = $this->moveStringCursor(1, 1)->readChar();
|
||||||
|
list(,$nextNextCode) = $this->moveStringCursor(1, 1)->readChar();
|
||||||
|
|
||||||
|
if ($nextCode === 34 && $nextNextCode === 34) {
|
||||||
$value .= $chunk;
|
$value .= $chunk;
|
||||||
|
|
||||||
// Skip trailing quote:
|
|
||||||
$this->moveStringCursor(1, 1);
|
$this->moveStringCursor(1, 1);
|
||||||
|
|
||||||
return new Token(
|
return new Token(
|
||||||
Token::STRING,
|
Token::BLOCK_STRING,
|
||||||
$start,
|
$start,
|
||||||
$this->position,
|
$this->position,
|
||||||
$line,
|
$line,
|
||||||
$col,
|
$col,
|
||||||
$prev,
|
$prev,
|
||||||
$value
|
BlockString::value($value)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// move cursor back to before the first quote
|
||||||
|
$this->moveStringCursor(-2, -2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertValidBlockStringCharacterCode($code, $this->position);
|
||||||
|
$this->moveStringCursor(1, $bytes);
|
||||||
|
|
||||||
|
list(,$nextCode) = $this->readChar();
|
||||||
|
list(,$nextNextCode) = $this->moveStringCursor(1, 1)->readChar();
|
||||||
|
list(,$nextNextNextCode) = $this->moveStringCursor(1, 1)->readChar();
|
||||||
|
|
||||||
|
// Escape Triple-Quote (\""")
|
||||||
|
if ($code === 92 &&
|
||||||
|
$nextCode === 34 &&
|
||||||
|
$nextNextCode === 34 &&
|
||||||
|
$nextNextNextCode === 34
|
||||||
|
) {
|
||||||
|
$this->moveStringCursor(1, 1);
|
||||||
|
$value .= $chunk . '"""';
|
||||||
|
$chunk = '';
|
||||||
|
} else {
|
||||||
|
$this->moveStringCursor(-2, -2);
|
||||||
|
$chunk .= $char;
|
||||||
|
}
|
||||||
|
|
||||||
|
list ($char, $code, $bytes) = $this->readChar();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new SyntaxError(
|
||||||
|
$this->source,
|
||||||
|
$this->position,
|
||||||
|
'Unterminated string.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,6 +543,18 @@ class Lexer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function assertValidBlockStringCharacterCode($code, $position)
|
||||||
|
{
|
||||||
|
// SourceCharacter
|
||||||
|
if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) {
|
||||||
|
throw new SyntaxError(
|
||||||
|
$this->source,
|
||||||
|
$position,
|
||||||
|
'Invalid character within String: ' . Utils::printCharCode($code)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads from body starting at startPosition until it finds a non-whitespace
|
* Reads from body starting at startPosition until it finds a non-whitespace
|
||||||
* or commented character, then places cursor to the position of that character.
|
* or commented character, then places cursor to the position of that character.
|
||||||
@ -537,7 +635,7 @@ class Lexer
|
|||||||
$byteStreamPosition = $this->byteStreamPosition;
|
$byteStreamPosition = $this->byteStreamPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
$code = 0;
|
$code = null;
|
||||||
$utf8char = '';
|
$utf8char = '';
|
||||||
$bytes = 0;
|
$bytes = 0;
|
||||||
$positionOffset = 0;
|
$positionOffset = 0;
|
||||||
|
@ -4,11 +4,15 @@ namespace GraphQL\Language;
|
|||||||
use GraphQL\Language\AST\ArgumentNode;
|
use GraphQL\Language\AST\ArgumentNode;
|
||||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||||
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\EnumTypeExtensionNode;
|
||||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\ExecutableDefinitionNode;
|
||||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
|
||||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
|
||||||
use GraphQL\Language\AST\ListValueNode;
|
use GraphQL\Language\AST\ListValueNode;
|
||||||
use GraphQL\Language\AST\BooleanValueNode;
|
use GraphQL\Language\AST\BooleanValueNode;
|
||||||
use GraphQL\Language\AST\DirectiveNode;
|
use GraphQL\Language\AST\DirectiveNode;
|
||||||
@ -33,12 +37,15 @@ use GraphQL\Language\AST\ObjectValueNode;
|
|||||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||||
use GraphQL\Language\AST\OperationTypeDefinitionNode;
|
use GraphQL\Language\AST\OperationTypeDefinitionNode;
|
||||||
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
|
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\ScalarTypeExtensionNode;
|
||||||
use GraphQL\Language\AST\SchemaDefinitionNode;
|
use GraphQL\Language\AST\SchemaDefinitionNode;
|
||||||
use GraphQL\Language\AST\SelectionSetNode;
|
use GraphQL\Language\AST\SelectionSetNode;
|
||||||
use GraphQL\Language\AST\StringValueNode;
|
use GraphQL\Language\AST\StringValueNode;
|
||||||
use GraphQL\Language\AST\TypeExtensionDefinitionNode;
|
use GraphQL\Language\AST\ObjectTypeExtensionNode;
|
||||||
|
use GraphQL\Language\AST\TypeExtensionNode;
|
||||||
use GraphQL\Language\AST\TypeSystemDefinitionNode;
|
use GraphQL\Language\AST\TypeSystemDefinitionNode;
|
||||||
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\UnionTypeExtensionNode;
|
||||||
use GraphQL\Language\AST\VariableNode;
|
use GraphQL\Language\AST\VariableNode;
|
||||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||||
use GraphQL\Error\SyntaxError;
|
use GraphQL\Error\SyntaxError;
|
||||||
@ -59,10 +66,25 @@ class Parser
|
|||||||
* in the source that they correspond to. This configuration flag
|
* in the source that they correspond to. This configuration flag
|
||||||
* disables that behavior for performance or testing.)
|
* disables that behavior for performance or testing.)
|
||||||
*
|
*
|
||||||
|
* experimentalFragmentVariables: boolean,
|
||||||
|
* (If enabled, the parser will understand and parse variable definitions
|
||||||
|
* contained in a fragment definition. They'll be represented in the
|
||||||
|
* `variableDefinitions` field of the FragmentDefinitionNode.
|
||||||
|
*
|
||||||
|
* The syntax is identical to normal, query-defined variables. For example:
|
||||||
|
*
|
||||||
|
* fragment A($var: Boolean = false) on T {
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Note: this feature is experimental and may change or be removed in the
|
||||||
|
* future.)
|
||||||
|
*
|
||||||
* @api
|
* @api
|
||||||
* @param Source|string $source
|
* @param Source|string $source
|
||||||
* @param array $options
|
* @param array $options
|
||||||
* @return DocumentNode
|
* @return DocumentNode
|
||||||
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
public static function parse($source, array $options = [])
|
public static function parse($source, array $options = [])
|
||||||
{
|
{
|
||||||
@ -321,26 +343,20 @@ class Parser
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return OperationDefinitionNode|FragmentDefinitionNode|TypeSystemDefinitionNode
|
* @return ExecutableDefinitionNode|TypeSystemDefinitionNode
|
||||||
* @throws SyntaxError
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
function parseDefinition()
|
function parseDefinition()
|
||||||
{
|
{
|
||||||
if ($this->peek(Token::BRACE_L)) {
|
|
||||||
return $this->parseOperationDefinition();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->peek(Token::NAME)) {
|
if ($this->peek(Token::NAME)) {
|
||||||
switch ($this->lexer->token->value) {
|
switch ($this->lexer->token->value) {
|
||||||
case 'query':
|
case 'query':
|
||||||
case 'mutation':
|
case 'mutation':
|
||||||
case 'subscription':
|
case 'subscription':
|
||||||
return $this->parseOperationDefinition();
|
|
||||||
|
|
||||||
case 'fragment':
|
case 'fragment':
|
||||||
return $this->parseFragmentDefinition();
|
return $this->parseExecutableDefinition();
|
||||||
|
|
||||||
// Note: the Type System IDL is an experimental non-spec addition.
|
// Note: The schema definition language is an experimental addition.
|
||||||
case 'schema':
|
case 'schema':
|
||||||
case 'scalar':
|
case 'scalar':
|
||||||
case 'type':
|
case 'type':
|
||||||
@ -350,8 +366,37 @@ class Parser
|
|||||||
case 'input':
|
case 'input':
|
||||||
case 'extend':
|
case 'extend':
|
||||||
case 'directive':
|
case 'directive':
|
||||||
|
// Note: The schema definition language is an experimental addition.
|
||||||
return $this->parseTypeSystemDefinition();
|
return $this->parseTypeSystemDefinition();
|
||||||
}
|
}
|
||||||
|
} else if ($this->peek(Token::BRACE_L)) {
|
||||||
|
return $this->parseExecutableDefinition();
|
||||||
|
} else if ($this->peekDescription()) {
|
||||||
|
// Note: The schema definition language is an experimental addition.
|
||||||
|
return $this->parseTypeSystemDefinition();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $this->unexpected();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ExecutableDefinitionNode
|
||||||
|
* @throws SyntaxError
|
||||||
|
*/
|
||||||
|
function parseExecutableDefinition()
|
||||||
|
{
|
||||||
|
if ($this->peek(Token::NAME)) {
|
||||||
|
switch ($this->lexer->token->value) {
|
||||||
|
case 'query':
|
||||||
|
case 'mutation':
|
||||||
|
case 'subscription':
|
||||||
|
return $this->parseOperationDefinition();
|
||||||
|
|
||||||
|
case 'fragment':
|
||||||
|
return $this->parseFragmentDefinition();
|
||||||
|
}
|
||||||
|
} else if ($this->peek(Token::BRACE_L)) {
|
||||||
|
return $this->parseOperationDefinition();
|
||||||
}
|
}
|
||||||
|
|
||||||
throw $this->unexpected();
|
throw $this->unexpected();
|
||||||
@ -370,7 +415,7 @@ class Parser
|
|||||||
return new OperationDefinitionNode([
|
return new OperationDefinitionNode([
|
||||||
'operation' => 'query',
|
'operation' => 'query',
|
||||||
'name' => null,
|
'name' => null,
|
||||||
'variableDefinitions' => null,
|
'variableDefinitions' => new NodeList([]),
|
||||||
'directives' => new NodeList([]),
|
'directives' => new NodeList([]),
|
||||||
'selectionSet' => $this->parseSelectionSet(),
|
'selectionSet' => $this->parseSelectionSet(),
|
||||||
'loc' => $this->loc($start)
|
'loc' => $this->loc($start)
|
||||||
@ -388,7 +433,7 @@ class Parser
|
|||||||
'operation' => $operation,
|
'operation' => $operation,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'variableDefinitions' => $this->parseVariableDefinitions(),
|
'variableDefinitions' => $this->parseVariableDefinitions(),
|
||||||
'directives' => $this->parseDirectives(),
|
'directives' => $this->parseDirectives(false),
|
||||||
'selectionSet' => $this->parseSelectionSet(),
|
'selectionSet' => $this->parseSelectionSet(),
|
||||||
'loc' => $this->loc($start)
|
'loc' => $this->loc($start)
|
||||||
]);
|
]);
|
||||||
@ -404,7 +449,6 @@ class Parser
|
|||||||
switch ($operationToken->value) {
|
switch ($operationToken->value) {
|
||||||
case 'query': return 'query';
|
case 'query': return 'query';
|
||||||
case 'mutation': return 'mutation';
|
case 'mutation': return 'mutation';
|
||||||
// Note: subscription is an experimental non-spec addition.
|
|
||||||
case 'subscription': return 'subscription';
|
case 'subscription': return 'subscription';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,6 +534,7 @@ class Parser
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @return FieldNode
|
* @return FieldNode
|
||||||
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
function parseField()
|
function parseField()
|
||||||
{
|
{
|
||||||
@ -507,20 +552,23 @@ class Parser
|
|||||||
return new FieldNode([
|
return new FieldNode([
|
||||||
'alias' => $alias,
|
'alias' => $alias,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'arguments' => $this->parseArguments(),
|
'arguments' => $this->parseArguments(false),
|
||||||
'directives' => $this->parseDirectives(),
|
'directives' => $this->parseDirectives(false),
|
||||||
'selectionSet' => $this->peek(Token::BRACE_L) ? $this->parseSelectionSet() : null,
|
'selectionSet' => $this->peek(Token::BRACE_L) ? $this->parseSelectionSet() : null,
|
||||||
'loc' => $this->loc($start)
|
'loc' => $this->loc($start)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param bool $isConst
|
||||||
* @return ArgumentNode[]|NodeList
|
* @return ArgumentNode[]|NodeList
|
||||||
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
function parseArguments()
|
function parseArguments($isConst)
|
||||||
{
|
{
|
||||||
|
$item = $isConst ? 'parseConstArgument' : 'parseArgument';
|
||||||
return $this->peek(Token::PAREN_L) ?
|
return $this->peek(Token::PAREN_L) ?
|
||||||
$this->many(Token::PAREN_L, [$this, 'parseArgument'], Token::PAREN_R) :
|
$this->many(Token::PAREN_L, [$this, $item], Token::PAREN_R) :
|
||||||
new NodeList([]);
|
new NodeList([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,6 +591,25 @@ class Parser
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ArgumentNode
|
||||||
|
* @throws SyntaxError
|
||||||
|
*/
|
||||||
|
function parseConstArgument()
|
||||||
|
{
|
||||||
|
$start = $this->lexer->token;
|
||||||
|
$name = $this->parseName();
|
||||||
|
|
||||||
|
$this->expect(Token::COLON);
|
||||||
|
$value = $this->parseConstValue();
|
||||||
|
|
||||||
|
return new ArgumentNode([
|
||||||
|
'name' => $name,
|
||||||
|
'value' => $value,
|
||||||
|
'loc' => $this->loc($start)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Implements the parsing rules in the Fragments section.
|
// Implements the parsing rules in the Fragments section.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -557,7 +624,7 @@ class Parser
|
|||||||
if ($this->peek(Token::NAME) && $this->lexer->token->value !== 'on') {
|
if ($this->peek(Token::NAME) && $this->lexer->token->value !== 'on') {
|
||||||
return new FragmentSpreadNode([
|
return new FragmentSpreadNode([
|
||||||
'name' => $this->parseFragmentName(),
|
'name' => $this->parseFragmentName(),
|
||||||
'directives' => $this->parseDirectives(),
|
'directives' => $this->parseDirectives(false),
|
||||||
'loc' => $this->loc($start)
|
'loc' => $this->loc($start)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -570,7 +637,7 @@ class Parser
|
|||||||
|
|
||||||
return new InlineFragmentNode([
|
return new InlineFragmentNode([
|
||||||
'typeCondition' => $typeCondition,
|
'typeCondition' => $typeCondition,
|
||||||
'directives' => $this->parseDirectives(),
|
'directives' => $this->parseDirectives(false),
|
||||||
'selectionSet' => $this->parseSelectionSet(),
|
'selectionSet' => $this->parseSelectionSet(),
|
||||||
'loc' => $this->loc($start)
|
'loc' => $this->loc($start)
|
||||||
]);
|
]);
|
||||||
@ -586,13 +653,21 @@ class Parser
|
|||||||
$this->expectKeyword('fragment');
|
$this->expectKeyword('fragment');
|
||||||
|
|
||||||
$name = $this->parseFragmentName();
|
$name = $this->parseFragmentName();
|
||||||
|
|
||||||
|
// Experimental support for defining variables within fragments changes
|
||||||
|
// the grammar of FragmentDefinition:
|
||||||
|
// - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet
|
||||||
|
$variableDefinitions = null;
|
||||||
|
if (isset($this->lexer->options['experimentalFragmentVariables'])) {
|
||||||
|
$variableDefinitions = $this->parseVariableDefinitions();
|
||||||
|
}
|
||||||
$this->expectKeyword('on');
|
$this->expectKeyword('on');
|
||||||
$typeCondition = $this->parseNamedType();
|
$typeCondition = $this->parseNamedType();
|
||||||
|
|
||||||
return new FragmentDefinitionNode([
|
return new FragmentDefinitionNode([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
|
'variableDefinitions' => $variableDefinitions,
|
||||||
'typeCondition' => $typeCondition,
|
'typeCondition' => $typeCondition,
|
||||||
'directives' => $this->parseDirectives(),
|
'directives' => $this->parseDirectives(false),
|
||||||
'selectionSet' => $this->parseSelectionSet(),
|
'selectionSet' => $this->parseSelectionSet(),
|
||||||
'loc' => $this->loc($start)
|
'loc' => $this->loc($start)
|
||||||
]);
|
]);
|
||||||
@ -655,11 +730,8 @@ class Parser
|
|||||||
'loc' => $this->loc($token)
|
'loc' => $this->loc($token)
|
||||||
]);
|
]);
|
||||||
case Token::STRING:
|
case Token::STRING:
|
||||||
$this->lexer->advance();
|
case Token::BLOCK_STRING:
|
||||||
return new StringValueNode([
|
return $this->parseStringLiteral();
|
||||||
'value' => $token->value,
|
|
||||||
'loc' => $this->loc($token)
|
|
||||||
]);
|
|
||||||
case Token::NAME:
|
case Token::NAME:
|
||||||
if ($token->value === 'true' || $token->value === 'false') {
|
if ($token->value === 'true' || $token->value === 'false') {
|
||||||
$this->lexer->advance();
|
$this->lexer->advance();
|
||||||
@ -690,6 +762,20 @@ class Parser
|
|||||||
throw $this->unexpected();
|
throw $this->unexpected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return StringValueNode
|
||||||
|
*/
|
||||||
|
function parseStringLiteral() {
|
||||||
|
$token = $this->lexer->token;
|
||||||
|
$this->lexer->advance();
|
||||||
|
|
||||||
|
return new StringValueNode([
|
||||||
|
'value' => $token->value,
|
||||||
|
'block' => $token->kind === Token::BLOCK_STRING,
|
||||||
|
'loc' => $this->loc($token)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode
|
* @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode
|
||||||
* @throws SyntaxError
|
* @throws SyntaxError
|
||||||
@ -760,28 +846,31 @@ class Parser
|
|||||||
// Implements the parsing rules in the Directives section.
|
// Implements the parsing rules in the Directives section.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param bool $isConst
|
||||||
* @return DirectiveNode[]|NodeList
|
* @return DirectiveNode[]|NodeList
|
||||||
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
function parseDirectives()
|
function parseDirectives($isConst)
|
||||||
{
|
{
|
||||||
$directives = [];
|
$directives = [];
|
||||||
while ($this->peek(Token::AT)) {
|
while ($this->peek(Token::AT)) {
|
||||||
$directives[] = $this->parseDirective();
|
$directives[] = $this->parseDirective($isConst);
|
||||||
}
|
}
|
||||||
return new NodeList($directives);
|
return new NodeList($directives);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param bool $isConst
|
||||||
* @return DirectiveNode
|
* @return DirectiveNode
|
||||||
* @throws SyntaxError
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
function parseDirective()
|
function parseDirective($isConst)
|
||||||
{
|
{
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
$this->expect(Token::AT);
|
$this->expect(Token::AT);
|
||||||
return new DirectiveNode([
|
return new DirectiveNode([
|
||||||
'name' => $this->parseName(),
|
'name' => $this->parseName(),
|
||||||
'arguments' => $this->parseArguments(),
|
'arguments' => $this->parseArguments($isConst),
|
||||||
'loc' => $this->loc($start)
|
'loc' => $this->loc($start)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -834,7 +923,7 @@ class Parser
|
|||||||
* TypeSystemDefinition :
|
* TypeSystemDefinition :
|
||||||
* - SchemaDefinition
|
* - SchemaDefinition
|
||||||
* - TypeDefinition
|
* - TypeDefinition
|
||||||
* - TypeExtensionDefinition
|
* - TypeExtension
|
||||||
* - DirectiveDefinition
|
* - DirectiveDefinition
|
||||||
*
|
*
|
||||||
* TypeDefinition :
|
* TypeDefinition :
|
||||||
@ -850,8 +939,13 @@ class Parser
|
|||||||
*/
|
*/
|
||||||
function parseTypeSystemDefinition()
|
function parseTypeSystemDefinition()
|
||||||
{
|
{
|
||||||
if ($this->peek(Token::NAME)) {
|
// Many definitions begin with a description and require a lookahead.
|
||||||
switch ($this->lexer->token->value) {
|
$keywordToken = $this->peekDescription()
|
||||||
|
? $this->lexer->lookahead()
|
||||||
|
: $this->lexer->token;
|
||||||
|
|
||||||
|
if ($keywordToken->kind === Token::NAME) {
|
||||||
|
switch ($keywordToken->value) {
|
||||||
case 'schema': return $this->parseSchemaDefinition();
|
case 'schema': return $this->parseSchemaDefinition();
|
||||||
case 'scalar': return $this->parseScalarTypeDefinition();
|
case 'scalar': return $this->parseScalarTypeDefinition();
|
||||||
case 'type': return $this->parseObjectTypeDefinition();
|
case 'type': return $this->parseObjectTypeDefinition();
|
||||||
@ -859,12 +953,28 @@ class Parser
|
|||||||
case 'union': return $this->parseUnionTypeDefinition();
|
case 'union': return $this->parseUnionTypeDefinition();
|
||||||
case 'enum': return $this->parseEnumTypeDefinition();
|
case 'enum': return $this->parseEnumTypeDefinition();
|
||||||
case 'input': return $this->parseInputObjectTypeDefinition();
|
case 'input': return $this->parseInputObjectTypeDefinition();
|
||||||
case 'extend': return $this->parseTypeExtensionDefinition();
|
case 'extend': return $this->parseTypeExtension();
|
||||||
case 'directive': return $this->parseDirectiveDefinition();
|
case 'directive': return $this->parseDirectiveDefinition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw $this->unexpected();
|
throw $this->unexpected($keywordToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
function peekDescription() {
|
||||||
|
return $this->peek(Token::STRING) || $this->peek(Token::BLOCK_STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return StringValueNode|null
|
||||||
|
*/
|
||||||
|
function parseDescription() {
|
||||||
|
if ($this->peekDescription()) {
|
||||||
|
return $this->parseStringLiteral();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -875,7 +985,7 @@ class Parser
|
|||||||
{
|
{
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
$this->expectKeyword('schema');
|
$this->expectKeyword('schema');
|
||||||
$directives = $this->parseDirectives();
|
$directives = $this->parseDirectives(true);
|
||||||
|
|
||||||
$operationTypes = $this->many(
|
$operationTypes = $this->many(
|
||||||
Token::BRACE_L,
|
Token::BRACE_L,
|
||||||
@ -892,6 +1002,7 @@ class Parser
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @return OperationTypeDefinitionNode
|
* @return OperationTypeDefinitionNode
|
||||||
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
function parseOperationTypeDefinition()
|
function parseOperationTypeDefinition()
|
||||||
{
|
{
|
||||||
@ -914,11 +1025,10 @@ class Parser
|
|||||||
function parseScalarTypeDefinition()
|
function parseScalarTypeDefinition()
|
||||||
{
|
{
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
|
$description = $this->parseDescription();
|
||||||
$this->expectKeyword('scalar');
|
$this->expectKeyword('scalar');
|
||||||
$name = $this->parseName();
|
$name = $this->parseName();
|
||||||
$directives = $this->parseDirectives();
|
$directives = $this->parseDirectives(true);
|
||||||
|
|
||||||
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
|
|
||||||
|
|
||||||
return new ScalarTypeDefinitionNode([
|
return new ScalarTypeDefinitionNode([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
@ -935,18 +1045,12 @@ class Parser
|
|||||||
function parseObjectTypeDefinition()
|
function parseObjectTypeDefinition()
|
||||||
{
|
{
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
|
$description = $this->parseDescription();
|
||||||
$this->expectKeyword('type');
|
$this->expectKeyword('type');
|
||||||
$name = $this->parseName();
|
$name = $this->parseName();
|
||||||
$interfaces = $this->parseImplementsInterfaces();
|
$interfaces = $this->parseImplementsInterfaces();
|
||||||
$directives = $this->parseDirectives();
|
$directives = $this->parseDirectives(true);
|
||||||
|
$fields = $this->parseFieldsDefinition();
|
||||||
$fields = $this->any(
|
|
||||||
Token::BRACE_L,
|
|
||||||
[$this, 'parseFieldDefinition'],
|
|
||||||
Token::BRACE_R
|
|
||||||
);
|
|
||||||
|
|
||||||
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
|
|
||||||
|
|
||||||
return new ObjectTypeDefinitionNode([
|
return new ObjectTypeDefinitionNode([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
@ -973,6 +1077,21 @@ class Parser
|
|||||||
return $types;
|
return $types;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return FieldDefinitionNode[]|NodeList
|
||||||
|
* @throws SyntaxError
|
||||||
|
*/
|
||||||
|
function parseFieldsDefinition()
|
||||||
|
{
|
||||||
|
return $this->peek(Token::BRACE_L)
|
||||||
|
? $this->many(
|
||||||
|
Token::BRACE_L,
|
||||||
|
[$this, 'parseFieldDefinition'],
|
||||||
|
Token::BRACE_R
|
||||||
|
)
|
||||||
|
: new NodeList([]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return FieldDefinitionNode
|
* @return FieldDefinitionNode
|
||||||
* @throws SyntaxError
|
* @throws SyntaxError
|
||||||
@ -980,13 +1099,12 @@ class Parser
|
|||||||
function parseFieldDefinition()
|
function parseFieldDefinition()
|
||||||
{
|
{
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
|
$description = $this->parseDescription();
|
||||||
$name = $this->parseName();
|
$name = $this->parseName();
|
||||||
$args = $this->parseArgumentDefs();
|
$args = $this->parseArgumentDefs();
|
||||||
$this->expect(Token::COLON);
|
$this->expect(Token::COLON);
|
||||||
$type = $this->parseTypeReference();
|
$type = $this->parseTypeReference();
|
||||||
$directives = $this->parseDirectives();
|
$directives = $this->parseDirectives(true);
|
||||||
|
|
||||||
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
|
|
||||||
|
|
||||||
return new FieldDefinitionNode([
|
return new FieldDefinitionNode([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
@ -1000,11 +1118,12 @@ class Parser
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @return InputValueDefinitionNode[]|NodeList
|
* @return InputValueDefinitionNode[]|NodeList
|
||||||
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
function parseArgumentDefs()
|
function parseArgumentDefs()
|
||||||
{
|
{
|
||||||
if (!$this->peek(Token::PAREN_L)) {
|
if (!$this->peek(Token::PAREN_L)) {
|
||||||
return [];
|
return new NodeList([]);
|
||||||
}
|
}
|
||||||
return $this->many(Token::PAREN_L, [$this, 'parseInputValueDef'], Token::PAREN_R);
|
return $this->many(Token::PAREN_L, [$this, 'parseInputValueDef'], Token::PAREN_R);
|
||||||
}
|
}
|
||||||
@ -1016,6 +1135,7 @@ class Parser
|
|||||||
function parseInputValueDef()
|
function parseInputValueDef()
|
||||||
{
|
{
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
|
$description = $this->parseDescription();
|
||||||
$name = $this->parseName();
|
$name = $this->parseName();
|
||||||
$this->expect(Token::COLON);
|
$this->expect(Token::COLON);
|
||||||
$type = $this->parseTypeReference();
|
$type = $this->parseTypeReference();
|
||||||
@ -1023,8 +1143,7 @@ class Parser
|
|||||||
if ($this->skip(Token::EQUALS)) {
|
if ($this->skip(Token::EQUALS)) {
|
||||||
$defaultValue = $this->parseConstValue();
|
$defaultValue = $this->parseConstValue();
|
||||||
}
|
}
|
||||||
$directives = $this->parseDirectives();
|
$directives = $this->parseDirectives(true);
|
||||||
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
|
|
||||||
return new InputValueDefinitionNode([
|
return new InputValueDefinitionNode([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
@ -1042,16 +1161,11 @@ class Parser
|
|||||||
function parseInterfaceTypeDefinition()
|
function parseInterfaceTypeDefinition()
|
||||||
{
|
{
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
|
$description = $this->parseDescription();
|
||||||
$this->expectKeyword('interface');
|
$this->expectKeyword('interface');
|
||||||
$name = $this->parseName();
|
$name = $this->parseName();
|
||||||
$directives = $this->parseDirectives();
|
$directives = $this->parseDirectives(true);
|
||||||
$fields = $this->any(
|
$fields = $this->parseFieldsDefinition();
|
||||||
Token::BRACE_L,
|
|
||||||
[$this, 'parseFieldDefinition'],
|
|
||||||
Token::BRACE_R
|
|
||||||
);
|
|
||||||
|
|
||||||
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
|
|
||||||
|
|
||||||
return new InterfaceTypeDefinitionNode([
|
return new InterfaceTypeDefinitionNode([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
@ -1069,13 +1183,11 @@ class Parser
|
|||||||
function parseUnionTypeDefinition()
|
function parseUnionTypeDefinition()
|
||||||
{
|
{
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
|
$description = $this->parseDescription();
|
||||||
$this->expectKeyword('union');
|
$this->expectKeyword('union');
|
||||||
$name = $this->parseName();
|
$name = $this->parseName();
|
||||||
$directives = $this->parseDirectives();
|
$directives = $this->parseDirectives(true);
|
||||||
$this->expect(Token::EQUALS);
|
$types = $this->parseMemberTypesDefinition();
|
||||||
$types = $this->parseUnionMembers();
|
|
||||||
|
|
||||||
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
|
|
||||||
|
|
||||||
return new UnionTypeDefinitionNode([
|
return new UnionTypeDefinitionNode([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
@ -1087,22 +1199,23 @@ class Parser
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UnionMembers :
|
* MemberTypes :
|
||||||
* - `|`? NamedType
|
* - `|`? NamedType
|
||||||
* - UnionMembers | NamedType
|
* - MemberTypes | NamedType
|
||||||
*
|
*
|
||||||
* @return NamedTypeNode[]
|
* @return NamedTypeNode[]
|
||||||
*/
|
*/
|
||||||
function parseUnionMembers()
|
function parseMemberTypesDefinition()
|
||||||
{
|
{
|
||||||
|
$types = [];
|
||||||
|
if ($this->skip(Token::EQUALS)) {
|
||||||
// Optional leading pipe
|
// Optional leading pipe
|
||||||
$this->skip(Token::PIPE);
|
$this->skip(Token::PIPE);
|
||||||
$members = [];
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
$members[] = $this->parseNamedType();
|
$types[] = $this->parseNamedType();
|
||||||
} while ($this->skip(Token::PIPE));
|
} while ($this->skip(Token::PIPE));
|
||||||
return $members;
|
}
|
||||||
|
return $types;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1112,16 +1225,11 @@ class Parser
|
|||||||
function parseEnumTypeDefinition()
|
function parseEnumTypeDefinition()
|
||||||
{
|
{
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
|
$description = $this->parseDescription();
|
||||||
$this->expectKeyword('enum');
|
$this->expectKeyword('enum');
|
||||||
$name = $this->parseName();
|
$name = $this->parseName();
|
||||||
$directives = $this->parseDirectives();
|
$directives = $this->parseDirectives(true);
|
||||||
$values = $this->many(
|
$values = $this->parseEnumValuesDefinition();
|
||||||
Token::BRACE_L,
|
|
||||||
[$this, 'parseEnumValueDefinition'],
|
|
||||||
Token::BRACE_R
|
|
||||||
);
|
|
||||||
|
|
||||||
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
|
|
||||||
|
|
||||||
return new EnumTypeDefinitionNode([
|
return new EnumTypeDefinitionNode([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
@ -1132,16 +1240,31 @@ class Parser
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return EnumValueDefinitionNode[]|NodeList
|
||||||
|
* @throws SyntaxError
|
||||||
|
*/
|
||||||
|
function parseEnumValuesDefinition()
|
||||||
|
{
|
||||||
|
return $this->peek(Token::BRACE_L)
|
||||||
|
? $this->many(
|
||||||
|
Token::BRACE_L,
|
||||||
|
[$this, 'parseEnumValueDefinition'],
|
||||||
|
Token::BRACE_R
|
||||||
|
)
|
||||||
|
: new NodeList([]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return EnumValueDefinitionNode
|
* @return EnumValueDefinitionNode
|
||||||
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
function parseEnumValueDefinition()
|
function parseEnumValueDefinition()
|
||||||
{
|
{
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
|
$description = $this->parseDescription();
|
||||||
$name = $this->parseName();
|
$name = $this->parseName();
|
||||||
$directives = $this->parseDirectives();
|
$directives = $this->parseDirectives(true);
|
||||||
|
|
||||||
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
|
|
||||||
|
|
||||||
return new EnumValueDefinitionNode([
|
return new EnumValueDefinitionNode([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
@ -1158,16 +1281,11 @@ class Parser
|
|||||||
function parseInputObjectTypeDefinition()
|
function parseInputObjectTypeDefinition()
|
||||||
{
|
{
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
|
$description = $this->parseDescription();
|
||||||
$this->expectKeyword('input');
|
$this->expectKeyword('input');
|
||||||
$name = $this->parseName();
|
$name = $this->parseName();
|
||||||
$directives = $this->parseDirectives();
|
$directives = $this->parseDirectives(true);
|
||||||
$fields = $this->any(
|
$fields = $this->parseInputFieldsDefinition();
|
||||||
Token::BRACE_L,
|
|
||||||
[$this, 'parseInputValueDef'],
|
|
||||||
Token::BRACE_R
|
|
||||||
);
|
|
||||||
|
|
||||||
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
|
|
||||||
|
|
||||||
return new InputObjectTypeDefinitionNode([
|
return new InputObjectTypeDefinitionNode([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
@ -1179,17 +1297,206 @@ class Parser
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return TypeExtensionDefinitionNode
|
* @return InputValueDefinitionNode[]|NodeList
|
||||||
* @throws SyntaxError
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
function parseTypeExtensionDefinition()
|
function parseInputFieldsDefinition() {
|
||||||
|
return $this->peek(Token::BRACE_L)
|
||||||
|
? $this->many(
|
||||||
|
Token::BRACE_L,
|
||||||
|
[$this, 'parseInputValueDef'],
|
||||||
|
Token::BRACE_R
|
||||||
|
)
|
||||||
|
: new NodeList([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeExtension :
|
||||||
|
* - ScalarTypeExtension
|
||||||
|
* - ObjectTypeExtension
|
||||||
|
* - InterfaceTypeExtension
|
||||||
|
* - UnionTypeExtension
|
||||||
|
* - EnumTypeExtension
|
||||||
|
* - InputObjectTypeDefinition
|
||||||
|
*
|
||||||
|
* @return TypeExtensionNode
|
||||||
|
* @throws SyntaxError
|
||||||
|
*/
|
||||||
|
function parseTypeExtension()
|
||||||
{
|
{
|
||||||
|
$keywordToken = $this->lexer->lookahead();
|
||||||
|
|
||||||
|
if ($keywordToken->kind === Token::NAME) {
|
||||||
|
switch ($keywordToken->value) {
|
||||||
|
case 'scalar':
|
||||||
|
return $this->parseScalarTypeExtension();
|
||||||
|
case 'type':
|
||||||
|
return $this->parseObjectTypeExtension();
|
||||||
|
case 'interface':
|
||||||
|
return $this->parseInterfaceTypeExtension();
|
||||||
|
case 'union':
|
||||||
|
return $this->parseUnionTypeExtension();
|
||||||
|
case 'enum':
|
||||||
|
return $this->parseEnumTypeExtension();
|
||||||
|
case 'input':
|
||||||
|
return $this->parseInputObjectTypeExtension();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $this->unexpected($keywordToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ScalarTypeExtensionNode
|
||||||
|
* @throws SyntaxError
|
||||||
|
*/
|
||||||
|
function parseScalarTypeExtension() {
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
$this->expectKeyword('extend');
|
$this->expectKeyword('extend');
|
||||||
$definition = $this->parseObjectTypeDefinition();
|
$this->expectKeyword('scalar');
|
||||||
|
$name = $this->parseName();
|
||||||
|
$directives = $this->parseDirectives(true);
|
||||||
|
if (count($directives) === 0) {
|
||||||
|
throw $this->unexpected();
|
||||||
|
}
|
||||||
|
|
||||||
return new TypeExtensionDefinitionNode([
|
return new ScalarTypeExtensionNode([
|
||||||
'definition' => $definition,
|
'name' => $name,
|
||||||
|
'directives' => $directives,
|
||||||
|
'loc' => $this->loc($start)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ObjectTypeExtensionNode
|
||||||
|
* @throws SyntaxError
|
||||||
|
*/
|
||||||
|
function parseObjectTypeExtension() {
|
||||||
|
$start = $this->lexer->token;
|
||||||
|
$this->expectKeyword('extend');
|
||||||
|
$this->expectKeyword('type');
|
||||||
|
$name = $this->parseName();
|
||||||
|
$interfaces = $this->parseImplementsInterfaces();
|
||||||
|
$directives = $this->parseDirectives(true);
|
||||||
|
$fields = $this->parseFieldsDefinition();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!$interfaces &&
|
||||||
|
count($directives) === 0 &&
|
||||||
|
count($fields) === 0
|
||||||
|
) {
|
||||||
|
throw $this->unexpected();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ObjectTypeExtensionNode([
|
||||||
|
'name' => $name,
|
||||||
|
'interfaces' => $interfaces,
|
||||||
|
'directives' => $directives,
|
||||||
|
'fields' => $fields,
|
||||||
|
'loc' => $this->loc($start)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return InterfaceTypeExtensionNode
|
||||||
|
* @throws SyntaxError
|
||||||
|
*/
|
||||||
|
function parseInterfaceTypeExtension() {
|
||||||
|
$start = $this->lexer->token;
|
||||||
|
$this->expectKeyword('extend');
|
||||||
|
$this->expectKeyword('interface');
|
||||||
|
$name = $this->parseName();
|
||||||
|
$directives = $this->parseDirectives(true);
|
||||||
|
$fields = $this->parseFieldsDefinition();
|
||||||
|
if (
|
||||||
|
count($directives) === 0 &&
|
||||||
|
count($fields) === 0
|
||||||
|
) {
|
||||||
|
throw $this->unexpected();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InterfaceTypeExtensionNode([
|
||||||
|
'name' => $name,
|
||||||
|
'directives' => $directives,
|
||||||
|
'fields' => $fields,
|
||||||
|
'loc' => $this->loc($start)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return UnionTypeExtensionNode
|
||||||
|
* @throws SyntaxError
|
||||||
|
*/
|
||||||
|
function parseUnionTypeExtension() {
|
||||||
|
$start = $this->lexer->token;
|
||||||
|
$this->expectKeyword('extend');
|
||||||
|
$this->expectKeyword('union');
|
||||||
|
$name = $this->parseName();
|
||||||
|
$directives = $this->parseDirectives(true);
|
||||||
|
$types = $this->parseMemberTypesDefinition();
|
||||||
|
if (
|
||||||
|
count($directives) === 0 &&
|
||||||
|
!$types
|
||||||
|
) {
|
||||||
|
throw $this->unexpected();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UnionTypeExtensionNode([
|
||||||
|
'name' => $name,
|
||||||
|
'directives' => $directives,
|
||||||
|
'types' => $types,
|
||||||
|
'loc' => $this->loc($start)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return EnumTypeExtensionNode
|
||||||
|
* @throws SyntaxError
|
||||||
|
*/
|
||||||
|
function parseEnumTypeExtension() {
|
||||||
|
$start = $this->lexer->token;
|
||||||
|
$this->expectKeyword('extend');
|
||||||
|
$this->expectKeyword('enum');
|
||||||
|
$name = $this->parseName();
|
||||||
|
$directives = $this->parseDirectives(true);
|
||||||
|
$values = $this->parseEnumValuesDefinition();
|
||||||
|
if (
|
||||||
|
count($directives) === 0 &&
|
||||||
|
count($values) === 0
|
||||||
|
) {
|
||||||
|
throw $this->unexpected();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EnumTypeExtensionNode([
|
||||||
|
'name' => $name,
|
||||||
|
'directives' => $directives,
|
||||||
|
'values' => $values,
|
||||||
|
'loc' => $this->loc($start)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return InputObjectTypeExtensionNode
|
||||||
|
* @throws SyntaxError
|
||||||
|
*/
|
||||||
|
function parseInputObjectTypeExtension() {
|
||||||
|
$start = $this->lexer->token;
|
||||||
|
$this->expectKeyword('extend');
|
||||||
|
$this->expectKeyword('input');
|
||||||
|
$name = $this->parseName();
|
||||||
|
$directives = $this->parseDirectives(true);
|
||||||
|
$fields = $this->parseInputFieldsDefinition();
|
||||||
|
if (
|
||||||
|
count($directives) === 0 &&
|
||||||
|
count($fields) === 0
|
||||||
|
) {
|
||||||
|
throw $this->unexpected();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InputObjectTypeExtensionNode([
|
||||||
|
'name' => $name,
|
||||||
|
'directives' => $directives,
|
||||||
|
'fields' => $fields,
|
||||||
'loc' => $this->loc($start)
|
'loc' => $this->loc($start)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -1204,6 +1511,7 @@ class Parser
|
|||||||
function parseDirectiveDefinition()
|
function parseDirectiveDefinition()
|
||||||
{
|
{
|
||||||
$start = $this->lexer->token;
|
$start = $this->lexer->token;
|
||||||
|
$description = $this->parseDescription();
|
||||||
$this->expectKeyword('directive');
|
$this->expectKeyword('directive');
|
||||||
$this->expect(Token::AT);
|
$this->expect(Token::AT);
|
||||||
$name = $this->parseName();
|
$name = $this->parseName();
|
||||||
@ -1215,12 +1523,14 @@ class Parser
|
|||||||
'name' => $name,
|
'name' => $name,
|
||||||
'arguments' => $args,
|
'arguments' => $args,
|
||||||
'locations' => $locations,
|
'locations' => $locations,
|
||||||
'loc' => $this->loc($start)
|
'loc' => $this->loc($start),
|
||||||
|
'description' => $description
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return NameNode[]
|
* @return NameNode[]
|
||||||
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
function parseDirectiveLocations()
|
function parseDirectiveLocations()
|
||||||
{
|
{
|
||||||
@ -1228,32 +1538,23 @@ class Parser
|
|||||||
$this->skip(Token::PIPE);
|
$this->skip(Token::PIPE);
|
||||||
$locations = [];
|
$locations = [];
|
||||||
do {
|
do {
|
||||||
$locations[] = $this->parseName();
|
$locations[] = $this->parseDirectiveLocation();
|
||||||
} while ($this->skip(Token::PIPE));
|
} while ($this->skip(Token::PIPE));
|
||||||
return $locations;
|
return $locations;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Token $nameToken
|
* @return NameNode
|
||||||
* @return null|string
|
* @throws SyntaxError
|
||||||
*/
|
*/
|
||||||
private function getDescriptionFromAdjacentCommentTokens(Token $nameToken)
|
function parseDirectiveLocation()
|
||||||
{
|
{
|
||||||
$description = null;
|
$start = $this->lexer->token;
|
||||||
|
$name = $this->parseName();
|
||||||
$currentToken = $nameToken;
|
if (DirectiveLocation::has($name->value)) {
|
||||||
$previousToken = $currentToken->prev;
|
return $name;
|
||||||
|
|
||||||
while ($previousToken->kind == Token::COMMENT
|
|
||||||
&& ($previousToken->line + 1) == $currentToken->line
|
|
||||||
) {
|
|
||||||
$description = $previousToken->value . $description;
|
|
||||||
|
|
||||||
// walk the tokens backwards until no longer adjacent comments
|
|
||||||
$currentToken = $previousToken;
|
|
||||||
$previousToken = $currentToken->prev;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $description;
|
throw $this->unexpected($start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,14 @@ namespace GraphQL\Language;
|
|||||||
use GraphQL\Language\AST\ArgumentNode;
|
use GraphQL\Language\AST\ArgumentNode;
|
||||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||||
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\EnumTypeExtensionNode;
|
||||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
|
||||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
|
||||||
use GraphQL\Language\AST\ListValueNode;
|
use GraphQL\Language\AST\ListValueNode;
|
||||||
use GraphQL\Language\AST\BooleanValueNode;
|
use GraphQL\Language\AST\BooleanValueNode;
|
||||||
use GraphQL\Language\AST\DirectiveNode;
|
use GraphQL\Language\AST\DirectiveNode;
|
||||||
@ -32,11 +35,13 @@ use GraphQL\Language\AST\ObjectValueNode;
|
|||||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||||
use GraphQL\Language\AST\OperationTypeDefinitionNode;
|
use GraphQL\Language\AST\OperationTypeDefinitionNode;
|
||||||
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
|
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\ScalarTypeExtensionNode;
|
||||||
use GraphQL\Language\AST\SchemaDefinitionNode;
|
use GraphQL\Language\AST\SchemaDefinitionNode;
|
||||||
use GraphQL\Language\AST\SelectionSetNode;
|
use GraphQL\Language\AST\SelectionSetNode;
|
||||||
use GraphQL\Language\AST\StringValueNode;
|
use GraphQL\Language\AST\StringValueNode;
|
||||||
use GraphQL\Language\AST\TypeExtensionDefinitionNode;
|
use GraphQL\Language\AST\ObjectTypeExtensionNode;
|
||||||
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\UnionTypeExtensionNode;
|
||||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
@ -126,7 +131,11 @@ class Printer
|
|||||||
], ' ');
|
], ' ');
|
||||||
},
|
},
|
||||||
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) {
|
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) {
|
||||||
return "fragment {$node->name} on {$node->typeCondition} "
|
// Note: fragment variable definitions are experimental and may be changed
|
||||||
|
// or removed in the future.
|
||||||
|
return "fragment {$node->name}"
|
||||||
|
. $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')')
|
||||||
|
. " on {$node->typeCondition} "
|
||||||
. $this->wrap('', $this->join($node->directives, ' '), ' ')
|
. $this->wrap('', $this->join($node->directives, ' '), ' ')
|
||||||
. $node->selectionSet;
|
. $node->selectionSet;
|
||||||
},
|
},
|
||||||
@ -138,7 +147,10 @@ class Printer
|
|||||||
NodeKind::FLOAT => function(FloatValueNode $node) {
|
NodeKind::FLOAT => function(FloatValueNode $node) {
|
||||||
return $node->value;
|
return $node->value;
|
||||||
},
|
},
|
||||||
NodeKind::STRING => function(StringValueNode $node) {
|
NodeKind::STRING => function(StringValueNode $node, $key) {
|
||||||
|
if ($node->block) {
|
||||||
|
return $this->printBlockString($node->value, $key === 'description');
|
||||||
|
}
|
||||||
return json_encode($node->value);
|
return json_encode($node->value);
|
||||||
},
|
},
|
||||||
NodeKind::BOOLEAN => function(BooleanValueNode $node) {
|
NodeKind::BOOLEAN => function(BooleanValueNode $node) {
|
||||||
@ -189,74 +201,150 @@ class Printer
|
|||||||
},
|
},
|
||||||
|
|
||||||
NodeKind::SCALAR_TYPE_DEFINITION => function(ScalarTypeDefinitionNode $def) {
|
NodeKind::SCALAR_TYPE_DEFINITION => function(ScalarTypeDefinitionNode $def) {
|
||||||
return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' ');
|
return $this->join([
|
||||||
|
$def->description,
|
||||||
|
$this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' ')
|
||||||
|
], "\n");
|
||||||
},
|
},
|
||||||
NodeKind::OBJECT_TYPE_DEFINITION => function(ObjectTypeDefinitionNode $def) {
|
NodeKind::OBJECT_TYPE_DEFINITION => function(ObjectTypeDefinitionNode $def) {
|
||||||
return $this->join([
|
return $this->join([
|
||||||
|
$def->description,
|
||||||
|
$this->join([
|
||||||
'type',
|
'type',
|
||||||
$def->name,
|
$def->name,
|
||||||
$this->wrap('implements ', $this->join($def->interfaces, ', ')),
|
$this->wrap('implements ', $this->join($def->interfaces, ', ')),
|
||||||
$this->join($def->directives, ' '),
|
$this->join($def->directives, ' '),
|
||||||
$this->block($def->fields)
|
$this->block($def->fields)
|
||||||
], ' ');
|
], ' ')
|
||||||
|
], "\n");
|
||||||
},
|
},
|
||||||
NodeKind::FIELD_DEFINITION => function(FieldDefinitionNode $def) {
|
NodeKind::FIELD_DEFINITION => function(FieldDefinitionNode $def) {
|
||||||
return $def->name
|
return $this->join([
|
||||||
|
$def->description,
|
||||||
|
$def->name
|
||||||
. $this->wrap('(', $this->join($def->arguments, ', '), ')')
|
. $this->wrap('(', $this->join($def->arguments, ', '), ')')
|
||||||
. ': ' . $def->type
|
. ': ' . $def->type
|
||||||
. $this->wrap(' ', $this->join($def->directives, ' '));
|
. $this->wrap(' ', $this->join($def->directives, ' '))
|
||||||
|
], "\n");
|
||||||
},
|
},
|
||||||
NodeKind::INPUT_VALUE_DEFINITION => function(InputValueDefinitionNode $def) {
|
NodeKind::INPUT_VALUE_DEFINITION => function(InputValueDefinitionNode $def) {
|
||||||
return $this->join([
|
return $this->join([
|
||||||
|
$def->description,
|
||||||
|
$this->join([
|
||||||
$def->name . ': ' . $def->type,
|
$def->name . ': ' . $def->type,
|
||||||
$this->wrap('= ', $def->defaultValue),
|
$this->wrap('= ', $def->defaultValue),
|
||||||
$this->join($def->directives, ' ')
|
$this->join($def->directives, ' ')
|
||||||
], ' ');
|
], ' ')
|
||||||
|
], "\n");
|
||||||
},
|
},
|
||||||
NodeKind::INTERFACE_TYPE_DEFINITION => function(InterfaceTypeDefinitionNode $def) {
|
NodeKind::INTERFACE_TYPE_DEFINITION => function(InterfaceTypeDefinitionNode $def) {
|
||||||
return $this->join([
|
return $this->join([
|
||||||
|
$def->description,
|
||||||
|
$this->join([
|
||||||
'interface',
|
'interface',
|
||||||
$def->name,
|
$def->name,
|
||||||
$this->join($def->directives, ' '),
|
$this->join($def->directives, ' '),
|
||||||
$this->block($def->fields)
|
$this->block($def->fields)
|
||||||
], ' ');
|
], ' ')
|
||||||
|
], "\n");
|
||||||
},
|
},
|
||||||
NodeKind::UNION_TYPE_DEFINITION => function(UnionTypeDefinitionNode $def) {
|
NodeKind::UNION_TYPE_DEFINITION => function(UnionTypeDefinitionNode $def) {
|
||||||
return $this->join([
|
return $this->join([
|
||||||
|
$def->description,
|
||||||
|
$this->join([
|
||||||
'union',
|
'union',
|
||||||
$def->name,
|
$def->name,
|
||||||
$this->join($def->directives, ' '),
|
$this->join($def->directives, ' '),
|
||||||
'= ' . $this->join($def->types, ' | ')
|
$def->types
|
||||||
], ' ');
|
? '= ' . $this->join($def->types, ' | ')
|
||||||
|
: ''
|
||||||
|
], ' ')
|
||||||
|
], "\n");
|
||||||
},
|
},
|
||||||
NodeKind::ENUM_TYPE_DEFINITION => function(EnumTypeDefinitionNode $def) {
|
NodeKind::ENUM_TYPE_DEFINITION => function(EnumTypeDefinitionNode $def) {
|
||||||
return $this->join([
|
return $this->join([
|
||||||
|
$def->description,
|
||||||
|
$this->join([
|
||||||
'enum',
|
'enum',
|
||||||
$def->name,
|
$def->name,
|
||||||
$this->join($def->directives, ' '),
|
$this->join($def->directives, ' '),
|
||||||
$this->block($def->values)
|
$this->block($def->values)
|
||||||
], ' ');
|
], ' ')
|
||||||
|
], "\n");
|
||||||
},
|
},
|
||||||
NodeKind::ENUM_VALUE_DEFINITION => function(EnumValueDefinitionNode $def) {
|
NodeKind::ENUM_VALUE_DEFINITION => function(EnumValueDefinitionNode $def) {
|
||||||
return $this->join([
|
return $this->join([
|
||||||
$def->name,
|
$def->description,
|
||||||
$this->join($def->directives, ' ')
|
$this->join([$def->name, $this->join($def->directives, ' ')], ' ')
|
||||||
], ' ');
|
], "\n");
|
||||||
},
|
},
|
||||||
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => function(InputObjectTypeDefinitionNode $def) {
|
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => function(InputObjectTypeDefinitionNode $def) {
|
||||||
return $this->join([
|
return $this->join([
|
||||||
|
$def->description,
|
||||||
|
$this->join([
|
||||||
'input',
|
'input',
|
||||||
$def->name,
|
$def->name,
|
||||||
$this->join($def->directives, ' '),
|
$this->join($def->directives, ' '),
|
||||||
$this->block($def->fields)
|
$this->block($def->fields)
|
||||||
|
], ' ')
|
||||||
|
], "\n");
|
||||||
|
},
|
||||||
|
NodeKind::SCALAR_TYPE_EXTENSION => function(ScalarTypeExtensionNode $def) {
|
||||||
|
return $this->join([
|
||||||
|
'extend scalar',
|
||||||
|
$def->name,
|
||||||
|
$this->join($def->directives, ' '),
|
||||||
], ' ');
|
], ' ');
|
||||||
},
|
},
|
||||||
NodeKind::TYPE_EXTENSION_DEFINITION => function(TypeExtensionDefinitionNode $def) {
|
NodeKind::OBJECT_TYPE_EXTENSION => function(ObjectTypeExtensionNode $def) {
|
||||||
return "extend {$def->definition}";
|
return $this->join([
|
||||||
|
'extend type',
|
||||||
|
$def->name,
|
||||||
|
$this->wrap('implements ', $this->join($def->interfaces, ', ')),
|
||||||
|
$this->join($def->directives, ' '),
|
||||||
|
$this->block($def->fields),
|
||||||
|
], ' ');
|
||||||
|
},
|
||||||
|
NodeKind::INTERFACE_TYPE_EXTENSION => function(InterfaceTypeExtensionNode $def) {
|
||||||
|
return $this->join([
|
||||||
|
'extend interface',
|
||||||
|
$def->name,
|
||||||
|
$this->join($def->directives, ' '),
|
||||||
|
$this->block($def->fields),
|
||||||
|
], ' ');
|
||||||
|
},
|
||||||
|
NodeKind::UNION_TYPE_EXTENSION => function(UnionTypeExtensionNode $def) {
|
||||||
|
return $this->join([
|
||||||
|
'extend union',
|
||||||
|
$def->name,
|
||||||
|
$this->join($def->directives, ' '),
|
||||||
|
$def->types
|
||||||
|
? '= ' . $this->join($def->types, ' | ')
|
||||||
|
: ''
|
||||||
|
], ' ');
|
||||||
|
},
|
||||||
|
NodeKind::ENUM_TYPE_EXTENSION => function(EnumTypeExtensionNode $def) {
|
||||||
|
return $this->join([
|
||||||
|
'extend enum',
|
||||||
|
$def->name,
|
||||||
|
$this->join($def->directives, ' '),
|
||||||
|
$this->block($def->values),
|
||||||
|
], ' ');
|
||||||
|
},
|
||||||
|
NodeKind::INPUT_OBJECT_TYPE_EXTENSION => function(InputObjectTypeExtensionNode $def) {
|
||||||
|
return $this->join([
|
||||||
|
'extend input',
|
||||||
|
$def->name,
|
||||||
|
$this->join($def->directives, ' '),
|
||||||
|
$this->block($def->fields),
|
||||||
|
], ' ');
|
||||||
},
|
},
|
||||||
NodeKind::DIRECTIVE_DEFINITION => function(DirectiveDefinitionNode $def) {
|
NodeKind::DIRECTIVE_DEFINITION => function(DirectiveDefinitionNode $def) {
|
||||||
return 'directive @' . $def->name . $this->wrap('(', $this->join($def->arguments, ', '), ')')
|
return $this->join([
|
||||||
. ' on ' . $this->join($def->locations, ' | ');
|
$def->description,
|
||||||
|
'directive @' . $def->name . $this->wrap('(', $this->join($def->arguments, ', '), ')')
|
||||||
|
. ' on ' . $this->join($def->locations, ' | ')
|
||||||
|
], "\n");
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
@ -277,12 +365,14 @@ class Printer
|
|||||||
*/
|
*/
|
||||||
public function block($array)
|
public function block($array)
|
||||||
{
|
{
|
||||||
return $array && $this->length($array) ? $this->indent("{\n" . $this->join($array, "\n")) . "\n}" : '{}';
|
return ($array && $this->length($array))
|
||||||
|
? "{\n" . $this->indent($this->join($array, "\n")) . "\n}"
|
||||||
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function indent($maybeString)
|
public function indent($maybeString)
|
||||||
{
|
{
|
||||||
return $maybeString ? str_replace("\n", "\n ", $maybeString) : '';
|
return $maybeString ? ' ' . str_replace("\n", "\n ", $maybeString) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function manyList($start, $list, $separator, $end)
|
public function manyList($start, $list, $separator, $end)
|
||||||
@ -307,4 +397,16 @@ class Printer
|
|||||||
)
|
)
|
||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print a block string in the indented block form by adding a leading and
|
||||||
|
* trailing blank line. However, if a block string starts with whitespace and is
|
||||||
|
* a single-line, adding a leading blank line would strip that whitespace.
|
||||||
|
*/
|
||||||
|
private function printBlockString($value, $isDescription) {
|
||||||
|
$escaped = str_replace('"""', '\\"""', $value);
|
||||||
|
return (($value[0] === ' ' || $value[0] === "\t") && strpos($value, "\n") === false)
|
||||||
|
? ('"""' . preg_replace('/"$/', "\"\n", $escaped) . '"""')
|
||||||
|
: ("\"\"\"\n" . ($isDescription ? $escaped : $this->indent($escaped)) . "\n\"\"\"");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,8 +39,8 @@ class Source
|
|||||||
* be "Foo.graphql" and location to be `{ line: 40, column: 0 }`.
|
* be "Foo.graphql" and location to be `{ line: 40, column: 0 }`.
|
||||||
* line and column in locationOffset are 1-indexed
|
* line and column in locationOffset are 1-indexed
|
||||||
*
|
*
|
||||||
* @param $body
|
* @param string $body
|
||||||
* @param null $name
|
* @param string|null $name
|
||||||
* @param SourceLocation|null $location
|
* @param SourceLocation|null $location
|
||||||
*/
|
*/
|
||||||
public function __construct($body, $name = null, SourceLocation $location = null)
|
public function __construct($body, $name = null, SourceLocation $location = null)
|
||||||
@ -52,7 +52,7 @@ class Source
|
|||||||
|
|
||||||
$this->body = $body;
|
$this->body = $body;
|
||||||
$this->length = mb_strlen($body, 'UTF-8');
|
$this->length = mb_strlen($body, 'UTF-8');
|
||||||
$this->name = $name ?: 'GraphQL';
|
$this->name = $name ?: 'GraphQL request';
|
||||||
$this->locationOffset = $location ?: new SourceLocation(1, 1);
|
$this->locationOffset = $location ?: new SourceLocation(1, 1);
|
||||||
|
|
||||||
Utils::invariant(
|
Utils::invariant(
|
||||||
|
@ -27,6 +27,7 @@ class Token
|
|||||||
const INT = 'Int';
|
const INT = 'Int';
|
||||||
const FLOAT = 'Float';
|
const FLOAT = 'Float';
|
||||||
const STRING = 'String';
|
const STRING = 'String';
|
||||||
|
const BLOCK_STRING = 'BlockString';
|
||||||
const COMMENT = 'Comment';
|
const COMMENT = 'Comment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,6 +58,7 @@ class Token
|
|||||||
$description[self::INT] = 'Int';
|
$description[self::INT] = 'Int';
|
||||||
$description[self::FLOAT] = 'Float';
|
$description[self::FLOAT] = 'Float';
|
||||||
$description[self::STRING] = 'String';
|
$description[self::STRING] = 'String';
|
||||||
|
$description[self::BLOCK_STRING] = 'BlockString';
|
||||||
$description[self::COMMENT] = 'Comment';
|
$description[self::COMMENT] = 'Comment';
|
||||||
|
|
||||||
return $description[$kind];
|
return $description[$kind];
|
||||||
|
@ -115,7 +115,15 @@ class Visitor
|
|||||||
NodeKind::ARGUMENT => ['name', 'value'],
|
NodeKind::ARGUMENT => ['name', 'value'],
|
||||||
NodeKind::FRAGMENT_SPREAD => ['name', 'directives'],
|
NodeKind::FRAGMENT_SPREAD => ['name', 'directives'],
|
||||||
NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'],
|
NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'],
|
||||||
NodeKind::FRAGMENT_DEFINITION => ['name', 'typeCondition', 'directives', 'selectionSet'],
|
NodeKind::FRAGMENT_DEFINITION => [
|
||||||
|
'name',
|
||||||
|
// Note: fragment variable definitions are experimental and may be changed
|
||||||
|
// or removed in the future.
|
||||||
|
'variableDefinitions',
|
||||||
|
'typeCondition',
|
||||||
|
'directives',
|
||||||
|
'selectionSet'
|
||||||
|
],
|
||||||
|
|
||||||
NodeKind::INT => [],
|
NodeKind::INT => [],
|
||||||
NodeKind::FLOAT => [],
|
NodeKind::FLOAT => [],
|
||||||
@ -133,17 +141,24 @@ class Visitor
|
|||||||
|
|
||||||
NodeKind::SCHEMA_DEFINITION => ['directives', 'operationTypes'],
|
NodeKind::SCHEMA_DEFINITION => ['directives', 'operationTypes'],
|
||||||
NodeKind::OPERATION_TYPE_DEFINITION => ['type'],
|
NodeKind::OPERATION_TYPE_DEFINITION => ['type'],
|
||||||
NodeKind::SCALAR_TYPE_DEFINITION => ['name', 'directives'],
|
NodeKind::SCALAR_TYPE_DEFINITION => ['description', 'name', 'directives'],
|
||||||
NodeKind::OBJECT_TYPE_DEFINITION => ['name', 'interfaces', 'directives', 'fields'],
|
NodeKind::OBJECT_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'],
|
||||||
NodeKind::FIELD_DEFINITION => ['name', 'arguments', 'type', 'directives'],
|
NodeKind::FIELD_DEFINITION => ['description', 'name', 'arguments', 'type', 'directives'],
|
||||||
NodeKind::INPUT_VALUE_DEFINITION => ['name', 'type', 'defaultValue', 'directives'],
|
NodeKind::INPUT_VALUE_DEFINITION => ['description', 'name', 'type', 'defaultValue', 'directives'],
|
||||||
NodeKind::INTERFACE_TYPE_DEFINITION => [ 'name', 'directives', 'fields' ],
|
NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'],
|
||||||
NodeKind::UNION_TYPE_DEFINITION => [ 'name', 'directives', 'types' ],
|
NodeKind::UNION_TYPE_DEFINITION => ['description', 'name', 'directives', 'types'],
|
||||||
NodeKind::ENUM_TYPE_DEFINITION => [ 'name', 'directives', 'values' ],
|
NodeKind::ENUM_TYPE_DEFINITION => ['description', 'name', 'directives', 'values'],
|
||||||
NodeKind::ENUM_VALUE_DEFINITION => [ 'name', 'directives' ],
|
NodeKind::ENUM_VALUE_DEFINITION => ['description', 'name', 'directives'],
|
||||||
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => [ 'name', 'directives', 'fields' ],
|
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'],
|
||||||
NodeKind::TYPE_EXTENSION_DEFINITION => [ 'definition' ],
|
|
||||||
NodeKind::DIRECTIVE_DEFINITION => [ 'name', 'arguments', 'locations' ]
|
NodeKind::SCALAR_TYPE_EXTENSION => ['name', 'directives'],
|
||||||
|
NodeKind::OBJECT_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'],
|
||||||
|
NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'directives', 'fields'],
|
||||||
|
NodeKind::UNION_TYPE_EXTENSION => ['name', 'directives', 'types'],
|
||||||
|
NodeKind::ENUM_TYPE_EXTENSION => ['name', 'directives', 'values'],
|
||||||
|
NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'],
|
||||||
|
|
||||||
|
NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations']
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -180,7 +195,7 @@ class Visitor
|
|||||||
$isEdited = $isLeaving && count($edits) !== 0;
|
$isEdited = $isLeaving && count($edits) !== 0;
|
||||||
|
|
||||||
if ($isLeaving) {
|
if ($isLeaving) {
|
||||||
$key = count($ancestors) === 0 ? $UNDEFINED : array_pop($path);
|
$key = !$ancestors ? $UNDEFINED : $path[count($path) - 1];
|
||||||
$node = $parent;
|
$node = $parent;
|
||||||
$parent = array_pop($ancestors);
|
$parent = array_pop($ancestors);
|
||||||
|
|
||||||
@ -277,7 +292,9 @@ class Visitor
|
|||||||
$edits[] = [$key, $node];
|
$edits[] = [$key, $node];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$isLeaving) {
|
if ($isLeaving) {
|
||||||
|
array_pop($path);
|
||||||
|
} else {
|
||||||
$stack = [
|
$stack = [
|
||||||
'inArray' => $inArray,
|
'inArray' => $inArray,
|
||||||
'index' => $index,
|
'index' => $index,
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
namespace GraphQL\Type\Definition;
|
namespace GraphQL\Type\Definition;
|
||||||
|
|
||||||
use GraphQL\Language\AST\BooleanValueNode;
|
use GraphQL\Language\AST\BooleanValueNode;
|
||||||
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class BooleanType
|
* Class BooleanType
|
||||||
@ -34,18 +35,19 @@ class BooleanType extends ScalarType
|
|||||||
*/
|
*/
|
||||||
public function parseValue($value)
|
public function parseValue($value)
|
||||||
{
|
{
|
||||||
return is_bool($value) ? $value : null;
|
return is_bool($value) ? $value : Utils::undefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $ast
|
* @param $valueNode
|
||||||
|
* @param array|null $variables
|
||||||
* @return bool|null
|
* @return bool|null
|
||||||
*/
|
*/
|
||||||
public function parseLiteral($ast)
|
public function parseLiteral($valueNode, array $variables = null)
|
||||||
{
|
{
|
||||||
if ($ast instanceof BooleanValueNode) {
|
if ($valueNode instanceof BooleanValueNode) {
|
||||||
return (bool) $ast->value;
|
return (bool) $valueNode->value;
|
||||||
}
|
}
|
||||||
return null;
|
return Utils::undefined();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Type\Definition;
|
namespace GraphQL\Type\Definition;
|
||||||
|
|
||||||
|
use GraphQL\Utils\AST;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,23 +25,28 @@ class CustomScalarType extends ScalarType
|
|||||||
*/
|
*/
|
||||||
public function parseValue($value)
|
public function parseValue($value)
|
||||||
{
|
{
|
||||||
|
if (Utils::isInvalid($value)) {
|
||||||
|
return Utils::undefined();
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($this->config['parseValue'])) {
|
if (isset($this->config['parseValue'])) {
|
||||||
return call_user_func($this->config['parseValue'], $value);
|
return call_user_func($this->config['parseValue'], $value);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $valueNode
|
* @param $valueNode
|
||||||
|
* @param array|null $variables
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode)
|
public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode, array $variables = null)
|
||||||
{
|
{
|
||||||
if (isset($this->config['parseLiteral'])) {
|
if (isset($this->config['parseLiteral'])) {
|
||||||
return call_user_func($this->config['parseLiteral'], $valueNode);
|
return call_user_func($this->config['parseLiteral'], $valueNode, $variables);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return AST::valueFromASTUntyped($valueNode, $variables);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
namespace GraphQL\Type\Definition;
|
namespace GraphQL\Type\Definition;
|
||||||
|
|
||||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||||
|
use GraphQL\Language\DirectiveLocation;
|
||||||
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Directive
|
* Class Directive
|
||||||
@ -18,35 +20,6 @@ class Directive
|
|||||||
|
|
||||||
// Schema Definitions
|
// Schema Definitions
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
* @deprecated as of 8.0 (use DirectiveLocation constants directly)
|
|
||||||
*/
|
|
||||||
public static $directiveLocations = [
|
|
||||||
// Operations:
|
|
||||||
DirectiveLocation::QUERY => DirectiveLocation::QUERY,
|
|
||||||
DirectiveLocation::MUTATION => DirectiveLocation::MUTATION,
|
|
||||||
DirectiveLocation::SUBSCRIPTION => DirectiveLocation::SUBSCRIPTION,
|
|
||||||
DirectiveLocation::FIELD => DirectiveLocation::FIELD,
|
|
||||||
DirectiveLocation::FRAGMENT_DEFINITION => DirectiveLocation::FRAGMENT_DEFINITION,
|
|
||||||
DirectiveLocation::FRAGMENT_SPREAD => DirectiveLocation::FRAGMENT_SPREAD,
|
|
||||||
DirectiveLocation::INLINE_FRAGMENT => DirectiveLocation::INLINE_FRAGMENT,
|
|
||||||
|
|
||||||
// Schema Definitions
|
|
||||||
DirectiveLocation::SCHEMA => DirectiveLocation::SCHEMA,
|
|
||||||
DirectiveLocation::SCALAR => DirectiveLocation::SCALAR,
|
|
||||||
DirectiveLocation::OBJECT => DirectiveLocation::OBJECT,
|
|
||||||
DirectiveLocation::FIELD_DEFINITION => DirectiveLocation::FIELD_DEFINITION,
|
|
||||||
DirectiveLocation::ARGUMENT_DEFINITION => DirectiveLocation::ARGUMENT_DEFINITION,
|
|
||||||
DirectiveLocation::IFACE => DirectiveLocation::IFACE,
|
|
||||||
DirectiveLocation::UNION => DirectiveLocation::UNION,
|
|
||||||
DirectiveLocation::ENUM => DirectiveLocation::ENUM,
|
|
||||||
DirectiveLocation::ENUM_VALUE => DirectiveLocation::ENUM_VALUE,
|
|
||||||
DirectiveLocation::INPUT_OBJECT => DirectiveLocation::INPUT_OBJECT,
|
|
||||||
DirectiveLocation::INPUT_FIELD_DEFINITION => DirectiveLocation::INPUT_FIELD_DEFINITION
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Directive
|
* @return Directive
|
||||||
*/
|
*/
|
||||||
@ -74,6 +47,15 @@ class Directive
|
|||||||
return $internal['deprecated'];
|
return $internal['deprecated'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Directive $directive
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isSpecifiedDirective(Directive $directive)
|
||||||
|
{
|
||||||
|
return in_array($directive->name, array_keys(self::getInternalDirectives()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
@ -178,6 +160,9 @@ class Directive
|
|||||||
foreach ($config as $key => $value) {
|
foreach ($config as $key => $value) {
|
||||||
$this->{$key} = $value;
|
$this->{$key} = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Utils::invariant($this->name, 'Directive must be named.');
|
||||||
|
Utils::invariant(is_array($this->locations), 'Must provide locations for directive.');
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Type\Definition;
|
namespace GraphQL\Type\Definition;
|
||||||
|
|
||||||
|
use GraphQL\Language\DirectiveLocation as NewDirectiveLocation;
|
||||||
|
|
||||||
|
trigger_error(
|
||||||
|
'GraphQL\Type\Definition\DirectiveLocation was moved to GraphQL\Language\DirectiveLocation and will be deleted on next release',
|
||||||
|
E_USER_DEPRECATED
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of available directive locations
|
* @deprecated moved to GraphQL\Language\DirectiveLocation
|
||||||
*/
|
*/
|
||||||
class DirectiveLocation
|
class DirectiveLocation extends NewDirectiveLocation
|
||||||
{
|
{
|
||||||
const IFACE = 'INTERFACE';
|
|
||||||
const SUBSCRIPTION = 'SUBSCRIPTION';
|
|
||||||
const FRAGMENT_SPREAD = 'FRAGMENT_SPREAD';
|
|
||||||
const QUERY = 'QUERY';
|
|
||||||
const MUTATION = 'MUTATION';
|
|
||||||
const FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION';
|
|
||||||
const INPUT_OBJECT = 'INPUT_OBJECT';
|
|
||||||
const INLINE_FRAGMENT = 'INLINE_FRAGMENT';
|
|
||||||
const UNION = 'UNION';
|
|
||||||
const SCALAR = 'SCALAR';
|
|
||||||
const FIELD_DEFINITION = 'FIELD_DEFINITION';
|
|
||||||
const ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION';
|
|
||||||
const ENUM = 'ENUM';
|
|
||||||
const OBJECT = 'OBJECT';
|
|
||||||
const ENUM_VALUE = 'ENUM_VALUE';
|
|
||||||
const FIELD = 'FIELD';
|
|
||||||
const SCHEMA = 'SCHEMA';
|
|
||||||
const INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION';
|
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ use GraphQL\Utils\Utils;
|
|||||||
* Class EnumType
|
* Class EnumType
|
||||||
* @package GraphQL\Type\Definition
|
* @package GraphQL\Type\Definition
|
||||||
*/
|
*/
|
||||||
class EnumType extends Type implements InputType, OutputType, LeafType
|
class EnumType extends Type implements InputType, OutputType, LeafType, NamedType
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var EnumTypeDefinitionNode|null
|
* @var EnumTypeDefinitionNode|null
|
||||||
@ -39,7 +39,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType
|
|||||||
$config['name'] = $this->tryInferName();
|
$config['name'] = $this->tryInferName();
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::assertValidName($config['name'], !empty($config['isIntrospection']));
|
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||||
|
|
||||||
Config::validate($config, [
|
Config::validate($config, [
|
||||||
'name' => Config::NAME | Config::REQUIRED,
|
'name' => Config::NAME | Config::REQUIRED,
|
||||||
@ -108,25 +108,11 @@ class EnumType extends Type implements InputType, OutputType, LeafType
|
|||||||
public function serialize($value)
|
public function serialize($value)
|
||||||
{
|
{
|
||||||
$lookup = $this->getValueLookup();
|
$lookup = $this->getValueLookup();
|
||||||
return isset($lookup[$value]) ? $lookup[$value]->name : null;
|
if (isset($lookup[$value])) {
|
||||||
|
return $lookup[$value]->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return Utils::undefined();
|
||||||
* @param string $value
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isValidValue($value)
|
|
||||||
{
|
|
||||||
return is_string($value) && $this->getNameLookup()->offsetExists($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $valueNode
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isValidLiteral($valueNode)
|
|
||||||
{
|
|
||||||
return $valueNode instanceof EnumValueNode && $this->getNameLookup()->offsetExists($valueNode->value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -136,14 +122,15 @@ class EnumType extends Type implements InputType, OutputType, LeafType
|
|||||||
public function parseValue($value)
|
public function parseValue($value)
|
||||||
{
|
{
|
||||||
$lookup = $this->getNameLookup();
|
$lookup = $this->getNameLookup();
|
||||||
return isset($lookup[$value]) ? $lookup[$value]->value : null;
|
return isset($lookup[$value]) ? $lookup[$value]->value : Utils::undefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $value
|
* @param $value
|
||||||
|
* @param array|null $variables
|
||||||
* @return null
|
* @return null
|
||||||
*/
|
*/
|
||||||
public function parseLiteral($value)
|
public function parseLiteral($value, array $variables = null)
|
||||||
{
|
{
|
||||||
if ($value instanceof EnumValueNode) {
|
if ($value instanceof EnumValueNode) {
|
||||||
$lookup = $this->getNameLookup();
|
$lookup = $this->getNameLookup();
|
||||||
@ -201,24 +188,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType
|
|||||||
);
|
);
|
||||||
|
|
||||||
$values = $this->getValues();
|
$values = $this->getValues();
|
||||||
|
|
||||||
Utils::invariant(
|
|
||||||
!empty($values),
|
|
||||||
"{$this->name} values must be not empty."
|
|
||||||
);
|
|
||||||
foreach ($values as $value) {
|
foreach ($values as $value) {
|
||||||
try {
|
|
||||||
Utils::assertValidName($value->name);
|
|
||||||
} catch (InvariantViolation $e) {
|
|
||||||
throw new InvariantViolation(
|
|
||||||
"{$this->name} has value with invalid name: " .
|
|
||||||
Utils::printSafe($value->name) . " ({$e->getMessage()})"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Utils::invariant(
|
|
||||||
!in_array($value->name, ['true', 'false', 'null']),
|
|
||||||
"{$this->name}: \"{$value->name}\" can not be used as an Enum value."
|
|
||||||
);
|
|
||||||
Utils::invariant(
|
Utils::invariant(
|
||||||
!isset($value->config['isDeprecated']),
|
!isset($value->config['isDeprecated']),
|
||||||
"{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"."
|
"{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"."
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
namespace GraphQL\Type\Definition;
|
namespace GraphQL\Type\Definition;
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Error\InvariantViolation;
|
|
||||||
use GraphQL\Language\AST\FloatValueNode;
|
use GraphQL\Language\AST\FloatValueNode;
|
||||||
use GraphQL\Language\AST\IntValueNode;
|
use GraphQL\Language\AST\IntValueNode;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
@ -29,39 +28,50 @@ values as specified by
|
|||||||
/**
|
/**
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
* @return float|null
|
* @return float|null
|
||||||
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public function serialize($value)
|
public function serialize($value)
|
||||||
{
|
{
|
||||||
if (is_numeric($value) || $value === true || $value === false) {
|
return $this->coerceFloat($value);
|
||||||
return (float) $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($value === '') {
|
|
||||||
$err = 'Float cannot represent non numeric value: (empty string)';
|
|
||||||
} else {
|
|
||||||
$err = sprintf('Float cannot represent non numeric value: %s', Utils::printSafe($value));
|
|
||||||
}
|
|
||||||
throw new InvariantViolation($err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
* @return float|null
|
* @return float|null
|
||||||
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public function parseValue($value)
|
public function parseValue($value)
|
||||||
{
|
{
|
||||||
return (is_numeric($value) && !is_string($value)) ? (float) $value : null;
|
return $this->coerceFloat($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $ast
|
* @param $valueNode
|
||||||
|
* @param array|null $variables
|
||||||
* @return float|null
|
* @return float|null
|
||||||
*/
|
*/
|
||||||
public function parseLiteral($ast)
|
public function parseLiteral($valueNode, array $variables = null)
|
||||||
{
|
{
|
||||||
if ($ast instanceof FloatValueNode || $ast instanceof IntValueNode) {
|
if ($valueNode instanceof FloatValueNode || $valueNode instanceof IntValueNode) {
|
||||||
return (float) $ast->value;
|
return (float) $valueNode->value;
|
||||||
}
|
}
|
||||||
return null;
|
return Utils::undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function coerceFloat($value) {
|
||||||
|
if ($value === '') {
|
||||||
|
throw new Error(
|
||||||
|
'Float cannot represent non numeric value: (empty string)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_numeric($value) && $value !== true && $value !== false) {
|
||||||
|
throw new Error(
|
||||||
|
'Float cannot represent non numeric value: ' .
|
||||||
|
Utils::printSafe($value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float) $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
namespace GraphQL\Type\Definition;
|
namespace GraphQL\Type\Definition;
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Error\InvariantViolation;
|
|
||||||
use GraphQL\Language\AST\IntValueNode;
|
use GraphQL\Language\AST\IntValueNode;
|
||||||
use GraphQL\Language\AST\StringValueNode;
|
use GraphQL\Language\AST\StringValueNode;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
@ -44,7 +43,7 @@ When expected as an input type, any string (such as `"4"`) or integer
|
|||||||
return 'null';
|
return 'null';
|
||||||
}
|
}
|
||||||
if (!is_scalar($value) && (!is_object($value) || !method_exists($value, '__toString'))) {
|
if (!is_scalar($value) && (!is_object($value) || !method_exists($value, '__toString'))) {
|
||||||
throw new InvariantViolation("ID type cannot represent non scalar value: " . Utils::printSafe($value));
|
throw new Error("ID type cannot represent non scalar value: " . Utils::printSafe($value));
|
||||||
}
|
}
|
||||||
return (string) $value;
|
return (string) $value;
|
||||||
}
|
}
|
||||||
@ -55,18 +54,19 @@ When expected as an input type, any string (such as `"4"`) or integer
|
|||||||
*/
|
*/
|
||||||
public function parseValue($value)
|
public function parseValue($value)
|
||||||
{
|
{
|
||||||
return (is_string($value) || is_int($value)) ? (string) $value : null;
|
return (is_string($value) || is_int($value)) ? (string) $value : Utils::undefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $ast
|
* @param $ast
|
||||||
|
* @param array|null $variables
|
||||||
* @return null|string
|
* @return null|string
|
||||||
*/
|
*/
|
||||||
public function parseLiteral($ast)
|
public function parseLiteral($valueNode, array $variables = null)
|
||||||
{
|
{
|
||||||
if ($ast instanceof StringValueNode || $ast instanceof IntValueNode) {
|
if ($valueNode instanceof StringValueNode || $valueNode instanceof IntValueNode) {
|
||||||
return $ast->value;
|
return $valueNode->value;
|
||||||
}
|
}
|
||||||
return null;
|
return Utils::undefined();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ use GraphQL\Utils\Utils;
|
|||||||
* Class InputObjectType
|
* Class InputObjectType
|
||||||
* @package GraphQL\Type\Definition
|
* @package GraphQL\Type\Definition
|
||||||
*/
|
*/
|
||||||
class InputObjectType extends Type implements InputType
|
class InputObjectType extends Type implements InputType, NamedType
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var InputObjectField[]
|
* @var InputObjectField[]
|
||||||
@ -31,7 +31,7 @@ class InputObjectType extends Type implements InputType
|
|||||||
$config['name'] = $this->tryInferName();
|
$config['name'] = $this->tryInferName();
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::assertValidName($config['name']);
|
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||||
|
|
||||||
Config::validate($config, [
|
Config::validate($config, [
|
||||||
'name' => Config::NAME | Config::REQUIRED,
|
'name' => Config::NAME | Config::REQUIRED,
|
||||||
@ -91,41 +91,4 @@ class InputObjectType extends Type implements InputType
|
|||||||
Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name);
|
Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name);
|
||||||
return $this->fields[$name];
|
return $this->fields[$name];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws InvariantViolation
|
|
||||||
*/
|
|
||||||
public function assertValid()
|
|
||||||
{
|
|
||||||
parent::assertValid();
|
|
||||||
|
|
||||||
$fields = $this->getFields();
|
|
||||||
|
|
||||||
Utils::invariant(
|
|
||||||
!empty($fields),
|
|
||||||
"{$this->name} fields must not be empty"
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($fields as $field) {
|
|
||||||
try {
|
|
||||||
Utils::assertValidName($field->name);
|
|
||||||
} catch (InvariantViolation $e) {
|
|
||||||
throw new InvariantViolation("{$this->name}.{$field->name}: {$e->getMessage()}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$fieldType = $field->type;
|
|
||||||
if ($fieldType instanceof WrappingType) {
|
|
||||||
$fieldType = $fieldType->getWrappedType(true);
|
|
||||||
}
|
|
||||||
Utils::invariant(
|
|
||||||
$fieldType instanceof InputType,
|
|
||||||
"{$this->name}.{$field->name} field type must be Input Type but got: %s.",
|
|
||||||
Utils::printSafe($field->type)
|
|
||||||
);
|
|
||||||
Utils::invariant(
|
|
||||||
!isset($field->config['resolve']),
|
|
||||||
"{$this->name}.{$field->name} field type has a resolve property, but Input Types cannot define resolvers."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,16 @@ namespace GraphQL\Type\Definition;
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
export type GraphQLInputType =
|
export type GraphQLInputType =
|
||||||
GraphQLScalarType |
|
| GraphQLScalarType
|
||||||
GraphQLEnumType |
|
| GraphQLEnumType
|
||||||
GraphQLInputObjectType |
|
| GraphQLInputObjectType
|
||||||
GraphQLList |
|
| GraphQLList<GraphQLInputType>
|
||||||
GraphQLNonNull;
|
| GraphQLNonNull<
|
||||||
|
| GraphQLScalarType
|
||||||
|
| GraphQLEnumType
|
||||||
|
| GraphQLInputObjectType
|
||||||
|
| GraphQLList<GraphQLInputType>,
|
||||||
|
>;
|
||||||
*/
|
*/
|
||||||
interface InputType
|
interface InputType
|
||||||
{
|
{
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
namespace GraphQL\Type\Definition;
|
namespace GraphQL\Type\Definition;
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Error\InvariantViolation;
|
|
||||||
use GraphQL\Language\AST\IntValueNode;
|
use GraphQL\Language\AST\IntValueNode;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
@ -35,63 +34,60 @@ values. Int can represent values between -(2^31) and 2^31 - 1. ';
|
|||||||
/**
|
/**
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
* @return int|null
|
* @return int|null
|
||||||
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public function serialize($value)
|
public function serialize($value)
|
||||||
{
|
{
|
||||||
if ($value === '') {
|
return $this->coerceInt($value);
|
||||||
throw new InvariantViolation('Int cannot represent non 32-bit signed integer value: (empty string)');
|
|
||||||
}
|
|
||||||
if (false === $value || true === $value) {
|
|
||||||
return (int) $value;
|
|
||||||
}
|
|
||||||
if (!is_numeric($value) || $value > self::MAX_INT || $value < self::MIN_INT) {
|
|
||||||
throw new InvariantViolation(sprintf(
|
|
||||||
'Int cannot represent non 32-bit signed integer value: %s',
|
|
||||||
Utils::printSafe($value)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
$num = (float) $value;
|
|
||||||
|
|
||||||
// The GraphQL specification does not allow serializing non-integer values
|
|
||||||
// as Int to avoid accidental data loss.
|
|
||||||
// Examples: 1.0 == 1; 1.1 != 1, etc
|
|
||||||
if ($num != (int) $value) {
|
|
||||||
// Additionally account for scientific notation (i.e. 1e3), because (float)'1e3' is 1000, but (int)'1e3' is 1
|
|
||||||
$trimmed = floor($num);
|
|
||||||
if ($trimmed !== $num) {
|
|
||||||
throw new InvariantViolation(sprintf(
|
|
||||||
'Int cannot represent non-integer value: %s',
|
|
||||||
Utils::printSafe($value)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (int) $value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
* @return int|null
|
* @return int|null
|
||||||
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public function parseValue($value)
|
public function parseValue($value)
|
||||||
{
|
{
|
||||||
// Below is a fix against PHP bug where (in some combinations of OSs and versions)
|
return $this->coerceInt($value);
|
||||||
// boundary values are treated as "double" vs "integer" and failing is_int() check
|
|
||||||
$isInt = is_int($value) || $value === self::MIN_INT || $value === self::MAX_INT;
|
|
||||||
return $isInt && $value <= self::MAX_INT && $value >= self::MIN_INT ? $value : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $ast
|
* @param $valueNode
|
||||||
|
* @param array|null $variables
|
||||||
* @return int|null
|
* @return int|null
|
||||||
*/
|
*/
|
||||||
public function parseLiteral($ast)
|
public function parseLiteral($valueNode, array $variables = null)
|
||||||
{
|
{
|
||||||
if ($ast instanceof IntValueNode) {
|
if ($valueNode instanceof IntValueNode) {
|
||||||
$val = (int) $ast->value;
|
$val = (int) $valueNode->value;
|
||||||
if ($ast->value === (string) $val && self::MIN_INT <= $val && $val <= self::MAX_INT) {
|
if ($valueNode->value === (string) $val && self::MIN_INT <= $val && $val <= self::MAX_INT) {
|
||||||
return $val;
|
return $val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return Utils::undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function coerceInt($value) {
|
||||||
|
if ($value === '') {
|
||||||
|
throw new Error(
|
||||||
|
'Int cannot represent non 32-bit signed integer value: (empty string)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$num = floatval($value);
|
||||||
|
if (!is_numeric($value) && !is_bool($value) || $num > self::MAX_INT || $num < self::MIN_INT) {
|
||||||
|
throw new Error(
|
||||||
|
'Int cannot represent non 32-bit signed integer value: ' .
|
||||||
|
Utils::printSafe($value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$int = intval($num);
|
||||||
|
if ($int != $num) {
|
||||||
|
throw new Error(
|
||||||
|
'Int cannot represent non-integer value: ' .
|
||||||
|
Utils::printSafe($value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $int;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,29 @@ 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class InterfaceType
|
* Class InterfaceType
|
||||||
* @package GraphQL\Type\Definition
|
* @package GraphQL\Type\Definition
|
||||||
*/
|
*/
|
||||||
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType
|
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NamedType
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $type
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function assertInterfaceType($type)
|
||||||
|
{
|
||||||
|
Utils::invariant(
|
||||||
|
$type instanceof self,
|
||||||
|
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Interface type.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var FieldDefinition[]
|
* @var FieldDefinition[]
|
||||||
*/
|
*/
|
||||||
@ -21,6 +36,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
|
||||||
@ -31,7 +51,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
|||||||
$config['name'] = $this->tryInferName();
|
$config['name'] = $this->tryInferName();
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::assertValidName($config['name']);
|
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||||
|
|
||||||
Config::validate($config, [
|
Config::validate($config, [
|
||||||
'name' => Config::NAME,
|
'name' => Config::NAME,
|
||||||
@ -46,6 +66,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,23 +120,9 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
|||||||
{
|
{
|
||||||
parent::assertValid();
|
parent::assertValid();
|
||||||
|
|
||||||
$fields = $this->getFields();
|
|
||||||
|
|
||||||
Utils::invariant(
|
Utils::invariant(
|
||||||
!isset($this->config['resolveType']) || is_callable($this->config['resolveType']),
|
!isset($this->config['resolveType']) || is_callable($this->config['resolveType']),
|
||||||
"{$this->name} must provide \"resolveType\" as a function."
|
"{$this->name} must provide \"resolveType\" as a function."
|
||||||
);
|
);
|
||||||
|
|
||||||
Utils::invariant(
|
|
||||||
!empty($fields),
|
|
||||||
"{$this->name} fields must not be empty"
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($fields as $field) {
|
|
||||||
$field->assertValid($this);
|
|
||||||
foreach ($field->args as $arg) {
|
|
||||||
$arg->assertValid($field, $this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Type\Definition;
|
namespace GraphQL\Type\Definition;
|
||||||
|
|
||||||
|
use \GraphQL\Language\AST\Node;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
export type GraphQLLeafType =
|
export type GraphQLLeafType =
|
||||||
GraphQLScalarType |
|
GraphQLScalarType |
|
||||||
@ -19,6 +21,8 @@ interface LeafType
|
|||||||
/**
|
/**
|
||||||
* Parses an externally provided value (query variable) to use as an input
|
* Parses an externally provided value (query variable) to use as an input
|
||||||
*
|
*
|
||||||
|
* In the case of an invalid value this method must return Utils::undefined()
|
||||||
|
*
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
@ -27,20 +31,11 @@ interface LeafType
|
|||||||
/**
|
/**
|
||||||
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input
|
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input
|
||||||
*
|
*
|
||||||
* @param \GraphQL\Language\AST\Node $valueNode
|
* In the case of an invalid value this method must return Utils::undefined()
|
||||||
|
*
|
||||||
|
* @param Node $valueNode
|
||||||
|
* @param array|null $variables
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function parseLiteral($valueNode);
|
public function parseLiteral($valueNode, array $variables = null);
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $value
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isValidValue($value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \GraphQL\Language\AST\Node $valueNode
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function isValidLiteral($valueNode);
|
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,7 @@ class ListOfType extends Type implements WrappingType, OutputType, InputType
|
|||||||
*/
|
*/
|
||||||
public function __construct($type)
|
public function __construct($type)
|
||||||
{
|
{
|
||||||
if (!$type instanceof Type && !is_callable($type)) {
|
$this->ofType = Type::assertType($type);
|
||||||
throw new InvariantViolation(
|
|
||||||
'Can only create List of a GraphQLType but got: ' . Utils::printSafe($type)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$this->ofType = $type;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
15
src/Type/Definition/NamedType.php
Normal file
15
src/Type/Definition/NamedType.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Type\Definition;
|
||||||
|
|
||||||
|
/*
|
||||||
|
export type GraphQLNamedType =
|
||||||
|
| GraphQLScalarType
|
||||||
|
| GraphQLObjectType
|
||||||
|
| GraphQLInterfaceType
|
||||||
|
| GraphQLUnionType
|
||||||
|
| GraphQLEnumType
|
||||||
|
| GraphQLInputObjectType;
|
||||||
|
*/
|
||||||
|
interface NamedType
|
||||||
|
{
|
||||||
|
}
|
@ -11,7 +11,35 @@ use GraphQL\Utils\Utils;
|
|||||||
class NonNull extends Type implements WrappingType, OutputType, InputType
|
class NonNull extends Type implements WrappingType, OutputType, InputType
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType
|
* @param mixed $type
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function assertNullType($type)
|
||||||
|
{
|
||||||
|
Utils::invariant(
|
||||||
|
$type instanceof self,
|
||||||
|
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Non-Null type.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $type
|
||||||
|
* @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType
|
||||||
|
*/
|
||||||
|
public static function assertNullableType($type)
|
||||||
|
{
|
||||||
|
Utils::invariant(
|
||||||
|
Type::isType($type) && !$type instanceof self,
|
||||||
|
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL nullable type.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType
|
||||||
*/
|
*/
|
||||||
private $ofType;
|
private $ofType;
|
||||||
|
|
||||||
@ -21,37 +49,17 @@ class NonNull extends Type implements WrappingType, OutputType, InputType
|
|||||||
*/
|
*/
|
||||||
public function __construct($type)
|
public function __construct($type)
|
||||||
{
|
{
|
||||||
if (!$type instanceof Type && !is_callable($type)) {
|
$this->ofType = self::assertNullableType($type);
|
||||||
throw new InvariantViolation(
|
|
||||||
'Can only create NonNull of a Nullable GraphQLType but got: ' . Utils::printSafe($type)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ($type instanceof NonNull) {
|
|
||||||
throw new InvariantViolation(
|
|
||||||
'Can only create NonNull of a Nullable GraphQLType but got: ' . Utils::printSafe($type)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Utils::invariant(
|
|
||||||
!($type instanceof NonNull),
|
|
||||||
'Cannot nest NonNull inside NonNull'
|
|
||||||
);
|
|
||||||
$this->ofType = $type;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param bool $recurse
|
* @param bool $recurse
|
||||||
* @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType
|
* @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType
|
||||||
* @throws InvariantViolation
|
* @throws InvariantViolation
|
||||||
*/
|
*/
|
||||||
public function getWrappedType($recurse = false)
|
public function getWrappedType($recurse = false)
|
||||||
{
|
{
|
||||||
$type = $this->ofType;
|
$type = $this->ofType;
|
||||||
|
|
||||||
Utils::invariant(
|
|
||||||
!($type instanceof NonNull),
|
|
||||||
'Cannot nest NonNull inside NonNull'
|
|
||||||
);
|
|
||||||
|
|
||||||
return ($recurse && $type instanceof WrappingType) ? $type->getWrappedType($recurse) : $type;
|
return ($recurse && $type instanceof WrappingType) ? $type->getWrappedType($recurse) : $type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ namespace GraphQL\Type\Definition;
|
|||||||
|
|
||||||
use GraphQL\Error\InvariantViolation;
|
use GraphQL\Error\InvariantViolation;
|
||||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||||
use GraphQL\Language\AST\TypeExtensionDefinitionNode;
|
use GraphQL\Language\AST\ObjectTypeExtensionNode;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
|
|
||||||
@ -47,8 +47,22 @@ use GraphQL\Utils\Utils;
|
|||||||
* ]);
|
* ]);
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
class ObjectType extends Type implements OutputType, CompositeType
|
class ObjectType extends Type implements OutputType, CompositeType, NamedType
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $type
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function assertObjectType($type)
|
||||||
|
{
|
||||||
|
Utils::invariant(
|
||||||
|
$type instanceof self,
|
||||||
|
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Object type.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var FieldDefinition[]
|
* @var FieldDefinition[]
|
||||||
*/
|
*/
|
||||||
@ -70,7 +84,7 @@ class ObjectType extends Type implements OutputType, CompositeType
|
|||||||
public $astNode;
|
public $astNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var TypeExtensionDefinitionNode[]
|
* @var ObjectTypeExtensionNode[]
|
||||||
*/
|
*/
|
||||||
public $extensionASTNodes;
|
public $extensionASTNodes;
|
||||||
|
|
||||||
@ -89,7 +103,7 @@ class ObjectType extends Type implements OutputType, CompositeType
|
|||||||
$config['name'] = $this->tryInferName();
|
$config['name'] = $this->tryInferName();
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::assertValidName($config['name'], !empty($config['isIntrospection']));
|
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||||
|
|
||||||
// Note: this validation is disabled by default, because it is resource-consuming
|
// Note: this validation is disabled by default, because it is resource-consuming
|
||||||
// TODO: add bin/validate script to check if schema is valid during development
|
// TODO: add bin/validate script to check if schema is valid during development
|
||||||
@ -152,13 +166,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;
|
||||||
}
|
}
|
||||||
@ -214,41 +228,5 @@ class ObjectType extends Type implements OutputType, CompositeType
|
|||||||
!isset($this->config['isTypeOf']) || is_callable($this->config['isTypeOf']),
|
!isset($this->config['isTypeOf']) || is_callable($this->config['isTypeOf']),
|
||||||
"{$this->name} must provide 'isTypeOf' as a function"
|
"{$this->name} must provide 'isTypeOf' as a function"
|
||||||
);
|
);
|
||||||
|
|
||||||
// getFields() and getInterfaceMap() will do structural validation
|
|
||||||
$fields = $this->getFields();
|
|
||||||
Utils::invariant(
|
|
||||||
!empty($fields),
|
|
||||||
"{$this->name} fields must not be empty"
|
|
||||||
);
|
|
||||||
foreach ($fields as $field) {
|
|
||||||
$field->assertValid($this);
|
|
||||||
foreach ($field->args as $arg) {
|
|
||||||
$arg->assertValid($field, $this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$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;
|
|
||||||
if (!isset($iface->config['resolveType'])) {
|
|
||||||
Utils::invariant(
|
|
||||||
isset($this->config['isTypeOf']),
|
|
||||||
"Interface Type {$iface->name} does not provide a \"resolveType\" " .
|
|
||||||
"function and implementing Type {$this->name} does not provide a " .
|
|
||||||
'"isTypeOf" function. There is no way to resolve this implementing ' .
|
|
||||||
'type during execution.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ use GraphQL\Utils\Utils;
|
|||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
abstract class ScalarType extends Type implements OutputType, InputType, LeafType
|
abstract class ScalarType extends Type implements OutputType, InputType, LeafType, NamedType
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var ScalarTypeDefinitionNode|null
|
* @var ScalarTypeDefinitionNode|null
|
||||||
@ -36,30 +36,6 @@ abstract class ScalarType extends Type implements OutputType, InputType, LeafTyp
|
|||||||
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
|
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
|
|
||||||
Utils::assertValidName($this->name);
|
Utils::invariant(is_string($this->name), 'Must provide name.');
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if an internal value is valid for this type.
|
|
||||||
* Equivalent to checking for if the parsedValue is nullish.
|
|
||||||
*
|
|
||||||
* @param $value
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isValidValue($value)
|
|
||||||
{
|
|
||||||
return null !== $this->parseValue($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if an internal value is valid for this type.
|
|
||||||
* Equivalent to checking for if the parsedLiteral is nullish.
|
|
||||||
*
|
|
||||||
* @param $valueNode
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isValidLiteral($valueNode)
|
|
||||||
{
|
|
||||||
return null !== $this->parseLiteral($valueNode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
namespace GraphQL\Type\Definition;
|
namespace GraphQL\Type\Definition;
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Error\InvariantViolation;
|
|
||||||
use GraphQL\Language\AST\StringValueNode;
|
use GraphQL\Language\AST\StringValueNode;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
@ -28,6 +27,7 @@ represent free-form human-readable text.';
|
|||||||
/**
|
/**
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
* @return mixed|string
|
* @return mixed|string
|
||||||
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public function serialize($value)
|
public function serialize($value)
|
||||||
{
|
{
|
||||||
@ -41,29 +41,42 @@ represent free-form human-readable text.';
|
|||||||
return 'null';
|
return 'null';
|
||||||
}
|
}
|
||||||
if (!is_scalar($value)) {
|
if (!is_scalar($value)) {
|
||||||
throw new InvariantViolation("String cannot represent non scalar value: " . Utils::printSafe($value));
|
throw new Error("String cannot represent non scalar value: " . Utils::printSafe($value));
|
||||||
}
|
}
|
||||||
return (string) $value;
|
return $this->coerceString($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
* @return string
|
* @return string
|
||||||
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public function parseValue($value)
|
public function parseValue($value)
|
||||||
{
|
{
|
||||||
return is_string($value) ? $value : null;
|
return $this->coerceString($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $ast
|
* @param $valueNode
|
||||||
|
* @param array|null $variables
|
||||||
* @return null|string
|
* @return null|string
|
||||||
*/
|
*/
|
||||||
public function parseLiteral($ast)
|
public function parseLiteral($valueNode, array $variables = null)
|
||||||
{
|
{
|
||||||
if ($ast instanceof StringValueNode) {
|
if ($valueNode instanceof StringValueNode) {
|
||||||
return $ast->value;
|
return $valueNode->value;
|
||||||
}
|
}
|
||||||
return null;
|
return Utils::undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function coerceString($value) {
|
||||||
|
if (is_array($value)) {
|
||||||
|
throw new Error(
|
||||||
|
'String cannot represent an array value: ' .
|
||||||
|
Utils::printSafe($value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,12 @@
|
|||||||
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\NonNullType;
|
||||||
use GraphQL\Language\AST\TypeDefinitionNode;
|
use GraphQL\Language\AST\TypeDefinitionNode;
|
||||||
|
use GraphQL\Type\Introspection;
|
||||||
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry of standard GraphQL types
|
* Registry of standard GraphQL types
|
||||||
@ -23,6 +28,11 @@ abstract class Type implements \JsonSerializable
|
|||||||
*/
|
*/
|
||||||
private static $internalTypes;
|
private static $internalTypes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private static $builtInTypes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api
|
* @api
|
||||||
* @return IDType
|
* @return IDType
|
||||||
@ -70,7 +80,7 @@ abstract class Type implements \JsonSerializable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @api
|
* @api
|
||||||
* @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
|
* @param Type|ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
|
||||||
* @return ListOfType
|
* @return ListOfType
|
||||||
*/
|
*/
|
||||||
public static function listOf($wrappedType)
|
public static function listOf($wrappedType)
|
||||||
@ -107,6 +117,8 @@ abstract class Type implements \JsonSerializable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Returns all builtin scalar types
|
||||||
|
*
|
||||||
* @return Type[]
|
* @return Type[]
|
||||||
*/
|
*/
|
||||||
public static function getInternalTypes()
|
public static function getInternalTypes()
|
||||||
@ -114,6 +126,34 @@ abstract class Type implements \JsonSerializable
|
|||||||
return self::getInternalType();
|
return self::getInternalType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all builtin in types including base scalar and
|
||||||
|
* introspection types
|
||||||
|
*
|
||||||
|
* @return Type[]
|
||||||
|
*/
|
||||||
|
public static function getAllBuiltInTypes()
|
||||||
|
{
|
||||||
|
if (null === self::$builtInTypes) {
|
||||||
|
self::$builtInTypes = array_merge(
|
||||||
|
Introspection::getTypes(),
|
||||||
|
self::getInternalTypes()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return self::$builtInTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the type is a builtin type
|
||||||
|
*
|
||||||
|
* @param Type $type
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isBuiltInType(Type $type)
|
||||||
|
{
|
||||||
|
return in_array($type->name, array_keys(self::getAllBuiltInTypes()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api
|
* @api
|
||||||
* @param Type $type
|
* @param Type $type
|
||||||
@ -121,8 +161,11 @@ abstract class Type implements \JsonSerializable
|
|||||||
*/
|
*/
|
||||||
public static function isInputType($type)
|
public static function isInputType($type)
|
||||||
{
|
{
|
||||||
$nakedType = self::getNamedType($type);
|
return $type instanceof InputType &&
|
||||||
return $nakedType instanceof InputType;
|
(
|
||||||
|
!$type instanceof WrappingType ||
|
||||||
|
self::getNamedType($type) instanceof InputType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -132,8 +175,11 @@ abstract class Type implements \JsonSerializable
|
|||||||
*/
|
*/
|
||||||
public static function isOutputType($type)
|
public static function isOutputType($type)
|
||||||
{
|
{
|
||||||
$nakedType = self::getNamedType($type);
|
return $type instanceof OutputType &&
|
||||||
return $nakedType instanceof OutputType;
|
(
|
||||||
|
!$type instanceof WrappingType ||
|
||||||
|
self::getNamedType($type) instanceof OutputType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -166,6 +212,39 @@ 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 ListOfType ||
|
||||||
|
$type instanceof NonNull
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $type
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function assertType($type)
|
||||||
|
{
|
||||||
|
Utils::invariant(
|
||||||
|
self::isType($type),
|
||||||
|
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL type.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api
|
* @api
|
||||||
* @param Type $type
|
* @param Type $type
|
||||||
@ -238,6 +317,7 @@ abstract class Type implements \JsonSerializable
|
|||||||
*/
|
*/
|
||||||
public function assertValid()
|
public function assertValid()
|
||||||
{
|
{
|
||||||
|
Utils::assertValidName($this->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -9,7 +9,7 @@ use GraphQL\Utils\Utils;
|
|||||||
* Class UnionType
|
* Class UnionType
|
||||||
* @package GraphQL\Type\Definition
|
* @package GraphQL\Type\Definition
|
||||||
*/
|
*/
|
||||||
class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
class UnionType extends Type implements AbstractType, OutputType, CompositeType, NamedType
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var UnionTypeDefinitionNode
|
* @var UnionTypeDefinitionNode
|
||||||
@ -36,7 +36,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
|||||||
$config['name'] = $this->tryInferName();
|
$config['name'] = $this->tryInferName();
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::assertValidName($config['name']);
|
Utils::invariant(is_string($config['name']), 'Must provide name.');
|
||||||
|
|
||||||
Config::validate($config, [
|
Config::validate($config, [
|
||||||
'name' => Config::NAME | Config::REQUIRED,
|
'name' => Config::NAME | Config::REQUIRED,
|
||||||
@ -81,7 +81,8 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
|||||||
|
|
||||||
if (!is_array($types)) {
|
if (!is_array($types)) {
|
||||||
throw new InvariantViolation(
|
throw new InvariantViolation(
|
||||||
"{$this->name} types must be an Array or a callable which returns an Array."
|
"Must provide Array of types or a callable which returns " .
|
||||||
|
"such an array for Union {$this->name}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,40 +134,11 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
|||||||
{
|
{
|
||||||
parent::assertValid();
|
parent::assertValid();
|
||||||
|
|
||||||
$types = $this->getTypes();
|
|
||||||
Utils::invariant(
|
|
||||||
!empty($types),
|
|
||||||
"{$this->name} types must not be empty"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isset($this->config['resolveType'])) {
|
if (isset($this->config['resolveType'])) {
|
||||||
Utils::invariant(
|
Utils::invariant(
|
||||||
is_callable($this->config['resolveType']),
|
is_callable($this->config['resolveType']),
|
||||||
"{$this->name} must provide \"resolveType\" as a function."
|
"{$this->name} must provide \"resolveType\" as a function."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$includedTypeNames = [];
|
|
||||||
foreach ($types as $objType) {
|
|
||||||
Utils::invariant(
|
|
||||||
$objType instanceof ObjectType,
|
|
||||||
"{$this->name} may only contain Object types, it cannot contain: %s.",
|
|
||||||
Utils::printSafe($objType)
|
|
||||||
);
|
|
||||||
Utils::invariant(
|
|
||||||
!isset($includedTypeNames[$objType->name]),
|
|
||||||
"{$this->name} can include {$objType->name} type only once."
|
|
||||||
);
|
|
||||||
$includedTypeNames[$objType->name] = true;
|
|
||||||
if (!isset($this->config['resolveType'])) {
|
|
||||||
Utils::invariant(
|
|
||||||
isset($objType->config['isTypeOf']) && is_callable($objType->config['isTypeOf']),
|
|
||||||
"Union type \"{$this->name}\" does not provide a \"resolveType\" " .
|
|
||||||
"function and possible type \"{$objType->name}\" does not provide an " .
|
|
||||||
'"isTypeOf" function. There is no way to resolve this possible type ' .
|
|
||||||
'during execution.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Type;
|
namespace GraphQL\Type;
|
||||||
|
|
||||||
|
|
||||||
use GraphQL\Language\Printer;
|
use GraphQL\Language\Printer;
|
||||||
use GraphQL\Type\Definition\Directive;
|
use GraphQL\Type\Definition\Directive;
|
||||||
use GraphQL\Type\Definition\DirectiveLocation;
|
use GraphQL\Language\DirectiveLocation;
|
||||||
use GraphQL\Type\Definition\EnumType;
|
use GraphQL\Type\Definition\EnumType;
|
||||||
use GraphQL\Type\Definition\FieldArgument;
|
use GraphQL\Type\Definition\FieldArgument;
|
||||||
use GraphQL\Type\Definition\FieldDefinition;
|
use GraphQL\Type\Definition\FieldDefinition;
|
||||||
@ -38,11 +37,25 @@ class Introspection
|
|||||||
private static $map = [];
|
private static $map = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Options:
|
||||||
|
* - descriptions
|
||||||
|
* Whether to include descriptions in the introspection result.
|
||||||
|
* Default: true
|
||||||
|
*
|
||||||
|
* @param array $options
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function getIntrospectionQuery($includeDescription = true)
|
public static function getIntrospectionQuery($options = [])
|
||||||
{
|
{
|
||||||
$withDescription = <<<'EOD'
|
if (is_bool($options)) {
|
||||||
|
trigger_error('Calling Introspection::getIntrospectionQuery(boolean) is deprecated. Please use Introspection::getIntrospectionQuery(["descriptions" => boolean]).', E_USER_DEPRECATED);
|
||||||
|
$descriptions = $options;
|
||||||
|
} else {
|
||||||
|
$descriptions = !array_key_exists('descriptions', $options) || $options['descriptions'] === true;
|
||||||
|
}
|
||||||
|
$descriptionField = $descriptions ? 'description' : '';
|
||||||
|
|
||||||
|
return <<<EOD
|
||||||
query IntrospectionQuery {
|
query IntrospectionQuery {
|
||||||
__schema {
|
__schema {
|
||||||
queryType { name }
|
queryType { name }
|
||||||
@ -53,7 +66,7 @@ class Introspection
|
|||||||
}
|
}
|
||||||
directives {
|
directives {
|
||||||
name
|
name
|
||||||
description
|
{$descriptionField}
|
||||||
locations
|
locations
|
||||||
args {
|
args {
|
||||||
...InputValue
|
...InputValue
|
||||||
@ -65,10 +78,10 @@ class Introspection
|
|||||||
fragment FullType on __Type {
|
fragment FullType on __Type {
|
||||||
kind
|
kind
|
||||||
name
|
name
|
||||||
description
|
{$descriptionField}
|
||||||
fields(includeDeprecated: true) {
|
fields(includeDeprecated: true) {
|
||||||
name
|
name
|
||||||
description
|
{$descriptionField}
|
||||||
args {
|
args {
|
||||||
...InputValue
|
...InputValue
|
||||||
}
|
}
|
||||||
@ -86,7 +99,7 @@ class Introspection
|
|||||||
}
|
}
|
||||||
enumValues(includeDeprecated: true) {
|
enumValues(includeDeprecated: true) {
|
||||||
name
|
name
|
||||||
description
|
{$descriptionField}
|
||||||
isDeprecated
|
isDeprecated
|
||||||
deprecationReason
|
deprecationReason
|
||||||
}
|
}
|
||||||
@ -97,7 +110,7 @@ class Introspection
|
|||||||
|
|
||||||
fragment InputValue on __InputValue {
|
fragment InputValue on __InputValue {
|
||||||
name
|
name
|
||||||
description
|
{$descriptionField}
|
||||||
type { ...TypeRef }
|
type { ...TypeRef }
|
||||||
defaultValue
|
defaultValue
|
||||||
}
|
}
|
||||||
@ -135,95 +148,6 @@ class Introspection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOD;
|
EOD;
|
||||||
$withoutDescription = <<<'EOD'
|
|
||||||
query IntrospectionQuery {
|
|
||||||
__schema {
|
|
||||||
queryType { name }
|
|
||||||
mutationType { name }
|
|
||||||
subscriptionType { name }
|
|
||||||
types {
|
|
||||||
...FullType
|
|
||||||
}
|
|
||||||
directives {
|
|
||||||
name
|
|
||||||
locations
|
|
||||||
args {
|
|
||||||
...InputValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment FullType on __Type {
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
fields(includeDeprecated: true) {
|
|
||||||
name
|
|
||||||
args {
|
|
||||||
...InputValue
|
|
||||||
}
|
|
||||||
type {
|
|
||||||
...TypeRef
|
|
||||||
}
|
|
||||||
isDeprecated
|
|
||||||
deprecationReason
|
|
||||||
}
|
|
||||||
inputFields {
|
|
||||||
...InputValue
|
|
||||||
}
|
|
||||||
interfaces {
|
|
||||||
...TypeRef
|
|
||||||
}
|
|
||||||
enumValues(includeDeprecated: true) {
|
|
||||||
name
|
|
||||||
isDeprecated
|
|
||||||
deprecationReason
|
|
||||||
}
|
|
||||||
possibleTypes {
|
|
||||||
...TypeRef
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment InputValue on __InputValue {
|
|
||||||
name
|
|
||||||
type { ...TypeRef }
|
|
||||||
defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment TypeRef on __Type {
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
ofType {
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
ofType {
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
ofType {
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
ofType {
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
ofType {
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
ofType {
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
ofType {
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOD;
|
|
||||||
return $includeDescription ? $withDescription : $withoutDescription;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getTypes()
|
public static function getTypes()
|
||||||
@ -240,6 +164,15 @@ EOD;
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Type $type
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isIntrospectionType($type)
|
||||||
|
{
|
||||||
|
return in_array($type->name, array_keys(self::getTypes()));
|
||||||
|
}
|
||||||
|
|
||||||
public static function _schema()
|
public static function _schema()
|
||||||
{
|
{
|
||||||
if (!isset(self::$map['__Schema'])) {
|
if (!isset(self::$map['__Schema'])) {
|
||||||
@ -593,7 +526,7 @@ EOD;
|
|||||||
],
|
],
|
||||||
'type' => [
|
'type' => [
|
||||||
'type' => Type::nonNull(self::_type()),
|
'type' => Type::nonNull(self::_type()),
|
||||||
'resolve' => function ($field) {
|
'resolve' => function (FieldDefinition $field) {
|
||||||
return $field->getType();
|
return $field->getType();
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -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,10 +89,18 @@ class Schema
|
|||||||
'subscription' => $subscriptionType
|
'subscription' => $subscriptionType
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_array($config)) {
|
if (is_array($config)) {
|
||||||
$config = SchemaConfig::create($config);
|
$config = SchemaConfig::create($config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(
|
Utils::invariant(
|
||||||
$config instanceof SchemaConfig,
|
$config instanceof SchemaConfig,
|
||||||
'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s',
|
'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s',
|
||||||
@ -103,15 +114,20 @@ class Schema
|
|||||||
]),
|
]),
|
||||||
Utils::getVariableType($config)
|
Utils::getVariableType($config)
|
||||||
);
|
);
|
||||||
|
|
||||||
Utils::invariant(
|
Utils::invariant(
|
||||||
$config->query instanceof ObjectType,
|
!$config->types || is_array($config->types) || is_callable($config->types),
|
||||||
"Schema query must be Object Type but got: " . Utils::getVariableType($config->query)
|
"\"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;
|
||||||
|
if ($config->query) {
|
||||||
$this->resolvedTypes[$config->query->name] = $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;
|
||||||
}
|
}
|
||||||
@ -208,7 +224,11 @@ class Schema
|
|||||||
public function getType($name)
|
public function getType($name)
|
||||||
{
|
{
|
||||||
if (!isset($this->resolvedTypes[$name])) {
|
if (!isset($this->resolvedTypes[$name])) {
|
||||||
$this->resolvedTypes[$name] = $this->loadType($name);
|
$type = $this->loadType($name);
|
||||||
|
if (!$type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$this->resolvedTypes[$name] = $type;
|
||||||
}
|
}
|
||||||
return $this->resolvedTypes[$name];
|
return $this->resolvedTypes[$name];
|
||||||
}
|
}
|
||||||
@ -393,6 +413,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 +449,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 +463,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 +473,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
728
src/Type/SchemaValidationContext.php
Normal file
728
src/Type/SchemaValidationContext.php
Normal file
@ -0,0 +1,728 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Type;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
|
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
|
||||||
|
use GraphQL\Language\AST\NamedTypeNode;
|
||||||
|
use GraphQL\Language\AST\Node;
|
||||||
|
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||||
|
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\EnumType;
|
||||||
|
use GraphQL\Type\Definition\EnumValueDefinition;
|
||||||
|
use GraphQL\Type\Definition\FieldDefinition;
|
||||||
|
use GraphQL\Type\Definition\InputObjectField;
|
||||||
|
use GraphQL\Type\Definition\InputObjectType;
|
||||||
|
use GraphQL\Type\Definition\InterfaceType;
|
||||||
|
use GraphQL\Type\Definition\NamedType;
|
||||||
|
use GraphQL\Type\Definition\NonNull;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use GraphQL\Type\Definition\UnionType;
|
||||||
|
use GraphQL\Utils\TypeComparators;
|
||||||
|
use GraphQL\Utils\Utils;
|
||||||
|
|
||||||
|
class SchemaValidationContext
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Error[]
|
||||||
|
*/
|
||||||
|
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, it cannot be ' . Utils::printSafe($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, it cannot be ' . Utils::printSafe($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, it cannot be ' . Utils::printSafe($subscriptionType) . '.',
|
||||||
|
$this->getOperationTypeNode($subscriptionType, 'subscription')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateDirectives()
|
||||||
|
{
|
||||||
|
$directives = $this->schema->getDirectives();
|
||||||
|
foreach($directives as $directive) {
|
||||||
|
// Ensure all directives are in fact GraphQL directives.
|
||||||
|
if (!$directive instanceof Directive) {
|
||||||
|
$this->reportError(
|
||||||
|
"Expected directive but got: " . Utils::printSafe($directive) . '.',
|
||||||
|
is_object($directive) ? $directive->astNode : null
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure they are named correctly.
|
||||||
|
$this->validateName($directive);
|
||||||
|
|
||||||
|
// TODO: Ensure proper locations.
|
||||||
|
|
||||||
|
$argNames = [];
|
||||||
|
foreach ($directive->args as $arg) {
|
||||||
|
$argName = $arg->name;
|
||||||
|
|
||||||
|
// Ensure they are named correctly.
|
||||||
|
$this->validateName($directive);
|
||||||
|
|
||||||
|
if (isset($argNames[$argName])) {
|
||||||
|
$this->reportError(
|
||||||
|
"Argument @{$directive->name}({$argName}:) can only be defined once.",
|
||||||
|
$this->getAllDirectiveArgNodes($directive, $argName)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$argNames[$argName] = true;
|
||||||
|
|
||||||
|
// Ensure the type is an input type.
|
||||||
|
if (!Type::isInputType($arg->getType())) {
|
||||||
|
$this->reportError(
|
||||||
|
"The type of @{$directive->name}({$argName}:) must be Input Type " .
|
||||||
|
'but got: ' . Utils::printSafe($arg->getType()) . '.',
|
||||||
|
$this->getDirectiveArgTypeNode($directive, $argName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Type|Directive|FieldDefinition|EnumValueDefinition|InputObjectField $node
|
||||||
|
*/
|
||||||
|
private function validateName($node)
|
||||||
|
{
|
||||||
|
// Ensure names are valid, however introspection types opt out.
|
||||||
|
$error = Utils::isValidNameError($node->name, $node->astNode);
|
||||||
|
if ($error && !Introspection::isIntrospectionType($node)) {
|
||||||
|
$this->addError($error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateTypes()
|
||||||
|
{
|
||||||
|
$typeMap = $this->schema->getTypeMap();
|
||||||
|
foreach($typeMap as $typeName => $type) {
|
||||||
|
// Ensure all provided types are in fact GraphQL type.
|
||||||
|
if (!$type instanceof NamedType) {
|
||||||
|
$this->reportError(
|
||||||
|
"Expected GraphQL named type but got: " . Utils::printSafe($type) . '.',
|
||||||
|
is_object($type) ? $type->astNode : null
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->validateName($type);
|
||||||
|
|
||||||
|
if ($type instanceof ObjectType) {
|
||||||
|
// Ensure fields are valid
|
||||||
|
$this->validateFields($type);
|
||||||
|
|
||||||
|
// Ensure objects implement the interfaces they claim to.
|
||||||
|
$this->validateObjectInterfaces($type);
|
||||||
|
} else if ($type instanceof InterfaceType) {
|
||||||
|
// Ensure fields are valid.
|
||||||
|
$this->validateFields($type);
|
||||||
|
} else if ($type instanceof UnionType) {
|
||||||
|
// Ensure Unions include valid member types.
|
||||||
|
$this->validateUnionMembers($type);
|
||||||
|
} else if ($type instanceof EnumType) {
|
||||||
|
// Ensure Enums have valid values.
|
||||||
|
$this->validateEnumValues($type);
|
||||||
|
} else if ($type instanceof InputObjectType) {
|
||||||
|
// Ensure Input Object fields are valid.
|
||||||
|
$this->validateInputFields($type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectType|InterfaceType $type
|
||||||
|
*/
|
||||||
|
private function validateFields($type) {
|
||||||
|
$fieldMap = $type->getFields();
|
||||||
|
|
||||||
|
// Objects and Interfaces both must define one or more fields.
|
||||||
|
if (!$fieldMap) {
|
||||||
|
$this->reportError(
|
||||||
|
"Type {$type->name} must define one or more fields.",
|
||||||
|
$this->getAllObjectOrInterfaceNodes($type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($fieldMap as $fieldName => $field) {
|
||||||
|
// Ensure they are named correctly.
|
||||||
|
$this->validateName($field);
|
||||||
|
|
||||||
|
// Ensure they were defined at most once.
|
||||||
|
$fieldNodes = $this->getAllFieldNodes($type, $fieldName);
|
||||||
|
if ($fieldNodes && count($fieldNodes) > 1) {
|
||||||
|
$this->reportError(
|
||||||
|
"Field {$type->name}.{$fieldName} can only be defined once.",
|
||||||
|
$fieldNodes
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the type is an output type
|
||||||
|
if (!Type::isOutputType($field->getType())) {
|
||||||
|
$this->reportError(
|
||||||
|
"The type of {$type->name}.{$fieldName} must be Output Type " .
|
||||||
|
'but got: ' . Utils::printSafe($field->getType()) . '.',
|
||||||
|
$this->getFieldTypeNode($type, $fieldName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the arguments are valid
|
||||||
|
$argNames = [];
|
||||||
|
foreach($field->args as $arg) {
|
||||||
|
$argName = $arg->name;
|
||||||
|
|
||||||
|
// Ensure they are named correctly.
|
||||||
|
$this->validateName($arg);
|
||||||
|
|
||||||
|
if (isset($argNames[$argName])) {
|
||||||
|
$this->reportError(
|
||||||
|
"Field argument {$type->name}.{$fieldName}({$argName}:) can only " .
|
||||||
|
'be defined once.',
|
||||||
|
$this->getAllFieldArgNodes($type, $fieldName, $argName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$argNames[$argName] = true;
|
||||||
|
|
||||||
|
// Ensure the type is an input type
|
||||||
|
if (!Type::isInputType($arg->getType())) {
|
||||||
|
$this->reportError(
|
||||||
|
"The type of {$type->name}.{$fieldName}({$argName}:) must be Input " .
|
||||||
|
'Type but got: '. Utils::printSafe($arg->getType()) . '.',
|
||||||
|
$this->getFieldArgTypeNode($type, $fieldName, $argName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateObjectInterfaces(ObjectType $object) {
|
||||||
|
$implementedTypeNames = [];
|
||||||
|
foreach($object->getInterfaces() as $iface) {
|
||||||
|
if (isset($implementedTypeNames[$iface->name])) {
|
||||||
|
$this->reportError(
|
||||||
|
"Type {$object->name} can only implement {$iface->name} once.",
|
||||||
|
$this->getAllImplementsInterfaceNodes($object, $iface)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$implementedTypeNames[$iface->name] = true;
|
||||||
|
$this->validateObjectImplementsInterface($object, $iface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectType $object
|
||||||
|
* @param InterfaceType $iface
|
||||||
|
*/
|
||||||
|
private function validateObjectImplementsInterface(ObjectType $object, $iface)
|
||||||
|
{
|
||||||
|
if (!$iface instanceof InterfaceType) {
|
||||||
|
$this->reportError(
|
||||||
|
"Type {$object->name} must only implement Interface types, " .
|
||||||
|
"it cannot implement ". Utils::printSafe($iface) . ".",
|
||||||
|
$this->getImplementsInterfaceNode($object, $iface)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$objectFieldMap = $object->getFields();
|
||||||
|
$ifaceFieldMap = $iface->getFields();
|
||||||
|
|
||||||
|
// Assert each interface field is implemented.
|
||||||
|
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
|
||||||
|
$objectField = array_key_exists($fieldName, $objectFieldMap)
|
||||||
|
? $objectFieldMap[$fieldName]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Assert interface field exists on object.
|
||||||
|
if (!$objectField) {
|
||||||
|
$this->reportError(
|
||||||
|
"Interface field {$iface->name}.{$fieldName} expected but " .
|
||||||
|
"{$object->name} does not provide it.",
|
||||||
|
[$this->getFieldNode($iface, $fieldName), $object->astNode]
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert interface field type is satisfied by object field type, by being
|
||||||
|
// a valid subtype. (covariant)
|
||||||
|
if (
|
||||||
|
!TypeComparators::isTypeSubTypeOf(
|
||||||
|
$this->schema,
|
||||||
|
$objectField->getType(),
|
||||||
|
$ifaceField->getType()
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
$this->reportError(
|
||||||
|
"Interface field {$iface->name}.{$fieldName} expects type ".
|
||||||
|
"{$ifaceField->getType()} but {$object->name}.{$fieldName} " .
|
||||||
|
"is type " . Utils::printSafe($objectField->getType()) . ".",
|
||||||
|
[
|
||||||
|
$this->getFieldTypeNode($iface, $fieldName),
|
||||||
|
$this->getFieldTypeNode($object, $fieldName),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert each interface field arg is implemented.
|
||||||
|
foreach($ifaceField->args as $ifaceArg) {
|
||||||
|
$argName = $ifaceArg->name;
|
||||||
|
$objectArg = null;
|
||||||
|
|
||||||
|
foreach($objectField->args as $arg) {
|
||||||
|
if ($arg->name === $argName) {
|
||||||
|
$objectArg = $arg;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert interface field arg exists on object field.
|
||||||
|
if (!$objectArg) {
|
||||||
|
$this->reportError(
|
||||||
|
"Interface field argument {$iface->name}.{$fieldName}({$argName}:) " .
|
||||||
|
"expected but {$object->name}.{$fieldName} does not provide it.",
|
||||||
|
[
|
||||||
|
$this->getFieldArgNode($iface, $fieldName, $argName),
|
||||||
|
$this->getFieldNode($object, $fieldName),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert interface field arg type matches object field arg type.
|
||||||
|
// (invariant)
|
||||||
|
// TODO: change to contravariant?
|
||||||
|
if (!TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType())) {
|
||||||
|
$this->reportError(
|
||||||
|
"Interface field argument {$iface->name}.{$fieldName}({$argName}:) ".
|
||||||
|
"expects type " . Utils::printSafe($ifaceArg->getType()) . " but " .
|
||||||
|
"{$object->name}.{$fieldName}({$argName}:) is type " .
|
||||||
|
Utils::printSafe($objectArg->getType()) . ".",
|
||||||
|
[
|
||||||
|
$this->getFieldArgTypeNode($iface, $fieldName, $argName),
|
||||||
|
$this->getFieldArgTypeNode($object, $fieldName, $argName),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: validate default values?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert additional arguments must not be required.
|
||||||
|
foreach($objectField->args as $objectArg) {
|
||||||
|
$argName = $objectArg->name;
|
||||||
|
$ifaceArg = null;
|
||||||
|
|
||||||
|
foreach($ifaceField->args as $arg) {
|
||||||
|
if ($arg->name === $argName) {
|
||||||
|
$ifaceArg = $arg;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$ifaceArg && $objectArg->getType() instanceof NonNull) {
|
||||||
|
$this->reportError(
|
||||||
|
"Object field argument {$object->name}.{$fieldName}({$argName}:) " .
|
||||||
|
"is of required type " . Utils::printSafe($objectArg->getType()) . " but is not also " .
|
||||||
|
"provided by the Interface field {$iface->name}.{$fieldName}.",
|
||||||
|
[
|
||||||
|
$this->getFieldArgTypeNode($object, $fieldName, $argName),
|
||||||
|
$this->getFieldNode($iface, $fieldName),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateUnionMembers(UnionType $union)
|
||||||
|
{
|
||||||
|
$memberTypes = $union->getTypes();
|
||||||
|
|
||||||
|
if (!$memberTypes) {
|
||||||
|
$this->reportError(
|
||||||
|
"Union type {$union->name} must define one or more member types.",
|
||||||
|
$union->astNode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$includedTypeNames = [];
|
||||||
|
|
||||||
|
foreach($memberTypes as $memberType) {
|
||||||
|
if (isset($includedTypeNames[$memberType->name])) {
|
||||||
|
$this->reportError(
|
||||||
|
"Union type {$union->name} can only include type ".
|
||||||
|
"{$memberType->name} once.",
|
||||||
|
$this->getUnionMemberTypeNodes($union, $memberType->name)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$includedTypeNames[$memberType->name] = true;
|
||||||
|
if (!$memberType instanceof ObjectType) {
|
||||||
|
$this->reportError(
|
||||||
|
"Union type {$union->name} can only include Object types, ".
|
||||||
|
"it cannot include " . Utils::printSafe($memberType) . ".",
|
||||||
|
$this->getUnionMemberTypeNodes($union, Utils::printSafe($memberType))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateEnumValues(EnumType $enumType)
|
||||||
|
{
|
||||||
|
$enumValues = $enumType->getValues();
|
||||||
|
|
||||||
|
if (!$enumValues) {
|
||||||
|
$this->reportError(
|
||||||
|
"Enum type {$enumType->name} must define one or more values.",
|
||||||
|
$enumType->astNode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($enumValues as $enumValue) {
|
||||||
|
$valueName = $enumValue->name;
|
||||||
|
|
||||||
|
// Ensure no duplicates
|
||||||
|
$allNodes = $this->getEnumValueNodes($enumType, $valueName);
|
||||||
|
if ($allNodes && count($allNodes) > 1) {
|
||||||
|
$this->reportError(
|
||||||
|
"Enum type {$enumType->name} can include value {$valueName} only once.",
|
||||||
|
$allNodes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure valid name.
|
||||||
|
$this->validateName($enumValue);
|
||||||
|
if ($valueName === 'true' || $valueName === 'false' || $valueName === 'null') {
|
||||||
|
$this->reportError(
|
||||||
|
"Enum type {$enumType->name} cannot include value: {$valueName}.",
|
||||||
|
$enumValue->astNode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateInputFields(InputObjectType $inputObj)
|
||||||
|
{
|
||||||
|
$fieldMap = $inputObj->getFields();
|
||||||
|
|
||||||
|
if (!$fieldMap) {
|
||||||
|
$this->reportError(
|
||||||
|
"Input Object type {$inputObj->name} must define one or more fields.",
|
||||||
|
$inputObj->astNode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the arguments are valid
|
||||||
|
foreach ($fieldMap as $fieldName => $field) {
|
||||||
|
// Ensure they are named correctly.
|
||||||
|
$this->validateName($field);
|
||||||
|
|
||||||
|
// TODO: Ensure they are unique per field.
|
||||||
|
|
||||||
|
// Ensure the type is an input type
|
||||||
|
if (!Type::isInputType($field->getType())) {
|
||||||
|
$this->reportError(
|
||||||
|
"The type of {$inputObj->name}.{$fieldName} must be Input Type " .
|
||||||
|
"but got: " . Utils::printSafe($field->getType()) . ".",
|
||||||
|
$field->astNode ? $field->astNode->type : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectType|InterfaceType $type
|
||||||
|
* @return ObjectTypeDefinitionNode[]|ObjectTypeExtensionNode[]|InterfaceTypeDefinitionNode[]|InterfaceTypeExtensionNode[]
|
||||||
|
*/
|
||||||
|
private function getAllObjectOrInterfaceNodes($type)
|
||||||
|
{
|
||||||
|
return $type->astNode
|
||||||
|
? ($type->extensionASTNodes
|
||||||
|
? array_merge([$type->astNode], $type->extensionASTNodes)
|
||||||
|
: [$type->astNode])
|
||||||
|
: ($type->extensionASTNodes ?: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectType $type
|
||||||
|
* @param InterfaceType $iface
|
||||||
|
* @return NamedTypeNode|null
|
||||||
|
*/
|
||||||
|
private function getImplementsInterfaceNode(ObjectType $type, $iface)
|
||||||
|
{
|
||||||
|
$nodes = $this->getAllImplementsInterfaceNodes($type, $iface);
|
||||||
|
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectType $type
|
||||||
|
* @param InterfaceType $iface
|
||||||
|
* @return NamedTypeNode[]
|
||||||
|
*/
|
||||||
|
private function getAllImplementsInterfaceNodes(ObjectType $type, $iface)
|
||||||
|
{
|
||||||
|
$implementsNodes = [];
|
||||||
|
$astNodes = $this->getAllObjectOrInterfaceNodes($type);
|
||||||
|
|
||||||
|
foreach($astNodes as $astNode) {
|
||||||
|
if ($astNode && $astNode->interfaces) {
|
||||||
|
foreach($astNode->interfaces as $node) {
|
||||||
|
if ($node->name->value === $iface->name) {
|
||||||
|
$implementsNodes[] = $node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $implementsNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectType|InterfaceType $type
|
||||||
|
* @param string $fieldName
|
||||||
|
* @return FieldDefinitionNode|null
|
||||||
|
*/
|
||||||
|
private function getFieldNode($type, $fieldName)
|
||||||
|
{
|
||||||
|
$nodes = $this->getAllFieldNodes($type, $fieldName);
|
||||||
|
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectType|InterfaceType $type
|
||||||
|
* @param string $fieldName
|
||||||
|
* @return FieldDefinitionNode[]
|
||||||
|
*/
|
||||||
|
private function getAllFieldNodes($type, $fieldName)
|
||||||
|
{
|
||||||
|
$fieldNodes = [];
|
||||||
|
$astNodes = $this->getAllObjectOrInterfaceNodes($type);
|
||||||
|
foreach($astNodes as $astNode) {
|
||||||
|
if ($astNode && $astNode->fields) {
|
||||||
|
foreach($astNode->fields as $node) {
|
||||||
|
if ($node->name->value === $fieldName) {
|
||||||
|
$fieldNodes[] = $node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $fieldNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectType|InterfaceType $type
|
||||||
|
* @param string $fieldName
|
||||||
|
* @return TypeNode|null
|
||||||
|
*/
|
||||||
|
private function getFieldTypeNode($type, $fieldName)
|
||||||
|
{
|
||||||
|
$fieldNode = $this->getFieldNode($type, $fieldName);
|
||||||
|
return $fieldNode ? $fieldNode->type : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectType|InterfaceType $type
|
||||||
|
* @param string $fieldName
|
||||||
|
* @param string $argName
|
||||||
|
* @return InputValueDefinitionNode|null
|
||||||
|
*/
|
||||||
|
private function getFieldArgNode($type, $fieldName, $argName)
|
||||||
|
{
|
||||||
|
$nodes = $this->getAllFieldArgNodes($type, $fieldName, $argName);
|
||||||
|
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectType|InterfaceType $type
|
||||||
|
* @param string $fieldName
|
||||||
|
* @param string $argName
|
||||||
|
* @return InputValueDefinitionNode[]
|
||||||
|
*/
|
||||||
|
private function getAllFieldArgNodes($type, $fieldName, $argName)
|
||||||
|
{
|
||||||
|
$argNodes = [];
|
||||||
|
$fieldNode = $this->getFieldNode($type, $fieldName);
|
||||||
|
if ($fieldNode && $fieldNode->arguments) {
|
||||||
|
foreach ($fieldNode->arguments as $node) {
|
||||||
|
if ($node->name->value === $argName) {
|
||||||
|
$argNodes[] = $node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $argNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectType|InterfaceType $type
|
||||||
|
* @param string $fieldName
|
||||||
|
* @param string $argName
|
||||||
|
* @return TypeNode|null
|
||||||
|
*/
|
||||||
|
private function getFieldArgTypeNode($type, $fieldName, $argName)
|
||||||
|
{
|
||||||
|
$fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName);
|
||||||
|
return $fieldArgNode ? $fieldArgNode->type : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Directive $directive
|
||||||
|
* @param string $argName
|
||||||
|
* @return InputValueDefinitionNode[]
|
||||||
|
*/
|
||||||
|
private function getAllDirectiveArgNodes(Directive $directive, $argName)
|
||||||
|
{
|
||||||
|
$argNodes = [];
|
||||||
|
$directiveNode = $directive->astNode;
|
||||||
|
if ($directiveNode && $directiveNode->arguments) {
|
||||||
|
foreach($directiveNode->arguments as $node) {
|
||||||
|
if ($node->name->value === $argName) {
|
||||||
|
$argNodes[] = $node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $argNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Directive $directive
|
||||||
|
* @param string $argName
|
||||||
|
* @return TypeNode|null
|
||||||
|
*/
|
||||||
|
private function getDirectiveArgTypeNode(Directive $directive, $argName)
|
||||||
|
{
|
||||||
|
$argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0];
|
||||||
|
return $argNode ? $argNode->type : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param UnionType $union
|
||||||
|
* @param string $typeName
|
||||||
|
* @return NamedTypeNode[]
|
||||||
|
*/
|
||||||
|
private function getUnionMemberTypeNodes(UnionType $union, $typeName)
|
||||||
|
{
|
||||||
|
if ($union->astNode && $union->astNode->types) {
|
||||||
|
return array_filter(
|
||||||
|
$union->astNode->types,
|
||||||
|
function (NamedTypeNode $value) use ($typeName) {
|
||||||
|
return $value->name->value === $typeName;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $union->astNode ?
|
||||||
|
$union->astNode->types : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param EnumType $enum
|
||||||
|
* @param string $valueName
|
||||||
|
* @return EnumValueDefinitionNode[]
|
||||||
|
*/
|
||||||
|
private function getEnumValueNodes(EnumType $enum, $valueName)
|
||||||
|
{
|
||||||
|
if ($enum->astNode && $enum->astNode->values) {
|
||||||
|
return array_filter(
|
||||||
|
iterator_to_array($enum->astNode->values),
|
||||||
|
function (EnumValueDefinitionNode $value) use ($valueName) {
|
||||||
|
return $value->name->value === $valueName;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $enum->astNode ?
|
||||||
|
$enum->astNode->values : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $message
|
||||||
|
* @param array|Node|TypeNode|TypeDefinitionNode $nodes
|
||||||
|
*/
|
||||||
|
private function reportError($message, $nodes = null) {
|
||||||
|
$nodes = array_filter($nodes && is_array($nodes) ? $nodes : [$nodes]);
|
||||||
|
$this->addError(new Error($message, $nodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Error $error
|
||||||
|
*/
|
||||||
|
private function addError($error) {
|
||||||
|
$this->errors[] = $error;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Utils;
|
namespace GraphQL\Utils;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Error\InvariantViolation;
|
use GraphQL\Error\InvariantViolation;
|
||||||
use GraphQL\Language\AST\BooleanValueNode;
|
use GraphQL\Language\AST\BooleanValueNode;
|
||||||
use GraphQL\Language\AST\DocumentNode;
|
use GraphQL\Language\AST\DocumentNode;
|
||||||
@ -30,9 +31,9 @@ use GraphQL\Type\Definition\InputType;
|
|||||||
use GraphQL\Type\Definition\LeafType;
|
use GraphQL\Type\Definition\LeafType;
|
||||||
use GraphQL\Type\Definition\ListOfType;
|
use GraphQL\Type\Definition\ListOfType;
|
||||||
use GraphQL\Type\Definition\NonNull;
|
use GraphQL\Type\Definition\NonNull;
|
||||||
|
use GraphQL\Type\Definition\ScalarType;
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
use GraphQL\Type\Schema;
|
use GraphQL\Type\Schema;
|
||||||
use GraphQL\Utils\Utils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Various utilities dealing with AST
|
* Various utilities dealing with AST
|
||||||
@ -205,15 +206,11 @@ class AST
|
|||||||
return new ObjectValueNode(['fields' => $fieldNodes]);
|
return new ObjectValueNode(['fields' => $fieldNodes]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($type instanceof ScalarType || $type instanceof EnumType) {
|
||||||
// Since value is an internally represented value, it must be serialized
|
// Since value is an internally represented value, it must be serialized
|
||||||
// to an externally represented value before converting into an AST.
|
// to an externally represented value before converting into an AST.
|
||||||
if ($type instanceof LeafType) {
|
|
||||||
$serialized = $type->serialize($value);
|
$serialized = $type->serialize($value);
|
||||||
} else {
|
if (null === $serialized || Utils::isInvalid($serialized)) {
|
||||||
throw new InvariantViolation("Must provide Input Type, cannot use: " . Utils::printSafe($type));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $serialized) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,6 +249,9 @@ class AST
|
|||||||
throw new InvariantViolation('Cannot convert value to AST: ' . Utils::printSafe($serialized));
|
throw new InvariantViolation('Cannot convert value to AST: ' . Utils::printSafe($serialized));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produces a PHP value given a GraphQL Value AST.
|
* Produces a PHP value given a GraphQL Value AST.
|
||||||
*
|
*
|
||||||
@ -383,19 +383,100 @@ class AST
|
|||||||
return $coercedObj;
|
return $coercedObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($type instanceof LeafType) {
|
if ($type instanceof EnumType) {
|
||||||
$parsed = $type->parseLiteral($valueNode);
|
if (!$valueNode instanceof EnumValueNode) {
|
||||||
|
return $undefined;
|
||||||
if (null === $parsed && !$type->isValidLiteral($valueNode)) {
|
}
|
||||||
// Invalid values represent a failure to parse correctly, in which case
|
$enumValue = $type->getValue($valueNode->value);
|
||||||
// no value is returned.
|
if (!$enumValue) {
|
||||||
return $undefined;
|
return $undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $parsed;
|
return $enumValue->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvariantViolation('Must be input type');
|
if ($type instanceof ScalarType) {
|
||||||
|
// Scalars fulfill parsing a literal value via parseLiteral().
|
||||||
|
// Invalid values represent a failure to parse correctly, in which case
|
||||||
|
// no value is returned.
|
||||||
|
try {
|
||||||
|
$result = $type->parseLiteral($valueNode, $variables);
|
||||||
|
} catch (\Exception $error) {
|
||||||
|
return $undefined;
|
||||||
|
} catch (\Throwable $error) {
|
||||||
|
return $undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Utils::isInvalid($result)) {
|
||||||
|
return $undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produces a PHP value given a GraphQL Value AST.
|
||||||
|
*
|
||||||
|
* Unlike `valueFromAST()`, no type is provided. The resulting JavaScript value
|
||||||
|
* will reflect the provided GraphQL value AST.
|
||||||
|
*
|
||||||
|
* | GraphQL Value | PHP Value |
|
||||||
|
* | -------------------- | ------------- |
|
||||||
|
* | Input Object | Assoc Array |
|
||||||
|
* | List | Array |
|
||||||
|
* | Boolean | Boolean |
|
||||||
|
* | String | String |
|
||||||
|
* | Int / Float | Int / Float |
|
||||||
|
* | Enum | Mixed |
|
||||||
|
* | Null | null |
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @param Node $valueNode
|
||||||
|
* @param array|null $variables
|
||||||
|
* @return mixed
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public static function valueFromASTUntyped($valueNode, array $variables = null) {
|
||||||
|
switch (true) {
|
||||||
|
case $valueNode instanceof NullValueNode:
|
||||||
|
return null;
|
||||||
|
case $valueNode instanceof IntValueNode:
|
||||||
|
return intval($valueNode->value, 10);
|
||||||
|
case $valueNode instanceof FloatValueNode:
|
||||||
|
return floatval($valueNode->value);
|
||||||
|
case $valueNode instanceof StringValueNode:
|
||||||
|
case $valueNode instanceof EnumValueNode:
|
||||||
|
case $valueNode instanceof BooleanValueNode:
|
||||||
|
return $valueNode->value;
|
||||||
|
case $valueNode instanceof ListValueNode:
|
||||||
|
return array_map(
|
||||||
|
function($node) use ($variables) {
|
||||||
|
return self::valueFromASTUntyped($node, $variables);
|
||||||
|
},
|
||||||
|
iterator_to_array($valueNode->values)
|
||||||
|
);
|
||||||
|
case $valueNode instanceof ObjectValueNode:
|
||||||
|
return array_combine(
|
||||||
|
array_map(
|
||||||
|
function($field) { return $field->name->value; },
|
||||||
|
iterator_to_array($valueNode->fields)
|
||||||
|
),
|
||||||
|
array_map(
|
||||||
|
function($field) use ($variables) { return self::valueFromASTUntyped($field->value, $variables); },
|
||||||
|
iterator_to_array($valueNode->fields)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
case $valueNode instanceof VariableNode:
|
||||||
|
$variableName = $valueNode->name->value;
|
||||||
|
return ($variables && isset($variables[$variableName]) && !Utils::isInvalid($variables[$variableName]))
|
||||||
|
? $variables[$variableName]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unexpected value kind: ' . $valueNode->kind . '.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -405,7 +486,7 @@ class AST
|
|||||||
* @param Schema $schema
|
* @param Schema $schema
|
||||||
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
|
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
|
||||||
* @return Type
|
* @return Type
|
||||||
* @throws InvariantViolation
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public static function typeFromAST(Schema $schema, $inputTypeNode)
|
public static function typeFromAST(Schema $schema, $inputTypeNode)
|
||||||
{
|
{
|
||||||
@ -417,11 +498,13 @@ class AST
|
|||||||
$innerType = self::typeFromAST($schema, $inputTypeNode->type);
|
$innerType = self::typeFromAST($schema, $inputTypeNode->type);
|
||||||
return $innerType ? new NonNull($innerType) : null;
|
return $innerType ? new NonNull($innerType) : null;
|
||||||
}
|
}
|
||||||
|
if ($inputTypeNode instanceof NamedTypeNode) {
|
||||||
Utils::invariant($inputTypeNode && $inputTypeNode instanceof NamedTypeNode, 'Must be a named type');
|
|
||||||
return $schema->getType($inputTypeNode->name->value);
|
return $schema->getType($inputTypeNode->name->value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error('Unexpected type kind: ' . $inputTypeNode->kind . '.');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the provided valueNode is a variable which is not defined
|
* Returns true if the provided valueNode is a variable which is not defined
|
||||||
* in the set of variables.
|
* in the set of variables.
|
||||||
|
455
src/Utils/ASTDefinitionBuilder.php
Normal file
455
src/Utils/ASTDefinitionBuilder.php
Normal file
@ -0,0 +1,455 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Utils;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
|
use GraphQL\Executor\Values;
|
||||||
|
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\ListTypeNode;
|
||||||
|
use GraphQL\Language\AST\NamedTypeNode;
|
||||||
|
use GraphQL\Language\AST\NodeKind;
|
||||||
|
use GraphQL\Language\AST\NonNullTypeNode;
|
||||||
|
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\TypeNode;
|
||||||
|
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
||||||
|
use GraphQL\Language\Token;
|
||||||
|
use GraphQL\Type\Definition\CustomScalarType;
|
||||||
|
use GraphQL\Type\Definition\EnumType;
|
||||||
|
use GraphQL\Type\Definition\InputObjectType;
|
||||||
|
use GraphQL\Type\Definition\InputType;
|
||||||
|
use GraphQL\Type\Definition\Directive;
|
||||||
|
use GraphQL\Type\Definition\InterfaceType;
|
||||||
|
use GraphQL\Type\Definition\FieldArgument;
|
||||||
|
use GraphQL\Type\Definition\NonNull;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use GraphQL\Type\Definition\UnionType;
|
||||||
|
|
||||||
|
class ASTDefinitionBuilder
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $typeDefintionsMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
|
private $typeConfigDecorator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $options;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
|
private $resolveType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $cache;
|
||||||
|
|
||||||
|
public function __construct(array $typeDefintionsMap, $options, callable $resolveType, callable $typeConfigDecorator = null)
|
||||||
|
{
|
||||||
|
$this->typeDefintionsMap = $typeDefintionsMap;
|
||||||
|
$this->typeConfigDecorator = $typeConfigDecorator;
|
||||||
|
$this->options = $options;
|
||||||
|
$this->resolveType = $resolveType;
|
||||||
|
|
||||||
|
$this->cache = Type::getAllBuiltInTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Type $innerType
|
||||||
|
* @param TypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
|
||||||
|
* @return Type
|
||||||
|
*/
|
||||||
|
private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode)
|
||||||
|
{
|
||||||
|
if ($inputTypeNode->kind == NodeKind::LIST_TYPE) {
|
||||||
|
return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type));
|
||||||
|
}
|
||||||
|
if ($inputTypeNode->kind == NodeKind::NON_NULL_TYPE) {
|
||||||
|
$wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type);
|
||||||
|
return Type::nonNull(NonNull::assertNullableType($wrappedType));
|
||||||
|
}
|
||||||
|
return $innerType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TypeNode|ListTypeNode|NonNullTypeNode $typeNode
|
||||||
|
* @return TypeNode
|
||||||
|
*/
|
||||||
|
private function getNamedTypeNode(TypeNode $typeNode)
|
||||||
|
{
|
||||||
|
$namedType = $typeNode;
|
||||||
|
while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) {
|
||||||
|
$namedType = $namedType->type;
|
||||||
|
}
|
||||||
|
return $namedType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $typeName
|
||||||
|
* @param NamedTypeNode|null $typeNode
|
||||||
|
* @return Type
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
private function internalBuildType($typeName, $typeNode = null) {
|
||||||
|
if (!isset($this->cache[$typeName])) {
|
||||||
|
if (isset($this->typeDefintionsMap[$typeName])) {
|
||||||
|
$type = $this->makeSchemaDef($this->typeDefintionsMap[$typeName]);
|
||||||
|
if ($this->typeConfigDecorator) {
|
||||||
|
$fn = $this->typeConfigDecorator;
|
||||||
|
try {
|
||||||
|
$config = $fn($type->config, $this->typeDefintionsMap[$typeName], $this->typeDefintionsMap);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
throw new Error(
|
||||||
|
"Type config decorator passed to " . (static::class) . " threw an error " .
|
||||||
|
"when building $typeName type: {$e->getMessage()}",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
throw new Error(
|
||||||
|
"Type config decorator passed to " . (static::class) . " threw an error " .
|
||||||
|
"when building $typeName type: {$e->getMessage()}",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!is_array($config) || isset($config[0])) {
|
||||||
|
throw new Error(
|
||||||
|
"Type config decorator passed to " . (static::class) . " is expected to return an array, but got " .
|
||||||
|
Utils::getVariableType($config)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$type = $this->makeSchemaDefFromConfig($this->typeDefintionsMap[$typeName], $config);
|
||||||
|
}
|
||||||
|
$this->cache[$typeName] = $type;
|
||||||
|
} else {
|
||||||
|
$fn = $this->resolveType;
|
||||||
|
$this->cache[$typeName] = $fn($typeName, $typeNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->cache[$typeName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|NamedTypeNode $ref
|
||||||
|
* @return Type
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
public function buildType($ref)
|
||||||
|
{
|
||||||
|
if (is_string($ref)) {
|
||||||
|
return $this->internalBuildType($ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->internalBuildType($ref->name->value, $ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TypeNode $typeNode
|
||||||
|
* @return Type|InputType
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
private function internalBuildWrappedType(TypeNode $typeNode)
|
||||||
|
{
|
||||||
|
$typeDef = $this->buildType($this->getNamedTypeNode($typeNode));
|
||||||
|
return $this->buildWrappedType($typeDef, $typeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildDirective(DirectiveDefinitionNode $directiveNode)
|
||||||
|
{
|
||||||
|
return new Directive([
|
||||||
|
'name' => $directiveNode->name->value,
|
||||||
|
'description' => $this->getDescription($directiveNode),
|
||||||
|
'locations' => Utils::map($directiveNode->locations, function ($node) {
|
||||||
|
return $node->value;
|
||||||
|
}),
|
||||||
|
'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null,
|
||||||
|
'astNode' => $directiveNode,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildField(FieldDefinitionNode $field)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Note: While this could make assertions to get the correctly typed
|
||||||
|
// value, that would throw immediately while type system validation
|
||||||
|
// with validateSchema() will produce more actionable results.
|
||||||
|
'type' => $this->internalBuildWrappedType($field->type),
|
||||||
|
'description' => $this->getDescription($field),
|
||||||
|
'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null,
|
||||||
|
'deprecationReason' => $this->getDeprecationReason($field),
|
||||||
|
'astNode' => $field,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeSchemaDef($def)
|
||||||
|
{
|
||||||
|
if (!$def) {
|
||||||
|
throw new Error('def must be defined.');
|
||||||
|
}
|
||||||
|
switch ($def->kind) {
|
||||||
|
case NodeKind::OBJECT_TYPE_DEFINITION:
|
||||||
|
return $this->makeTypeDef($def);
|
||||||
|
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
||||||
|
return $this->makeInterfaceDef($def);
|
||||||
|
case NodeKind::ENUM_TYPE_DEFINITION:
|
||||||
|
return $this->makeEnumDef($def);
|
||||||
|
case NodeKind::UNION_TYPE_DEFINITION:
|
||||||
|
return $this->makeUnionDef($def);
|
||||||
|
case NodeKind::SCALAR_TYPE_DEFINITION:
|
||||||
|
return $this->makeScalarDef($def);
|
||||||
|
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
||||||
|
return $this->makeInputObjectDef($def);
|
||||||
|
default:
|
||||||
|
throw new Error("Type kind of {$def->kind} not supported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeSchemaDefFromConfig($def, array $config)
|
||||||
|
{
|
||||||
|
if (!$def) {
|
||||||
|
throw new Error('def must be defined.');
|
||||||
|
}
|
||||||
|
switch ($def->kind) {
|
||||||
|
case NodeKind::OBJECT_TYPE_DEFINITION:
|
||||||
|
return new ObjectType($config);
|
||||||
|
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
||||||
|
return new InterfaceType($config);
|
||||||
|
case NodeKind::ENUM_TYPE_DEFINITION:
|
||||||
|
return new EnumType($config);
|
||||||
|
case NodeKind::UNION_TYPE_DEFINITION:
|
||||||
|
return new UnionType($config);
|
||||||
|
case NodeKind::SCALAR_TYPE_DEFINITION:
|
||||||
|
return new CustomScalarType($config);
|
||||||
|
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
||||||
|
return new InputObjectType($config);
|
||||||
|
default:
|
||||||
|
throw new Error("Type kind of {$def->kind} not supported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeTypeDef(ObjectTypeDefinitionNode $def)
|
||||||
|
{
|
||||||
|
$typeName = $def->name->value;
|
||||||
|
return new ObjectType([
|
||||||
|
'name' => $typeName,
|
||||||
|
'description' => $this->getDescription($def),
|
||||||
|
'fields' => function () use ($def) {
|
||||||
|
return $this->makeFieldDefMap($def);
|
||||||
|
},
|
||||||
|
'interfaces' => function () use ($def) {
|
||||||
|
return $this->makeImplementedInterfaces($def);
|
||||||
|
},
|
||||||
|
'astNode' => $def
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeFieldDefMap($def)
|
||||||
|
{
|
||||||
|
return $def->fields
|
||||||
|
? Utils::keyValMap(
|
||||||
|
$def->fields,
|
||||||
|
function ($field) {
|
||||||
|
return $field->name->value;
|
||||||
|
},
|
||||||
|
function ($field) {
|
||||||
|
return $this->buildField($field);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
|
||||||
|
{
|
||||||
|
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->buildType($iface);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeInputValues($values)
|
||||||
|
{
|
||||||
|
return Utils::keyValMap(
|
||||||
|
$values,
|
||||||
|
function ($value) {
|
||||||
|
return $value->name->value;
|
||||||
|
},
|
||||||
|
function ($value) {
|
||||||
|
// Note: While this could make assertions to get the correctly typed
|
||||||
|
// value, that would throw immediately while type system validation
|
||||||
|
// with validateSchema() will produce more actionable results.
|
||||||
|
$type = $this->internalBuildWrappedType($value->type);
|
||||||
|
$config = [
|
||||||
|
'name' => $value->name->value,
|
||||||
|
'type' => $type,
|
||||||
|
'description' => $this->getDescription($value),
|
||||||
|
'astNode' => $value
|
||||||
|
];
|
||||||
|
if (isset($value->defaultValue)) {
|
||||||
|
$config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
|
||||||
|
}
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeInterfaceDef(InterfaceTypeDefinitionNode $def)
|
||||||
|
{
|
||||||
|
$typeName = $def->name->value;
|
||||||
|
return new InterfaceType([
|
||||||
|
'name' => $typeName,
|
||||||
|
'description' => $this->getDescription($def),
|
||||||
|
'fields' => function () use ($def) {
|
||||||
|
return $this->makeFieldDefMap($def);
|
||||||
|
},
|
||||||
|
'astNode' => $def
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeEnumDef(EnumTypeDefinitionNode $def)
|
||||||
|
{
|
||||||
|
return new EnumType([
|
||||||
|
'name' => $def->name->value,
|
||||||
|
'description' => $this->getDescription($def),
|
||||||
|
'values' => $def->values
|
||||||
|
? Utils::keyValMap(
|
||||||
|
$def->values,
|
||||||
|
function ($enumValue) {
|
||||||
|
return $enumValue->name->value;
|
||||||
|
},
|
||||||
|
function ($enumValue) {
|
||||||
|
return [
|
||||||
|
'description' => $this->getDescription($enumValue),
|
||||||
|
'deprecationReason' => $this->getDeprecationReason($enumValue),
|
||||||
|
'astNode' => $enumValue
|
||||||
|
];
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
'astNode' => $def,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeUnionDef(UnionTypeDefinitionNode $def)
|
||||||
|
{
|
||||||
|
return new UnionType([
|
||||||
|
'name' => $def->name->value,
|
||||||
|
'description' => $this->getDescription($def),
|
||||||
|
// Note: While this could make assertions to get the correctly typed
|
||||||
|
// values below, that would throw immediately while type system
|
||||||
|
// validation with validateSchema() will produce more actionable results.
|
||||||
|
'types' => $def->types
|
||||||
|
? Utils::map($def->types, function ($typeNode) {
|
||||||
|
return $this->buildType($typeNode);
|
||||||
|
}):
|
||||||
|
[],
|
||||||
|
'astNode' => $def,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeScalarDef(ScalarTypeDefinitionNode $def)
|
||||||
|
{
|
||||||
|
return new CustomScalarType([
|
||||||
|
'name' => $def->name->value,
|
||||||
|
'description' => $this->getDescription($def),
|
||||||
|
'astNode' => $def,
|
||||||
|
'serialize' => function($value) {
|
||||||
|
return $value;
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeInputObjectDef(InputObjectTypeDefinitionNode $def)
|
||||||
|
{
|
||||||
|
return new InputObjectType([
|
||||||
|
'name' => $def->name->value,
|
||||||
|
'description' => $this->getDescription($def),
|
||||||
|
'fields' => function () use ($def) {
|
||||||
|
return $def->fields
|
||||||
|
? $this->makeInputValues($def->fields)
|
||||||
|
: [];
|
||||||
|
},
|
||||||
|
'astNode' => $def,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a collection of directives, returns the string value for the
|
||||||
|
* deprecation reason.
|
||||||
|
*
|
||||||
|
* @param EnumValueDefinitionNode | FieldDefinitionNode $node
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getDeprecationReason($node)
|
||||||
|
{
|
||||||
|
$deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node);
|
||||||
|
return isset($deprecated['reason']) ? $deprecated['reason'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an ast node, returns its string description.
|
||||||
|
*/
|
||||||
|
private function getDescription($node)
|
||||||
|
{
|
||||||
|
if ($node->description) {
|
||||||
|
return $node->description->value;
|
||||||
|
}
|
||||||
|
if (isset($this->options['commentDescriptions'])) {
|
||||||
|
$rawValue = $this->getLeadingCommentBlock($node);
|
||||||
|
if ($rawValue !== null) {
|
||||||
|
return BlockString::value("\n" . $rawValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLeadingCommentBlock($node)
|
||||||
|
{
|
||||||
|
$loc = $node->loc;
|
||||||
|
if (!$loc || !$loc->startToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$comments = [];
|
||||||
|
$token = $loc->startToken->prev;
|
||||||
|
while (
|
||||||
|
$token &&
|
||||||
|
$token->kind === Token::COMMENT &&
|
||||||
|
$token->next && $token->prev &&
|
||||||
|
$token->line + 1 === $token->next->line &&
|
||||||
|
$token->line !== $token->prev->line
|
||||||
|
) {
|
||||||
|
$value = $token->value;
|
||||||
|
$comments[] = $value;
|
||||||
|
$token = $token->prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n", array_reverse($comments));
|
||||||
|
}
|
||||||
|
}
|
61
src/Utils/BlockString.php
Normal file
61
src/Utils/BlockString.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Utils;
|
||||||
|
|
||||||
|
class BlockString {
|
||||||
|
/**
|
||||||
|
* Produces the value of a block string from its parsed raw value, similar to
|
||||||
|
* Coffeescript's block string, Python's docstring trim or Ruby's strip_heredoc.
|
||||||
|
*
|
||||||
|
* This implements the GraphQL spec's BlockStringValue() static algorithm.
|
||||||
|
*/
|
||||||
|
public static function value($rawString) {
|
||||||
|
// Expand a block string's raw value into independent lines.
|
||||||
|
$lines = preg_split("/\\r\\n|[\\n\\r]/", $rawString);
|
||||||
|
|
||||||
|
// Remove common indentation from all lines but first.
|
||||||
|
$commonIndent = null;
|
||||||
|
$linesLength = count($lines);
|
||||||
|
|
||||||
|
for ($i = 1; $i < $linesLength; $i++) {
|
||||||
|
$line = $lines[$i];
|
||||||
|
$indent = self::leadingWhitespace($line);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$indent < mb_strlen($line) &&
|
||||||
|
($commonIndent === null || $indent < $commonIndent)
|
||||||
|
) {
|
||||||
|
$commonIndent = $indent;
|
||||||
|
if ($commonIndent === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($commonIndent) {
|
||||||
|
for ($i = 1; $i < $linesLength; $i++) {
|
||||||
|
$line = $lines[$i];
|
||||||
|
$lines[$i] = mb_substr($line, $commonIndent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove leading and trailing blank lines.
|
||||||
|
while (count($lines) > 0 && trim($lines[0], " \t") === '') {
|
||||||
|
array_shift($lines);
|
||||||
|
}
|
||||||
|
while (count($lines) > 0 && trim($lines[count($lines) - 1], " \t") === '') {
|
||||||
|
array_pop($lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a string of the lines joined with U+000A.
|
||||||
|
return implode("\n", $lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function leadingWhitespace($str) {
|
||||||
|
$i = 0;
|
||||||
|
while ($i < mb_strlen($str) && ($str[$i] === ' ' || $str[$i] === '\t')) {
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $i;
|
||||||
|
}
|
||||||
|
}
|
@ -2,35 +2,13 @@
|
|||||||
namespace GraphQL\Utils;
|
namespace GraphQL\Utils;
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Executor\Values;
|
|
||||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
|
||||||
use GraphQL\Language\AST\DocumentNode;
|
use GraphQL\Language\AST\DocumentNode;
|
||||||
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
|
||||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
|
||||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
|
||||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
|
||||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
|
||||||
use GraphQL\Language\AST\NodeKind;
|
use GraphQL\Language\AST\NodeKind;
|
||||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
use GraphQL\Language\AST\SchemaDefinitionNode;
|
||||||
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
|
|
||||||
use GraphQL\Language\AST\TypeDefinitionNode;
|
|
||||||
use GraphQL\Language\AST\TypeNode;
|
|
||||||
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
|
||||||
use GraphQL\Language\Parser;
|
use GraphQL\Language\Parser;
|
||||||
use GraphQL\Language\Source;
|
use GraphQL\Language\Source;
|
||||||
use GraphQL\Language\Token;
|
|
||||||
use GraphQL\Type\Schema;
|
use GraphQL\Type\Schema;
|
||||||
use GraphQL\Type\Definition\Directive;
|
use GraphQL\Type\Definition\Directive;
|
||||||
use GraphQL\Type\Definition\EnumType;
|
|
||||||
use GraphQL\Type\Definition\InputObjectType;
|
|
||||||
use GraphQL\Type\Definition\InterfaceType;
|
|
||||||
use GraphQL\Type\Definition\FieldArgument;
|
|
||||||
use GraphQL\Type\Definition\NonNull;
|
|
||||||
use GraphQL\Type\Definition\ObjectType;
|
|
||||||
use GraphQL\Type\Definition\CustomScalarType;
|
|
||||||
use GraphQL\Type\Definition\Type;
|
|
||||||
use GraphQL\Type\Definition\UnionType;
|
|
||||||
use GraphQL\Type\Introspection;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build instance of `GraphQL\Type\Schema` out of type language definition (string or parsed AST)
|
* Build instance of `GraphQL\Type\Schema` out of type language definition (string or parsed AST)
|
||||||
@ -38,33 +16,6 @@ use GraphQL\Type\Introspection;
|
|||||||
*/
|
*/
|
||||||
class BuildSchema
|
class BuildSchema
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @param Type $innerType
|
|
||||||
* @param TypeNode $inputTypeNode
|
|
||||||
* @return Type
|
|
||||||
*/
|
|
||||||
private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode)
|
|
||||||
{
|
|
||||||
if ($inputTypeNode->kind == NodeKind::LIST_TYPE) {
|
|
||||||
return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type));
|
|
||||||
}
|
|
||||||
if ($inputTypeNode->kind == NodeKind::NON_NULL_TYPE) {
|
|
||||||
$wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type);
|
|
||||||
Utils::invariant(!($wrappedType instanceof NonNull), 'No nesting nonnull.');
|
|
||||||
return Type::nonNull($wrappedType);
|
|
||||||
}
|
|
||||||
return $innerType;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getNamedTypeNode(TypeNode $typeNode)
|
|
||||||
{
|
|
||||||
$namedType = $typeNode;
|
|
||||||
while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) {
|
|
||||||
$namedType = $namedType->type;
|
|
||||||
}
|
|
||||||
return $namedType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This takes the ast of a schema document produced by the parse function in
|
* This takes the ast of a schema document produced by the parse function in
|
||||||
* GraphQL\Language\Parser.
|
* GraphQL\Language\Parser.
|
||||||
@ -75,33 +26,40 @@ class BuildSchema
|
|||||||
* Given that AST it constructs a GraphQL\Type\Schema. The resulting schema
|
* Given that AST it constructs a GraphQL\Type\Schema. The resulting schema
|
||||||
* has no resolve methods, so execution will use default resolvers.
|
* has no resolve methods, so execution will use default resolvers.
|
||||||
*
|
*
|
||||||
|
* Accepts options as a third argument:
|
||||||
|
*
|
||||||
|
* - commentDescriptions:
|
||||||
|
* Provide true to use preceding comments as the description.
|
||||||
|
*
|
||||||
|
*
|
||||||
* @api
|
* @api
|
||||||
* @param DocumentNode $ast
|
* @param DocumentNode $ast
|
||||||
* @param callable $typeConfigDecorator
|
* @param callable $typeConfigDecorator
|
||||||
|
* @param array $options
|
||||||
* @return Schema
|
* @return Schema
|
||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null)
|
public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = [])
|
||||||
{
|
{
|
||||||
$builder = new self($ast, $typeConfigDecorator);
|
$builder = new self($ast, $typeConfigDecorator, $options);
|
||||||
return $builder->buildSchema();
|
return $builder->buildSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
private $ast;
|
private $ast;
|
||||||
private $innerTypeMap;
|
|
||||||
private $nodeMap;
|
private $nodeMap;
|
||||||
private $typeConfigDecorator;
|
private $typeConfigDecorator;
|
||||||
private $loadedTypeDefs;
|
private $options;
|
||||||
|
|
||||||
public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null)
|
public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = [])
|
||||||
{
|
{
|
||||||
$this->ast = $ast;
|
$this->ast = $ast;
|
||||||
$this->typeConfigDecorator = $typeConfigDecorator;
|
$this->typeConfigDecorator = $typeConfigDecorator;
|
||||||
$this->loadedTypeDefs = [];
|
$this->options = $options;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildSchema()
|
public function buildSchema()
|
||||||
{
|
{
|
||||||
|
/** @var SchemaDefinitionNode $schemaDef */
|
||||||
$schemaDef = null;
|
$schemaDef = null;
|
||||||
$typeDefs = [];
|
$typeDefs = [];
|
||||||
$this->nodeMap = [];
|
$this->nodeMap = [];
|
||||||
@ -133,121 +91,70 @@ class BuildSchema
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$queryTypeName = null;
|
$operationTypes = $schemaDef
|
||||||
$mutationTypeName = null;
|
? $this->getOperationTypes($schemaDef)
|
||||||
$subscriptionTypeName = null;
|
: [
|
||||||
if ($schemaDef) {
|
'query' => isset($this->nodeMap['Query']) ? 'Query' : null,
|
||||||
foreach ($schemaDef->operationTypes as $operationType) {
|
'mutation' => isset($this->nodeMap['Mutation']) ? 'Mutation' : null,
|
||||||
$typeName = $operationType->type->name->value;
|
'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null,
|
||||||
if ($operationType->operation === 'query') {
|
|
||||||
if ($queryTypeName) {
|
|
||||||
throw new Error('Must provide only one query type in schema.');
|
|
||||||
}
|
|
||||||
if (!isset($this->nodeMap[$typeName])) {
|
|
||||||
throw new Error(
|
|
||||||
'Specified query type "' . $typeName . '" not found in document.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$queryTypeName = $typeName;
|
|
||||||
} else if ($operationType->operation === 'mutation') {
|
|
||||||
if ($mutationTypeName) {
|
|
||||||
throw new Error('Must provide only one mutation type in schema.');
|
|
||||||
}
|
|
||||||
if (!isset($this->nodeMap[$typeName])) {
|
|
||||||
throw new Error(
|
|
||||||
'Specified mutation type "' . $typeName . '" not found in document.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$mutationTypeName = $typeName;
|
|
||||||
} else if ($operationType->operation === 'subscription') {
|
|
||||||
if ($subscriptionTypeName) {
|
|
||||||
throw new Error('Must provide only one subscription type in schema.');
|
|
||||||
}
|
|
||||||
if (!isset($this->nodeMap[$typeName])) {
|
|
||||||
throw new Error(
|
|
||||||
'Specified subscription type "' . $typeName . '" not found in document.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$subscriptionTypeName = $typeName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isset($this->nodeMap['Query'])) {
|
|
||||||
$queryTypeName = 'Query';
|
|
||||||
}
|
|
||||||
if (isset($this->nodeMap['Mutation'])) {
|
|
||||||
$mutationTypeName = 'Mutation';
|
|
||||||
}
|
|
||||||
if (isset($this->nodeMap['Subscription'])) {
|
|
||||||
$subscriptionTypeName = 'Subscription';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$queryTypeName) {
|
|
||||||
throw new Error(
|
|
||||||
'Must provide schema definition with query type or a type named Query.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->innerTypeMap = [
|
|
||||||
'String' => Type::string(),
|
|
||||||
'Int' => Type::int(),
|
|
||||||
'Float' => Type::float(),
|
|
||||||
'Boolean' => Type::boolean(),
|
|
||||||
'ID' => Type::id(),
|
|
||||||
'__Schema' => Introspection::_schema(),
|
|
||||||
'__Directive' => Introspection::_directive(),
|
|
||||||
'__DirectiveLocation' => Introspection::_directiveLocation(),
|
|
||||||
'__Type' => Introspection::_type(),
|
|
||||||
'__Field' => Introspection::_field(),
|
|
||||||
'__InputValue' => Introspection::_inputValue(),
|
|
||||||
'__EnumValue' => Introspection::_enumValue(),
|
|
||||||
'__TypeKind' => Introspection::_typeKind(),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$directives = array_map([$this, 'getDirective'], $directiveDefs);
|
$defintionBuilder = new ASTDefinitionBuilder(
|
||||||
|
$this->nodeMap,
|
||||||
|
$this->options,
|
||||||
|
function($typeName) { throw new Error('Type "'. $typeName . '" not found in document.'); },
|
||||||
|
$this->typeConfigDecorator
|
||||||
|
);
|
||||||
|
|
||||||
|
$directives = array_map(function($def) use ($defintionBuilder) {
|
||||||
|
return $defintionBuilder->buildDirective($def);
|
||||||
|
}, $directiveDefs);
|
||||||
|
|
||||||
// If specified directives were not explicitly declared, add them.
|
// If specified directives were not explicitly declared, add them.
|
||||||
$skip = array_reduce($directives, function($hasSkip, $directive) {
|
$skip = array_reduce($directives, function ($hasSkip, $directive) {
|
||||||
return $hasSkip || $directive->name == 'skip';
|
return $hasSkip || $directive->name == 'skip';
|
||||||
});
|
});
|
||||||
if (!$skip) {
|
if (!$skip) {
|
||||||
$directives[] = Directive::skipDirective();
|
$directives[] = Directive::skipDirective();
|
||||||
}
|
}
|
||||||
|
|
||||||
$include = array_reduce($directives, function($hasInclude, $directive) {
|
$include = array_reduce($directives, function ($hasInclude, $directive) {
|
||||||
return $hasInclude || $directive->name == 'include';
|
return $hasInclude || $directive->name == 'include';
|
||||||
});
|
});
|
||||||
if (!$include) {
|
if (!$include) {
|
||||||
$directives[] = Directive::includeDirective();
|
$directives[] = Directive::includeDirective();
|
||||||
}
|
}
|
||||||
|
|
||||||
$deprecated = array_reduce($directives, function($hasDeprecated, $directive) {
|
$deprecated = array_reduce($directives, function ($hasDeprecated, $directive) {
|
||||||
return $hasDeprecated || $directive->name == 'deprecated';
|
return $hasDeprecated || $directive->name == 'deprecated';
|
||||||
});
|
});
|
||||||
if (!$deprecated) {
|
if (!$deprecated) {
|
||||||
$directives[] = Directive::deprecatedDirective();
|
$directives[] = Directive::deprecatedDirective();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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([
|
$schema = new Schema([
|
||||||
'query' => $this->getObjectType($this->nodeMap[$queryTypeName]),
|
'query' => isset($operationTypes['query'])
|
||||||
'mutation' => $mutationTypeName ?
|
? $defintionBuilder->buildType($operationTypes['query'])
|
||||||
$this->getObjectType($this->nodeMap[$mutationTypeName]) :
|
: null,
|
||||||
null,
|
'mutation' => isset($operationTypes['mutation'])
|
||||||
'subscription' => $subscriptionTypeName ?
|
? $defintionBuilder->buildType($operationTypes['mutation'])
|
||||||
$this->getObjectType($this->nodeMap[$subscriptionTypeName]) :
|
: null,
|
||||||
null,
|
'subscription' => isset($operationTypes['subscription'])
|
||||||
'typeLoader' => function($name) {
|
? $defintionBuilder->buildType($operationTypes['subscription'])
|
||||||
return $this->typeDefNamed($name);
|
: null,
|
||||||
|
'typeLoader' => function ($name) use ($defintionBuilder) {
|
||||||
|
return $defintionBuilder->buildType($name);
|
||||||
},
|
},
|
||||||
'directives' => $directives,
|
'directives' => $directives,
|
||||||
'astNode' => $schemaDef,
|
'astNode' => $schemaDef,
|
||||||
'types' => function() {
|
'types' => function () use ($defintionBuilder) {
|
||||||
$types = [];
|
$types = [];
|
||||||
foreach ($this->nodeMap as $name => $def) {
|
foreach ($this->nodeMap as $name => $def) {
|
||||||
if (!isset($this->loadedTypeDefs[$name])) {
|
$types[] = $defintionBuilder->buildType($def->name->value);
|
||||||
$types[] = $this->typeDefNamed($def->name->value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return $types;
|
return $types;
|
||||||
}
|
}
|
||||||
@ -256,364 +163,31 @@ class BuildSchema
|
|||||||
return $schema;
|
return $schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getDirective(DirectiveDefinitionNode $directiveNode)
|
/**
|
||||||
|
* @param SchemaDefinitionNode $schemaDef
|
||||||
|
* @return array
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
private function getOperationTypes($schemaDef)
|
||||||
{
|
{
|
||||||
return new Directive([
|
$opTypes = [];
|
||||||
'name' => $directiveNode->name->value,
|
|
||||||
'description' => $this->getDescription($directiveNode),
|
|
||||||
'locations' => Utils::map($directiveNode->locations, function($node) {
|
|
||||||
return $node->value;
|
|
||||||
}),
|
|
||||||
'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null,
|
|
||||||
'astNode' => $directiveNode
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getObjectType(TypeDefinitionNode $typeNode)
|
foreach ($schemaDef->operationTypes as $operationType) {
|
||||||
{
|
$typeName = $operationType->type->name->value;
|
||||||
$type = $this->typeDefNamed($typeNode->name->value);
|
$operation = $operationType->operation;
|
||||||
Utils::invariant(
|
|
||||||
$type instanceof ObjectType,
|
|
||||||
'AST must provide object type.'
|
|
||||||
);
|
|
||||||
return $type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function produceType(TypeNode $typeNode)
|
if (isset($opTypes[$operation])) {
|
||||||
{
|
throw new Error("Must provide only one $operation type in schema.");
|
||||||
$typeName = $this->getNamedTypeNode($typeNode)->name->value;
|
|
||||||
$typeDef = $this->typeDefNamed($typeName);
|
|
||||||
return $this->buildWrappedType($typeDef, $typeNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function produceInputType(TypeNode $typeNode)
|
|
||||||
{
|
|
||||||
$type = $this->produceType($typeNode);
|
|
||||||
Utils::invariant(Type::isInputType($type), 'Expected Input type.');
|
|
||||||
return $type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function produceOutputType(TypeNode $typeNode)
|
|
||||||
{
|
|
||||||
$type = $this->produceType($typeNode);
|
|
||||||
Utils::invariant(Type::isOutputType($type), 'Expected Input type.');
|
|
||||||
return $type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function produceObjectType(TypeNode $typeNode)
|
|
||||||
{
|
|
||||||
$type = $this->produceType($typeNode);
|
|
||||||
Utils::invariant($type instanceof ObjectType, 'Expected Object type.');
|
|
||||||
return $type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function produceInterfaceType(TypeNode $typeNode)
|
|
||||||
{
|
|
||||||
$type = $this->produceType($typeNode);
|
|
||||||
Utils::invariant($type instanceof InterfaceType, 'Expected Interface type.');
|
|
||||||
return $type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function typeDefNamed($typeName)
|
|
||||||
{
|
|
||||||
if (isset($this->innerTypeMap[$typeName])) {
|
|
||||||
return $this->innerTypeMap[$typeName];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($this->nodeMap[$typeName])) {
|
if (!isset($this->nodeMap[$typeName])) {
|
||||||
throw new Error('Type "' . $typeName . '" not found in document.');
|
throw new Error("Specified $operation type \"$typeName\" not found in document.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->loadedTypeDefs[$typeName] = true;
|
$opTypes[$operation] = $typeName;
|
||||||
|
|
||||||
$config = $this->makeSchemaDefConfig($this->nodeMap[$typeName]);
|
|
||||||
|
|
||||||
if ($this->typeConfigDecorator) {
|
|
||||||
$fn = $this->typeConfigDecorator;
|
|
||||||
try {
|
|
||||||
$config = $fn($config, $this->nodeMap[$typeName], $this->nodeMap);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
throw new Error(
|
|
||||||
"Type config decorator passed to " . (static::class) . " threw an error ".
|
|
||||||
"when building $typeName type: {$e->getMessage()}",
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
$e
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
throw new Error(
|
|
||||||
"Type config decorator passed to " . (static::class) . " threw an error ".
|
|
||||||
"when building $typeName type: {$e->getMessage()}",
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
$e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!is_array($config) || isset($config[0])) {
|
|
||||||
throw new Error(
|
|
||||||
"Type config decorator passed to " . (static::class) . " is expected to return an array, but got ".
|
|
||||||
Utils::getVariableType($config)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$innerTypeDef = $this->makeSchemaDef($this->nodeMap[$typeName], $config);
|
return $opTypes;
|
||||||
|
|
||||||
if (!$innerTypeDef) {
|
|
||||||
throw new Error("Nothing constructed for $typeName.");
|
|
||||||
}
|
|
||||||
$this->innerTypeMap[$typeName] = $innerTypeDef;
|
|
||||||
return $innerTypeDef;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeSchemaDefConfig($def)
|
|
||||||
{
|
|
||||||
if (!$def) {
|
|
||||||
throw new Error('def must be defined.');
|
|
||||||
}
|
|
||||||
switch ($def->kind) {
|
|
||||||
case NodeKind::OBJECT_TYPE_DEFINITION:
|
|
||||||
return $this->makeTypeDefConfig($def);
|
|
||||||
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
|
||||||
return $this->makeInterfaceDefConfig($def);
|
|
||||||
case NodeKind::ENUM_TYPE_DEFINITION:
|
|
||||||
return $this->makeEnumDefConfig($def);
|
|
||||||
case NodeKind::UNION_TYPE_DEFINITION:
|
|
||||||
return $this->makeUnionDefConfig($def);
|
|
||||||
case NodeKind::SCALAR_TYPE_DEFINITION:
|
|
||||||
return $this->makeScalarDefConfig($def);
|
|
||||||
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
|
||||||
return $this->makeInputObjectDefConfig($def);
|
|
||||||
default:
|
|
||||||
throw new Error("Type kind of {$def->kind} not supported.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeSchemaDef($def, array $config = null)
|
|
||||||
{
|
|
||||||
if (!$def) {
|
|
||||||
throw new Error('def must be defined.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = $config ?: $this->makeSchemaDefConfig($def);
|
|
||||||
|
|
||||||
switch ($def->kind) {
|
|
||||||
case NodeKind::OBJECT_TYPE_DEFINITION:
|
|
||||||
return new ObjectType($config);
|
|
||||||
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
|
||||||
return new InterfaceType($config);
|
|
||||||
case NodeKind::ENUM_TYPE_DEFINITION:
|
|
||||||
return new EnumType($config);
|
|
||||||
case NodeKind::UNION_TYPE_DEFINITION:
|
|
||||||
return new UnionType($config);
|
|
||||||
case NodeKind::SCALAR_TYPE_DEFINITION:
|
|
||||||
return new CustomScalarType($config);
|
|
||||||
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
|
||||||
return new InputObjectType($config);
|
|
||||||
default:
|
|
||||||
throw new Error("Type kind of {$def->kind} not supported.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeTypeDefConfig(ObjectTypeDefinitionNode $def)
|
|
||||||
{
|
|
||||||
$typeName = $def->name->value;
|
|
||||||
return [
|
|
||||||
'name' => $typeName,
|
|
||||||
'description' => $this->getDescription($def),
|
|
||||||
'fields' => function() use ($def) {
|
|
||||||
return $this->makeFieldDefMap($def);
|
|
||||||
},
|
|
||||||
'interfaces' => function() use ($def) {
|
|
||||||
return $this->makeImplementedInterfaces($def);
|
|
||||||
},
|
|
||||||
'astNode' => $def
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeFieldDefMap($def)
|
|
||||||
{
|
|
||||||
return Utils::keyValMap(
|
|
||||||
$def->fields,
|
|
||||||
function ($field) {
|
|
||||||
return $field->name->value;
|
|
||||||
},
|
|
||||||
function($field) {
|
|
||||||
return [
|
|
||||||
'type' => $this->produceOutputType($field->type),
|
|
||||||
'description' => $this->getDescription($field),
|
|
||||||
'args' => $this->makeInputValues($field->arguments),
|
|
||||||
'deprecationReason' => $this->getDeprecationReason($field),
|
|
||||||
'astNode' => $field
|
|
||||||
];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
|
|
||||||
{
|
|
||||||
if (isset($def->interfaces)) {
|
|
||||||
return Utils::map($def->interfaces, function ($iface) {
|
|
||||||
return $this->produceInterfaceType($iface);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeInputValues($values)
|
|
||||||
{
|
|
||||||
return Utils::keyValMap(
|
|
||||||
$values,
|
|
||||||
function ($value) {
|
|
||||||
return $value->name->value;
|
|
||||||
},
|
|
||||||
function($value) {
|
|
||||||
$type = $this->produceInputType($value->type);
|
|
||||||
$config = [
|
|
||||||
'name' => $value->name->value,
|
|
||||||
'type' => $type,
|
|
||||||
'description' => $this->getDescription($value),
|
|
||||||
'astNode' => $value
|
|
||||||
];
|
|
||||||
if (isset($value->defaultValue)) {
|
|
||||||
$config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
|
|
||||||
}
|
|
||||||
return $config;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeInterfaceDefConfig(InterfaceTypeDefinitionNode $def)
|
|
||||||
{
|
|
||||||
$typeName = $def->name->value;
|
|
||||||
return [
|
|
||||||
'name' => $typeName,
|
|
||||||
'description' => $this->getDescription($def),
|
|
||||||
'fields' => function() use ($def) {
|
|
||||||
return $this->makeFieldDefMap($def);
|
|
||||||
},
|
|
||||||
'astNode' => $def,
|
|
||||||
'resolveType' => function() {
|
|
||||||
$this->cannotExecuteSchema();
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeEnumDefConfig(EnumTypeDefinitionNode $def)
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => $def->name->value,
|
|
||||||
'description' => $this->getDescription($def),
|
|
||||||
'astNode' => $def,
|
|
||||||
'values' => Utils::keyValMap(
|
|
||||||
$def->values,
|
|
||||||
function($enumValue) {
|
|
||||||
return $enumValue->name->value;
|
|
||||||
},
|
|
||||||
function($enumValue) {
|
|
||||||
return [
|
|
||||||
'description' => $this->getDescription($enumValue),
|
|
||||||
'deprecationReason' => $this->getDeprecationReason($enumValue),
|
|
||||||
'astNode' => $enumValue
|
|
||||||
];
|
|
||||||
}
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeUnionDefConfig(UnionTypeDefinitionNode $def)
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => $def->name->value,
|
|
||||||
'description' => $this->getDescription($def),
|
|
||||||
'types' => Utils::map($def->types, function($typeNode) {
|
|
||||||
return $this->produceObjectType($typeNode);
|
|
||||||
}),
|
|
||||||
'astNode' => $def,
|
|
||||||
'resolveType' => [$this, 'cannotExecuteSchema']
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeScalarDefConfig(ScalarTypeDefinitionNode $def)
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => $def->name->value,
|
|
||||||
'description' => $this->getDescription($def),
|
|
||||||
'astNode' => $def,
|
|
||||||
'serialize' => function() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
// Note: validation calls the parse functions to determine if a
|
|
||||||
// literal value is correct. Returning null would cause use of custom
|
|
||||||
// scalars to always fail validation. Returning false causes them to
|
|
||||||
// always pass validation.
|
|
||||||
'parseValue' => function() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
'parseLiteral' => function() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeInputObjectDefConfig(InputObjectTypeDefinitionNode $def)
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => $def->name->value,
|
|
||||||
'description' => $this->getDescription($def),
|
|
||||||
'fields' => function() use ($def) { return $this->makeInputValues($def->fields); },
|
|
||||||
'astNode' => $def,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a collection of directives, returns the string value for the
|
|
||||||
* deprecation reason.
|
|
||||||
*
|
|
||||||
* @param EnumValueDefinitionNode | FieldDefinitionNode $node
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function getDeprecationReason($node)
|
|
||||||
{
|
|
||||||
$deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node);
|
|
||||||
return isset($deprecated['reason']) ? $deprecated['reason'] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an ast node, returns its string description based on a contiguous
|
|
||||||
* block full-line of comments preceding it.
|
|
||||||
*/
|
|
||||||
public function getDescription($node)
|
|
||||||
{
|
|
||||||
$loc = $node->loc;
|
|
||||||
if (!$loc || !$loc->startToken) {
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
$comments = [];
|
|
||||||
$minSpaces = null;
|
|
||||||
$token = $loc->startToken->prev;
|
|
||||||
while (
|
|
||||||
$token &&
|
|
||||||
$token->kind === Token::COMMENT &&
|
|
||||||
$token->next && $token->prev &&
|
|
||||||
$token->line + 1 === $token->next->line &&
|
|
||||||
$token->line !== $token->prev->line
|
|
||||||
) {
|
|
||||||
$value = $token->value;
|
|
||||||
$spaces = $this->leadingSpaces($value);
|
|
||||||
if ($minSpaces === null || $spaces < $minSpaces) {
|
|
||||||
$minSpaces = $spaces;
|
|
||||||
}
|
|
||||||
$comments[] = $value;
|
|
||||||
$token = $token->prev;
|
|
||||||
}
|
|
||||||
return implode("\n", array_map(function($comment) use ($minSpaces) {
|
|
||||||
return mb_substr(str_replace("\n", '', $comment), $minSpaces);
|
|
||||||
}, array_reverse($comments)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -623,25 +197,12 @@ class BuildSchema
|
|||||||
* @api
|
* @api
|
||||||
* @param DocumentNode|Source|string $source
|
* @param DocumentNode|Source|string $source
|
||||||
* @param callable $typeConfigDecorator
|
* @param callable $typeConfigDecorator
|
||||||
|
* @param array $options
|
||||||
* @return Schema
|
* @return Schema
|
||||||
*/
|
*/
|
||||||
public static function build($source, callable $typeConfigDecorator = null)
|
public static function build($source, callable $typeConfigDecorator = null, array $options = [])
|
||||||
{
|
{
|
||||||
$doc = $source instanceof DocumentNode ? $source : Parser::parse($source);
|
$doc = $source instanceof DocumentNode ? $source : Parser::parse($source);
|
||||||
return self::buildAST($doc, $typeConfigDecorator);
|
return self::buildAST($doc, $typeConfigDecorator, $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count the number of spaces on the starting side of a string.
|
|
||||||
private function leadingSpaces($str)
|
|
||||||
{
|
|
||||||
return strlen($str) - strlen(ltrim($str));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function cannotExecuteSchema()
|
|
||||||
{
|
|
||||||
throw new Error(
|
|
||||||
'Generated Schema cannot use Interface or Union types for execution.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -5,10 +5,12 @@
|
|||||||
|
|
||||||
namespace GraphQL\Utils;
|
namespace GraphQL\Utils;
|
||||||
|
|
||||||
|
use GraphQL\Type\Definition\Directive;
|
||||||
use GraphQL\Type\Definition\EnumType;
|
use GraphQL\Type\Definition\EnumType;
|
||||||
use GraphQL\Type\Definition\InputObjectType;
|
use GraphQL\Type\Definition\InputObjectType;
|
||||||
use GraphQL\Type\Definition\InterfaceType;
|
use GraphQL\Type\Definition\InterfaceType;
|
||||||
use GraphQL\Type\Definition\ListOfType;
|
use GraphQL\Type\Definition\ListOfType;
|
||||||
|
use GraphQL\Type\Definition\NamedType;
|
||||||
use GraphQL\Type\Definition\NonNull;
|
use GraphQL\Type\Definition\NonNull;
|
||||||
use GraphQL\Type\Definition\ObjectType;
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
use GraphQL\Type\Definition\ScalarType;
|
use GraphQL\Type\Definition\ScalarType;
|
||||||
@ -19,35 +21,28 @@ use GraphQL\Type\Schema;
|
|||||||
class FindBreakingChanges
|
class FindBreakingChanges
|
||||||
{
|
{
|
||||||
|
|
||||||
const BREAKING_CHANGE_FIELD_CHANGED = 'FIELD_CHANGED_KIND';
|
const BREAKING_CHANGE_FIELD_CHANGED_KIND = 'FIELD_CHANGED_KIND';
|
||||||
const BREAKING_CHANGE_FIELD_REMOVED = 'FIELD_REMOVED';
|
const BREAKING_CHANGE_FIELD_REMOVED = 'FIELD_REMOVED';
|
||||||
const BREAKING_CHANGE_TYPE_CHANGED = 'TYPE_CHANGED_KIND';
|
const BREAKING_CHANGE_TYPE_CHANGED_KIND = 'TYPE_CHANGED_KIND';
|
||||||
const BREAKING_CHANGE_TYPE_REMOVED = 'TYPE_REMOVED';
|
const BREAKING_CHANGE_TYPE_REMOVED = 'TYPE_REMOVED';
|
||||||
const BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION = 'TYPE_REMOVED_FROM_UNION';
|
const BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION = 'TYPE_REMOVED_FROM_UNION';
|
||||||
const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM = 'VALUE_REMOVED_FROM_ENUM';
|
const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM = 'VALUE_REMOVED_FROM_ENUM';
|
||||||
const BREAKING_CHANGE_ARG_REMOVED = 'ARG_REMOVED';
|
const BREAKING_CHANGE_ARG_REMOVED = 'ARG_REMOVED';
|
||||||
const BREAKING_CHANGE_ARG_CHANGED = 'ARG_CHANGED_KIND';
|
const BREAKING_CHANGE_ARG_CHANGED_KIND = 'ARG_CHANGED_KIND';
|
||||||
const BREAKING_CHANGE_NON_NULL_ARG_ADDED = 'NON_NULL_ARG_ADDED';
|
const BREAKING_CHANGE_NON_NULL_ARG_ADDED = 'NON_NULL_ARG_ADDED';
|
||||||
const BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED = 'NON_NULL_INPUT_FIELD_ADDED';
|
const BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED = 'NON_NULL_INPUT_FIELD_ADDED';
|
||||||
const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'INTERFACE_REMOVED_FROM_OBJECT';
|
const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'INTERFACE_REMOVED_FROM_OBJECT';
|
||||||
|
const BREAKING_CHANGE_DIRECTIVE_REMOVED = 'DIRECTIVE_REMOVED';
|
||||||
|
const BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED = 'DIRECTIVE_ARG_REMOVED';
|
||||||
|
const BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED = 'DIRECTIVE_LOCATION_REMOVED';
|
||||||
|
const BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED = 'NON_NULL_DIRECTIVE_ARG_ADDED';
|
||||||
|
|
||||||
const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE = 'ARG_DEFAULT_VALUE_CHANGE';
|
const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED = 'ARG_DEFAULT_VALUE_CHANGE';
|
||||||
const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM';
|
const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM';
|
||||||
|
const DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT = 'INTERFACE_ADDED_TO_OBJECT';
|
||||||
const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION';
|
const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION';
|
||||||
|
const DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED = 'NULLABLE_INPUT_FIELD_ADDED';
|
||||||
/**
|
const DANGEROUS_CHANGE_NULLABLE_ARG_ADDED = 'NULLABLE_ARG_ADDED';
|
||||||
* Given two schemas, returns an Array containing descriptions of all the types
|
|
||||||
* of potentially dangerous changes covered by the other functions down below.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema)
|
|
||||||
{
|
|
||||||
return array_merge(self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'],
|
|
||||||
self::findValuesAddedToEnums($oldSchema, $newSchema),
|
|
||||||
self::findTypesAddedToUnions($oldSchema, $newSchema)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given two schemas, returns an Array containing descriptions of all the types
|
* Given two schemas, returns an Array containing descriptions of all the types
|
||||||
@ -60,11 +55,33 @@ class FindBreakingChanges
|
|||||||
return array_merge(
|
return array_merge(
|
||||||
self::findRemovedTypes($oldSchema, $newSchema),
|
self::findRemovedTypes($oldSchema, $newSchema),
|
||||||
self::findTypesThatChangedKind($oldSchema, $newSchema),
|
self::findTypesThatChangedKind($oldSchema, $newSchema),
|
||||||
self::findFieldsThatChangedType($oldSchema, $newSchema),
|
self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema),
|
||||||
|
self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'],
|
||||||
self::findTypesRemovedFromUnions($oldSchema, $newSchema),
|
self::findTypesRemovedFromUnions($oldSchema, $newSchema),
|
||||||
self::findValuesRemovedFromEnums($oldSchema, $newSchema),
|
self::findValuesRemovedFromEnums($oldSchema, $newSchema),
|
||||||
self::findArgChanges($oldSchema, $newSchema)['breakingChanges'],
|
self::findArgChanges($oldSchema, $newSchema)['breakingChanges'],
|
||||||
self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema)
|
self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema),
|
||||||
|
self::findRemovedDirectives($oldSchema, $newSchema),
|
||||||
|
self::findRemovedDirectiveArgs($oldSchema, $newSchema),
|
||||||
|
self::findAddedNonNullDirectiveArgs($oldSchema, $newSchema),
|
||||||
|
self::findRemovedDirectiveLocations($oldSchema, $newSchema)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given two schemas, returns an Array containing descriptions of all the types
|
||||||
|
* of potentially dangerous changes covered by the other functions down below.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema)
|
||||||
|
{
|
||||||
|
return array_merge(
|
||||||
|
self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'],
|
||||||
|
self::findValuesAddedToEnums($oldSchema, $newSchema),
|
||||||
|
self::findInterfacesAddedToObjectTypes($oldSchema, $newSchema),
|
||||||
|
self::findTypesAddedToUnions($oldSchema, $newSchema),
|
||||||
|
self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,20 +92,21 @@ class FindBreakingChanges
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function findRemovedTypes(
|
public static function findRemovedTypes(
|
||||||
Schema $oldSchema, Schema $newSchema
|
Schema $oldSchema,
|
||||||
)
|
Schema $newSchema
|
||||||
{
|
) {
|
||||||
$oldTypeMap = $oldSchema->getTypeMap();
|
$oldTypeMap = $oldSchema->getTypeMap();
|
||||||
$newTypeMap = $newSchema->getTypeMap();
|
$newTypeMap = $newSchema->getTypeMap();
|
||||||
|
|
||||||
$breakingChanges = [];
|
$breakingChanges = [];
|
||||||
foreach ($oldTypeMap as $typeName => $typeDefinition) {
|
foreach (array_keys($oldTypeMap) as $typeName) {
|
||||||
if (!isset($newTypeMap[$typeName])) {
|
if (!isset($newTypeMap[$typeName])) {
|
||||||
$breakingChanges[] =
|
$breakingChanges[] = [
|
||||||
['type' => self::BREAKING_CHANGE_TYPE_REMOVED, 'description' => "${typeName} was removed."];
|
'type' => self::BREAKING_CHANGE_TYPE_REMOVED,
|
||||||
|
'description' => "${typeName} was removed."
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $breakingChanges;
|
return $breakingChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,28 +117,27 @@ class FindBreakingChanges
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function findTypesThatChangedKind(
|
public static function findTypesThatChangedKind(
|
||||||
Schema $oldSchema, Schema $newSchema
|
Schema $oldSchema,
|
||||||
)
|
Schema $newSchema
|
||||||
{
|
) {
|
||||||
$oldTypeMap = $oldSchema->getTypeMap();
|
$oldTypeMap = $oldSchema->getTypeMap();
|
||||||
$newTypeMap = $newSchema->getTypeMap();
|
$newTypeMap = $newSchema->getTypeMap();
|
||||||
|
|
||||||
$breakingChanges = [];
|
$breakingChanges = [];
|
||||||
foreach ($oldTypeMap as $typeName => $typeDefinition) {
|
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||||
if (!isset($newTypeMap[$typeName])) {
|
if (!isset($newTypeMap[$typeName])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$newTypeDefinition = $newTypeMap[$typeName];
|
$newType = $newTypeMap[$typeName];
|
||||||
if (!($typeDefinition instanceof $newTypeDefinition)) {
|
if (!($oldType instanceof $newType)) {
|
||||||
$oldTypeKindName = self::typeKindName($typeDefinition);
|
$oldTypeKindName = self::typeKindName($oldType);
|
||||||
$newTypeKindName = self::typeKindName($newTypeDefinition);
|
$newTypeKindName = self::typeKindName($newType);
|
||||||
$breakingChanges[] = [
|
$breakingChanges[] = [
|
||||||
'type' => self::BREAKING_CHANGE_TYPE_CHANGED,
|
'type' => self::BREAKING_CHANGE_TYPE_CHANGED_KIND,
|
||||||
'description' => "${typeName} changed from ${oldTypeKindName} to ${newTypeKindName}."
|
'description' => "${typeName} changed from ${oldTypeKindName} to ${newTypeKindName}."
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $breakingChanges;
|
return $breakingChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,56 +150,63 @@ class FindBreakingChanges
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function findArgChanges(
|
public static function findArgChanges(
|
||||||
Schema $oldSchema, Schema $newSchema
|
Schema $oldSchema,
|
||||||
)
|
Schema $newSchema
|
||||||
{
|
) {
|
||||||
$oldTypeMap = $oldSchema->getTypeMap();
|
$oldTypeMap = $oldSchema->getTypeMap();
|
||||||
$newTypeMap = $newSchema->getTypeMap();
|
$newTypeMap = $newSchema->getTypeMap();
|
||||||
|
|
||||||
$breakingChanges = [];
|
$breakingChanges = [];
|
||||||
$dangerousChanges = [];
|
$dangerousChanges = [];
|
||||||
foreach ($oldTypeMap as $oldTypeName => $oldTypeDefinition) {
|
|
||||||
$newTypeDefinition = isset($newTypeMap[$oldTypeName]) ? $newTypeMap[$oldTypeName] : null;
|
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||||
if (!($oldTypeDefinition instanceof ObjectType || $oldTypeDefinition instanceof InterfaceType) ||
|
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
||||||
!($newTypeDefinition instanceof $oldTypeDefinition)) {
|
if (
|
||||||
|
!($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
|
||||||
|
!($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
|
||||||
|
!($newType instanceof $oldType)
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$oldTypeFields = $oldTypeDefinition->getFields();
|
$oldTypeFields = $oldType->getFields();
|
||||||
$newTypeFields = $newTypeDefinition->getFields();
|
$newTypeFields = $newType->getFields();
|
||||||
|
|
||||||
foreach ($oldTypeFields as $fieldName => $fieldDefinition) {
|
foreach ($oldTypeFields as $fieldName => $oldField) {
|
||||||
if (!isset($newTypeFields[$fieldName])) {
|
if (!isset($newTypeFields[$fieldName])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($fieldDefinition->args as $oldArgDef) {
|
foreach ($oldField->args as $oldArgDef) {
|
||||||
$newArgs = $newTypeFields[$fieldName]->args;
|
$newArgs = $newTypeFields[$fieldName]->args;
|
||||||
$newArgDef = Utils::find(
|
$newArgDef = Utils::find(
|
||||||
$newArgs, function ($arg) use ($oldArgDef) {
|
$newArgs,
|
||||||
|
function ($arg) use ($oldArgDef) {
|
||||||
return $arg->name === $oldArgDef->name;
|
return $arg->name === $oldArgDef->name;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!$newArgDef) {
|
if (!$newArgDef) {
|
||||||
$argName = $oldArgDef->name;
|
|
||||||
$breakingChanges[] = [
|
$breakingChanges[] = [
|
||||||
'type' => self::BREAKING_CHANGE_ARG_REMOVED,
|
'type' => self::BREAKING_CHANGE_ARG_REMOVED,
|
||||||
'description' => "${oldTypeName}->${fieldName} arg ${argName} was removed"
|
'description' => "${typeName}.${fieldName} arg {$oldArgDef->name} was removed"
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
$isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg($oldArgDef->getType(), $newArgDef->getType());
|
$isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
|
||||||
|
$oldArgDef->getType(),
|
||||||
|
$newArgDef->getType()
|
||||||
|
);
|
||||||
$oldArgType = $oldArgDef->getType();
|
$oldArgType = $oldArgDef->getType();
|
||||||
$oldArgName = $oldArgDef->name;
|
$oldArgName = $oldArgDef->name;
|
||||||
if (!$isSafe) {
|
if (!$isSafe) {
|
||||||
$newArgType = $newArgDef->getType();
|
$newArgType = $newArgDef->getType();
|
||||||
$breakingChanges[] = [
|
$breakingChanges[] = [
|
||||||
'type' => self::BREAKING_CHANGE_ARG_CHANGED,
|
'type' => self::BREAKING_CHANGE_ARG_CHANGED_KIND,
|
||||||
'description' => "${oldTypeName}->${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}."
|
'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}"
|
||||||
];
|
];
|
||||||
} elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) {
|
} elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) {
|
||||||
$dangerousChanges[] = [
|
$dangerousChanges[] = [
|
||||||
'type' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE,
|
'type' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED,
|
||||||
'description' => "${oldTypeName}->${fieldName} arg ${oldArgName} has changed defaultValue"
|
'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed defaultValue"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,25 +214,36 @@ class FindBreakingChanges
|
|||||||
foreach ($newTypeFields[$fieldName]->args as $newArgDef) {
|
foreach ($newTypeFields[$fieldName]->args as $newArgDef) {
|
||||||
$oldArgs = $oldTypeFields[$fieldName]->args;
|
$oldArgs = $oldTypeFields[$fieldName]->args;
|
||||||
$oldArgDef = Utils::find(
|
$oldArgDef = Utils::find(
|
||||||
$oldArgs, function ($arg) use ($newArgDef) {
|
$oldArgs,
|
||||||
|
function ($arg) use ($newArgDef) {
|
||||||
return $arg->name === $newArgDef->name;
|
return $arg->name === $newArgDef->name;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!$oldArgDef && $newArgDef->getType() instanceof NonNull) {
|
if (!$oldArgDef) {
|
||||||
$newTypeName = $newTypeDefinition->name;
|
$newTypeName = $newType->name;
|
||||||
$newArgName = $newArgDef->name;
|
$newArgName = $newArgDef->name;
|
||||||
|
if ($newArgDef->getType() instanceof NonNull) {
|
||||||
$breakingChanges[] = [
|
$breakingChanges[] = [
|
||||||
'type' => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED,
|
'type' => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED,
|
||||||
'description' => "A non-null arg ${newArgName} on ${newTypeName}->${fieldName} was added."
|
'description' => "A non-null arg ${newArgName} on ${newTypeName}.${fieldName} was added"
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$dangerousChanges[] = [
|
||||||
|
'type' => self::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED,
|
||||||
|
'description' => "A nullable arg ${newArgName} on ${newTypeName}.${fieldName} was added"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ['breakingChanges' => $breakingChanges, 'dangerousChanges' => $dangerousChanges];
|
return [
|
||||||
|
'breakingChanges' => $breakingChanges,
|
||||||
|
'dangerousChanges' => $dangerousChanges,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -236,155 +271,188 @@ class FindBreakingChanges
|
|||||||
throw new \TypeError('unknown type ' . $type->name);
|
throw new \TypeError('unknown type ' . $type->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(
|
||||||
* Given two schemas, returns an Array containing descriptions of any breaking
|
Schema $oldSchema,
|
||||||
* changes in the newSchema related to the fields on a type. This includes if
|
Schema $newSchema
|
||||||
* a field has been removed from a type, if a field has changed type, or if
|
) {
|
||||||
* a non-null field is added to an input type.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function findFieldsThatChangedType(
|
|
||||||
Schema $oldSchema, Schema $newSchema
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return array_merge(
|
|
||||||
self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema),
|
|
||||||
self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Schema $oldSchema
|
|
||||||
* @param Schema $newSchema
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
private static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(Schema $oldSchema, Schema $newSchema)
|
|
||||||
{
|
|
||||||
$oldTypeMap = $oldSchema->getTypeMap();
|
$oldTypeMap = $oldSchema->getTypeMap();
|
||||||
$newTypeMap = $newSchema->getTypeMap();
|
$newTypeMap = $newSchema->getTypeMap();
|
||||||
|
|
||||||
$breakingFieldChanges = [];
|
$breakingChanges = [];
|
||||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||||
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
||||||
if (!($oldType instanceof ObjectType || $oldType instanceof InterfaceType) || !($newType instanceof $oldType)) {
|
if (
|
||||||
|
!($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
|
||||||
|
!($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
|
||||||
|
!($newType instanceof $oldType)
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$oldTypeFieldsDef = $oldType->getFields();
|
$oldTypeFieldsDef = $oldType->getFields();
|
||||||
$newTypeFieldsDef = $newType->getFields();
|
$newTypeFieldsDef = $newType->getFields();
|
||||||
foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) {
|
foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) {
|
||||||
|
// Check if the field is missing on the type in the new schema.
|
||||||
if (!isset($newTypeFieldsDef[$fieldName])) {
|
if (!isset($newTypeFieldsDef[$fieldName])) {
|
||||||
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "${typeName}->${fieldName} was removed."];
|
$breakingChanges[] = [
|
||||||
|
'type' => self::BREAKING_CHANGE_FIELD_REMOVED,
|
||||||
|
'description' => "${typeName}.${fieldName} was removed."
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
$oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
|
$oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
|
||||||
$newfieldType = $newTypeFieldsDef[$fieldName]->getType();
|
$newFieldType = $newTypeFieldsDef[$fieldName]->getType();
|
||||||
$isSafe = self::isChangeSafeForObjectOrInterfaceField($oldFieldType, $newfieldType);
|
$isSafe = self::isChangeSafeForObjectOrInterfaceField(
|
||||||
|
$oldFieldType,
|
||||||
|
$newFieldType
|
||||||
|
);
|
||||||
if (!$isSafe) {
|
if (!$isSafe) {
|
||||||
|
$oldFieldTypeString = $oldFieldType instanceof NamedType
|
||||||
$oldFieldTypeString = self::isNamedType($oldFieldType) ? $oldFieldType->name : $oldFieldType;
|
? $oldFieldType->name
|
||||||
$newFieldTypeString = self::isNamedType($newfieldType) ? $newfieldType->name : $newfieldType;
|
: $oldFieldType;
|
||||||
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_CHANGED, 'description' => "${typeName}->${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."];
|
$newFieldTypeString = $newFieldType instanceof NamedType
|
||||||
|
? $newFieldType->name
|
||||||
|
: $newFieldType;
|
||||||
|
$breakingChanges[] = [
|
||||||
|
'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
|
||||||
|
'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $breakingFieldChanges;
|
return $breakingChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Schema $oldSchema
|
|
||||||
* @param Schema $newSchema
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function findFieldsThatChangedTypeOnInputObjectTypes(
|
public static function findFieldsThatChangedTypeOnInputObjectTypes(
|
||||||
Schema $oldSchema, Schema $newSchema
|
Schema $oldSchema,
|
||||||
)
|
Schema $newSchema
|
||||||
{
|
) {
|
||||||
$oldTypeMap = $oldSchema->getTypeMap();
|
$oldTypeMap = $oldSchema->getTypeMap();
|
||||||
$newTypeMap = $newSchema->getTypeMap();
|
$newTypeMap = $newSchema->getTypeMap();
|
||||||
|
|
||||||
$breakingFieldChanges = [];
|
$breakingChanges = [];
|
||||||
|
$dangerousChanges = [];
|
||||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||||
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
||||||
if (!($oldType instanceof InputObjectType) || !($newType instanceof InputObjectType)) {
|
if (!($oldType instanceof InputObjectType) || !($newType instanceof InputObjectType)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$oldTypeFieldsDef = $oldType->getFields();
|
$oldTypeFieldsDef = $oldType->getFields();
|
||||||
$newTypeFieldsDef = $newType->getFields();
|
$newTypeFieldsDef = $newType->getFields();
|
||||||
foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) {
|
foreach (array_keys($oldTypeFieldsDef) as $fieldName) {
|
||||||
if (!isset($newTypeFieldsDef[$fieldName])) {
|
if (!isset($newTypeFieldsDef[$fieldName])) {
|
||||||
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "${typeName}->${fieldName} was removed."];
|
$breakingChanges[] = [
|
||||||
|
'type' => self::BREAKING_CHANGE_FIELD_REMOVED,
|
||||||
|
'description' => "${typeName}.${fieldName} was removed."
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
$oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
|
$oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
|
||||||
$newfieldType = $newTypeFieldsDef[$fieldName]->getType();
|
$newFieldType = $newTypeFieldsDef[$fieldName]->getType();
|
||||||
$isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg($oldFieldType, $newfieldType);
|
|
||||||
|
$isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
|
||||||
|
$oldFieldType,
|
||||||
|
$newFieldType
|
||||||
|
);
|
||||||
if (!$isSafe) {
|
if (!$isSafe) {
|
||||||
$oldFieldTypeString = self::isNamedType($oldFieldType) ? $oldFieldType->name : $oldFieldType;
|
$oldFieldTypeString = $oldFieldType instanceof NamedType
|
||||||
$newFieldTypeString = self::isNamedType($newfieldType) ? $newfieldType->name : $newfieldType;
|
? $oldFieldType->name
|
||||||
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_CHANGED, 'description' => "${typeName}->${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."];
|
: $oldFieldType;
|
||||||
|
$newFieldTypeString = $newFieldType instanceof NamedType
|
||||||
|
? $newFieldType->name
|
||||||
|
: $newFieldType;
|
||||||
|
$breakingChanges[] = [
|
||||||
|
'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
|
||||||
|
'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Check if a field was added to the input object type
|
||||||
foreach ($newTypeFieldsDef as $fieldName => $fieldDef) {
|
foreach ($newTypeFieldsDef as $fieldName => $fieldDef) {
|
||||||
if (!isset($oldTypeFieldsDef[$fieldName]) && $fieldDef->getType() instanceof NonNull) {
|
if (!isset($oldTypeFieldsDef[$fieldName])) {
|
||||||
$newTypeName = $newType->name;
|
$newTypeName = $newType->name;
|
||||||
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, 'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added."];
|
if ($fieldDef->getType() instanceof NonNull) {
|
||||||
|
$breakingChanges[] = [
|
||||||
|
'type' => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED,
|
||||||
|
'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added."
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$dangerousChanges[] = [
|
||||||
|
'type' => self::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED,
|
||||||
|
'description' => "A nullable field ${fieldName} on input type ${newTypeName} was added."
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $breakingFieldChanges;
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'breakingChanges' => $breakingChanges,
|
||||||
|
'dangerousChanges' => $dangerousChanges,
|
||||||
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function isChangeSafeForObjectOrInterfaceField(
|
private static function isChangeSafeForObjectOrInterfaceField(
|
||||||
Type $oldType, Type $newType
|
Type $oldType,
|
||||||
)
|
Type $newType
|
||||||
{
|
) {
|
||||||
if (self::isNamedType($oldType)) {
|
if ($oldType instanceof NamedType) {
|
||||||
|
return (
|
||||||
// if they're both named types, see if their names are equivalent
|
// if they're both named types, see if their names are equivalent
|
||||||
return (self::isNamedType($newType) && $oldType->name === $newType->name)
|
($newType instanceof NamedType && $oldType->name === $newType->name) ||
|
||||||
// moving from nullable to non-null of the same underlying type is safe
|
// moving from nullable to non-null of the same underlying type is safe
|
||||||
|| ($newType instanceof NonNull
|
($newType instanceof NonNull &&
|
||||||
&& self::isChangeSafeForObjectOrInterfaceField(
|
self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType())
|
||||||
$oldType, $newType->getWrappedType()
|
)
|
||||||
));
|
);
|
||||||
} elseif ($oldType instanceof ListOfType) {
|
} elseif ($oldType instanceof ListOfType) {
|
||||||
|
return (
|
||||||
// if they're both lists, make sure the underlying types are compatible
|
// if they're both lists, make sure the underlying types are compatible
|
||||||
return ($newType instanceof ListOfType &&
|
($newType instanceof ListOfType &&
|
||||||
self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType())) ||
|
self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType())) ||
|
||||||
// moving from nullable to non-null of the same underlying type is safe
|
// moving from nullable to non-null of the same underlying type is safe
|
||||||
($newType instanceof NonNull &&
|
($newType instanceof NonNull &&
|
||||||
self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()));
|
self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()))
|
||||||
|
);
|
||||||
} elseif ($oldType instanceof NonNull) {
|
} elseif ($oldType instanceof NonNull) {
|
||||||
// if they're both non-null, make sure the underlying types are compatible
|
// if they're both non-null, make sure the underlying types are compatible
|
||||||
return $newType instanceof NonNull &&
|
return (
|
||||||
self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType());
|
$newType instanceof NonNull &&
|
||||||
|
self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Type $oldType
|
* @param Type $oldType
|
||||||
* @param Schema $newSchema
|
* @param Type $newType
|
||||||
*
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
private static function isChangeSafeForInputObjectFieldOrFieldArg(
|
private static function isChangeSafeForInputObjectFieldOrFieldArg(
|
||||||
Type $oldType, Type $newType
|
Type $oldType,
|
||||||
)
|
Type $newType
|
||||||
{
|
) {
|
||||||
if (self::isNamedType($oldType)) {
|
if ($oldType instanceof NamedType) {
|
||||||
return self::isNamedType($newType) && $oldType->name === $newType->name;
|
// if they're both named types, see if their names are equivalent
|
||||||
|
return $newType instanceof NamedType && $oldType->name === $newType->name;
|
||||||
} elseif ($oldType instanceof ListOfType) {
|
} elseif ($oldType instanceof ListOfType) {
|
||||||
|
// if they're both lists, make sure the underlying types are compatible
|
||||||
return $newType instanceof ListOfType && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType->getWrappedType());
|
return $newType instanceof ListOfType && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType->getWrappedType());
|
||||||
} elseif ($oldType instanceof NonNull) {
|
} elseif ($oldType instanceof NonNull) {
|
||||||
return (
|
return (
|
||||||
$newType instanceof NonNull && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType->getWrappedType())
|
// if they're both non-null, make sure the underlying types are
|
||||||
) || (
|
// compatible
|
||||||
!($newType instanceof NonNull) && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType)
|
($newType instanceof NonNull &&
|
||||||
|
self::isChangeSafeForInputObjectFieldOrFieldArg(
|
||||||
|
$oldType->getWrappedType(),
|
||||||
|
$newType->getWrappedType()
|
||||||
|
)) ||
|
||||||
|
// moving from non-null to nullable of the same underlying type is safe
|
||||||
|
(!($newType instanceof NonNull) &&
|
||||||
|
self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -397,9 +465,9 @@ class FindBreakingChanges
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function findTypesRemovedFromUnions(
|
public static function findTypesRemovedFromUnions(
|
||||||
Schema $oldSchema, Schema $newSchema
|
Schema $oldSchema,
|
||||||
)
|
Schema $newSchema
|
||||||
{
|
) {
|
||||||
$oldTypeMap = $oldSchema->getTypeMap();
|
$oldTypeMap = $oldSchema->getTypeMap();
|
||||||
$newTypeMap = $newSchema->getTypeMap();
|
$newTypeMap = $newSchema->getTypeMap();
|
||||||
|
|
||||||
@ -415,8 +483,10 @@ class FindBreakingChanges
|
|||||||
}
|
}
|
||||||
foreach ($oldType->getTypes() as $type) {
|
foreach ($oldType->getTypes() as $type) {
|
||||||
if (!isset($typeNamesInNewUnion[$type->name])) {
|
if (!isset($typeNamesInNewUnion[$type->name])) {
|
||||||
$missingTypeName = $type->name;
|
$typesRemovedFromUnion[] = [
|
||||||
$typesRemovedFromUnion[] = ['type' => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, 'description' => "${missingTypeName} was removed from union type ${typeName}."];
|
'type' => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
|
||||||
|
'description' => "{$type->name} was removed from union type ${typeName}.",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -430,14 +500,13 @@ class FindBreakingChanges
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function findTypesAddedToUnions(
|
public static function findTypesAddedToUnions(
|
||||||
Schema $oldSchema, Schema $newSchema
|
Schema $oldSchema,
|
||||||
)
|
Schema $newSchema
|
||||||
{
|
) {
|
||||||
$oldTypeMap = $oldSchema->getTypeMap();
|
$oldTypeMap = $oldSchema->getTypeMap();
|
||||||
$newTypeMap = $newSchema->getTypeMap();
|
$newTypeMap = $newSchema->getTypeMap();
|
||||||
|
|
||||||
$typesAddedToUnion = [];
|
$typesAddedToUnion = [];
|
||||||
|
|
||||||
foreach ($newTypeMap as $typeName => $newType) {
|
foreach ($newTypeMap as $typeName => $newType) {
|
||||||
$oldType = isset($oldTypeMap[$typeName]) ? $oldTypeMap[$typeName] : null;
|
$oldType = isset($oldTypeMap[$typeName]) ? $oldTypeMap[$typeName] : null;
|
||||||
if (!($oldType instanceof UnionType) || !($newType instanceof UnionType)) {
|
if (!($oldType instanceof UnionType) || !($newType instanceof UnionType)) {
|
||||||
@ -450,12 +519,13 @@ class FindBreakingChanges
|
|||||||
}
|
}
|
||||||
foreach ($newType->getTypes() as $type) {
|
foreach ($newType->getTypes() as $type) {
|
||||||
if (!isset($typeNamesInOldUnion[$type->name])) {
|
if (!isset($typeNamesInOldUnion[$type->name])) {
|
||||||
$addedTypeName = $type->name;
|
$typesAddedToUnion[] = [
|
||||||
$typesAddedToUnion[] = ['type' => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, 'description' => "${addedTypeName} was added to union type ${typeName}"];
|
'type' => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
|
||||||
|
'description' => "{$type->name} was added to union type ${typeName}.",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $typesAddedToUnion;
|
return $typesAddedToUnion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -466,14 +536,13 @@ class FindBreakingChanges
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function findValuesRemovedFromEnums(
|
public static function findValuesRemovedFromEnums(
|
||||||
Schema $oldSchema, Schema $newSchema
|
Schema $oldSchema,
|
||||||
)
|
Schema $newSchema
|
||||||
{
|
) {
|
||||||
$oldTypeMap = $oldSchema->getTypeMap();
|
$oldTypeMap = $oldSchema->getTypeMap();
|
||||||
$newTypeMap = $newSchema->getTypeMap();
|
$newTypeMap = $newSchema->getTypeMap();
|
||||||
|
|
||||||
$valuesRemovedFromEnums = [];
|
$valuesRemovedFromEnums = [];
|
||||||
|
|
||||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||||
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
||||||
if (!($oldType instanceof EnumType) || !($newType instanceof EnumType)) {
|
if (!($oldType instanceof EnumType) || !($newType instanceof EnumType)) {
|
||||||
@ -485,12 +554,13 @@ class FindBreakingChanges
|
|||||||
}
|
}
|
||||||
foreach ($oldType->getValues() as $value) {
|
foreach ($oldType->getValues() as $value) {
|
||||||
if (!isset($valuesInNewEnum[$value->name])) {
|
if (!isset($valuesInNewEnum[$value->name])) {
|
||||||
$valueName = $value->name;
|
$valuesRemovedFromEnums[] = [
|
||||||
$valuesRemovedFromEnums[] = ['type' => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, 'description' => "${valueName} was removed from enum type ${typeName}."];
|
'type' => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
|
||||||
|
'description' => "{$value->name} was removed from enum type ${typeName}.",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $valuesRemovedFromEnums;
|
return $valuesRemovedFromEnums;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,9 +571,9 @@ class FindBreakingChanges
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function findValuesAddedToEnums(
|
public static function findValuesAddedToEnums(
|
||||||
Schema $oldSchema, Schema $newSchema
|
Schema $oldSchema,
|
||||||
)
|
Schema $newSchema
|
||||||
{
|
) {
|
||||||
$oldTypeMap = $oldSchema->getTypeMap();
|
$oldTypeMap = $oldSchema->getTypeMap();
|
||||||
$newTypeMap = $newSchema->getTypeMap();
|
$newTypeMap = $newSchema->getTypeMap();
|
||||||
|
|
||||||
@ -519,12 +589,13 @@ class FindBreakingChanges
|
|||||||
}
|
}
|
||||||
foreach ($newType->getValues() as $value) {
|
foreach ($newType->getValues() as $value) {
|
||||||
if (!isset($valuesInOldEnum[$value->name])) {
|
if (!isset($valuesInOldEnum[$value->name])) {
|
||||||
$valueName = $value->name;
|
$valuesAddedToEnums[] = [
|
||||||
$valuesAddedToEnums[] = ['type' => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, 'description' => "${valueName} was added to enum type ${typeName}"];
|
'type' => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM,
|
||||||
|
'description' => "{$value->name} was added to enum type ${typeName}.",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $valuesAddedToEnums;
|
return $valuesAddedToEnums;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -535,13 +606,13 @@ class FindBreakingChanges
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function findInterfacesRemovedFromObjectTypes(
|
public static function findInterfacesRemovedFromObjectTypes(
|
||||||
Schema $oldSchema, Schema $newSchema
|
Schema $oldSchema,
|
||||||
)
|
Schema $newSchema
|
||||||
{
|
) {
|
||||||
$oldTypeMap = $oldSchema->getTypeMap();
|
$oldTypeMap = $oldSchema->getTypeMap();
|
||||||
$newTypeMap = $newSchema->getTypeMap();
|
$newTypeMap = $newSchema->getTypeMap();
|
||||||
|
|
||||||
$breakingChanges = [];
|
$breakingChanges = [];
|
||||||
|
|
||||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||||
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
||||||
if (!($oldType instanceof ObjectType) || !($newType instanceof ObjectType)) {
|
if (!($oldType instanceof ObjectType) || !($newType instanceof ObjectType)) {
|
||||||
@ -554,9 +625,9 @@ class FindBreakingChanges
|
|||||||
if (!Utils::find($newInterfaces, function (InterfaceType $interface) use ($oldInterface) {
|
if (!Utils::find($newInterfaces, function (InterfaceType $interface) use ($oldInterface) {
|
||||||
return $interface->name === $oldInterface->name;
|
return $interface->name === $oldInterface->name;
|
||||||
})) {
|
})) {
|
||||||
$oldInterfaceName = $oldInterface->name;
|
$breakingChanges[] = [
|
||||||
$breakingChanges[] = ['type' => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
|
'type' => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
|
||||||
'description' => "${typeName} no longer implements interface ${oldInterfaceName}."
|
'description' => "${typeName} no longer implements interface {$oldInterface->name}."
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -565,19 +636,170 @@ class FindBreakingChanges
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Type $type
|
* @param Schema $oldSchema
|
||||||
|
* @param Schema $newSchema
|
||||||
*
|
*
|
||||||
* @return bool
|
* @return array
|
||||||
*/
|
*/
|
||||||
private static function isNamedType(Type $type)
|
public static function findInterfacesAddedToObjectTypes(
|
||||||
|
Schema $oldSchema,
|
||||||
|
Schema $newSchema
|
||||||
|
) {
|
||||||
|
$oldTypeMap = $oldSchema->getTypeMap();
|
||||||
|
$newTypeMap = $newSchema->getTypeMap();
|
||||||
|
$interfacesAddedToObjectTypes = [];
|
||||||
|
|
||||||
|
foreach ($newTypeMap as $typeName => $newType) {
|
||||||
|
$oldType = isset($oldTypeMap[$typeName]) ? $oldTypeMap[$typeName] : null;
|
||||||
|
if (!($oldType instanceof ObjectType) || !($newType instanceof ObjectType)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$oldInterfaces = $oldType->getInterfaces();
|
||||||
|
$newInterfaces = $newType->getInterfaces();
|
||||||
|
foreach ($newInterfaces as $newInterface) {
|
||||||
|
if (!Utils::find($oldInterfaces, function (InterfaceType $interface) use ($newInterface) {
|
||||||
|
return $interface->name === $newInterface->name;
|
||||||
|
})) {
|
||||||
|
$interfacesAddedToObjectTypes[] = [
|
||||||
|
'type' => self::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT,
|
||||||
|
'description' => "{$newInterface->name} added to interfaces implemented by {$typeName}.",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $interfacesAddedToObjectTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema)
|
||||||
{
|
{
|
||||||
return (
|
$removedDirectives = [];
|
||||||
$type instanceof ScalarType ||
|
|
||||||
$type instanceof ObjectType ||
|
$newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema);
|
||||||
$type instanceof InterfaceType ||
|
foreach($oldSchema->getDirectives() as $directive) {
|
||||||
$type instanceof UnionType ||
|
if (!isset($newSchemaDirectiveMap[$directive->name])) {
|
||||||
$type instanceof EnumType ||
|
$removedDirectives[] = [
|
||||||
$type instanceof InputObjectType
|
'type' => self::BREAKING_CHANGE_DIRECTIVE_REMOVED,
|
||||||
);
|
'description' => "{$directive->name} was removed",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $removedDirectives;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective)
|
||||||
|
{
|
||||||
|
$removedArgs = [];
|
||||||
|
$newArgMap = self::getArgumentMapForDirective($newDirective);
|
||||||
|
foreach((array) $oldDirective->args as $arg) {
|
||||||
|
if (!isset($newArgMap[$arg->name])) {
|
||||||
|
$removedArgs[] = $arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $removedArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema)
|
||||||
|
{
|
||||||
|
$removedDirectiveArgs = [];
|
||||||
|
$oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
|
||||||
|
|
||||||
|
foreach($newSchema->getDirectives() as $newDirective) {
|
||||||
|
if (!isset($oldSchemaDirectiveMap[$newDirective->name])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(self::findRemovedArgsForDirectives($oldSchemaDirectiveMap[$newDirective->name], $newDirective) as $arg) {
|
||||||
|
$removedDirectiveArgs[] = [
|
||||||
|
'type' => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
|
||||||
|
'description' => "{$arg->name} was removed from {$newDirective->name}",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $removedDirectiveArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective)
|
||||||
|
{
|
||||||
|
$addedArgs = [];
|
||||||
|
$oldArgMap = self::getArgumentMapForDirective($oldDirective);
|
||||||
|
foreach((array) $newDirective->args as $arg) {
|
||||||
|
if (!isset($oldArgMap[$arg->name])) {
|
||||||
|
$addedArgs[] = $arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $addedArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema)
|
||||||
|
{
|
||||||
|
$addedNonNullableArgs = [];
|
||||||
|
$oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
|
||||||
|
|
||||||
|
foreach($newSchema->getDirectives() as $newDirective) {
|
||||||
|
if (!isset($oldSchemaDirectiveMap[$newDirective->name])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(self::findAddedArgsForDirective($oldSchemaDirectiveMap[$newDirective->name], $newDirective) as $arg) {
|
||||||
|
if (!$arg->getType() instanceof NonNull) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$addedNonNullableArgs[] = [
|
||||||
|
'type' => self::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED,
|
||||||
|
'description' => "A non-null arg {$arg->name} on directive {$newDirective->name} was added",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $addedNonNullableArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective)
|
||||||
|
{
|
||||||
|
$removedLocations = [];
|
||||||
|
$newLocationSet = array_flip($newDirective->locations);
|
||||||
|
foreach($oldDirective->locations as $oldLocation) {
|
||||||
|
if (!array_key_exists($oldLocation, $newLocationSet)) {
|
||||||
|
$removedLocations[] = $oldLocation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $removedLocations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema)
|
||||||
|
{
|
||||||
|
$removedLocations = [];
|
||||||
|
$oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
|
||||||
|
|
||||||
|
foreach($newSchema->getDirectives() as $newDirective) {
|
||||||
|
if (!isset($oldSchemaDirectiveMap[$newDirective->name])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(self::findRemovedLocationsForDirective($oldSchemaDirectiveMap[$newDirective->name], $newDirective) as $location) {
|
||||||
|
$removedLocations[] = [
|
||||||
|
'type' => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
|
||||||
|
'description' => "{$location} was removed from {$newDirective->name}",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $removedLocations;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getDirectiveMapForSchema(Schema $schema)
|
||||||
|
{
|
||||||
|
return Utils::keyMap($schema->getDirectives(), function ($dir) { return $dir->name; });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getArgumentMapForDirective(Directive $directive)
|
||||||
|
{
|
||||||
|
return Utils::keyMap($directive->args ?: [], function ($arg) { return $arg->name; });
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,86 +2,65 @@
|
|||||||
namespace GraphQL\Utils;
|
namespace GraphQL\Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class PairSet
|
* A way to keep track of pairs of things when the ordering of the pair does
|
||||||
* @package GraphQL\Utils
|
* not matter. We do this by maintaining a sort of double adjacency sets.
|
||||||
*/
|
*/
|
||||||
class PairSet
|
class PairSet
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var \SplObjectStorage<any, Set<any>>
|
|
||||||
*/
|
|
||||||
private $data;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
private $wrappers = [];
|
private $data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PairSet constructor.
|
* PairSet constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->data = new \SplObjectStorage(); // SplObject hash instead?
|
$this->data = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $a
|
* @param string $a
|
||||||
* @param $b
|
* @param string $b
|
||||||
* @return null|object
|
* @param bool $areMutuallyExclusive
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function has($a, $b)
|
public function has($a, $b, $areMutuallyExclusive)
|
||||||
{
|
{
|
||||||
$a = $this->toObj($a);
|
|
||||||
$b = $this->toObj($b);
|
|
||||||
|
|
||||||
/** @var \SplObjectStorage $first */
|
|
||||||
$first = isset($this->data[$a]) ? $this->data[$a] : null;
|
$first = isset($this->data[$a]) ? $this->data[$a] : null;
|
||||||
return isset($first, $first[$b]) ? $first[$b] : null;
|
$result = ($first && isset($first[$b])) ? $first[$b] : null;
|
||||||
|
if ($result === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// areMutuallyExclusive being false is a superset of being true,
|
||||||
|
// hence if we want to know if this PairSet "has" these two with no
|
||||||
|
// exclusivity, we have to ensure it was added as such.
|
||||||
|
if ($areMutuallyExclusive === false) {
|
||||||
|
return $result === false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $a
|
* @param string $a
|
||||||
* @param $b
|
* @param string $b
|
||||||
|
* @param bool $areMutuallyExclusive
|
||||||
*/
|
*/
|
||||||
public function add($a, $b)
|
public function add($a, $b, $areMutuallyExclusive)
|
||||||
{
|
{
|
||||||
$this->pairSetAdd($a, $b);
|
$this->pairSetAdd($a, $b, $areMutuallyExclusive);
|
||||||
$this->pairSetAdd($b, $a);
|
$this->pairSetAdd($b, $a, $areMutuallyExclusive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $var
|
* @param string $a
|
||||||
* @return mixed
|
* @param string $b
|
||||||
|
* @param bool $areMutuallyExclusive
|
||||||
*/
|
*/
|
||||||
private function toObj($var)
|
private function pairSetAdd($a, $b, $areMutuallyExclusive)
|
||||||
{
|
{
|
||||||
// SplObjectStorage expects objects, so wrapping non-objects to objects
|
$this->data[$a] = isset($this->data[$a]) ? $this->data[$a] : [];
|
||||||
if (is_object($var)) {
|
$this->data[$a][$b] = $areMutuallyExclusive;
|
||||||
return $var;
|
|
||||||
}
|
|
||||||
if (!isset($this->wrappers[$var])) {
|
|
||||||
$tmp = new \stdClass();
|
|
||||||
$tmp->_internal = $var;
|
|
||||||
$this->wrappers[$var] = $tmp;
|
|
||||||
}
|
|
||||||
return $this->wrappers[$var];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $a
|
|
||||||
* @param $b
|
|
||||||
*/
|
|
||||||
private function pairSetAdd($a, $b)
|
|
||||||
{
|
|
||||||
$a = $this->toObj($a);
|
|
||||||
$b = $this->toObj($b);
|
|
||||||
$set = isset($this->data[$a]) ? $this->data[$a] : null;
|
|
||||||
|
|
||||||
if (!isset($set)) {
|
|
||||||
$set = new \SplObjectStorage();
|
|
||||||
$this->data[$a] = $set;
|
|
||||||
}
|
|
||||||
$set[$b] = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Utils;
|
namespace GraphQL\Utils;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Language\Printer;
|
use GraphQL\Language\Printer;
|
||||||
|
use GraphQL\Type\Introspection;
|
||||||
use GraphQL\Type\Schema;
|
use GraphQL\Type\Schema;
|
||||||
use GraphQL\Type\Definition\CompositeType;
|
|
||||||
use GraphQL\Type\Definition\EnumType;
|
use GraphQL\Type\Definition\EnumType;
|
||||||
use GraphQL\Type\Definition\InputObjectType;
|
use GraphQL\Type\Definition\InputObjectType;
|
||||||
use GraphQL\Type\Definition\InterfaceType;
|
use GraphQL\Type\Definition\InterfaceType;
|
||||||
@ -19,15 +20,26 @@ use GraphQL\Type\Definition\Directive;
|
|||||||
class SchemaPrinter
|
class SchemaPrinter
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
* Accepts options as a second argument:
|
||||||
|
*
|
||||||
|
* - commentDescriptions:
|
||||||
|
* Provide true to use preceding comments as the description.
|
||||||
* @api
|
* @api
|
||||||
* @param Schema $schema
|
* @param Schema $schema
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function doPrint(Schema $schema)
|
public static function doPrint(Schema $schema, array $options = [])
|
||||||
{
|
{
|
||||||
return self::printFilteredSchema($schema, function($n) {
|
return self::printFilteredSchema(
|
||||||
return !self::isSpecDirective($n);
|
$schema,
|
||||||
}, 'self::isDefinedType');
|
function($type) {
|
||||||
|
return !Directive::isSpecifiedDirective($type);
|
||||||
|
},
|
||||||
|
function ($type) {
|
||||||
|
return !Type::isBuiltInType($type);
|
||||||
|
},
|
||||||
|
$options
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,55 +47,29 @@ class SchemaPrinter
|
|||||||
* @param Schema $schema
|
* @param Schema $schema
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function printIntrosepctionSchema(Schema $schema)
|
public static function printIntrosepctionSchema(Schema $schema, array $options = [])
|
||||||
{
|
{
|
||||||
return self::printFilteredSchema($schema, [__CLASS__, 'isSpecDirective'], [__CLASS__, 'isIntrospectionType']);
|
return self::printFilteredSchema(
|
||||||
}
|
$schema,
|
||||||
|
[Directive::class, 'isSpecifiedDirective'],
|
||||||
private static function isSpecDirective($directiveName)
|
[Introspection::class, 'isIntrospectionType'],
|
||||||
{
|
$options
|
||||||
return (
|
|
||||||
$directiveName === 'skip' ||
|
|
||||||
$directiveName === 'include' ||
|
|
||||||
$directiveName === 'deprecated'
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function isDefinedType($typename)
|
private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter, $options)
|
||||||
{
|
|
||||||
return !self::isIntrospectionType($typename) && !self::isBuiltInScalar($typename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function isIntrospectionType($typename)
|
|
||||||
{
|
|
||||||
return strpos($typename, '__') === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function isBuiltInScalar($typename)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
$typename === Type::STRING ||
|
|
||||||
$typename === Type::BOOLEAN ||
|
|
||||||
$typename === Type::INT ||
|
|
||||||
$typename === Type::FLOAT ||
|
|
||||||
$typename === Type::ID
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter)
|
|
||||||
{
|
{
|
||||||
$directives = array_filter($schema->getDirectives(), function($directive) use ($directiveFilter) {
|
$directives = array_filter($schema->getDirectives(), function($directive) use ($directiveFilter) {
|
||||||
return $directiveFilter($directive->name);
|
return $directiveFilter($directive);
|
||||||
});
|
});
|
||||||
$typeMap = $schema->getTypeMap();
|
$types = $schema->getTypeMap();
|
||||||
$types = array_filter(array_keys($typeMap), $typeFilter);
|
ksort($types);
|
||||||
sort($types);
|
$types = array_filter($types, $typeFilter);
|
||||||
$types = array_map(function($typeName) use ($typeMap) { return $typeMap[$typeName]; }, $types);
|
|
||||||
|
|
||||||
return implode("\n\n", array_filter(array_merge(
|
return implode("\n\n", array_filter(array_merge(
|
||||||
[self::printSchemaDefinition($schema)],
|
[self::printSchemaDefinition($schema)],
|
||||||
array_map('self::printDirective', $directives),
|
array_map(function($directive) use ($options) { return self::printDirective($directive, $options); }, $directives),
|
||||||
array_map('self::printType', $types)
|
array_map(function($type) use ($options) { return self::printType($type, $options); }, $types)
|
||||||
))) . "\n";
|
))) . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,95 +131,97 @@ class SchemaPrinter
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function printType(Type $type)
|
public static function printType(Type $type, array $options = [])
|
||||||
{
|
{
|
||||||
if ($type instanceof ScalarType) {
|
if ($type instanceof ScalarType) {
|
||||||
return self::printScalar($type);
|
return self::printScalar($type, $options);
|
||||||
} else if ($type instanceof ObjectType) {
|
} else if ($type instanceof ObjectType) {
|
||||||
return self::printObject($type);
|
return self::printObject($type, $options);
|
||||||
} else if ($type instanceof InterfaceType) {
|
} else if ($type instanceof InterfaceType) {
|
||||||
return self::printInterface($type);
|
return self::printInterface($type, $options);
|
||||||
} else if ($type instanceof UnionType) {
|
} else if ($type instanceof UnionType) {
|
||||||
return self::printUnion($type);
|
return self::printUnion($type, $options);
|
||||||
} else if ($type instanceof EnumType) {
|
} else if ($type instanceof EnumType) {
|
||||||
return self::printEnum($type);
|
return self::printEnum($type, $options);
|
||||||
}
|
} else if ($type instanceof InputObjectType) {
|
||||||
Utils::invariant($type instanceof InputObjectType);
|
return self::printInputObject($type, $options);
|
||||||
return self::printInputObject($type);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function printScalar(ScalarType $type)
|
throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function printScalar(ScalarType $type, array $options)
|
||||||
{
|
{
|
||||||
return self::printDescription($type) . "scalar {$type->name}";
|
return self::printDescription($options, $type) . "scalar {$type->name}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function printObject(ObjectType $type)
|
private static function printObject(ObjectType $type, array $options)
|
||||||
{
|
{
|
||||||
$interfaces = $type->getInterfaces();
|
$interfaces = $type->getInterfaces();
|
||||||
$implementedInterfaces = !empty($interfaces) ?
|
$implementedInterfaces = !empty($interfaces) ?
|
||||||
' implements ' . implode(', ', array_map(function($i) {
|
' implements ' . implode(', ', array_map(function($i) {
|
||||||
return $i->name;
|
return $i->name;
|
||||||
}, $interfaces)) : '';
|
}, $interfaces)) : '';
|
||||||
return self::printDescription($type) .
|
return self::printDescription($options, $type) .
|
||||||
"type {$type->name}$implementedInterfaces {\n" .
|
"type {$type->name}$implementedInterfaces {\n" .
|
||||||
self::printFields($type) . "\n" .
|
self::printFields($options, $type) . "\n" .
|
||||||
"}";
|
"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function printInterface(InterfaceType $type)
|
private static function printInterface(InterfaceType $type, array $options)
|
||||||
{
|
{
|
||||||
return self::printDescription($type) .
|
return self::printDescription($options, $type) .
|
||||||
"interface {$type->name} {\n" .
|
"interface {$type->name} {\n" .
|
||||||
self::printFields($type) . "\n" .
|
self::printFields($options, $type) . "\n" .
|
||||||
"}";
|
"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function printUnion(UnionType $type)
|
private static function printUnion(UnionType $type, array $options)
|
||||||
{
|
{
|
||||||
return self::printDescription($type) .
|
return self::printDescription($options, $type) .
|
||||||
"union {$type->name} = " . implode(" | ", $type->getTypes());
|
"union {$type->name} = " . implode(" | ", $type->getTypes());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function printEnum(EnumType $type)
|
private static function printEnum(EnumType $type, array $options)
|
||||||
{
|
{
|
||||||
return self::printDescription($type) .
|
return self::printDescription($options, $type) .
|
||||||
"enum {$type->name} {\n" .
|
"enum {$type->name} {\n" .
|
||||||
self::printEnumValues($type->getValues()) . "\n" .
|
self::printEnumValues($type->getValues(), $options) . "\n" .
|
||||||
"}";
|
"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function printEnumValues($values)
|
private static function printEnumValues($values, $options)
|
||||||
{
|
{
|
||||||
return implode("\n", array_map(function($value, $i) {
|
return implode("\n", array_map(function($value, $i) use ($options) {
|
||||||
return self::printDescription($value, ' ', !$i) . ' ' .
|
return self::printDescription($options, $value, ' ', !$i) . ' ' .
|
||||||
$value->name . self::printDeprecated($value);
|
$value->name . self::printDeprecated($value);
|
||||||
}, $values, array_keys($values)));
|
}, $values, array_keys($values)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function printInputObject(InputObjectType $type)
|
private static function printInputObject(InputObjectType $type, array $options)
|
||||||
{
|
{
|
||||||
$fields = array_values($type->getFields());
|
$fields = array_values($type->getFields());
|
||||||
return self::printDescription($type) .
|
return self::printDescription($options, $type) .
|
||||||
"input {$type->name} {\n" .
|
"input {$type->name} {\n" .
|
||||||
implode("\n", array_map(function($f, $i) {
|
implode("\n", array_map(function($f, $i) use ($options) {
|
||||||
return self::printDescription($f, ' ', !$i) . ' ' . self::printInputValue($f);
|
return self::printDescription($options, $f, ' ', !$i) . ' ' . self::printInputValue($f);
|
||||||
}, $fields, array_keys($fields))) . "\n" .
|
}, $fields, array_keys($fields))) . "\n" .
|
||||||
"}";
|
"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function printFields($type)
|
private static function printFields($options, $type)
|
||||||
{
|
{
|
||||||
$fields = array_values($type->getFields());
|
$fields = array_values($type->getFields());
|
||||||
return implode("\n", array_map(function($f, $i) {
|
return implode("\n", array_map(function($f, $i) use ($options) {
|
||||||
return self::printDescription($f, ' ', !$i) . ' ' .
|
return self::printDescription($options, $f, ' ', !$i) . ' ' .
|
||||||
$f->name . self::printArgs($f->args, ' ') . ': ' .
|
$f->name . self::printArgs($options, $f->args, ' ') . ': ' .
|
||||||
(string) $f->getType() . self::printDeprecated($f);
|
(string) $f->getType() . self::printDeprecated($f);
|
||||||
}, $fields, array_keys($fields)));
|
}, $fields, array_keys($fields)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function printArgs($args, $indentation = '')
|
private static function printArgs($options, $args, $indentation = '')
|
||||||
{
|
{
|
||||||
if (count($args) === 0) {
|
if (!$args) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,8 +230,8 @@ class SchemaPrinter
|
|||||||
return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')';
|
return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
return "(\n" . implode("\n", array_map(function($arg, $i) use ($indentation) {
|
return "(\n" . implode("\n", array_map(function($arg, $i) use ($indentation, $options) {
|
||||||
return self::printDescription($arg, ' ' . $indentation, !$i) . ' ' . $indentation .
|
return self::printDescription($options, $arg, ' ' . $indentation, !$i) . ' ' . $indentation .
|
||||||
self::printInputValue($arg);
|
self::printInputValue($arg);
|
||||||
}, $args, array_keys($args))) . "\n" . $indentation . ')';
|
}, $args, array_keys($args))) . "\n" . $indentation . ')';
|
||||||
}
|
}
|
||||||
@ -257,10 +245,10 @@ class SchemaPrinter
|
|||||||
return $argDecl;
|
return $argDecl;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function printDirective($directive)
|
private static function printDirective($directive, $options)
|
||||||
{
|
{
|
||||||
return self::printDescription($directive) .
|
return self::printDescription($options, $directive) .
|
||||||
'directive @' . $directive->name . self::printArgs($directive->args) .
|
'directive @' . $directive->name . self::printArgs($options, $directive->args) .
|
||||||
' on ' . implode(' | ', $directive->locations);
|
' on ' . implode(' | ', $directive->locations);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,34 +265,74 @@ class SchemaPrinter
|
|||||||
Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')';
|
Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function printDescription($def, $indentation = '', $firstInBlock = true)
|
private static function printDescription($options, $def, $indentation = '', $firstInBlock = true)
|
||||||
{
|
{
|
||||||
if (!$def->description) {
|
if (!$def->description) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
$lines = explode("\n", $def->description);
|
$lines = self::descriptionLines($def->description, 120 - strlen($indentation));
|
||||||
|
if (isset($options['commentDescriptions'])) {
|
||||||
|
return self::printDescriptionWithComments($lines, $indentation, $firstInBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
$description = ($indentation && !$firstInBlock) ? "\n" : '';
|
||||||
|
if (count($lines) === 1 && mb_strlen($lines[0]) < 70) {
|
||||||
|
$description .= $indentation . '"""' . self::escapeQuote($lines[0]) . "\"\"\"\n";
|
||||||
|
return $description;
|
||||||
|
}
|
||||||
|
|
||||||
|
$description .= $indentation . "\"\"\"\n";
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$description .= $indentation . self::escapeQuote($line) . "\n";
|
||||||
|
}
|
||||||
|
$description .= $indentation . "\"\"\"\n";
|
||||||
|
|
||||||
|
return $description;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function escapeQuote($line)
|
||||||
|
{
|
||||||
|
return str_replace('"""', '\\"""', $line);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function printDescriptionWithComments($lines, $indentation, $firstInBlock)
|
||||||
|
{
|
||||||
$description = $indentation && !$firstInBlock ? "\n" : '';
|
$description = $indentation && !$firstInBlock ? "\n" : '';
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
if ($line === '') {
|
if ($line === '') {
|
||||||
$description .= $indentation . "#\n";
|
$description .= $indentation . "#\n";
|
||||||
} else {
|
} else {
|
||||||
// For > 120 character long lines, cut at space boundaries into sublines
|
$description .= $indentation . '# ' . $line . "\n";
|
||||||
// of ~80 chars.
|
|
||||||
$sublines = self::breakLine($line, 120 - strlen($indentation));
|
|
||||||
foreach ($sublines as $subline) {
|
|
||||||
$description .= $indentation . '# ' . $subline . "\n";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $description;
|
return $description;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function breakLine($line, $len)
|
private static function descriptionLines($description, $maxLen) {
|
||||||
|
$lines = [];
|
||||||
|
$rawLines = explode("\n", $description);
|
||||||
|
foreach($rawLines as $line) {
|
||||||
|
if ($line === '') {
|
||||||
|
$lines[] = $line;
|
||||||
|
} else {
|
||||||
|
// For > 120 character long lines, cut at space boundaries into sublines
|
||||||
|
// of ~80 chars.
|
||||||
|
$sublines = self::breakLine($line, $maxLen);
|
||||||
|
foreach ($sublines as $subline) {
|
||||||
|
$lines[] = $subline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function breakLine($line, $maxLen)
|
||||||
{
|
{
|
||||||
if (strlen($line) < $len + 5) {
|
if (strlen($line) < $maxLen + 5) {
|
||||||
return [$line];
|
return [$line];
|
||||||
}
|
}
|
||||||
preg_match_all("/((?: |^).{15," . ($len - 40) . "}(?= |$))/", $line, $parts);
|
preg_match_all("/((?: |^).{15," . ($maxLen - 40) . "}(?= |$))/", $line, $parts);
|
||||||
$parts = $parts[0];
|
$parts = $parts[0];
|
||||||
return array_map(function($part) {
|
return array_map(function($part) {
|
||||||
return trim($part);
|
return trim($part);
|
||||||
|
@ -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) {
|
||||||
|
@ -19,7 +19,6 @@ use GraphQL\Type\Definition\InputObjectType;
|
|||||||
use GraphQL\Type\Definition\InputType;
|
use GraphQL\Type\Definition\InputType;
|
||||||
use GraphQL\Type\Definition\InterfaceType;
|
use GraphQL\Type\Definition\InterfaceType;
|
||||||
use GraphQL\Type\Definition\ListOfType;
|
use GraphQL\Type\Definition\ListOfType;
|
||||||
use GraphQL\Type\Definition\NonNull;
|
|
||||||
use GraphQL\Type\Definition\ObjectType;
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
use GraphQL\Type\Definition\UnionType;
|
use GraphQL\Type\Definition\UnionType;
|
||||||
@ -121,7 +120,7 @@ class TypeInfo
|
|||||||
if ($type instanceof ObjectType) {
|
if ($type instanceof ObjectType) {
|
||||||
$nestedTypes = array_merge($nestedTypes, $type->getInterfaces());
|
$nestedTypes = array_merge($nestedTypes, $type->getInterfaces());
|
||||||
}
|
}
|
||||||
if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) {
|
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
|
||||||
foreach ((array) $type->getFields() as $fieldName => $field) {
|
foreach ((array) $type->getFields() as $fieldName => $field) {
|
||||||
if (!empty($field->args)) {
|
if (!empty($field->args)) {
|
||||||
$fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args);
|
$fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args);
|
||||||
@ -130,6 +129,11 @@ class TypeInfo
|
|||||||
$nestedTypes[] = $field->getType();
|
$nestedTypes[] = $field->getType();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($type instanceof InputObjectType) {
|
||||||
|
foreach ((array) $type->getFields() as $fieldName => $field) {
|
||||||
|
$nestedTypes[] = $field->getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
foreach ($nestedTypes as $type) {
|
foreach ($nestedTypes as $type) {
|
||||||
$typeMap = self::extractTypes($type, $typeMap);
|
$typeMap = self::extractTypes($type, $typeMap);
|
||||||
}
|
}
|
||||||
@ -211,14 +215,26 @@ class TypeInfo
|
|||||||
/**
|
/**
|
||||||
* TypeInfo constructor.
|
* TypeInfo constructor.
|
||||||
* @param Schema $schema
|
* @param Schema $schema
|
||||||
|
* @param Type|null $initialType
|
||||||
*/
|
*/
|
||||||
public function __construct(Schema $schema)
|
public function __construct(Schema $schema, $initialType = null)
|
||||||
{
|
{
|
||||||
$this->schema = $schema;
|
$this->schema = $schema;
|
||||||
$this->typeStack = [];
|
$this->typeStack = [];
|
||||||
$this->parentTypeStack = [];
|
$this->parentTypeStack = [];
|
||||||
$this->inputTypeStack = [];
|
$this->inputTypeStack = [];
|
||||||
$this->fieldDefStack = [];
|
$this->fieldDefStack = [];
|
||||||
|
if ($initialType) {
|
||||||
|
if (Type::isInputType($initialType)) {
|
||||||
|
$this->inputTypeStack[] = $initialType;
|
||||||
|
}
|
||||||
|
if (Type::isCompositeType($initialType)) {
|
||||||
|
$this->parentTypeStack[] = $initialType;
|
||||||
|
}
|
||||||
|
if (Type::isOutputType($initialType)) {
|
||||||
|
$this->typeStack[] = $initialType;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -233,7 +249,7 @@ class TypeInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Type
|
* @return CompositeType
|
||||||
*/
|
*/
|
||||||
function getParentType()
|
function getParentType()
|
||||||
{
|
{
|
||||||
@ -254,6 +270,17 @@ class TypeInfo
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return InputType|null
|
||||||
|
*/
|
||||||
|
public function getParentInputType()
|
||||||
|
{
|
||||||
|
$inputTypeStackLength = count($this->inputTypeStack);
|
||||||
|
if ($inputTypeStackLength > 1) {
|
||||||
|
return $this->inputTypeStack[$inputTypeStackLength - 2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return FieldDefinition
|
* @return FieldDefinition
|
||||||
*/
|
*/
|
||||||
@ -296,6 +323,10 @@ class TypeInfo
|
|||||||
{
|
{
|
||||||
$schema = $this->schema;
|
$schema = $this->schema;
|
||||||
|
|
||||||
|
// Note: many of the types below are explicitly typed as "mixed" to drop
|
||||||
|
// any assumptions of a valid schema to ensure runtime types are properly
|
||||||
|
// checked before continuing since TypeInfo is used as part of validation
|
||||||
|
// which occurs before guarantees of schema and document validity.
|
||||||
switch ($node->kind) {
|
switch ($node->kind) {
|
||||||
case NodeKind::SELECTION_SET:
|
case NodeKind::SELECTION_SET:
|
||||||
$namedType = Type::getNamedType($this->getType());
|
$namedType = Type::getNamedType($this->getType());
|
||||||
@ -308,8 +339,12 @@ class TypeInfo
|
|||||||
if ($parentType) {
|
if ($parentType) {
|
||||||
$fieldDef = self::getFieldDefinition($schema, $parentType, $node);
|
$fieldDef = self::getFieldDefinition($schema, $parentType, $node);
|
||||||
}
|
}
|
||||||
$this->fieldDefStack[] = $fieldDef; // push
|
$fieldType = null;
|
||||||
$this->typeStack[] = $fieldDef ? $fieldDef->getType() : null; // push
|
if ($fieldDef) {
|
||||||
|
$fieldType = $fieldDef->getType();
|
||||||
|
}
|
||||||
|
$this->fieldDefStack[] = $fieldDef;
|
||||||
|
$this->typeStack[] = Type::isOutputType($fieldType) ? $fieldType : null;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NodeKind::DIRECTIVE:
|
case NodeKind::DIRECTIVE:
|
||||||
@ -325,14 +360,14 @@ class TypeInfo
|
|||||||
} else if ($node->operation === 'subscription') {
|
} else if ($node->operation === 'subscription') {
|
||||||
$type = $schema->getSubscriptionType();
|
$type = $schema->getSubscriptionType();
|
||||||
}
|
}
|
||||||
$this->typeStack[] = $type; // push
|
$this->typeStack[] = Type::isOutputType($type) ? $type : null;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NodeKind::INLINE_FRAGMENT:
|
case NodeKind::INLINE_FRAGMENT:
|
||||||
case NodeKind::FRAGMENT_DEFINITION:
|
case NodeKind::FRAGMENT_DEFINITION:
|
||||||
$typeConditionNode = $node->typeCondition;
|
$typeConditionNode = $node->typeCondition;
|
||||||
$outputType = $typeConditionNode ? self::typeFromAST($schema, $typeConditionNode) : $this->getType();
|
$outputType = $typeConditionNode ? self::typeFromAST($schema, $typeConditionNode) : Type::getNamedType($this->getType());
|
||||||
$this->typeStack[] = Type::isOutputType($outputType) ? $outputType : null; // push
|
$this->typeStack[] = Type::isOutputType($outputType) ? $outputType : null;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NodeKind::VARIABLE_DEFINITION:
|
case NodeKind::VARIABLE_DEFINITION:
|
||||||
@ -350,23 +385,27 @@ class TypeInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this->argument = $argDef;
|
$this->argument = $argDef;
|
||||||
$this->inputTypeStack[] = $argType; // push
|
$this->inputTypeStack[] = Type::isInputType($argType) ? $argType : null;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NodeKind::LST:
|
case NodeKind::LST:
|
||||||
$listType = Type::getNullableType($this->getInputType());
|
$listType = Type::getNullableType($this->getInputType());
|
||||||
$this->inputTypeStack[] = ($listType instanceof ListOfType ? $listType->getWrappedType() : null); // push
|
$itemType = $listType instanceof ListOfType
|
||||||
|
? $listType->getWrappedType()
|
||||||
|
: $listType;
|
||||||
|
$this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NodeKind::OBJECT_FIELD:
|
case NodeKind::OBJECT_FIELD:
|
||||||
$objectType = Type::getNamedType($this->getInputType());
|
$objectType = Type::getNamedType($this->getInputType());
|
||||||
$fieldType = null;
|
$fieldType = null;
|
||||||
|
$inputFieldType = null;
|
||||||
if ($objectType instanceof InputObjectType) {
|
if ($objectType instanceof InputObjectType) {
|
||||||
$tmp = $objectType->getFields();
|
$tmp = $objectType->getFields();
|
||||||
$inputField = isset($tmp[$node->name->value]) ? $tmp[$node->name->value] : null;
|
$inputField = isset($tmp[$node->name->value]) ? $tmp[$node->name->value] : null;
|
||||||
$fieldType = $inputField ? $inputField->getType() : null;
|
$inputFieldType = $inputField ? $inputField->getType() : null;
|
||||||
}
|
}
|
||||||
$this->inputTypeStack[] = $fieldType;
|
$this->inputTypeStack[] = Type::isInputType($inputFieldType) ? $inputFieldType : null;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NodeKind::ENUM:
|
case NodeKind::ENUM:
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Utils;
|
namespace GraphQL\Utils;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Error\InvariantViolation;
|
use GraphQL\Error\InvariantViolation;
|
||||||
use GraphQL\Error\Warning;
|
use GraphQL\Error\Warning;
|
||||||
|
use GraphQL\Language\AST\Node;
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
use GraphQL\Type\Definition\WrappingType;
|
use GraphQL\Type\Definition\WrappingType;
|
||||||
use \Traversable, \InvalidArgumentException;
|
use \Traversable, \InvalidArgumentException;
|
||||||
@ -15,12 +17,23 @@ class Utils
|
|||||||
return $undefined ?: $undefined = new \stdClass();
|
return $undefined ?: $undefined = new \stdClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the value is invalid
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isInvalid($value)
|
||||||
|
{
|
||||||
|
return self::undefined() === $value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param object $obj
|
* @param object $obj
|
||||||
* @param array $vars
|
* @param array $vars
|
||||||
* @param array $requiredKeys
|
* @param array $requiredKeys
|
||||||
*
|
*
|
||||||
* @return array
|
* @return object
|
||||||
*/
|
*/
|
||||||
public static function assign($obj, array $vars, array $requiredKeys = [])
|
public static function assign($obj, array $vars, array $requiredKeys = [])
|
||||||
{
|
{
|
||||||
@ -218,7 +231,7 @@ class Utils
|
|||||||
* @param string $message
|
* @param string $message
|
||||||
* @param mixed $sprintfParam1
|
* @param mixed $sprintfParam1
|
||||||
* @param mixed $sprintfParam2 ...
|
* @param mixed $sprintfParam2 ...
|
||||||
* @throws InvariantViolation
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public static function invariant($test, $message = '')
|
public static function invariant($test, $message = '')
|
||||||
{
|
{
|
||||||
@ -228,6 +241,7 @@ class Utils
|
|||||||
array_shift($args);
|
array_shift($args);
|
||||||
$message = call_user_func_array('sprintf', $args);
|
$message = call_user_func_array('sprintf', $args);
|
||||||
}
|
}
|
||||||
|
// TODO switch to Error here
|
||||||
throw new InvariantViolation($message);
|
throw new InvariantViolation($message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -258,22 +272,7 @@ class Utils
|
|||||||
$var = (array) $var;
|
$var = (array) $var;
|
||||||
}
|
}
|
||||||
if (is_array($var)) {
|
if (is_array($var)) {
|
||||||
$count = count($var);
|
return json_encode($var);
|
||||||
if (!isset($var[0]) && $count > 0) {
|
|
||||||
$keys = [];
|
|
||||||
$keyCount = 0;
|
|
||||||
foreach ($var as $key => $value) {
|
|
||||||
$keys[] = '"' . $key . '"';
|
|
||||||
if ($keyCount++ > 4) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$keysLabel = $keyCount === 1 ? 'key' : 'keys';
|
|
||||||
$msg = "object with first $keysLabel: " . implode(', ', $keys);
|
|
||||||
} else {
|
|
||||||
$msg = "array($count)";
|
|
||||||
}
|
|
||||||
return $msg;
|
|
||||||
}
|
}
|
||||||
if ('' === $var) {
|
if ('' === $var) {
|
||||||
return '(empty string)';
|
return '(empty string)';
|
||||||
@ -285,7 +284,7 @@ class Utils
|
|||||||
return 'false';
|
return 'false';
|
||||||
}
|
}
|
||||||
if (true === $var) {
|
if (true === $var) {
|
||||||
return 'false';
|
return 'true';
|
||||||
}
|
}
|
||||||
if (is_string($var)) {
|
if (is_string($var)) {
|
||||||
return "\"$var\"";
|
return "\"$var\"";
|
||||||
@ -306,25 +305,14 @@ class Utils
|
|||||||
return $var->toString();
|
return $var->toString();
|
||||||
}
|
}
|
||||||
if (is_object($var)) {
|
if (is_object($var)) {
|
||||||
|
if (method_exists($var, '__toString')) {
|
||||||
|
return (string) $var;
|
||||||
|
} else {
|
||||||
return 'instance of ' . get_class($var);
|
return 'instance of ' . get_class($var);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (is_array($var)) {
|
if (is_array($var)) {
|
||||||
$count = count($var);
|
return json_encode($var);
|
||||||
if (!isset($var[0]) && $count > 0) {
|
|
||||||
$keys = [];
|
|
||||||
$keyCount = 0;
|
|
||||||
foreach ($var as $key => $value) {
|
|
||||||
$keys[] = '"' . $key . '"';
|
|
||||||
if ($keyCount++ > 4) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$keysLabel = $keyCount === 1 ? 'key' : 'keys';
|
|
||||||
$msg = "associative array($count) with first $keysLabel: " . implode(', ', $keys);
|
|
||||||
} else {
|
|
||||||
$msg = "array($count)";
|
|
||||||
}
|
|
||||||
return $msg;
|
|
||||||
}
|
}
|
||||||
if ('' === $var) {
|
if ('' === $var) {
|
||||||
return '(empty string)';
|
return '(empty string)';
|
||||||
@ -339,7 +327,7 @@ class Utils
|
|||||||
return 'true';
|
return 'true';
|
||||||
}
|
}
|
||||||
if (is_string($var)) {
|
if (is_string($var)) {
|
||||||
return "\"$var\"";
|
return $var;
|
||||||
}
|
}
|
||||||
if (is_scalar($var)) {
|
if (is_scalar($var)) {
|
||||||
return (string) $var;
|
return (string) $var;
|
||||||
@ -418,34 +406,46 @@ class Utils
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Upholds the spec rules about naming.
|
||||||
|
*
|
||||||
* @param $name
|
* @param $name
|
||||||
* @param bool $isIntrospection
|
* @throws Error
|
||||||
* @throws InvariantViolation
|
|
||||||
*/
|
*/
|
||||||
public static function assertValidName($name, $isIntrospection = false)
|
public static function assertValidName($name)
|
||||||
{
|
{
|
||||||
$regex = '/^[_a-zA-Z][_a-zA-Z0-9]*$/';
|
$error = self::isValidNameError($name);
|
||||||
|
if ($error) {
|
||||||
|
throw $error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!$name || !is_string($name)) {
|
/**
|
||||||
throw new InvariantViolation(
|
* Returns an Error if a name is invalid.
|
||||||
"Must be named. Unexpected name: " . self::printSafe($name)
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @param Node|null $node
|
||||||
|
* @return Error|null
|
||||||
|
*/
|
||||||
|
public static function isValidNameError($name, $node = null)
|
||||||
|
{
|
||||||
|
Utils::invariant(is_string($name), 'Expected string');
|
||||||
|
|
||||||
|
if (isset($name[1]) && $name[0] === '_' && $name[1] === '_') {
|
||||||
|
return new Error(
|
||||||
|
"Name \"{$name}\" must not begin with \"__\", which is reserved by " .
|
||||||
|
"GraphQL introspection.",
|
||||||
|
$node
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$isIntrospection && isset($name[1]) && $name[0] === '_' && $name[1] === '_') {
|
if (!preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name)) {
|
||||||
Warning::warnOnce(
|
return new Error(
|
||||||
'Name "'.$name.'" must not begin with "__", which is reserved by ' .
|
"Names must match /^[_a-zA-Z][_a-zA-Z0-9]*\$/ but \"{$name}\" does not.",
|
||||||
'GraphQL introspection. In a future release of graphql this will ' .
|
$node
|
||||||
'become an exception',
|
|
||||||
Warning::WARNING_NAME
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preg_match($regex, $name)) {
|
return null;
|
||||||
throw new InvariantViolation(
|
|
||||||
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "'.$name.'" does not.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -471,4 +471,72 @@ class Utils
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $items
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function quotedOrList(array $items)
|
||||||
|
{
|
||||||
|
$items = array_map(function($item) { return "\"$item\""; }, $items);
|
||||||
|
return self::orList($items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function orList(array $items)
|
||||||
|
{
|
||||||
|
if (!$items) {
|
||||||
|
throw new \LogicException('items must not need to be empty.');
|
||||||
|
}
|
||||||
|
$selected = array_slice($items, 0, 5);
|
||||||
|
$selectedLength = count($selected);
|
||||||
|
$firstSelected = $selected[0];
|
||||||
|
|
||||||
|
if ($selectedLength === 1) {
|
||||||
|
return $firstSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_reduce(
|
||||||
|
range(1, $selectedLength - 1),
|
||||||
|
function ($list, $index) use ($selected, $selectedLength) {
|
||||||
|
return $list.
|
||||||
|
($selectedLength > 2 ? ', ' : ' ') .
|
||||||
|
($index === $selectedLength - 1 ? 'or ' : '') .
|
||||||
|
$selected[$index];
|
||||||
|
},
|
||||||
|
$firstSelected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an invalid input string and a list of valid options, returns a filtered
|
||||||
|
* list of valid options sorted based on their similarity with the input.
|
||||||
|
*
|
||||||
|
* Includes a custom alteration from Damerau-Levenshtein to treat case changes
|
||||||
|
* as a single edit which helps identify mis-cased values with an edit distance
|
||||||
|
* of 1
|
||||||
|
* @param string $input
|
||||||
|
* @param array $options
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function suggestionList($input, array $options)
|
||||||
|
{
|
||||||
|
$optionsByDistance = [];
|
||||||
|
$inputThreshold = mb_strlen($input) / 2;
|
||||||
|
foreach ($options as $option) {
|
||||||
|
$distance = $input === $option
|
||||||
|
? 0
|
||||||
|
: (strtolower($input) === strtolower($option)
|
||||||
|
? 1
|
||||||
|
: levenshtein($input, $option));
|
||||||
|
$threshold = max($inputThreshold, mb_strlen($option) / 2, 1);
|
||||||
|
if ($distance <= $threshold) {
|
||||||
|
$optionsByDistance[$option] = $distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
asort($optionsByDistance);
|
||||||
|
|
||||||
|
return array_keys($optionsByDistance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
261
src/Utils/Value.php
Normal file
261
src/Utils/Value.php
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Utils;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
|
use GraphQL\Language\AST\Node;
|
||||||
|
use GraphQL\Type\Definition\EnumType;
|
||||||
|
use GraphQL\Type\Definition\InputObjectType;
|
||||||
|
use GraphQL\Type\Definition\InputType;
|
||||||
|
use GraphQL\Type\Definition\ListOfType;
|
||||||
|
use GraphQL\Type\Definition\NonNull;
|
||||||
|
use GraphQL\Type\Definition\ScalarType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerces a PHP value given a GraphQL Type.
|
||||||
|
*
|
||||||
|
* Returns either a value which is valid for the provided type or a list of
|
||||||
|
* encountered coercion errors.
|
||||||
|
*/
|
||||||
|
class Value
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Given a type and any value, return a runtime value coerced to match the type.
|
||||||
|
*/
|
||||||
|
public static function coerceValue($value, InputType $type, $blameNode = null, array $path = null)
|
||||||
|
{
|
||||||
|
if ($type instanceof NonNull) {
|
||||||
|
if ($value === null) {
|
||||||
|
return self::ofErrors([
|
||||||
|
self::coercionError(
|
||||||
|
"Expected non-nullable type $type not to be null",
|
||||||
|
$blameNode,
|
||||||
|
$path
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return self::coerceValue($value, $type->getWrappedType(), $blameNode, $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value) {
|
||||||
|
// Explicitly return the value null.
|
||||||
|
return self::ofValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type instanceof ScalarType) {
|
||||||
|
// Scalars determine if a value is valid via parseValue(), which can
|
||||||
|
// throw to indicate failure. If it throws, maintain a reference to
|
||||||
|
// the original error.
|
||||||
|
try {
|
||||||
|
$parseResult = $type->parseValue($value);
|
||||||
|
if (Utils::isInvalid($parseResult)) {
|
||||||
|
return self::ofErrors([
|
||||||
|
self::coercionError("Expected type {$type->name}", $blameNode, $path),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::ofValue($parseResult);
|
||||||
|
} catch (\Exception $error) {
|
||||||
|
return self::ofErrors([
|
||||||
|
self::coercionError(
|
||||||
|
"Expected type {$type->name}",
|
||||||
|
$blameNode,
|
||||||
|
$path,
|
||||||
|
$error->getMessage(),
|
||||||
|
$error
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $error) {
|
||||||
|
return self::ofErrors([
|
||||||
|
self::coercionError(
|
||||||
|
"Expected type {$type->name}",
|
||||||
|
$blameNode,
|
||||||
|
$path,
|
||||||
|
$error->getMessage(),
|
||||||
|
$error
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type instanceof EnumType) {
|
||||||
|
if (is_string($value)) {
|
||||||
|
$enumValue = $type->getValue($value);
|
||||||
|
if ($enumValue) {
|
||||||
|
return self::ofValue($enumValue->value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$suggestions = Utils::suggestionList(
|
||||||
|
Utils::printSafe($value),
|
||||||
|
array_map(function($enumValue) { return $enumValue->name; }, $type->getValues())
|
||||||
|
);
|
||||||
|
$didYouMean = $suggestions
|
||||||
|
? "did you mean " . Utils::orList($suggestions) . "?"
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return self::ofErrors([
|
||||||
|
self::coercionError(
|
||||||
|
"Expected type {$type->name}",
|
||||||
|
$blameNode,
|
||||||
|
$path,
|
||||||
|
$didYouMean
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type instanceof ListOfType) {
|
||||||
|
$itemType = $type->getWrappedType();
|
||||||
|
if (is_array($value) || $value instanceof \Traversable) {
|
||||||
|
$errors = [];
|
||||||
|
$coercedValue = [];
|
||||||
|
foreach ($value as $index => $itemValue) {
|
||||||
|
$coercedItem = self::coerceValue(
|
||||||
|
$itemValue,
|
||||||
|
$itemType,
|
||||||
|
$blameNode,
|
||||||
|
self::atPath($path, $index)
|
||||||
|
);
|
||||||
|
if ($coercedItem['errors']) {
|
||||||
|
$errors = self::add($errors, $coercedItem['errors']);
|
||||||
|
} else {
|
||||||
|
$coercedValue[] = $coercedItem['value'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
|
||||||
|
}
|
||||||
|
// Lists accept a non-list value as a list of one.
|
||||||
|
$coercedItem = self::coerceValue($value, $itemType, $blameNode);
|
||||||
|
return $coercedItem['errors'] ? $coercedItem : self::ofValue([$coercedItem['value']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type instanceof InputObjectType) {
|
||||||
|
if (!is_object($value) && !is_array($value) && !$value instanceof \Traversable) {
|
||||||
|
return self::ofErrors([
|
||||||
|
self::coercionError(
|
||||||
|
"Expected type {$type->name} to be an object",
|
||||||
|
$blameNode,
|
||||||
|
$path
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$coercedValue = [];
|
||||||
|
$fields = $type->getFields();
|
||||||
|
foreach ($fields as $fieldName => $field) {
|
||||||
|
if (!array_key_exists($fieldName, $value)) {
|
||||||
|
if ($field->defaultValueExists()) {
|
||||||
|
$coercedValue[$fieldName] = $field->defaultValue;
|
||||||
|
} else if ($field->getType() instanceof NonNull) {
|
||||||
|
$fieldPath = self::printPath(self::atPath($path, $fieldName));
|
||||||
|
$errors = self::add(
|
||||||
|
$errors,
|
||||||
|
self::coercionError(
|
||||||
|
"Field {$fieldPath} of required " .
|
||||||
|
"type {$field->type} was not provided",
|
||||||
|
$blameNode
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$fieldValue = $value[$fieldName];
|
||||||
|
$coercedField = self::coerceValue(
|
||||||
|
$fieldValue,
|
||||||
|
$field->getType(),
|
||||||
|
$blameNode,
|
||||||
|
self::atPath($path, $fieldName)
|
||||||
|
);
|
||||||
|
if ($coercedField['errors']) {
|
||||||
|
$errors = self::add($errors, $coercedField['errors']);
|
||||||
|
} else {
|
||||||
|
$coercedValue[$fieldName] = $coercedField['value'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure every provided field is defined.
|
||||||
|
foreach ($value as $fieldName => $field) {
|
||||||
|
if (!array_key_exists($fieldName, $fields)) {
|
||||||
|
$suggestions = Utils::suggestionList(
|
||||||
|
$fieldName,
|
||||||
|
array_keys($fields)
|
||||||
|
);
|
||||||
|
$didYouMean = $suggestions
|
||||||
|
? "did you mean " . Utils::orList($suggestions) . "?"
|
||||||
|
: null;
|
||||||
|
$errors = self::add(
|
||||||
|
$errors,
|
||||||
|
self::coercionError(
|
||||||
|
"Field \"{$fieldName}\" is not defined by type {$type->name}",
|
||||||
|
$blameNode,
|
||||||
|
$path,
|
||||||
|
$didYouMean
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unexpected type {$type}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function ofValue($value) {
|
||||||
|
return ['errors' => null, 'value' => $value];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function ofErrors($errors) {
|
||||||
|
return ['errors' => $errors, 'value' => Utils::undefined()];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function add($errors, $moreErrors) {
|
||||||
|
return array_merge($errors, is_array($moreErrors) ? $moreErrors : [$moreErrors]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function atPath($prev, $key) {
|
||||||
|
return ['prev' => $prev, 'key' => $key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $message
|
||||||
|
* @param Node $blameNode
|
||||||
|
* @param array|null $path
|
||||||
|
* @param string $subMessage
|
||||||
|
* @param \Exception|\Throwable|null $originalError
|
||||||
|
* @return Error
|
||||||
|
*/
|
||||||
|
private static function coercionError($message, $blameNode, array $path = null, $subMessage = null, $originalError = null) {
|
||||||
|
$pathStr = self::printPath($path);
|
||||||
|
// Return a GraphQLError instance
|
||||||
|
return new Error(
|
||||||
|
$message .
|
||||||
|
($pathStr ? ' at ' . $pathStr : '') .
|
||||||
|
($subMessage ? '; ' . $subMessage : '.'),
|
||||||
|
$blameNode,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$originalError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a string describing the path into the value where the error was found
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function printPath(array $path = null) {
|
||||||
|
$pathStr = '';
|
||||||
|
$currentPath = $path;
|
||||||
|
while($currentPath) {
|
||||||
|
$pathStr =
|
||||||
|
(is_string($currentPath['key'])
|
||||||
|
? '.' . $currentPath['key']
|
||||||
|
: '[' . $currentPath['key'] . ']') . $pathStr;
|
||||||
|
$currentPath = $currentPath['prev'];
|
||||||
|
}
|
||||||
|
return $pathStr ? 'value' . $pathStr : '';
|
||||||
|
}
|
||||||
|
}
|
@ -2,26 +2,15 @@
|
|||||||
namespace GraphQL\Validator;
|
namespace GraphQL\Validator;
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Error\InvariantViolation;
|
|
||||||
use GraphQL\Language\AST\ListValueNode;
|
|
||||||
use GraphQL\Language\AST\DocumentNode;
|
use GraphQL\Language\AST\DocumentNode;
|
||||||
use GraphQL\Language\AST\NodeKind;
|
|
||||||
use GraphQL\Language\AST\NullValueNode;
|
|
||||||
use GraphQL\Language\AST\VariableNode;
|
|
||||||
use GraphQL\Language\Printer;
|
|
||||||
use GraphQL\Language\Visitor;
|
use GraphQL\Language\Visitor;
|
||||||
use GraphQL\Type\Schema;
|
use GraphQL\Type\Schema;
|
||||||
use GraphQL\Type\Definition\InputObjectType;
|
|
||||||
use GraphQL\Type\Definition\LeafType;
|
|
||||||
use GraphQL\Type\Definition\ListOfType;
|
|
||||||
use GraphQL\Type\Definition\NonNull;
|
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
use GraphQL\Utils\Utils;
|
|
||||||
use GraphQL\Utils\TypeInfo;
|
use GraphQL\Utils\TypeInfo;
|
||||||
use GraphQL\Validator\Rules\AbstractValidationRule;
|
use GraphQL\Validator\Rules\AbstractValidationRule;
|
||||||
use GraphQL\Validator\Rules\ArgumentsOfCorrectType;
|
use GraphQL\Validator\Rules\ValuesOfCorrectType;
|
||||||
use GraphQL\Validator\Rules\DefaultValuesOfCorrectType;
|
|
||||||
use GraphQL\Validator\Rules\DisableIntrospection;
|
use GraphQL\Validator\Rules\DisableIntrospection;
|
||||||
|
use GraphQL\Validator\Rules\ExecutableDefinitions;
|
||||||
use GraphQL\Validator\Rules\FieldsOnCorrectType;
|
use GraphQL\Validator\Rules\FieldsOnCorrectType;
|
||||||
use GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
|
use GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
|
||||||
use GraphQL\Validator\Rules\KnownArgumentNames;
|
use GraphQL\Validator\Rules\KnownArgumentNames;
|
||||||
@ -46,6 +35,7 @@ use GraphQL\Validator\Rules\UniqueInputFieldNames;
|
|||||||
use GraphQL\Validator\Rules\UniqueOperationNames;
|
use GraphQL\Validator\Rules\UniqueOperationNames;
|
||||||
use GraphQL\Validator\Rules\UniqueVariableNames;
|
use GraphQL\Validator\Rules\UniqueVariableNames;
|
||||||
use GraphQL\Validator\Rules\VariablesAreInputTypes;
|
use GraphQL\Validator\Rules\VariablesAreInputTypes;
|
||||||
|
use GraphQL\Validator\Rules\VariablesDefaultValueAllowed;
|
||||||
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
|
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,6 +112,7 @@ class DocumentValidator
|
|||||||
{
|
{
|
||||||
if (null === self::$defaultRules) {
|
if (null === self::$defaultRules) {
|
||||||
self::$defaultRules = [
|
self::$defaultRules = [
|
||||||
|
ExecutableDefinitions::class => new ExecutableDefinitions(),
|
||||||
UniqueOperationNames::class => new UniqueOperationNames(),
|
UniqueOperationNames::class => new UniqueOperationNames(),
|
||||||
LoneAnonymousOperation::class => new LoneAnonymousOperation(),
|
LoneAnonymousOperation::class => new LoneAnonymousOperation(),
|
||||||
KnownTypeNames::class => new KnownTypeNames(),
|
KnownTypeNames::class => new KnownTypeNames(),
|
||||||
@ -141,9 +132,9 @@ class DocumentValidator
|
|||||||
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
|
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
|
||||||
KnownArgumentNames::class => new KnownArgumentNames(),
|
KnownArgumentNames::class => new KnownArgumentNames(),
|
||||||
UniqueArgumentNames::class => new UniqueArgumentNames(),
|
UniqueArgumentNames::class => new UniqueArgumentNames(),
|
||||||
ArgumentsOfCorrectType::class => new ArgumentsOfCorrectType(),
|
ValuesOfCorrectType::class => new ValuesOfCorrectType(),
|
||||||
ProvidedNonNullArguments::class => new ProvidedNonNullArguments(),
|
ProvidedNonNullArguments::class => new ProvidedNonNullArguments(),
|
||||||
DefaultValuesOfCorrectType::class => new DefaultValuesOfCorrectType(),
|
VariablesDefaultValueAllowed::class => new VariablesDefaultValueAllowed(),
|
||||||
VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
|
VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
|
||||||
OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(),
|
OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(),
|
||||||
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
|
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
|
||||||
@ -223,100 +214,23 @@ class DocumentValidator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility for validators which determines if a value literal AST is valid given
|
* Utility which determines if a value literal node is valid for an input type.
|
||||||
* an input type.
|
|
||||||
*
|
*
|
||||||
* Note that this only validates literal values, variables are assumed to
|
* Deprecated. Rely on validation for documents containing literal values.
|
||||||
* provide values of the correct type.
|
|
||||||
*
|
*
|
||||||
* @return array
|
* @deprecated
|
||||||
|
* @return Error[]
|
||||||
*/
|
*/
|
||||||
public static function isValidLiteralValue(Type $type, $valueNode)
|
public static function isValidLiteralValue(Type $type, $valueNode)
|
||||||
{
|
{
|
||||||
// A value must be provided if the type is non-null.
|
$emptySchema = new Schema([]);
|
||||||
if ($type instanceof NonNull) {
|
$emptyDoc = new DocumentNode(['definitions' => []]);
|
||||||
if (!$valueNode || $valueNode instanceof NullValueNode) {
|
$typeInfo = new TypeInfo($emptySchema, $type);
|
||||||
return [ 'Expected "' . Utils::printSafe($type) . '", found null.' ];
|
$context = new ValidationContext($emptySchema, $emptyDoc, $typeInfo);
|
||||||
}
|
$validator = new ValuesOfCorrectType();
|
||||||
return static::isValidLiteralValue($type->getWrappedType(), $valueNode);
|
$visitor = $validator->getVisitor($context);
|
||||||
}
|
Visitor::visit($valueNode, Visitor::visitWithTypeInfo($typeInfo, $visitor));
|
||||||
|
return $context->getErrors();
|
||||||
if (!$valueNode || $valueNode instanceof NullValueNode) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function only tests literals, and assumes variables will provide
|
|
||||||
// values of the correct type.
|
|
||||||
if ($valueNode instanceof VariableNode) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lists accept a non-list value as a list of one.
|
|
||||||
if ($type instanceof ListOfType) {
|
|
||||||
$itemType = $type->getWrappedType();
|
|
||||||
if ($valueNode instanceof ListValueNode) {
|
|
||||||
$errors = [];
|
|
||||||
foreach($valueNode->values as $index => $itemNode) {
|
|
||||||
$tmp = static::isValidLiteralValue($itemType, $itemNode);
|
|
||||||
|
|
||||||
if ($tmp) {
|
|
||||||
$errors = array_merge($errors, Utils::map($tmp, function($error) use ($index) {
|
|
||||||
return "In element #$index: $error";
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $errors;
|
|
||||||
} else {
|
|
||||||
return static::isValidLiteralValue($itemType, $valueNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input objects check each defined field and look for undefined fields.
|
|
||||||
if ($type instanceof InputObjectType) {
|
|
||||||
if ($valueNode->kind !== NodeKind::OBJECT) {
|
|
||||||
return [ "Expected \"{$type->name}\", found not an object." ];
|
|
||||||
}
|
|
||||||
|
|
||||||
$fields = $type->getFields();
|
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
// Ensure every provided field is defined.
|
|
||||||
$fieldNodes = $valueNode->fields;
|
|
||||||
|
|
||||||
foreach ($fieldNodes as $providedFieldNode) {
|
|
||||||
if (empty($fields[$providedFieldNode->name->value])) {
|
|
||||||
$errors[] = "In field \"{$providedFieldNode->name->value}\": Unknown field.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure every defined field is valid.
|
|
||||||
$fieldNodeMap = Utils::keyMap($fieldNodes, function($fieldNode) {return $fieldNode->name->value;});
|
|
||||||
foreach ($fields as $fieldName => $field) {
|
|
||||||
$result = static::isValidLiteralValue(
|
|
||||||
$field->getType(),
|
|
||||||
isset($fieldNodeMap[$fieldName]) ? $fieldNodeMap[$fieldName]->value : null
|
|
||||||
);
|
|
||||||
if ($result) {
|
|
||||||
$errors = array_merge($errors, Utils::map($result, function($error) use ($fieldName) {
|
|
||||||
return "In field \"$fieldName\": $error";
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type instanceof LeafType) {
|
|
||||||
// Scalars must parse to a non-null value
|
|
||||||
if (!$type->isValidLiteral($valueNode)) {
|
|
||||||
$printed = Printer::doPrint($valueNode);
|
|
||||||
return [ "Expected type \"{$type->name}\", found $printed." ];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvariantViolation('Must be input type');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace GraphQL\Validator\Rules;
|
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
|
||||||
use GraphQL\Language\AST\ArgumentNode;
|
|
||||||
use GraphQL\Language\AST\NodeKind;
|
|
||||||
use GraphQL\Language\Printer;
|
|
||||||
use GraphQL\Language\Visitor;
|
|
||||||
use GraphQL\Validator\DocumentValidator;
|
|
||||||
use GraphQL\Validator\ValidationContext;
|
|
||||||
|
|
||||||
class ArgumentsOfCorrectType extends AbstractValidationRule
|
|
||||||
{
|
|
||||||
static function badValueMessage($argName, $type, $value, $verboseErrors = [])
|
|
||||||
{
|
|
||||||
$message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : '';
|
|
||||||
return "Argument \"$argName\" has invalid value $value.$message";
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getVisitor(ValidationContext $context)
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
NodeKind::ARGUMENT => function(ArgumentNode $argNode) use ($context) {
|
|
||||||
$argDef = $context->getArgument();
|
|
||||||
if ($argDef) {
|
|
||||||
$errors = DocumentValidator::isValidLiteralValue($argDef->getType(), $argNode->value);
|
|
||||||
|
|
||||||
if (!empty($errors)) {
|
|
||||||
$context->reportError(new Error(
|
|
||||||
self::badValueMessage($argNode->name->value, $argDef->getType(), Printer::doPrint($argNode->value), $errors),
|
|
||||||
[$argNode->value]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Visitor::skipNode();
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace GraphQL\Validator\Rules;
|
|
||||||
|
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
|
||||||
use GraphQL\Language\AST\Node;
|
|
||||||
use GraphQL\Language\AST\NodeKind;
|
|
||||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
|
||||||
use GraphQL\Language\Printer;
|
|
||||||
use GraphQL\Language\Visitor;
|
|
||||||
use GraphQL\Type\Definition\NonNull;
|
|
||||||
use GraphQL\Validator\DocumentValidator;
|
|
||||||
use GraphQL\Validator\ValidationContext;
|
|
||||||
|
|
||||||
class DefaultValuesOfCorrectType extends AbstractValidationRule
|
|
||||||
{
|
|
||||||
static function badValueForDefaultArgMessage($varName, $type, $value, $verboseErrors = null)
|
|
||||||
{
|
|
||||||
$message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : '';
|
|
||||||
return "Variable \$$varName has invalid default value: $value.$message";
|
|
||||||
}
|
|
||||||
|
|
||||||
static function defaultForNonNullArgMessage($varName, $type, $guessType)
|
|
||||||
{
|
|
||||||
return "Variable \$$varName of type $type " .
|
|
||||||
"is required and will never use the default value. " .
|
|
||||||
"Perhaps you meant to use type $guessType.";
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getVisitor(ValidationContext $context)
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $varDefNode) use ($context) {
|
|
||||||
$name = $varDefNode->variable->name->value;
|
|
||||||
$defaultValue = $varDefNode->defaultValue;
|
|
||||||
$type = $context->getInputType();
|
|
||||||
|
|
||||||
if ($type instanceof NonNull && $defaultValue) {
|
|
||||||
$context->reportError(new Error(
|
|
||||||
static::defaultForNonNullArgMessage($name, $type, $type->getWrappedType()),
|
|
||||||
[$defaultValue]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if ($type && $defaultValue) {
|
|
||||||
$errors = DocumentValidator::isValidLiteralValue($type, $defaultValue);
|
|
||||||
if (!empty($errors)) {
|
|
||||||
$context->reportError(new Error(
|
|
||||||
static::badValueForDefaultArgMessage($name, $type, Printer::doPrint($defaultValue), $errors),
|
|
||||||
[$defaultValue]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Visitor::skipNode();
|
|
||||||
},
|
|
||||||
NodeKind::SELECTION_SET => function() {return Visitor::skipNode();},
|
|
||||||
NodeKind::FRAGMENT_DEFINITION => function() {return Visitor::skipNode();}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
47
src/Validator/Rules/ExecutableDefinitions.php
Normal file
47
src/Validator/Rules/ExecutableDefinitions.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Validator\Rules;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
|
use GraphQL\Language\AST\DocumentNode;
|
||||||
|
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\Node;
|
||||||
|
use GraphQL\Language\AST\NodeKind;
|
||||||
|
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||||
|
use GraphQL\Language\Visitor;
|
||||||
|
use GraphQL\Validator\ValidationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executable definitions
|
||||||
|
*
|
||||||
|
* A GraphQL document is only valid for execution if all definitions are either
|
||||||
|
* operation or fragment definitions.
|
||||||
|
*/
|
||||||
|
class ExecutableDefinitions extends AbstractValidationRule
|
||||||
|
{
|
||||||
|
static function nonExecutableDefinitionMessage($defName)
|
||||||
|
{
|
||||||
|
return "The \"$defName\" definition is not executable.";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVisitor(ValidationContext $context)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
NodeKind::DOCUMENT => function (DocumentNode $node) use ($context) {
|
||||||
|
/** @var Node $definition */
|
||||||
|
foreach ($node->definitions as $definition) {
|
||||||
|
if (
|
||||||
|
!$definition instanceof OperationDefinitionNode &&
|
||||||
|
!$definition instanceof FragmentDefinitionNode
|
||||||
|
) {
|
||||||
|
$context->reportError(new Error(
|
||||||
|
self::nonExecutableDefinitionMessage($definition->name->value),
|
||||||
|
[$definition->name]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Visitor::skipNode();
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -4,27 +4,27 @@ namespace GraphQL\Validator\Rules;
|
|||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Language\AST\FieldNode;
|
use GraphQL\Language\AST\FieldNode;
|
||||||
use GraphQL\Language\AST\NodeKind;
|
use GraphQL\Language\AST\NodeKind;
|
||||||
|
use GraphQL\Type\Definition\InputObjectType;
|
||||||
|
use GraphQL\Type\Definition\InterfaceType;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use GraphQL\Type\Definition\UnionType;
|
||||||
|
use GraphQL\Type\Schema;
|
||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
use GraphQL\Validator\ValidationContext;
|
use GraphQL\Validator\ValidationContext;
|
||||||
|
|
||||||
class FieldsOnCorrectType extends AbstractValidationRule
|
class FieldsOnCorrectType extends AbstractValidationRule
|
||||||
{
|
{
|
||||||
static function undefinedFieldMessage($field, $type, array $suggestedTypes = [])
|
static function undefinedFieldMessage($fieldName, $type, array $suggestedTypeNames, array $suggestedFieldNames)
|
||||||
{
|
{
|
||||||
$message = 'Cannot query field "' . $field . '" on type "' . $type.'".';
|
$message = 'Cannot query field "' . $fieldName . '" on type "' . $type.'".';
|
||||||
|
|
||||||
$maxLength = 5;
|
if ($suggestedTypeNames) {
|
||||||
$count = count($suggestedTypes);
|
$suggestions = Utils::quotedOrList($suggestedTypeNames);
|
||||||
if ($count > 0) {
|
$message .= " Did you mean to use an inline fragment on $suggestions?";
|
||||||
$suggestions = array_slice($suggestedTypes, 0, $maxLength);
|
} else if ($suggestedFieldNames) {
|
||||||
$suggestions = Utils::map($suggestions, function($t) { return "\"$t\""; });
|
$suggestions = Utils::quotedOrList($suggestedFieldNames);
|
||||||
$suggestions = implode(', ', $suggestions);
|
$message .= " Did you mean {$suggestions}?";
|
||||||
|
|
||||||
if ($count > $maxLength) {
|
|
||||||
$suggestions .= ', and ' . ($count - $maxLength) . ' other types';
|
|
||||||
}
|
|
||||||
$message .= " However, this field exists on $suggestions.";
|
|
||||||
$message .= ' Perhaps you meant to use an inline fragment?';
|
|
||||||
}
|
}
|
||||||
return $message;
|
return $message;
|
||||||
}
|
}
|
||||||
@ -37,8 +37,32 @@ class FieldsOnCorrectType extends AbstractValidationRule
|
|||||||
if ($type) {
|
if ($type) {
|
||||||
$fieldDef = $context->getFieldDef();
|
$fieldDef = $context->getFieldDef();
|
||||||
if (!$fieldDef) {
|
if (!$fieldDef) {
|
||||||
|
// This isn't valid. Let's find suggestions, if any.
|
||||||
|
$schema = $context->getSchema();
|
||||||
|
$fieldName = $node->name->value;
|
||||||
|
// First determine if there are any suggested types to condition on.
|
||||||
|
$suggestedTypeNames = $this->getSuggestedTypeNames(
|
||||||
|
$schema,
|
||||||
|
$type,
|
||||||
|
$fieldName
|
||||||
|
);
|
||||||
|
// If there are no suggested types, then perhaps this was a typo?
|
||||||
|
$suggestedFieldNames = $suggestedTypeNames
|
||||||
|
? []
|
||||||
|
: $this->getSuggestedFieldNames(
|
||||||
|
$schema,
|
||||||
|
$type,
|
||||||
|
$fieldName
|
||||||
|
);
|
||||||
|
|
||||||
|
// Report an error, including helpful suggestions.
|
||||||
$context->reportError(new Error(
|
$context->reportError(new Error(
|
||||||
static::undefinedFieldMessage($node->name->value, $type->name),
|
static::undefinedFieldMessage(
|
||||||
|
$node->name->value,
|
||||||
|
$type->name,
|
||||||
|
$suggestedTypeNames,
|
||||||
|
$suggestedFieldNames
|
||||||
|
),
|
||||||
[$node]
|
[$node]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -46,4 +70,72 @@ class FieldsOnCorrectType extends AbstractValidationRule
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go through all of the implementations of type, as well as the interfaces
|
||||||
|
* that they implement. If any of those types include the provided field,
|
||||||
|
* suggest them, sorted by how often the type is referenced, starting
|
||||||
|
* with Interfaces.
|
||||||
|
*
|
||||||
|
* @param Schema $schema
|
||||||
|
* @param $type
|
||||||
|
* @param string $fieldName
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getSuggestedTypeNames(Schema $schema, $type, $fieldName)
|
||||||
|
{
|
||||||
|
if (Type::isAbstractType($type)) {
|
||||||
|
$suggestedObjectTypes = [];
|
||||||
|
$interfaceUsageCount = [];
|
||||||
|
|
||||||
|
foreach($schema->getPossibleTypes($type) as $possibleType) {
|
||||||
|
$fields = $possibleType->getFields();
|
||||||
|
if (!isset($fields[$fieldName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// This object type defines this field.
|
||||||
|
$suggestedObjectTypes[] = $possibleType->name;
|
||||||
|
foreach($possibleType->getInterfaces() as $possibleInterface) {
|
||||||
|
$fields = $possibleInterface->getFields();
|
||||||
|
if (!isset($fields[$fieldName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// This interface type defines this field.
|
||||||
|
$interfaceUsageCount[$possibleInterface->name] =
|
||||||
|
!isset($interfaceUsageCount[$possibleInterface->name])
|
||||||
|
? 0
|
||||||
|
: $interfaceUsageCount[$possibleInterface->name] + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggest interface types based on how common they are.
|
||||||
|
arsort($interfaceUsageCount);
|
||||||
|
$suggestedInterfaceTypes = array_keys($interfaceUsageCount);
|
||||||
|
|
||||||
|
// Suggest both interface and object types.
|
||||||
|
return array_merge($suggestedInterfaceTypes, $suggestedObjectTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, must be an Object type, which does not have possible fields.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For the field name provided, determine if there are any similar field names
|
||||||
|
* that may be the result of a typo.
|
||||||
|
*
|
||||||
|
* @param Schema $schema
|
||||||
|
* @param $type
|
||||||
|
* @param string $fieldName
|
||||||
|
* @return array|string[]
|
||||||
|
*/
|
||||||
|
private function getSuggestedFieldNames(Schema $schema, $type, $fieldName)
|
||||||
|
{
|
||||||
|
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
|
||||||
|
$possibleFieldNames = array_keys($type->getFields());
|
||||||
|
return Utils::suggestionList($fieldName, $possibleFieldNames);
|
||||||
|
}
|
||||||
|
// Otherwise, must be a Union type, which does not define fields.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,56 +7,68 @@ use GraphQL\Language\AST\NodeKind;
|
|||||||
use GraphQL\Utils\Utils;
|
use GraphQL\Utils\Utils;
|
||||||
use GraphQL\Validator\ValidationContext;
|
use GraphQL\Validator\ValidationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known argument names
|
||||||
|
*
|
||||||
|
* A GraphQL field is only valid if all supplied arguments are defined by
|
||||||
|
* that field.
|
||||||
|
*/
|
||||||
class KnownArgumentNames extends AbstractValidationRule
|
class KnownArgumentNames extends AbstractValidationRule
|
||||||
{
|
{
|
||||||
public static function unknownArgMessage($argName, $fieldName, $type)
|
public static function unknownArgMessage($argName, $fieldName, $typeName, array $suggestedArgs)
|
||||||
{
|
{
|
||||||
return "Unknown argument \"$argName\" on field \"$fieldName\" of type \"$type\".";
|
$message = "Unknown argument \"$argName\" on field \"$fieldName\" of type \"$typeName\".";
|
||||||
|
if ($suggestedArgs) {
|
||||||
|
$message .= ' Did you mean ' . Utils::quotedOrList($suggestedArgs) . '?';
|
||||||
|
}
|
||||||
|
return $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function unknownDirectiveArgMessage($argName, $directiveName)
|
public static function unknownDirectiveArgMessage($argName, $directiveName, array $suggestedArgs)
|
||||||
{
|
{
|
||||||
return "Unknown argument \"$argName\" on directive \"@$directiveName\".";
|
$message = "Unknown argument \"$argName\" on directive \"@$directiveName\".";
|
||||||
|
if ($suggestedArgs) {
|
||||||
|
$message .= ' Did you mean ' . Utils::quotedOrList($suggestedArgs) . '?';
|
||||||
|
}
|
||||||
|
return $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getVisitor(ValidationContext $context)
|
public function getVisitor(ValidationContext $context)
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
NodeKind::ARGUMENT => function(ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) {
|
NodeKind::ARGUMENT => function(ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) {
|
||||||
|
$argDef = $context->getArgument();
|
||||||
|
if (!$argDef) {
|
||||||
$argumentOf = $ancestors[count($ancestors) - 1];
|
$argumentOf = $ancestors[count($ancestors) - 1];
|
||||||
if ($argumentOf->kind === NodeKind::FIELD) {
|
if ($argumentOf->kind === NodeKind::FIELD) {
|
||||||
$fieldDef = $context->getFieldDef();
|
$fieldDef = $context->getFieldDef();
|
||||||
|
|
||||||
if ($fieldDef) {
|
|
||||||
$fieldArgDef = null;
|
|
||||||
foreach ($fieldDef->args as $arg) {
|
|
||||||
if ($arg->name === $node->name->value) {
|
|
||||||
$fieldArgDef = $arg;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$fieldArgDef) {
|
|
||||||
$parentType = $context->getParentType();
|
$parentType = $context->getParentType();
|
||||||
Utils::invariant($parentType);
|
if ($fieldDef && $parentType) {
|
||||||
$context->reportError(new Error(
|
$context->reportError(new Error(
|
||||||
self::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name),
|
self::unknownArgMessage(
|
||||||
|
$node->name->value,
|
||||||
|
$fieldDef->name,
|
||||||
|
$parentType->name,
|
||||||
|
Utils::suggestionList(
|
||||||
|
$node->name->value,
|
||||||
|
array_map(function ($arg) { return $arg->name; }, $fieldDef->args)
|
||||||
|
)
|
||||||
|
),
|
||||||
[$node]
|
[$node]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else if ($argumentOf->kind === NodeKind::DIRECTIVE) {
|
} else if ($argumentOf->kind === NodeKind::DIRECTIVE) {
|
||||||
$directive = $context->getDirective();
|
$directive = $context->getDirective();
|
||||||
if ($directive) {
|
if ($directive) {
|
||||||
$directiveArgDef = null;
|
|
||||||
foreach ($directive->args as $arg) {
|
|
||||||
if ($arg->name === $node->name->value) {
|
|
||||||
$directiveArgDef = $arg;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$directiveArgDef) {
|
|
||||||
$context->reportError(new Error(
|
$context->reportError(new Error(
|
||||||
self::unknownDirectiveArgMessage($node->name->value, $directive->name),
|
self::unknownDirectiveArgMessage(
|
||||||
|
$node->name->value,
|
||||||
|
$directive->name,
|
||||||
|
Utils::suggestionList(
|
||||||
|
$node->name->value,
|
||||||
|
array_map(function ($arg) { return $arg->name; }, $directive->args)
|
||||||
|
)
|
||||||
|
),
|
||||||
[$node]
|
[$node]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Validator\Rules;
|
namespace GraphQL\Validator\Rules;
|
||||||
|
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Language\AST\DirectiveNode;
|
use GraphQL\Language\AST\DirectiveNode;
|
||||||
use GraphQL\Language\AST\Node;
|
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||||
use GraphQL\Language\AST\NodeKind;
|
use GraphQL\Language\AST\NodeKind;
|
||||||
|
use GraphQL\Language\DirectiveLocation;
|
||||||
use GraphQL\Validator\ValidationContext;
|
use GraphQL\Validator\ValidationContext;
|
||||||
use GraphQL\Type\Definition\DirectiveLocation;
|
|
||||||
|
|
||||||
class KnownDirectives extends AbstractValidationRule
|
class KnownDirectives extends AbstractValidationRule
|
||||||
{
|
{
|
||||||
@ -38,10 +37,9 @@ class KnownDirectives extends AbstractValidationRule
|
|||||||
self::unknownDirectiveMessage($node->name->value),
|
self::unknownDirectiveMessage($node->name->value),
|
||||||
[$node]
|
[$node]
|
||||||
));
|
));
|
||||||
return ;
|
return;
|
||||||
}
|
}
|
||||||
$appliedTo = $ancestors[count($ancestors) - 1];
|
$candidateLocation = $this->getDirectiveLocationForASTPath($ancestors);
|
||||||
$candidateLocation = $this->getLocationForAppliedNode($appliedTo);
|
|
||||||
|
|
||||||
if (!$candidateLocation) {
|
if (!$candidateLocation) {
|
||||||
$context->reportError(new Error(
|
$context->reportError(new Error(
|
||||||
@ -58,8 +56,9 @@ class KnownDirectives extends AbstractValidationRule
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getLocationForAppliedNode(Node $appliedTo)
|
private function getDirectiveLocationForASTPath(array $ancestors)
|
||||||
{
|
{
|
||||||
|
$appliedTo = $ancestors[count($ancestors) - 1];
|
||||||
switch ($appliedTo->kind) {
|
switch ($appliedTo->kind) {
|
||||||
case NodeKind::OPERATION_DEFINITION:
|
case NodeKind::OPERATION_DEFINITION:
|
||||||
switch ($appliedTo->operation) {
|
switch ($appliedTo->operation) {
|
||||||
@ -68,10 +67,43 @@ class KnownDirectives extends AbstractValidationRule
|
|||||||
case 'subscription': return DirectiveLocation::SUBSCRIPTION;
|
case 'subscription': return DirectiveLocation::SUBSCRIPTION;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case NodeKind::FIELD: return DirectiveLocation::FIELD;
|
case NodeKind::FIELD:
|
||||||
case NodeKind::FRAGMENT_SPREAD: return DirectiveLocation::FRAGMENT_SPREAD;
|
return DirectiveLocation::FIELD;
|
||||||
case NodeKind::INLINE_FRAGMENT: return DirectiveLocation::INLINE_FRAGMENT;
|
case NodeKind::FRAGMENT_SPREAD:
|
||||||
case NodeKind::FRAGMENT_DEFINITION: return DirectiveLocation::FRAGMENT_DEFINITION;
|
return DirectiveLocation::FRAGMENT_SPREAD;
|
||||||
|
case NodeKind::INLINE_FRAGMENT:
|
||||||
|
return DirectiveLocation::INLINE_FRAGMENT;
|
||||||
|
case NodeKind::FRAGMENT_DEFINITION:
|
||||||
|
return DirectiveLocation::FRAGMENT_DEFINITION;
|
||||||
|
case NodeKind::SCHEMA_DEFINITION:
|
||||||
|
return DirectiveLocation::SCHEMA;
|
||||||
|
case NodeKind::SCALAR_TYPE_DEFINITION:
|
||||||
|
case NodeKind::SCALAR_TYPE_EXTENSION:
|
||||||
|
return DirectiveLocation::SCALAR;
|
||||||
|
case NodeKind::OBJECT_TYPE_DEFINITION:
|
||||||
|
case NodeKind::OBJECT_TYPE_EXTENSION:
|
||||||
|
return DirectiveLocation::OBJECT;
|
||||||
|
case NodeKind::FIELD_DEFINITION:
|
||||||
|
return DirectiveLocation::FIELD_DEFINITION;
|
||||||
|
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
||||||
|
case NodeKind::INTERFACE_TYPE_EXTENSION:
|
||||||
|
return DirectiveLocation::IFACE;
|
||||||
|
case NodeKind::UNION_TYPE_DEFINITION:
|
||||||
|
case NodeKind::UNION_TYPE_EXTENSION:
|
||||||
|
return DirectiveLocation::UNION;
|
||||||
|
case NodeKind::ENUM_TYPE_DEFINITION:
|
||||||
|
case NodeKind::ENUM_TYPE_EXTENSION:
|
||||||
|
return DirectiveLocation::ENUM;
|
||||||
|
case NodeKind::ENUM_VALUE_DEFINITION:
|
||||||
|
return DirectiveLocation::ENUM_VALUE;
|
||||||
|
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
||||||
|
case NodeKind::INPUT_OBJECT_TYPE_EXTENSION:
|
||||||
|
return DirectiveLocation::INPUT_OBJECT;
|
||||||
|
case NodeKind::INPUT_VALUE_DEFINITION:
|
||||||
|
$parentNode = $ancestors[count($ancestors) - 3];
|
||||||
|
return $parentNode instanceof InputObjectTypeDefinitionNode
|
||||||
|
? DirectiveLocation::INPUT_FIELD_DEFINITION
|
||||||
|
: DirectiveLocation::ARGUMENT_DEFINITION;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,55 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Validator\Rules;
|
namespace GraphQL\Validator\Rules;
|
||||||
|
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Language\AST\NamedTypeNode;
|
use GraphQL\Language\AST\NamedTypeNode;
|
||||||
use GraphQL\Language\AST\NodeKind;
|
use GraphQL\Language\AST\NodeKind;
|
||||||
use GraphQL\Language\Visitor;
|
use GraphQL\Language\Visitor;
|
||||||
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use GraphQL\Utils\Utils;
|
||||||
use GraphQL\Validator\ValidationContext;
|
use GraphQL\Validator\ValidationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known type names
|
||||||
|
*
|
||||||
|
* A GraphQL document is only valid if referenced types (specifically
|
||||||
|
* variable definitions and fragment conditions) are defined by the type schema.
|
||||||
|
*/
|
||||||
class KnownTypeNames extends AbstractValidationRule
|
class KnownTypeNames extends AbstractValidationRule
|
||||||
{
|
{
|
||||||
static function unknownTypeMessage($type)
|
static function unknownTypeMessage($type, array $suggestedTypes)
|
||||||
{
|
{
|
||||||
return "Unknown type \"$type\".";
|
$message = "Unknown type \"$type\".";
|
||||||
|
if ($suggestedTypes) {
|
||||||
|
$suggestions = Utils::quotedOrList($suggestedTypes);
|
||||||
|
$message .= " Did you mean $suggestions?";
|
||||||
|
}
|
||||||
|
return $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getVisitor(ValidationContext $context)
|
public function getVisitor(ValidationContext $context)
|
||||||
{
|
{
|
||||||
$skip = function() {return Visitor::skipNode();};
|
$skip = function() { return Visitor::skipNode(); };
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
// TODO: when validating IDL, re-enable these. Experimental version does not
|
||||||
|
// add unreferenced types, resulting in false-positive errors. Squelched
|
||||||
|
// errors for now.
|
||||||
NodeKind::OBJECT_TYPE_DEFINITION => $skip,
|
NodeKind::OBJECT_TYPE_DEFINITION => $skip,
|
||||||
NodeKind::INTERFACE_TYPE_DEFINITION => $skip,
|
NodeKind::INTERFACE_TYPE_DEFINITION => $skip,
|
||||||
NodeKind::UNION_TYPE_DEFINITION => $skip,
|
NodeKind::UNION_TYPE_DEFINITION => $skip,
|
||||||
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip,
|
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip,
|
||||||
|
NodeKind::NAMED_TYPE => function(NamedTypeNode $node) use ($context) {
|
||||||
NodeKind::NAMED_TYPE => function(NamedTypeNode $node, $key) use ($context) {
|
$schema = $context->getSchema();
|
||||||
$typeName = $node->name->value;
|
$typeName = $node->name->value;
|
||||||
$type = $context->getSchema()->getType($typeName);
|
$type = $schema->getType($typeName);
|
||||||
if (!$type) {
|
if (!$type) {
|
||||||
$context->reportError(new Error(self::unknownTypeMessage($typeName), [$node]));
|
$context->reportError(new Error(
|
||||||
|
self::unknownTypeMessage(
|
||||||
|
$typeName,
|
||||||
|
Utils::suggestionList($typeName, array_keys($schema->getTypeMap()))
|
||||||
|
), [$node])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -61,7 +61,13 @@ class PossibleFragmentSpreads extends AbstractValidationRule
|
|||||||
private function getFragmentType(ValidationContext $context, $name)
|
private function getFragmentType(ValidationContext $context, $name)
|
||||||
{
|
{
|
||||||
$frag = $context->getFragment($name);
|
$frag = $context->getFragment($name);
|
||||||
return $frag ? TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition) : null;
|
if ($frag) {
|
||||||
|
$type = TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition);
|
||||||
|
if ($type instanceof CompositeType) {
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType)
|
private function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType)
|
||||||
|
@ -203,11 +203,21 @@ class QueryComplexity extends AbstractQuerySecurity
|
|||||||
$args = [];
|
$args = [];
|
||||||
|
|
||||||
if ($fieldDef instanceof FieldDefinition) {
|
if ($fieldDef instanceof FieldDefinition) {
|
||||||
$variableValues = Values::getVariableValues(
|
$variableValuesResult = Values::getVariableValues(
|
||||||
$this->context->getSchema(),
|
$this->context->getSchema(),
|
||||||
$this->variableDefs,
|
$this->variableDefs,
|
||||||
$rawVariableValues
|
$rawVariableValues
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($variableValuesResult['errors']) {
|
||||||
|
throw new Error(implode("\n\n", array_map(
|
||||||
|
function ($error) {
|
||||||
|
return $error->getMessage();
|
||||||
|
}
|
||||||
|
, $variableValuesResult['errors'])));
|
||||||
|
}
|
||||||
|
$variableValues = $variableValuesResult['coerced'];
|
||||||
|
|
||||||
$args = Values::getArgumentValues($fieldDef, $node, $variableValues);
|
$args = Values::getArgumentValues($fieldDef, $node, $variableValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,12 +230,21 @@ class QueryComplexity extends AbstractQuerySecurity
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$variableValues = Values::getVariableValues(
|
$variableValuesResult = Values::getVariableValues(
|
||||||
$this->context->getSchema(),
|
$this->context->getSchema(),
|
||||||
$this->variableDefs,
|
$this->variableDefs,
|
||||||
$this->getRawVariableValues()
|
$this->getRawVariableValues()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($variableValuesResult['errors']) {
|
||||||
|
throw new Error(implode("\n\n", array_map(
|
||||||
|
function ($error) {
|
||||||
|
return $error->getMessage();
|
||||||
|
}
|
||||||
|
, $variableValuesResult['errors'])));
|
||||||
|
}
|
||||||
|
$variableValues = $variableValuesResult['coerced'];
|
||||||
|
|
||||||
if ($directiveNode->name->value === 'include') {
|
if ($directiveNode->name->value === 'include') {
|
||||||
$directive = Directive::includeDirective();
|
$directive = Directive::includeDirective();
|
||||||
$directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);
|
$directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);
|
||||||
|
238
src/Validator/Rules/ValuesOfCorrectType.php
Normal file
238
src/Validator/Rules/ValuesOfCorrectType.php
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Validator\Rules;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
|
use GraphQL\Language\AST\BooleanValueNode;
|
||||||
|
use GraphQL\Language\AST\EnumValueNode;
|
||||||
|
use GraphQL\Language\AST\FloatValueNode;
|
||||||
|
use GraphQL\Language\AST\IntValueNode;
|
||||||
|
use GraphQL\Language\AST\ListValueNode;
|
||||||
|
use GraphQL\Language\AST\NodeKind;
|
||||||
|
use GraphQL\Language\AST\NullValueNode;
|
||||||
|
use GraphQL\Language\AST\ObjectFieldNode;
|
||||||
|
use GraphQL\Language\AST\ObjectValueNode;
|
||||||
|
use GraphQL\Language\AST\StringValueNode;
|
||||||
|
use GraphQL\Language\AST\ValueNode;
|
||||||
|
use GraphQL\Language\Printer;
|
||||||
|
use GraphQL\Language\Visitor;
|
||||||
|
use GraphQL\Type\Definition\EnumType;
|
||||||
|
use GraphQL\Type\Definition\EnumValueDefinition;
|
||||||
|
use GraphQL\Type\Definition\InputObjectType;
|
||||||
|
use GraphQL\Type\Definition\ListOfType;
|
||||||
|
use GraphQL\Type\Definition\NonNull;
|
||||||
|
use GraphQL\Type\Definition\ScalarType;
|
||||||
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use GraphQL\Utils\Utils;
|
||||||
|
use GraphQL\Validator\ValidationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value literals of correct type
|
||||||
|
*
|
||||||
|
* A GraphQL document is only valid if all value literals are of the type
|
||||||
|
* expected at their position.
|
||||||
|
*/
|
||||||
|
class ValuesOfCorrectType extends AbstractValidationRule
|
||||||
|
{
|
||||||
|
static function badValueMessage($typeName, $valueName, $message = null)
|
||||||
|
{
|
||||||
|
return "Expected type {$typeName}, found {$valueName}" .
|
||||||
|
($message ? "; ${message}" : '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
static function requiredFieldMessage($typeName, $fieldName, $fieldTypeName)
|
||||||
|
{
|
||||||
|
return "Field {$typeName}.{$fieldName} of required type " .
|
||||||
|
"{$fieldTypeName} was not provided.";
|
||||||
|
}
|
||||||
|
|
||||||
|
static function unknownFieldMessage($typeName, $fieldName, $message = null)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
"Field \"{$fieldName}\" is not defined by type {$typeName}" .
|
||||||
|
($message ? "; {$message}" : '.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVisitor(ValidationContext $context)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
NodeKind::NULL => function(NullValueNode $node) use ($context) {
|
||||||
|
$type = $context->getInputType();
|
||||||
|
if ($type instanceof NonNull) {
|
||||||
|
$context->reportError(
|
||||||
|
new Error(
|
||||||
|
self::badValueMessage((string) $type, Printer::doPrint($node)),
|
||||||
|
$node
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
NodeKind::LST => function(ListValueNode $node) use ($context) {
|
||||||
|
// Note: TypeInfo will traverse into a list's item type, so look to the
|
||||||
|
// parent input type to check if it is a list.
|
||||||
|
$type = Type::getNullableType($context->getParentInputType());
|
||||||
|
if (!$type instanceof ListOfType) {
|
||||||
|
$this->isValidScalar($context, $node);
|
||||||
|
return Visitor::skipNode();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
NodeKind::OBJECT => function(ObjectValueNode $node) use ($context) {
|
||||||
|
// Note: TypeInfo will traverse into a list's item type, so look to the
|
||||||
|
// parent input type to check if it is a list.
|
||||||
|
$type = Type::getNamedType($context->getInputType());
|
||||||
|
if (!$type instanceof InputObjectType) {
|
||||||
|
$this->isValidScalar($context, $node);
|
||||||
|
return Visitor::skipNode();
|
||||||
|
}
|
||||||
|
// Ensure every required field exists.
|
||||||
|
$inputFields = $type->getFields();
|
||||||
|
$nodeFields = iterator_to_array($node->fields);
|
||||||
|
$fieldNodeMap = array_combine(
|
||||||
|
array_map(function ($field) { return $field->name->value; }, $nodeFields),
|
||||||
|
array_values($nodeFields)
|
||||||
|
);
|
||||||
|
foreach ($inputFields as $fieldName => $fieldDef) {
|
||||||
|
$fieldType = $fieldDef->getType();
|
||||||
|
if (!isset($fieldNodeMap[$fieldName]) && $fieldType instanceof NonNull) {
|
||||||
|
$context->reportError(
|
||||||
|
new Error(
|
||||||
|
self::requiredFieldMessage($type->name, $fieldName, (string) $fieldType),
|
||||||
|
$node
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
NodeKind::OBJECT_FIELD => function(ObjectFieldNode $node) use ($context) {
|
||||||
|
$parentType = Type::getNamedType($context->getParentInputType());
|
||||||
|
$fieldType = $context->getInputType();
|
||||||
|
if (!$fieldType && $parentType instanceof InputObjectType) {
|
||||||
|
$suggestions = Utils::suggestionList(
|
||||||
|
$node->name->value,
|
||||||
|
array_keys($parentType->getFields())
|
||||||
|
);
|
||||||
|
$didYouMean = $suggestions
|
||||||
|
? "Did you mean " . Utils::orList($suggestions) . "?"
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$context->reportError(
|
||||||
|
new Error(
|
||||||
|
self::unknownFieldMessage($parentType->name, $node->name->value, $didYouMean),
|
||||||
|
$node
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
NodeKind::ENUM => function(EnumValueNode $node) use ($context) {
|
||||||
|
$type = Type::getNamedType($context->getInputType());
|
||||||
|
if (!$type instanceof EnumType) {
|
||||||
|
$this->isValidScalar($context, $node);
|
||||||
|
} else if (!$type->getValue($node->value)) {
|
||||||
|
$context->reportError(
|
||||||
|
new Error(
|
||||||
|
self::badValueMessage(
|
||||||
|
$type->name,
|
||||||
|
Printer::doPrint($node),
|
||||||
|
$this->enumTypeSuggestion($type, $node)
|
||||||
|
),
|
||||||
|
$node
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
NodeKind::INT => function (IntValueNode $node) use ($context) { $this->isValidScalar($context, $node); },
|
||||||
|
NodeKind::FLOAT => function (FloatValueNode $node) use ($context) { $this->isValidScalar($context, $node); },
|
||||||
|
NodeKind::STRING => function (StringValueNode $node) use ($context) { $this->isValidScalar($context, $node); },
|
||||||
|
NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context) { $this->isValidScalar($context, $node); },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isValidScalar(ValidationContext $context, ValueNode $node)
|
||||||
|
{
|
||||||
|
// Report any error at the full type expected by the location.
|
||||||
|
$locationType = $context->getInputType();
|
||||||
|
|
||||||
|
if (!$locationType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = Type::getNamedType($locationType);
|
||||||
|
|
||||||
|
if (!$type instanceof ScalarType) {
|
||||||
|
$context->reportError(
|
||||||
|
new Error(
|
||||||
|
self::badValueMessage(
|
||||||
|
(string) $locationType,
|
||||||
|
Printer::doPrint($node),
|
||||||
|
$this->enumTypeSuggestion($type, $node)
|
||||||
|
),
|
||||||
|
$node
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalars determine if a literal value is valid via parseLiteral() which
|
||||||
|
// may throw or return an invalid value to indicate failure.
|
||||||
|
try {
|
||||||
|
$parseResult = $type->parseLiteral($node);
|
||||||
|
if (Utils::isInvalid($parseResult)) {
|
||||||
|
$context->reportError(
|
||||||
|
new Error(
|
||||||
|
self::badValueMessage(
|
||||||
|
(string) $locationType,
|
||||||
|
Printer::doPrint($node)
|
||||||
|
),
|
||||||
|
$node
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (\Exception $error) {
|
||||||
|
// Ensure a reference to the original error is maintained.
|
||||||
|
$context->reportError(
|
||||||
|
new Error(
|
||||||
|
self::badValueMessage(
|
||||||
|
(string) $locationType,
|
||||||
|
Printer::doPrint($node),
|
||||||
|
$error->getMessage()
|
||||||
|
),
|
||||||
|
$node,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$error
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (\Throwable $error) {
|
||||||
|
// Ensure a reference to the original error is maintained.
|
||||||
|
$context->reportError(
|
||||||
|
new Error(
|
||||||
|
self::badValueMessage(
|
||||||
|
(string) $locationType,
|
||||||
|
Printer::doPrint($node),
|
||||||
|
$error->getMessage()
|
||||||
|
),
|
||||||
|
$node,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$error
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function enumTypeSuggestion($type, ValueNode $node)
|
||||||
|
{
|
||||||
|
if ($type instanceof EnumType) {
|
||||||
|
$suggestions = Utils::suggestionList(
|
||||||
|
Printer::doPrint($node),
|
||||||
|
array_map(function (EnumValueDefinition $value) {
|
||||||
|
return $value->name;
|
||||||
|
}, $type->getValues())
|
||||||
|
);
|
||||||
|
|
||||||
|
return $suggestions ? 'Did you mean the enum value ' . Utils::orList($suggestions) . '?' : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
60
src/Validator/Rules/VariablesDefaultValueAllowed.php
Normal file
60
src/Validator/Rules/VariablesDefaultValueAllowed.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Validator\Rules;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
|
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\NodeKind;
|
||||||
|
use GraphQL\Language\AST\SelectionSetNode;
|
||||||
|
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||||
|
use GraphQL\Language\Visitor;
|
||||||
|
use GraphQL\Type\Definition\NonNull;
|
||||||
|
use GraphQL\Validator\ValidationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variable's default value is allowed
|
||||||
|
*
|
||||||
|
* A GraphQL document is only valid if all variable default values are allowed
|
||||||
|
* due to a variable not being required.
|
||||||
|
*/
|
||||||
|
class VariablesDefaultValueAllowed extends AbstractValidationRule
|
||||||
|
{
|
||||||
|
static function defaultForRequiredVarMessage($varName, $type, $guessType)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
"Variable \"\${$varName}\" of type \"{$type}\" is required and " .
|
||||||
|
'will not use the default value. ' .
|
||||||
|
"Perhaps you meant to use type \"{$guessType}\"."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVisitor(ValidationContext $context)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) use ($context) {
|
||||||
|
$name = $node->variable->name->value;
|
||||||
|
$defaultValue = $node->defaultValue;
|
||||||
|
$type = $context->getInputType();
|
||||||
|
if ($type instanceof NonNull && $defaultValue) {
|
||||||
|
$context->reportError(
|
||||||
|
new Error(
|
||||||
|
self::defaultForRequiredVarMessage(
|
||||||
|
$name,
|
||||||
|
$type,
|
||||||
|
$type->getWrappedType()
|
||||||
|
),
|
||||||
|
[$defaultValue]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Visitor::skipNode();
|
||||||
|
},
|
||||||
|
NodeKind::SELECTION_SET => function(SelectionSetNode $node) use ($context) {
|
||||||
|
return Visitor::skipNode();
|
||||||
|
},
|
||||||
|
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) use ($context) {
|
||||||
|
return Visitor::skipNode();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -12,11 +12,9 @@ use GraphQL\Error\Error;
|
|||||||
use GraphQL\Type\Schema;
|
use GraphQL\Type\Schema;
|
||||||
use GraphQL\Language\AST\DocumentNode;
|
use GraphQL\Language\AST\DocumentNode;
|
||||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||||
use GraphQL\Language\AST\Node;
|
|
||||||
use GraphQL\Type\Definition\CompositeType;
|
use GraphQL\Type\Definition\CompositeType;
|
||||||
use GraphQL\Type\Definition\FieldDefinition;
|
use GraphQL\Type\Definition\FieldDefinition;
|
||||||
use GraphQL\Type\Definition\InputType;
|
use GraphQL\Type\Definition\InputType;
|
||||||
use GraphQL\Type\Definition\OutputType;
|
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
use GraphQL\Utils\TypeInfo;
|
use GraphQL\Utils\TypeInfo;
|
||||||
|
|
||||||
@ -124,7 +122,7 @@ class ValidationContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $name
|
* @param string $name
|
||||||
* @return FragmentDefinitionNode|null
|
* @return FragmentDefinitionNode|null
|
||||||
*/
|
*/
|
||||||
function getFragment($name)
|
function getFragment($name)
|
||||||
@ -275,6 +273,14 @@ class ValidationContext
|
|||||||
return $this->typeInfo->getInputType();
|
return $this->typeInfo->getInputType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return InputType
|
||||||
|
*/
|
||||||
|
function getParentInputType()
|
||||||
|
{
|
||||||
|
return $this->typeInfo->getParentInputType();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return FieldDefinition
|
* @return FieldDefinition
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Tests;
|
namespace GraphQL\Tests\Error;
|
||||||
|
|
||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Language\Parser;
|
use GraphQL\Language\Parser;
|
||||||
@ -37,6 +37,24 @@ class ErrorTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals([new SourceLocation(2, 7)], $e->getLocations());
|
$this->assertEquals([new SourceLocation(2, 7)], $e->getLocations());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @it converts single node to positions and locations
|
||||||
|
*/
|
||||||
|
public function testConvertSingleNodeToPositionsAndLocations()
|
||||||
|
{
|
||||||
|
$source = new Source('{
|
||||||
|
field
|
||||||
|
}');
|
||||||
|
$ast = Parser::parse($source);
|
||||||
|
$fieldNode = $ast->definitions[0]->selectionSet->selections[0];
|
||||||
|
$e = new Error('msg', $fieldNode); // Non-array value.
|
||||||
|
|
||||||
|
$this->assertEquals([$fieldNode], $e->nodes);
|
||||||
|
$this->assertEquals($source, $e->getSource());
|
||||||
|
$this->assertEquals([8], $e->getPositions());
|
||||||
|
$this->assertEquals([new SourceLocation(2, 7)], $e->getLocations());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @it converts node with loc.start === 0 to positions and locations
|
* @it converts node with loc.start === 0 to positions and locations
|
||||||
*/
|
*/
|
||||||
@ -110,4 +128,23 @@ class ErrorTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals([ 'path', 3, 'to', 'field' ], $e->path);
|
$this->assertEquals([ 'path', 3, 'to', 'field' ], $e->path);
|
||||||
$this->assertEquals(['message' => 'msg', 'path' => [ 'path', 3, 'to', 'field' ]], $e->toSerializableArray());
|
$this->assertEquals(['message' => 'msg', 'path' => [ 'path', 3, 'to', 'field' ]], $e->toSerializableArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @it default error formatter includes extension fields
|
||||||
|
*/
|
||||||
|
public function testDefaultErrorFormatterIncludesExtensionFields()
|
||||||
|
{
|
||||||
|
$e = new Error(
|
||||||
|
'msg',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
['foo' => 'bar']
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(['foo' => 'bar'], $e->getExtensions());
|
||||||
|
$this->assertEquals(['message' => 'msg', 'foo' => 'bar'], $e->toSerializableArray());
|
||||||
|
}
|
||||||
}
|
}
|
61
tests/Error/PrintErrorTest.php
Normal file
61
tests/Error/PrintErrorTest.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Tests\Error;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
|
use GraphQL\Error\FormattedError;
|
||||||
|
use GraphQL\Language\Parser;
|
||||||
|
use GraphQL\Language\Source;
|
||||||
|
|
||||||
|
class PrintErrorTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
// Describe printError
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @it prints an error with nodes from different sources
|
||||||
|
*/
|
||||||
|
public function testPrintsAnErrorWithNodesFromDifferentSources()
|
||||||
|
{
|
||||||
|
$sourceA = Parser::parse(new Source('type Foo {
|
||||||
|
field: String
|
||||||
|
}',
|
||||||
|
'SourceA'
|
||||||
|
));
|
||||||
|
|
||||||
|
$fieldTypeA = $sourceA->definitions[0]->fields[0]->type;
|
||||||
|
|
||||||
|
$sourceB = Parser::parse(new Source('type Foo {
|
||||||
|
field: Int
|
||||||
|
}',
|
||||||
|
'SourceB'
|
||||||
|
));
|
||||||
|
|
||||||
|
$fieldTypeB = $sourceB->definitions[0]->fields[0]->type;
|
||||||
|
|
||||||
|
|
||||||
|
$error = new Error(
|
||||||
|
'Example error with two nodes',
|
||||||
|
[
|
||||||
|
$fieldTypeA,
|
||||||
|
$fieldTypeB,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'Example error with two nodes
|
||||||
|
|
||||||
|
SourceA (2:10)
|
||||||
|
1: type Foo {
|
||||||
|
2: field: String
|
||||||
|
^
|
||||||
|
3: }
|
||||||
|
|
||||||
|
SourceB (2:10)
|
||||||
|
1: type Foo {
|
||||||
|
2: field: Int
|
||||||
|
^
|
||||||
|
3: }
|
||||||
|
',
|
||||||
|
FormattedError::printError($error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -87,9 +87,7 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase
|
|||||||
}
|
}
|
||||||
}';
|
}';
|
||||||
|
|
||||||
Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN);
|
|
||||||
$result = GraphQL::execute($schema, $query);
|
$result = GraphQL::execute($schema, $query);
|
||||||
Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN);
|
|
||||||
|
|
||||||
$expected = [
|
$expected = [
|
||||||
'data' => [
|
'data' => [
|
||||||
@ -174,9 +172,7 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase
|
|||||||
}
|
}
|
||||||
}';
|
}';
|
||||||
|
|
||||||
Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN);
|
|
||||||
$result = GraphQL::execute($schema, $query);
|
$result = GraphQL::execute($schema, $query);
|
||||||
Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN);
|
|
||||||
|
|
||||||
$expected = [
|
$expected = [
|
||||||
'data' => [
|
'data' => [
|
||||||
|
@ -965,14 +965,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @it fails to execute a query containing a type definition
|
* @it executes ignoring invalid non-executable definitions
|
||||||
*/
|
*/
|
||||||
public function testFailsToExecuteQueryContainingTypeDefinition()
|
public function testExecutesIgnoringInvalidNonExecutableDefinitions()
|
||||||
{
|
{
|
||||||
$query = Parser::parse('
|
$query = Parser::parse('
|
||||||
{ foo }
|
{ foo }
|
||||||
|
|
||||||
type Query { foo: String }
|
type Query { bar: String }
|
||||||
');
|
');
|
||||||
|
|
||||||
$schema = new Schema([
|
$schema = new Schema([
|
||||||
@ -988,12 +988,9 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
|||||||
$result = Executor::execute($schema, $query);
|
$result = Executor::execute($schema, $query);
|
||||||
|
|
||||||
$expected = [
|
$expected = [
|
||||||
'errors' => [
|
'data' => [
|
||||||
[
|
'foo' => null,
|
||||||
'message' => 'GraphQL cannot execute a request containing a ObjectTypeDefinition.',
|
],
|
||||||
'locations' => [['line' => 4, 'column' => 7]],
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->assertArraySubset($expected, $result->toArray());
|
$this->assertArraySubset($expected, $result->toArray());
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user