From b5e8273839308baab4c9d64dfc0917b90c4added Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 7 Jul 2022 19:39:42 +1200 Subject: [PATCH] Fix custom entity id properties --- app/controllers/api/graphql.php | 81 ++++++------------- src/Appwrite/GraphQL/Builder.php | 31 ++++--- tests/e2e/Services/GraphQL/GraphQLBase.php | 55 ++++++++++++- .../GraphQL/GraphQLDatabaseServerTest.php | 56 +++++++------ 4 files changed, 129 insertions(+), 94 deletions(-) diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index 51cf5fa444..d61340ed74 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -55,68 +55,14 @@ App::post('/v1/graphql') ->param('query', '', new Text(1024), 'The query to execute. Max 1024 chars.', true) ->param('operationName', null, new Text(256), 'Name of the operation to execute', true) ->param('variables', [], new JSON(), 'Variables to use in the operation', true) + ->param('operations', '', new Text(1024), 'Variables to use in the operation', true) + ->param('map', '', new Text(1024), 'Variables to use in the operation', true) ->inject('request') ->inject('response') ->inject('promiseAdapter') ->inject('gqlSchema') ->action(Closure::fromCallable('graphqlRequest')); -App::post('/v1/graphql') - ->desc('GraphQL Endpoint') - ->groups(['grapgql']) - ->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', 'upload') - ->label('sdk.description', '/docs/references/graphql/upload.md') - ->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) - ->param('operations', '', new Text(1024), 'Query and variables for operation', true) - ->param('map', '', new Text(1024), 'Map of form data keys to file replacement dot paths within the operations JSON. For example: "variables.code"', true) - ->inject('request') - ->inject('response') - ->inject('promiseAdapter') - ->inject('gqlSchema') - ->action(Closure::fromCallable('graphqlUpload')); - -/** - * @throws Exception - */ -function graphqlUpload( - ?string $operations, - ?string $map, - Appwrite\Utopia\Request $request, - Appwrite\Utopia\Response $response, - CoroutinePromiseAdapter $promiseAdapter, - Type\Schema $gqlSchema -): void { - $contentType = $request->getHeader('content-type'); - if (!\str_starts_with($contentType, 'multipart/form-data')) { - throw new Exception('Invalid content type', 400); - } - $map = \json_decode($map, true); - $operations = \json_decode($operations, true); - foreach ($map as $fileKey => $locations) { - foreach ($locations as $location) { - $items = &$operations; - foreach (explode('.', $location) as $key) { - if (!isset($items[$key]) || !is_array($items[$key])) { - $items[$key] = []; - } - $items = &$items[$key]; - } - $items = $request->getFiles($fileKey); - } - } - $query = $operations['query']; - $variables = $operations['variables']; - - graphqlRequest($query, null, $variables, $request, $response, $promiseAdapter, $gqlSchema); -} - /** * @throws Exception * @throws \Exception @@ -125,15 +71,38 @@ function graphqlRequest( string $query, ?string $operationName, ?array $variables, + ?string $operations, + ?string $map, Appwrite\Utopia\Request $request, Appwrite\Utopia\Response $response, CoroutinePromiseAdapter $promiseAdapter, Type\Schema $gqlSchema ): void { $contentType = $request->getHeader('content-type'); + if ($contentType === 'application/graphql') { $query = $request->getSwoole()->rawContent(); } + + if (\str_starts_with($contentType, 'multipart/form-data')) { + $map = \json_decode($map, true); + $operations = \json_decode($operations, true); + foreach ($map as $fileKey => $locations) { + foreach ($locations as $location) { + $items = &$operations; + foreach (explode('.', $location) as $key) { + if (!isset($items[$key]) || !is_array($items[$key])) { + $items[$key] = []; + } + $items = &$items[$key]; + } + $items = $request->getFiles($fileKey); + } + } + $query = $operations['query']; + $variables = $operations['variables']; + } + if (empty($query)) { throw new Exception('No query supplied.', 400, Exception::GRAPHQL_NO_QUERY); } diff --git a/src/Appwrite/GraphQL/Builder.php b/src/Appwrite/GraphQL/Builder.php index 61bad0ce21..f4c38d7f4e 100644 --- a/src/Appwrite/GraphQL/Builder.php +++ b/src/Appwrite/GraphQL/Builder.php @@ -16,7 +16,6 @@ use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; -use Utopia\Registry\Registry; use Utopia\Route; use Utopia\Validator; @@ -497,7 +496,7 @@ class Builder foreach ($collections as $collectionId => $attributes) { $objectType = new ObjectType([ 'name' => $collectionId, - 'fields' => $attributes + 'fields' => \array_merge(["_id" => ['type' => Type::string()]], $attributes), ]); $attributes = \array_merge( @@ -695,7 +694,6 @@ class Builder } $gqlResponse = $response; - $request = new Request($swoole); $apiResponse = new Response($response->getSwoole()); $apiResponse->setContentType(Response::CONTENT_TYPE_NULL); @@ -710,31 +708,42 @@ class Builder $utopia->execute($route, $request); } catch (\Throwable $e) { - $gqlResponse->setStatusCode($apiResponse->getStatusCode()); + self::reassign($gqlResponse, $apiResponse); $reject($e); return; } + self::reassign($gqlResponse, $apiResponse); + $result = $apiResponse->getPayload(); - $gqlResponse->setContentType($apiResponse->getContentType()); - $gqlResponse->setStatusCode($apiResponse->getStatusCode()); + if ($result['$id']) { + $result['_id'] = $result['$id']; + } if ($apiResponse->getStatusCode() < 200 || $apiResponse->getStatusCode() >= 400) { $reject(new GQLException($result['message'], $apiResponse->getStatusCode())); return; } - // Add headers and cookies from inner to outer response - // TODO: Add setters to response to allow setting entire array at once + $resolve($result); + } + + /** + * @param Response $gqlResponse + * @param Response $apiResponse + * @return void + * @throws \Utopia\Exception + */ + private static function reassign(Response $gqlResponse, Response $apiResponse): void + { + $gqlResponse->setContentType($apiResponse->getContentType()); + $gqlResponse->setStatusCode($apiResponse->getStatusCode()); foreach ($apiResponse->getHeaders() as $key => $value) { $gqlResponse->addHeader($key, $value); } foreach ($apiResponse->getCookies() as $name => $cookie) { $gqlResponse->addCookie($name, $cookie['value'], $cookie['expire'], $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly']); } - - - $resolve($result); } } diff --git a/tests/e2e/Services/GraphQL/GraphQLBase.php b/tests/e2e/Services/GraphQL/GraphQLBase.php index 3ff58a10a0..36fcea0454 100644 --- a/tests/e2e/Services/GraphQL/GraphQLBase.php +++ b/tests/e2e/Services/GraphQL/GraphQLBase.php @@ -35,12 +35,18 @@ trait GraphQLBase public static string $DELETE_INDEX = 'delete_index'; // Documents public static string $CREATE_DOCUMENT = 'create_document_rest'; - public static string $CREATE_CUSTOM_ENTITY = 'create_document_hooks'; public static string $GET_DOCUMENTS = 'list_documents'; public static string $GET_DOCUMENT = 'get_document'; public static string $UPDATE_DOCUMENT = 'update_document'; public static string $DELETE_DOCUMENT = 'delete_document'; + // Custom Entities + public static string $CREATE_CUSTOM_ENTITY = 'create_custom_entity'; + public static string $GET_CUSTOM_ENTITIES = 'get_custom_entities'; + public static string $GET_CUSTOM_ENTITY = 'get_custom_entity'; + public static string $UPDATE_CUSTOM_ENTITY = 'update_custom_entity'; + public static string $DELETE_CUSTOM_ENTITY = 'delete_custom_entity'; + // Localization public static string $GET_LOCALE = 'get_locale'; public static string $LIST_COUNTRIES = 'list_countries'; @@ -414,6 +420,7 @@ trait GraphQLBase case self::$CREATE_CUSTOM_ENTITY: return 'mutation createActor($name: String!, $age: Int!, $alive: Boolean!, $salary: Float, $email: String!, $role: String!, $ip: String, $url: String){ actorsCreate(name: $name, age: $age, alive: $alive, salary: $salary, email: $email, role: $role, ip: $ip, url: $url) { + _id name age alive @@ -422,6 +429,52 @@ trait GraphQLBase role } }'; + case self::$GET_CUSTOM_ENTITIES: + return 'query getCustomEntities($name: String!){ + actorsList(name: $name) { + total + actors { + name + age + alive + salary + email + role + ip + url + } + } + }'; + case self::$GET_CUSTOM_ENTITY: + return 'query getCustomEntity($id: String!){ + actorsGet(id: $id) { + name + age + alive + salary + email + role + ip + url + } + }'; + case self::$UPDATE_CUSTOM_ENTITY: + return 'mutation updateCustomEntity($id: String!, $name: String!, $age: Int!, $alive: Boolean!, $salary: Float, $email: String!, $role: String!, $ip: String, $url: String){ + actorsUpdate(id: $id, name: $name, age: $age, alive: $alive, salary: $salary, email: $email, role: $role, ip: $ip, url: $url) { + name + age + alive + salary + email + role + ip + url + } + }'; + case self::$DELETE_CUSTOM_ENTITY: + return 'mutation deleteCustomEntity($id: String!){ + actorsDelete(id: $id) + }'; case self::$UPDATE_DOCUMENT: return 'mutation updateDocument($databaseId: String!, $collectionId: String!, $documentId: String!, $data: Json!, $read: [String!], $write: [String!]){ databasesUpdateDocument(databaseId: $databaseId, collectionId: $collectionId, documentId: $documentId, data: $data, read: $read, write: $write) { diff --git a/tests/e2e/Services/GraphQL/GraphQLDatabaseServerTest.php b/tests/e2e/Services/GraphQL/GraphQLDatabaseServerTest.php index 62e98e262b..615c0e46c9 100644 --- a/tests/e2e/Services/GraphQL/GraphQLDatabaseServerTest.php +++ b/tests/e2e/Services/GraphQL/GraphQLDatabaseServerTest.php @@ -426,7 +426,7 @@ class GraphQLDatabaseServerTest extends Scope * @depends testCreateEnumAttribute * @throws Exception */ - public function testCreateCustomEntity(): void + public function testCreateCustomEntity(): array { $projectId = $this->getProject()['$id']; $query = $this->getQuery(self::$CREATE_CUSTOM_ENTITY); @@ -447,9 +447,13 @@ class GraphQLDatabaseServerTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $gqlPayload); + $this->assertArrayNotHasKey('errors', $actor['body']); $this->assertIsArray($actor['body']['data']); - $this->assertIsArray($actor['body']['data']['actorsCreate']); + $actor = $actor['body']['data']['actorsCreate']; + $this->assertIsArray($actor); + + return $actor; } public function testGetDatabases(): void @@ -706,30 +710,30 @@ class GraphQLDatabaseServerTest extends Scope $this->assertIsArray($document['body']['data']['databasesGetDocument']); } -// /** -// * @depends testCreateCustomEntity -// * @throws Exception -// */ -// public function testGetCustomEntity($data) -// { -// $projectId = $this->getProject()['$id']; -// $query = $this->getQuery(self::$GET_CUSTOM_ENTITY); -// $gqlPayload = [ -// 'query' => $query, -// 'variables' => [ -// 'id' => $data['entity']['_id'], -// ] -// ]; -// -// $entity = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([ -// 'content-type' => 'application/json', -// 'x-appwrite-project' => $projectId, -// ], $this->getHeaders()), $gqlPayload); -// -// $this->assertArrayNotHasKey('errors', $entity['body']); -// $this->assertIsArray($entity['body']['data']); -// $this->assertIsArray($entity['body']['data']['actorGet']); -// } + /** + * @depends testCreateCustomEntity + * @throws Exception + */ + public function testGetCustomEntity($data) + { + $projectId = $this->getProject()['$id']; + $query = $this->getQuery(self::$GET_CUSTOM_ENTITY); + $gqlPayload = [ + 'query' => $query, + 'variables' => [ + 'id' => $data['_id'], + ] + ]; + + $entity = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $gqlPayload); + + $this->assertArrayNotHasKey('errors', $entity['body']); + $this->assertIsArray($entity['body']['data']); + $this->assertIsArray($entity['body']['data']['actorsGet']); + } /** * @depends testCreateDatabase