diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 71e6feb224..a708fe0e45 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -10,7 +10,9 @@ 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; use Appwrite\Utopia\Database\Validator\Queries\Users; +use Appwrite\Utopia\Database\Validator\Queries\LimitOffsetQuery; use Appwrite\Utopia\Response; use Utopia\App; use Utopia\Audit\Audit; @@ -30,7 +32,6 @@ use Utopia\Database\Validator\Authorization; use Utopia\Validator\Assoc; use Utopia\Validator\WhiteList; use Utopia\Validator\Text; -use Utopia\Validator\Range; use Utopia\Validator\Boolean; use MaxMind\Db\Reader; @@ -306,14 +307,13 @@ App::get('/v1/users/:userId/logs') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_LOG_LIST) ->param('userId', '', new UID(), 'User ID.') - ->param('limit', 25, new Range(0, 100), 'Maximum number of logs 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 value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true) + ->param('queries', [], new Queries(new LimitOffsetQuery(), strict: false), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Only supported methods are limit and offset', true) ->inject('response') ->inject('dbForProject') ->inject('locale') ->inject('geodb') ->inject('usage') - ->action(function (string $userId, int $limit, int $offset, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Stats $usage) { + ->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Stats $usage) { $user = $dbForProject->getDocument('users', $userId); @@ -321,6 +321,11 @@ App::get('/v1/users/:userId/logs') throw new Exception(Exception::USER_NOT_FOUND); } + $queries = Query::parseQueries($queries); + $grouped = Query::groupByType($queries); + $limit = $grouped['limit'] ?? 25; + $offset = $grouped['offset'] ?? 0; + $audit = new Audit($dbForProject); $logs = $audit->getLogsByUser($user->getId(), $limit, $offset); diff --git a/composer.json b/composer.json index fee2f93c96..0aa9f510f2 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "utopia-php/cache": "0.6.*", "utopia-php/cli": "0.13.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.22.*", + "utopia-php/database": "0.23.0 as 0.22.0", "utopia-php/locale": "0.4.*", "utopia-php/registry": "0.5.*", "utopia-php/preloader": "0.2.*", @@ -88,6 +88,9 @@ "ext-phpiredis": "*" }, "config": { + "optimize-autoloader": true, + "allow-plugins": false, + "preferred-install": "dist", "platform": { "php": "8.0" } diff --git a/composer.lock b/composer.lock index 2ce4203494..c4b157f5ec 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "175fe4abafd8bde4053b91eea905c328", + "content-hash": "1858c544a5b5ae59bc6ff146a375a683", "packages": [ { "name": "adhocore/jwt", @@ -2052,16 +2052,16 @@ }, { "name": "utopia-php/database", - "version": "0.22.0", + "version": "0.23.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "22c45ae83612e907203b7571cd8e3115ae3ae4c5" + "reference": "e4a23f8e26a3d96f292e549cebe0ff6937ec5d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/22c45ae83612e907203b7571cd8e3115ae3ae4c5", - "reference": "22c45ae83612e907203b7571cd8e3115ae3ae4c5", + "url": "https://api.github.com/repos/utopia-php/database/zipball/e4a23f8e26a3d96f292e549cebe0ff6937ec5d71", + "reference": "e4a23f8e26a3d96f292e549cebe0ff6937ec5d71", "shasum": "" }, "require": { @@ -2110,9 +2110,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.22.0" + "source": "https://github.com/utopia-php/database/tree/0.23.0" }, - "time": "2022-08-17T12:55:37+00:00" + "time": "2022-08-18T19:08:33+00:00" }, { "name": "utopia-php/domains", @@ -5354,6 +5354,12 @@ "version": "9999999-dev", "alias": "0.19.5", "alias_normalized": "0.19.5.0" + }, + { + "package": "utopia-php/database", + "version": "0.23.0.0", + "alias": "0.22.0", + "alias_normalized": "0.22.0.0" } ], "minimum-stability": "stable", diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/LimitOffsetQuery.php b/src/Appwrite/Utopia/Database/Validator/Queries/LimitOffsetQuery.php new file mode 100644 index 0000000000..d22c610542 --- /dev/null +++ b/src/Appwrite/Utopia/Database/Validator/Queries/LimitOffsetQuery.php @@ -0,0 +1,58 @@ +maxLimit = $maxLimit; + $this->maxOffset = $maxOffset; + } + + /** + * Is valid. + * + * Returns true if method is limit or offset and values are within range. + * + * @param Query $value + * + * @return bool + */ + public function isValid($query): bool + { + // Validate method + $method = $query->getMethod(); + switch ($method) { + case Query::TYPE_LIMIT: + $limit = $query->getValue(); + return $this->isValidLimit($limit); + + case Query::TYPE_OFFSET: + $offset = $query->getValue(); + return $this->isValidOffset($offset); + + default: + $this->message = 'Query method invalid: ' . $method; + return false; + } + } +} diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php index 1d3f7412a0..321f4f8e42 100644 --- a/tests/e2e/Services/Users/UsersBase.php +++ b/tests/e2e/Services/Users/UsersBase.php @@ -3,7 +3,6 @@ namespace Tests\E2E\Services\Users; use Tests\E2E\Client; -use Utopia\Database\Database; use Utopia\Database\ID; trait UsersBase @@ -673,26 +672,44 @@ trait UsersBase /** * Test for SUCCESS */ - $logs = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + $i = 0; + do { + $logs = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $i++; + } while ($logs['body']['total'] === 0 && $i < 1000); $this->assertEquals($logs['headers']['status-code'], 200); - $this->assertIsArray($logs['body']['logs']); - $this->assertIsNumeric($logs['body']['total']); + $this->assertCount(1, $logs['body']['logs']); + $this->assertEquals(1, $logs['body']['total']); + $this->assertIsArray($logs['body']['logs'][0]); $logs = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'limit' => 1 + 'queries' => ['limit(1)'] ]); $this->assertEquals($logs['headers']['status-code'], 200); $this->assertIsArray($logs['body']['logs']); - $this->assertLessThanOrEqual(1, count($logs['body']['logs'])); - $this->assertIsNumeric($logs['body']['total']); + $this->assertCount(1, $logs['body']['logs']); + $this->assertEquals(1, $logs['body']['total']); + + $logs = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['limit(0)'] + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertCount(0, $logs['body']['logs']); + $this->assertEquals(1, $logs['body']['total']); $logs = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ 'content-type' => 'application/json', @@ -703,7 +720,8 @@ trait UsersBase $this->assertEquals($logs['headers']['status-code'], 200); $this->assertIsArray($logs['body']['logs']); - $this->assertIsNumeric($logs['body']['total']); + $this->assertCount(0, $logs['body']['logs']); + $this->assertEquals(1, $logs['body']['total']); $logs = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ 'content-type' => 'application/json', @@ -717,8 +735,74 @@ trait UsersBase $this->assertEquals($logs['headers']['status-code'], 200); $this->assertIsArray($logs['body']['logs']); - $this->assertLessThanOrEqual(1, count($logs['body']['logs'])); - $this->assertIsNumeric($logs['body']['total']); + $this->assertCount(0, $logs['body']['logs']); + $this->assertEquals(1, $logs['body']['total']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['limit(-1)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['limit(101)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['offset(-1)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['offset(5001)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("$id", "asdf")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['orderAsc("$id")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['cursorAsc("$id")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); } /** diff --git a/tests/unit/Utopia/Database/Validator/Queries/LimitOffsetQueryTest.php b/tests/unit/Utopia/Database/Validator/Queries/LimitOffsetQueryTest.php new file mode 100644 index 0000000000..d749dc7292 --- /dev/null +++ b/tests/unit/Utopia/Database/Validator/Queries/LimitOffsetQueryTest.php @@ -0,0 +1,63 @@ +validator = new LimitOffsetQuery(); + } + + public function tearDown(): void + { + } + + public function testValue(): void + { + // Test for Success + $this->assertEquals($this->validator->isValid(Query::limit(1)), true, $this->validator->getDescription()); + $this->assertEquals($this->validator->isValid(Query::limit(0)), true, $this->validator->getDescription()); + $this->assertEquals($this->validator->isValid(Query::limit(100)), true, $this->validator->getDescription()); + $this->assertEquals($this->validator->isValid(Query::offset(1)), true, $this->validator->getDescription()); + $this->assertEquals($this->validator->isValid(Query::offset(0)), true, $this->validator->getDescription()); + $this->assertEquals($this->validator->isValid(Query::offset(5000)), true, $this->validator->getDescription()); + + // Test for Failure + $this->assertEquals($this->validator->isValid(Query::limit(-1)), false, $this->validator->getDescription()); + $this->assertEquals($this->validator->isValid(Query::limit(101)), false, $this->validator->getDescription()); + $this->assertEquals($this->validator->isValid(Query::offset(-1)), false, $this->validator->getDescription()); + $this->assertEquals($this->validator->isValid(Query::offset(5001)), false, $this->validator->getDescription()); + } + + public function testValues(): void + { + + $validator = new Queries($this->validator, strict: false); + + // Test for Success + $this->assertEquals($validator->isValid(['limit(1)']), true, $validator->getDescription()); + $this->assertEquals($validator->isValid(['limit(0)']), true, $validator->getDescription()); + $this->assertEquals($validator->isValid(['limit(100)']), true, $validator->getDescription()); + $this->assertEquals($validator->isValid(['offset(1)']), true, $validator->getDescription()); + $this->assertEquals($validator->isValid(['offset(0)']), true, $validator->getDescription()); + $this->assertEquals($validator->isValid(['offset(5000)']), true, $validator->getDescription()); + $this->assertEquals($validator->isValid(['limit(25)', 'offset(25)']), true, $validator->getDescription()); + + // Test for Failure + $this->assertEquals($validator->isValid(['limit(-1)']), false, $validator->getDescription()); + $this->assertEquals($validator->isValid(['limit(101)']), false, $validator->getDescription()); + $this->assertEquals($validator->isValid(['offset(-1)']), false, $validator->getDescription()); + $this->assertEquals($validator->isValid(['offset(5001)']), false, $validator->getDescription()); + } +}