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);