mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-28 15:56:02 +03:00
Mapping Executor
This commit is contained in:
parent
aeb56d139a
commit
3edf6248b0
@ -278,11 +278,12 @@ Option | Type | Notes
|
||||
name | `string` | Required. Name of the field. If not set - GraphQL will look use `key` of fields array on type definition.
|
||||
type | `Type` or `callback() => Type` | Required. One of internal or custom types. Alternatively - callback that returns `type`.
|
||||
args | `array` | Array of possible type arguments. Each entry is expected to be an array with following keys: **name** (`string`), **type** (`Type` or `callback() => Type`), **defaultValue** (`any`)
|
||||
resolve | `callback($value, $args, ResolveInfo $info) => $data` | Function that receives `$value` describing parent type and returns `$data` for this field.
|
||||
resolve | `callback($value, $args, ResolveInfo $info) => $fieldValue` | Function that receives `$value` of parent type and returns value for this field. Mutually exclusive with `map`
|
||||
map | `callback($listOfValues, $args, ResolveInfo $info) => $fieldValues[]` | Function that receives list of parent type values and maps them to list of field values. Mutually exclusive with `resolve`
|
||||
description | `string` | Field description for clients
|
||||
deprecationReason | `string` | Text describing why this field is deprecated. When not empty - field will not be returned by introspection queries (unless forced)
|
||||
|
||||
The `resolve` option is exactly the place where your custom fetching logic lives.
|
||||
Use `map` or `resolve` for custom data fetching logic. `resolve` is easier to use, but `map` allows batching of queries to backend storage (for example you can use Redis MGET or IN(?) for SQL queries).
|
||||
|
||||
|
||||
### Schema
|
||||
|
@ -147,10 +147,12 @@ class Executor
|
||||
$fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
|
||||
|
||||
if ($operation->operation === 'mutation') {
|
||||
return self::executeFieldsSerially($exeContext, $type, $rootValue, $fields->getArrayCopy());
|
||||
$result = self::executeFieldsSerially($exeContext, $type, [$rootValue], $fields);
|
||||
} else {
|
||||
$result = self::executeFields($exeContext, $type, [$rootValue], $fields);
|
||||
}
|
||||
|
||||
return self::executeFields($exeContext, $type, $rootValue, $fields);
|
||||
return null === $result || $result === [] ? [] : $result[0];
|
||||
}
|
||||
|
||||
|
||||
@ -187,30 +189,40 @@ class Executor
|
||||
/**
|
||||
* Implements the "Evaluating selection sets" section of the spec
|
||||
* for "write" mode.
|
||||
*
|
||||
* @param ExecutionContext $exeContext
|
||||
* @param ObjectType $parentType
|
||||
* @param $sourceList
|
||||
* @param $sourceIsList
|
||||
* @param $fields
|
||||
* @return array
|
||||
* @throws Error
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceValue, $fields)
|
||||
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields)
|
||||
{
|
||||
$results = [];
|
||||
foreach ($fields as $responseName => $fieldASTs) {
|
||||
$result = self::resolveField($exeContext, $parentType, $sourceValue, $fieldASTs);
|
||||
self::resolveField($exeContext, $parentType, $sourceList, $fieldASTs, $responseName, $results);
|
||||
}
|
||||
|
||||
if ($result !== self::$UNDEFINED) {
|
||||
// Undefined means that field is not defined in schema
|
||||
$results[$responseName] = $result;
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the "Evaluating selection sets" section of the spec
|
||||
* for "read" mode.
|
||||
* @param ExecutionContext $exeContext
|
||||
* @param ObjectType $parentType
|
||||
* @param $sourceList
|
||||
* @param $fields
|
||||
* @return array
|
||||
*/
|
||||
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $source, $fields)
|
||||
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields)
|
||||
{
|
||||
// Native PHP doesn't support promises.
|
||||
// Custom executor should be built for platforms like ReactPHP
|
||||
return self::executeFieldsSerially($exeContext, $parentType, $source, $fields);
|
||||
return self::executeFieldsSerially($exeContext, $parentType, $sourceList, $fields);
|
||||
}
|
||||
|
||||
|
||||
@ -345,12 +357,21 @@ class Executor
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the field on the given source object. In particular, this
|
||||
* figures out the value that the field returns by calling its resolve function,
|
||||
* then calls completeValue to complete promises, serialize scalars, or execute
|
||||
* the sub-selection-set for objects.
|
||||
* Given list of parent type values returns corresponding list of field values
|
||||
*
|
||||
* In particular, this
|
||||
* figures out the value that the field returns by calling its `resolve` or `map` function,
|
||||
* then calls `completeValue` on each value to serialize scalars, or execute the sub-selection-set
|
||||
* for objects.
|
||||
*
|
||||
* @param ExecutionContext $exeContext
|
||||
* @param ObjectType $parentType
|
||||
* @param $sourceValueList
|
||||
* @param $fieldASTs
|
||||
* @return array
|
||||
* @throws Error
|
||||
*/
|
||||
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldASTs)
|
||||
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $sourceValueList, $fieldASTs, $responseName, &$resolveResult)
|
||||
{
|
||||
$fieldAST = $fieldASTs[0];
|
||||
$fieldName = $fieldAST->name->value;
|
||||
@ -358,19 +379,11 @@ class Executor
|
||||
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
|
||||
|
||||
if (!$fieldDef) {
|
||||
return self::$UNDEFINED;
|
||||
return ;
|
||||
}
|
||||
|
||||
$returnType = $fieldDef->getType();
|
||||
|
||||
if (isset($fieldDef->resolve)) {
|
||||
$resolveFn = $fieldDef->resolve;
|
||||
} else if (isset($parentType->resolveField)) {
|
||||
$resolveFn = $parentType->resolveField;
|
||||
} else {
|
||||
$resolveFn = self::$defaultResolveFn;
|
||||
}
|
||||
|
||||
// Build hash of arguments from the field.arguments AST, using the
|
||||
// variables scope to fulfill any variable references.
|
||||
// TODO: find a way to memoize, in case this field is within a List type.
|
||||
@ -394,12 +407,22 @@ class Executor
|
||||
'variableValues' => $exeContext->variableValues,
|
||||
]);
|
||||
|
||||
// If an error occurs while calling the field `resolve` function, ensure that
|
||||
$mapFn = $fieldDef->mapFn;
|
||||
|
||||
// If an error occurs while calling the field `map` or `resolve` function, ensure that
|
||||
// it is wrapped as a GraphQLError with locations. Log this error and return
|
||||
// null if allowed, otherwise throw the error so the parent field can handle
|
||||
// it.
|
||||
if ($mapFn) {
|
||||
try {
|
||||
$result = call_user_func($resolveFn, $source, $args, $info);
|
||||
$mapped = call_user_func($mapFn, $sourceValueList, $args, $info);
|
||||
|
||||
Utils::invariant(
|
||||
is_array($mapped) && count($mapped) === count($sourceValueList),
|
||||
"Function `map` of $parentType.$fieldName is expected to return array " .
|
||||
"with exact same number of items as list being mapped (first argument of `map`)"
|
||||
);
|
||||
|
||||
} catch (\Exception $error) {
|
||||
$reportedError = Error::createLocatedError($error, $fieldASTs);
|
||||
|
||||
@ -411,14 +434,48 @@ class Executor
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::completeValueCatchingError(
|
||||
foreach ($mapped as $index => $value) {
|
||||
$resolveResult[$index][$responseName] = self::completeValueCatchingError(
|
||||
$exeContext,
|
||||
$returnType,
|
||||
$fieldASTs,
|
||||
$info,
|
||||
$result
|
||||
$value
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (isset($fieldDef->resolveFn)) {
|
||||
$resolveFn = $fieldDef->resolveFn;
|
||||
} else if (isset($parentType->resolveFieldFn)) {
|
||||
$resolveFn = $parentType->resolveFieldFn;
|
||||
} else {
|
||||
$resolveFn = self::$defaultResolveFn;
|
||||
}
|
||||
|
||||
foreach ($sourceValueList as $index => $value) {
|
||||
try {
|
||||
$resolved = call_user_func($resolveFn, $value, $args, $info);
|
||||
} catch (\Exception $error) {
|
||||
$reportedError = Error::createLocatedError($error, $fieldASTs);
|
||||
|
||||
if ($returnType instanceof NonNull) {
|
||||
throw $reportedError;
|
||||
}
|
||||
|
||||
$exeContext->addError($reportedError);
|
||||
$resolved = null;
|
||||
}
|
||||
|
||||
$resolveResult[$index][$responseName] = self::completeValueCatchingError(
|
||||
$exeContext,
|
||||
$returnType,
|
||||
$fieldASTs,
|
||||
$info,
|
||||
$resolved
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function completeValueCatchingError(
|
||||
@ -489,14 +546,100 @@ class Executor
|
||||
return null;
|
||||
}
|
||||
|
||||
// If field type is List, complete each item in the list with the inner type
|
||||
// If field type is Scalar or Enum, serialize to a valid value, returning
|
||||
// null if serialization is not possible.
|
||||
if ($returnType instanceof ScalarType ||
|
||||
$returnType instanceof EnumType) {
|
||||
return $returnType->serialize($result);
|
||||
}
|
||||
|
||||
// If field type is List, and return type is Composite - complete by executing these fields with list value as parameter
|
||||
if ($returnType instanceof ListOfType) {
|
||||
$itemType = $returnType->getWrappedType();
|
||||
|
||||
Utils::invariant(
|
||||
is_array($result) || $result instanceof \Traversable,
|
||||
'User Error: expected iterable, but did not find one.'
|
||||
);
|
||||
|
||||
// For Object[]:
|
||||
// Allow all object fields to process list value in it's `map` callback:
|
||||
if ($itemType instanceof ObjectType) {
|
||||
// Filter out nulls (as `map` doesn't expect it):
|
||||
$list = [];
|
||||
foreach ($result as $index => $item) {
|
||||
if (null !== $item) {
|
||||
$list[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$subFieldASTs = self::collectSubFields($exeContext, $itemType, $fieldASTs);
|
||||
$mapped = self::executeFields($exeContext, $itemType, $list, $subFieldASTs);
|
||||
|
||||
$i = 0;
|
||||
$completed = [];
|
||||
foreach ($result as $index => $item) {
|
||||
if (null === $item) {
|
||||
// Complete nulls separately
|
||||
$completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
|
||||
} else {
|
||||
// Assuming same order of mapped values
|
||||
$completed[] = $mapped[$i++];
|
||||
}
|
||||
}
|
||||
return $completed;
|
||||
|
||||
} else if ($itemType instanceof AbstractType) {
|
||||
|
||||
// Values sharded by ObjectType
|
||||
$listPerObjectType = [];
|
||||
|
||||
// Helper structures to restore ordering after resolve calls
|
||||
$resultTypeMap = [];
|
||||
$typeNameMap = [];
|
||||
$cursors = [];
|
||||
|
||||
foreach ($result as $index => $item) {
|
||||
if (null !== $item) {
|
||||
$objectType = $itemType->getObjectType($item, $info);
|
||||
|
||||
if ($objectType && !$itemType->isPossibleType($objectType)) {
|
||||
$exeContext->addError(new Error(
|
||||
"Runtime Object type \"$objectType\" is not a possible type for \"$itemType\"."
|
||||
));
|
||||
$result[$index] = null;
|
||||
} else {
|
||||
$listPerObjectType[$objectType->name][] = $item;
|
||||
$resultTypeMap[$index] = $objectType->name;
|
||||
$typeNameMap[$objectType->name] = $objectType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$mapped = [];
|
||||
foreach ($listPerObjectType as $typeName => $list) {
|
||||
$objectType = $typeNameMap[$typeName];
|
||||
$subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs);
|
||||
$mapped[$typeName] = self::executeFields($exeContext, $objectType, $list, $subFieldASTs);
|
||||
$cursors[$typeName] = 0;
|
||||
}
|
||||
|
||||
// Restore order:
|
||||
$completed = [];
|
||||
foreach ($result as $index => $item) {
|
||||
if (null === $item) {
|
||||
// Complete nulls separately
|
||||
$completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
|
||||
} else {
|
||||
$typeName = $resultTypeMap[$index];
|
||||
$completed[] = $mapped[$typeName][$cursors[$typeName]++];
|
||||
}
|
||||
}
|
||||
|
||||
return $completed;
|
||||
} else {
|
||||
|
||||
// For simple lists:
|
||||
$tmp = [];
|
||||
foreach ($result as $item) {
|
||||
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
|
||||
@ -504,17 +647,11 @@ class Executor
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
// If field type is Scalar or Enum, serialize to a valid value, returning
|
||||
// null if serialization is not possible.
|
||||
if ($returnType instanceof ScalarType ||
|
||||
$returnType instanceof EnumType) {
|
||||
Utils::invariant(method_exists($returnType, 'serialize'), 'Missing serialize method on type');
|
||||
return $returnType->serialize($result);
|
||||
}
|
||||
|
||||
// Field type must be Object, Interface or Union and expect sub-selections.
|
||||
if ($returnType instanceof ObjectType) {
|
||||
$objectType = $returnType;
|
||||
|
||||
} else if ($returnType instanceof AbstractType) {
|
||||
$objectType = $returnType->getObjectType($result, $info);
|
||||
|
||||
@ -542,6 +679,18 @@ class Executor
|
||||
}
|
||||
|
||||
// Collect sub-fields to execute to complete this value.
|
||||
$subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs);
|
||||
return self::executeFields($exeContext, $objectType, [$result], $subFieldASTs)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExecutionContext $exeContext
|
||||
* @param ObjectType $objectType
|
||||
* @param $fieldASTs
|
||||
* @return \ArrayObject
|
||||
*/
|
||||
private static function collectSubFields(ExecutionContext $exeContext, ObjectType $objectType, $fieldASTs)
|
||||
{
|
||||
$subFieldASTs = new \ArrayObject();
|
||||
$visitedFragmentNames = new \ArrayObject();
|
||||
for ($i = 0; $i < count($fieldASTs); $i++) {
|
||||
@ -556,11 +705,9 @@ class Executor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return self::executeFields($exeContext, $objectType, $result, $subFieldASTs);
|
||||
return $subFieldASTs;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If a resolve function is not given, then a default resolve behavior is used
|
||||
* which takes the property of the source object of the same name as the field
|
||||
|
@ -23,13 +23,20 @@ class FieldDefinition
|
||||
public $args;
|
||||
|
||||
/**
|
||||
* source?: any,
|
||||
* args?: ?{[argName: string]: any},
|
||||
* info
|
||||
* Callback for resolving field value given parent value.
|
||||
* Mutually exclusive with `map`
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
public $resolve;
|
||||
public $resolveFn;
|
||||
|
||||
/**
|
||||
* Callback for mapping list of parent values to list of field values.
|
||||
* Mutually exclusive with `resolve`
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
public $mapFn;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
@ -62,6 +69,7 @@ class FieldDefinition
|
||||
'defaultValue' => Config::ANY
|
||||
], Config::KEY_AS_NAME),
|
||||
'resolve' => Config::CALLBACK,
|
||||
'map' => Config::CALLBACK,
|
||||
'description' => Config::STRING,
|
||||
'deprecationReason' => Config::STRING,
|
||||
]);
|
||||
@ -97,7 +105,8 @@ class FieldDefinition
|
||||
{
|
||||
$this->name = $config['name'];
|
||||
$this->type = $config['type'];
|
||||
$this->resolve = isset($config['resolve']) ? $config['resolve'] : null;
|
||||
$this->resolveFn = isset($config['resolve']) ? $config['resolve'] : null;
|
||||
$this->mapFn = isset($config['map']) ? $config['map'] : null;
|
||||
$this->args = isset($config['args']) ? FieldArgument::createMap($config['args']) : [];
|
||||
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
|
@ -26,7 +26,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
||||
/**
|
||||
* @var callback
|
||||
*/
|
||||
private $_resolveType;
|
||||
private $_resolveTypeFn;
|
||||
|
||||
/**
|
||||
* Update the interfaces to know about this implementation.
|
||||
@ -52,14 +52,14 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
||||
FieldDefinition::getDefinition(),
|
||||
Config::KEY_AS_NAME
|
||||
),
|
||||
'resolveType' => Config::CALLBACK,
|
||||
'resolveType' => Config::CALLBACK, // function($value, ResolveInfo $info) => ObjectType
|
||||
'description' => Config::STRING
|
||||
]);
|
||||
|
||||
$this->name = $config['name'];
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
$this->_fields = !empty($config['fields']) ? FieldDefinition::createMap($config['fields']) : [];
|
||||
$this->_resolveType = isset($config['resolveType']) ? $config['resolveType'] : null;
|
||||
$this->_resolveTypeFn = isset($config['resolveType']) ? $config['resolveType'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,7 +104,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
||||
*/
|
||||
public function getObjectType($value, ResolveInfo $info)
|
||||
{
|
||||
$resolver = $this->_resolveType;
|
||||
$resolver = $this->_resolveTypeFn;
|
||||
return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this);
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ class ObjectType extends Type implements OutputType, CompositeType
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
public $resolveField;
|
||||
public $resolveFieldFn;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
@ -77,7 +77,7 @@ class ObjectType extends Type implements OutputType, CompositeType
|
||||
|
||||
$this->name = $config['name'];
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
$this->resolveField = isset($config['resolveField']) ? $config['resolveField'] : null;
|
||||
$this->resolveFieldFn = isset($config['resolveField']) ? $config['resolveField'] : null;
|
||||
$this->_config = $config;
|
||||
|
||||
if (isset($config['interfaces'])) {
|
||||
|
@ -25,7 +25,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
||||
Config::validate($config, [
|
||||
'name' => Config::STRING | Config::REQUIRED,
|
||||
'types' => Config::arrayOf(Config::OBJECT_TYPE | Config::REQUIRED),
|
||||
'resolveType' => Config::CALLBACK,
|
||||
'resolveType' => Config::CALLBACK, // function($value, ResolveInfo $info) => ObjectType
|
||||
'description' => Config::STRING
|
||||
]);
|
||||
|
||||
@ -66,16 +66,19 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
||||
$this->_possibleTypeNames[$possibleType->name] = true;
|
||||
}
|
||||
}
|
||||
return $this->_possibleTypeNames[$type->name] === true;
|
||||
return isset($this->_possibleTypeNames[$type->name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $value
|
||||
* @param ResolveInfo $info
|
||||
*
|
||||
* @return Type
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getObjectType($value, ResolveInfo $info)
|
||||
{
|
||||
$resolver = $this->_resolveType;
|
||||
return $resolver ? call_user_func($resolver, $value) : Type::getTypeOf($value, $info, $this);
|
||||
return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this);
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,12 @@ use GraphQL\FormattedError;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\Config;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Utils;
|
||||
|
||||
class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
@ -164,11 +167,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
'c' => ['type' => Type::string(), 'resolve' => function () {
|
||||
return 'Cherry';
|
||||
}],
|
||||
'deep' => ['type' => function () use (&$Type) {
|
||||
'deep' => [
|
||||
'type' => function () use (&$Type) {
|
||||
return $Type;
|
||||
}, 'resolve' => function () {
|
||||
},
|
||||
'resolve' => function () {
|
||||
return [];
|
||||
}]
|
||||
}
|
||||
]
|
||||
]
|
||||
]);
|
||||
$schema = new Schema($Type);
|
||||
@ -503,72 +509,264 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $result->toArray());
|
||||
|
||||
/*
|
||||
var query = parse('{ field(a: true, c: false, e: 0) }');
|
||||
var result = await execute(schema, query);
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
data: {
|
||||
field: '{"a":true,"c":false,"e":0}'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when an isTypeOf check is not met', async () => {
|
||||
class Special {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
class NotSpecial {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
public function testExecutesMapCallbacksIfSet()
|
||||
{
|
||||
$fooData = [
|
||||
['field' => '1'],
|
||||
['field' => null],
|
||||
null,
|
||||
['field' => '4'],
|
||||
];
|
||||
|
||||
var SpecialType = new GraphQLObjectType({
|
||||
name: 'SpecialType',
|
||||
isTypeOf(obj) {
|
||||
return obj instanceof Special;
|
||||
},
|
||||
fields: {
|
||||
value: { type: GraphQLString }
|
||||
}
|
||||
});
|
||||
$foo = new ObjectType([
|
||||
'name' => 'Foo',
|
||||
'fields' => [
|
||||
'field' => [
|
||||
'type' => Type::string(),
|
||||
'map' => function($listOfFoo, $args, $resolveInfo) use ($fooData) {
|
||||
|
||||
var schema = new GraphQLSchema({
|
||||
query: new GraphQLObjectType({
|
||||
name: 'Query',
|
||||
fields: {
|
||||
specials: {
|
||||
type: new GraphQLList(SpecialType),
|
||||
resolve: rootValue => rootValue.specials
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
var query = parse('{ specials { value } }');
|
||||
var value = {
|
||||
specials: [ new Special('foo'), new NotSpecial('bar') ]
|
||||
};
|
||||
var result = await execute(schema, query, value);
|
||||
|
||||
expect(result.data).to.deep.equal({
|
||||
specials: [
|
||||
{ value: 'foo' },
|
||||
null
|
||||
]
|
||||
});
|
||||
expect(result.errors).to.have.lengthOf(1);
|
||||
expect(result.errors).to.containSubset([
|
||||
{ message:
|
||||
'Expected value of type "SpecialType" but got: [object Object].',
|
||||
locations: [ { line: 1, column: 3 } ] }
|
||||
return Utils::map($listOfFoo, function($fooData) use ($args, $resolveInfo) {
|
||||
return json_encode([
|
||||
'value' => $fooData['field'] === null ? null : $fooData['field'] . 'x',
|
||||
'args' => $args,
|
||||
'gotResolveInfo' => $resolveInfo instanceof ResolveInfo
|
||||
]);
|
||||
});
|
||||
*/
|
||||
},
|
||||
'args' => [
|
||||
'a' => ['type' => Type::boolean()],
|
||||
'b' => ['type' => Type::boolean()],
|
||||
'c' => ['type' => Type::int()]
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$bar = new ObjectType([
|
||||
'name' => 'Bar',
|
||||
'fields' => [
|
||||
'foo' => [
|
||||
'type' => Type::listOf($foo),
|
||||
'resolve' => function() use ($fooData) {
|
||||
return $fooData;
|
||||
}
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema($bar);
|
||||
|
||||
$query = Parser::parse('{ foo { field(a: true, c: 0) } }');
|
||||
$result = Executor::execute($schema, $query);
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
'foo' => [
|
||||
['field' => '{"value":"1x","args":{"a":true,"c":0},"gotResolveInfo":true}'],
|
||||
['field' => '{"value":null,"args":{"a":true,"c":0},"gotResolveInfo":true}'],
|
||||
null,
|
||||
['field' => '{"value":"4x","args":{"a":true,"c":0},"gotResolveInfo":true}'],
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $result->toArray());
|
||||
}
|
||||
|
||||
public function testRespectsListsOfAbstractTypeWhenResolvingViaMap()
|
||||
{
|
||||
$type1 = null;
|
||||
$type2 = null;
|
||||
$type3 = null;
|
||||
|
||||
$resolveType = function($value) use (&$type1, &$type2, &$type3) {
|
||||
switch ($value['type']) {
|
||||
case 'Type1':
|
||||
return $type1;
|
||||
case 'Type2':
|
||||
return $type2;
|
||||
case 'Type3':
|
||||
default:
|
||||
return $type3;
|
||||
}
|
||||
};
|
||||
|
||||
$mapValues = function($typeValues, $args) {
|
||||
return Utils::map($typeValues, function($value) use ($args) {
|
||||
if (array_key_exists('foo', $value)) {
|
||||
return json_encode([
|
||||
'value' => $value,
|
||||
'args' => $args,
|
||||
]);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$interface = new InterfaceType([
|
||||
'name' => 'SomeInterface',
|
||||
'fields' => [
|
||||
'foo' => ['type' => Type::string()],
|
||||
],
|
||||
'resolveType' => $resolveType
|
||||
]);
|
||||
|
||||
$type1 = new ObjectType([
|
||||
'name' => 'Type1',
|
||||
'fields' => [
|
||||
'foo' => [
|
||||
'type' => Type::string(),
|
||||
'map' => $mapValues
|
||||
]
|
||||
],
|
||||
'interfaces' => [$interface]
|
||||
]);
|
||||
|
||||
$type2 = new ObjectType([
|
||||
'name' => 'Type2',
|
||||
'fields' => [
|
||||
'foo' => [
|
||||
'type' => Type::string(),
|
||||
'map' => $mapValues
|
||||
]
|
||||
],
|
||||
'interfaces' => [$interface]
|
||||
]);
|
||||
|
||||
$type3 = new ObjectType([
|
||||
'name' => 'Type3',
|
||||
'fields' => [
|
||||
'bar' => [
|
||||
'type' => Type::listOf(Type::string()),
|
||||
'map' => function($type3Values, $args) {
|
||||
return Utils::map($type3Values, function($value) use ($args) {
|
||||
return [
|
||||
json_encode([
|
||||
'value' => $value,
|
||||
'args' => $args
|
||||
])
|
||||
];
|
||||
});
|
||||
}
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$union = new UnionType([
|
||||
'name' => 'SomeUnion',
|
||||
'types' => [$type1, $type3],
|
||||
'resolveType' => $resolveType
|
||||
]);
|
||||
|
||||
$complexType = new ObjectType([
|
||||
'name' => 'ComplexType',
|
||||
'fields' => [
|
||||
'iface' => [
|
||||
'type' => $interface
|
||||
],
|
||||
'ifaceList' => [
|
||||
'type' => Type::listOf($interface)
|
||||
],
|
||||
'union' => [
|
||||
'type' => $union
|
||||
],
|
||||
'unionList' => [
|
||||
'type' => Type::listOf($union)
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$type1values = [
|
||||
['type' => 'Type1', 'foo' => 'str1'],
|
||||
['type' => 'Type1'],
|
||||
['type' => 'Type1', 'foo' => null],
|
||||
];
|
||||
|
||||
$type2values = [
|
||||
['type' => 'Type2', 'foo' => 'str1'],
|
||||
['type' => 'Type2', 'foo' => null],
|
||||
['type' => 'Type2'],
|
||||
];
|
||||
|
||||
$type3values = [
|
||||
['type' => 'Type3', 'bar' => ['str1', 'str2']],
|
||||
['type' => 'Type3', 'bar' => null],
|
||||
];
|
||||
|
||||
$complexTypeValues = [
|
||||
'iface' => $type1values[0],
|
||||
'ifaceList' => array_merge($type1values, $type2values),
|
||||
'union' => $type3values[0],
|
||||
'unionList' => array_merge($type1values, $type3values)
|
||||
];
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
'test' => [
|
||||
'iface' => ['foo' => json_encode(['value' => $type1values[0], 'args' => []])],
|
||||
'ifaceList' => [
|
||||
['foo' => '{"value":{"type":"Type1","foo":"str1"},"args":[]}'],
|
||||
['foo' => null],
|
||||
['foo' => '{"value":{"type":"Type1","foo":null},"args":[]}'],
|
||||
['foo' => '{"value":{"type":"Type2","foo":"str1"},"args":[]}'],
|
||||
['foo' => '{"value":{"type":"Type2","foo":null},"args":[]}'],
|
||||
['foo' => null],
|
||||
],
|
||||
'union' => [
|
||||
'bar' => ['{"value":{"type":"Type3","bar":["str1","str2"]},"args":[]}']
|
||||
],
|
||||
'unionList' => [
|
||||
['foo' => '{"value":{"type":"Type1","foo":"str1"},"args":[]}'],
|
||||
['foo' => null],
|
||||
['foo' => '{"value":{"type":"Type1","foo":null},"args":[]}'],
|
||||
['bar' => ['{"value":{"type":"Type3","bar":["str1","str2"]},"args":[]}']],
|
||||
['bar' => ['{"value":{"type":"Type3","bar":null},"args":[]}']],
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$schema = new Schema(new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
'test' => [
|
||||
'type' => $complexType,
|
||||
'resolve' => function() use ($complexTypeValues) {
|
||||
return $complexTypeValues;
|
||||
}
|
||||
]
|
||||
]
|
||||
]));
|
||||
|
||||
$query = '{
|
||||
test {
|
||||
iface{foo},
|
||||
ifaceList{foo}
|
||||
union {
|
||||
... on Type1 {
|
||||
foo
|
||||
}
|
||||
... on Type3 {
|
||||
bar
|
||||
}
|
||||
}
|
||||
unionList {
|
||||
... on Type1 {
|
||||
foo
|
||||
}
|
||||
... on Type3 {
|
||||
bar
|
||||
}
|
||||
}
|
||||
}
|
||||
}';
|
||||
|
||||
$query = Parser::parse($query);
|
||||
$result = Executor::execute($schema, $query);
|
||||
|
||||
$this->assertEquals($expected, $result->toArray());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user