1
0
Fork 0
mirror of synced 2024-06-14 16:54:52 +12:00
appwrite/src/Appwrite/GraphQL/Types/Mapper.php
2024-03-06 18:34:21 +01:00

445 lines
14 KiB
PHP

<?php
namespace Appwrite\GraphQL\Types;
use Appwrite\GraphQL\Resolvers;
use Appwrite\GraphQL\Types;
use Exception;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use Utopia\App;
use Utopia\Route;
use Utopia\Validator;
use Utopia\Validator\Nullable;
class Mapper
{
private static array $models = [];
private static array $args = [];
private static array $blacklist = [
'/v1/mock',
'/v1/graphql',
'/v1/account/sessions/oauth2',
];
public static function init(array $models): void
{
self::$models = $models;
self::$args = [
'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' => [],
]
],
];
$defaults = [
'boolean' => Type::boolean(),
'string' => Type::string(),
'integer' => Type::int(),
'double' => Type::float(),
'datetime' => Type::string(),
'json' => Types::json(),
'none' => Types::json(),
'any' => Types::json(),
];
foreach ($defaults as $type => $default) {
Registry::set($type, $default);
}
}
/**
* Get the registered default arguments for a given key.
*
* @param string $key
* @return array
*/
public static function args(string $key): array
{
return self::$args[$key] ?? [];
}
public static function route(
App $utopia,
Route $route,
callable $complexity
): iterable {
foreach (self::$blacklist as $blacklist) {
if (\str_starts_with($route->getPath(), $blacklist)) {
return;
}
}
$names = $route->getLabel('sdk.response.model', 'none');
$models = \is_array($names)
? \array_map(static fn ($m) => static::$models[$m], $names)
: [static::$models[$names]];
foreach ($models as $model) {
$type = Mapper::model(\ucfirst($model->getType()));
$description = $route->getDesc();
$params = [];
$list = false;
foreach ($route->getParams() as $name => $parameter) {
if ($name === 'queries') {
$list = true;
}
$parameterType = Mapper::param(
$utopia,
$parameter['validator'],
!$parameter['optional'],
$parameter['injections']
);
$params[$name] = [
'type' => $parameterType,
'description' => $parameter['description'],
];
}
$field = [
'type' => $type,
'description' => $description,
'args' => $params,
'resolve' => Resolvers::api($utopia, $route)
];
if ($list) {
$field['complexity'] = $complexity;
}
yield $field;
}
}
/**
* Get a type from the registry, creating it if it does not already exist.
*
* @param string $name
* @return Type
*/
public static function model(string $name): Type
{
if (Registry::has($name)) {
return Registry::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 function ($object, $args, $context, $info) {
$data = \array_filter(
(array)$object,
fn ($key) => !\str_starts_with($key, '_'),
ARRAY_FILTER_USE_KEY
);
return \json_encode($data, JSON_FORCE_OBJECT);
}
];
}
// If model has no properties, explicitly add a 'status' field
// because GraphQL requires at least 1 field per type.
if (!$model->isAny() && empty($model->getRules())) {
$fields['status'] = [
'type' => Type::string(),
'description' => 'Status',
'resolve' => static fn ($object, $args, $context, $info) => 'OK',
];
}
foreach ($model->getRules() as $key => $rule) {
$escapedKey = str_replace('$', '_', $key);
if (\is_array($rule['type'])) {
$type = self::getUnionType($escapedKey, $rule);
} else {
$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,
]);
Registry::set($name, $type);
return $type;
}
/**
* 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 param(
App $utopia,
Validator|callable $validator,
bool $required,
array $injections
): Type {
$validator = \is_callable($validator)
? \call_user_func_array($validator, $utopia->getResources($injections))
: $validator;
$isNullable = $validator instanceof Nullable;
if ($isNullable) {
$validator = $validator->getValidator();
}
switch ((!empty($validator)) ? $validator::class : '') {
case 'Appwrite\Network\Validator\CNAME':
case 'Appwrite\Task\Validator\Cron':
case 'Appwrite\Utopia\Database\Validator\CustomId':
case 'Utopia\Validator\Domain':
case 'Appwrite\Network\Validator\Email':
case 'Appwrite\Event\Validator\Event':
case 'Appwrite\Event\Validator\FunctionEvent':
case 'Utopia\Validator\HexColor':
case 'Utopia\Validator\Host':
case 'Utopia\Validator\IP':
case 'Utopia\Database\Validator\Key':
case 'Utopia\Validator\Origin':
case 'Appwrite\Auth\Validator\Password':
case 'Utopia\Validator\Text':
case 'Utopia\Database\Validator\UID':
case 'Utopia\Validator\URL':
case 'Utopia\Validator\WhiteList':
default:
$type = Type::string();
break;
case 'Utopia\Database\Validator\Authorization':
case 'Appwrite\Utopia\Database\Validator\Queries\Base':
case 'Appwrite\Utopia\Database\Validator\Queries\Buckets':
case 'Appwrite\Utopia\Database\Validator\Queries\Collections':
case 'Appwrite\Utopia\Database\Validator\Queries\Attributes':
case 'Appwrite\Utopia\Database\Validator\Queries\Indexes':
case 'Appwrite\Utopia\Database\Validator\Queries\Databases':
case 'Appwrite\Utopia\Database\Validator\Queries\Deployments':
case 'Appwrite\Utopia\Database\Validator\Queries\Installations':
case 'Utopia\Database\Validator\Queries\Documents':
case 'Appwrite\Utopia\Database\Validator\Queries\Executions':
case 'Appwrite\Utopia\Database\Validator\Queries\Files':
case 'Appwrite\Utopia\Database\Validator\Queries\Functions':
case 'Appwrite\Utopia\Database\Validator\Queries\Rules':
case 'Appwrite\Utopia\Database\Validator\Queries\Memberships':
case 'Utopia\Database\Validator\Permissions':
case 'Appwrite\Utopia\Database\Validator\Queries\Projects':
case 'Utopia\Database\Validator\Queries':
case 'Utopia\Database\Validator\Roles':
case 'Appwrite\Utopia\Database\Validator\Queries\Teams':
case 'Appwrite\Utopia\Database\Validator\Queries\Users':
case 'Appwrite\Utopia\Database\Validator\Queries\Variables':
$type = Type::listOf(Type::string());
break;
case 'Utopia\Validator\Boolean':
$type = Type::boolean();
break;
case 'Utopia\Validator\ArrayList':
$type = Type::listOf(self::param(
$utopia,
$validator->getValidator(),
$required,
$injections
));
break;
case 'Utopia\Validator\Integer':
case 'Utopia\Validator\Numeric':
case 'Utopia\Validator\Range':
$type = Type::int();
break;
case 'Utopia\Validator\FloatValidator':
$type = Type::float();
break;
case 'Utopia\Validator\Assoc':
$type = Types::assoc();
break;
case 'Utopia\Validator\JSON':
$type = Types::json();
break;
case 'Utopia\Storage\Validator\File':
$type = Types::inputFile();
break;
}
if ($required && !$isNullable) {
$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 attribute(string $type, bool $array, bool $required): Type
{
if ($array) {
return Type::listOf(self::attribute(
$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;
}
private static function getObjectType(array $rule): Type
{
$type = $rule['type'];
if (Registry::has($type)) {
return Registry::get($type);
}
$complexModel = self::$models[$type];
return self::model(\ucfirst($complexModel->getType()));
}
private static function getUnionType(string $name, array $rule): Type
{
$unionName = \ucfirst($name);
if (Registry::has($unionName)) {
return Registry::get($unionName);
}
$types = [];
foreach ($rule['type'] as $type) {
$types[] = self::model(\ucfirst($type));
}
$unionType = new UnionType([
'name' => $unionName,
'types' => $types,
'resolveType' => static function ($object) use ($unionName) {
return static::getUnionImplementation($unionName, $object);
},
]);
Registry::set($unionName, $unionType);
return $unionType;
}
private static function getUnionImplementation(string $name, array $object): Type
{
// TODO: Find a better way to do this
switch ($name) {
case 'Attributes':
return static::getAttributeImplementation($object);
case 'HashOptions':
return static::getHashOptionsImplementation($object);
}
throw new Exception('Unknown union type: ' . $name);
}
private static function getAttributeImplementation(array $object): Type
{
switch ($object['type']) {
case 'string':
return match ($object['format'] ?? '') {
'email' => static::model('AttributeEmail'),
'url' => static::model('AttributeUrl'),
'ip' => static::model('AttributeIp'),
default => static::model('AttributeString'),
};
case 'integer':
return static::model('AttributeInteger');
case 'double':
return static::model('AttributeFloat');
case 'boolean':
return static::model('AttributeBoolean');
case 'datetime':
return static::model('AttributeDatetime');
case 'relationship':
return static::model('AttributeRelationship');
}
throw new Exception('Unknown attribute implementation');
}
private static function getHashOptionsImplementation(array $object): Type
{
switch ($object['type']) {
case 'argon2':
return static::model('AlgoArgon2');
case 'bcrypt':
return static::model('AlgoBcrypt');
case 'md5':
return static::model('AlgoMd5');
case 'phpass':
return static::model('AlgoPhpass');
case 'scrypt':
return static::model('AlgoScrypt');
case 'scryptMod':
return static::model('AlgoScryptModified');
case 'sha':
return static::model('AlgoSha');
}
throw new Exception('Unknown hash options implementation');
}
}