Move mapping functions to TypeMapper

This commit is contained in:
Jake Barnby 2022-09-30 19:58:26 +13:00
parent f1429e8155
commit 1fbcedec42
No known key found for this signature in database
GPG key ID: C437A8CC85B96E9C
2 changed files with 230 additions and 86 deletions

View file

@ -3,12 +3,10 @@
namespace Appwrite\GraphQL;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\None;
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\Database\Database;
use Utopia\Database\Query;
@ -108,100 +106,55 @@ class SchemaBuilder
*/
public static function &buildAPISchema(App $utopia): array
{
$queryFields = [];
$mutationFields = [];
$response = new Response(new SwooleResponse());
$models = $response->getModels();
$models = $utopia
->getResource('response')
->getModels();
TypeRegistry::init($models);
TypeMapper::init($models);
foreach (App::getRoutes() as $method => $routes) {
$queries = [];
$mutations = [];
foreach (App::getRoutes() as $type => $routes) {
foreach ($routes as $route) {
/** @var Route $route */
if (\str_starts_with($route->getPath(), '/v1/mock/')) {
$namespace = $route->getLabel('sdk.namespace', '');
$method = $route->getLabel('sdk.method', '');
$name = $namespace . \ucfirst($method);
if (empty($name)) {
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) {
if (empty($responseModel->getRules())) {
continue;
}
$type = TypeRegistry::get(\ucfirst($responseModel->getType()));
$description = $route->getDesc();
$params = [];
$list = false;
foreach ($route->getParams() as $name => $parameter) {
if ($name === 'queries') {
$list = true;
}
$argType = TypeMapper::fromRouteParameter(
$utopia,
$parameter['validator'],
!$parameter['optional'],
$parameter['injections']
);
$params[$name] = [
'type' => $argType,
'description' => $parameter['description'],
];
if ($parameter['optional']) {
$params[$name]['defaultValue'] = $parameter['default'];
}
}
$field = [
'type' => $type,
'description' => $description,
'args' => $params,
'resolve' => Resolvers::resolveAPIRequest($utopia, $route)
];
if ($list) {
$field['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;
};
}
switch ($method) {
foreach (TypeMapper::fromRoute($utopia, $route) as $field) {
switch ($route->getMethod()) {
case 'GET':
$queryFields[$methodName] = $field;
$queries[$name] = $field;
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
$mutationFields[$methodName] = $field;
$mutations[$name] = $field;
break;
default:
throw new \Exception("Unsupported method: $method");
throw new \Exception("Unsupported method: {$route->getMethod()}");
}
}
}
}
$schema = [
'query' => $queryFields,
'mutation' => $mutationFields
'query' => $queries,
'mutation' => $mutations
];
return $schema;
}
/**
* Iterates all a projects attributes and builds GraphQL
* Iterates all of a projects attributes and builds GraphQL
* queries and mutations for the collections they make up.
*
* @param App $utopia
@ -223,13 +176,10 @@ class SchemaBuilder
$wg = new WaitGroup();
while (
!empty($attrs = Authorization::skip(fn() => $dbForProject->find(
collection: 'attributes',
queries: [
Query::limit($limit),
Query::offset($offset),
]
)))
!empty($attrs = Authorization::skip(fn() => $dbForProject->find('attributes', [
Query::limit($limit),
Query::offset($offset),
])))
) {
$wg->add();
$count += count($attrs);
@ -266,12 +216,12 @@ class SchemaBuilder
]);
$attributes = \array_merge(
$attributes,
TypeRegistry::argumentsFor('mutate')
TypeMapper::argumentsFor('mutate')
);
$queryFields[$collectionId . 'Get'] = [
'type' => $objectType,
'args' => TypeRegistry::argumentsFor('id'),
'args' => TypeMapper::argumentsFor('id'),
'resolve' => Resolvers::resolveDocumentGet(
$utopia,
$dbForProject,
@ -281,7 +231,7 @@ class SchemaBuilder
];
$queryFields[$collectionId . 'List'] = [
'type' => Type::listOf($objectType),
'args' => TypeRegistry::argumentsFor('list'),
'args' => TypeMapper::argumentsFor('list'),
'resolve' => Resolvers::resolveDocumentList(
$utopia,
$dbForProject,
@ -310,7 +260,7 @@ class SchemaBuilder
$mutationFields[$collectionId . 'Update'] = [
'type' => $objectType,
'args' => \array_merge(
TypeRegistry::argumentsFor('id'),
TypeMapper::argumentsFor('id'),
\array_map(
fn($attr) => $attr['type'] = Type::getNullableType($attr['type']),
$attributes
@ -324,8 +274,8 @@ class SchemaBuilder
)
];
$mutationFields[$collectionId . 'Delete'] = [
'type' => TypeRegistry::get(Response::MODEL_NONE),
'args' => TypeRegistry::argumentsFor('id'),
'type' => TypeMapper::fromResponseModel(Response::MODEL_NONE),
'args' => TypeMapper::argumentsFor('id'),
'resolve' => Resolvers::resolveDocumentDelete(
$utopia,
$dbForProject,

View file

@ -28,10 +28,16 @@ use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Database\Validator\Queries\Teams;
use Appwrite\Utopia\Database\Validator\Queries\Users;
use Appwrite\Utopia\Database\Validator\Queries\Variables;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response\Model\Attribute;
use Exception;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\Permissions;
@ -54,6 +60,181 @@ use Utopia\Validator\WhiteList;
class TypeMapper
{
private static array $models = [];
private static array $defaultArgs = [];
public static function init(array $models): void
{
self::$models = $models;
self::$defaultArgs = [
'id' => [
'id' => [
'type' => Type::nonNull(Type::string()),
],
],
'list' => [
'queries' => [
'type' => Type::listOf(Type::nonNull(Type::string())),
'defaultValue' => [],
],
],
'mutate' => [
'permissions' => [
'type' => Type::listOf(Type::nonNull(Type::string())),
'defaultValue' => [],
]
],
];
$defaultTypes = [
Model::TYPE_BOOLEAN => Type::boolean(),
Model::TYPE_STRING => Type::string(),
Model::TYPE_INTEGER => Type::int(),
Model::TYPE_FLOAT => Type::float(),
Model::TYPE_DATETIME => Type::string(),
Model::TYPE_JSON => Types::json(),
Response::MODEL_NONE => Types::json(),
Response::MODEL_ANY => Types::json(),
];
foreach ($defaultTypes as $type => $default) {
TypeRegistry::set($type, $default);
}
}
/**
* Get the registered default arguments for a given key.
*
* @param string $key
* @return array
*/
public static function argumentsFor(string $key): array
{
if (isset(self::$defaultArgs[$key])) {
return self::$defaultArgs[$key];
}
return [];
}
public static function fromRoute(App $utopia, Route $route): iterable
{
if (\str_starts_with($route->getPath(), '/v1/mock/')) {
return;
}
if ($route->getLabel('sdk.methodType', '') === 'webAuth') {
return;
}
$modelNames = $route->getLabel('sdk.response.model', 'none');
$models = \is_array($modelNames)
? \array_map(static fn($m) => static::$models[$m], $modelNames)
: [static::$models[$modelNames]];
foreach ($models as $model) {
// if (empty($responseModel->getRules())) {
// \var_dump('No rules: ' . $responseModel->getType());
// continue;
// }
$type = TypeMapper::fromResponseModel(\ucfirst($model->getType()));
$description = $route->getDesc();
$params = [];
$list = false;
foreach ($route->getParams() as $name => $parameter) {
if ($name === 'queries') {
$list = true;
}
$parameterType = TypeMapper::fromRouteParameter(
$utopia,
$parameter['validator'],
!$parameter['optional'],
$parameter['injections']
);
$params[$name] = [
'type' => $parameterType,
'description' => $parameter['description'],
];
if ($parameter['optional']) {
$params[$name]['defaultValue'] = $parameter['default'];
}
}
$field = [
'type' => $type,
'description' => $description,
'args' => $params,
'resolve' => Resolvers::resolveAPIRequest($utopia, $route)
];
if ($list) {
$field['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;
};
}
yield $field;
}
}
/**
* Get a type from the registry, creating it if it does not already exist.
*
* @param string $name
* @return Type
*/
public static function fromResponseModel(string $name): Type
{
if (TypeRegistry::has($name)) {
return TypeRegistry::get($name);
}
$fields = [];
$model = self::$models[\lcfirst($name)];
// If model has additional properties, explicitly add a 'data' field
if ($model->isAny()) {
$fields['data'] = [
'type' => Type::string(),
'description' => 'Additional data',
'resolve' => static fn($object, $args, $context, $info) => \json_encode($object, JSON_FORCE_OBJECT),
];
}
foreach ($model->getRules() as $key => $rule) {
$escapedKey = str_replace('$', '_', $key);
$type = self::getObjectType($rule);
if ($rule['array']) {
$type = Type::listOf($type);
}
$fields[$escapedKey] = [
'type' => $type,
'description' => $rule['description'],
];
if (!$rule['required']) {
$fields[$escapedKey]['defaultValue'] = $rule['default'];
}
}
$type = new ObjectType([
'name' => $name,
'fields' => $fields,
]);
TypeRegistry::set($name, $type);
return $type;
}
/**
* Map a {@see Route} parameter to a GraphQL Type
*
@ -136,10 +317,10 @@ class TypeMapper
break;
case Assoc::class:
case JSON::class:
$type = TypeRegistry::json();
$type = Types::json();
break;
case File::class:
$type = TypeRegistry::inputFile();
$type = Types::inputFile();
break;
}
@ -166,9 +347,9 @@ class TypeMapper
}
$type = match ($type) {
'boolean' => Type::boolean(),
'integer' => Type::int(),
'double' => Type::float(),
Database::VAR_BOOLEAN => Type::boolean(),
Database::VAR_INTEGER => Type::int(),
Database::VAR_FLOAT => Type::float(),
default => Type::string(),
};
@ -178,4 +359,17 @@ class TypeMapper
return $type;
}
private static function getObjectType(array $rule): Type
{
$type = $rule['type'];
if (TypeRegistry::has($type)) {
return TypeRegistry::get($type);
}
$complexModel = self::$models[$type];
return self::fromResponseModel(\ucfirst($complexModel->getType()));
}
}