Mapping Executor

This commit is contained in:
vladar 2015-09-01 01:44:03 +06:00
parent aeb56d139a
commit 3edf6248b0
7 changed files with 499 additions and 141 deletions

View File

@ -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. 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`. 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`) 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 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) 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 ### Schema

View File

@ -147,10 +147,12 @@ class Executor
$fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject()); $fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
if ($operation->operation === 'mutation') { 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 * Implements the "Evaluating selection sets" section of the spec
* for "write" mode. * 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 = []; $results = [];
foreach ($fields as $responseName => $fieldASTs) { 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; return $results;
} }
/** /**
* Implements the "Evaluating selection sets" section of the spec * Implements the "Evaluating selection sets" section of the spec
* for "read" mode. * 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. // Native PHP doesn't support promises.
// Custom executor should be built for platforms like ReactPHP // 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 * Given list of parent type values returns corresponding list of field values
* figures out the value that the field returns by calling its resolve function, *
* then calls completeValue to complete promises, serialize scalars, or execute * In particular, this
* the sub-selection-set for objects. * 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]; $fieldAST = $fieldASTs[0];
$fieldName = $fieldAST->name->value; $fieldName = $fieldAST->name->value;
@ -358,19 +379,11 @@ class Executor
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName); $fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
if (!$fieldDef) { if (!$fieldDef) {
return self::$UNDEFINED; return ;
} }
$returnType = $fieldDef->getType(); $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 // Build hash of arguments from the field.arguments AST, using the
// variables scope to fulfill any variable references. // variables scope to fulfill any variable references.
// TODO: find a way to memoize, in case this field is within a List type. // TODO: find a way to memoize, in case this field is within a List type.
@ -394,30 +407,74 @@ class Executor
'variableValues' => $exeContext->variableValues, '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 // 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 // null if allowed, otherwise throw the error so the parent field can handle
// it. // it.
try { if ($mapFn) {
$result = call_user_func($resolveFn, $source, $args, $info); try {
} catch (\Exception $error) { $mapped = call_user_func($mapFn, $sourceValueList, $args, $info);
$reportedError = Error::createLocatedError($error, $fieldASTs);
if ($returnType instanceof NonNull) { Utils::invariant(
throw $reportedError; 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);
if ($returnType instanceof NonNull) {
throw $reportedError;
}
$exeContext->addError($reportedError);
return null;
} }
$exeContext->addError($reportedError); foreach ($mapped as $index => $value) {
return null; $resolveResult[$index][$responseName] = self::completeValueCatchingError(
} $exeContext,
$returnType,
$fieldASTs,
$info,
$value
);
}
} else {
if (isset($fieldDef->resolveFn)) {
$resolveFn = $fieldDef->resolveFn;
} else if (isset($parentType->resolveFieldFn)) {
$resolveFn = $parentType->resolveFieldFn;
} else {
$resolveFn = self::$defaultResolveFn;
}
return self::completeValueCatchingError( foreach ($sourceValueList as $index => $value) {
$exeContext, try {
$returnType, $resolved = call_user_func($resolveFn, $value, $args, $info);
$fieldASTs, } catch (\Exception $error) {
$info, $reportedError = Error::createLocatedError($error, $fieldASTs);
$result
); if ($returnType instanceof NonNull) {
throw $reportedError;
}
$exeContext->addError($reportedError);
$resolved = null;
}
$resolveResult[$index][$responseName] = self::completeValueCatchingError(
$exeContext,
$returnType,
$fieldASTs,
$info,
$resolved
);
}
}
} }
@ -489,32 +546,112 @@ class Executor
return null; 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) { if ($returnType instanceof ListOfType) {
$itemType = $returnType->getWrappedType(); $itemType = $returnType->getWrappedType();
Utils::invariant( Utils::invariant(
is_array($result) || $result instanceof \Traversable, is_array($result) || $result instanceof \Traversable,
'User Error: expected iterable, but did not find one.' 'User Error: expected iterable, but did not find one.'
); );
$tmp = []; // For Object[]:
foreach ($result as $item) { // Allow all object fields to process list value in it's `map` callback:
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item); 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);
}
return $tmp;
} }
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) { if ($returnType instanceof ObjectType) {
$objectType = $returnType; $objectType = $returnType;
} else if ($returnType instanceof AbstractType) { } else if ($returnType instanceof AbstractType) {
$objectType = $returnType->getObjectType($result, $info); $objectType = $returnType->getObjectType($result, $info);
@ -542,6 +679,18 @@ class Executor
} }
// Collect sub-fields to execute to complete this value. // 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(); $subFieldASTs = new \ArrayObject();
$visitedFragmentNames = new \ArrayObject(); $visitedFragmentNames = new \ArrayObject();
for ($i = 0; $i < count($fieldASTs); $i++) { for ($i = 0; $i < count($fieldASTs); $i++) {
@ -556,11 +705,9 @@ class Executor
); );
} }
} }
return $subFieldASTs;
return self::executeFields($exeContext, $objectType, $result, $subFieldASTs);
} }
/** /**
* If a resolve function is not given, then a default resolve behavior is used * 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 * which takes the property of the source object of the same name as the field

View File

@ -23,13 +23,20 @@ class FieldDefinition
public $args; public $args;
/** /**
* source?: any, * Callback for resolving field value given parent value.
* args?: ?{[argName: string]: any}, * Mutually exclusive with `map`
* info
* *
* @var callable * @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 * @var string|null
@ -62,6 +69,7 @@ class FieldDefinition
'defaultValue' => Config::ANY 'defaultValue' => Config::ANY
], Config::KEY_AS_NAME), ], Config::KEY_AS_NAME),
'resolve' => Config::CALLBACK, 'resolve' => Config::CALLBACK,
'map' => Config::CALLBACK,
'description' => Config::STRING, 'description' => Config::STRING,
'deprecationReason' => Config::STRING, 'deprecationReason' => Config::STRING,
]); ]);
@ -97,7 +105,8 @@ class FieldDefinition
{ {
$this->name = $config['name']; $this->name = $config['name'];
$this->type = $config['type']; $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->args = isset($config['args']) ? FieldArgument::createMap($config['args']) : [];
$this->description = isset($config['description']) ? $config['description'] : null; $this->description = isset($config['description']) ? $config['description'] : null;

View File

@ -26,7 +26,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
/** /**
* @var callback * @var callback
*/ */
private $_resolveType; private $_resolveTypeFn;
/** /**
* Update the interfaces to know about this implementation. * Update the interfaces to know about this implementation.
@ -52,14 +52,14 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
FieldDefinition::getDefinition(), FieldDefinition::getDefinition(),
Config::KEY_AS_NAME Config::KEY_AS_NAME
), ),
'resolveType' => Config::CALLBACK, 'resolveType' => Config::CALLBACK, // function($value, ResolveInfo $info) => ObjectType
'description' => Config::STRING 'description' => Config::STRING
]); ]);
$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->_fields = !empty($config['fields']) ? FieldDefinition::createMap($config['fields']) : []; $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) 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); return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this);
} }
} }

View File

@ -69,7 +69,7 @@ class ObjectType extends Type implements OutputType, CompositeType
/** /**
* @var callable * @var callable
*/ */
public $resolveField; public $resolveFieldFn;
public function __construct(array $config) public function __construct(array $config)
{ {
@ -77,7 +77,7 @@ class ObjectType extends Type implements OutputType, CompositeType
$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->resolveField = isset($config['resolveField']) ? $config['resolveField'] : null; $this->resolveFieldFn = isset($config['resolveField']) ? $config['resolveField'] : null;
$this->_config = $config; $this->_config = $config;
if (isset($config['interfaces'])) { if (isset($config['interfaces'])) {

View File

@ -25,7 +25,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
Config::validate($config, [ Config::validate($config, [
'name' => Config::STRING | Config::REQUIRED, 'name' => Config::STRING | Config::REQUIRED,
'types' => Config::arrayOf(Config::OBJECT_TYPE | Config::REQUIRED), 'types' => Config::arrayOf(Config::OBJECT_TYPE | Config::REQUIRED),
'resolveType' => Config::CALLBACK, 'resolveType' => Config::CALLBACK, // function($value, ResolveInfo $info) => ObjectType
'description' => Config::STRING 'description' => Config::STRING
]); ]);
@ -66,16 +66,19 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
$this->_possibleTypeNames[$possibleType->name] = true; $this->_possibleTypeNames[$possibleType->name] = true;
} }
} }
return $this->_possibleTypeNames[$type->name] === true; return isset($this->_possibleTypeNames[$type->name]);
} }
/** /**
* @param ObjectType $value * @param ObjectType $value
* @param ResolveInfo $info
*
* @return Type * @return Type
* @throws \Exception
*/ */
public function getObjectType($value, ResolveInfo $info) public function getObjectType($value, ResolveInfo $info)
{ {
$resolver = $this->_resolveType; $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);
} }
} }

View File

@ -6,9 +6,12 @@ use GraphQL\FormattedError;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\Config; use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Utils;
class ExecutorTest extends \PHPUnit_Framework_TestCase class ExecutorTest extends \PHPUnit_Framework_TestCase
{ {
@ -164,11 +167,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
'c' => ['type' => Type::string(), 'resolve' => function () { 'c' => ['type' => Type::string(), 'resolve' => function () {
return 'Cherry'; return 'Cherry';
}], }],
'deep' => ['type' => function () use (&$Type) { 'deep' => [
return $Type; 'type' => function () use (&$Type) {
}, 'resolve' => function () { return $Type;
return []; },
}] 'resolve' => function () {
return [];
}
]
] ]
]); ]);
$schema = new Schema($Type); $schema = new Schema($Type);
@ -503,72 +509,264 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
]; ];
$this->assertEquals($expected, $result->toArray()); $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 { public function testExecutesMapCallbacksIfSet()
constructor(value) { {
this.value = value; $fooData = [
} ['field' => '1'],
['field' => null],
null,
['field' => '4'],
];
$foo = new ObjectType([
'name' => 'Foo',
'fields' => [
'field' => [
'type' => Type::string(),
'map' => function($listOfFoo, $args, $resolveInfo) use ($fooData) {
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());
} }
var SpecialType = new GraphQLObjectType({ public function testRespectsListsOfAbstractTypeWhenResolvingViaMap()
name: 'SpecialType', {
isTypeOf(obj) { $type1 = null;
return obj instanceof Special; $type2 = null;
}, $type3 = null;
fields: {
value: { type: GraphQLString }
}
});
var schema = new GraphQLSchema({ $resolveType = function($value) use (&$type1, &$type2, &$type3) {
query: new GraphQLObjectType({ switch ($value['type']) {
name: 'Query', case 'Type1':
fields: { return $type1;
specials: { case 'Type2':
type: new GraphQLList(SpecialType), return $type2;
resolve: rootValue => rootValue.specials case 'Type3':
} default:
} return $type3;
}) }
}); };
var query = parse('{ specials { value } }'); $mapValues = function($typeValues, $args) {
var value = { return Utils::map($typeValues, function($value) use ($args) {
specials: [ new Special('foo'), new NotSpecial('bar') ] if (array_key_exists('foo', $value)) {
}; return json_encode([
var result = await execute(schema, query, value); 'value' => $value,
'args' => $args,
]);
} else {
return null;
}
});
};
expect(result.data).to.deep.equal({ $interface = new InterfaceType([
specials: [ 'name' => 'SomeInterface',
{ value: 'foo' }, 'fields' => [
null 'foo' => ['type' => Type::string()],
] ],
}); 'resolveType' => $resolveType
expect(result.errors).to.have.lengthOf(1); ]);
expect(result.errors).to.containSubset([
{ message: $type1 = new ObjectType([
'Expected value of type "SpecialType" but got: [object Object].', 'name' => 'Type1',
locations: [ { line: 1, column: 3 } ] } '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());
} }
} }