mirror of
https://github.com/retailcrm/graphql-php.git
synced 2025-02-06 07:49:24 +03:00
Server: batched queries with shared deferreds (promises) #105
This commit is contained in:
parent
24ffd605f4
commit
919cf80240
@ -1,16 +1,20 @@
|
||||
<?php
|
||||
namespace GraphQL\Server;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\FormattedError;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Error\UserError;
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
use GraphQL\Executor\Executor;
|
||||
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Utils\AST;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
|
||||
/**
|
||||
* Class Helper
|
||||
@ -21,7 +25,8 @@ use GraphQL\Utils\Utils;
|
||||
class Helper
|
||||
{
|
||||
/**
|
||||
* Executes GraphQL operation with given server configuration and returns execution result (or promise)
|
||||
* Executes GraphQL operation with given server configuration and returns execution result
|
||||
* (or promise when promise adapter is different from SyncPromiseAdapter)
|
||||
*
|
||||
* @param ServerConfig $config
|
||||
* @param OperationParams $op
|
||||
@ -29,36 +34,98 @@ class Helper
|
||||
* @return ExecutionResult|Promise
|
||||
*/
|
||||
public function executeOperation(ServerConfig $config, OperationParams $op)
|
||||
{
|
||||
$promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter();
|
||||
$result = $this->promiseToExecuteOperation($promiseAdapter, $config, $op);
|
||||
|
||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes batched GraphQL operations with shared promise queue
|
||||
* (thus, effectively batching deferreds|promises of all queries at once)
|
||||
*
|
||||
* @param ServerConfig $config
|
||||
* @param OperationParams[] $operations
|
||||
* @return ExecutionResult[]|Promise
|
||||
*/
|
||||
public function executeBatch(ServerConfig $config, array $operations)
|
||||
{
|
||||
$promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter();
|
||||
$result = [];
|
||||
|
||||
foreach ($operations as $operation) {
|
||||
$result[] = $this->promiseToExecuteOperation($promiseAdapter, $config, $operation);
|
||||
}
|
||||
|
||||
$result = $promiseAdapter->all($result);
|
||||
|
||||
// Wait for promised results when using sync promises
|
||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PromiseAdapter $promiseAdapter
|
||||
* @param ServerConfig $config
|
||||
* @param OperationParams $op
|
||||
* @return Promise
|
||||
*/
|
||||
private function promiseToExecuteOperation(PromiseAdapter $promiseAdapter, ServerConfig $config, OperationParams $op)
|
||||
{
|
||||
$phpErrors = [];
|
||||
$execute = function() use ($config, $op) {
|
||||
$doc = $op->queryId ? static::loadPersistedQuery($config, $op) : $op->query;
|
||||
|
||||
if (!$doc instanceof DocumentNode) {
|
||||
$doc = Parser::parse($doc);
|
||||
}
|
||||
if ($op->isReadOnly() && AST::isMutation($op->operation, $doc)) {
|
||||
throw new UserError("Cannot execute mutation in read-only context");
|
||||
}
|
||||
$execute = function() use ($config, $op, $promiseAdapter) {
|
||||
try {
|
||||
$doc = $op->queryId ? static::loadPersistedQuery($config, $op) : $op->query;
|
||||
|
||||
return GraphQL::executeAndReturnResult(
|
||||
$config->getSchema(),
|
||||
$doc,
|
||||
$config->getRootValue(),
|
||||
$config->getContext(),
|
||||
$op->variables,
|
||||
$op->operation,
|
||||
$config->getDefaultFieldResolver(),
|
||||
static::resolveValidationRules($config, $op),
|
||||
$config->getPromiseAdapter()
|
||||
);
|
||||
if (!$doc instanceof DocumentNode) {
|
||||
$doc = Parser::parse($doc);
|
||||
}
|
||||
if ($op->isReadOnly() && AST::isMutation($op->operation, $doc)) {
|
||||
throw new UserError("Cannot execute mutation in read-only context");
|
||||
}
|
||||
|
||||
$validationErrors = DocumentValidator::validate(
|
||||
$config->getSchema(),
|
||||
$doc,
|
||||
$this->resolveValidationRules($config, $op)
|
||||
);
|
||||
|
||||
if (!empty($validationErrors)) {
|
||||
return $promiseAdapter->createFulfilled(
|
||||
new ExecutionResult(null, $validationErrors)
|
||||
);
|
||||
} else {
|
||||
return Executor::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$config->getSchema(),
|
||||
$doc,
|
||||
$config->getRootValue(),
|
||||
$config->getContext(),
|
||||
$op->variables,
|
||||
$op->operation,
|
||||
$config->getDefaultFieldResolver()
|
||||
);
|
||||
}
|
||||
} catch (Error $e) {
|
||||
return $promiseAdapter->createFulfilled(
|
||||
new ExecutionResult(null, [$e])
|
||||
);
|
||||
}
|
||||
};
|
||||
if ($config->getDebug()) {
|
||||
$execute = Utils::withErrorHandling($execute, $phpErrors);
|
||||
}
|
||||
$result = $execute();
|
||||
|
||||
$applyErrorFormatting = function(ExecutionResult $result) use ($config, $phpErrors) {
|
||||
$applyErrorFormatting = function (ExecutionResult $result) use ($config, $phpErrors) {
|
||||
if ($config->getDebug()) {
|
||||
$errorFormatter = function($e) {
|
||||
return FormattedError::createFromException($e, true);
|
||||
@ -73,15 +140,15 @@ class Helper
|
||||
return $result;
|
||||
};
|
||||
|
||||
return $result instanceof Promise ?
|
||||
$result->then($applyErrorFormatting) :
|
||||
$applyErrorFormatting($result);
|
||||
return $result->then($applyErrorFormatting);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerConfig $config
|
||||
* @param OperationParams $op
|
||||
* @return string|DocumentNode
|
||||
* @return mixed
|
||||
* @throws Error
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public function loadPersistedQuery(ServerConfig $config, OperationParams $op)
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace GraphQL\Tests\Server;
|
||||
|
||||
use GraphQL\Deferred;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\UserError;
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
@ -67,6 +68,21 @@ class QueryExecutionTest extends \PHPUnit_Framework_TestCase
|
||||
'resolve' => function($root, $args) {
|
||||
return $args['arg'];
|
||||
}
|
||||
],
|
||||
'dfd' => [
|
||||
'type' => Type::string(),
|
||||
'args' => [
|
||||
'num' => [
|
||||
'type' => Type::nonNull(Type::int())
|
||||
],
|
||||
],
|
||||
'resolve' => function($root, $args, $context) {
|
||||
$context['buffer']($args['num']);
|
||||
|
||||
return new Deferred(function() use ($args, $context) {
|
||||
return $context['load']($args['num']);
|
||||
});
|
||||
}
|
||||
]
|
||||
]
|
||||
])
|
||||
@ -353,6 +369,117 @@ class QueryExecutionTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, $result->toArray());
|
||||
}
|
||||
|
||||
public function testExecutesBatchedQueries()
|
||||
{
|
||||
$batch = [
|
||||
[
|
||||
'query' => '{invalid}'
|
||||
],
|
||||
[
|
||||
'query' => '{f1,fieldWithException}'
|
||||
],
|
||||
[
|
||||
'query' => '
|
||||
query ($a: String!, $b: String!) {
|
||||
a: fieldWithArg(arg: $a)
|
||||
b: fieldWithArg(arg: $b)
|
||||
}
|
||||
',
|
||||
'variables' => ['a' => 'a', 'b' => 'b'],
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this->executeBatchedQuery($batch);
|
||||
|
||||
$expected = [
|
||||
[
|
||||
'errors' => [['message' => 'Cannot query field "invalid" on type "Query".']]
|
||||
],
|
||||
[
|
||||
'data' => [
|
||||
'f1' => 'f1',
|
||||
'fieldWithException' => null
|
||||
],
|
||||
'errors' => [
|
||||
['message' => 'This is the exception we want']
|
||||
]
|
||||
],
|
||||
[
|
||||
'data' => [
|
||||
'a' => 'a',
|
||||
'b' => 'b'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$this->assertArraySubset($expected[0], $result[0]->toArray());
|
||||
$this->assertArraySubset($expected[1], $result[1]->toArray());
|
||||
$this->assertArraySubset($expected[2], $result[2]->toArray());
|
||||
}
|
||||
|
||||
public function testDeferredsAreSharedAmongAllBatchedQueries()
|
||||
{
|
||||
$batch = [
|
||||
[
|
||||
'query' => '{dfd(num: 1)}'
|
||||
],
|
||||
[
|
||||
'query' => '{dfd(num: 2)}'
|
||||
],
|
||||
[
|
||||
'query' => '{dfd(num: 3)}',
|
||||
]
|
||||
];
|
||||
|
||||
$calls = [];
|
||||
|
||||
$this->config
|
||||
->setRootValue('1')
|
||||
->setContext([
|
||||
'buffer' => function($num) use (&$calls) {
|
||||
$calls[] = "buffer: $num";
|
||||
},
|
||||
'load' => function($num) use (&$calls) {
|
||||
$calls[] = "load: $num";
|
||||
return "loaded: $num";
|
||||
}
|
||||
]);
|
||||
|
||||
$result = $this->executeBatchedQuery($batch);
|
||||
|
||||
$expectedCalls = [
|
||||
'buffer: 1',
|
||||
'buffer: 2',
|
||||
'buffer: 3',
|
||||
'load: 1',
|
||||
'load: 2',
|
||||
'load: 3',
|
||||
];
|
||||
$this->assertEquals($expectedCalls, $calls);
|
||||
|
||||
$expected = [
|
||||
[
|
||||
'data' => [
|
||||
'dfd' => 'loaded: 1'
|
||||
]
|
||||
],
|
||||
[
|
||||
'data' => [
|
||||
'dfd' => 'loaded: 2'
|
||||
]
|
||||
],
|
||||
[
|
||||
'data' => [
|
||||
'dfd' => 'loaded: 3'
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected[0], $result[0]->toArray());
|
||||
$this->assertEquals($expected[1], $result[1]->toArray());
|
||||
$this->assertEquals($expected[2], $result[2]->toArray());
|
||||
}
|
||||
|
||||
private function executePersistedQuery($queryId, $variables = null)
|
||||
{
|
||||
$op = OperationParams::create(['queryId' => $queryId, 'variables' => $variables]);
|
||||
@ -371,6 +498,23 @@ class QueryExecutionTest extends \PHPUnit_Framework_TestCase
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function executeBatchedQuery(array $qs)
|
||||
{
|
||||
$batch = [];
|
||||
foreach ($qs as $params) {
|
||||
$batch[] = OperationParams::create($params, true);
|
||||
}
|
||||
$helper = new Helper();
|
||||
$result = $helper->executeBatch($this->config, $batch);
|
||||
$this->assertInternalType('array', $result);
|
||||
$this->assertCount(count($qs), $result);
|
||||
|
||||
foreach ($result as $index => $entry) {
|
||||
$this->assertInstanceOf(ExecutionResult::class, $entry, "Result at $index is not an instance of " . ExecutionResult::class);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function assertQueryResultEquals($expected, $query, $variables = null)
|
||||
{
|
||||
$result = $this->executeQuery($query, $variables);
|
||||
|
Loading…
x
Reference in New Issue
Block a user