1
0
Fork 0
mirror of synced 2024-06-28 19:20:25 +12:00

Restructure schema building

This commit is contained in:
Jake Barnby 2022-10-12 14:04:11 +13:00
parent 39fcbe4d76
commit b6621f5e87
No known key found for this signature in database
GPG key ID: C437A8CC85B96E9C
5 changed files with 290 additions and 334 deletions

View file

@ -1,7 +1,7 @@
<?php
use Appwrite\Extend\Exception;
use Appwrite\GraphQL\Promises\CoroutinePromiseAdapter;
use Appwrite\GraphQL\Promises\Adapter;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use GraphQL\Error\DebugFlag;
@ -85,7 +85,7 @@ App::post('/v1/graphql/upload')
*
* @param Request $request
* @param Response $response
* @param CoroutinePromiseAdapter $promiseAdapter
* @param Adapter $promiseAdapter
* @param Type\Schema $schema
* @return void
* @throws Exception
@ -93,7 +93,7 @@ App::post('/v1/graphql/upload')
function executeRequest(
Appwrite\Utopia\Request $request,
Appwrite\Utopia\Response $response,
CoroutinePromiseAdapter $promiseAdapter,
Adapter $promiseAdapter,
Type\Schema $schema
): void {
$query = $request->getParams();

View file

@ -21,12 +21,6 @@ error_reporting(E_ALL);
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\SMS\Adapter\Mock;
use Appwrite\SMS\Adapter\Telesign;
use Appwrite\SMS\Adapter\TextMagic;
use Appwrite\SMS\Adapter\Twilio;
use Appwrite\SMS\Adapter\Msg91;
use Appwrite\SMS\Adapter\Vonage;
use Appwrite\DSN\DSN;
use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
@ -36,15 +30,20 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Phone;
use Appwrite\Extend\Exception;
use Appwrite\Extend\PDO;
use Appwrite\GraphQL\SchemaBuilder;
use Appwrite\GraphQL\Promises\CoroutinePromiseAdapter;
use Appwrite\GraphQL\Promises\Adapter\Swoole;
use Appwrite\GraphQL\Schema;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
use Appwrite\Network\Validator\URL;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\SMS\Adapter\Mock;
use Appwrite\SMS\Adapter\Msg91;
use Appwrite\SMS\Adapter\Telesign;
use Appwrite\SMS\Adapter\TextMagic;
use Appwrite\SMS\Adapter\Twilio;
use Appwrite\SMS\Adapter\Vonage;
use Appwrite\Usage\Stats;
use Appwrite\Utopia\View;
use Utopia\Database\ID;
use MaxMind\Db\Reader;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Database\PDOConfig;
@ -58,9 +57,10 @@ use Utopia\Config\Config;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\DatetimeValidator;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\DatetimeValidator;
use Utopia\Database\Validator\Structure;
use Utopia\Locale\Locale;
use Utopia\Logger\Logger;
@ -632,7 +632,7 @@ $register->set('cache', function () {
return $redis;
});
$register->set('promiseAdapter', function () {
return new CoroutinePromiseAdapter();
return new Swoole();
});
/*
@ -1061,5 +1061,5 @@ App::setResource('promiseAdapter', function ($register) {
}, ['register']);
App::setResource('schema', function ($utopia, $project, $dbForProject) {
return SchemaBuilder::buildSchema($utopia, $project->getId(), $dbForProject);
return Schema::build($utopia, $project->getId(), $dbForProject);
}, ['utopia', 'project', 'dbForProject']);

View file

@ -2,7 +2,7 @@
namespace Appwrite\GraphQL;
use Appwrite\GraphQL\Promises\CoroutinePromise;
use Appwrite\Promises\Swoole;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
@ -14,17 +14,17 @@ use Utopia\Route;
class Resolvers
{
/**
* Create a resolver for a given {@see Route}.
* Create a resolver for a given API {@see Route}.
*
* @param App $utopia
* @param ?Route $route
* @return callable
*/
public static function resolveAPIRequest(
public static function api(
App $utopia,
?Route $route,
): callable {
return static fn($type, $args, $context, $info) => new CoroutinePromise(
return static fn($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) {
$utopia = $utopia->getResource('current', true);
$request = $utopia->getResource('request', true);
@ -57,22 +57,23 @@ class Resolvers
}
/**
* Create a resolver for getting a document in a specified database and collection.
* Create a resolver for a document in a specified database and collection with a specific method type.
*
* @param App $utopia
* @param Database $dbForProject
* @param string $databaseId
* @param string $collectionId
* @param string $methodType
* @return callable
*/
public static function resolveDocument(
public static function document(
App $utopia,
Database $dbForProject,
string $databaseId,
string $collectionId,
string $methodType,
): callable {
return [self::class, 'resolveDocument' . \ucfirst($methodType)](
return [self::class, 'document' . \ucfirst($methodType)](
$utopia,
$dbForProject,
$databaseId,
@ -89,13 +90,13 @@ class Resolvers
* @param string $collectionId
* @return callable
*/
public static function resolveDocumentGet(
public static function documentGet(
App $utopia,
Database $dbForProject,
string $databaseId,
string $collectionId
): callable {
return static fn($type, $args, $context, $info) => new CoroutinePromise(
return static fn($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $type, $args) {
$utopia = $utopia->getResource('current', true);
$request = $utopia->getResource('request', true);
@ -120,13 +121,13 @@ class Resolvers
* @param string $collectionId
* @return callable
*/
public static function resolveDocumentList(
public static function documentList(
App $utopia,
Database $dbForProject,
string $databaseId,
string $collectionId,
): callable {
return static fn($type, $args, $context, $info) => new CoroutinePromise(
return static fn($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $type, $args) {
$utopia = $utopia->getResource('current', true);
$request = $utopia->getResource('request', true);
@ -159,13 +160,13 @@ class Resolvers
* @param string $collectionId
* @return callable
*/
public static function resolveDocumentCreate(
public static function documentCreate(
App $utopia,
Database $dbForProject,
string $databaseId,
string $collectionId,
): callable {
return static fn($type, $args, $context, $info) => new CoroutinePromise(
return static fn($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $type, $args) {
$utopia = $utopia->getResource('current', true);
$request = $utopia->getResource('request', true);
@ -204,13 +205,13 @@ class Resolvers
* @param string $collectionId
* @return callable
*/
public static function resolveDocumentUpdate(
public static function documentUpdate(
App $utopia,
Database $dbForProject,
string $databaseId,
string $collectionId,
): callable {
return static fn($type, $args, $context, $info) => new CoroutinePromise(
return static fn($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $type, $args) {
$utopia = $utopia->getResource('current', true);
$request = $utopia->getResource('request', true);
@ -249,13 +250,13 @@ class Resolvers
* @param string $collectionId
* @return callable
*/
public static function resolveDocumentDelete(
public static function documentDelete(
App $utopia,
Database $dbForProject,
string $databaseId,
string $collectionId
): callable {
return static fn($type, $args, $context, $info) => new CoroutinePromise(
return static fn($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $type, $args) {
$utopia = $utopia->getResource('current', true);
$request = $utopia->getResource('request', true);
@ -279,10 +280,12 @@ class Resolvers
* @param Response $response
* @param callable $resolve
* @param callable $reject
* @param callable|null $beforeResolve
* @param callable|null $beforeReject
* @return void
* @throws Exception
*/
public static function resolve(
private static function resolve(
App $utopia,
Request $request,
Response $response,
@ -318,7 +321,7 @@ class Resolvers
if ($beforeReject) {
$payload = $beforeReject($payload);
}
$reject(new GQLException(
$reject(new Exception(
message: $payload['message'],
code: $response->getStatusCode()
));

View file

@ -0,0 +1,253 @@
<?php
namespace Appwrite\GraphQL;
use Appwrite\GraphQL\Types\Mapper;
use Appwrite\Utopia\Response;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema as GQLSchema;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Route;
class Schema
{
protected static ?GQLSchema $schema = null;
protected static bool $dirty = false;
/**
* @throws \Exception
*/
public static function build(
App $utopia,
string $projectId,
Database $dbForProject
): GQLSchema {
App::setResource('utopia:self', static function () use ($utopia) {
return $utopia;
});
if (!self::$dirty && self::$schema) {
return self::$schema;
}
$api = static::api($utopia);
//$collections = static::collections($utopia, $dbForProject);
$queries = \array_merge_recursive(
$api['query'],
//$collections['query']
);
$mutations = \array_merge_recursive(
$api['mutation'],
//$collections['mutation']
);
\ksort($queries);
\ksort($mutations);
return static::$schema = new GQLSchema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => $queries
]),
'mutation' => new ObjectType([
'name' => 'Mutation',
'fields' => $mutations
])
]);
}
/**
* This function iterates all API routes and builds a GraphQL
* schema defining types and resolvers for all response models.
*
* @param App $utopia
* @return array
* @throws \Exception
*/
public static function api(App $utopia): array
{
Mapper::init($utopia
->getResource('response')
->getModels());
$queries = [];
$mutations = [];
foreach ($utopia->getRoutes() as $routes) {
foreach ($routes as $route) {
/** @var Route $route */
$namespace = $route->getLabel('sdk.namespace', '');
$method = $route->getLabel('sdk.method', '');
$name = $namespace . \ucfirst($method);
if (empty($name)) {
continue;
}
foreach (Mapper::route($utopia, $route) as $field) {
switch ($route->getMethod()) {
case 'GET':
$queries[$name] = $field;
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
$mutations[$name] = $field;
break;
default:
throw new \Exception("Unsupported method: {$route->getMethod()}");
}
}
}
}
return [
'query' => $queries,
'mutation' => $mutations
];
}
/**
* Iterates all of a projects attributes and builds GraphQL
* queries and mutations for the collections they make up.
*
* @param App $utopia
* @param Database $dbForProject
* @return array
* @throws \Exception
*/
public static function collections(
App $utopia,
Database $dbForProject
): array {
$collections = [];
$queryFields = [];
$mutationFields = [];
$limit = 1000;
$offset = 0;
$count = 0;
while (
!empty($attrs = Authorization::skip(fn() => $dbForProject->find('attributes', [
Query::limit($limit),
Query::offset($offset),
])))
) {
$count += count($attrs);
foreach ($attrs as $attr) {
if ($attr->getAttribute('status') !== 'available') {
continue;
}
$databaseId = $attr->getAttribute('databaseId');
$collectionId = $attr->getAttribute('collectionId');
$key = $attr->getAttribute('key');
$type = $attr->getAttribute('type');
$array = $attr->getAttribute('array');
$required = $attr->getAttribute('required');
$default = $attr->getAttribute('default');
$escapedKey = str_replace('$', '_', $key);
$collections[$collectionId][$escapedKey] = [
'type' => Mapper::fromCollectionAttribute(
$type,
$array,
$required
),
'defaultValue' => $default,
];
}
foreach ($collections as $collectionId => $attributes) {
$objectType = new ObjectType([
'name' => $collectionId,
'fields' => \array_merge(
["_id" => ['type' => Type::string()]],
$attributes
),
]);
$attributes = \array_merge(
$attributes,
Mapper::argumentsFor('mutate')
);
$queryFields[$collectionId . 'Get'] = [
'type' => $objectType,
'args' => Mapper::argumentsFor('id'),
'resolve' => Resolvers::documentGet(
$utopia,
$dbForProject,
$databaseId,
$collectionId
)
];
$queryFields[$collectionId . 'List'] = [
'type' => Type::listOf($objectType),
'args' => Mapper::argumentsFor('list'),
'resolve' => Resolvers::documentList(
$utopia,
$dbForProject,
$databaseId,
$collectionId
),
'complexity' => function (int $complexity, array $args) {
$queries = Query::parseQueries($args['queries'] ?? []);
$query = Query::getByType($queries, Query::TYPE_LIMIT)[0] ?? null;
$limit = $query ? $query->getValue() : APP_LIMIT_LIST_DEFAULT;
return $complexity * $limit;
},
];
$mutationFields[$collectionId . 'Create'] = [
'type' => $objectType,
'args' => $attributes,
'resolve' => Resolvers::documentCreate(
$utopia,
$dbForProject,
$databaseId,
$collectionId,
)
];
$mutationFields[$collectionId . 'Update'] = [
'type' => $objectType,
'args' => \array_merge(
Mapper::argumentsFor('id'),
\array_map(
fn($attr) => $attr['type'] = Type::getNullableType($attr['type']),
$attributes
)
),
'resolve' => Resolvers::documentUpdate(
$utopia,
$dbForProject,
$databaseId,
$collectionId,
)
];
$mutationFields[$collectionId . 'Delete'] = [
'type' => Mapper::fromResponseModel(Response::MODEL_NONE),
'args' => Mapper::argumentsFor('id'),
'resolve' => Resolvers::documentDelete(
$utopia,
$dbForProject,
$databaseId,
$collectionId
)
];
}
$offset += $limit;
}
return [
'query' => $queryFields,
'mutation' => $mutationFields
];
}
}

View file

@ -1,300 +0,0 @@
<?php
namespace Appwrite\GraphQL;
use Appwrite\Utopia\Response;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use Swoole\Coroutine\WaitGroup;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Registry\Registry;
use Utopia\Route;
class SchemaBuilder
{
/**
* @throws \Exception
*/
public static function buildSchema(
App $utopia,
string $projectId,
Database $dbForProject
): Schema {
App::setResource('current', static fn() => $utopia);
/** @var Registry $register */
$register = $utopia->getResource('register');
$appVersion = App::getEnv('_APP_VERSION');
$apiSchemaKey = 'apiSchema';
$apiVersionKey = 'apiSchemaVersion';
$collectionSchemaKey = $projectId . 'CollectionSchema';
$collectionsDirtyKey = $projectId . 'SchemaDirty';
$fullSchemaKey = $projectId . 'FullSchema';
$schemaVersion = $register->has($apiVersionKey) ? $register->get($apiVersionKey) : '';
$collectionSchemaDirty = $register->has($collectionsDirtyKey) ? $register->get($collectionsDirtyKey) : true;
$apiSchemaDirty = \version_compare($appVersion, $schemaVersion, "!=");
if (
!$collectionSchemaDirty
&& !$apiSchemaDirty
&& $register->has($fullSchemaKey)
) {
return $register->get($fullSchemaKey);
}
if ($register->has($apiSchemaKey) && !$apiSchemaDirty) {
$apiSchema = $register->get($apiSchemaKey);
} else {
$apiSchema = &self::buildAPISchema($utopia);
$register->set($apiSchemaKey, static function &() use (&$apiSchema) {
return $apiSchema;
});
$register->set($apiVersionKey, static fn() => $appVersion);
}
if ($register->has($collectionSchemaKey) && !$collectionSchemaDirty) {
$collectionSchema = $register->get($collectionSchemaKey);
} else {
$collectionSchema = &self::buildCollectionSchema($utopia, $dbForProject);
$register->set($collectionSchemaKey, static function &() use (&$collectionSchema) {
return $collectionSchema;
});
$register->set($collectionsDirtyKey, static fn() => false);
}
$queryFields = \array_merge_recursive(
$apiSchema['query'],
$collectionSchema['query']
);
$mutationFields = \array_merge_recursive(
$apiSchema['mutation'],
$collectionSchema['mutation']
);
\ksort($queryFields);
\ksort($mutationFields);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => $queryFields
]),
'mutation' => new ObjectType([
'name' => 'Mutation',
'fields' => $mutationFields
])
]);
$register->set($fullSchemaKey, static fn() => $schema);
return $schema;
}
/**
* This function iterates all API routes and builds a GraphQL
* schema defining types and resolvers for all response models.
*
* @param App $utopia
* @return array
* @throws \Exception
*/
public static function &buildAPISchema(App $utopia): array
{
$models = $utopia
->getResource('response')
->getModels();
TypeMapper::init($models);
$queries = [];
$mutations = [];
foreach (App::getRoutes() as $type => $routes) {
foreach ($routes as $route) {
/** @var Route $route */
$namespace = $route->getLabel('sdk.namespace', '');
$method = $route->getLabel('sdk.method', '');
$name = $namespace . \ucfirst($method);
if (empty($name)) {
continue;
}
foreach (TypeMapper::fromRoute($utopia, $route) as $field) {
switch ($route->getMethod()) {
case 'GET':
$queries[$name] = $field;
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
$mutations[$name] = $field;
break;
default:
throw new \Exception("Unsupported method: {$route->getMethod()}");
}
}
}
}
$schema = [
'query' => $queries,
'mutation' => $mutations
];
return $schema;
}
/**
* Iterates all of a projects attributes and builds GraphQL
* queries and mutations for the collections they make up.
*
* @param App $utopia
* @param Database $dbForProject
* @return array
* @throws \Exception
*/
public static function &buildCollectionSchema(
App $utopia,
Database $dbForProject
): array {
$collections = [];
$queryFields = [];
$mutationFields = [];
$limit = 1000;
$offset = 0;
$count = 0;
$wg = new WaitGroup();
while (
!empty($attrs = Authorization::skip(fn() => $dbForProject->find('attributes', [
Query::limit($limit),
Query::offset($offset),
])))
) {
$wg->add();
$count += count($attrs);
\go(function () use ($utopia, $dbForProject, &$collections, &$queryFields, &$mutationFields, $limit, &$offset, $attrs, $wg) {
foreach ($attrs as $attr) {
if ($attr->getAttribute('status') !== 'available') {
continue;
}
$databaseId = $attr->getAttribute('databaseId');
$collectionId = $attr->getAttribute('collectionId');
$key = $attr->getAttribute('key');
$type = $attr->getAttribute('type');
$array = $attr->getAttribute('array');
$required = $attr->getAttribute('required');
$default = $attr->getAttribute('default');
$escapedKey = str_replace('$', '_', $key);
$collections[$collectionId][$escapedKey] = [
'type' => TypeMapper::fromCollectionAttribute(
$type,
$array,
$required
),
'defaultValue' => $default,
];
}
foreach ($collections as $collectionId => $attributes) {
$objectType = new ObjectType([
'name' => $collectionId,
'fields' => \array_merge(
["_id" => ['type' => Type::string()]],
$attributes
),
]);
$attributes = \array_merge(
$attributes,
TypeMapper::argumentsFor('mutate')
);
$queryFields[$collectionId . 'Get'] = [
'type' => $objectType,
'args' => TypeMapper::argumentsFor('id'),
'resolve' => Resolvers::resolveDocumentGet(
$utopia,
$dbForProject,
$databaseId,
$collectionId
)
];
$queryFields[$collectionId . 'List'] = [
'type' => Type::listOf($objectType),
'args' => TypeMapper::argumentsFor('list'),
'resolve' => Resolvers::resolveDocumentList(
$utopia,
$dbForProject,
$databaseId,
$collectionId
),
'complexity' => function (int $complexity, array $args) {
$queries = Query::parseQueries($args['queries'] ?? []);
$query = Query::getByType($queries, Query::TYPE_LIMIT)[0] ?? null;
$limit = $query ? $query->getValue() : APP_LIMIT_LIST_DEFAULT;
return $complexity * $limit;
},
];
$mutationFields[$collectionId . 'Create'] = [
'type' => $objectType,
'args' => $attributes,
'resolve' => Resolvers::resolveDocumentCreate(
$utopia,
$dbForProject,
$databaseId,
$collectionId,
)
];
$mutationFields[$collectionId . 'Update'] = [
'type' => $objectType,
'args' => \array_merge(
TypeMapper::argumentsFor('id'),
\array_map(
fn($attr) => $attr['type'] = Type::getNullableType($attr['type']),
$attributes
)
),
'resolve' => Resolvers::resolveDocumentUpdate(
$utopia,
$dbForProject,
$databaseId,
$collectionId,
)
];
$mutationFields[$collectionId . 'Delete'] = [
'type' => TypeMapper::fromResponseModel(Response::MODEL_NONE),
'args' => TypeMapper::argumentsFor('id'),
'resolve' => Resolvers::resolveDocumentDelete(
$utopia,
$dbForProject,
$databaseId,
$collectionId
)
];
}
$wg->done();
});
$offset += $limit;
}
$wg->wait();
$schema = [
'query' => $queryFields,
'mutation' => $mutationFields
];
return $schema;
}
}