Merge pull request #944 from christyjacob4/graphql
GraphQL Support in Appwrite
This commit is contained in:
commit
2eb024200e
16 changed files with 540 additions and 5940 deletions
2
.env
2
.env
|
@ -16,7 +16,7 @@ _APP_DB_PORT=3306
|
||||||
_APP_DB_SCHEMA=appwrite
|
_APP_DB_SCHEMA=appwrite
|
||||||
_APP_DB_USER=user
|
_APP_DB_USER=user
|
||||||
_APP_DB_PASS=password
|
_APP_DB_PASS=password
|
||||||
_APP_STORAGE_ANTIVIRUS=enabled
|
_APP_STORAGE_ANTIVIRUS=disabled
|
||||||
_APP_STORAGE_ANTIVIRUS_HOST=clamav
|
_APP_STORAGE_ANTIVIRUS_HOST=clamav
|
||||||
_APP_STORAGE_ANTIVIRUS_PORT=3310
|
_APP_STORAGE_ANTIVIRUS_PORT=3310
|
||||||
_APP_INFLUXDB_HOST=influxdb
|
_APP_INFLUXDB_HOST=influxdb
|
||||||
|
|
0
.swiftlint.yml
Normal file
0
.swiftlint.yml
Normal file
|
@ -7,6 +7,7 @@ $member = [
|
||||||
'home',
|
'home',
|
||||||
'console',
|
'console',
|
||||||
'account',
|
'account',
|
||||||
|
'graphql',
|
||||||
'teams.read',
|
'teams.read',
|
||||||
'teams.write',
|
'teams.write',
|
||||||
'documents.read',
|
'documents.read',
|
||||||
|
@ -22,6 +23,7 @@ $member = [
|
||||||
];
|
];
|
||||||
|
|
||||||
$admins = [
|
$admins = [
|
||||||
|
'graphql',
|
||||||
'teams.read',
|
'teams.read',
|
||||||
'teams.write',
|
'teams.write',
|
||||||
'documents.read',
|
'documents.read',
|
||||||
|
@ -56,6 +58,7 @@ return [
|
||||||
'public',
|
'public',
|
||||||
'home',
|
'home',
|
||||||
'console',
|
'console',
|
||||||
|
'graphql',
|
||||||
'documents.read',
|
'documents.read',
|
||||||
'files.read',
|
'files.read',
|
||||||
'locale.read',
|
'locale.read',
|
||||||
|
@ -82,6 +85,6 @@ return [
|
||||||
],
|
],
|
||||||
Auth::USER_ROLE_APP => [
|
Auth::USER_ROLE_APP => [
|
||||||
'label' => 'Application',
|
'label' => 'Application',
|
||||||
'scopes' => ['health.read'],
|
'scopes' => ['health.read', 'graphql'],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,5 +1,17 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Appwrite\GraphQL\GraphQLBuilder;
|
||||||
|
use GraphQL\GraphQL;
|
||||||
|
use GraphQL\Type\Schema;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use Appwrite\Utopia\Response;
|
||||||
|
use Appwrite\Utopia\Response\Model;
|
||||||
|
use Appwrite\GraphQL\Types\JsonType;
|
||||||
|
use GraphQL\Error\Error;
|
||||||
|
use GraphQL\Error\ClientAware;
|
||||||
|
use GraphQL\Error\FormattedError;
|
||||||
|
use GraphQL\Type\Definition\ListOfType;
|
||||||
use Utopia\App;
|
use Utopia\App;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,17 +19,78 @@ use Utopia\App;
|
||||||
* 1. Map all objects, object-params, object-fields
|
* 1. Map all objects, object-params, object-fields
|
||||||
* 2. Parse GraphQL request payload (use: https://github.com/webonyx/graphql-php)
|
* 2. Parse GraphQL request payload (use: https://github.com/webonyx/graphql-php)
|
||||||
* 3. Route request to relevant controllers (of REST API?) / resolvers and aggergate data
|
* 3. Route request to relevant controllers (of REST API?) / resolvers and aggergate data
|
||||||
* 4. Handle errors if any
|
* 4. Handle scope authentication
|
||||||
* 5. Returen JSON response
|
* 5. Handle errors
|
||||||
* 6. Write tests!
|
* 6. Return response
|
||||||
|
* 7. Write tests!
|
||||||
|
*
|
||||||
|
* Demo
|
||||||
|
* curl -H "Content-Type: application/json" http://localhost/v1/graphql -d '{"query": "query { echo(message: \"Hello World\") }" }'
|
||||||
|
*
|
||||||
|
* Explorers:
|
||||||
|
* - https://shopify.dev/tools/graphiql-admin-api
|
||||||
|
* - https://developer.github.com/v4/explorer/
|
||||||
|
* - http://localhost:4000
|
||||||
|
*
|
||||||
|
* Docs
|
||||||
|
* - Overview
|
||||||
|
* - Clients
|
||||||
|
*
|
||||||
|
* - Queries
|
||||||
|
* - Mutations
|
||||||
|
*
|
||||||
|
* - Objects
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
class MySafeException extends \Exception implements ClientAware
|
||||||
|
{
|
||||||
|
public function isClientSafe()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCategory()
|
||||||
|
{
|
||||||
|
return 'businessLogic';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
App::post('/v1/graphql')
|
App::post('/v1/graphql')
|
||||||
->desc('GraphQL Endpoint')
|
->desc('GraphQL Endpoint')
|
||||||
->groups(['api', 'graphql'])
|
->label('scope', 'graphql')
|
||||||
->label('scope', 'public')
|
->inject('request')
|
||||||
->action(
|
->inject('response')
|
||||||
function () {
|
->inject('schema')
|
||||||
throw new Exception('GraphQL support is coming soon!', 502);
|
->middleware(false)
|
||||||
|
->action(function ($request, $response, $schema) {
|
||||||
|
|
||||||
|
// $myErrorFormatter = function(Error $error) {
|
||||||
|
// $formattedError = FormattedError::createFromException($error);
|
||||||
|
// var_dump("***** IN ERROR FORMATTER ******");
|
||||||
|
// return $formattedError;
|
||||||
|
// };
|
||||||
|
|
||||||
|
$query = $request->getPayload('query', '');
|
||||||
|
$variables = $request->getPayload('variables', null);
|
||||||
|
$response->setContentType(Response::CONTENT_TYPE_NULL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$rootValue = [];
|
||||||
|
$result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variables);
|
||||||
|
$output = $result->toArray();
|
||||||
|
} catch (\Exception $error) {
|
||||||
|
$output = [
|
||||||
|
'errors' => [
|
||||||
|
[
|
||||||
|
'message' => $error->getMessage().'xxx',
|
||||||
|
'code' => $error->getCode(),
|
||||||
|
'file' => $error->getFile(),
|
||||||
|
'line' => $error->getLine(),
|
||||||
|
'trace' => $error->getTrace(),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$response->json($output);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -98,6 +98,8 @@ App::get('/v1/users')
|
||||||
/** @var Appwrite\Utopia\Response $response */
|
/** @var Appwrite\Utopia\Response $response */
|
||||||
/** @var Appwrite\Database\Database $projectDB */
|
/** @var Appwrite\Database\Database $projectDB */
|
||||||
|
|
||||||
|
var_dump("Running execute method for list users");
|
||||||
|
|
||||||
$results = $projectDB->getCollection([
|
$results = $projectDB->getCollection([
|
||||||
'limit' => $limit,
|
'limit' => $limit,
|
||||||
'offset' => $offset,
|
'offset' => $offset,
|
||||||
|
@ -217,7 +219,7 @@ App::get('/v1/users/:userId/sessions')
|
||||||
'sum' => count($sessions),
|
'sum' => count($sessions),
|
||||||
'sessions' => $sessions
|
'sessions' => $sessions
|
||||||
]), Response::MODEL_SESSION_LIST);
|
]), Response::MODEL_SESSION_LIST);
|
||||||
}, ['response', 'projectDB', 'locale']);
|
});
|
||||||
|
|
||||||
App::get('/v1/users/:userId/logs')
|
App::get('/v1/users/:userId/logs')
|
||||||
->desc('Get User Logs')
|
->desc('Get User Logs')
|
||||||
|
|
|
@ -33,6 +33,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo
|
||||||
/** @var bool $mode */
|
/** @var bool $mode */
|
||||||
/** @var array $clients */
|
/** @var array $clients */
|
||||||
|
|
||||||
|
|
||||||
$localeParam = (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
|
$localeParam = (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
|
||||||
|
|
||||||
if (\in_array($localeParam, Config::getParam('locale-codes'))) {
|
if (\in_array($localeParam, Config::getParam('locale-codes'))) {
|
||||||
|
@ -41,6 +42,8 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo
|
||||||
|
|
||||||
$route = $utopia->match($request);
|
$route = $utopia->match($request);
|
||||||
|
|
||||||
|
var_dump("*********** In general.php init with route {$route->getURL()} *************");
|
||||||
|
|
||||||
if (!empty($route->getLabel('sdk.platform', [])) && empty($project->getId()) && ($route->getLabel('scope', '') !== 'public')) {
|
if (!empty($route->getLabel('sdk.platform', [])) && empty($project->getId()) && ($route->getLabel('scope', '') !== 'public')) {
|
||||||
throw new Exception('Missing or unknown project ID', 400);
|
throw new Exception('Missing or unknown project ID', 400);
|
||||||
}
|
}
|
||||||
|
@ -167,7 +170,9 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo
|
||||||
$scopes = $roles[$role]['scopes']; // Allowed scopes for user role
|
$scopes = $roles[$role]['scopes']; // Allowed scopes for user role
|
||||||
|
|
||||||
$authKey = $request->getHeader('x-appwrite-key', '');
|
$authKey = $request->getHeader('x-appwrite-key', '');
|
||||||
|
var_dump("***** AUTH KEY ******");
|
||||||
|
|
||||||
|
var_dump($authKey);
|
||||||
if (!empty($authKey)) { // API Key authentication
|
if (!empty($authKey)) { // API Key authentication
|
||||||
// Check if given key match project API keys
|
// Check if given key match project API keys
|
||||||
$key = $project->search('secret', $authKey, $project->getAttribute('keys', []));
|
$key = $project->search('secret', $authKey, $project->getAttribute('keys', []));
|
||||||
|
@ -210,6 +215,10 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo
|
||||||
|
|
||||||
// TDOO Check if user is god
|
// TDOO Check if user is god
|
||||||
|
|
||||||
|
// var_dump("*********** Allowed Scopes *********");
|
||||||
|
// var_dump($scopes);
|
||||||
|
// var_dump($scope);
|
||||||
|
|
||||||
if (!\in_array($scope, $scopes)) {
|
if (!\in_array($scope, $scopes)) {
|
||||||
if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS !== $project->getCollection()) { // Check if permission is denied because project is missing
|
if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS !== $project->getCollection()) { // Check if permission is denied because project is missing
|
||||||
throw new Exception('Project not found', 404);
|
throw new Exception('Project not found', 404);
|
||||||
|
@ -252,6 +261,8 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project) {
|
||||||
/** @var Utopia\View $layout */
|
/** @var Utopia\View $layout */
|
||||||
/** @var Appwrite\Database\Document $project */
|
/** @var Appwrite\Database\Document $project */
|
||||||
|
|
||||||
|
var_dump("*********** In general.php error *************");
|
||||||
|
|
||||||
$route = $utopia->match($request);
|
$route = $utopia->match($request);
|
||||||
$template = ($route) ? $route->getLabel('error', null) : null;
|
$template = ($route) ? $route->getLabel('error', null) : null;
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e
|
||||||
/** @var Appwrite\Event\Event $deletes */
|
/** @var Appwrite\Event\Event $deletes */
|
||||||
/** @var Appwrite\Event\Event $functions */
|
/** @var Appwrite\Event\Event $functions */
|
||||||
|
|
||||||
|
var_dump("*********** In api.php init *************");
|
||||||
|
|
||||||
Storage::setDevice('files', new Local(APP_STORAGE_UPLOADS.'/app-'.$project->getId()));
|
Storage::setDevice('files', new Local(APP_STORAGE_UPLOADS.'/app-'.$project->getId()));
|
||||||
Storage::setDevice('functions', new Local(APP_STORAGE_FUNCTIONS.'/app-'.$project->getId()));
|
Storage::setDevice('functions', new Local(APP_STORAGE_FUNCTIONS.'/app-'.$project->getId()));
|
||||||
|
|
||||||
|
@ -47,9 +49,12 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e
|
||||||
|
|
||||||
//TODO make sure we get array here
|
//TODO make sure we get array here
|
||||||
|
|
||||||
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
|
|
||||||
$timeLimit->setParam('{param-'.$key.'}', (\is_array($value)) ? \json_encode($value) : $value);
|
// var_dump($request->getParams());
|
||||||
}
|
|
||||||
|
// foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
|
||||||
|
// $timeLimit->setParam('{param-'.$key.'}', (\is_array($value)) ? \json_encode($value) : $value);
|
||||||
|
// }
|
||||||
|
|
||||||
$abuse = new Abuse($timeLimit);
|
$abuse = new Abuse($timeLimit);
|
||||||
|
|
||||||
|
@ -122,6 +127,8 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
|
||||||
/** @var Appwrite\Event\Event $functions */
|
/** @var Appwrite\Event\Event $functions */
|
||||||
/** @var bool $mode */
|
/** @var bool $mode */
|
||||||
|
|
||||||
|
var_dump("*********** In api.php shutdown *************");
|
||||||
|
|
||||||
if (!empty($events->getParam('event'))) {
|
if (!empty($events->getParam('event'))) {
|
||||||
if(empty($events->getParam('payload'))) {
|
if(empty($events->getParam('payload'))) {
|
||||||
$events->setParam('payload', $response->getPayload());
|
$events->setParam('payload', $response->getPayload());
|
||||||
|
|
|
@ -81,6 +81,8 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
|
||||||
$request = new Request($swooleRequest);
|
$request = new Request($swooleRequest);
|
||||||
$response = new Response($swooleResponse);
|
$response = new Response($swooleResponse);
|
||||||
|
|
||||||
|
var_dump($swooleRequest->header);
|
||||||
|
|
||||||
if(Files::isFileLoaded($request->getURI())) {
|
if(Files::isFileLoaded($request->getURI())) {
|
||||||
$time = (60 * 60 * 24 * 365 * 2); // 45 days cache
|
$time = (60 * 60 * 24 * 365 * 2); // 45 days cache
|
||||||
|
|
||||||
|
@ -99,9 +101,11 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
|
||||||
try {
|
try {
|
||||||
Authorization::cleanRoles();
|
Authorization::cleanRoles();
|
||||||
Authorization::setRole('*');
|
Authorization::setRole('*');
|
||||||
|
var_dump("******* Running App ******* ");
|
||||||
|
|
||||||
$app->run($request, $response);
|
$app->run($request, $response);
|
||||||
} catch (\Throwable $th) {
|
} catch (\Throwable $th) {
|
||||||
|
var_dump("*********** In http.php catching error *************");
|
||||||
Console::error('[Error] Type: '.get_class($th));
|
Console::error('[Error] Type: '.get_class($th));
|
||||||
Console::error('[Error] Message: '.$th->getMessage());
|
Console::error('[Error] Message: '.$th->getMessage());
|
||||||
Console::error('[Error] File: '.$th->getFile());
|
Console::error('[Error] File: '.$th->getFile());
|
||||||
|
|
22
app/init.php
22
app/init.php
|
@ -21,6 +21,7 @@ use Appwrite\Database\Document;
|
||||||
use Appwrite\Database\Validator\Authorization;
|
use Appwrite\Database\Validator\Authorization;
|
||||||
use Appwrite\Event\Event;
|
use Appwrite\Event\Event;
|
||||||
use Appwrite\Extend\PDO;
|
use Appwrite\Extend\PDO;
|
||||||
|
use Appwrite\GraphQL\GraphQLBuilder;
|
||||||
use Appwrite\OpenSSL\OpenSSL;
|
use Appwrite\OpenSSL\OpenSSL;
|
||||||
use Utopia\App;
|
use Utopia\App;
|
||||||
use Utopia\View;
|
use Utopia\View;
|
||||||
|
@ -502,3 +503,24 @@ App::setResource('geodb', function($register) {
|
||||||
/** @var Utopia\Registry\Registry $register */
|
/** @var Utopia\Registry\Registry $register */
|
||||||
return $register->get('geodb');
|
return $register->get('geodb');
|
||||||
}, ['register']);
|
}, ['register']);
|
||||||
|
|
||||||
|
App::setResource('schema', function($utopia, $response, $request, $register) {
|
||||||
|
|
||||||
|
$schema = null;
|
||||||
|
try {
|
||||||
|
/*
|
||||||
|
Try to get the schema from the register.
|
||||||
|
If there is no schema, an exception will be thrown
|
||||||
|
*/
|
||||||
|
var_dump('[INFO] Getting Schema from register..');
|
||||||
|
$schema = $register->get('_schema');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
var_dump('[INFO] Exception, Schema not present. Generating Schema');
|
||||||
|
$schema = GraphQLBuilder::buildSchema($utopia, $response, $request);
|
||||||
|
$register->set('_schema', function () use ($schema){ // Register cache connection
|
||||||
|
return $schema;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}, ['utopia', 'response', 'request', 'register']);
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
"appwrite/php-clamav": "1.0.*",
|
"appwrite/php-clamav": "1.0.*",
|
||||||
|
|
||||||
"utopia-php/framework": "0.10.0",
|
"utopia-php/framework": "0.12.0",
|
||||||
"utopia-php/abuse": "0.3.*",
|
"utopia-php/abuse": "0.3.*",
|
||||||
"utopia-php/analytics": "0.1.*",
|
"utopia-php/analytics": "0.1.*",
|
||||||
"utopia-php/audit": "0.5.*",
|
"utopia-php/audit": "0.5.*",
|
||||||
|
@ -50,6 +50,7 @@
|
||||||
"utopia-php/storage": "0.4.*",
|
"utopia-php/storage": "0.4.*",
|
||||||
"utopia-php/image": "0.1.*",
|
"utopia-php/image": "0.1.*",
|
||||||
|
|
||||||
|
"webonyx/graphql-php": "14.4.0",
|
||||||
"resque/php-resque": "1.3.6",
|
"resque/php-resque": "1.3.6",
|
||||||
"matomo/device-detector": "4.1.0",
|
"matomo/device-detector": "4.1.0",
|
||||||
"dragonmantank/cron-expression": "3.1.0",
|
"dragonmantank/cron-expression": "3.1.0",
|
||||||
|
|
5920
composer.lock
generated
5920
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -63,7 +63,7 @@ services:
|
||||||
- ./psalm.xml:/usr/src/code/psalm.xml
|
- ./psalm.xml:/usr/src/code/psalm.xml
|
||||||
- ./tests:/usr/src/code/tests
|
- ./tests:/usr/src/code/tests
|
||||||
- ./app:/usr/src/code/app
|
- ./app:/usr/src/code/app
|
||||||
# - ./vendor:/usr/src/code/vendor
|
- ./vendor:/usr/src/code/vendor
|
||||||
- ./docs:/usr/src/code/docs
|
- ./docs:/usr/src/code/docs
|
||||||
- ./public:/usr/src/code/public
|
- ./public:/usr/src/code/public
|
||||||
- ./src:/usr/src/code/src
|
- ./src:/usr/src/code/src
|
||||||
|
@ -523,6 +523,14 @@ services:
|
||||||
|
|
||||||
# Dev Tools End ------------------------------------------------------------------------------------------
|
# Dev Tools End ------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
graphiql:
|
||||||
|
container_name: graphiql
|
||||||
|
ports:
|
||||||
|
- '9506:4000'
|
||||||
|
environment:
|
||||||
|
- API_URL=http://localhost/v1/graphql
|
||||||
|
image: npalm/graphiql
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
gateway:
|
gateway:
|
||||||
appwrite:
|
appwrite:
|
||||||
|
|
295
src/Appwrite/GraphQL/GraphQLBuilder.php
Normal file
295
src/Appwrite/GraphQL/GraphQLBuilder.php
Normal file
|
@ -0,0 +1,295 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Appwrite\GraphQL;
|
||||||
|
|
||||||
|
use Appwrite\GraphQL\Types\JsonType;
|
||||||
|
use Appwrite\Utopia\Response;
|
||||||
|
use Appwrite\Utopia\Response\Model;
|
||||||
|
use Exception;
|
||||||
|
use GraphQL\Type\Definition\ListOfType;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use GraphQL\Type\Schema;
|
||||||
|
use MySafeException;
|
||||||
|
|
||||||
|
class GraphQLBuilder {
|
||||||
|
|
||||||
|
public static $jsonParser;
|
||||||
|
|
||||||
|
public static $typeMapping;
|
||||||
|
|
||||||
|
private static function init() {
|
||||||
|
self::$jsonParser = new JsonType();
|
||||||
|
self::$typeMapping = [
|
||||||
|
Model::TYPE_BOOLEAN => Type::boolean(),
|
||||||
|
Model::TYPE_STRING => Type::string(),
|
||||||
|
Model::TYPE_INTEGER => Type::int(),
|
||||||
|
Model::TYPE_FLOAT => Type::float(),
|
||||||
|
Response::MODEL_NONE => Type::string(),
|
||||||
|
Model::TYPE_JSON => self::$jsonParser,
|
||||||
|
Response::MODEL_ANY => self::$jsonParser,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
static function createTypeMapping(Model $model, Response $response) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
If the map already contains the type, end the recursion
|
||||||
|
and return.
|
||||||
|
*/
|
||||||
|
if (isset(self::$typeMapping[$model->getType()])) return;
|
||||||
|
|
||||||
|
$rules = $model->getRules();
|
||||||
|
$name = $model->getType();
|
||||||
|
$fields = [];
|
||||||
|
$type = null;
|
||||||
|
/*
|
||||||
|
Iterate through all the rules in the response model. Each rule is of the form
|
||||||
|
[
|
||||||
|
[KEY 1] => [
|
||||||
|
'type' => A string from Appwrite/Utopia/Response
|
||||||
|
'description' => A description of the type
|
||||||
|
'default' => A default value for this type
|
||||||
|
'example' => An example of this type
|
||||||
|
'require' => a boolean representing whether this field is required
|
||||||
|
'array' => a boolean representing whether this field is an array
|
||||||
|
],
|
||||||
|
[KEY 2] => [
|
||||||
|
],
|
||||||
|
[KEY 3] => [
|
||||||
|
] .....
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
foreach ($rules as $key => $props) {
|
||||||
|
/*
|
||||||
|
If there are any field names containing characters other than a-z, A-Z, 0-9, _ ,
|
||||||
|
we need to remove all those characters. Currently Appwrite's Response model has only the
|
||||||
|
$ sign which is prohibited. So we're only replacing that. We need to replace this with a regex
|
||||||
|
based approach.
|
||||||
|
*/
|
||||||
|
$keyWithoutSpecialChars = str_replace('$', '', $key);
|
||||||
|
if (isset(self::$typeMapping[$props['type']])) {
|
||||||
|
$type = self::$typeMapping[$props['type']];
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$complexModel = $response->getModel($props['type']);
|
||||||
|
self::createTypeMapping($complexModel, $response);
|
||||||
|
$type = self::$typeMapping[$props['type']];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
var_dump("Could Not find model for : {$props['type']}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
If any of the rules is a list,
|
||||||
|
Wrap the base type with a listOf Type
|
||||||
|
*/
|
||||||
|
if ($props['array']) {
|
||||||
|
$type = Type::listOf($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields[$keyWithoutSpecialChars] = [
|
||||||
|
'type' => $type,
|
||||||
|
'description' => $props['description'],
|
||||||
|
'resolve' => function ($object, $args, $context, $info) use ($key, $type) {
|
||||||
|
|
||||||
|
// var_dump("************* RESOLVING FIELD {$info->fieldName} *************");
|
||||||
|
// var_dump($info->returnType->getWrappedType());
|
||||||
|
// var_dump("isListType : ", $info->returnType instanceof ListOfType);
|
||||||
|
// var_dump("isCompositeType : ", Type::isCompositeType($info->returnType));
|
||||||
|
// var_dump("isBuiltinType : ", Type::isBuiltInType($info->returnType));
|
||||||
|
// var_dump("isLeafType : ", Type::isLeafType($info->returnType));
|
||||||
|
// var_dump("isOutputType : ", Type::isOutputType($info->returnType));
|
||||||
|
// var_dump("PHP Type of object: " . gettype($object[$key]));
|
||||||
|
|
||||||
|
return $object[$key];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$objectType = [
|
||||||
|
'name' => $name,
|
||||||
|
'fields' => $fields
|
||||||
|
];
|
||||||
|
|
||||||
|
self::$typeMapping[$name] = new ObjectType($objectType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getArgType($validator, bool $required, $utopia, $injections) {
|
||||||
|
$validator = (\is_callable($validator)) ? call_user_func_array($validator, $utopia->getResources($injections)) : $validator;
|
||||||
|
$type = [];
|
||||||
|
switch ((!empty($validator)) ? \get_class($validator) : '') {
|
||||||
|
case 'Utopia\Validator\Text':
|
||||||
|
$type = Type::string();
|
||||||
|
break;
|
||||||
|
case 'Utopia\Validator\Boolean':
|
||||||
|
$type = Type::boolean();
|
||||||
|
break;
|
||||||
|
case 'Appwrite\Database\Validator\UID':
|
||||||
|
$type = Type::string();
|
||||||
|
break;
|
||||||
|
case 'Utopia\Validator\Email':
|
||||||
|
$type = Type::string();
|
||||||
|
break;
|
||||||
|
case 'Utopia\Validator\URL':
|
||||||
|
$type = Type::string();
|
||||||
|
break;
|
||||||
|
case 'Utopia\Validator\JSON':
|
||||||
|
case 'Utopia\Validator\Mock':
|
||||||
|
case 'Utopia\Validator\Assoc':
|
||||||
|
$type = Type::string();
|
||||||
|
break;
|
||||||
|
case 'Appwrite\Storage\Validator\File':
|
||||||
|
$type = Type::string();
|
||||||
|
case 'Utopia\Validator\ArrayList':
|
||||||
|
$type = Type::listOf(Type::string());
|
||||||
|
break;
|
||||||
|
case 'Appwrite\Auth\Validator\Password':
|
||||||
|
$type = Type::string();
|
||||||
|
break;
|
||||||
|
case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */
|
||||||
|
$type = Type::int();
|
||||||
|
break;
|
||||||
|
case 'Utopia\Validator\Numeric':
|
||||||
|
$type = Type::int();
|
||||||
|
break;
|
||||||
|
case 'Utopia\Validator\Length':
|
||||||
|
$type = Type::string();
|
||||||
|
break;
|
||||||
|
case 'Utopia\Validator\Host':
|
||||||
|
$type = Type::string();
|
||||||
|
break;
|
||||||
|
case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */
|
||||||
|
$type = Type::string();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$type = Type::string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($required) {
|
||||||
|
$type = Type::nonNull($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getArgs(array $params, $utopia) {
|
||||||
|
$args = [];
|
||||||
|
foreach ($params as $key => $value) {
|
||||||
|
$args[$key] = [
|
||||||
|
'type' => self::getArgType($value['validator'],!$value['optional'], $utopia, $value['injections']),
|
||||||
|
'description' => $value['description'],
|
||||||
|
'defaultValue' => $value['default']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function isModel($response, Model $model) {
|
||||||
|
|
||||||
|
foreach ($model->getRules() as $key => $rule) {
|
||||||
|
if (!isset($response[$key])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buildSchema($utopia, $response) {
|
||||||
|
|
||||||
|
self::init();
|
||||||
|
|
||||||
|
var_dump("[INFO] Building GraphQL Schema...");
|
||||||
|
$start = microtime(true);
|
||||||
|
|
||||||
|
$queryFields = [];
|
||||||
|
$mutationFields = [];
|
||||||
|
foreach($utopia->getRoutes() as $method => $routes ){
|
||||||
|
foreach($routes as $route) {
|
||||||
|
$namespace = $route->getLabel('sdk.namespace', '');
|
||||||
|
|
||||||
|
if ($namespace == 'database' || true) {
|
||||||
|
$methodName = $namespace.'_'.$route->getLabel('sdk.method', '');
|
||||||
|
$responseModelName = $route->getLabel('sdk.response.model', "");
|
||||||
|
// var_dump("******************************************");
|
||||||
|
// var_dump("Processing route : ${method} : {$route->getURL()}");
|
||||||
|
// var_dump("Model Name : ${responseModelName}");
|
||||||
|
if ( $responseModelName !== "" && $responseModelName !== Response::MODEL_NONE ) {
|
||||||
|
$responseModel = $response->getModel($responseModelName);
|
||||||
|
self::createTypeMapping($responseModel, $response);
|
||||||
|
$type = self::$typeMapping[$responseModel->getType()];
|
||||||
|
// var_dump("Type Created : ${type}");
|
||||||
|
$args = self::getArgs($route->getParams(), $utopia);
|
||||||
|
// var_dump("Args Generated :");
|
||||||
|
// var_dump($args);
|
||||||
|
|
||||||
|
$field = [
|
||||||
|
'type' => $type,
|
||||||
|
'description' => $route->getDesc(),
|
||||||
|
'args' => $args,
|
||||||
|
'resolve' => function ($type, $args, $context, $info) use (&$utopia, $route, $response) {
|
||||||
|
// var_dump("************* REACHED RESOLVE FOR {$info->fieldName} *****************");
|
||||||
|
// var_dump($route);
|
||||||
|
// var_dump("************* CONTEXT *****************");
|
||||||
|
// var_dump($context);
|
||||||
|
// var_dump("********************** ARGS *******************");
|
||||||
|
// var_dump($args);
|
||||||
|
|
||||||
|
$utopia->setRoute($route);
|
||||||
|
$utopia->execute($route, $args);
|
||||||
|
|
||||||
|
// var_dump("**************** OUTPUT ************");
|
||||||
|
// var_dump($response->getPayload());
|
||||||
|
|
||||||
|
$result = $response->getPayload();
|
||||||
|
|
||||||
|
if (self::isModel($result, $response->getModel(Response::MODEL_ERROR)) || self::isModel($result, $response->getModel(Response::MODEL_ERROR_DEV))) {
|
||||||
|
throw new MySafeException($result['message'], $result['code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($method == 'GET') {
|
||||||
|
$queryFields[$methodName] = $field;
|
||||||
|
} else if ($method == 'POST' || $method == 'PUT' || $method == 'PATCH' || $method == 'DELETE') {
|
||||||
|
$mutationFields[$methodName] = $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
// var_dump("Processed route : ${method} : {$route->getURL()}");
|
||||||
|
} else {
|
||||||
|
// var_dump("Skipping route : {$route->getURL()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($queryFields);
|
||||||
|
ksort($mutationFields);
|
||||||
|
|
||||||
|
$queryType = new ObjectType([
|
||||||
|
'name' => 'Query',
|
||||||
|
'description' => 'The root of all your queries',
|
||||||
|
'fields' => $queryFields
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mutationType = new ObjectType([
|
||||||
|
'name' => 'Mutation',
|
||||||
|
'description' => 'The root of all your mutations',
|
||||||
|
'fields' => $mutationFields
|
||||||
|
]);
|
||||||
|
|
||||||
|
$schema = new Schema([
|
||||||
|
'query' => $queryType,
|
||||||
|
'mutation' => $mutationType
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
$time_elapsed_secs = microtime(true) - $start;
|
||||||
|
var_dump("[INFO] Time Taken To Build Schema : ${time_elapsed_secs}s");
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
}
|
68
src/Appwrite/GraphQL/Types/JsonType.php
Normal file
68
src/Appwrite/GraphQL/Types/JsonType.php
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Appwrite\GraphQL\Types;
|
||||||
|
|
||||||
|
use GraphQL\Language\AST\BooleanValueNode;
|
||||||
|
use GraphQL\Language\AST\FloatValueNode;
|
||||||
|
use GraphQL\Language\AST\IntValueNode;
|
||||||
|
use GraphQL\Language\AST\ListValueNode;
|
||||||
|
use GraphQL\Language\AST\Node;
|
||||||
|
use GraphQL\Language\AST\ObjectValueNode;
|
||||||
|
use GraphQL\Language\AST\StringValueNode;
|
||||||
|
use GraphQL\Type\Definition\ScalarType;
|
||||||
|
|
||||||
|
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
|
||||||
|
class JsonType 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).';
|
||||||
|
|
||||||
|
public function __construct(?string $name = null)
|
||||||
|
{
|
||||||
|
if ($name) {
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parseValue($value)
|
||||||
|
{
|
||||||
|
return $this->identity($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serialize($value)
|
||||||
|
{
|
||||||
|
return $this->identity($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parseLiteral(Node $valueNode, ?array $variables = null)
|
||||||
|
{
|
||||||
|
switch ($valueNode) {
|
||||||
|
case ($valueNode instanceof StringValueNode):
|
||||||
|
case ($valueNode instanceof BooleanValueNode):
|
||||||
|
return $valueNode->value;
|
||||||
|
case ($valueNode instanceof IntValueNode):
|
||||||
|
case ($valueNode instanceof FloatValueNode):
|
||||||
|
return floatval($valueNode->value);
|
||||||
|
case ($valueNode instanceof ObjectValueNode): {
|
||||||
|
$value = [];
|
||||||
|
foreach ($valueNode->fields as $field) {
|
||||||
|
$value[$field->name->value] = $this->parseLiteral($field->value);
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
case ($valueNode instanceof ListValueNode):
|
||||||
|
return array_map([$this, 'parseLiteral'], $valueNode->values);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function identity($value)
|
||||||
|
{
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -120,6 +120,9 @@ class Response extends SwooleResponse
|
||||||
// Tests (keep last)
|
// Tests (keep last)
|
||||||
const MODEL_MOCK = 'mock';
|
const MODEL_MOCK = 'mock';
|
||||||
|
|
||||||
|
// Content type
|
||||||
|
const CONTENT_TYPE_NULL = 'null';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Filter
|
* @var Filter
|
||||||
*/
|
*/
|
||||||
|
@ -266,7 +269,22 @@ class Response extends SwooleResponse
|
||||||
$output = self::getFilter()->parse($output, $model);
|
$output = self::getFilter()->parse($output, $model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch($this->getContentType()) {
|
||||||
|
case self::CONTENT_TYPE_JSON:
|
||||||
$this->json(!empty($output) ? $output : new stdClass());
|
$this->json(!empty($output) ? $output : new stdClass());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::CONTENT_TYPE_NULL:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::CONTENT_TYPE_YAML:
|
||||||
|
$this->yaml(!empty($output) ? $output : new stdClass());
|
||||||
|
break;
|
||||||
|
|
||||||
|
default :
|
||||||
|
$this->json(!empty($output) ? $output : new stdClass());
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -318,6 +336,14 @@ class Response extends SwooleResponse
|
||||||
|
|
||||||
$this->payload = $output;
|
$this->payload = $output;
|
||||||
|
|
||||||
|
// var_dump("********************** PAYLOAD SET *********************");
|
||||||
|
// var_dump("Message : {$output['message']}");
|
||||||
|
// var_dump("Code : {$output['code']}");
|
||||||
|
// var_dump("Version : {$output['version']}");
|
||||||
|
// var_dump("File : {$output['file']}");
|
||||||
|
// var_dump("Line : {$output['line']}");
|
||||||
|
// var_dump("Trace : ");
|
||||||
|
|
||||||
return $this->payload;
|
return $this->payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue