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