From ddd15921a3795869d0e0528ae53f0d6dfd070580 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Thu, 8 Sep 2022 23:55:29 +0000 Subject: [PATCH] Update V15 request filters Request filters are in place for the following services: * account * databases * functions * projects --- src/Appwrite/Utopia/Request/Filters/V15.php | 244 ++++++-- tests/unit/Utopia/Request/Filters/V15Test.php | 570 ++++++++++++++++++ 2 files changed, 770 insertions(+), 44 deletions(-) create mode 100644 tests/unit/Utopia/Request/Filters/V15Test.php diff --git a/src/Appwrite/Utopia/Request/Filters/V15.php b/src/Appwrite/Utopia/Request/Filters/V15.php index 917038628..b6b2497fb 100644 --- a/src/Appwrite/Utopia/Request/Filters/V15.php +++ b/src/Appwrite/Utopia/Request/Filters/V15.php @@ -3,7 +3,7 @@ namespace Appwrite\Utopia\Request\Filters; use Appwrite\Utopia\Request\Filter; -use Utopia\Database\Query; +use Utopia\Database\Database; class V15 extends Filter { @@ -11,27 +11,64 @@ class V15 extends Filter public function parse(array $content, string $model): array { switch ($model) { - // Old Query -> New Query - case "account.logs": - $content = $this->handleAccountLogs($content); + case 'account.logs': + case 'databases.listLogs': + case 'databases.listCollectionLogs': + case 'databases.listDocumentLogs': + $content = $this->convertLimitAndOffset($content); + break; + case 'account.initials': + unset($content['color']); + break; + case 'databases.list': + case 'databases.listCollections': + case 'functions.list': + case 'functions.listDeployments': + case 'projects.list': + $content = $this->convertLimitAndOffset($content); + $content = $this->convertCursor($content); + $content = $this->convertOrderType($content); + break; + case 'databases.createCollection': + case 'databases.updateCollection': + $content = $this->convertCollectionPermission($content); + $content = $this->convertReadWrite($content); + break; + case 'databases.createDocument': + case 'databases.updateDocument': + $content = $this->convertReadWrite($content); + break; + case 'databases.listDocuments': + $content = $this->convertFilters($content); + $content = $this->convertLimitAndOffset($content); + $content = $this->convertCursor($content); + $content = $this->convertOrders($content); + break; + case 'functions.create': + case 'functions.update': + $content = $this->convertExecute($content); + break; + case 'functions.listExecutions': + $content = $this->convertLimitAndOffset($content); + $content = $this->convertCursor($content); + break; + case 'projects.createKey': + case 'projects.updateKey': + $content = $this->convertExpire($content); break; - case "account.initials": - $content = $this->handleInitials($content); } return $content; } - protected function handleAccountLogs($content) + protected function convertLimitAndOffset($content) { - // Translate Old Query System to New Query System + if (isset($content['limit'])) { + $content['queries'][] = 'limit(' . $content['limit'] . ')'; + } - if (!empty($content['limit'])) { - $content['queries'][] = 'Query.limit('.$content['limit'].')'; - } - - if (!empty($content['offset'])) { - $content['queries'][] = 'Query.offset('.$content['offset'].')'; + if (isset($content['offset'])) { + $content['queries'][] = 'offset(' . $content['offset'] . ')'; } unset($content['limit']); @@ -40,52 +77,171 @@ class V15 extends Filter return $content; } - protected function handleInitials($content) + protected function convertCursor($content) { - unset($content[' color']); + if (isset($content['cursor'])) { + $cursorDirection = $content['cursorDirection'] ?? Database::CURSOR_AFTER; + + if ($cursorDirection === Database::CURSOR_BEFORE) { + $content['queries'][] = 'cursorBefore("' . $content["cursor"] . '")'; + } else { + $content['queries'][] = 'cursorAfter("' . $content["cursor"] . '")'; + } + } + + unset($content['cursor']); + unset($content['cursorDirection']); return $content; } - protected function handleQueryTranslation($content) { - $content['queries'] = []; - - if (isset($content['limit'])) { - $content['queries'][] = Query::limit($content['limit']); - } - - if (isset($content['offset'])) { - $content['queries'][] = Query::offset($content['offset']); - } - - if (isset($content['cursor'])) { - $direction = $content['cursorDirection'] ?? 'after'; - - if ($direction === 'after') { - $content['queries'][] = Query::cursorAfter($content['cursor']); + protected function convertOrderType($content) + { + if (isset($content['orderType'])) { + if ($content['orderType'] === Database::ORDER_DESC) { + $content['queries'][] = 'orderDesc("")'; } else { - $content['queries'][] = Query::cursorBefore($content['cursor']); + $content['queries'][] = 'orderAsc("")'; } } + unset($content['orderType']); - if (isset($content['orderAttributes'])) { - foreach ($content['orderAttributes'] as $i=>$attribute) { - if ($content['orderTypes'][$i] === 'ASC') { - $content['queries'][] = Query::orderAsc($attribute); - } else if ($content['orderTypes'][$i] === 'DESC') { - $content['queries'][] = Query::orderDesc($attribute); + return $content; + } + + protected function convertOrders($content) + { + if (isset($content['orderTypes'])) { + foreach ($content['orderTypes'] as $i => $type) { + $attribute = $content['orderAttributes'][$i] ?? ''; + + if ($type === Database::ORDER_DESC) { + $content['queries'][] = 'orderDesc("' . $attribute . '")'; } else { - continue; + $content['queries'][] = 'orderAsc("' . $attribute . '")'; } } } - - unset($content['limit']); - unset($content['offset']); - unset($content['cursor']); + unset($content['orderAttributes']); unset($content['orderTypes']); return $content; } + + protected function convertCollectionPermission($content) + { + if (isset($content['permission'])) { + $content['documentSecurity'] = $content['permission'] === 'document'; + } + + unset($content['permission']); + + return $content; + } + + protected function convertReadWrite($content) + { + if (isset($content['read'])) { + foreach ($content['read'] as $read) { + if ($read === 'role:all') { + $content['permissions'][] = 'read("any")'; + } elseif ($read === 'role:guest') { + $content['permissions'][] = 'read("guests")'; + } elseif ($read === 'role:member') { + $content['permissions'][] = 'read("users")'; + } elseif (str_contains($read, ':')) { + $content['permissions'][] = 'read("' . $read . '")'; + } + } + } + + if (isset($content['write'])) { + foreach ($content['write'] as $write) { + if ($write === 'role:all' || $write === 'role:member') { + $content['permissions'][] = 'write("users")'; + } elseif ($write === 'role:guest') { + // don't add because, historically, + // role:guest for write did nothing + } elseif (str_contains($write, ':')) { + $content['permissions'][] = 'write("' . $write . '")'; + } + } + } + + unset($content['read']); + unset($content['write']); + + return $content; + } + + protected function convertFilters($content) + { + if (!isset($content['queries'])) { + return $content; + } + + $operations = [ + 'equal' => 'equal', + 'notEqual' => 'notEqual', + 'lesser' => 'lessThan', + 'lesserEqual' => 'lessThanEqual', + 'greater' => 'greaterThan', + 'greaterEqual' => 'greaterThanEqual', + 'search' => 'search', + ]; + foreach ($content['queries'] as $i => $query) { + foreach ($operations as $oldOperation => $newOperation) { + $middle = ".$oldOperation("; + if (str_contains($query, $middle)) { + $parts = explode($middle, $query); + if (count($parts) > 1) { + $attribute = $parts[0]; + $value = rtrim($parts[1], ")"); + $content['queries'][$i] = $newOperation . '("' . $attribute . '", [' . $value . '])'; + } + } + } + } + return $content; + } + + protected function convertExecute($content) + { + if (!isset($content['execute'])) { + return $content; + } + + $execute = []; + foreach ($content['execute'] as $role) { + if ($role === 'role:all' || $role === 'role:member') { + $execute[] = 'users'; + } elseif ($role === 'role:guest') { + // don't add because, historically, + // role:guest for write did nothing + } elseif (str_contains($role, ':')) { + $execute[] = $role; + } + } + $content['execute'] = $execute; + + return $content; + } + + protected function convertExpire($content) + { + if (!isset($content['expire'])) { + return $content; + } + + $expire = (int) $content['expire']; + + if ($expire === 0) { + $content['expire'] = null; + } else { + $content['expire'] = date(\DateTime::RFC3339_EXTENDED, $expire); + } + + return $content; + } } diff --git a/tests/unit/Utopia/Request/Filters/V15Test.php b/tests/unit/Utopia/Request/Filters/V15Test.php new file mode 100644 index 000000000..1c8552d73 --- /dev/null +++ b/tests/unit/Utopia/Request/Filters/V15Test.php @@ -0,0 +1,570 @@ +filter = new V15(); + } + + public function tearDown(): void + { + } + + public function limitOffsetProvider(): array + { + return [ + 'basic test' => [ + ['limit' => '12', 'offset' => '0'], + ['queries' => ['limit(12)', 'offset(0)']] + ], + ]; + } + + /** + * @dataProvider limitOffsetProvider + */ + public function testGetAccountLogs(array $content, array $expected): void + { + $model = 'account.logs'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public function testGetAccountInitials(): void + { + $model = 'account.initials'; + + $content = ['color' => 'deadbeef']; + $expected = []; + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public function limitOffsetCursorOrderTypeProvider(): array + { + return [ + 'basic test' => [ + [ + 'limit' => '12', + 'offset' => '0', + 'cursor' => 'abcd', + 'cursorDirection' => 'before', + 'orderType' => 'asc', + ], + [ + 'queries' => [ + 'limit(12)', + 'offset(0)', + 'cursorBefore("abcd")', + 'orderAsc("")' + ] + ], + ], + ]; + } + + public function cursorProvider(): array + { + return [ + 'cursorDirection after' => [ + [ + 'cursor' => 'abcd', + 'cursorDirection' => 'after', + ], + [ + 'queries' => [ + 'cursorAfter("abcd")', + ] + ], + ], + 'cursorDirection invalid' => [ + [ + 'cursor' => 'abcd', + 'cursorDirection' => 'invalid', + ], + [ + 'queries' => [ + 'cursorAfter("abcd")', + ] + ], + ], + ]; + } + + public function orderTypeProvider(): array + { + return [ + 'orderType desc' => [ + [ + 'orderType' => 'DESC', + ], + [ + 'queries' => [ + 'orderDesc("")', + ] + ], + ], + 'orderType invalid' => [ + [ + 'orderType' => 'invalid', + ], + [ + 'queries' => [ + 'orderAsc("")', + ] + ], + ], + ]; + } + + /** + * @dataProvider limitOffsetCursorOrderTypeProvider + * @dataProvider limitOffsetProvider + * @dataProvider cursorProvider + * @dataProvider orderTypeProvider + */ + public function testListDatabases(array $content, array $expected): void + { + $model = 'databases.list'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider limitOffsetProvider + */ + public function testListDatabaseLogs(array $content, array $expected): void + { + $model = 'databases.listLogs'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public function permissionProvider(): array + { + return [ + 'permission collection' => [ + ['permission' => 'collection'], + ['documentSecurity' => false], + ], + 'permission document' => [ + ['permission' => 'document'], + ['documentSecurity' => true], + ], + 'permission empty' => [ + [], + [], + ], + 'permission invalid' => [ + ['permission' => 'invalid'], + ['documentSecurity' => false], + ], + ]; + } + + public function readWriteProvider(): array + { + return [ + 'read all types' => [ + [ + 'read' => [ + 'role:all', + 'role:guest', + 'role:member', + 'user:a', + 'team:b', + 'team:c/member', + 'member:z', + ], + ], + [ + 'permissions' => [ + 'read("any")', + 'read("guests")', + 'read("users")', + 'read("user:a")', + 'read("team:b")', + 'read("team:c/member")', + 'read("member:z")', + ], + ], + ], + 'read invalid' => [ + ['read' => ['invalid', 'invalid:a']], + ['permissions' => ['read("invalid:a")']], + ], + 'write all types' => [ + [ + 'write' => [ + 'role:all', + 'role:guest', + 'role:member', + 'user:a', + 'team:b', + 'team:c/member', + 'member:z', + ], + ], + [ + 'permissions' => [ + 'write("users")', + 'write("users")', + 'write("user:a")', + 'write("team:b")', + 'write("team:c/member")', + 'write("member:z")', + ], + ], + ], + 'write invalid' => [ + ['write' => ['invalid', 'invalid:a']], + ['permissions' => ['write("invalid:a")']], + ] + ]; + } + + /** + * @dataProvider permissionProvider + * @dataProvider readWriteProvider + */ + public function testCreateCollection(array $content, array $expected): void + { + $model = 'databases.createCollection'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider limitOffsetCursorOrderTypeProvider + * @dataProvider limitOffsetProvider + * @dataProvider cursorProvider + * @dataProvider orderTypeProvider + */ + public function testListCollections(array $content, array $expected): void + { + $model = 'databases.listCollections'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider limitOffsetProvider + */ + public function testListCollectionLogs(array $content, array $expected): void + { + $model = 'databases.listCollectionLogs'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider permissionProvider + * @dataProvider readWriteProvider + */ + public function testUpdateCollection(array $content, array $expected): void + { + $model = 'databases.updateCollection'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider readWriteProvider + */ + public function testCreateDocument(array $content, array $expected): void + { + $model = 'databases.createDocument'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public function ordersProvider(): array + { + return [ + 'basic test' => [ + [ + 'orderAttributes' => ['lastName', 'firstName'], + 'orderTypes' => ['DESC', 'ASC'], + ], + [ + 'queries' => [ + 'orderDesc("lastName")', + 'orderAsc("firstName")', + ] + ], + ], + 'orderType only' => [ + [ + 'orderTypes' => ['DESC'], + ], + [ + 'queries' => [ + 'orderDesc("")', + ] + ], + ], + 'orderType invalid' => [ + [ + 'orderAttributes' => ['lastName'], + 'orderTypes' => ['invalid'], + ], + [ + 'queries' => [ + 'orderAsc("lastName")', + ] + ], + ], + ]; + } + + public function filtersProvider(): array + { + return [ + 'all filters' => [ + [ + 'queries' => [ + 'lastName.equal("Smith", "Jackson")', + 'firstName.notEqual("John")', + 'age.lesser(50)', + 'age.lesserEqual(51)', + 'age.greater(20)', + 'age.greaterEqual(21)', + 'address.search("pla")', + ], + ], + [ + 'queries' => [ + 'equal("lastName", ["Smith", "Jackson"])', + 'notEqual("firstName", ["John"])', + 'lessThan("age", [50])', + 'lessThanEqual("age", [51])', + 'greaterThan("age", [20])', + 'greaterThanEqual("age", [21])', + 'search("address", ["pla"])', + ] + ], + ], + ]; + } + + /** + * @dataProvider limitOffsetProvider + * @dataProvider cursorProvider + * @dataProvider ordersProvider + * @dataProvider filtersProvider + */ + public function testListDocuments(array $content, array $expected): void + { + $model = 'databases.listDocuments'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result, 'fail'); + } + + /** + * @dataProvider limitOffsetProvider + */ + public function testListDocumentLogs(array $content, array $expected): void + { + $model = 'databases.listDocumentLogs'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider readWriteProvider + */ + public function testUpdateDocument(array $content, array $expected): void + { + $model = 'databases.updateDocument'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public function executeProvider() : array { + return [ + 'all roles' => [ + [ + 'execute' => [ + 'role:all', + 'role:guest', + 'role:member', + 'user:a', + 'team:b', + 'team:c/member', + 'member:z', + ], + ], + [ + 'execute' => [ + 'users', + 'users', + 'user:a', + 'team:b', + 'team:c/member', + 'member:z', + ] + ], + ], + ]; + } + + /** + * @dataProvider executeProvider + */ + public function testCreateFunction(array $content, array $expected): void + { + $model = 'functions.create'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider limitOffsetCursorOrderTypeProvider + * @dataProvider limitOffsetProvider + * @dataProvider cursorProvider + * @dataProvider orderTypeProvider + */ + public function testListFunctions(array $content, array $expected): void + { + $model = 'functions.list'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider executeProvider + */ + public function testUpdateFunction(array $content, array $expected): void + { + $model = 'functions.update'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider limitOffsetCursorOrderTypeProvider + * @dataProvider limitOffsetProvider + * @dataProvider cursorProvider + * @dataProvider orderTypeProvider + */ + public function testListDeployments(array $content, array $expected): void + { + $model = 'functions.listDeployments'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider limitOffsetProvider + * @dataProvider cursorProvider + */ + public function testListExecutions(array $content, array $expected): void + { + $model = 'functions.listExecutions'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider limitOffsetCursorOrderTypeProvider + * @dataProvider limitOffsetProvider + * @dataProvider cursorProvider + * @dataProvider orderTypeProvider + */ + public function testListProjects(array $content, array $expected): void + { + $model = 'projects.list'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public function expireProvider() : array + { + return [ + 'empty' => [ + [], + [], + ], + 'zero' => [ + ['expire' => '0'], + ['expire' => null], + ], + 'value' => [ + ['expire' => '1602743880'], + ['expire' => Model::TYPE_DATETIME_EXAMPLE], + ], + ]; + } + + /** + * @dataProvider expireProvider + */ + public function testCreateKey(array $content, array $expected) + { + $model = 'projects.createKey'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider expireProvider + */ + public function testUpdateKey(array $content, array $expected) + { + $model = 'projects.updateKey'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } +}