1
0
Fork 0
mirror of synced 2024-06-01 18:39:57 +12:00

Add resolvers for get, list, create, update, delete for user collections

This commit is contained in:
Jake Barnby 2022-04-07 11:23:20 +12:00
parent 69e7c2fed9
commit 48ba76f365
No known key found for this signature in database
GPG key ID: A4674EBC0E404657
9 changed files with 279 additions and 116 deletions

View file

@ -1,6 +1,5 @@
<?php
use Appwrite\GraphQL\GraphQLPromiseAdapter;
use Appwrite\Utopia\Response;
use GraphQL\Error\DebugFlag;
use GraphQL\Executor\ExecutionResult;
@ -29,9 +28,15 @@ App::post('/v1/graphql')
$query = $request->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

View file

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

View file

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

24
composer.lock generated
View file

@ -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",

View file

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

View file

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

View file

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

View file

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

View file

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