1
0
Fork 0
mirror of synced 2024-06-28 19:20:25 +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,
'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,
],
];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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