Ability for interface types to resolve type asynchronously

This commit is contained in:
Vladimir Razuvaev 2017-07-03 23:08:20 +07:00
parent 445f579f09
commit d64c352262
2 changed files with 788 additions and 11 deletions

View File

@ -969,9 +969,59 @@ class Executor
$runtimeType = $returnType->resolveType($result, $exeContext->contextValue, $info);
if (null === $runtimeType) {
$runtimeType = self::inferTypeOf($result, $exeContext->contextValue, $info, $returnType);
$runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
}
if ($this->promises->isThenable($runtimeType)) {
$runtimeType = $this->promises->convertThenable($runtimeType);
Utils::invariant($runtimeType instanceof Promise);
}
if ($runtimeType instanceof Promise) {
return $runtimeType->then(function($resolvedRuntimeType) use ($returnType, $fieldNodes, $info, $path, &$result) {
return $this->validateRuntimeTypeAndCompleteObjectValue(
$returnType,
$fieldNodes,
$info,
$path,
$resolvedRuntimeType,
$result
);
});
}
return $this->validateRuntimeTypeAndCompleteObjectValue(
$returnType,
$fieldNodes,
$info,
$path,
$runtimeType,
$result
);
}
/**
* @param AbstractType $returnType
* @param FieldNode[] $fieldNodes
* @param ResolveInfo $info
* @param array $path
* @param mixed $returnedRuntimeType
* @param $result
* @return array|Promise|\stdClass
* @throws Error
*/
private function validateRuntimeTypeAndCompleteObjectValue(
AbstractType $returnType,
$fieldNodes,
ResolveInfo $info,
$path,
$returnedRuntimeType,
&$result
)
{
$exeContext = $this->exeContext;
$runtimeType = $returnedRuntimeType;
// If resolveType returns a string, we assume it's a ObjectType name.
if (is_string($runtimeType)) {
$runtimeType = $exeContext->schema->getType($runtimeType);
@ -1064,12 +1114,61 @@ class Executor
*/
private function completeObjectValue(ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
{
$exeContext = $this->exeContext;
$isTypeOf = $returnType->isTypeOf($result, $this->exeContext->contextValue, $info);
// If there is an isTypeOf predicate function, call it with the
// current result. If isTypeOf returns false, then raise an error rather
// than continuing execution.
if (false === $returnType->isTypeOf($result, $exeContext->contextValue, $info)) {
if (null === $isTypeOf) {
return $this->validateResultTypeAndExecuteFields(
$returnType,
$fieldNodes,
$info,
$path,
$result,
true
);
}
if ($this->promises->isThenable($isTypeOf)) {
/** @var Promise $isTypeOf */
$isTypeOf = $this->promises->convertThenable($isTypeOf);
Utils::invariant($isTypeOf instanceof Promise);
return $isTypeOf->then(function($isTypeOfResult) use ($returnType, $fieldNodes, $info, $path, &$result) {
return $this->validateResultTypeAndExecuteFields(
$returnType,
$fieldNodes,
$info,
$path,
$result,
$isTypeOfResult
);
});
}
return $this->validateResultTypeAndExecuteFields($returnType, $fieldNodes, $info, $path, $result, $isTypeOf);
}
/**
* @param ObjectType $returnType
* @param FieldNode[] $fieldNodes
* @param ResolveInfo $info
* @param array $path
* @param array $result
* @param bool $isTypeOfResult
* @return array|Promise|\stdClass
* @throws Error
*/
private function validateResultTypeAndExecuteFields(
ObjectType $returnType,
$fieldNodes,
ResolveInfo $info,
$path,
&$result,
$isTypeOfResult
)
{
// If isTypeOf returns false, then raise an error
// rather than continuing execution.
if (false === $isTypeOfResult) {
throw new Error(
"Expected value of type $returnType but got: " . Utils::getVariableType($result),
$fieldNodes
@ -1095,23 +1194,47 @@ class Executor
}
/**
* Infer type of the value using isTypeOf of corresponding AbstractType
* 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
* isTypeOf for the object being coerced, returning the first type that matches.
*
* @param $value
* @param $context
* @param ResolveInfo $info
* @param AbstractType $abstractType
* @return ObjectType|null
* @return ObjectType|Promise|null
*/
private static function inferTypeOf($value, $context, ResolveInfo $info, AbstractType $abstractType)
private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractType $abstractType)
{
$possibleTypes = $info->schema->getPossibleTypes($abstractType);
$promisedIsTypeOfResults = [];
$promisedIsTypeOfResultTypes = [];
foreach ($possibleTypes as $type) {
if ($type->isTypeOf($value, $context, $info)) {
return $type;
$isTypeOfResult = $type->isTypeOf($value, $context, $info);
if (null !== $isTypeOfResult) {
if ($this->promises->isThenable($isTypeOfResult)) {
$promisedIsTypeOfResults[] = $this->promises->convertThenable($isTypeOfResult);
$promisedIsTypeOfResultTypes[] = $type;
} else if ($isTypeOfResult) {
return $type;
}
}
}
if (!empty($promisedIsTypeOfResults)) {
return $this->promises->all($promisedIsTypeOfResults)
->then(function($isTypeOfResults) use ($promisedIsTypeOfResultTypes) {
foreach ($isTypeOfResults as $index => $result) {
if ($result) {
return $promisedIsTypeOfResultTypes[$index];
}
}
return null;
});
}
return null;
}

View File

@ -0,0 +1,654 @@
<?php
namespace GraphQL\Tests\Executor;
use GraphQL\Deferred;
use GraphQL\GraphQL;
use GraphQL\Schema;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
require_once __DIR__ . '/TestClasses.php';
class AbstractPromiseTest extends \PHPUnit_Framework_TestCase
{
// DESCRIBE: Execute: Handles execution of abstract types with promises
/**
* @it isTypeOf used to resolve runtime type for Interface
*/
public function testIsTypeOfUsedToResolveRuntimeTypeForInterface()
{
$PetType = new InterfaceType([
'name' => 'Pet',
'fields' => [
'name' => [ 'type' => Type::string() ]
]
]);
$DogType = new ObjectType([
'name' => 'Dog',
'interfaces' => [ $PetType ],
'isTypeOf' => function($obj) {
return new Deferred(function() use ($obj) {
return $obj instanceof Dog;
});
},
'fields' => [
'name' => [ 'type' => Type::string() ],
'woofs' => [ 'type' => Type::boolean() ],
]
]);
$CatType = new ObjectType([
'name' => 'Cat',
'interfaces' => [ $PetType ],
'isTypeOf' => function($obj) {
return new Deferred(function() use ($obj) {
return $obj instanceof Cat;
});
},
'fields' => [
'name' => [ 'type' => Type::string() ],
'meows' => [ 'type' => Type::boolean() ],
]
]);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($PetType),
'resolve' => function() {
return [
new Dog('Odie', true),
new Cat('Garfield', false)
];
}
]
]
]),
'types' => [ $CatType, $DogType ]
]);
$query = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
$result = GraphQL::execute($schema, $query);
$expected = [
'data' => [
'pets' => [
[ 'name' => 'Odie', 'woofs' => true ],
[ 'name' => 'Garfield', 'meows' => false ]
]
]
];
$this->assertEquals($expected, $result);
}
/**
* @it isTypeOf can be rejected
*/
public function testIsTypeOfCanBeRejected()
{
$PetType = new InterfaceType([
'name' => 'Pet',
'fields' => [
'name' => ['type' => Type::string()]
]
]);
$DogType = new ObjectType([
'name' => 'Dog',
'interfaces' => [$PetType],
'isTypeOf' => function () {
return new Deferred(function () {
throw new \Exception('We are testing this error');
});
},
'fields' => [
'name' => ['type' => Type::string()],
'woofs' => ['type' => Type::boolean()],
]
]);
$CatType = new ObjectType([
'name' => 'Cat',
'interfaces' => [$PetType],
'isTypeOf' => function ($obj) {
return new Deferred(function () use ($obj) {
return $obj instanceof Cat;
});
},
'fields' => [
'name' => ['type' => Type::string()],
'meows' => ['type' => Type::boolean()],
]
]);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($PetType),
'resolve' => function () {
return [
new Dog('Odie', true),
new Cat('Garfield', false)
];
}
]
]
]),
'types' => [$CatType, $DogType]
]);
$query = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
$result = GraphQL::execute($schema, $query);
$expected = [
'data' => [
'pets' => [null, null]
],
'errors' => [
[
'message' => 'We are testing this error',
'locations' => [['line' => 2, 'column' => 7]],
'path' => ['pets', 0]
],
[
'message' => 'We are testing this error',
'locations' => [['line' => 2, 'column' => 7]],
'path' => ['pets', 1]
]
]
];
$this->assertEquals($expected, $result);
}
/**
* @it isTypeOf used to resolve runtime type for Union
*/
public function testIsTypeOfUsedToResolveRuntimeTypeForUnion()
{
$DogType = new ObjectType([
'name' => 'Dog',
'isTypeOf' => function ($obj) {
return new Deferred(function () use ($obj) {
return $obj instanceof Dog;
});
},
'fields' => [
'name' => ['type' => Type::string()],
'woofs' => ['type' => Type::boolean()],
]
]);
$CatType = new ObjectType([
'name' => 'Cat',
'isTypeOf' => function ($obj) {
return new Deferred(function () use ($obj) {
return $obj instanceof Cat;
});
},
'fields' => [
'name' => ['type' => Type::string()],
'meows' => ['type' => Type::boolean()],
]
]);
$PetType = new UnionType([
'name' => 'Pet',
'types' => [$DogType, $CatType]
]);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($PetType),
'resolve' => function () {
return [new Dog('Odie', true), new Cat('Garfield', false)];
}
]
]
])
]);
$query = '{
pets {
... on Dog {
name
woofs
}
... on Cat {
name
meows
}
}
}';
$result = GraphQL::execute($schema, $query);
$expected = [
'data' => [
'pets' => [
['name' => 'Odie', 'woofs' => true],
['name' => 'Garfield', 'meows' => false]
]
]
];
$this->assertEquals($expected, $result);
}
/**
* @it resolveType on Interface yields useful error
*/
public function testResolveTypeOnInterfaceYieldsUsefulError()
{
$PetType = new InterfaceType([
'name' => 'Pet',
'resolveType' => function ($obj) use (&$DogType, &$CatType, &$HumanType) {
return new Deferred(function () use ($obj, $DogType, $CatType, $HumanType) {
if ($obj instanceof Dog) {
return $DogType;
}
if ($obj instanceof Cat) {
return $CatType;
}
if ($obj instanceof Human) {
return $HumanType;
}
return null;
});
},
'fields' => [
'name' => ['type' => Type::string()]
]
]);
$HumanType = new ObjectType([
'name' => 'Human',
'fields' => [
'name' => ['type' => Type::string()],
]
]);
$DogType = new ObjectType([
'name' => 'Dog',
'interfaces' => [$PetType],
'fields' => [
'name' => ['type' => Type::string()],
'woofs' => ['type' => Type::boolean()],
]
]);
$CatType = new ObjectType([
'name' => 'Cat',
'interfaces' => [$PetType],
'fields' => [
'name' => ['type' => Type::string()],
'meows' => ['type' => Type::boolean()],
]
]);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($PetType),
'resolve' => function () {
return new Deferred(function () {
return [
new Dog('Odie', true),
new Cat('Garfield', false),
new Human('Jon')
];
});
}
]
]
]),
'types' => [$CatType, $DogType]
]);
$query = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
$result = GraphQL::execute($schema, $query);
$expected = [
'data' => [
'pets' => [
['name' => 'Odie', 'woofs' => true],
['name' => 'Garfield', 'meows' => false],
null
]
],
'errors' => [
[
'message' => 'Runtime Object type "Human" is not a possible type for "Pet".',
'locations' => [['line' => 2, 'column' => 7]],
'path' => ['pets', 2]
],
]
];
$this->assertEquals($expected, $result);
}
/**
* @it resolveType on Union yields useful error
*/
public function testResolveTypeOnUnionYieldsUsefulError()
{
$HumanType = new ObjectType([
'name' => 'Human',
'fields' => [
'name' => ['type' => Type::string()],
]
]);
$DogType = new ObjectType([
'name' => 'Dog',
'fields' => [
'name' => ['type' => Type::string()],
'woofs' => ['type' => Type::boolean()],
]
]);
$CatType = new ObjectType([
'name' => 'Cat',
'fields' => [
'name' => ['type' => Type::string()],
'meows' => ['type' => Type::boolean()],
]
]);
$PetType = new UnionType([
'name' => 'Pet',
'resolveType' => function ($obj) use ($DogType, $CatType, $HumanType) {
return new Deferred(function () use ($obj, $DogType, $CatType, $HumanType) {
if ($obj instanceof Dog) {
return $DogType;
}
if ($obj instanceof Cat) {
return $CatType;
}
if ($obj instanceof Human) {
return $HumanType;
}
return null;
});
},
'types' => [$DogType, $CatType]
]);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($PetType),
'resolve' => function () {
return [
new Dog('Odie', true),
new Cat('Garfield', false),
new Human('Jon')
];
}
]
]
])
]);
$query = '{
pets {
... on Dog {
name
woofs
}
... on Cat {
name
meows
}
}
}';
$result = GraphQL::execute($schema, $query);
$expected = [
'data' => [
'pets' => [
['name' => 'Odie', 'woofs' => true],
['name' => 'Garfield', 'meows' => false],
null
]
],
'errors' => [
[
'message' => 'Runtime Object type "Human" is not a possible type for "Pet".',
'locations' => [['line' => 2, 'column' => 7]],
'path' => ['pets', 2]
]
]
];
$this->assertEquals($expected, $result);
}
/**
* @it resolveType allows resolving with type name
*/
public function testResolveTypeAllowsResolvingWithTypeName()
{
$PetType = new InterfaceType([
'name' => 'Pet',
'resolveType' => function ($obj) {
return new Deferred(function () use ($obj) {
if ($obj instanceof Dog) {
return 'Dog';
}
if ($obj instanceof Cat) {
return 'Cat';
}
return null;
});
},
'fields' => [
'name' => ['type' => Type::string()]
]
]);
$DogType = new ObjectType([
'name' => 'Dog',
'interfaces' => [$PetType],
'fields' => [
'name' => ['type' => Type::string()],
'woofs' => ['type' => Type::boolean()],
]
]);
$CatType = new ObjectType([
'name' => 'Cat',
'interfaces' => [$PetType],
'fields' => [
'name' => ['type' => Type::string()],
'meows' => ['type' => Type::boolean()],
]
]);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($PetType),
'resolve' => function () {
return [
new Dog('Odie', true),
new Cat('Garfield', false)
];
}
]
]
]),
'types' => [$CatType, $DogType]
]);
$query = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
$result = GraphQL::execute($schema, $query);
$expected = [
'data' => [
'pets' => [
['name' => 'Odie', 'woofs' => true],
['name' => 'Garfield', 'meows' => false],
]
]
];
$this->assertEquals($expected, $result);
}
/**
* @it resolveType can be caught
*/
public function testResolveTypeCanBeCaught()
{
$PetType = new InterfaceType([
'name' => 'Pet',
'resolveType' => function () {
return new Deferred(function () {
throw new \Exception('We are testing this error');
});
},
'fields' => [
'name' => ['type' => Type::string()]
]
]);
$DogType = new ObjectType([
'name' => 'Dog',
'interfaces' => [$PetType],
'fields' => [
'name' => ['type' => Type::string()],
'woofs' => ['type' => Type::boolean()],
]
]);
$CatType = new ObjectType([
'name' => 'Cat',
'interfaces' => [$PetType],
'fields' => [
'name' => ['type' => Type::string()],
'meows' => ['type' => Type::boolean()],
]
]);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($PetType),
'resolve' => function () {
return [
new Dog('Odie', true),
new Cat('Garfield', false)
];
}
]
]
]),
'types' => [$CatType, $DogType]
]);
$query = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
$result = GraphQL::execute($schema, $query);
$expected = [
'data' => [
'pets' => [null, null]
],
'errors' => [
[
'message' => 'We are testing this error',
'locations' => [['line' => 2, 'column' => 7]],
'path' => ['pets', 0]
],
[
'message' => 'We are testing this error',
'locations' => [['line' => 2, 'column' => 7]],
'path' => ['pets', 1]
]
]
];
$this->assertEquals($expected, $result);
}
}