From 40b04a3721608a9d650d7e7ad8df4f4f1db37144 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 8 Apr 2022 02:02:48 +1200 Subject: [PATCH] Fix collection type arg and type setting --- CONTRIBUTING.md | 17 +- app/controllers/api/graphql.php | 10 +- public/images/services/graphql.png | Bin 0 -> 7157 bytes src/Appwrite/GraphQL/Builder.php | 436 ++++++++++++++--------------- 4 files changed, 230 insertions(+), 233 deletions(-) create mode 100644 public/images/services/graphql.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae04e7732e..c3de52689b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -309,7 +309,7 @@ Things to remember when releasing SDKs Appwrite uses [yasd](https://github.com/swoole/yasd) debugger, which can be made available during build of Appwrite. You can connect to the debugger using VS Code [PHP Debug](https://marketplace.visualstudio.com/items?itemName=felixfbecker.php-debug) extension or if you are in PHP Storm you don't need any plugin. Below are the settings required for remote debugger connection. -First, you need to create an init file. Duplicate **dev/yasd_init.php.stub** file and name it **dev/yasd_init.php** and there change the IP address to your development machine's IP. Without the proper IP address debugger wont connect. And you also need to set **DEBUG** build arg in **appwrite** service in **docker-compose.yml** file. +First, you need to create an init file. Duplicate **dev/yasd_init.php.stub** file and name it **dev/yasd_init.php** and there change the IP address to your development machine's IP. Without the proper IP address debugger will not connect. You will also need to set **DEBUG** build arg in **appwrite** service in **docker-compose.yml** file. ### VS Code Launch Configuration @@ -321,13 +321,13 @@ First, you need to create an init file. Duplicate **dev/yasd_init.php.stub** fil "port": 9005, "pathMappings": { "/usr/src/code": "${workspaceRoot}" - }, + } } ``` ### PHPStorm Setup -In settings, go to **Languages & Frameworks** > **PHP** > **Debug**, there under **Xdebug** set the debug port to **9005** and enable **can accept external connections** checkbox. +In settings, go to **Languages & Frameworks** > **PHP** > **Debug**, there under **Xdebug** set the debug port to **9005** and enable the **can accept external connection** checkbox. ## Tests @@ -349,7 +349,7 @@ To run end-2-end tests use: docker-compose exec appwrite test /usr/src/code/tests/e2e ``` -To run end-2-end tests for a spcific service use: +To run end-2-end tests for a specific service use: ```bash docker-compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName] @@ -406,9 +406,11 @@ docker-compose exec appwrite /usr/src/code/vendor/bin/psalm From time to time, our team will add tutorials that will help contributors find their way in the Appwrite source code. Below is a list of currently available tutorials: * [Adding Support for a New OAuth2 Provider](./docs/tutorials/add-oauth2-provider.md) -* [Appwrite Environment Variables](./docs/tutorials/environment-variables.md) -* [Running in Production](./docs/tutorials/running-in-production.md) +* [Appwrite Environment Variables](./docs/tutorials/add-environment-variable.md) +* [Adding Support for a New Runtime](./docs/tutorials/add-runtime.md) * [Adding Storage Adapter](./docs/tutorials/add-storage-adapter.md) +* [Adding Translations](./docs/tutorials/add-translations.md) +* [Multi-Architecture Support](./docs/tutorials/multi-architecture-support.md) ## Other Ways to Help @@ -416,7 +418,7 @@ Pull requests are great, but there are many other areas where you can help Appwr ### Blogging & Speaking -Blogging, speaking about, or creating tutorials about one of Appwrite’s many features. Mention [@appwrite](https://twitter.com/appwrite) on Twitter and/or [email team@appwrite.io](mailto:team@appwrite.io) so we can give pointers and tips and help you spread the word by promoting your content on the different Appwrite communication channels. Please add your blog posts and videos of talks to our [Awesome Appwrite](https://github.com/appwrite/awesome-appwrite) repo on GitHub. +Blogging, speaking about, or creating tutorials about one of Appwrite’s many features. Mention [@appwrite](https://twitter.com/appwrite) on Twitter and/or [email team@appwrite.io](mailto:team@appwrite.io), so we can give pointers and tips and help you spread the word by promoting your content on the different Appwrite communication channels. Please add your blog posts and videos of talks to our [Awesome Appwrite](https://github.com/appwrite/awesome-appwrite) repo on GitHub. ### Presenting at Meetups @@ -437,4 +439,3 @@ Submitting documentation updates, enhancements, designs, or bug fixes. Spelling ### Helping Someone Searching for Appwrite on Discord, GitHub, or StackOverflow and helping someone else who needs help. You can also help by teaching others how to contribute to Appwrite's repo! - diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index 52e42a6366..4b424e8c1a 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -65,7 +65,7 @@ App::post('/v1/graphql') $apiSchema, $register, $dbForProject - );; + ); $promise = GraphQL::promiseToExecute( $promiseAdapter, @@ -76,18 +76,18 @@ App::post('/v1/graphql') validationRules: $validations ); - // Blocking wait while queries resolve asynchronously. + // Blocking wait while queries resolve asynchronously $wg = new WaitGroup(); $wg->add(); $promise->then( function ($result) use ($response, $debugFlags, $wg) { - $response->json(['data' => $result->toArray($debugFlags)]); + $response->json($result->toArray($debugFlags)); $wg->done(); }, function ($error) use ($response, $wg) { - $response->json(['errors' => [\json_encode($error)]]); + $response->text(\json_encode(['errors' => [\json_encode($error)]])); $wg->done(); } ); - $wg->wait(); + $wg->wait(App::getEnv('_APP_GRAPHQL_REQUEST_TIMEOUT', 30)); }); diff --git a/public/images/services/graphql.png b/public/images/services/graphql.png new file mode 100644 index 0000000000000000000000000000000000000000..fedd6125fbd33aa2a71b8e8df66aed8557bdf8b8 GIT binary patch literal 7157 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 D{MW6o literal 0 HcmV?d00001 diff --git a/src/Appwrite/GraphQL/Builder.php b/src/Appwrite/GraphQL/Builder.php index a39edbb88a..7bd4fcedff 100644 --- a/src/Appwrite/GraphQL/Builder.php +++ b/src/Appwrite/GraphQL/Builder.php @@ -3,6 +3,7 @@ namespace Appwrite\GraphQL; use Appwrite\GraphQL\Types\JsonType; +use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; use GraphQL\Error\Error; @@ -14,7 +15,6 @@ use Utopia\App; use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Registry\Registry; @@ -95,28 +95,34 @@ class Builder foreach ($rules as $key => $props) { $escapedKey = str_replace('$', '_', $key); - if (isset(self::$typeMapping[$props['type']])) { - $type = self::$typeMapping[$props['type']]; - } else { - try { - $complexModel = $response->getModel($props['type']); - $type = self::getTypeMapping($complexModel, $response); - } catch (\Exception $e) { - Console::error("Could Not find model for : {$props['type']}"); - } - } + $types = \is_array($props['type']) + ? $props['type'] + : [$props['type']]; - if ($props['array']) { - $type = Type::listOf($type); - } - - $fields[$escapedKey] = [ - 'type' => $type, - 'description' => $props['description'], - 'resolve' => function ($object, $args, $context, $info) use ($key) { - return $object[$key]; + foreach ($types as $type) { + if (isset(self::$typeMapping[$type])) { + $type = self::$typeMapping[$type]; + } else { + try { + $complexModel = $response->getModel($type); + $type = self::getTypeMapping($complexModel, $response); + } catch (\Exception $e) { + Console::error("Could not find model for : {$type}"); + } } - ]; + + if ($props['array']) { + $type = Type::listOf($type); + } + + $fields[$escapedKey] = [ + 'type' => $type, + 'description' => $props['description'], + 'resolve' => function ($object, $args, $context, $info) use ($key) { + return $object[$key]; + } + ]; + } } $objectType = [ 'name' => $name, @@ -135,8 +141,9 @@ class Builder * @param $utopia * @param $injections * @return Type + * @throws \Exception */ - protected static function getArgType($validator, bool $required, $utopia, $injections): Type + private static function getParameterArgType($validator, bool $required, $utopia, $injections): Type { $validator = \is_callable($validator) ? \call_user_func_array($validator, $utopia->getResources($injections)) @@ -158,13 +165,15 @@ class Builder $type = Type::boolean(); break; case 'Utopia\Validator\ArrayList': - $type = Type::listOf(self::json()); + $nested = (fn() => $this->validator)->bindTo($validator, $validator)(); + $type = Type::listOf(self::getParameterArgType($nested, $required, $utopia, $injections)); break; case 'Utopia\Validator\Numeric': case 'Utopia\Validator\Range': $type = Type::int(); break; case 'Utopia\Validator\Assoc': + case 'Utopia\Validator\JSON': default: $type = self::json(); break; @@ -177,6 +186,35 @@ class Builder return $type; } + /** + * Function to map an attribute type to a valid GraphQL Type + * + * @param $validator + * @param bool $required + * @param $utopia + * @param $injections + * @return Type + * @throws \Exception + */ + private static function getAttributeArgType($type, $array, $required): Type + { + if ($array) { + return Type::listOf(self::getAttributeArgType($type, false, $required)); + } + $type = match ($type) { + 'boolean' => Type::boolean(), + 'integer' => Type::int(), + 'double' => Type::float(), + default => Type::string(), + }; + + if ($required) { + $type = Type::nonNull($type); + } + + return $type; + } + /** * @throws \Exception */ @@ -186,7 +224,8 @@ class Builder Database $dbForProject ): Schema { - Console::info("[INFO] Appending GraphQL Database Schema..."); + Console::info("[INFO] Merging Schema..."); + $start = microtime(true); $db = self::buildCollectionsSchema($register, $dbForProject); @@ -211,39 +250,12 @@ class Builder ]); $time_elapsed_secs = microtime(true) - $start; - Console::info("[INFO] Time Taken To Append Database to API Schema : ${time_elapsed_secs}s"); + + Console::info("[INFO] Time Taken To Merge Schema : ${time_elapsed_secs}s"); return $schema; } - /** - * @throws \Exception - */ - public static function buildSchema($utopia, $response, $register, $dbForProject): Schema - { - $db = self::buildCollectionsSchema($register, $dbForProject); - $api = self::buildAPISchema($utopia, $response, $register); - - $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 queries', - 'fields' => $queryFields - ]), - 'mutation' => new ObjectType([ - 'name' => 'Mutation', - 'description' => 'The root of all mutations', - 'fields' => $mutationFields - ]) - ]); - } - /** * This function goes through all the project attributes and builds a * GraphQL schema for all the collections they make up. @@ -255,116 +267,91 @@ class Builder */ public static function buildCollectionsSchema(Registry &$register, Database $dbForProject): array { - Console::log("[INFO] Building GraphQL Database Schema..."); + Console::info("[INFO] Building GraphQL Project Collection Schema..."); $start = microtime(true); $collections = []; $queryFields = []; $mutationFields = []; + $limit = 50; $offset = 0; - $attrs = Authorization::skip(static fn() => $dbForProject->find('attributes', [new Query('collectionId', Query::TYPE_EQUAL, ['movies'])])); - - Authorization::skip(function () use ($mutationFields, $queryFields, $collections, $register, $offset, $dbForProject) { + Authorization::skip(function () use (&$mutationFields, &$queryFields, &$collections, $register, $limit, $offset, $dbForProject) { while (!empty($attrs = $dbForProject->find( 'attributes', - limit: $dbForProject->getAttributeLimit(), + limit: $limit, offset: $offset ))) { - go(function ($attrs, $dbForProject, $register, $collections, $queryFields, $mutationFields) { - foreach ($attrs as $attr) { - go(function ($attr, &$collections) { - /** @var Document $attr */ + foreach ($attrs as $attr) { + $collectionId = $attr->getAttribute('collectionId'); - $collectionId = $attr->getAttribute('collectionId'); - - if (isset(self::$typeMapping[$collectionId])) { - return; - } - if ($attr->getAttribute('status') !== 'available') { - return; - } - - $key = $attr->getAttribute('key'); - $type = $attr->getAttribute('type'); - - $escapedKey = str_replace('$', '_', $key); - - $collections[$collectionId][$escapedKey] = [ - 'type' => $type, - 'resolve' => function ($object, $args, $context, $info) use ($key) { - return $object->getAttribute($key); - } - ]; - - }, $attr, $collections); + if ($attr->getAttribute('status') !== 'available') { + return; } - foreach ($collections as $collectionId => $attributes) { - go(function ($collectionId, $attributes, $dbForProject, $register, &$queryFields, &$mutationFields) { - if (isset(self::$typeMapping[$collectionId])) { - return; - } + $key = $attr->getAttribute('key'); + $type = $attr->getAttribute('type'); + $array = $attr->getAttribute('array'); + $required = $attr->getAttribute('required'); - $objectType = new ObjectType([ - 'name' => \ucfirst($collectionId), - 'fields' => $attributes - ]); + $escapedKey = str_replace('$', '_', $key); - self::$typeMapping[$collectionId] = $objectType; + $collections[$collectionId][$escapedKey] = [ + 'type' => self::getAttributeArgType($type, $array, $required), + 'resolve' => function ($object, $args, $context, $info) use ($key) { + return $object->getAttribute($key); + } + ]; + } - $mutateArgs = []; + foreach ($collections as $collectionId => $attributes) { + $objectType = new ObjectType([ + 'name' => $collectionId, + 'fields' => $attributes + ]); - foreach ($attributes as $name => $attribute) { - $mutateArgs[$name] = [ - 'type' => $attribute['type'] - ]; - } + $idArgs = [ + 'id' => [ + 'type' => Type::string() + ] + ]; - $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' => [ + 'types' => Type::listOf(Type::string()), + 'defaultValue' => [], + ] + ]; - $listArgs = [ - 'limit' => [ - 'type' => Type::int() - ], - 'offset' => [ - 'type' => Type::int() - ], - 'cursor' => [ - 'type' => Type::string() - ], - 'orderAttributes' => [ - 'type' => Type::listOf(Type::string()) - ], - 'orderType' => [ - 'types' => Type::listOf(Type::string()) - ] - ]; + self::createCollectionGetQuery($collectionId, $register, $dbForProject, $idArgs, $queryFields, $objectType); + self::createCollectionListQuery($collectionId, $register, $dbForProject, $listArgs, $queryFields, $objectType); + self::createCollectionCreateMutation($collectionId, $register, $dbForProject, $attributes, $mutationFields, $objectType); + self::createCollectionUpdateMutation($collectionId, $register, $dbForProject, $attributes, $mutationFields, $objectType); + self::createCollectionDeleteMutation($collectionId, $register, $dbForProject, $idArgs, $mutationFields, $objectType); + } - self::createCollectionGetQuery($collectionId, $register, $dbForProject, $idArgs, $queryFields); - self::createCollectionListQuery($collectionId, $register, $dbForProject, $listArgs, $queryFields); - self::createCollectionCreateMutation($collectionId, $register, $dbForProject, $mutateArgs, $mutationFields); - self::createCollectionUpdateMutation($collectionId, $register, $dbForProject, $mutateArgs, $mutationFields); - self::createCollectionDeleteMutation($collectionId, $register, $dbForProject, $idArgs, $mutationFields); - - }, $collectionId, $attributes, $dbForProject, $register, $queryFields, $mutationFields); - } - }, $attrs, $dbForProject, $register, $collections, $queryFields, $mutationFields); - - $offset += $dbForProject->getAttributeLimit(); + $offset += $limit; } }); $time_elapsed_secs = microtime(true) - $start; - Console::log("[INFO] Time Taken To Build Database Schema : ${time_elapsed_secs}s"); - Console::info('[INFO] Schema : ' . json_encode([ - 'query' => $queryFields, - 'mutation' => $mutationFields - ])); + Console::info("[INFO] Time Taken To Build Project Collection Schema : ${time_elapsed_secs}s"); return [ 'query' => $queryFields, @@ -372,102 +359,103 @@ class Builder ]; } - private static function createCollectionGetQuery($collectionId, $register, $dbForProject, $args, &$queryFields) + private static function createCollectionGetQuery($collectionId, $register, $dbForProject, $args, &$queryFields, $objectType) { - $resolve = function ($type, $args, $context, $info) use ($collectionId, &$register, $dbForProject) { - return SwoolePromise::create(function (callable $resolve, callable $reject) use ($collectionId, $type, $args, $dbForProject) { + $resolve = fn($type, $args, $context, $info) => new SwoolePromise( + function (callable $resolve, callable $reject) use ($collectionId, $type, $args, $dbForProject) { try { $resolve($dbForProject->getDocument($collectionId, $args['id'])); } catch (\Throwable $e) { $reject($e); } - }); - }; + } + ); $get = [ - 'type' => \ucfirst($collectionId), + 'type' => $objectType, 'args' => $args, 'resolve' => $resolve ]; - $queryFields['get' . \ucfirst($collectionId)] = $get; + $queryFields[$collectionId . 'Get'] = $get; } - private static function createCollectionListQuery($collectionId, $register, $dbForProject, $args, &$queryFields) + private static function createCollectionListQuery($collectionId, $register, $dbForProject, $args, &$queryFields, $objectType) { - $resolve = function ($type, $args, $context, $info) use ($collectionId, &$register, $dbForProject) { - return SwoolePromise::create(function (callable $resolve, callable $reject) use ($collectionId, $type, $args, $dbForProject) { + $resolve = fn($type, $args, $context, $info) => new SwoolePromise( + function (callable $resolve, callable $reject) use ($collectionId, $type, $args, $dbForProject) { try { $resolve($dbForProject->getCollection($collectionId)); } catch (\Throwable $e) { $reject($e); } - }); - }; + } + ); + $list = [ - 'type' => \ucfirst($collectionId), + 'type' => $objectType, 'args' => $args, 'resolve' => $resolve ]; - $queryFields['list' . \ucfirst($collectionId)] = $list; + $queryFields[$collectionId . 'List'] = $list; } - private static function createCollectionCreateMutation($collectionId, $register, $dbForProject, $args, &$mutationFields) + private static function createCollectionCreateMutation($collectionId, $register, $dbForProject, $args, &$mutationFields, $objectType) { - $resolve = function ($type, $args, $context, $info) use ($collectionId, &$register, $dbForProject) { - return SwoolePromise::create(function (callable $resolve, callable $reject) use ($collectionId, $type, $args, $dbForProject) { + $resolve = fn($type, $args, $context, $info) => new SwoolePromise( + function (callable $resolve, callable $reject) use ($collectionId, $type, $args, $dbForProject) { try { $resolve($dbForProject->createDocument($collectionId, new Document($args))); } catch (\Throwable $e) { $reject($e); } - }); - }; + } + ); $create = [ - 'type' => \ucfirst($collectionId), + 'type' => $objectType, 'args' => $args, 'resolve' => $resolve ]; - $mutationFields['create' . \ucfirst($collectionId)] = $create; + $mutationFields[$collectionId . 'Create'] = $create; } - private static function createCollectionUpdateMutation($collectionId, $register, $dbForProject, $args, &$mutationFields) + private static function createCollectionUpdateMutation($collectionId, $register, $dbForProject, $args, &$mutationFields, $objectType) { - $resolve = function ($type, $args, $context, $info) use ($collectionId, &$register, $dbForProject) { - return SwoolePromise::create(function (callable $resolve, callable $reject) use ($collectionId, $type, $args, $dbForProject) { + $resolve = fn($type, $args, $context, $info) => new SwoolePromise( + function (callable $resolve, callable $reject) use ($collectionId, $type, $args, $dbForProject) { try { $resolve($dbForProject->updateDocument($collectionId, $args['id'], new Document($args))); } catch (\Throwable $e) { $reject($e); } - }); - }; + } + ); $update = [ - 'type' => \ucfirst($collectionId), + 'type' => $objectType, 'args' => $args, 'resolve' => $resolve ]; - $mutationFields['update' . \ucfirst($collectionId)] = $update; + $mutationFields[$collectionId . 'Update'] = $update; } - private static function createCollectionDeleteMutation($collectionId, $register, $dbForProject, $args, &$mutationFields) + private static function createCollectionDeleteMutation($collectionId, $register, $dbForProject, $args, &$mutationFields, $objectType) { - $resolve = function ($type, $args, $context, $info) use ($collectionId, &$register, $dbForProject) { - return SwoolePromise::create(function (callable $resolve, callable $reject) use ($collectionId, $type, $args, $dbForProject) { + $resolve = fn($type, $args, $context, $info) => new SwoolePromise( + function (callable $resolve, callable $reject) use ($collectionId, $type, $args, $dbForProject) { try { $resolve($dbForProject->deleteDocument($collectionId, $args['id'])); } catch (\Throwable $e) { $reject($e); } - }); - }; + } + ); $delete = [ - 'type' => \ucfirst($collectionId), + 'type' => $objectType, 'args' => $args, 'resolve' => $resolve ]; - $mutationFields['delete' . \ucfirst($collectionId)] = $delete; + $mutationFields[$collectionId . 'Delete'] = $delete; } /** @@ -475,14 +463,15 @@ class Builder * GraphQL schema for all those routes whose response model is neither empty nor NONE * * @param App $utopia + * @param Request $request * @param Response $response * @param Registry $register * @return array * @throws \Exception */ - public static function buildAPISchema(App $utopia, Response $response, Registry $register): array + public static function buildAPISchema(App $utopia, Request $request, Response $response, Registry $register): array { - Console::log("[INFO] Building GraphQL API Schema..."); + Console::info("[INFO] Building GraphQL REST API Schema..."); $start = microtime(true); self::init(); @@ -491,66 +480,73 @@ class Builder foreach ($utopia->getRoutes() as $method => $routes) { foreach ($routes as $route) { + if (str_starts_with($route->getPath(), '/v1/mock/')) { + continue; + } $namespace = $route->getLabel('sdk.namespace', ''); $methodName = $namespace . \ucfirst($route->getLabel('sdk.method', '')); - $responseModelName = $route->getLabel('sdk.response.model', "none"); + $responseModelNames = $route->getLabel('sdk.response.model', "none"); - Console::info("Namespace: $namespace"); - Console::info("Method: $methodName"); - Console::info("Response Model: $responseModelName"); - Console::info("Raw routes: " . \json_encode($routes)); - Console::info("Raw route: " . \json_encode($route)); + if ($responseModelNames !== "none") { + $responseModels = \is_array($responseModelNames) + ? \array_map(static fn($m) => $response->getModel($m), $responseModelNames) + : [$response->getModel($responseModelNames)]; - if ($responseModelName !== "none") { - $responseModel = $response->getModel($responseModelName); + foreach ($responseModels as $responseModel) { + $type = self::getTypeMapping($responseModel, $response); + $description = $route->getDesc(); + $args = []; - /* Create a GraphQL type for the current response model */ - $type = self::getTypeMapping($responseModel, $response); - /* Get a description for this type */ - $description = $route->getDesc(); - /* Create the args required for this type */ - $args = []; - foreach ($route->getParams() as $key => $value) { - $args[$key] = [ - '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 ($utopia, $response, &$register, $route) { - return SwoolePromise::create(function (callable $resolve, callable $reject) use ($utopia, $response, &$register, $route, $args) { - $utopia->setRoute($route)->execute($route, $args); - $result = $response->getPayload(); + foreach ($route->getParams() as $key => $value) { + $args[$key] = [ + 'type' => self::getParameterArgType( + $value['validator'], + !$value['optional'], + $utopia, + $value['injections'] + ), + 'description' => $value['description'], + 'defaultValue' => $value['default'] + ]; + } - 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'])); + /* Define a resolve function that defines how to fetch data for this type */ + $resolve = fn($type, $args, $context, $info) => new SwoolePromise( + function (callable $resolve, callable $reject) use ($utopia, $request, $response, &$register, $route, $args) { + $utopia + ->setRoute($route) + ->execute($route, $request); + + $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); } + ); - $resolve($result); - }); - }; + $field = [ + 'type' => $type, + 'description' => $description, + 'args' => $args, + 'resolve' => $resolve + ]; - $field = [ - 'type' => $type, - 'description' => $description, - 'args' => $args, - 'resolve' => $resolve - ]; - - if ($method == 'GET') { - $queryFields[$methodName] = $field; - } else if ($method == 'POST' || $method == 'PUT' || $method == 'PATCH' || $method == 'DELETE') { - $mutationFields[$methodName] = $field; + if ($method == 'GET') { + $queryFields[$methodName] = $field; + } else if ($method == 'POST' || $method == 'PUT' || $method == 'PATCH' || $method == 'DELETE') { + $mutationFields[$methodName] = $field; + } } } } } $time_elapsed_secs = microtime(true) - $start; - Console::log("[INFO] Time Taken To Build API Schema : ${time_elapsed_secs}s"); + Console::info("[INFO] Time Taken To Build REST API Schema : ${time_elapsed_secs}s"); return [ 'query' => $queryFields,