diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index afc47e019..5f19d461b 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -1,6 +1,5 @@ getPayload('query', ''); $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(); $debugFlags = $isDevelopment diff --git a/app/init.php b/app/init.php index 89aecb2ad..0e086e1f5 100644 --- a/app/init.php +++ b/app/init.php @@ -789,6 +789,8 @@ App::setResource('dbForProject', function($db, $cache, $project) { $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); $database->setNamespace("_{$project->getId()}"); + Console::info("Getting dbForProject with ID: {$project->getId()}"); + return $database; }, ['db', 'cache', 'project']); @@ -858,16 +860,14 @@ App::setResource('geodb', function($register) { App::setResource('schema', function($utopia, $response, $request, $register, $dbForProject) { try { - // 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...'); + Console::log('Getting GraphQL schema from register...'); $schema = $register->get('_schema'); $schema = Builder::appendSchema($schema, $dbForProject); - } catch (Exception $e) { - Console::error('Schema not present. Generating Schema...'); + } catch (\Exception $e) { + Console::error('Base GraphQL schema not present. Generating...'); $schema = Builder::buildSchema($utopia, $response, $register, $dbForProject); - $register->set('_schema', function () use ($schema){ + Console::error('Built GraphQL schema: ' . \json_encode($schema)); + $register->set('_schema', function () use ($schema) { return $schema; }); } diff --git a/app/realtime.php b/app/realtime.php index 8d36069ed..92436eddb 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -29,7 +29,7 @@ use Utopia\WebSocket\Adapter; require_once __DIR__ . '/init.php'; -Runtime::enableCoroutine(SWOOLE_HOOK_ALL); +Runtime::enableCoroutine(true,SWOOLE_HOOK_ALL); $realtime = new Realtime(); diff --git a/composer.lock b/composer.lock index be132fd9a..61d719ae7 100644 --- a/composer.lock +++ b/composer.lock @@ -1583,16 +1583,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.0.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced" + "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/c726b64c1ccfe2896cb7df2e1331c357ad1c8ced", - "reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", + "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", "shasum": "" }, "require": { @@ -1630,7 +1630,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.1" }, "funding": [ { @@ -1646,7 +1646,7 @@ "type": "tidelift" } ], - "time": "2021-11-01T23:48:49+00:00" + "time": "2022-01-02T09:55:41+00:00" }, { "name": "symfony/polyfill-ctype", @@ -6127,16 +6127,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.0.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "36715ebf9fb9db73db0cb24263c79077c6fe8603" + "reference": "e517458f278c2131ca9f262f8fbaf01410f2c65c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/36715ebf9fb9db73db0cb24263c79077c6fe8603", - "reference": "36715ebf9fb9db73db0cb24263c79077c6fe8603", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e517458f278c2131ca9f262f8fbaf01410f2c65c", + "reference": "e517458f278c2131ca9f262f8fbaf01410f2c65c", "shasum": "" }, "require": { @@ -6189,7 +6189,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.0.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.0.1" }, "funding": [ { @@ -6205,7 +6205,7 @@ "type": "tidelift" } ], - "time": "2021-11-04T17:53:12+00:00" + "time": "2022-03-13T20:10:05+00:00" }, { "name": "symfony/string", diff --git a/docker-compose.yml b/docker-compose.yml index e804bf91a..793486eae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -727,13 +727,13 @@ services: # - '3001:80' graphql-explorer: - container_name: graphql-explorer + container_name: appwrite-graphql-explorer image: appwrite/altair:0.1.0 restart: unless-stopped networks: - appwrite ports: - - 9509:3000 + - "9509:3000" environment: - SERVER_URL=http://localhost/v1/graphql diff --git a/src/Appwrite/GraphQL/Builder.php b/src/Appwrite/GraphQL/Builder.php index 62ed9710e..1a06f4c35 100644 --- a/src/Appwrite/GraphQL/Builder.php +++ b/src/Appwrite/GraphQL/Builder.php @@ -11,6 +11,10 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use Utopia\CLI\Console; +use Utopia\Database\Database; +use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; +use Utopia\Registry\Registry; class Builder { @@ -171,7 +175,7 @@ class Builder Console::log("[INFO] Appending GraphQL Database Schema..."); $start = microtime(true); - $db = self::buildDatabaseSchema($dbForProject); + $db = self::buildCollectionsSchema($dbForProject); $queryFields = $schema->getQueryType()?->getFields() ?? []; $mutationFields = $schema->getMutationType()?->getFields() ?? []; @@ -201,10 +205,13 @@ class Builder return $schema; } + /** + * @throws \Exception + */ public static function buildSchema($utopia, $response, $register, $dbForProject): Schema { - $db = self::buildDatabaseSchema($dbForProject); - $api = self::buildAPISchema($utopia, $response, $register, $dbForProject); + $db = self::buildCollectionsSchema($dbForProject, $register); + $api = self::buildAPISchema($utopia, $response, $register); $queryFields = \array_merge($api['query'], $db['query']); $mutationFields = \array_merge($api['mutation'], $db['mutation']); @@ -215,12 +222,12 @@ class Builder return new Schema([ 'query' => new ObjectType([ 'name' => 'Query', - 'description' => 'The root of all your queries', + 'description' => 'The root of all queries', 'fields' => $queryFields ]), 'mutation' => new ObjectType([ 'name' => 'Mutation', - 'description' => 'The root of all your mutations', + 'description' => 'The root of all mutations', 'fields' => $mutationFields ]) ]); @@ -230,77 +237,120 @@ class Builder * This function goes through all the project attributes and builds a * GraphQL schema for all the collections they make up. * - * @param $dbForProject + * @param Database $dbForProject * @return array + * @throws \Exception */ - public static function buildDatabaseSchema($dbForProject): array + public static function buildCollectionsSchema(Database $dbForProject, Registry &$register): array { Console::log("[INFO] Building GraphQL Database Schema..."); $start = microtime(true); - $attrs = $dbForProject->getCollection('attributes'); - + $collections = []; $queryFields = []; $mutationFields = []; - $collections = []; + $offset = 0; - foreach ($attrs as $attr) { - $collectionId = $attr->getAttribute('collectionId'); + Authorization::skip(function () use ($mutationFields, $queryFields, $collections, $register, $offset, $dbForProject) { + while (!empty($attrs = $dbForProject->find( + 'attributes', + limit: $dbForProject->getAttributeLimit(), + offset: $offset + ))) { + go(function ($attrs, $dbForProject, $register, $collections, $queryFields, $mutationFields) { + foreach ($attrs as $attr) { + go(function ($attr, &$collections) { + /** @var Document $attr */ - if (isset(self::$typeMapping[$collectionId])) { - continue; - } + $collectionId = $attr->getAttribute('collectionId'); - $key = $attr->getAttribute('key'); - $type = $attr->getAttribute('type'); - $keyWithoutSpecialChars = str_replace('$', '_', $key); + if (isset(self::$typeMapping[$collectionId])) { + return; + } + if ($attr->getAttribute('status') !== 'available') { + return; + } - $collections[$collectionId][$keyWithoutSpecialChars] = [ - 'type' => $type, - 'resolve' => function ($object, $args, $context, $info) use ($key) { - return $object->getAttribute($key); - } - ]; - } + $key = $attr->getAttribute('key'); + $type = $attr->getAttribute('type'); - $args = []; + $escapedKey = str_replace('$', '_', $key); - foreach ($collections as $id => $fields) { - $objectType = new ObjectType([ - 'name' => $id, - 'fields' => $fields - ]); + $collections[$collectionId][$escapedKey] = [ + 'type' => $type, + 'resolve' => function ($object, $args, $context, $info) use ($key) { + return $object->getAttribute($key); + } + ]; - 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); + }, $attr, $collections); } - }); - }; - $field = [ - 'type' => $type, - 'args' => $args, - 'resolve' => $resolve - ]; + foreach ($collections as $collectionId => $attributes) { + go(function ($collectionId, $attributes, $dbForProject, $register, &$queryFields, &$mutationFields) { + if (isset(self::$typeMapping[$collectionId])) { + return; + } - $queryFields[$id] = $field; - $mutationFields[$id] = $field; - } + $objectType = new ObjectType([ + 'name' => \ucfirst($collectionId), + 'fields' => $attributes + ]); + + self::$typeMapping[$collectionId] = $objectType; + + $mutateArgs = []; + + foreach ($attributes as $name => $attribute) { + $mutateArgs[$name] = [ + 'type' => $attribute['type'] + ]; + } + + $idArgs = [ + 'id' => [ + 'type' => Type::string() + ] + ]; + + $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); + 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(); + } + }); $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 + ])); return [ 'query' => $queryFields, @@ -308,6 +358,104 @@ class Builder ]; } + private static function createCollectionGetQuery($collectionId, $register, $dbForProject, $args, &$queryFields) + { + $resolve = function ($type, $args, $context, $info) use ($collectionId, &$register, $dbForProject) { + return SwoolePromise::create(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), + 'args' => $args, + 'resolve' => $resolve + ]; + $queryFields['get' . \ucfirst($collectionId)] = $get; + } + + private static function createCollectionListQuery($collectionId, $register, $dbForProject, $args, &$queryFields) + { + $resolve = function ($type, $args, $context, $info) use ($collectionId, &$register, $dbForProject) { + return SwoolePromise::create(function (callable $resolve, callable $reject) use ($collectionId, $type, $args, $dbForProject) { + try { + $resolve($dbForProject->getCollection($collectionId)); + } catch (\Throwable $e) { + $reject($e); + } + }); + }; + $list = [ + 'type' => \ucfirst($collectionId), + 'args' => $args, + 'resolve' => $resolve + ]; + $queryFields['list' . \ucfirst($collectionId)] = $list; + } + + private static function createCollectionCreateMutation($collectionId, $register, $dbForProject, $args, &$mutationFields) + { + $resolve = function ($type, $args, $context, $info) use ($collectionId, &$register, $dbForProject) { + return SwoolePromise::create(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), + 'args' => $args, + 'resolve' => $resolve + ]; + $mutationFields['create' . \ucfirst($collectionId)] = $create; + } + + private static function createCollectionUpdateMutation($collectionId, $register, $dbForProject, $args, &$mutationFields) + { + $resolve = function ($type, $args, $context, $info) use ($collectionId, &$register, $dbForProject) { + return SwoolePromise::create(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), + 'args' => $args, + 'resolve' => $resolve + ]; + + $mutationFields['update' . \ucfirst($collectionId)] = $update; + } + + + private static function createCollectionDeleteMutation($collectionId, $register, $dbForProject, $args, &$mutationFields) + { + $resolve = function ($type, $args, $context, $info) use ($collectionId, &$register, $dbForProject) { + return SwoolePromise::create(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), + 'args' => $args, + 'resolve' => $resolve + ]; + $mutationFields['delete' . \ucfirst($collectionId)] = $delete; + } + /** * 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 @@ -315,10 +463,9 @@ class Builder * @param $utopia * @param $response * @param $register - * @param $dbForProject * @return array */ - public static function buildAPISchema($utopia, $response, $register, $dbForProject): array + public static function buildAPISchema($utopia, $response, $register): array { Console::log("[INFO] Building GraphQL API Schema..."); $start = microtime(true); @@ -329,12 +476,17 @@ class Builder foreach ($utopia->getRoutes() as $method => $routes) { foreach ($routes as $route) { - $namespace = $route->getLabel('sdk.namespace', ''); - $methodName = $namespace . '_' . $route->getLabel('sdk.method', ''); - $responseModelName = $route->getLabel('sdk.response.model', ""); + $methodName = $namespace . \ucfirst($route->getLabel('sdk.method', '')); + $responseModelName = $route->getLabel('sdk.response.model', "none"); - if ($responseModelName !== "") { + 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 ($responseModelName !== "none") { $responseModel = $response->getModel($responseModelName); /* Create a GraphQL type for the current response model */ @@ -351,8 +503,8 @@ class Builder ]; } /* Define a resolve function that defines how to fetch data for this type */ - $resolve = function ($type, $args, $context, $info) use (&$register, $route, $dbForProject) { - return SwoolePromise::create(function (callable $resolve, callable $reject) use (&$register, $route, $dbForProject, $args) { + $resolve = function ($type, $args, $context, $info) use (&$register, $route) { + return SwoolePromise::create(function (callable $resolve, callable $reject) use (&$register, $route, $args) { $utopia = $register->get('__app'); $utopia->setRoute($route)->execute($route, $args); @@ -403,12 +555,12 @@ class Builder * @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) { + return function (Error $error) use ($isDevelopment, $version) { $formattedError = FormattedError::createFromException($error); - /** Previous error represents the actual error thrown by Appwrite server */ + + // Previous error represents the actual error thrown by Appwrite server $previousError = $error->getPrevious() ?? $error; $formattedError['code'] = $previousError->getCode(); $formattedError['version'] = $version; @@ -418,7 +570,5 @@ class Builder } return $formattedError; }; - - return $errorFormatter; } } diff --git a/src/Appwrite/GraphQL/SwoolePromise.php b/src/Appwrite/GraphQL/SwoolePromise.php index 64f769db2..f0329190e 100644 --- a/src/Appwrite/GraphQL/SwoolePromise.php +++ b/src/Appwrite/GraphQL/SwoolePromise.php @@ -2,8 +2,8 @@ namespace Appwrite\GraphQL; -use Swoole\Coroutine; use Swoole\Coroutine\Channel; +use function Co\go; /** * Class SwoolePromise @@ -40,7 +40,8 @@ class SwoolePromise $this->setState(self::STATE_REJECTED); } }; - Coroutine::create(function (callable $executor, callable $resolve, callable $reject) { + + go(function (callable $executor, callable $resolve, callable $reject) { try { $executor($resolve, $reject); } catch (\Throwable $exception) { diff --git a/src/Appwrite/GraphQL/GraphQLPromiseAdapter.php b/src/Appwrite/GraphQL/SwoolePromiseAdapter.php similarity index 59% rename from src/Appwrite/GraphQL/GraphQLPromiseAdapter.php rename to src/Appwrite/GraphQL/SwoolePromiseAdapter.php index 06b889809..eac1c0d29 100644 --- a/src/Appwrite/GraphQL/GraphQLPromiseAdapter.php +++ b/src/Appwrite/GraphQL/SwoolePromiseAdapter.php @@ -6,17 +6,19 @@ use GraphQL\Error\InvariantViolation; use GraphQL\Executor\Promise\Promise; use GraphQL\Executor\Promise\PromiseAdapter; use GraphQL\Utils\Utils; +use function Co\go; +use function Co\run; -class GraphQLPromiseAdapter implements PromiseAdapter +class SwoolePromiseAdapter implements PromiseAdapter { public function isThenable($value): bool { - return $value instanceof SwoolePromise; + return $value instanceof Promise; } public function convertThenable($thenable): Promise { - if (!$thenable instanceof SwoolePromise) { + if (!$thenable instanceof Promise) { throw new InvariantViolation('Expected instance of SwoolePromise, got ' . Utils::printSafe($thenable)); } return new Promise($thenable, $this); @@ -66,25 +68,30 @@ class GraphQLPromiseAdapter implements PromiseAdapter $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; + run(function ($promisesOrValues, $all, $total, &$count, $result) { + foreach ($promisesOrValues as $index => $promiseOrValue) { + go(function ($index, $promiseOrValue, $all, $total, &$count, $result) { + if (!($promiseOrValue instanceof SwoolePromise)) { + $result[$index] = $promiseOrValue; $count++; - if ($count < $total) { - return; - } - $all->resolve($result); - }, - [$all, 'reject'] - ); - } else { - $result[$index] = $promiseOrValue; - $count++; + return; + } + $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'] + ); + }, $index, $promiseOrValue, $all, $total, $count, $result); } - } + }, $promisesOrValues, $all, $total, $count, $result); + if ($count === $total) { $all->resolve($result); } diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 723f210b8..6f392c2b3 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -89,7 +89,7 @@ abstract class Migration */ public function forEachDocument(callable $callback): void { - Runtime::enableCoroutine(SWOOLE_HOOK_ALL); + Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); foreach ($this->collections as $collection) { $sum = 0;