diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index 7ccb462e69..afc47e019d 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -1,12 +1,11 @@ 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)); + }); + }); diff --git a/app/init.php b/app/init.php index 7220beb2b7..89aecb2ad7 100644 --- a/app/init.php +++ b/app/init.php @@ -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']); diff --git a/composer.lock b/composer.lock index 1bd0a912a9..be132fd9ae 100644 --- a/composer.lock +++ b/composer.lock @@ -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" } diff --git a/src/Appwrite/GraphQL/Builder.php b/src/Appwrite/GraphQL/Builder.php index 54b5c51281..62ed9710ef 100644 --- a/src/Appwrite/GraphQL/Builder.php +++ b/src/Appwrite/GraphQL/Builder.php @@ -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; diff --git a/src/Appwrite/GraphQL/GraphQLPromiseAdapter.php b/src/Appwrite/GraphQL/GraphQLPromiseAdapter.php new file mode 100644 index 0000000000..06b8898096 --- /dev/null +++ b/src/Appwrite/GraphQL/GraphQLPromiseAdapter.php @@ -0,0 +1,94 @@ +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); + } +} \ No newline at end of file diff --git a/src/Appwrite/GraphQL/SwoolePromise.php b/src/Appwrite/GraphQL/SwoolePromise.php new file mode 100644 index 0000000000..64f769db2d --- /dev/null +++ b/src/Appwrite/GraphQL/SwoolePromise.php @@ -0,0 +1,229 @@ +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; + } +} \ No newline at end of file