Async query resolution

This commit is contained in:
Jake Barnby
2022-04-06 01:48:51 +12:00
parent 48c610348e
commit 69e7c2fed9
6 changed files with 653 additions and 270 deletions
+27 -58
View File
@@ -1,12 +1,11 @@
<?php
use Appwrite\GraphQL\Builder;
use GraphQL\GraphQL;
use GraphQL\Type;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Utils\SchemaExtender;
use Appwrite\GraphQL\GraphQLPromiseAdapter;
use Appwrite\Utopia\Response;
use GraphQL\Error\DebugFlag;
use GraphQL\Executor\ExecutionResult;
use GraphQL\GraphQL;
use GraphQL\Type;
use Utopia\App;
App::post('/v1/graphql')
@@ -17,67 +16,37 @@ App::post('/v1/graphql')
->inject('schema')
->inject('utopia')
->inject('register')
->middleware(true)
->action(function ($request, $response, $schema, $utopia, $register) {
->inject('dbForProject')
->inject('promiseAdapter')
->middleware(true)
->action(function ($request, $response, $schema, $utopia, $register, $dbForProject, $promiseAdapter) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Type\Schema $schema */
/** @var Utopia\App $utopia */
/** @var Utopia\Registry\Registry $register */
$queryType = new ObjectType([
'name' => 'Query',
'description' => 'The root of all your queries',
'fields' => [
'accountGet' => [
'type' => Type\Definition\Type::string(),
'description' => 'Extension description',
'args' => [],
'resolve' => fn() => "Replacing account get response"
],
'testQuery' => [
'type' => Type\Definition\Type::string(),
'description' => 'Extension description 2',
'args' => [],
'resolve' => fn() => "Test query response"
]
]
]);
$extendedSchema = SchemaExtender::extend($schema, $queryType->astNode);
/** @var \Utopia\Database\Database $dbForProject */
$query = $request->getPayload('query', '');
$variables = $request->getPayload('variables', null);
$variables = $request->getPayload('variables');
$response->setContentType(Response::CONTENT_TYPE_NULL);
$register->set('__app', function() use ($utopia) {
return $utopia;
});
$register->set('__response', function() use ($response) {
return $response;
});
$isDevelopment = App::isDevelopment();
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
try {
$debug = $isDevelopment ? ( DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE ) : DebugFlag::NONE;
$rootValue = [];
$result = GraphQL::executeQuery($extendedSchema, $query, $rootValue, null, $variables)
->setErrorFormatter(Builder::getErrorFormatter($isDevelopment, $version));
$output = $result->toArray($debug);
} catch (\Exception $error) {
$output = [
'errors' => [
[
'message' => $error->getMessage().'xxx',
'code' => $error->getCode(),
'file' => $error->getFile(),
'line' => $error->getLine(),
'trace' => $error->getTrace(),
]
]
];
}
$response->json($output);
}
);
$debugFlags = $isDevelopment
? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE
: DebugFlag::NONE;
$rootValue = [];
GraphQL::promiseToExecute(
$promiseAdapter,
$schema,
$query,
$rootValue,
null,
$variables
)->then(function (ExecutionResult $result) use ($response, $debugFlags) {
$response->json($result->toArray($debugFlags));
});
});
+8 -9
View File
@@ -23,6 +23,7 @@ use Ahc\Jwt\JWTException;
use Appwrite\Extend\Exception;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\GraphQL\Builder;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
use Appwrite\Network\Validator\URL;
@@ -855,22 +856,20 @@ App::setResource('geodb', function($register) {
return $register->get('geodb');
}, ['register']);
App::setResource('schema', function($utopia, $response, $request, $register) {
$schema = null;
App::setResource('schema', function($utopia, $response, $request, $register, $dbForProject) {
try {
/*
* Try to get the schema from the register.
* If there is no schema catch the exception and generate it.
*/
// Try to get the schema from the register.
// If there is no base schema catch the exception and generate it.
// If the base schema exists, extend it with the current project schema.
Console::log('Getting Schema from register...');
$schema = $register->get('_schema');
$schema = Builder::appendSchema($schema, $dbForProject);
} catch (Exception $e) {
Console::error('Schema not present. Generating Schema...');
$schema = Builder::buildModelSchema($utopia, $response, $register);
$schema = Builder::buildSchema($utopia, $response, $register, $dbForProject);
$register->set('_schema', function () use ($schema){
return $schema;
});
}
return $schema;
}, ['utopia', 'response', 'request', 'register']);
}, ['utopia', 'response', 'request', 'register', 'dbForProject']);
Generated
+48 -45
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3ef20ad6919a1844f207e06719ad8929",
"content-hash": "de0b7c4c493217f3e9575af0e09277f2",
"packages": [
{
"name": "adhocore/jwt",
@@ -300,20 +300,23 @@
},
{
"name": "colinmollenhour/credis",
"version": "v1.12.1",
"version": "v1.12.2",
"source": {
"type": "git",
"url": "https://github.com/colinmollenhour/credis.git",
"reference": "c27faa11724229986335c23f4b6d0f1d8d6547fb"
"reference": "77e6ede2e01c4cfaade114fe1e07d2f9756949f1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/colinmollenhour/credis/zipball/c27faa11724229986335c23f4b6d0f1d8d6547fb",
"reference": "c27faa11724229986335c23f4b6d0f1d8d6547fb",
"url": "https://api.github.com/repos/colinmollenhour/credis/zipball/77e6ede2e01c4cfaade114fe1e07d2f9756949f1",
"reference": "77e6ede2e01c4cfaade114fe1e07d2f9756949f1",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
"php": ">=5.6.0"
},
"suggest": {
"ext-redis": "Improved performance for communicating with redis"
},
"type": "library",
"autoload": {
@@ -338,9 +341,9 @@
"homepage": "https://github.com/colinmollenhour/credis",
"support": {
"issues": "https://github.com/colinmollenhour/credis/issues",
"source": "https://github.com/colinmollenhour/credis/tree/v1.12.1"
"source": "https://github.com/colinmollenhour/credis/tree/v1.12.2"
},
"time": "2020-11-06T16:09:14+00:00"
"time": "2022-03-08T18:12:43+00:00"
},
{
"name": "composer/package-versions-deprecated",
@@ -3258,16 +3261,16 @@
},
{
"name": "composer/semver",
"version": "3.3.1",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "5d8e574bb0e69188786b8ef77d43341222a41a71"
"reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/5d8e574bb0e69188786b8ef77d43341222a41a71",
"reference": "5d8e574bb0e69188786b8ef77d43341222a41a71",
"url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9",
"reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9",
"shasum": ""
},
"require": {
@@ -3319,7 +3322,7 @@
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.3.1"
"source": "https://github.com/composer/semver/tree/3.3.2"
},
"funding": [
{
@@ -3335,7 +3338,7 @@
"type": "tidelift"
}
],
"time": "2022-03-16T11:22:07+00:00"
"time": "2022-04-01T19:23:25+00:00"
},
{
"name": "composer/xdebug-handler",
@@ -3557,16 +3560,16 @@
},
{
"name": "felixfbecker/language-server-protocol",
"version": "1.5.1",
"version": "v1.5.2",
"source": {
"type": "git",
"url": "https://github.com/felixfbecker/php-language-server-protocol.git",
"reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730"
"reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/9d846d1f5cf101deee7a61c8ba7caa0a975cd730",
"reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730",
"url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842",
"reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842",
"shasum": ""
},
"require": {
@@ -3607,9 +3610,9 @@
],
"support": {
"issues": "https://github.com/felixfbecker/php-language-server-protocol/issues",
"source": "https://github.com/felixfbecker/php-language-server-protocol/tree/1.5.1"
"source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2"
},
"time": "2021-02-22T14:02:09+00:00"
"time": "2022-03-02T22:36:06+00:00"
},
{
"name": "matthiasmullie/minify",
@@ -4184,16 +4187,16 @@
},
{
"name": "phpdocumentor/type-resolver",
"version": "1.6.0",
"version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706"
"reference": "77a32518733312af16a44300404e945338981de3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706",
"reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3",
"reference": "77a32518733312af16a44300404e945338981de3",
"shasum": ""
},
"require": {
@@ -4228,9 +4231,9 @@
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.0"
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1"
},
"time": "2022-01-04T19:58:01+00:00"
"time": "2022-03-15T21:29:03+00:00"
},
{
"name": "phpspec/prophecy",
@@ -5139,16 +5142,16 @@
},
{
"name": "sebastian/environment",
"version": "5.1.3",
"version": "5.1.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "388b6ced16caa751030f6a69e588299fa09200ac"
"reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac",
"reference": "388b6ced16caa751030f6a69e588299fa09200ac",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7",
"reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7",
"shasum": ""
},
"require": {
@@ -5190,7 +5193,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"source": "https://github.com/sebastianbergmann/environment/tree/5.1.3"
"source": "https://github.com/sebastianbergmann/environment/tree/5.1.4"
},
"funding": [
{
@@ -5198,7 +5201,7 @@
"type": "github"
}
],
"time": "2020-09-28T05:52:38+00:00"
"time": "2022-04-03T09:37:03+00:00"
},
{
"name": "sebastian/exporter",
@@ -5781,16 +5784,16 @@
},
{
"name": "symfony/console",
"version": "v6.0.5",
"version": "v6.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "3bebf4108b9e07492a2a4057d207aa5a77d146b1"
"reference": "70dcf7b2ca2ea08ad6ebcc475f104a024fb5632e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/3bebf4108b9e07492a2a4057d207aa5a77d146b1",
"reference": "3bebf4108b9e07492a2a4057d207aa5a77d146b1",
"url": "https://api.github.com/repos/symfony/console/zipball/70dcf7b2ca2ea08ad6ebcc475f104a024fb5632e",
"reference": "70dcf7b2ca2ea08ad6ebcc475f104a024fb5632e",
"shasum": ""
},
"require": {
@@ -5856,7 +5859,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v6.0.5"
"source": "https://github.com/symfony/console/tree/v6.0.7"
},
"funding": [
{
@@ -5872,7 +5875,7 @@
"type": "tidelift"
}
],
"time": "2022-02-25T10:48:52+00:00"
"time": "2022-03-31T17:18:25+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
@@ -6390,16 +6393,16 @@
},
{
"name": "twig/twig",
"version": "v3.3.8",
"version": "v3.3.9",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "972d8604a92b7054828b539f2febb0211dd5945c"
"reference": "6ff9b0e440fa66f97f207e181c41340ddfa5683d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/972d8604a92b7054828b539f2febb0211dd5945c",
"reference": "972d8604a92b7054828b539f2febb0211dd5945c",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/6ff9b0e440fa66f97f207e181c41340ddfa5683d",
"reference": "6ff9b0e440fa66f97f207e181c41340ddfa5683d",
"shasum": ""
},
"require": {
@@ -6450,7 +6453,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.3.8"
"source": "https://github.com/twigphp/Twig/tree/v3.3.9"
},
"funding": [
{
@@ -6462,7 +6465,7 @@
"type": "tidelift"
}
],
"time": "2022-02-04T06:59:48+00:00"
"time": "2022-03-25T09:37:52+00:00"
},
{
"name": "vimeo/psalm",
@@ -6646,5 +6649,5 @@
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.2.0"
"plugin-api-version": "2.3.0"
}
+247 -158
View File
@@ -5,28 +5,25 @@ namespace Appwrite\GraphQL;
use Appwrite\GraphQL\Types\JsonType;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use GraphQL\Error\Error;
use GraphQL\Error\FormattedError;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use Appwrite\GraphQL\Exception;
use GraphQL\Error\Error;
use GraphQL\Error\FormattedError;
use Utopia\CLI\Console;
class Builder {
class Builder
{
protected static ?JsonType $jsonParser = null;
/** @var JsonType $jsonParser */
protected static $jsonParser = null;
/** @var array $typeMapping */
protected static $typeMapping = null;
protected static array $typeMapping = [];
/**
* Function to initialise the typeMapping array with the base cases of the recursion
*
* @return void
*/
public static function init()
* Function to initialise the typeMapping array with the base cases of the recursion
*
* @return void
*/
public static function init()
{
self::$typeMapping = [
Model::TYPE_BOOLEAN => Type::boolean(),
@@ -40,11 +37,11 @@ class Builder {
}
/**
* Function to create a singleton for $jsonParser
*
* @return JsonType
*/
public static function json()
* Function to create a singleton for $jsonParser
*
* @return JsonType
*/
public static function json()
{
if (is_null(self::$jsonParser)) {
self::$jsonParser = new JsonType();
@@ -53,31 +50,31 @@ class Builder {
}
/**
* If the map already contains the type, end the recursion and return.
* Iterate through all the rules in the response model. Each rule is of the form
* [
* [KEY 1] => [
* 'type' => A string from Appwrite/Utopia/Response
* 'description' => A description of the type
* 'default' => A default value for this type
* 'example' => An example of this type
* 'require' => a boolean representing whether this field is required
* 'array' => a boolean representing whether this field is an array
* ],
* [KEY 2] => [
* ],
* [KEY 3] => [
* ] .....
* ]
* If there are any field names containing characters other than a-z, A-Z, 0-9, _ ,
* we need to remove all those characters. Currently Appwrite's Response model has only the
* $ sign which is prohibited by the GraphQL spec. So we're only replacing that. We need to replace this with a regex
* based approach.
*
* @param Model $model
* @param Response $response
* @return Type
*/
* If the map already contains the type, end the recursion and return.
* Iterate through all the rules in the response model. Each rule is of the form
* [
* [KEY 1] => [
* 'type' => A string from Appwrite/Utopia/Response
* 'description' => A description of the type
* 'default' => A default value for this type
* 'example' => An example of this type
* 'require' => a boolean representing whether this field is required
* 'array' => a boolean representing whether this field is an array
* ],
* [KEY 2] => [
* ],
* [KEY 3] => [
* ] .....
* ]
* If there are any field names containing characters other than a-z, A-Z, 0-9, _ ,
* we need to remove all those characters. Currently Appwrite's Response model has only the
* $ sign which is prohibited by the GraphQL spec. So we're only replacing that. We need to replace this with a regex
* based approach.
*
* @param Model $model
* @param Response $response
* @return Type
*/
static function getTypeMapping(Model $model, Response $response): Type
{
if (isset(self::$typeMapping[$model->getType()])) {
@@ -88,6 +85,7 @@ class Builder {
$name = $model->getType();
$fields = [];
$type = null;
foreach ($rules as $key => $props) {
$keyWithoutSpecialChars = str_replace('$', '_', $key);
if (isset(self::$typeMapping[$props['type']])) {
@@ -96,7 +94,7 @@ class Builder {
try {
$complexModel = $response->getModel($props['type']);
$type = self::getTypeMapping($complexModel, $response);
} catch (Exception $e) {
} catch (\Exception $e) {
Console::error("Could Not find model for : {$props['type']}");
}
}
@@ -112,82 +110,204 @@ class Builder {
];
}
$objectType = [
'name' => $name,
'name' => $name,
'fields' => $fields
];
self::$typeMapping[$name] = new ObjectType($objectType);
self::$typeMapping[$name] = new ObjectType($objectType);
return self::$typeMapping[$name];
}
/**
* Function to map a Utopia\Validator to a valid GraphQL Type
*
* @param $validator
* @param bool $required
* @param $utopia
* @param $injections
* @return GraphQL\Type\Definition\Type
*/
protected static function getArgType($validator, bool $required, $utopia, $injections): Type
/**
* Function to map a Utopia\Validator to a valid GraphQL Type
*
* @param $validator
* @param bool $required
* @param $utopia
* @param $injections
* @return GraphQL\Type\Definition\Type
*/
protected static function getArgType($validator, bool $required, $utopia, $injections): Type
{
$validator = (\is_callable($validator)) ? call_user_func_array($validator, $utopia->getResources($injections)) : $validator;
$type = [];
switch ((!empty($validator)) ? \get_class($validator) : '') {
case 'Utopia\Validator\Email':
case 'Utopia\Validator\Host':
case 'Utopia\Validator\Length':
case 'Appwrite\Auth\Validator\Password':
case 'Utopia\Validator\URL':
case 'Appwrite\Database\Validator\UID':
case 'Appwrite\Storage\Validator\File':
case 'Utopia\Validator\WhiteList':
case 'Utopia\Validator\Text':
$type = Type::string();
break;
case 'Utopia\Validator\Boolean':
$type = Type::boolean();
break;
case 'Appwrite\Database\Validator\UID':
$type = Type::string();
break;
case 'Utopia\Validator\Email':
$type = Type::string();
break;
case 'Utopia\Validator\URL':
$type = Type::string();
break;
case 'Utopia\Validator\JSON':
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
$type = self::json();
break;
case 'Appwrite\Storage\Validator\File':
$type = Type::string();
case 'Utopia\Validator\ArrayList':
$type = Type::listOf(self::json());
break;
case 'Appwrite\Auth\Validator\Password':
$type = Type::string();
break;
case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */
$type = Type::int();
break;
case 'Utopia\Validator\Numeric':
case 'Utopia\Validator\Range':
$type = Type::int();
break;
case 'Utopia\Validator\Length':
$type = Type::string();
break;
case 'Utopia\Validator\Host':
$type = Type::string();
break;
case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */
$type = Type::string();
break;
case 'Utopia\Validator\Assoc':
default:
$type = self::json();
break;
}
if ($required) {
$type = Type::nonNull($type);
}
return $type;
}
public static function appendSchema($schema, $dbForProject): Schema
{
Console::log("[INFO] Appending GraphQL Database Schema...");
$start = microtime(true);
$db = self::buildDatabaseSchema($dbForProject);
$queryFields = $schema->getQueryType()?->getFields() ?? [];
$mutationFields = $schema->getMutationType()?->getFields() ?? [];
$queryFields = \array_merge($queryFields, $db['query']);
$mutationFields = \array_merge($mutationFields, $db['mutation']);
ksort($queryFields);
ksort($mutationFields);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'description' => 'The root of all queries',
'fields' => $queryFields
]),
'mutation' => new ObjectType([
'name' => 'Mutation',
'description' => 'The root of all mutations',
'fields' => $mutationFields
])
]);
$time_elapsed_secs = microtime(true) - $start;
Console::log("[INFO] Time Taken To Append Database to API Schema : ${time_elapsed_secs}s");
return $schema;
}
public static function buildSchema($utopia, $response, $register, $dbForProject): Schema
{
$db = self::buildDatabaseSchema($dbForProject);
$api = self::buildAPISchema($utopia, $response, $register, $dbForProject);
$queryFields = \array_merge($api['query'], $db['query']);
$mutationFields = \array_merge($api['mutation'], $db['mutation']);
ksort($queryFields);
ksort($mutationFields);
return new Schema([
'query' => new ObjectType([
'name' => 'Query',
'description' => 'The root of all your queries',
'fields' => $queryFields
]),
'mutation' => new ObjectType([
'name' => 'Mutation',
'description' => 'The root of all your mutations',
'fields' => $mutationFields
])
]);
}
/**
* This function goes through all the project attributes and builds a
* GraphQL schema for all the collections they make up.
*
* @param $dbForProject
* @return array
*/
public static function buildDatabaseSchema($dbForProject): array
{
Console::log("[INFO] Building GraphQL Database Schema...");
$start = microtime(true);
$attrs = $dbForProject->getCollection('attributes');
$queryFields = [];
$mutationFields = [];
$collections = [];
foreach ($attrs as $attr) {
$collectionId = $attr->getAttribute('collectionId');
if (isset(self::$typeMapping[$collectionId])) {
continue;
}
$key = $attr->getAttribute('key');
$type = $attr->getAttribute('type');
$keyWithoutSpecialChars = str_replace('$', '_', $key);
$collections[$collectionId][$keyWithoutSpecialChars] = [
'type' => $type,
'resolve' => function ($object, $args, $context, $info) use ($key) {
return $object->getAttribute($key);
}
];
}
$args = [];
foreach ($collections as $id => $fields) {
$objectType = new ObjectType([
'name' => $id,
'fields' => $fields
]);
self::$typeMapping[$id] = $objectType;
foreach ($fields as $field => $fieldInfo) {
$args[$field] = [
'type' => $fieldInfo['type']
];
}
$resolve = function ($type, $args, $context, $info) use (&$register, $dbForProject) {
return SwoolePromise::create(function (callable $resolve, callable $reject) use ($type, $args, $dbForProject) {
try {
$resolve($dbForProject->getCollection($type));
} catch (\Throwable $e) {
$reject($e);
}
});
};
$field = [
'type' => $type,
'args' => $args,
'resolve' => $resolve
];
$queryFields[$id] = $field;
$mutationFields[$id] = $field;
}
$time_elapsed_secs = microtime(true) - $start;
Console::log("[INFO] Time Taken To Build Database Schema : ${time_elapsed_secs}s");
return [
'query' => $queryFields,
'mutation' => $mutationFields
];
}
/**
* This function goes through all the REST endpoints in the API and builds a
* GraphQL schema for all those routes whose response model is neither empty nor NONE
@@ -195,46 +315,23 @@ class Builder {
* @param $utopia
* @param $response
* @param $register
* @return Schema
* @param $dbForProject
* @return array
*/
public static function buildDatabaseSchema($utopia, $response, $register)
public static function buildAPISchema($utopia, $response, $register, $dbForProject): array
{
/** @var Model\Collection[] $collections */
Console::log("[INFO] Building GraphQL Database Schema...");
Console::log("[INFO] Building GraphQL API Schema...");
$start = microtime(true);
$collections = [];
foreach($collections as $collection) {
foreach ($collection->getRules() as $rule) {
/** @var Model\Rule $rule */
$modelName = $rule->getName();
}
}
}
/**
* This function goes through all the REST endpoints in the API and builds a
* GraphQL schema for all those routes whose response model is neither empty nor NONE
*
* @param $utopia
* @param $response
* @param $register
* @return Schema
*/
public static function buildModelSchema($utopia, $response, $register) {
Console::log("[INFO] Building GraphQL Schema...");
$start = microtime(true);
self::init();
$queryFields = [];
$mutationFields = [];
foreach($utopia->getRoutes() as $method => $routes ){
foreach($routes as $route) {
foreach ($utopia->getRoutes() as $method => $routes) {
foreach ($routes as $route) {
$namespace = $route->getLabel('sdk.namespace', '');
$methodName = $namespace.'_'.$route->getLabel('sdk.method', '');
$methodName = $namespace . '_' . $route->getLabel('sdk.method', '');
$responseModelName = $route->getLabel('sdk.response.model', "");
if ($responseModelName !== "") {
@@ -248,28 +345,33 @@ class Builder {
$args = [];
foreach ($route->getParams() as $key => $value) {
$args[$key] = [
'type' => self::getArgType($value['validator'],!$value['optional'], $utopia, $value['injections']),
'type' => self::getArgType($value['validator'], !$value['optional'], $utopia, $value['injections']),
'description' => $value['description'],
'defaultValue' => $value['default']
];
}
/* Define a resolve function that defines how to fetch data for this type */
$resolve = function ($type, $args, $context, $info) use (&$register, $route) {
$utopia = $register->get('__app');
$utopia->setRoute($route)->execute($route, $args);
$response = $register->get('__response');
$result = $response->getPayload();
if ( $response->getCurrentModel() == Response::MODEL_ERROR_DEV ) {
throw new ExceptionDev($result['message'], $result['code'], $result['version'], $result['file'], $result['line'], $result['trace']);
} else if ( $response->getCurrentModel() == Response::MODEL_ERROR ) {
throw new Exception($result['message'], $result['code']);
}
return $result;
$resolve = function ($type, $args, $context, $info) use (&$register, $route, $dbForProject) {
return SwoolePromise::create(function (callable $resolve, callable $reject) use (&$register, $route, $dbForProject, $args) {
$utopia = $register->get('__app');
$utopia->setRoute($route)->execute($route, $args);
$response = $register->get('__response');
$result = $response->getPayload();
if ($response->getCurrentModel() == Response::MODEL_ERROR_DEV) {
$reject(new ExceptionDev($result['message'], $result['code'], $result['version'], $result['file'], $result['line'], $result['trace']));
} else if ($response->getCurrentModel() == Response::MODEL_ERROR) {
$reject(new \Exception($result['message'], $result['code']));
}
$resolve($result);
});
};
$field = [
'type' => $type,
'description' => $description,
'description' => $description,
'args' => $args,
'resolve' => $resolve
];
@@ -283,41 +385,28 @@ class Builder {
}
}
ksort($queryFields);
ksort($mutationFields);
$queryType = new ObjectType([
'name' => 'Query',
'description' => 'The root of all your queries',
'fields' => $queryFields
]);
$mutationType = new ObjectType([
'name' => 'Mutation',
'description' => 'The root of all your mutations',
'fields' => $mutationFields
]);
$schema = new Schema([
'query' => $queryType,
'mutation' => $mutationType
]);
$time_elapsed_secs = microtime(true) - $start;
Console::log("[INFO] Time Taken To Build Schema : ${time_elapsed_secs}s");
return $schema;
Console::log("[INFO] Time Taken To Build API Schema : ${time_elapsed_secs}s");
return [
'query' => $queryFields,
'mutation' => $mutationFields
];
}
/**
* Function to create an appropriate GraphQL Error Formatter
* Based on whether we're on a development build or production
* build of Appwrite.
*
* build of Appwrite.
*
* @param bool $isDevelopment
* @param string $version
* @param string $version
* @return callable
*/
public static function getErrorFormatter(bool $isDevelopment, string $version): callable
public
static function getErrorFormatter(bool $isDevelopment, string $version): callable
{
$errorFormatter = function(Error $error) use ($isDevelopment, $version) {
$errorFormatter = function (Error $error) use ($isDevelopment, $version) {
$formattedError = FormattedError::createFromException($error);
/** Previous error represents the actual error thrown by Appwrite server */
$previousError = $error->getPrevious() ?? $error;
@@ -0,0 +1,94 @@
<?php
namespace Appwrite\GraphQL;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Utils\Utils;
class GraphQLPromiseAdapter implements PromiseAdapter
{
public function isThenable($value): bool
{
return $value instanceof SwoolePromise;
}
public function convertThenable($thenable): Promise
{
if (!$thenable instanceof SwoolePromise) {
throw new InvariantViolation('Expected instance of SwoolePromise, got ' . Utils::printSafe($thenable));
}
return new Promise($thenable, $this);
}
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
{
/** @var SwoolePromise $adoptedPromise */
$adoptedPromise = $promise->adoptedPromise;
return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
}
public function create(callable $resolver): Promise
{
$promise = new SwoolePromise();
try {
$resolver(
[$promise, 'resolve'],
[$promise, 'reject'],
);
} catch (\Throwable $e) {
$promise->reject($e);
}
return new Promise($promise, $this);
}
public function createFulfilled($value = null): Promise
{
$promise = new SwoolePromise();
return new Promise($promise->resolve($value), $this);
}
public function createRejected($reason): Promise
{
$promise = new SwoolePromise();
return new Promise($promise->reject($reason), $this);
}
public function all(array $promisesOrValues): Promise
{
$all = new SwoolePromise();
$total = count($promisesOrValues);
$count = 0;
$result = [];
foreach ($promisesOrValues as $index => $promiseOrValue) {
if ($promiseOrValue instanceof Promise) {
$result[$index] = null;
$promiseOrValue->then(
static function ($value) use ($index, &$count, $total, &$result, $all): void {
$result[$index] = $value;
$count++;
if ($count < $total) {
return;
}
$all->resolve($result);
},
[$all, 'reject']
);
} else {
$result[$index] = $promiseOrValue;
$count++;
}
}
if ($count === $total) {
$all->resolve($result);
}
return new Promise($all, $this);
}
}
+229
View File
@@ -0,0 +1,229 @@
<?php
namespace Appwrite\GraphQL;
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;
/**
* Class SwoolePromise
*
* Inspired by https://github.com/streamcommon/promise/blob/master/lib/ExtSwoolePromise.php
*
* @package Appwrite\GraphQL
*/
class SwoolePromise
{
const STATE_PENDING = 1;
const STATE_FULFILLED = 0;
const STATE_REJECTED = -1;
protected int $state = self::STATE_PENDING;
private mixed $result;
public function __construct(?callable $executor = null)
{
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);
};
$reject = function ($value) {
if ($this->isPending()) {
$this->setResult($value);
$this->setState(self::STATE_REJECTED);
}
};
Coroutine::create(function (callable $executor, callable $resolve, callable $reject) {
try {
$executor($resolve, $reject);
} catch (\Throwable $exception) {
$reject($exception);
}
}, $executor, $resolve, $reject);
}
/**
* Create a new promise from the given callable.
*
* @param callable $promise
* @return SwoolePromise
*/
final public static function create(callable $promise): SwoolePromise
{
return new static($promise);
}
/**
* Resolve promise with given value.
*
* @param mixed $value
* @return SwoolePromise
*/
final public static function resolve(mixed $value): SwoolePromise
{
return new static(function (callable $resolve) use ($value) {
$resolve($value);
});
}
/**
* Rejects the promise with the given reason.
*
* @param mixed $value
* @return SwoolePromise
*/
final public static function reject(mixed $value): SwoolePromise
{
return new static(function (callable $resolve, callable $reject) use ($value) {
$reject($value);
});
}
/**
* Catch any exception thrown by the executor.
*
* @param callable $onRejected
* @return SwoolePromise
*/
final public function catch(callable $onRejected): SwoolePromise
{
return $this->then(null, $onRejected);
}
/**
* Execute the promise.
*
* @param callable|null $onFulfilled
* @param callable|null $onRejected
* @return SwoolePromise
*/
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): SwoolePromise
{
return self::create(function (callable $resolve, callable $reject) use ($onFulfilled, $onRejected) {
while ($this->isPending()) {
usleep(25000);
}
$callable = $this->isFulfilled() ? $onFulfilled : $onRejected;
if (!is_callable($callable)) {
$resolve($this->result);
return;
}
try {
$resolve($callable($this->result));
} catch (\Throwable $error) {
$reject($error);
}
});
}
/**
* Returns a promise that completes when all passed in promises complete.
*
* @param iterable|SwoolePromise[] $promises
* @return SwoolePromise
*/
public static function all(iterable $promises): SwoolePromise
{
return self::create(function (callable $resolve, callable $reject) use ($promises) {
$ticks = count($promises);
$firstError = null;
$channel = new Channel($ticks);
$result = [];
$key = 0;
foreach ($promises as $promise) {
if (!$promise instanceof SwoolePromise) {
$channel->close();
throw new \RuntimeException(
'Supported only Appwrite\GraphQL\SwoolePromise instance'
);
}
$promise->then(function ($value) use ($key, $result, $channel) {
$result[$key] = $value;
$channel->push(true);
return $value;
}, function ($error) use ($channel, &$firstError) {
$channel->push(true);
if ($firstError === null) {
$firstError = $error;
}
});
$key++;
}
while ($ticks--) {
$channel->pop();
}
$channel->close();
if ($firstError !== null) {
$reject($firstError);
return;
}
$resolve($result);
});
}
/**
* Set resolved result
*
* @param mixed $value
* @return void
*/
private function setResult(mixed $value): void
{
if (!$value instanceof SwoolePromise) {
throw new \RuntimeException('Supported only Appwrite\GraphQL\SwoolePromise instance');
}
$resolved = false;
$callable = function ($value) use (&$resolved) {
$this->setResult($value);
$resolved = true;
};
$value->then($callable, $callable);
while (!$resolved) {
usleep(25000);
}
$this->result = $value;
}
/**
* Change promise state
*
* @param integer $state
* @return void
*/
final protected function setState(int $state): void
{
$this->state = $state;
}
/**
* Promise is pending
*
* @return boolean
*/
final protected function isPending(): bool
{
return $this->state == self::STATE_PENDING;
}
/**
* Promise is fulfilled
*
* @return boolean
*/
final protected function isFulfilled(): bool
{
return $this->state == self::STATE_FULFILLED;
}
}