Code cleanup
This commit is contained in:
parent
92c8e2c3a1
commit
a656699fa7
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\GraphQL\CoroutinePromiseAdapter;
|
||||
use Appwrite\GraphQL\Promises\CoroutinePromiseAdapter;
|
||||
use Appwrite\Utopia\Response;
|
||||
use GraphQL\Error\DebugFlag;
|
||||
use GraphQL\GraphQL;
|
||||
|
|
45
app/init.php
45
app/init.php
|
@ -18,16 +18,14 @@ ini_set('display_startup_errors', 1);
|
|||
ini_set('default_socket_timeout', -1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
use Appwrite\Extend\PDO;
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Phone\Mock;
|
||||
use Appwrite\Auth\Phone\Msg91;
|
||||
use Appwrite\Auth\Phone\Telesign;
|
||||
use Appwrite\Auth\Phone\TextMagic;
|
||||
use Appwrite\Auth\Phone\Twilio;
|
||||
use Appwrite\Auth\Phone\Msg91;
|
||||
use Appwrite\DSN\DSN;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
|
@ -35,44 +33,45 @@ use Appwrite\Event\Delete;
|
|||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Phone;
|
||||
use Appwrite\GraphQL\Builder;
|
||||
use Appwrite\GraphQL\CoroutinePromiseAdapter;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Extend\PDO;
|
||||
use Appwrite\GraphQL\SchemaBuilder;
|
||||
use Appwrite\GraphQL\Promises\CoroutinePromiseAdapter;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Network\Validator\IP;
|
||||
use Appwrite\Network\Validator\URL;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\Stats\Stats;
|
||||
use Appwrite\Utopia\View;
|
||||
use Utopia\App;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Registry\Registry;
|
||||
use MaxMind\Db\Reader;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Database\Adapter\MariaDB;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Structure;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Swoole\Database\PDOConfig;
|
||||
use Swoole\Database\PDOPool;
|
||||
use Swoole\Database\RedisConfig;
|
||||
use Swoole\Database\RedisPool;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Adapter\MariaDB;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Structure;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\Storage\Device\Backblaze;
|
||||
use Utopia\Storage\Device\DOSpaces;
|
||||
use Utopia\Storage\Device\Linode;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Storage\Device\S3;
|
||||
use Utopia\Storage\Device\Linode;
|
||||
use Utopia\Storage\Device\Wasabi;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
const APP_NAME = 'Appwrite';
|
||||
const APP_DOMAIN = 'appwrite.io';
|
||||
|
@ -1008,5 +1007,5 @@ App::setResource('promiseAdapter', function ($register) {
|
|||
}, ['register']);
|
||||
|
||||
App::setResource('gqlSchema', function ($utopia, $dbForProject, $user) {
|
||||
return Builder::buildSchema($utopia, $dbForProject, $user);
|
||||
return SchemaBuilder::buildSchema($utopia, $dbForProject, $user);
|
||||
}, ['utopia', 'dbForProject', 'user']);
|
||||
|
|
|
@ -1,849 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL;
|
||||
|
||||
use Appwrite\GraphQL\Types\InputFile;
|
||||
use Appwrite\GraphQL\Types\Json;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema;
|
||||
use Swoole\Coroutine\WaitGroup;
|
||||
use Swoole\Http\Response as SwooleResponse;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Route;
|
||||
use Utopia\Validator;
|
||||
|
||||
use function Co\go;
|
||||
|
||||
class Builder
|
||||
{
|
||||
protected static ?Json $jsonType = null;
|
||||
protected static ?InputFile $inputFile = null;
|
||||
|
||||
protected static array $typeMapping = [];
|
||||
protected static array $defaultDocumentArgs = [];
|
||||
|
||||
/**
|
||||
* Initialise the typeMapping array with the base cases of the recursion
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void
|
||||
{
|
||||
self::$typeMapping = [
|
||||
Model::TYPE_BOOLEAN => Type::boolean(),
|
||||
Model::TYPE_STRING => Type::string(),
|
||||
Model::TYPE_INTEGER => Type::int(),
|
||||
Model::TYPE_FLOAT => Type::float(),
|
||||
Model::TYPE_JSON => self::json(),
|
||||
Response::MODEL_NONE => self::json(),
|
||||
Response::MODEL_ANY => self::json(),
|
||||
];
|
||||
|
||||
self::$defaultDocumentArgs = [
|
||||
'id' => [
|
||||
'id' => [
|
||||
'type' => Type::string(),
|
||||
],
|
||||
],
|
||||
'list' => [
|
||||
'limit' => [
|
||||
'type' => Type::int(),
|
||||
'defaultValue' => 25,
|
||||
],
|
||||
'offset' => [
|
||||
'type' => Type::int(),
|
||||
'defaultValue' => 0,
|
||||
],
|
||||
'cursor' => [
|
||||
'type' => Type::string(),
|
||||
'defaultValue' => '',
|
||||
],
|
||||
'cursorDirection' => [
|
||||
'type' => Type::string(),
|
||||
'defaultValue' => Database::CURSOR_AFTER,
|
||||
],
|
||||
'orderAttributes' => [
|
||||
'type' => Type::listOf(Type::string()),
|
||||
'defaultValue' => [],
|
||||
],
|
||||
'orderType' => [
|
||||
'type' => Type::listOf(Type::string()),
|
||||
'defaultValue' => [],
|
||||
],
|
||||
],
|
||||
'mutate' => [
|
||||
'read' => [
|
||||
'type' => Type::listOf(Type::string()),
|
||||
'defaultValue' => ["role:member"],
|
||||
],
|
||||
'write' => [
|
||||
'type' => Type::listOf(Type::string()),
|
||||
'defaultValue' => ["role:member"],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function json(): Json
|
||||
{
|
||||
if (is_null(self::$jsonType)) {
|
||||
self::$jsonType = new Json();
|
||||
}
|
||||
return self::$jsonType;
|
||||
}
|
||||
|
||||
public static function inputFile(): InputFile
|
||||
{
|
||||
if (is_null(self::$inputFile)) {
|
||||
self::$inputFile = new InputFile();
|
||||
}
|
||||
return self::$inputFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a GraphQL type from a Utopia Model
|
||||
*
|
||||
* @param Model $model
|
||||
* @param array $models
|
||||
* @return Type
|
||||
*/
|
||||
private static function getModelTypeMapping(Model $model, array $models): Type
|
||||
{
|
||||
if (isset(self::$typeMapping[$model->getType()])) {
|
||||
return self::$typeMapping[$model->getType()];
|
||||
}
|
||||
|
||||
$rules = $model->getRules();
|
||||
$name = $model->getType();
|
||||
$fields = [];
|
||||
|
||||
if ($model->isAny()) {
|
||||
$fields['data'] = [
|
||||
'type' => Type::string(),
|
||||
'description' => 'Data field',
|
||||
'resolve' => fn($object, $args, $context, $info) => \json_encode($object, JSON_FORCE_OBJECT),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($rules as $key => $props) {
|
||||
$escapedKey = str_replace('$', '_', $key);
|
||||
|
||||
$types = \is_array($props['type'])
|
||||
? $props['type']
|
||||
: [$props['type']];
|
||||
|
||||
foreach ($types as $type) {
|
||||
if (isset(self::$typeMapping[$type])) {
|
||||
$type = self::$typeMapping[$type];
|
||||
} else {
|
||||
try {
|
||||
$complexModel = $models[$type];
|
||||
$type = self::getModelTypeMapping($complexModel, $models);
|
||||
} 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' => fn($object, $args, $context, $info) => $object[$key],
|
||||
];
|
||||
}
|
||||
}
|
||||
$objectType = [
|
||||
'name' => $name,
|
||||
'fields' => $fields
|
||||
];
|
||||
|
||||
self::$typeMapping[$name] = new ObjectType($objectType);
|
||||
|
||||
return self::$typeMapping[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a Utopia\Validator to a valid GraphQL Type
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param Validator|callable $validator
|
||||
* @param bool $required
|
||||
* @param array $injections
|
||||
* @return Type
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function getParameterType(
|
||||
App $utopia,
|
||||
Validator|callable $validator,
|
||||
bool $required,
|
||||
array $injections
|
||||
): Type {
|
||||
$validator = \is_callable($validator)
|
||||
? \call_user_func_array($validator, $utopia->getResources($injections))
|
||||
: $validator;
|
||||
|
||||
switch ((!empty($validator)) ? \get_class($validator) : '') {
|
||||
case 'Appwrite\Auth\Validator\Password':
|
||||
case 'Appwrite\Event\Validator\Event':
|
||||
case 'Appwrite\Network\Validator\CNAME':
|
||||
case 'Appwrite\Network\Validator\Domain':
|
||||
case 'Appwrite\Network\Validator\Email':
|
||||
case 'Appwrite\Network\Validator\Host':
|
||||
case 'Appwrite\Network\Validator\IP':
|
||||
case 'Appwrite\Network\Validator\Origin':
|
||||
case 'Appwrite\Network\Validator\URL':
|
||||
case 'Appwrite\Task\Validator\Cron':
|
||||
case 'Appwrite\Utopia\Database\Validator\CustomId':
|
||||
case 'Utopia\Database\Validator\Key':
|
||||
case 'Utopia\Database\Validator\CustomId':
|
||||
case 'Utopia\Database\Validator\UID':
|
||||
case 'Utopia\Validator\HexColor':
|
||||
case 'Utopia\Validator\Length':
|
||||
case 'Utopia\Validator\Text':
|
||||
case 'Utopia\Validator\WhiteList':
|
||||
default:
|
||||
$type = Type::string();
|
||||
break;
|
||||
case 'Utopia\Validator\Boolean':
|
||||
$type = Type::boolean();
|
||||
break;
|
||||
case 'Utopia\Validator\ArrayList':
|
||||
$type = Type::listOf(self::getParameterType(
|
||||
$utopia,
|
||||
$validator->getValidator(),
|
||||
$required,
|
||||
$injections
|
||||
));
|
||||
break;
|
||||
case 'Utopia\Validator\Numeric':
|
||||
case 'Utopia\Validator\Integer':
|
||||
case 'Utopia\Validator\Range':
|
||||
$type = Type::int();
|
||||
break;
|
||||
case 'Utopia\Validator\FloatValidator':
|
||||
$type = Type::float();
|
||||
break;
|
||||
case 'Utopia\Database\Validator\Authorization':
|
||||
case 'Utopia\Database\Validator\Permissions':
|
||||
$type = Type::listOf(Type::string());
|
||||
break;
|
||||
case 'Utopia\Validator\Assoc':
|
||||
case 'Utopia\Validator\JSON':
|
||||
$type = self::json();
|
||||
break;
|
||||
case 'Utopia\Storage\Validator\File':
|
||||
$type = self::inputFile();
|
||||
break;
|
||||
}
|
||||
|
||||
if ($required) {
|
||||
$type = Type::nonNull($type);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an Attribute type to a valid GraphQL Type
|
||||
*
|
||||
* @param string $type
|
||||
* @param bool $array
|
||||
* @param bool $required
|
||||
* @return Type
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function getAttributeType(string $type, bool $array, bool $required): Type
|
||||
{
|
||||
if ($array) {
|
||||
return Type::listOf(self::getAttributeType($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
|
||||
*/
|
||||
public static function buildSchema(
|
||||
App $utopia,
|
||||
Database $dbForProject,
|
||||
Document $user,
|
||||
): Schema {
|
||||
App::setResource('current', fn() => $utopia);
|
||||
|
||||
$start = microtime(true);
|
||||
$register = $utopia->getResource('register');
|
||||
$envVersion = App::getEnv('_APP_VERSION');
|
||||
$schemaVersion = $register->has('apiSchemaVersion') ? $register->get('apiSchemaVersion') : '';
|
||||
$collectionSchemaDirty = $register->has('schemaDirty') && $register->get('schemaDirty');
|
||||
$apiSchemaDirty = \version_compare($envVersion, $schemaVersion, "!=");
|
||||
|
||||
if (
|
||||
!$collectionSchemaDirty
|
||||
&& !$apiSchemaDirty
|
||||
&& $register->has('fullSchema')
|
||||
) {
|
||||
$timeElapsedMillis = (microtime(true) - $start) * 1000;
|
||||
$timeElapsedMillis = \number_format((float) $timeElapsedMillis, 3, '.', '');
|
||||
Console::info('[INFO] Fetched GraphQL Schema in ' . $timeElapsedMillis . 'ms');
|
||||
|
||||
return $register->get('fullSchema');
|
||||
}
|
||||
|
||||
if ($register->has('apiSchema') && !$apiSchemaDirty) {
|
||||
$apiSchema = $register->get('apiSchema');
|
||||
} else {
|
||||
$apiSchema = self::buildAPISchema($utopia);
|
||||
$register->set('apiSchema', fn() => $apiSchema);
|
||||
$register->set('apiSchemaVersion', fn() => $envVersion);
|
||||
}
|
||||
|
||||
if ($register->has('collectionSchema') && !$collectionSchemaDirty) {
|
||||
$collectionSchema = $register->get('collectionSchema');
|
||||
} else {
|
||||
$collectionSchema = self::buildCollectionSchema($utopia, $dbForProject, $user);
|
||||
$register->set('collectionSchema', fn() => $collectionSchema);
|
||||
$register->set('schemaDirty', fn() => false);
|
||||
}
|
||||
|
||||
$queryFields = \array_merge_recursive($apiSchema['query'], $collectionSchema['query']);
|
||||
$mutationFields = \array_merge_recursive($apiSchema['mutation'], $collectionSchema['mutation']);
|
||||
|
||||
ksort($queryFields);
|
||||
ksort($mutationFields);
|
||||
|
||||
$timeElapsedMillis = (microtime(true) - $start) * 1000;
|
||||
$timeElapsedMillis = \number_format((float) $timeElapsedMillis, 3, '.', '');
|
||||
Console::info('[INFO] Built GraphQL Schema in ' . $timeElapsedMillis . 'ms');
|
||||
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => $queryFields
|
||||
]),
|
||||
'mutation' => new ObjectType([
|
||||
'name' => 'Mutation',
|
||||
'fields' => $mutationFields
|
||||
])
|
||||
]);
|
||||
|
||||
$register->set('fullSchema', 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
|
||||
{
|
||||
$start = microtime(true);
|
||||
|
||||
self::init();
|
||||
$queryFields = [];
|
||||
$mutationFields = [];
|
||||
$response = new Response(new SwooleResponse());
|
||||
$models = $response->getModels();
|
||||
|
||||
foreach (App::getRoutes() as $method => $routes) {
|
||||
foreach ($routes as $route) {
|
||||
/** @var Route $route */
|
||||
|
||||
if (str_starts_with($route->getPath(), '/v1/mock/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$namespace = $route->getLabel('sdk.namespace', '');
|
||||
$methodName = $namespace . \ucfirst($route->getLabel('sdk.method', ''));
|
||||
$responseModelNames = $route->getLabel('sdk.response.model', "none");
|
||||
|
||||
$responseModels = \is_array($responseModelNames)
|
||||
? \array_map(static fn($m) => $models[$m], $responseModelNames)
|
||||
: [$models[$responseModelNames]];
|
||||
|
||||
foreach ($responseModels as $responseModel) {
|
||||
$type = self::getModelTypeMapping($responseModel, $models);
|
||||
$description = $route->getDesc();
|
||||
$args = [];
|
||||
|
||||
foreach ($route->getParams() as $key => $value) {
|
||||
$argType = self::getParameterType(
|
||||
$utopia,
|
||||
$value['validator'],
|
||||
!$value['optional'],
|
||||
$value['injections']
|
||||
);
|
||||
$args[$key] = [
|
||||
'type' => $argType,
|
||||
'description' => $value['description'],
|
||||
'defaultValue' => $value['default']
|
||||
];
|
||||
}
|
||||
|
||||
$field = [
|
||||
'type' => $type,
|
||||
'description' => $description,
|
||||
'args' => $args,
|
||||
'resolve' => self::resolveAPIRequest($utopia, $route)
|
||||
];
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$queryFields[$methodName] = $field;
|
||||
break;
|
||||
case 'POST':
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
case 'DELETE':
|
||||
$mutationFields[$methodName] = $field;
|
||||
break;
|
||||
default:
|
||||
throw new \Exception("Unsupported method: $method");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$timeElapsedMillis = (microtime(true) - $start) * 1000;
|
||||
$timeElapsedMillis = \number_format((float) $timeElapsedMillis, 3, '.', '');
|
||||
Console::info("[INFO] Built GraphQL REST API Schema in ${timeElapsedMillis}ms");
|
||||
|
||||
return [
|
||||
'query' => $queryFields,
|
||||
'mutation' => $mutationFields
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param App $utopia
|
||||
* @param ?Route $route
|
||||
* @return callable
|
||||
*/
|
||||
private static function resolveAPIRequest(
|
||||
App $utopia,
|
||||
?Route $route,
|
||||
): callable {
|
||||
return fn($type, $args, $context, $info) => new CoroutinePromise(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) {
|
||||
// Mutate the original request object to match route
|
||||
$utopia = $utopia->getResource('current', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
$swoole = $request->getSwoole();
|
||||
$swoole->server['request_method'] = $route->getMethod();
|
||||
$swoole->server['request_uri'] = $route->getPath();
|
||||
$swoole->server['path_info'] = $route->getPath();
|
||||
|
||||
switch ($route->getMethod()) {
|
||||
case 'GET':
|
||||
$swoole->get = $args;
|
||||
break;
|
||||
default:
|
||||
$swoole->post = $args;
|
||||
break;
|
||||
}
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function iterates all a projects attributes and builds
|
||||
* GraphQL queries and mutations for the collections they make up.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param Database $dbForProject
|
||||
* @param Document|null $user
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function buildCollectionSchema(
|
||||
App $utopia,
|
||||
Database $dbForProject,
|
||||
?Document $user = null,
|
||||
): array {
|
||||
$start = microtime(true);
|
||||
|
||||
$userId = $user?->getId();
|
||||
$collections = [];
|
||||
$queryFields = [];
|
||||
$mutationFields = [];
|
||||
$limit = 1000;
|
||||
$offset = 0;
|
||||
$count = 0;
|
||||
|
||||
$wg = new WaitGroup();
|
||||
|
||||
while (
|
||||
!empty($attrs = Authorization::skip(fn() => $dbForProject->find(
|
||||
'attributes',
|
||||
limit: $limit,
|
||||
offset: $offset
|
||||
)))
|
||||
) {
|
||||
$wg->add();
|
||||
$count += count($attrs);
|
||||
go(function () use ($utopia, $dbForProject, &$collections, &$queryFields, &$mutationFields, $limit, &$offset, $attrs, $userId, $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');
|
||||
$escapedKey = str_replace('$', '_', $key);
|
||||
$collections[$collectionId][$escapedKey] = [
|
||||
'type' => self::getAttributeType($type, $array, $required),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($collections as $collectionId => $attributes) {
|
||||
$objectType = new ObjectType([
|
||||
'name' => $collectionId,
|
||||
'fields' => \array_merge(["_id" => ['type' => Type::string()]], $attributes),
|
||||
]);
|
||||
|
||||
$attributes = \array_merge(
|
||||
$attributes,
|
||||
self::$defaultDocumentArgs['mutate']
|
||||
);
|
||||
|
||||
$queryFields[$collectionId . 'Get'] = [
|
||||
'type' => $objectType,
|
||||
'args' => self::$defaultDocumentArgs['id'],
|
||||
'resolve' => self::resolveDocumentGet($utopia, $dbForProject, $databaseId, $collectionId)
|
||||
];
|
||||
$queryFields[$collectionId . 'List'] = [
|
||||
'type' => $objectType,
|
||||
'args' => self::$defaultDocumentArgs['list'],
|
||||
'resolve' => self::resolveDocumentList($utopia, $dbForProject, $databaseId, $collectionId)
|
||||
];
|
||||
$mutationFields[$collectionId . 'Create'] = [
|
||||
'type' => $objectType,
|
||||
'args' => $attributes,
|
||||
'resolve' => self::resolveDocumentMutate($utopia, $dbForProject, $databaseId, $collectionId, 'POST')
|
||||
];
|
||||
$mutationFields[$collectionId . 'Update'] = [
|
||||
'type' => $objectType,
|
||||
'args' => $attributes,
|
||||
'resolve' => self::resolveDocumentMutate($utopia, $dbForProject, $databaseId, $collectionId, 'PATCH')
|
||||
];
|
||||
$mutationFields[$collectionId . 'Delete'] = [
|
||||
'type' => $objectType,
|
||||
'args' => self::$defaultDocumentArgs['id'],
|
||||
'resolve' => self::resolveDocumentDelete($utopia, $dbForProject, $databaseId, $collectionId)
|
||||
];
|
||||
}
|
||||
$wg->done();
|
||||
});
|
||||
$offset += $limit;
|
||||
}
|
||||
$wg->wait();
|
||||
|
||||
$timeElapsedMillis = (microtime(true) - $start) * 1000;
|
||||
$timeElapsedMillis = \number_format((float) $timeElapsedMillis, 3, '.', '');
|
||||
Console::info('[INFO] Built GraphQL Project Collection Schema in ' . $timeElapsedMillis . 'ms (' . $count . ' attributes)');
|
||||
|
||||
return [
|
||||
'query' => $queryFields,
|
||||
'mutation' => $mutationFields
|
||||
];
|
||||
}
|
||||
|
||||
private static function resolveDocumentGet(
|
||||
App $utopia,
|
||||
Database $dbForProject,
|
||||
string $databaseId,
|
||||
string $collectionId
|
||||
): callable {
|
||||
return fn($type, $args, $context, $info) => new CoroutinePromise(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $type, $args) {
|
||||
try {
|
||||
$utopia = $utopia->getResource('current', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
$swoole = $request->getSwoole();
|
||||
$swoole->post = [
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $collectionId,
|
||||
'documentId' => $args['id'],
|
||||
];
|
||||
$swoole->server['request_method'] = 'GET';
|
||||
$swoole->server['request_uri'] = "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['id']}";
|
||||
$swoole->server['path_info'] = "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['id']}";
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
} catch (\Throwable $e) {
|
||||
$reject($e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static function resolveDocumentList(
|
||||
App $utopia,
|
||||
Database $dbForProject,
|
||||
string $databaseId,
|
||||
string $collectionId,
|
||||
): callable {
|
||||
return fn($type, $args, $context, $info) => new CoroutinePromise(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $type, $args) {
|
||||
$utopia = $utopia->getResource('current', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
$swoole = $request->getSwoole();
|
||||
$swoole->post = [
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $collectionId,
|
||||
'limit' => $args['limit'],
|
||||
'offset' => $args['offset'],
|
||||
'cursor' => $args['cursor'],
|
||||
'cursorDirection' => $args['cursorDirection'],
|
||||
'orderAttributes' => $args['orderAttributes'],
|
||||
'orderType' => $args['orderType'],
|
||||
];
|
||||
$swoole->server['request_method'] = 'GET';
|
||||
$swoole->server['request_uri'] = "/v1/databases/$databaseId/collections/$collectionId/documents";
|
||||
$swoole->server['path_info'] = "/v1/databases/$databaseId/collections/$collectionId/documents";
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static function resolveDocumentMutate(
|
||||
App $utopia,
|
||||
Database $dbForProject,
|
||||
string $databaseId,
|
||||
string $collectionId,
|
||||
string $method,
|
||||
): callable {
|
||||
return fn($type, $args, $context, $info) => new CoroutinePromise(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $method, $type, $args) {
|
||||
$utopia = $utopia->getResource('current', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
$swoole = $request->getSwoole();
|
||||
|
||||
$id = $args['id'] ?? 'unique()';
|
||||
$read = $args['read'];
|
||||
$write = $args['write'];
|
||||
|
||||
unset($args['id']);
|
||||
unset($args['read']);
|
||||
unset($args['write']);
|
||||
|
||||
// Order must be the same as the route params
|
||||
$swoole->post = [
|
||||
'databaseId' => $databaseId,
|
||||
'documentId' => $id,
|
||||
'collectionId' => $collectionId,
|
||||
'data' => $args,
|
||||
'read' => $read,
|
||||
'write' => $write,
|
||||
];
|
||||
$swoole->server['request_method'] = $method;
|
||||
$swoole->server['request_uri'] = "/v1/databases/$databaseId/collections/$collectionId/documents";
|
||||
$swoole->server['path_info'] = "/v1/databases/$databaseId/collections/$collectionId/documents";
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static function resolveDocumentDelete(
|
||||
App $utopia,
|
||||
Database $dbForProject,
|
||||
string $databaseId,
|
||||
string $collectionId
|
||||
): callable {
|
||||
return fn($type, $args, $context, $info) => new CoroutinePromise(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $type, $args) {
|
||||
$utopia = $utopia->getResource('current', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
$swoole = $request->getSwoole();
|
||||
$swoole->post = [
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $collectionId,
|
||||
'documentId' => $args['id'],
|
||||
];
|
||||
$swoole->server['request_method'] = 'DELETE';
|
||||
$swoole->server['request_uri'] = "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['id']}";
|
||||
$swoole->server['path_info'] = "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['id']}";
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param App $utopia
|
||||
* @param callable $resolve
|
||||
* @param callable $reject
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function resolve(
|
||||
App $utopia,
|
||||
Request $request,
|
||||
Response $response,
|
||||
callable $resolve,
|
||||
callable $reject,
|
||||
): void {
|
||||
// Drop json content type so post args are used directly
|
||||
if ($request->getHeader('content-type') === 'application/json') {
|
||||
unset($request->getSwoole()->header['content-type']);
|
||||
}
|
||||
|
||||
$request = new Request($request->getSwoole());
|
||||
$utopia->setResource('request', fn() => $request);
|
||||
|
||||
$response->setContentType(Response::CONTENT_TYPE_NULL);
|
||||
|
||||
try {
|
||||
// Set route to null so match doesn't early return the GraphQL route
|
||||
// Then get the inner route by matching the mutated request
|
||||
$route = $utopia->setRoute(null)->match($request);
|
||||
|
||||
$utopia->execute($route, $request);
|
||||
} catch (\Throwable $e) {
|
||||
$reject($e);
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = $response->getPayload();
|
||||
|
||||
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400) {
|
||||
$reject(new GQLException($payload['message'], $response->getStatusCode()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (\array_key_exists('$id', $payload)) {
|
||||
$payload['_id'] = $payload['$id'];
|
||||
}
|
||||
|
||||
$resolve($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function applyChange(array $collectionSchema, array $change): array
|
||||
{
|
||||
$collectionId = $change['data']['collectionId'];
|
||||
$get = $collectionSchema['query'][$collectionId . 'Get'];
|
||||
$list = $collectionSchema['query'][$collectionId . 'List'];
|
||||
$create = $collectionSchema['mutation'][$collectionId . 'Create'];
|
||||
$update = $collectionSchema['mutation'][$collectionId . 'Update'];
|
||||
$delete = $collectionSchema['mutation'][$collectionId . 'Delete'];
|
||||
|
||||
switch ($change['type']) {
|
||||
case 'create':
|
||||
$collectionSchema['query'][$collectionId . 'Get'] = self::addAttribute($get, $change['data']);
|
||||
$collectionSchema['query'][$collectionId . 'List'] = self::addAttribute($list, $change['data']);
|
||||
$collectionSchema['mutation'][$collectionId . 'Create'] = self::addAttribute($create, $change['data']);
|
||||
$collectionSchema['mutation'][$collectionId . 'Update'] = self::addAttribute($update, $change['data']);
|
||||
$collectionSchema['mutation'][$collectionId . 'Delete'] = self::addAttribute($delete, $change['data']);
|
||||
break;
|
||||
case 'delete':
|
||||
$collectionSchema['query'][$collectionId . 'Get'] = self::removeAttribute($get, $change['data']);
|
||||
$collectionSchema['query'][$collectionId . 'List'] = self::removeAttribute($list, $change['data']);
|
||||
$collectionSchema['mutation'][$collectionId . 'Create'] = self::removeAttribute($create, $change['data']);
|
||||
$collectionSchema['mutation'][$collectionId . 'Update'] = self::removeAttribute($update, $change['data']);
|
||||
$collectionSchema['mutation'][$collectionId . 'Delete'] = self::removeAttribute($delete, $change['data']);
|
||||
break;
|
||||
default:
|
||||
throw new \Exception('Unknown change type');
|
||||
}
|
||||
|
||||
return $collectionSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $root
|
||||
* @param array $attribute
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function addAttribute(array $root, array $attribute): array
|
||||
{
|
||||
$databaseId = $attribute['databaseId'];
|
||||
$collectionId = $attribute['collectionId'];
|
||||
$key = $attribute['key'];
|
||||
$type = $attribute['type'];
|
||||
$array = $attribute['array'];
|
||||
$required = $attribute['required'];
|
||||
$escapedKey = str_replace('$', '_', $key);
|
||||
|
||||
/** @var ObjectType $rootType */
|
||||
$rootType = $root['type'];
|
||||
$rootFields = $rootType->config['fields'];
|
||||
$rootFields[$escapedKey] = [
|
||||
'type' => self::getAttributeType($type, $array, $required),
|
||||
];
|
||||
$root['type'] = new ObjectType([
|
||||
'name' => $collectionId,
|
||||
'fields' => $rootFields,
|
||||
]);
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $root
|
||||
* @param array $attribute
|
||||
* @return array
|
||||
*/
|
||||
private static function removeAttribute(array $root, array $attribute): array
|
||||
{
|
||||
$databaseId = $attribute['databaseId'];
|
||||
$collectionId = $attribute['collectionId'];
|
||||
$key = $attribute['key'];
|
||||
$escapedKey = str_replace('$', '_', $key);
|
||||
|
||||
/** @var ObjectType $rootType */
|
||||
$rootType = $root['type'];
|
||||
$rootFields = $rootType->config['fields'];
|
||||
|
||||
unset($rootFields[$escapedKey]);
|
||||
|
||||
$root['type'] = new ObjectType([
|
||||
'name' => $collectionId,
|
||||
'fields' => $rootFields,
|
||||
]);
|
||||
|
||||
return $root;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL;
|
||||
namespace Appwrite\GraphQL\Promises;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Utils\Utils;
|
||||
use Swoole\Coroutine\Channel;
|
||||
|
||||
use function Co\go;
|
||||
|
||||
/**
|
||||
* Inspired by https://github.com/streamcommon/promise/blob/master/lib/ExtSwoolePromise.php
|
||||
*
|
||||
|
@ -34,8 +34,7 @@ class CoroutinePromise
|
|||
$this->setResult($value);
|
||||
$this->setState(self::STATE_REJECTED);
|
||||
};
|
||||
|
||||
go(function () use ($executor, $resolve, $reject) {
|
||||
\go(function () use ($executor, $resolve, $reject) {
|
||||
try {
|
||||
$executor($resolve, $reject);
|
||||
} catch (\Throwable $exception) {
|
||||
|
@ -143,7 +142,7 @@ class CoroutinePromise
|
|||
foreach ($promises as $promise) {
|
||||
if (!$promise instanceof CoroutinePromise) {
|
||||
$channel->close();
|
||||
throw new \RuntimeException('Not an Appwrite\GraphQL\CoroutinePromise');
|
||||
throw new InvariantViolation('Expected instance of CoroutinePromise, got ' . Utils::printSafe($promise));
|
||||
}
|
||||
$promise->then(function ($value) use ($key, &$result, $channel) {
|
||||
$result[$key] = $value;
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL;
|
||||
namespace Appwrite\GraphQL\Promises;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
|
@ -8,24 +8,42 @@ use GraphQL\Executor\Promise\PromiseAdapter;
|
|||
use GraphQL\Utils\Utils;
|
||||
use Swoole\Coroutine\Channel;
|
||||
|
||||
use function Co\go;
|
||||
|
||||
class CoroutinePromiseAdapter implements PromiseAdapter
|
||||
{
|
||||
/**
|
||||
* Returns true if the given value is a {@see CoroutinePromise}.
|
||||
*
|
||||
* @param $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isThenable($value): bool
|
||||
{
|
||||
return $value instanceof CoroutinePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a {@see CoroutinePromise} into a {@see Promise}
|
||||
*
|
||||
* @param $thenable
|
||||
* @return Promise
|
||||
*/
|
||||
public function convertThenable($thenable): Promise
|
||||
{
|
||||
if (!$thenable instanceof CoroutinePromise) {
|
||||
throw new InvariantViolation('Expected instance of SwoolePromise, got ' . Utils::printSafe($thenable));
|
||||
throw new InvariantViolation('Expected instance of CoroutinePromise, got ' . Utils::printSafe($thenable));
|
||||
}
|
||||
|
||||
return new Promise($thenable, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the passed in promise resolves.
|
||||
*
|
||||
* @param Promise $promise
|
||||
* @param callable|null $onFulfilled
|
||||
* @param callable|null $onRejected
|
||||
* @return Promise
|
||||
*/
|
||||
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
|
||||
{
|
||||
/** @var CoroutinePromise $adoptedPromise */
|
||||
|
@ -34,6 +52,12 @@ class CoroutinePromiseAdapter implements PromiseAdapter
|
|||
return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new promise with the given resolver function.
|
||||
*
|
||||
* @param callable $resolver
|
||||
* @return Promise
|
||||
*/
|
||||
public function create(callable $resolver): Promise
|
||||
{
|
||||
$promise = new CoroutinePromise(function ($resolve, $reject) use ($resolver) {
|
||||
|
@ -43,6 +67,12 @@ class CoroutinePromiseAdapter implements PromiseAdapter
|
|||
return new Promise($promise, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new promise that is fulfilled with the given value.
|
||||
*
|
||||
* @param $value
|
||||
* @return Promise
|
||||
*/
|
||||
public function createFulfilled($value = null): Promise
|
||||
{
|
||||
$promise = new CoroutinePromise(function ($resolve, $reject) use ($value) {
|
||||
|
@ -52,6 +82,12 @@ class CoroutinePromiseAdapter implements PromiseAdapter
|
|||
return new Promise($promise, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new promise that is rejected with the given reason.
|
||||
*
|
||||
* @param $reason
|
||||
* @return Promise
|
||||
*/
|
||||
public function createRejected($reason): Promise
|
||||
{
|
||||
$promise = new CoroutinePromise(function ($resolve, $reject) use ($reason) {
|
||||
|
@ -61,6 +97,12 @@ class CoroutinePromiseAdapter implements PromiseAdapter
|
|||
return new Promise($promise, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new promise that resolves when all passed in promises resolve.
|
||||
*
|
||||
* @param array $promisesOrValues
|
||||
* @return Promise
|
||||
*/
|
||||
public function all(array $promisesOrValues): Promise
|
||||
{
|
||||
$all = new CoroutinePromise(function (callable $resolve, callable $reject) use ($promisesOrValues) {
|
259
src/Appwrite/GraphQL/Resolvers.php
Normal file
259
src/Appwrite/GraphQL/Resolvers.php
Normal file
|
@ -0,0 +1,259 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL;
|
||||
|
||||
use Appwrite\GraphQL\Promises\CoroutinePromise;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Exception;
|
||||
use Utopia\Route;
|
||||
|
||||
class Resolvers
|
||||
{
|
||||
/**
|
||||
* Create a resolver for a given {@see Route}.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param ?Route $route
|
||||
* @return callable
|
||||
*/
|
||||
public static function resolveAPIRequest(
|
||||
App $utopia,
|
||||
?Route $route,
|
||||
): callable {
|
||||
return fn($type, $args, $context, $info) => new CoroutinePromise(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) {
|
||||
$utopia = $utopia->getResource('current', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
$swoole = $request->getSwoole();
|
||||
$swoole->server['request_method'] = $route->getMethod();
|
||||
$swoole->server['request_uri'] = $route->getPath();
|
||||
$swoole->server['path_info'] = $route->getPath();
|
||||
|
||||
switch ($route->getMethod()) {
|
||||
case 'GET':
|
||||
$swoole->get = $args;
|
||||
break;
|
||||
default:
|
||||
$swoole->post = $args;
|
||||
break;
|
||||
}
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resolver for getting a document in a specified database and collection.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param Database $dbForProject
|
||||
* @param string $databaseId
|
||||
* @param string $collectionId
|
||||
* @return callable
|
||||
*/
|
||||
public static function resolveDocumentGet(
|
||||
App $utopia,
|
||||
Database $dbForProject,
|
||||
string $databaseId,
|
||||
string $collectionId
|
||||
): callable {
|
||||
return fn($type, $args, $context, $info) => new CoroutinePromise(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $type, $args) {
|
||||
$utopia = $utopia->getResource('current', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
$swoole = $request->getSwoole();
|
||||
$swoole->post = [
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $collectionId,
|
||||
'documentId' => $args['id'],
|
||||
];
|
||||
$swoole->server['request_method'] = 'GET';
|
||||
$swoole->server['request_uri'] = "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['id']}";
|
||||
$swoole->server['path_info'] = "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['id']}";
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resolver for listing documents in a specified database and collection.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param Database $dbForProject
|
||||
* @param string $databaseId
|
||||
* @param string $collectionId
|
||||
* @return callable
|
||||
*/
|
||||
public static function resolveDocumentList(
|
||||
App $utopia,
|
||||
Database $dbForProject,
|
||||
string $databaseId,
|
||||
string $collectionId,
|
||||
): callable {
|
||||
return fn($type, $args, $context, $info) => new CoroutinePromise(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $type, $args) {
|
||||
$utopia = $utopia->getResource('current', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
$swoole = $request->getSwoole();
|
||||
$swoole->post = [
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $collectionId,
|
||||
'limit' => $args['limit'],
|
||||
'offset' => $args['offset'],
|
||||
'cursor' => $args['cursor'],
|
||||
'cursorDirection' => $args['cursorDirection'],
|
||||
'orderAttributes' => $args['orderAttributes'],
|
||||
'orderType' => $args['orderType'],
|
||||
];
|
||||
$swoole->server['request_method'] = 'GET';
|
||||
$swoole->server['request_uri'] = "/v1/databases/$databaseId/collections/$collectionId/documents";
|
||||
$swoole->server['path_info'] = "/v1/databases/$databaseId/collections/$collectionId/documents";
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resolver for mutating a document in a specified database and collection.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param Database $dbForProject
|
||||
* @param string $databaseId
|
||||
* @param string $collectionId
|
||||
* @param string $method
|
||||
* @return callable
|
||||
*/
|
||||
public static function resolveDocumentMutate(
|
||||
App $utopia,
|
||||
Database $dbForProject,
|
||||
string $databaseId,
|
||||
string $collectionId,
|
||||
string $method,
|
||||
): callable {
|
||||
return fn($type, $args, $context, $info) => new CoroutinePromise(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $method, $type, $args) {
|
||||
$utopia = $utopia->getResource('current', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
$swoole = $request->getSwoole();
|
||||
|
||||
$id = $args['id'] ?? 'unique()';
|
||||
$read = $args['read'];
|
||||
$write = $args['write'];
|
||||
|
||||
unset($args['id']);
|
||||
unset($args['read']);
|
||||
unset($args['write']);
|
||||
|
||||
// Order must be the same as the route params
|
||||
$swoole->post = [
|
||||
'databaseId' => $databaseId,
|
||||
'documentId' => $id,
|
||||
'collectionId' => $collectionId,
|
||||
'data' => $args,
|
||||
'read' => $read,
|
||||
'write' => $write,
|
||||
];
|
||||
$swoole->server['request_method'] = $method;
|
||||
$swoole->server['request_uri'] = "/v1/databases/$databaseId/collections/$collectionId/documents";
|
||||
$swoole->server['path_info'] = "/v1/databases/$databaseId/collections/$collectionId/documents";
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resolver for deleting a document in a specified database and collection.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param Database $dbForProject
|
||||
* @param string $databaseId
|
||||
* @param string $collectionId
|
||||
* @return callable
|
||||
*/
|
||||
public static function resolveDocumentDelete(
|
||||
App $utopia,
|
||||
Database $dbForProject,
|
||||
string $databaseId,
|
||||
string $collectionId
|
||||
): callable {
|
||||
return fn($type, $args, $context, $info) => new CoroutinePromise(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $dbForProject, $databaseId, $collectionId, $type, $args) {
|
||||
$utopia = $utopia->getResource('current', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
$swoole = $request->getSwoole();
|
||||
$swoole->post = [
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $collectionId,
|
||||
'documentId' => $args['id'],
|
||||
];
|
||||
$swoole->server['request_method'] = 'DELETE';
|
||||
$swoole->server['request_uri'] = "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['id']}";
|
||||
$swoole->server['path_info'] = "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['id']}";
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param App $utopia
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param callable $resolve
|
||||
* @param callable $reject
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function resolve(
|
||||
App $utopia,
|
||||
Request $request,
|
||||
Response $response,
|
||||
callable $resolve,
|
||||
callable $reject,
|
||||
): void {
|
||||
// Drop json content type so post args are used directly
|
||||
if ($request->getHeader('content-type') === 'application/json') {
|
||||
unset($request->getSwoole()->header['content-type']);
|
||||
}
|
||||
|
||||
$request = new Request($request->getSwoole());
|
||||
$utopia->setResource('request', fn() => $request);
|
||||
$response->setContentType(Response::CONTENT_TYPE_NULL);
|
||||
|
||||
try {
|
||||
// Set route to null so match doesn't early return the GraphQL route
|
||||
// Then get the inner route by matching the mutated request
|
||||
$route = $utopia->setRoute(null)->match($request);
|
||||
|
||||
$utopia->execute($route, $request);
|
||||
} catch (\Throwable $e) {
|
||||
$reject($e);
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = $response->getPayload();
|
||||
|
||||
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400) {
|
||||
$reject(new GQLException($payload['message'], $response->getStatusCode()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (\array_key_exists('$id', $payload)) {
|
||||
$payload['_id'] = $payload['$id'];
|
||||
}
|
||||
|
||||
$resolve($payload);
|
||||
}
|
||||
}
|
336
src/Appwrite/GraphQL/SchemaBuilder.php
Normal file
336
src/Appwrite/GraphQL/SchemaBuilder.php
Normal file
|
@ -0,0 +1,336 @@
|
|||
<?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 Swoole\Http\Response as SwooleResponse;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Route;
|
||||
|
||||
class SchemaBuilder
|
||||
{
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function buildSchema(
|
||||
App $utopia,
|
||||
Database $dbForProject
|
||||
): Schema {
|
||||
App::setResource('current', fn() => $utopia);
|
||||
|
||||
$start = microtime(true);
|
||||
$register = $utopia->getResource('register');
|
||||
$envVersion = App::getEnv('_APP_VERSION');
|
||||
$schemaVersion = $register->has('apiSchemaVersion') ? $register->get('apiSchemaVersion') : '';
|
||||
$collectionSchemaDirty = $register->has('schemaDirty') && $register->get('schemaDirty');
|
||||
$apiSchemaDirty = \version_compare($envVersion, $schemaVersion, "!=");
|
||||
|
||||
if (
|
||||
!$collectionSchemaDirty
|
||||
&& !$apiSchemaDirty
|
||||
&& $register->has('fullSchema')
|
||||
) {
|
||||
self::printBuildTimeFrom($start);
|
||||
|
||||
return $register->get('fullSchema');
|
||||
}
|
||||
|
||||
$apiSchema = self::getApiSchema($utopia, $register, $apiSchemaDirty, $envVersion);
|
||||
$collectionSchema = self::getCollectionSchema($utopia, $register, $dbForProject, $collectionSchemaDirty);
|
||||
$schema = self::collateSchema($apiSchema, $collectionSchema);
|
||||
|
||||
self::printBuildTimeFrom($start);
|
||||
|
||||
$register->set('fullSchema', 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
|
||||
{
|
||||
$start = microtime(true);
|
||||
$queryFields = [];
|
||||
$mutationFields = [];
|
||||
$response = new Response(new SwooleResponse());
|
||||
$models = $response->getModels();
|
||||
|
||||
TypeRegistry::init($models);
|
||||
|
||||
foreach (App::getRoutes() as $method => $routes) {
|
||||
foreach ($routes as $route) {
|
||||
/** @var Route $route */
|
||||
|
||||
if (str_starts_with($route->getPath(), '/v1/mock/')) {
|
||||
continue;
|
||||
}
|
||||
$namespace = $route->getLabel('sdk.namespace', '');
|
||||
$methodName = $namespace . \ucfirst($route->getLabel('sdk.method', ''));
|
||||
$responseModelNames = $route->getLabel('sdk.response.model', "none");
|
||||
$responseModels = \is_array($responseModelNames)
|
||||
? \array_map(static fn($m) => $models[$m], $responseModelNames)
|
||||
: [$models[$responseModelNames]];
|
||||
|
||||
foreach ($responseModels as $responseModel) {
|
||||
$type = TypeRegistry::get($responseModel->getType());
|
||||
$description = $route->getDesc();
|
||||
$args = [];
|
||||
|
||||
foreach ($route->getParams() as $key => $value) {
|
||||
$argType = TypeMapper::typeFromParameter(
|
||||
$utopia,
|
||||
$value['validator'],
|
||||
!$value['optional'],
|
||||
$value['injections']
|
||||
);
|
||||
$args[$key] = [
|
||||
'type' => $argType,
|
||||
'description' => $value['description'],
|
||||
'defaultValue' => $value['default']
|
||||
];
|
||||
}
|
||||
|
||||
$field = [
|
||||
'type' => $type,
|
||||
'description' => $description,
|
||||
'args' => $args,
|
||||
'resolve' => Resolvers::resolveAPIRequest($utopia, $route)
|
||||
];
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$queryFields[$methodName] = $field;
|
||||
break;
|
||||
case 'POST':
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
case 'DELETE':
|
||||
$mutationFields[$methodName] = $field;
|
||||
break;
|
||||
default:
|
||||
throw new \Exception("Unsupported method: $method");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$timeElapsedMillis = (microtime(true) - $start) * 1000;
|
||||
$timeElapsedMillis = \number_format((float)$timeElapsedMillis, 3, '.', '');
|
||||
Console::info("[INFO] Built GraphQL REST API Schema in ${timeElapsedMillis}ms");
|
||||
|
||||
return [
|
||||
'query' => $queryFields,
|
||||
'mutation' => $mutationFields
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates all 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 {
|
||||
$start = microtime(true);
|
||||
|
||||
$collections = [];
|
||||
$queryFields = [];
|
||||
$mutationFields = [];
|
||||
$limit = 1000;
|
||||
$offset = 0;
|
||||
$count = 0;
|
||||
|
||||
$wg = new WaitGroup();
|
||||
|
||||
while (
|
||||
!empty($attrs = Authorization::skip(fn() => $dbForProject->find(
|
||||
'attributes',
|
||||
limit: $limit,
|
||||
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');
|
||||
$escapedKey = str_replace('$', '_', $key);
|
||||
$collections[$collectionId][$escapedKey] = [
|
||||
'type' => TypeMapper::typeFromAttribute($type, $array, $required),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($collections as $collectionId => $attributes) {
|
||||
$objectType = new ObjectType([
|
||||
'name' => $collectionId,
|
||||
'fields' => \array_merge(
|
||||
["_id" => ['type' => Type::string()]],
|
||||
$attributes
|
||||
),
|
||||
]);
|
||||
$attributes = \array_merge(
|
||||
$attributes,
|
||||
TypeRegistry::defaultArgsFor('mutate')
|
||||
);
|
||||
$queryFields[$collectionId . 'Get'] = [
|
||||
'type' => $objectType,
|
||||
'args' => TypeRegistry::defaultArgsFor('id'),
|
||||
'resolve' => Resolvers::resolveDocumentGet(
|
||||
$utopia,
|
||||
$dbForProject,
|
||||
$databaseId,
|
||||
$collectionId
|
||||
)
|
||||
];
|
||||
$queryFields[$collectionId . 'List'] = [
|
||||
'type' => $objectType,
|
||||
'args' => TypeRegistry::defaultArgsFor('list'),
|
||||
'resolve' => Resolvers::resolveDocumentList(
|
||||
$utopia,
|
||||
$dbForProject,
|
||||
$databaseId,
|
||||
$collectionId
|
||||
)
|
||||
];
|
||||
$mutationFields[$collectionId . 'Create'] = [
|
||||
'type' => $objectType,
|
||||
'args' => $attributes,
|
||||
'resolve' => Resolvers::resolveDocumentMutate(
|
||||
$utopia,
|
||||
$dbForProject,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
'POST'
|
||||
)
|
||||
];
|
||||
$mutationFields[$collectionId . 'Update'] = [
|
||||
'type' => $objectType,
|
||||
'args' => $attributes,
|
||||
'resolve' => Resolvers::resolveDocumentMutate(
|
||||
$utopia,
|
||||
$dbForProject,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
'PATCH'
|
||||
)
|
||||
];
|
||||
$mutationFields[$collectionId . 'Delete'] = [
|
||||
'type' => $objectType,
|
||||
'args' => TypeRegistry::defaultArgsFor('id'),
|
||||
'resolve' => Resolvers::resolveDocumentDelete(
|
||||
$utopia,
|
||||
$dbForProject,
|
||||
$databaseId,
|
||||
$collectionId
|
||||
)
|
||||
];
|
||||
}
|
||||
$wg->done();
|
||||
});
|
||||
$offset += $limit;
|
||||
}
|
||||
$wg->wait();
|
||||
|
||||
$timeElapsedMillis = (microtime(true) - $start) * 1000;
|
||||
$timeElapsedMillis = \number_format((float)$timeElapsedMillis, 3, '.', '');
|
||||
Console::info('[INFO] Built GraphQL Project Collection Schema in ' . $timeElapsedMillis . 'ms (' . $count . ' attributes)');
|
||||
|
||||
return [
|
||||
'query' => $queryFields,
|
||||
'mutation' => $mutationFields
|
||||
];
|
||||
}
|
||||
|
||||
private static function getApiSchema(
|
||||
App $utopia,
|
||||
Registry $register,
|
||||
bool $apiSchemaDirty,
|
||||
string $envVersion
|
||||
): array {
|
||||
if ($register->has('apiSchema') && !$apiSchemaDirty) {
|
||||
$apiSchema = $register->get('apiSchema');
|
||||
} else {
|
||||
$apiSchema = self::buildAPISchema($utopia);
|
||||
$register->set('apiSchema', fn() => $apiSchema);
|
||||
$register->set('apiSchemaVersion', fn() => $envVersion);
|
||||
}
|
||||
return $apiSchema;
|
||||
}
|
||||
|
||||
private static function getCollectionSchema(
|
||||
App $utopia,
|
||||
Registry $register,
|
||||
Database $dbForProject,
|
||||
bool $collectionSchemaDirty
|
||||
): array {
|
||||
if ($register->has('collectionSchema') && !$collectionSchemaDirty) {
|
||||
$collectionSchema = $register->get('collectionSchema');
|
||||
} else {
|
||||
$collectionSchema = self::buildCollectionSchema($utopia, $dbForProject);
|
||||
$register->set('collectionSchema', fn() => $collectionSchema);
|
||||
$register->set('schemaDirty', fn() => false);
|
||||
}
|
||||
return $collectionSchema;
|
||||
}
|
||||
|
||||
private static function collateSchema(
|
||||
array $apiSchema,
|
||||
array $collectionSchema
|
||||
): Schema {
|
||||
$queryFields = \array_merge_recursive($apiSchema['query'], $collectionSchema['query']);
|
||||
$mutationFields = \array_merge_recursive($apiSchema['mutation'], $collectionSchema['mutation']);
|
||||
|
||||
\ksort($queryFields);
|
||||
\ksort($mutationFields);
|
||||
|
||||
return new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => $queryFields
|
||||
]),
|
||||
'mutation' => new ObjectType([
|
||||
'name' => 'Mutation',
|
||||
'fields' => $mutationFields
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $start
|
||||
* @return void
|
||||
*/
|
||||
private static function printBuildTimeFrom($start): void
|
||||
{
|
||||
$timeElapsedMillis = (\microtime(true) - $start) * 1000;
|
||||
$timeElapsedMillis = \number_format((float)$timeElapsedMillis, 3, '.', '');
|
||||
Console::info('[INFO] Built GraphQL Schema in ' . $timeElapsedMillis . 'ms');
|
||||
}
|
||||
}
|
124
src/Appwrite/GraphQL/TypeMapper.php
Normal file
124
src/Appwrite/GraphQL/TypeMapper.php
Normal file
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL;
|
||||
|
||||
use Appwrite\Utopia\Response\Model\Attribute;
|
||||
use Exception;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use Utopia\App;
|
||||
use Utopia\Route;
|
||||
use Utopia\Validator;
|
||||
|
||||
class TypeMapper
|
||||
{
|
||||
/**
|
||||
* Map a {@see Route} parameter to a GraphQL Type
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param Validator|callable $validator
|
||||
* @param bool $required
|
||||
* @param array $injections
|
||||
* @return Type
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function typeFromParameter(
|
||||
App $utopia,
|
||||
Validator|callable $validator,
|
||||
bool $required,
|
||||
array $injections
|
||||
): Type {
|
||||
$validator = \is_callable($validator)
|
||||
? \call_user_func_array($validator, $utopia->getResources($injections))
|
||||
: $validator;
|
||||
|
||||
switch ((!empty($validator)) ? \get_class($validator) : '') {
|
||||
case 'Appwrite\Auth\Validator\Password':
|
||||
case 'Appwrite\Event\Validator\Event':
|
||||
case 'Appwrite\Network\Validator\CNAME':
|
||||
case 'Appwrite\Network\Validator\Domain':
|
||||
case 'Appwrite\Network\Validator\Email':
|
||||
case 'Appwrite\Network\Validator\Host':
|
||||
case 'Appwrite\Network\Validator\IP':
|
||||
case 'Appwrite\Network\Validator\Origin':
|
||||
case 'Appwrite\Network\Validator\URL':
|
||||
case 'Appwrite\Task\Validator\Cron':
|
||||
case 'Appwrite\Utopia\Database\Validator\CustomId':
|
||||
case 'Utopia\Database\Validator\Key':
|
||||
case 'Utopia\Database\Validator\CustomId':
|
||||
case 'Utopia\Database\Validator\UID':
|
||||
case 'Utopia\Validator\HexColor':
|
||||
case 'Utopia\Validator\Length':
|
||||
case 'Utopia\Validator\Text':
|
||||
case 'Utopia\Validator\WhiteList':
|
||||
default:
|
||||
$type = Type::string();
|
||||
break;
|
||||
case 'Utopia\Validator\Boolean':
|
||||
$type = Type::boolean();
|
||||
break;
|
||||
case 'Utopia\Validator\ArrayList':
|
||||
/** @noinspection PhpPossiblePolymorphicInvocationInspection */
|
||||
$type = Type::listOf(self::typeFromParameter(
|
||||
$utopia,
|
||||
$validator->getValidator(),
|
||||
$required,
|
||||
$injections
|
||||
));
|
||||
break;
|
||||
case 'Utopia\Validator\Numeric':
|
||||
case 'Utopia\Validator\Integer':
|
||||
case 'Utopia\Validator\Range':
|
||||
$type = Type::int();
|
||||
break;
|
||||
case 'Utopia\Validator\FloatValidator':
|
||||
$type = Type::float();
|
||||
break;
|
||||
case 'Utopia\Database\Validator\Authorization':
|
||||
case 'Utopia\Database\Validator\Permissions':
|
||||
$type = Type::listOf(Type::string());
|
||||
break;
|
||||
case 'Utopia\Validator\Assoc':
|
||||
case 'Utopia\Validator\JSON':
|
||||
$type = TypeRegistry::json();
|
||||
break;
|
||||
case 'Utopia\Storage\Validator\File':
|
||||
$type = TypeRegistry::inputFile();
|
||||
break;
|
||||
}
|
||||
|
||||
if ($required) {
|
||||
$type = Type::nonNull($type);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an {@see Attribute} to a GraphQL Type
|
||||
*
|
||||
* @param string $type
|
||||
* @param bool $array
|
||||
* @param bool $required
|
||||
* @return Type
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function typeFromAttribute(string $type, bool $array, bool $required): Type
|
||||
{
|
||||
if ($array) {
|
||||
return Type::listOf(self::typeFromAttribute($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;
|
||||
}
|
||||
}
|
206
src/Appwrite/GraphQL/TypeRegistry.php
Normal file
206
src/Appwrite/GraphQL/TypeRegistry.php
Normal file
|
@ -0,0 +1,206 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL;
|
||||
|
||||
use Appwrite\GraphQL\Types\InputFile;
|
||||
use Appwrite\GraphQL\Types\Json;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
|
||||
class TypeRegistry
|
||||
{
|
||||
private static ?Json $jsonType = null;
|
||||
private static ?InputFile $inputFile = null;
|
||||
|
||||
private static array $typeMapping = [];
|
||||
private static array $defaultDocumentArgs = [];
|
||||
private static array $models = [];
|
||||
|
||||
public static function init($models): void
|
||||
{
|
||||
self::$typeMapping = [
|
||||
Model::TYPE_BOOLEAN => Type::boolean(),
|
||||
Model::TYPE_STRING => Type::string(),
|
||||
Model::TYPE_INTEGER => Type::int(),
|
||||
Model::TYPE_FLOAT => Type::float(),
|
||||
Model::TYPE_JSON => self::json(),
|
||||
Response::MODEL_NONE => self::json(),
|
||||
Response::MODEL_ANY => self::json(),
|
||||
];
|
||||
self::$defaultDocumentArgs = [
|
||||
'id' => [
|
||||
'id' => [
|
||||
'type' => Type::string(),
|
||||
],
|
||||
],
|
||||
'list' => [
|
||||
'limit' => [
|
||||
'type' => Type::int(),
|
||||
'defaultValue' => 25,
|
||||
],
|
||||
'offset' => [
|
||||
'type' => Type::int(),
|
||||
'defaultValue' => 0,
|
||||
],
|
||||
'cursor' => [
|
||||
'type' => Type::string(),
|
||||
'defaultValue' => '',
|
||||
],
|
||||
'cursorDirection' => [
|
||||
'type' => Type::string(),
|
||||
'defaultValue' => Database::CURSOR_AFTER,
|
||||
],
|
||||
'orderAttributes' => [
|
||||
'type' => Type::listOf(Type::string()),
|
||||
'defaultValue' => [],
|
||||
],
|
||||
'orderType' => [
|
||||
'type' => Type::listOf(Type::string()),
|
||||
'defaultValue' => [],
|
||||
],
|
||||
],
|
||||
'mutate' => [
|
||||
'read' => [
|
||||
'type' => Type::listOf(Type::string()),
|
||||
'defaultValue' => ["role:member"],
|
||||
],
|
||||
'write' => [
|
||||
'type' => Type::listOf(Type::string()),
|
||||
'defaultValue' => ["role:member"],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
self::$models = $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a type exists in the registry.
|
||||
*
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
public static function has(string $type): bool
|
||||
{
|
||||
return isset(self::$typeMapping[$type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a type from the registry, creating it if it does not already exist.
|
||||
*
|
||||
* @param string $name
|
||||
* @return Type
|
||||
*/
|
||||
public static function get(string $name): Type
|
||||
{
|
||||
if (self::has($name)) {
|
||||
return self::$typeMapping[$name];
|
||||
}
|
||||
|
||||
$fields = [];
|
||||
|
||||
$model = self::$models[$name];
|
||||
|
||||
if ($model->isAny()) {
|
||||
$fields['data'] = [
|
||||
'type' => Type::string(),
|
||||
'description' => 'Data field',
|
||||
'resolve' => fn($object, $args, $context, $info) => \json_encode($object, JSON_FORCE_OBJECT),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($model->getRules() as $key => $props) {
|
||||
$escapedKey = str_replace('$', '_', $key);
|
||||
|
||||
$types = \is_array($props['type'])
|
||||
? $props['type']
|
||||
: [$props['type']];
|
||||
|
||||
foreach ($types as $type) {
|
||||
if (self::has($type)) {
|
||||
$type = self::$typeMapping[$type];
|
||||
} else {
|
||||
try {
|
||||
$complexModel = self::$models[$type];
|
||||
$type = self::get($complexModel);
|
||||
} catch (\Exception) {
|
||||
Console::error('Could not find model for ' . $type);
|
||||
}
|
||||
}
|
||||
|
||||
if ($props['array']) {
|
||||
$type = Type::listOf($type);
|
||||
}
|
||||
|
||||
$fields[$escapedKey] = [
|
||||
'type' => $type,
|
||||
'description' => $props['description'],
|
||||
'resolve' => fn($object, $args, $context, $info) => $object[$key],
|
||||
];
|
||||
}
|
||||
}
|
||||
$objectType = [
|
||||
'name' => $name,
|
||||
'fields' => $fields
|
||||
];
|
||||
|
||||
self::set($name, new ObjectType($objectType));
|
||||
|
||||
return self::$typeMapping[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a type in the registry.
|
||||
*
|
||||
* @param string $type
|
||||
* @param Type $typeObject
|
||||
*/
|
||||
public static function set(string $type, Type $typeObject): void
|
||||
{
|
||||
self::$typeMapping[$type] = $typeObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registered default arguments for a given key.
|
||||
*
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
public static function defaultArgsFor(string $key): array
|
||||
{
|
||||
if (isset(self::$defaultDocumentArgs[$key])) {
|
||||
return self::$defaultDocumentArgs[$key];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON type.
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
public static function json(): Json
|
||||
{
|
||||
if (\is_null(self::$jsonType)) {
|
||||
self::$jsonType = new Json();
|
||||
}
|
||||
return self::$jsonType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the InputFile type.
|
||||
*
|
||||
* @return InputFile
|
||||
*/
|
||||
public static function inputFile(): InputFile
|
||||
{
|
||||
if (\is_null(self::$inputFile)) {
|
||||
self::$inputFile = new InputFile();
|
||||
}
|
||||
return self::$inputFile;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Appwrite\GraphQL\Types;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
|
@ -11,50 +9,22 @@ use GraphQL\Type\Definition\ScalarType;
|
|||
|
||||
class InputFile extends ScalarType
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'InputFile';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $description
|
||||
= 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
|
||||
[graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec).';
|
||||
|
||||
/**
|
||||
* Serializes an internal value to include in a response.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function serialize($value)
|
||||
{
|
||||
throw new InvariantViolation('`InputFile` cannot be serialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an externally provided value (query variable) to use as an input.
|
||||
*
|
||||
* @param mixed $value
|
||||
**/
|
||||
public function parseValue($value)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input.
|
||||
*
|
||||
* @param Node $valueNode
|
||||
*
|
||||
* @return mixed
|
||||
* @throws Error
|
||||
*/
|
||||
public function parseLiteral($valueNode, ?array $variables = null)
|
||||
public function parseLiteral(Node $valueNode, ?array $variables = null)
|
||||
{
|
||||
throw new Error('`InputFile` cannot be hardcoded in query, be sure to conform to GraphQL multipart request specification. Instead got: ' . $valueNode->kind, $valueNode);
|
||||
}
|
||||
|
|
|
@ -17,22 +17,14 @@ class Json extends ScalarType
|
|||
public $name = 'Json';
|
||||
public $description =
|
||||
'The `JSON` scalar type represents JSON values as specified by
|
||||
[ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
|
||||
[ECMA-404](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
|
||||
|
||||
public function __construct(?string $name = null)
|
||||
{
|
||||
if ($name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function parseValue($value)
|
||||
public function serialize($value)
|
||||
{
|
||||
return $this->identity($value);
|
||||
}
|
||||
|
||||
public function serialize($value)
|
||||
public function parseValue($value)
|
||||
{
|
||||
return $this->identity($value);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
namespace Appwrite\Tests;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\GraphQL\Builder;
|
||||
use Appwrite\GraphQL\SchemaBuilder;
|
||||
use Appwrite\GraphQL\TypeRegistry;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
@ -12,20 +13,20 @@ use Utopia\App;
|
|||
|
||||
class BuilderTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var Response
|
||||
*/
|
||||
protected $response = null;
|
||||
protected ?Response $response = null;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->response = new Response(new SwooleResponse());
|
||||
Builder::init();
|
||||
TypeRegistry::init($this->response->getModels());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testCreateTypeMapping()
|
||||
{
|
||||
$model = $this->response->getModel(Response::MODEL_COLLECTION);
|
||||
$typeMapping = Builder::getModelTypeMapping($model, $this->response);
|
||||
$typeMapping = TypeRegistry::get($model->getType());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue