From f0d66985f7765c1a9c6cdb04be7be600d42e56cd Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 15 Aug 2022 19:35:50 +0000 Subject: [PATCH] Consolidate List Users API params Replace search, limit, offset, cursor, cursorDirection, and orderType params with a single queries param since the Queries V2 change now allows for different types of queries. --- app/controllers/api/users.php | 36 ++-- .../Database/Validator/Queries/Collection.php | 58 ++++++ .../Database/Validator/Queries/Users.php | 28 +++ .../Account/AccountCustomClientTest.php | 9 +- tests/e2e/Services/Users/UsersBase.php | 171 ++++++++++++++++-- 5 files changed, 263 insertions(+), 39 deletions(-) create mode 100644 src/Appwrite/Utopia/Database/Validator/Queries/Collection.php create mode 100644 src/Appwrite/Utopia/Database/Validator/Queries/Users.php diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index bbd0146c8b..71e6feb224 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -10,6 +10,7 @@ use Appwrite\Event\Audit as EventAudit; use Appwrite\Network\Validator\Email; use Appwrite\Stats\Stats; use Appwrite\Utopia\Database\Validator\CustomId; +use Appwrite\Utopia\Database\Validator\Queries\Users; use Appwrite\Utopia\Response; use Utopia\App; use Utopia\Audit\Audit; @@ -107,43 +108,44 @@ App::get('/v1/users') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USER_LIST) + ->param('queries', [], new Users(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Users::ALLOWED_ATTRIBUTES), true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) - ->param('limit', 25, new Range(0, 100), 'Maximum number of users to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) - ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true) - ->param('cursor', '', new UID(), 'ID of the user used as the starting point for the query, excluding the user itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true) - ->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true) - ->param('orderType', Database::ORDER_ASC, new WhiteList([Database::ORDER_ASC, Database::ORDER_DESC], true), 'Order result by ASC or DESC order.', true) ->inject('response') ->inject('dbForProject') ->inject('usage') - ->action(function (string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject, Stats $usage) { + ->action(function (array $queries, string $search, Response $response, Database $dbForProject, Stats $usage) { - $filterQueries = []; + $queries = Query::parseQueries($queries); if (!empty($search)) { - $filterQueries[] = Query::search('search', $search); + $queries[] = Query::search('search', $search); } - $queries = []; - $queries[] = Query::limit($limit); - $queries[] = Query::offset($offset); - $queries[] = $orderType === Database::ORDER_ASC ? Query::orderAsc('') : Query::orderDesc(''); - if (!empty($cursor)) { - $cursorDocument = $dbForProject->getDocument('users', $cursor); + // Set default limit + $queries[] = Query::limit(25); + + // Get cursor document if there was a cursor query + $cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)[0] ?? null; + if ($cursor !== null) { + /** @var Query $cursor */ + $userId = $cursor->getValue(); + $cursorDocument = $dbForProject->getDocument('users', $userId); if ($cursorDocument->isEmpty()) { - throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "User '{$cursor}' for the 'cursor' value not found."); + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "User '{$userId}' for the 'cursor' value not found."); } - $queries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument); + $cursor->setValue($cursorDocument); } + $filterQueries = Query::groupByType($queries)['filters']; + $usage ->setParam('users.read', 1) ; $response->dynamic(new Document([ - 'users' => $dbForProject->find('users', \array_merge($filterQueries, $queries)), + 'users' => $dbForProject->find('users', $queries), 'total' => $dbForProject->count('users', $filterQueries, APP_LIMIT_COUNT), ]), Response::MODEL_USER_LIST); }); diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Collection.php b/src/Appwrite/Utopia/Database/Validator/Queries/Collection.php new file mode 100644 index 0000000000..0404f299af --- /dev/null +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Collection.php @@ -0,0 +1,58 @@ + $key, + 'type' => $attribute['type'], + 'array' => $attribute['array'], + ]); + } + + $indexes = []; + foreach ($allowedAttributes as $attribute) { + $indexes[] = new Document([ + 'status' => 'available', + 'type' => Database::INDEX_KEY, + 'attributes' => [$attribute] + ]); + } + $indexes[] = new Document([ + 'status' => 'available', + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['search'] + ]); + + parent::__construct(new QueryValidator($attributes), $attributes, $indexes, true); + } +} diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Users.php b/src/Appwrite/Utopia/Database/Validator/Queries/Users.php new file mode 100644 index 0000000000..fcef3f9928 --- /dev/null +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Users.php @@ -0,0 +1,28 @@ + $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ], [ - 'search' => $newName + 'search' => $newName, ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -628,7 +628,7 @@ class AccountCustomClientTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ], [ - 'search' => $id + 'search' => $id, ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -654,7 +654,8 @@ class AccountCustomClientTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ], [ - 'search' => '"' . $email . '"' + 'search' => '"' . $email . '"', + ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -668,7 +669,7 @@ class AccountCustomClientTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ], [ - 'search' => $id + 'search' => $id, ]); $this->assertEquals($response['headers']['status-code'], 200); diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php index 8cad10f37c..1d3f7412a0 100644 --- a/tests/e2e/Services/Users/UsersBase.php +++ b/tests/e2e/Services/Users/UsersBase.php @@ -80,11 +80,145 @@ trait UsersBase $this->assertEquals($response['body']['users'][0]['$id'], $data['userId']); $this->assertEquals($response['body']['users'][1]['$id'], 'user1'); + $user1 = $response['body']['users'][1]; + $response = $this->client->call(Client::METHOD_GET, '/users', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'cursor' => $response['body']['users'][0]['$id'] + 'queries' => ['equal("name", "' . $user1['name'] . '")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['users']); + $this->assertCount(1, $response['body']['users']); + $this->assertEquals($response['body']['users'][0]['name'], $user1['name']); + + $response = $this->client->call(Client::METHOD_GET, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("email", "' . $user1['email'] . '")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['users']); + $this->assertCount(1, $response['body']['users']); + $this->assertEquals($response['body']['users'][0]['email'], $user1['email']); + + $response = $this->client->call(Client::METHOD_GET, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("status", true)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['users']); + $this->assertCount(2, $response['body']['users']); + $this->assertEquals($response['body']['users'][0]['$id'], $data['userId']); + $this->assertEquals($response['body']['users'][0]['status'], $user1['status']); + $this->assertEquals($response['body']['users'][1]['$id'], $user1['$id']); + $this->assertEquals($response['body']['users'][1]['status'], $user1['status']); + + $response = $this->client->call(Client::METHOD_GET, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("status", false)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']); + $this->assertEmpty($response['body']['users']); + $this->assertCount(0, $response['body']['users']); + + $response = $this->client->call(Client::METHOD_GET, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("passwordUpdate", "' . $user1['passwordUpdate'] . '")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['users']); + $this->assertCount(1, $response['body']['users']); + $this->assertEquals($response['body']['users'][0]['passwordUpdate'], $user1['passwordUpdate']); + + $response = $this->client->call(Client::METHOD_GET, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("registration", "' . $user1['registration'] . '")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['users']); + $this->assertCount(1, $response['body']['users']); + $this->assertEquals($response['body']['users'][0]['registration'], $user1['registration']); + + $response = $this->client->call(Client::METHOD_GET, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("emailVerification", false)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['users']); + $this->assertCount(2, $response['body']['users']); + $this->assertEquals($response['body']['users'][0]['$id'], $data['userId']); + $this->assertEquals($response['body']['users'][0]['status'], $user1['status']); + $this->assertEquals($response['body']['users'][1]['$id'], $user1['$id']); + $this->assertEquals($response['body']['users'][1]['status'], $user1['status']); + + $response = $this->client->call(Client::METHOD_GET, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("emailVerification", true)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']); + $this->assertEmpty($response['body']['users']); + $this->assertCount(0, $response['body']['users']); + + $response = $this->client->call(Client::METHOD_GET, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("phoneVerification", false)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']); + $this->assertEmpty($response['body']['users']); + $this->assertCount(0, $response['body']['users']); + + $response = $this->client->call(Client::METHOD_GET, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("phoneVerification", true)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']); + $this->assertEmpty($response['body']['users']); + $this->assertCount(0, $response['body']['users']); + + $response = $this->client->call(Client::METHOD_GET, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['cursorAfter("' . $data['userId'] . '")'] ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -97,8 +231,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'cursor' => 'user1', - 'cursorDirection' => Database::CURSOR_BEFORE + 'queries' => ['cursorBefore("user1")'] ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -114,7 +247,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => 'Ronaldo' + 'search' => "Ronaldo", ]); $this->assertEquals($response['headers']['status-code'], 200); $this->assertNotEmpty($response['body']); @@ -126,7 +259,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => 'cristiano.ronaldo@manchester-united.co.uk' + 'search' => "cristiano.ronaldo@manchester-united.co.uk", ]); $this->assertEquals($response['headers']['status-code'], 200); $this->assertNotEmpty($response['body']); @@ -138,7 +271,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => 'cristiano.ronaldo' + 'search' => "cristiano.ronaldo", ]); $this->assertEquals($response['headers']['status-code'], 200); $this->assertNotEmpty($response['body']); @@ -150,7 +283,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => 'manchester' + 'search' => "manchester", ]); $this->assertEquals($response['headers']['status-code'], 200); $this->assertNotEmpty($response['body']); @@ -162,7 +295,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => 'united.co.uk' + 'search' => "united.co.uk", ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -176,7 +309,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => 'man' + 'search' => "man", ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -190,7 +323,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => $data['userId'] + 'search' => $data['userId'], ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -206,7 +339,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'cursor' => 'unknown' + 'queries' => ['cursorAfter("unknown")'] ]); $this->assertEquals(400, $response['headers']['status-code']); @@ -297,7 +430,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => $newName + 'search' => $newName, ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -310,7 +443,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => $id + 'search' => $id, ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -364,7 +497,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => $newEmail + 'search' => $newEmail, ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -377,7 +510,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => $id + 'search' => $id, ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -565,7 +698,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'offset' => 1 + 'queries' => ['offset(1)'] ]); $this->assertEquals($logs['headers']['status-code'], 200); @@ -576,8 +709,10 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'offset' => 1, - 'limit' => 1 + 'queries' => [ + 'offset(1)', + 'limit(1)', + ] ]); $this->assertEquals($logs['headers']['status-code'], 200);