1
0
Fork 0
mirror of synced 2024-07-01 04:30:59 +12:00

Async resolution fixes

This commit is contained in:
Jake Barnby 2022-04-26 19:49:36 +12:00
parent 5b1f973b45
commit dbb49ac7bf
No known key found for this signature in database
GPG key ID: A4674EBC0E404657
8 changed files with 172 additions and 158 deletions

View file

@ -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,
],
]; ];

View file

@ -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
$validations[] = new DisableIntrospection(); $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( $promise = GraphQL::promiseToExecute(
$promiseAdapter, $promiseAdapter,
@ -117,23 +112,25 @@ 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(
$result = $result->toArray($debugFlags); function ($result) use ($response, $debugFlags, $wg) {
\var_dump("Result:" . $result); $result = $result->toArray($debugFlags);
if (isset($result['errors'])) { \var_dump("Result:" . $result);
$response->json(['data' => [], ...$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(); $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(); $wg->wait();
} }

View file

@ -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']);

View file

@ -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 = '';

View file

@ -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']);
@ -304,121 +309,127 @@ class Builder
* @throws \Exception * @throws \Exception
*/ */
public static function buildCollectionsSchema( public static function buildCollectionsSchema(
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();
foreach ($attrs as $attr) { go(function () use ($utopia, $request, $response, $dbForProject, &$collections, &$queryFields, &$mutationFields, $limit, &$offset, $attrs, $userId, $wg) {
$collectionId = $attr->getAttribute('collectionId'); foreach ($attrs as $attr) {
if ($attr->getAttribute('status') !== 'available') { $collectionId = $attr->getAttribute('collectionId');
continue; 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) { foreach ($collections as $collectionId => $attributes) {
$objectType = new ObjectType([ $objectType = new ObjectType([
'name' => $collectionId, 'name' => $collectionId,
'fields' => $attributes 'fields' => $attributes
]); ]);
$idArgs = [ $idArgs = [
'id' => [ 'id' => [
'type' => Type::string() 'type' => Type::string()
] ]
]; ];
$listArgs = [ $listArgs = [
'limit' => [ 'limit' => [
'type' => Type::int(), 'type' => Type::int(),
'defaultValue' => $limit, 'defaultValue' => $limit,
], ],
'offset' => [ 'offset' => [
'type' => Type::int(), 'type' => Type::int(),
'defaultValue' => 0, 'defaultValue' => 0,
], ],
'cursor' => [ 'cursor' => [
'type' => Type::string(), 'type' => Type::string(),
'defaultValue' => null, 'defaultValue' => null,
], ],
'orderAttributes' => [ 'orderAttributes' => [
'type' => Type::listOf(Type::string()),
'defaultValue' => [],
],
'orderType' => [
'type' => Type::listOf(Type::string()),
'defaultValue' => [],
]
];
$attributes['read'] = [
'type' => Type::listOf(Type::string()), 'type' => Type::listOf(Type::string()),
'defaultValue' => [], 'defaultValue' => ["user:$userId"],
], 'resolve' => function ($object, $args, $context, $info) use ($collectionId) {
'orderType' => [ return $object->getAttribute('$read');
}
];
$attributes['write'] = [
'type' => Type::listOf(Type::string()), 'type' => Type::listOf(Type::string()),
'defaultValue' => [], 'defaultValue' => ["user:$userId"],
] 'resolve' => function ($object, $args, $context, $info) use ($collectionId) {
]; return $object->getAttribute('$write');
}
$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)
];
}
//});
$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; $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);

View file

@ -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);
}); });
} }

View file

@ -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++;
break;
}
$result[$index] = null;
$promiseOrValue->then(
function ($value) use ($index, &$count, $total, &$result, $all): void {
$result[$index] = $value;
$count++; $count++;
return; if ($count === $total) {
} $all->resolve($result);
$result[$index] = null; }
$promiseOrValue->then( },
function ($value) use ($index, &$count, $total, &$result, $all): void { [$all, 'reject']
$result[$index] = $value; );
if ($count++ === $total) {
$all->resolve($result);
}
},
[$all, 'reject']
);
}, $index, $promiseOrValue, $all, $total, $count, $result);
}
if ($count === $total) {
$all->resolve($result);
} }
return new Promise($all, $this); return new Promise($all, $this);

View file

@ -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
*/ */