Async resolution fixes
This commit is contained in:
parent
5b1f973b45
commit
dbb49ac7bf
8 changed files with 172 additions and 158 deletions
|
@ -491,5 +491,10 @@ return [
|
||||||
'name' => Exception::DOMAIN_VERIFICATION_FAILED,
|
'name' => Exception::DOMAIN_VERIFICATION_FAILED,
|
||||||
'description' => 'Domain verification for the requested domain has failed.',
|
'description' => 'Domain verification for the requested domain has failed.',
|
||||||
'code' => 401,
|
'code' => 401,
|
||||||
]
|
],
|
||||||
|
Exception::GRAPHQL_NO_QUERY => [
|
||||||
|
'name' => Exception::GRAPHQL_NO_QUERY,
|
||||||
|
'description' => 'Query is required and can be provided via parameter or as the raw body if the content-type header is application/graphql.',
|
||||||
|
'code' => 400,
|
||||||
|
],
|
||||||
];
|
];
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Appwrite\GraphQL\Builder;
|
use Appwrite\Extend\Exception;
|
||||||
use Appwrite\Utopia\Response;
|
use Appwrite\Utopia\Response;
|
||||||
use GraphQL\Error\DebugFlag;
|
use GraphQL\Error\DebugFlag;
|
||||||
use GraphQL\GraphQL;
|
use GraphQL\GraphQL;
|
||||||
|
@ -56,8 +56,6 @@ App::post('/v1/graphql')
|
||||||
->param('variables', [], new JSON(), 'Variables to use in the operation', true)
|
->param('variables', [], new JSON(), 'Variables to use in the operation', true)
|
||||||
->inject('request')
|
->inject('request')
|
||||||
->inject('response')
|
->inject('response')
|
||||||
->inject('utopia')
|
|
||||||
->inject('dbForProject')
|
|
||||||
->inject('promiseAdapter')
|
->inject('promiseAdapter')
|
||||||
->inject('gqlSchema')
|
->inject('gqlSchema')
|
||||||
->action(Closure::fromCallable('graphqlRequest'));
|
->action(Closure::fromCallable('graphqlRequest'));
|
||||||
|
@ -71,8 +69,6 @@ function graphqlRequest(
|
||||||
$variables,
|
$variables,
|
||||||
$request,
|
$request,
|
||||||
$response,
|
$response,
|
||||||
$utopia,
|
|
||||||
$dbForProject,
|
|
||||||
$promiseAdapter,
|
$promiseAdapter,
|
||||||
$gqlSchema
|
$gqlSchema
|
||||||
)
|
)
|
||||||
|
@ -89,24 +85,23 @@ function graphqlRequest(
|
||||||
$query = $request->getSwoole()->rawContent();
|
$query = $request->getSwoole()->rawContent();
|
||||||
}
|
}
|
||||||
if (empty($query)) {
|
if (empty($query)) {
|
||||||
throw new Exception('Query is empty', Response::STATUS_CODE_BAD_REQUEST);
|
throw new Exception('No query supplied.', 400, Exception::GRAPHQL_NO_QUERY);
|
||||||
}
|
}
|
||||||
|
|
||||||
$debugFlags = App::isDevelopment()
|
$debugFlags = App::isDevelopment()
|
||||||
? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE | DebugFlag::RETHROW_INTERNAL_EXCEPTIONS
|
? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE | DebugFlag::RETHROW_INTERNAL_EXCEPTIONS
|
||||||
: DebugFlag::NONE;
|
: DebugFlag::NONE;
|
||||||
|
|
||||||
$validations = array_merge(
|
// Roughly equivalent to 200 REST requests of work per GraphQL request
|
||||||
GraphQL::getStandardValidationRules(),
|
$maxComplexity = App::getEnv('_APP_GRAPHQL_MAX_QUERY_COMPLEXITY', 200);
|
||||||
[
|
|
||||||
new QueryComplexity(App::getEnv('_APP_GRAPHQL_MAX_QUERY_COMPLEXITY', 200)),
|
|
||||||
new QueryDepth(App::getEnv('_APP_GRAPHQL_MAX_QUERY_DEPTH', 3)),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (App::isProduction()) {
|
// Maximum nested query depth. Limited to 3 as we don't have more than 3 levels of data relationships
|
||||||
|
$maxDepth = App::getEnv('_APP_GRAPHQL_MAX_QUERY_DEPTH', 3);
|
||||||
|
|
||||||
|
$validations = GraphQL::getStandardValidationRules();
|
||||||
|
$validations[] = new QueryComplexity($maxComplexity);
|
||||||
|
$validations[] = new QueryDepth($maxDepth);
|
||||||
$validations[] = new DisableIntrospection();
|
$validations[] = new DisableIntrospection();
|
||||||
}
|
|
||||||
|
|
||||||
$promise = GraphQL::promiseToExecute(
|
$promise = GraphQL::promiseToExecute(
|
||||||
$promiseAdapter,
|
$promiseAdapter,
|
||||||
|
@ -117,10 +112,11 @@ function graphqlRequest(
|
||||||
validationRules: $validations
|
validationRules: $validations
|
||||||
);
|
);
|
||||||
|
|
||||||
// Blocking wait while queries resolve asynchronously
|
// Blocking wait while queries resolve
|
||||||
$wg = new WaitGroup();
|
$wg = new WaitGroup();
|
||||||
$wg->add();
|
$wg->add();
|
||||||
$promise->then(function ($result) use ($response, $debugFlags, $wg) {
|
$promise->then(
|
||||||
|
function ($result) use ($response, $debugFlags, $wg) {
|
||||||
$result = $result->toArray($debugFlags);
|
$result = $result->toArray($debugFlags);
|
||||||
\var_dump("Result:" . $result);
|
\var_dump("Result:" . $result);
|
||||||
if (isset($result['errors'])) {
|
if (isset($result['errors'])) {
|
||||||
|
@ -134,6 +130,7 @@ function graphqlRequest(
|
||||||
function ($error) use ($response, $wg) {
|
function ($error) use ($response, $wg) {
|
||||||
$response->text(\json_encode(['errors' => [\json_encode($error)]]));
|
$response->text(\json_encode(['errors' => [\json_encode($error)]]));
|
||||||
$wg->done();
|
$wg->done();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
$wg->wait();
|
$wg->wait();
|
||||||
}
|
}
|
||||||
|
|
|
@ -865,7 +865,7 @@ App::setResource('promiseAdapter', function ($register) {
|
||||||
return $register->get('promiseAdapter');
|
return $register->get('promiseAdapter');
|
||||||
}, ['register']);
|
}, ['register']);
|
||||||
|
|
||||||
App::setResource('gqlSchema', function ($utopia, $request, $response, $register, $dbForProject) {
|
App::setResource('gqlSchema', function ($utopia, $request, $response, $register, $dbForProject, $user) {
|
||||||
return Builder::buildSchema($utopia, $request, $response, $register, $dbForProject);
|
return Builder::buildSchema($utopia, $request, $response, $register, $dbForProject, $user);
|
||||||
}, ['utopia', 'request', 'response', 'register', 'dbForProject']);
|
}, ['utopia', 'request', 'response', 'register', 'dbForProject', 'user']);
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ class Exception extends \Exception
|
||||||
* - Keys
|
* - Keys
|
||||||
* - Platform
|
* - Platform
|
||||||
* - Domain
|
* - Domain
|
||||||
|
* - GraphQL
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** General */
|
/** General */
|
||||||
|
@ -160,6 +161,8 @@ class Exception extends \Exception
|
||||||
const DOMAIN_ALREADY_EXISTS = 'domain_already_exists';
|
const DOMAIN_ALREADY_EXISTS = 'domain_already_exists';
|
||||||
const DOMAIN_VERIFICATION_FAILED = 'domain_verification_failed';
|
const DOMAIN_VERIFICATION_FAILED = 'domain_verification_failed';
|
||||||
|
|
||||||
|
/** GraphqQL */
|
||||||
|
const GRAPHQL_NO_QUERY = 'graphql_no_query';
|
||||||
|
|
||||||
private $type = '';
|
private $type = '';
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,13 @@ use Appwrite\GraphQL\Types\JsonType;
|
||||||
use Appwrite\Utopia\Request;
|
use Appwrite\Utopia\Request;
|
||||||
use Appwrite\Utopia\Response;
|
use Appwrite\Utopia\Response;
|
||||||
use Appwrite\Utopia\Response\Model;
|
use Appwrite\Utopia\Response\Model;
|
||||||
|
use Appwrite\Utopia\Response\Model\User;
|
||||||
use GraphQL\Error\Error;
|
use GraphQL\Error\Error;
|
||||||
use GraphQL\Error\FormattedError;
|
use GraphQL\Error\FormattedError;
|
||||||
use GraphQL\Type\Definition\ObjectType;
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
use GraphQL\Type\Schema;
|
use GraphQL\Type\Schema;
|
||||||
|
use Swoole\Coroutine\WaitGroup;
|
||||||
use Utopia\App;
|
use Utopia\App;
|
||||||
use Utopia\CLI\Console;
|
use Utopia\CLI\Console;
|
||||||
use Utopia\Database\Database;
|
use Utopia\Database\Database;
|
||||||
|
@ -20,6 +22,8 @@ use Utopia\Registry\Registry;
|
||||||
use Utopia\Route;
|
use Utopia\Route;
|
||||||
use Utopia\Validator;
|
use Utopia\Validator;
|
||||||
|
|
||||||
|
use function \Co\go;
|
||||||
|
|
||||||
class Builder
|
class Builder
|
||||||
{
|
{
|
||||||
protected static ?JsonType $jsonParser = null;
|
protected static ?JsonType $jsonParser = null;
|
||||||
|
@ -100,7 +104,7 @@ class Builder
|
||||||
$fields[$escapedKey] = [
|
$fields[$escapedKey] = [
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
'description' => $props['description'],
|
'description' => $props['description'],
|
||||||
'resolve' => fn ($object, $args, $context, $info) => new CoroutinePromise(
|
'resolve' => fn($object, $args, $context, $info) => new CoroutinePromise(
|
||||||
fn($resolve, $reject) => $resolve($object[$key])
|
fn($resolve, $reject) => $resolve($object[$key])
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
@ -268,11 +272,12 @@ class Builder
|
||||||
Request $request,
|
Request $request,
|
||||||
Response $response,
|
Response $response,
|
||||||
Registry &$register,
|
Registry &$register,
|
||||||
Database $dbForProject
|
Database $dbForProject,
|
||||||
|
Document $user,
|
||||||
): Schema
|
): Schema
|
||||||
{
|
{
|
||||||
$apiSchema = self::buildAPISchema($utopia, $request, $response, $register);
|
$apiSchema = self::buildAPISchema($utopia, $request, $response, $register);
|
||||||
$db = self::buildCollectionsSchema($utopia, $request, $response, $dbForProject);
|
$db = self::buildCollectionsSchema($utopia, $request, $response, $dbForProject, $user);
|
||||||
|
|
||||||
$queryFields = \array_merge_recursive($apiSchema['query'], $db['query']);
|
$queryFields = \array_merge_recursive($apiSchema['query'], $db['query']);
|
||||||
$mutationFields = \array_merge_recursive($apiSchema['mutation'], $db['mutation']);
|
$mutationFields = \array_merge_recursive($apiSchema['mutation'], $db['mutation']);
|
||||||
|
@ -307,23 +312,27 @@ class Builder
|
||||||
App $utopia,
|
App $utopia,
|
||||||
Request $request,
|
Request $request,
|
||||||
Response $response,
|
Response $response,
|
||||||
Database $dbForProject
|
Database $dbForProject,
|
||||||
|
?Document $user = null,
|
||||||
): array
|
): array
|
||||||
{
|
{
|
||||||
$start = microtime(true);
|
$start = microtime(true);
|
||||||
|
|
||||||
|
$userId = $user?->getId();
|
||||||
$collections = [];
|
$collections = [];
|
||||||
$queryFields = [];
|
$queryFields = [];
|
||||||
$mutationFields = [];
|
$mutationFields = [];
|
||||||
$limit = 1000 * swoole_cpu_num();
|
$limit = 1000;
|
||||||
$offset = 0;
|
$offset = 0;
|
||||||
|
$wg = new WaitGroup();
|
||||||
|
|
||||||
while (!empty($attrs = Authorization::skip(fn() => $dbForProject->find(
|
while (!empty($attrs = Authorization::skip(fn() => $dbForProject->find(
|
||||||
'attributes',
|
'attributes',
|
||||||
limit: $limit,
|
limit: $limit,
|
||||||
offset: $offset
|
offset: $offset
|
||||||
)))) {
|
)))) {
|
||||||
//go(function() use ($utopia, $request, $response, $dbForProject, &$collections, &$queryFields, &$mutationFields, $limit, &$offset, $attrs) {
|
$wg->add();
|
||||||
|
go(function () use ($utopia, $request, $response, $dbForProject, &$collections, &$queryFields, &$mutationFields, $limit, &$offset, $attrs, $userId, $wg) {
|
||||||
foreach ($attrs as $attr) {
|
foreach ($attrs as $attr) {
|
||||||
$collectionId = $attr->getAttribute('collectionId');
|
$collectionId = $attr->getAttribute('collectionId');
|
||||||
if ($attr->getAttribute('status') !== 'available') {
|
if ($attr->getAttribute('status') !== 'available') {
|
||||||
|
@ -336,7 +345,7 @@ class Builder
|
||||||
$escapedKey = str_replace('$', '_', $key);
|
$escapedKey = str_replace('$', '_', $key);
|
||||||
$collections[$collectionId][$escapedKey] = [
|
$collections[$collectionId][$escapedKey] = [
|
||||||
'type' => self::getAttributeArgType($type, $array, $required),
|
'type' => self::getAttributeArgType($type, $array, $required),
|
||||||
'resolve' => fn ($object, $args, $context, $info) => $object->then(function ($obj) use ($key) {
|
'resolve' => fn($object, $args, $context, $info) => $object->then(function ($obj) use ($key) {
|
||||||
return $obj['result'][$key];
|
return $obj['result'][$key];
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
@ -377,12 +386,14 @@ class Builder
|
||||||
|
|
||||||
$attributes['read'] = [
|
$attributes['read'] = [
|
||||||
'type' => Type::listOf(Type::string()),
|
'type' => Type::listOf(Type::string()),
|
||||||
|
'defaultValue' => ["user:$userId"],
|
||||||
'resolve' => function ($object, $args, $context, $info) use ($collectionId) {
|
'resolve' => function ($object, $args, $context, $info) use ($collectionId) {
|
||||||
return $object->getAttribute('$read');
|
return $object->getAttribute('$read');
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
$attributes['write'] = [
|
$attributes['write'] = [
|
||||||
'type' => Type::listOf(Type::string()),
|
'type' => Type::listOf(Type::string()),
|
||||||
|
'defaultValue' => ["user:$userId"],
|
||||||
'resolve' => function ($object, $args, $context, $info) use ($collectionId) {
|
'resolve' => function ($object, $args, $context, $info) use ($collectionId) {
|
||||||
return $object->getAttribute('$write');
|
return $object->getAttribute('$write');
|
||||||
}
|
}
|
||||||
|
@ -414,11 +425,11 @@ class Builder
|
||||||
'resolve' => self::mutateDelete($collectionId, $dbForProject)
|
'resolve' => self::mutateDelete($collectionId, $dbForProject)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
//});
|
$wg->done();
|
||||||
|
});
|
||||||
$offset += $limit;
|
$offset += $limit;
|
||||||
}
|
}
|
||||||
|
$wg->wait();
|
||||||
$time_elapsed_secs = (microtime(true) - $start) * 1000;
|
$time_elapsed_secs = (microtime(true) - $start) * 1000;
|
||||||
Console::info("[INFO] Built GraphQL Project Collection Schema in ${time_elapsed_secs}ms");
|
Console::info("[INFO] Built GraphQL Project Collection Schema in ${time_elapsed_secs}ms");
|
||||||
|
|
||||||
|
@ -455,6 +466,7 @@ class Builder
|
||||||
$utopia
|
$utopia
|
||||||
->setRoute($route)
|
->setRoute($route)
|
||||||
->execute($route, $request);
|
->execute($route, $request);
|
||||||
|
|
||||||
$resolve($response->getPayload());
|
$resolve($response->getPayload());
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$reject($e);
|
$reject($e);
|
||||||
|
|
|
@ -25,9 +25,6 @@ class CoroutinePromise
|
||||||
if (\is_null($executor)) {
|
if (\is_null($executor)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!\extension_loaded('swoole')) {
|
|
||||||
throw new \RuntimeException('Swoole ext missing!');
|
|
||||||
}
|
|
||||||
$resolve = function ($value) {
|
$resolve = function ($value) {
|
||||||
$this->setResult($value);
|
$this->setResult($value);
|
||||||
$this->setState(self::STATE_FULFILLED);
|
$this->setState(self::STATE_FULFILLED);
|
||||||
|
@ -141,11 +138,9 @@ class CoroutinePromise
|
||||||
foreach ($promises as $promise) {
|
foreach ($promises as $promise) {
|
||||||
if (!$promise instanceof CoroutinePromise) {
|
if (!$promise instanceof CoroutinePromise) {
|
||||||
$channel->close();
|
$channel->close();
|
||||||
throw new \RuntimeException(
|
throw new \RuntimeException('Not an Appwrite\GraphQL\CoroutinePromise');
|
||||||
'Supported only Appwrite\GraphQL\SwoolePromise instance'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
$promise->then(function ($value) use ($key, $result, $channel) {
|
$promise->then(function ($value) use ($key, &$result, $channel) {
|
||||||
$result[$key] = $value;
|
$result[$key] = $value;
|
||||||
$channel->push(true);
|
$channel->push(true);
|
||||||
return $value;
|
return $value;
|
||||||
|
@ -166,6 +161,7 @@ class CoroutinePromise
|
||||||
$reject($firstError);
|
$reject($firstError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$resolve($result);
|
$resolve($result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,27 +68,22 @@ class CoroutinePromiseAdapter implements PromiseAdapter
|
||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
foreach ($promisesOrValues as $index => $promiseOrValue) {
|
foreach ($promisesOrValues as $index => $promiseOrValue) {
|
||||||
go(function ($index, $promiseOrValue, $all, $total, &$count, $result) {
|
if (!($promiseOrValue instanceof Promise)) {
|
||||||
if (!($promiseOrValue instanceof CoroutinePromise)) {
|
|
||||||
$result[$index] = $promiseOrValue;
|
$result[$index] = $promiseOrValue;
|
||||||
$count++;
|
$count++;
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
$result[$index] = null;
|
$result[$index] = null;
|
||||||
$promiseOrValue->then(
|
$promiseOrValue->then(
|
||||||
function ($value) use ($index, &$count, $total, &$result, $all): void {
|
function ($value) use ($index, &$count, $total, &$result, $all): void {
|
||||||
$result[$index] = $value;
|
$result[$index] = $value;
|
||||||
if ($count++ === $total) {
|
$count++;
|
||||||
|
if ($count === $total) {
|
||||||
$all->resolve($result);
|
$all->resolve($result);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[$all, 'reject']
|
[$all, 'reject']
|
||||||
);
|
);
|
||||||
}, $index, $promiseOrValue, $all, $total, $count, $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($count === $total) {
|
|
||||||
$all->resolve($result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise($all, $this);
|
return new Promise($all, $this);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace Appwrite\Utopia;
|
namespace Appwrite\Utopia;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Swoole\Http\Request as SwooleRequest;
|
||||||
use Utopia\Swoole\Response as SwooleResponse;
|
use Utopia\Swoole\Response as SwooleResponse;
|
||||||
use Swoole\Http\Response as SwooleHTTPResponse;
|
use Swoole\Http\Response as SwooleHTTPResponse;
|
||||||
use Utopia\Database\Document;
|
use Utopia\Database\Document;
|
||||||
|
@ -307,6 +308,11 @@ class Response extends SwooleResponse
|
||||||
parent::__construct($response);
|
parent::__construct($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSwoole(): SwooleHTTPResponse
|
||||||
|
{
|
||||||
|
return $this->swoole;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP content types
|
* HTTP content types
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue