2020-01-04 10:16:26 +13:00
|
|
|
<?php
|
|
|
|
|
2022-04-26 19:49:36 +12:00
|
|
|
use Appwrite\Extend\Exception;
|
2022-10-12 14:04:11 +13:00
|
|
|
use Appwrite\GraphQL\Promises\Adapter;
|
2022-10-28 10:04:31 +13:00
|
|
|
use Appwrite\GraphQL\Schema;
|
2022-07-13 23:21:02 +12:00
|
|
|
use Appwrite\Utopia\Request;
|
2021-02-26 09:43:21 +13:00
|
|
|
use Appwrite\Utopia\Response;
|
2022-06-22 16:44:58 +12:00
|
|
|
use GraphQL\Error\DebugFlag;
|
2022-04-06 01:48:51 +12:00
|
|
|
use GraphQL\GraphQL;
|
2022-10-28 10:04:31 +13:00
|
|
|
use GraphQL\Type\Schema as GQLSchema;
|
2022-04-20 22:30:48 +12:00
|
|
|
use GraphQL\Validator\Rules\DisableIntrospection;
|
2022-04-07 18:40:28 +12:00
|
|
|
use GraphQL\Validator\Rules\QueryComplexity;
|
|
|
|
use GraphQL\Validator\Rules\QueryDepth;
|
|
|
|
use Swoole\Coroutine\WaitGroup;
|
2020-06-29 05:31:21 +12:00
|
|
|
use Utopia\App;
|
2022-10-28 10:04:31 +13:00
|
|
|
use Utopia\Database\Document;
|
2022-04-07 18:40:28 +12:00
|
|
|
use Utopia\Validator\JSON;
|
2022-10-14 14:32:16 +13:00
|
|
|
use Utopia\Validator\Text;
|
2020-01-04 10:16:26 +13:00
|
|
|
|
2022-04-11 23:08:57 +12:00
|
|
|
App::get('/v1/graphql')
|
2021-02-26 09:43:21 +13:00
|
|
|
->desc('GraphQL Endpoint')
|
2022-07-19 13:31:14 +12:00
|
|
|
->groups(['graphql'])
|
2021-03-05 07:40:52 +13:00
|
|
|
->label('scope', 'graphql')
|
2022-04-11 23:08:57 +12:00
|
|
|
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
2022-04-07 18:39:33 +12:00
|
|
|
->label('sdk.namespace', 'graphql')
|
2022-12-21 20:45:34 +13:00
|
|
|
->label('sdk.hide', true)
|
2022-10-25 15:53:15 +13:00
|
|
|
->label('sdk.description', '/docs/references/graphql/get.md')
|
2022-04-07 18:39:33 +12:00
|
|
|
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
|
|
|
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
|
|
|
->label('sdk.response.model', Response::MODEL_ANY)
|
|
|
|
->label('abuse-limit', 60)
|
2022-04-07 18:39:42 +12:00
|
|
|
->label('abuse-time', 60)
|
2023-03-02 01:43:34 +13:00
|
|
|
->param('query', '', new Text(0, 0), 'The query to execute.')
|
2022-10-14 14:32:16 +13:00
|
|
|
->param('operationName', '', new Text(256), 'The name of the operation to execute.', true)
|
|
|
|
->param('variables', '', new Text(0), 'The JSON encoded variables to use in the query.', true)
|
2021-02-26 09:43:21 +13:00
|
|
|
->inject('request')
|
|
|
|
->inject('response')
|
2022-07-14 20:11:39 +12:00
|
|
|
->inject('schema')
|
2022-10-14 14:32:16 +13:00
|
|
|
->inject('promiseAdapter')
|
2022-10-28 10:04:31 +13:00
|
|
|
->action(function (string $query, string $operationName, string $variables, Request $request, Response $response, GQLSchema $schema, Adapter $promiseAdapter) {
|
2022-10-14 14:32:16 +13:00
|
|
|
$query = [
|
|
|
|
'query' => $query,
|
|
|
|
];
|
|
|
|
|
2023-08-24 05:12:43 +12:00
|
|
|
if (! empty($operationName)) {
|
2022-10-14 14:32:16 +13:00
|
|
|
$query['operationName'] = $operationName;
|
|
|
|
}
|
|
|
|
|
2023-08-24 05:12:43 +12:00
|
|
|
if (! empty($variables)) {
|
2022-10-14 14:32:16 +13:00
|
|
|
$query['variables'] = \json_decode($variables, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
$output = execute($schema, $promiseAdapter, $query);
|
|
|
|
|
|
|
|
$response
|
|
|
|
->setStatusCode(Response::STATUS_CODE_OK)
|
|
|
|
->json($output);
|
|
|
|
});
|
2022-04-11 23:08:57 +12:00
|
|
|
|
2022-12-15 13:36:47 +13:00
|
|
|
App::post('/v1/graphql/mutation')
|
|
|
|
->desc('GraphQL Endpoint')
|
|
|
|
->groups(['graphql'])
|
|
|
|
->label('scope', 'graphql')
|
|
|
|
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
|
|
|
->label('sdk.namespace', 'graphql')
|
|
|
|
->label('sdk.method', 'mutation')
|
|
|
|
->label('sdk.methodType', 'graphql')
|
|
|
|
->label('sdk.description', '/docs/references/graphql/post.md')
|
|
|
|
->label('sdk.parameters', [
|
|
|
|
'query' => ['default' => [], 'validator' => new JSON(), 'description' => 'The query or queries to execute.', 'optional' => false],
|
|
|
|
])
|
|
|
|
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
|
|
|
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
|
|
|
->label('sdk.response.model', Response::MODEL_ANY)
|
|
|
|
->label('abuse-limit', 60)
|
|
|
|
->label('abuse-time', 60)
|
|
|
|
->inject('request')
|
|
|
|
->inject('response')
|
|
|
|
->inject('schema')
|
|
|
|
->inject('promiseAdapter')
|
|
|
|
->action(function (Request $request, Response $response, GQLSchema $schema, Adapter $promiseAdapter) {
|
|
|
|
$query = $request->getParams();
|
|
|
|
|
|
|
|
if ($request->getHeader('x-sdk-graphql') == 'true') {
|
|
|
|
$query = $query['query'];
|
|
|
|
}
|
|
|
|
|
|
|
|
$type = $request->getHeader('content-type');
|
|
|
|
|
|
|
|
if (\str_starts_with($type, 'application/graphql')) {
|
|
|
|
$query = parseGraphql($request);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (\str_starts_with($type, 'multipart/form-data')) {
|
|
|
|
$query = parseMultipart($query, $request);
|
|
|
|
}
|
|
|
|
|
|
|
|
$output = execute($schema, $promiseAdapter, $query);
|
|
|
|
|
|
|
|
$response
|
|
|
|
->setStatusCode(Response::STATUS_CODE_OK)
|
|
|
|
->json($output);
|
|
|
|
});
|
|
|
|
|
2022-04-11 23:08:57 +12:00
|
|
|
App::post('/v1/graphql')
|
|
|
|
->desc('GraphQL Endpoint')
|
2022-07-19 13:31:14 +12:00
|
|
|
->groups(['graphql'])
|
2022-04-11 23:08:57 +12:00
|
|
|
->label('scope', 'graphql')
|
|
|
|
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
|
|
|
->label('sdk.namespace', 'graphql')
|
2022-12-15 13:36:47 +13:00
|
|
|
->label('sdk.method', 'query')
|
2022-07-21 16:37:34 +12:00
|
|
|
->label('sdk.methodType', 'graphql')
|
2022-10-25 15:53:15 +13:00
|
|
|
->label('sdk.description', '/docs/references/graphql/post.md')
|
2022-10-13 17:33:55 +13:00
|
|
|
->label('sdk.parameters', [
|
2022-10-14 14:32:16 +13:00
|
|
|
'query' => ['default' => [], 'validator' => new JSON(), 'description' => 'The query or queries to execute.', 'optional' => false],
|
2022-10-13 17:33:55 +13:00
|
|
|
])
|
2022-04-11 23:08:57 +12:00
|
|
|
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
|
|
|
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
|
|
|
->label('sdk.response.model', Response::MODEL_ANY)
|
|
|
|
->label('abuse-limit', 60)
|
|
|
|
->label('abuse-time', 60)
|
|
|
|
->inject('request')
|
|
|
|
->inject('response')
|
2022-07-14 20:11:39 +12:00
|
|
|
->inject('schema')
|
2022-10-14 14:32:16 +13:00
|
|
|
->inject('promiseAdapter')
|
2022-10-28 10:04:31 +13:00
|
|
|
->action(function (Request $request, Response $response, GQLSchema $schema, Adapter $promiseAdapter) {
|
2022-10-14 14:32:16 +13:00
|
|
|
$query = $request->getParams();
|
|
|
|
|
|
|
|
if ($request->getHeader('x-sdk-graphql') == 'true') {
|
|
|
|
$query = $query['query'];
|
|
|
|
}
|
|
|
|
|
|
|
|
$type = $request->getHeader('content-type');
|
2022-12-13 15:43:29 +13:00
|
|
|
|
2022-10-14 14:32:16 +13:00
|
|
|
if (\str_starts_with($type, 'application/graphql')) {
|
|
|
|
$query = parseGraphql($request);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (\str_starts_with($type, 'multipart/form-data')) {
|
|
|
|
$query = parseMultipart($query, $request);
|
|
|
|
}
|
|
|
|
|
|
|
|
$output = execute($schema, $promiseAdapter, $query);
|
|
|
|
|
|
|
|
$response
|
|
|
|
->setStatusCode(Response::STATUS_CODE_OK)
|
|
|
|
->json($output);
|
|
|
|
});
|
2021-11-25 21:04:39 +13:00
|
|
|
|
2022-07-07 18:13:12 +12:00
|
|
|
/**
|
2022-07-19 13:41:31 +12:00
|
|
|
* Execute a GraphQL request
|
|
|
|
*
|
2023-08-24 05:12:43 +12:00
|
|
|
* @param GQLSchema $schema
|
|
|
|
* @param Adapter $promiseAdapter
|
|
|
|
* @param array $query
|
2022-10-14 14:32:16 +13:00
|
|
|
* @return array
|
2023-08-24 05:12:43 +12:00
|
|
|
*
|
2022-07-07 18:13:12 +12:00
|
|
|
* @throws Exception
|
|
|
|
*/
|
2022-10-14 14:32:16 +13:00
|
|
|
function execute(
|
2022-10-28 10:04:31 +13:00
|
|
|
GQLSchema $schema,
|
2022-10-12 14:04:11 +13:00
|
|
|
Adapter $promiseAdapter,
|
2022-10-14 14:32:16 +13:00
|
|
|
array $query
|
|
|
|
): array {
|
2022-07-18 19:29:37 +12:00
|
|
|
$maxBatchSize = App::getEnv('_APP_GRAPHQL_MAX_BATCH_SIZE', 10);
|
2022-09-23 15:55:10 +12:00
|
|
|
$maxComplexity = App::getEnv('_APP_GRAPHQL_MAX_COMPLEXITY', 250);
|
|
|
|
$maxDepth = App::getEnv('_APP_GRAPHQL_MAX_DEPTH', 3);
|
2022-07-07 19:39:42 +12:00
|
|
|
|
2023-08-24 05:12:43 +12:00
|
|
|
if (! empty($query) && ! isset($query[0])) {
|
2022-07-13 15:49:59 +12:00
|
|
|
$query = [$query];
|
2022-07-07 18:13:12 +12:00
|
|
|
}
|
2022-07-14 15:57:34 +12:00
|
|
|
if (empty($query)) {
|
2022-09-21 18:36:43 +12:00
|
|
|
throw new Exception(Exception::GRAPHQL_NO_QUERY);
|
2022-04-11 23:08:57 +12:00
|
|
|
}
|
2022-07-14 15:56:02 +12:00
|
|
|
if (\count($query) > $maxBatchSize) {
|
2022-09-21 18:36:43 +12:00
|
|
|
throw new Exception(Exception::GRAPHQL_TOO_MANY_QUERIES);
|
2022-07-14 15:57:34 +12:00
|
|
|
}
|
2022-07-14 15:57:51 +12:00
|
|
|
foreach ($query as $item) {
|
2022-07-19 14:11:58 +12:00
|
|
|
if (empty($item['query'])) {
|
2022-10-14 13:18:08 +13:00
|
|
|
throw new Exception(Exception::GRAPHQL_NO_QUERY);
|
2022-07-14 15:57:51 +12:00
|
|
|
}
|
2022-07-14 15:56:02 +12:00
|
|
|
}
|
2022-07-13 17:21:41 +12:00
|
|
|
|
2022-07-20 01:28:47 +12:00
|
|
|
$flags = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE;
|
2022-04-26 19:49:36 +12:00
|
|
|
$validations = GraphQL::getStandardValidationRules();
|
2022-07-14 15:57:34 +12:00
|
|
|
|
2022-10-28 20:12:11 +13:00
|
|
|
if (App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
|
2022-06-22 16:44:58 +12:00
|
|
|
$validations[] = new DisableIntrospection();
|
2022-09-29 17:46:01 +13:00
|
|
|
$validations[] = new QueryComplexity($maxComplexity);
|
|
|
|
$validations[] = new QueryDepth($maxDepth);
|
2022-12-15 13:44:23 +13:00
|
|
|
}
|
|
|
|
if (App::getMode() === App::MODE_TYPE_PRODUCTION) {
|
2022-07-20 01:28:47 +12:00
|
|
|
$flags = DebugFlag::NONE;
|
2022-06-22 16:44:58 +12:00
|
|
|
}
|
2022-07-13 17:21:41 +12:00
|
|
|
|
2022-07-13 15:49:59 +12:00
|
|
|
$promises = [];
|
2022-07-13 17:21:41 +12:00
|
|
|
foreach ($query as $indexed) {
|
2022-07-13 15:49:59 +12:00
|
|
|
$promises[] = GraphQL::promiseToExecute(
|
|
|
|
$promiseAdapter,
|
2022-07-14 20:11:39 +12:00
|
|
|
$schema,
|
2022-07-13 15:49:59 +12:00
|
|
|
$indexed['query'],
|
2022-07-13 17:06:48 +12:00
|
|
|
variableValues: $indexed['variables'] ?? null,
|
|
|
|
operationName: $indexed['operationName'] ?? null,
|
2022-07-13 15:49:59 +12:00
|
|
|
validationRules: $validations
|
|
|
|
);
|
|
|
|
}
|
2022-04-07 18:40:28 +12:00
|
|
|
|
2022-05-02 20:21:40 +12:00
|
|
|
$output = [];
|
2022-04-11 23:08:57 +12:00
|
|
|
$wg = new WaitGroup();
|
|
|
|
$wg->add();
|
2022-07-13 15:49:59 +12:00
|
|
|
$promiseAdapter->all($promises)->then(
|
2022-07-20 01:28:47 +12:00
|
|
|
function (array $results) use (&$output, &$wg, $flags) {
|
2022-07-14 20:11:39 +12:00
|
|
|
try {
|
2022-07-20 01:28:47 +12:00
|
|
|
$output = processResult($results, $flags);
|
2022-07-14 20:11:39 +12:00
|
|
|
} finally {
|
|
|
|
$wg->done();
|
|
|
|
}
|
2022-04-11 23:08:57 +12:00
|
|
|
}
|
2022-04-26 19:49:36 +12:00
|
|
|
);
|
2022-04-11 23:08:57 +12:00
|
|
|
$wg->wait();
|
2022-05-02 20:21:40 +12:00
|
|
|
|
2022-10-14 14:32:16 +13:00
|
|
|
return $output;
|
2022-04-11 23:08:57 +12:00
|
|
|
}
|
2022-07-13 23:21:02 +12:00
|
|
|
|
|
|
|
/**
|
2022-10-14 14:32:16 +13:00
|
|
|
* Parse an "application/graphql" type request
|
2022-07-13 23:21:02 +12:00
|
|
|
*
|
2023-08-24 05:12:43 +12:00
|
|
|
* @param Request $request
|
2022-07-13 23:21:02 +12:00
|
|
|
* @return array
|
|
|
|
*/
|
2022-10-12 20:20:44 +13:00
|
|
|
function parseGraphql(Request $request): array
|
2022-07-13 23:21:02 +12:00
|
|
|
{
|
2022-10-14 14:32:16 +13:00
|
|
|
return ['query' => $request->getRawPayload()];
|
2022-07-13 23:21:02 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-10-14 14:32:16 +13:00
|
|
|
* Parse an "multipart/form-data" type request
|
2022-07-13 23:21:02 +12:00
|
|
|
*
|
2023-08-24 05:12:43 +12:00
|
|
|
* @param array $query
|
|
|
|
* @param Request $request
|
2022-07-13 23:21:02 +12:00
|
|
|
* @return array
|
|
|
|
*/
|
2022-10-12 20:20:44 +13:00
|
|
|
function parseMultipart(array $query, Request $request): array
|
2022-07-13 23:21:02 +12:00
|
|
|
{
|
|
|
|
$operations = \json_decode($query['operations'], true);
|
|
|
|
$map = \json_decode($query['map'], true);
|
2022-12-13 15:43:29 +13:00
|
|
|
|
2022-07-13 23:21:02 +12:00
|
|
|
foreach ($map as $fileKey => $locations) {
|
|
|
|
foreach ($locations as $location) {
|
|
|
|
$items = &$operations;
|
|
|
|
foreach (\explode('.', $location) as $key) {
|
2023-08-24 05:12:43 +12:00
|
|
|
if (! isset($items[$key]) || ! \is_array($items[$key])) {
|
2022-07-13 23:21:02 +12:00
|
|
|
$items[$key] = [];
|
|
|
|
}
|
|
|
|
$items = &$items[$key];
|
|
|
|
}
|
|
|
|
$items = $request->getFiles($fileKey);
|
|
|
|
}
|
|
|
|
}
|
2022-12-13 15:43:29 +13:00
|
|
|
|
2022-07-13 23:21:02 +12:00
|
|
|
$query['query'] = $operations['query'];
|
|
|
|
$query['variables'] = $operations['variables'];
|
|
|
|
|
2022-12-13 15:43:29 +13:00
|
|
|
unset($query['operations']);
|
|
|
|
unset($query['map']);
|
|
|
|
|
2022-07-13 23:21:02 +12:00
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-10-13 17:33:55 +13:00
|
|
|
* Process an array of results for output.
|
2022-07-13 23:21:02 +12:00
|
|
|
*
|
|
|
|
* @param $result
|
|
|
|
* @param $debugFlags
|
2022-07-19 13:41:31 +12:00
|
|
|
* @return array
|
2022-07-13 23:21:02 +12:00
|
|
|
*/
|
2022-07-19 13:41:31 +12:00
|
|
|
function processResult($result, $debugFlags): array
|
2022-07-13 23:21:02 +12:00
|
|
|
{
|
2022-10-14 14:33:42 +13:00
|
|
|
// Only one query, return the result
|
2023-08-24 05:12:43 +12:00
|
|
|
if (! isset($result[1])) {
|
2022-07-19 13:41:31 +12:00
|
|
|
return $result[0]->toArray($debugFlags);
|
2022-07-13 23:21:02 +12:00
|
|
|
}
|
2022-10-13 17:33:55 +13:00
|
|
|
|
2022-10-14 14:33:42 +13:00
|
|
|
// Batched queries, return an array of results
|
|
|
|
return \array_map(
|
|
|
|
static function ($item) use ($debugFlags) {
|
|
|
|
return $item->toArray($debugFlags);
|
|
|
|
},
|
2022-10-13 17:33:55 +13:00
|
|
|
$result
|
2022-10-14 14:33:42 +13:00
|
|
|
);
|
2022-07-13 23:21:02 +12:00
|
|
|
}
|
2022-10-28 10:04:31 +13:00
|
|
|
|
|
|
|
App::shutdown()
|
|
|
|
->groups(['schema'])
|
|
|
|
->inject('project')
|
|
|
|
->action(function (Document $project) {
|
|
|
|
Schema::setDirty($project->getId());
|
|
|
|
});
|