diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 8319f94e8f..045a04c1b1 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -8,8 +8,10 @@ use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\Network\Validator\Email; use Appwrite\Utopia\Database\Validator\CustomId; +use Appwrite\Utopia\Database\Validator\Queries\Attributes; use Appwrite\Utopia\Database\Validator\Queries\Collections; use Appwrite\Utopia\Database\Validator\Queries\Databases; +use Appwrite\Utopia\Database\Validator\Queries\Indexes; use Appwrite\Utopia\Response; use Appwrite\Utopia\Request; use MaxMind\Db\Reader; @@ -1689,9 +1691,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_LIST) ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('queries', [], new Attributes(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). 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(', ', Attributes::ALLOWED_ATTRIBUTES), true) ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject) { + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) { $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); @@ -1705,11 +1708,33 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $attributes = $collection->getAttribute('attributes'); + $queries = Query::parseQueries($queries); + + \array_push($queries, Query::equal('collectionId', [$collectionId]), Query::equal('databaseId', [$databaseId])); + + // Get cursor document if there was a cursor query + $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = reset($cursor); + + if ($cursor) { + $attributeId = $cursor->getValue(); + $cursorDocument = Authorization::skip(fn() => $dbForProject->find('attributes', [ + Query::equal('collectionId', [$collectionId]), + Query::equal('databaseId', [$databaseId]), + Query::equal('key', [$attributeId]), + Query::limit(1), + ])); + if (empty($cursorDocument) || $cursorDocument[0]->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Attribute '{$attributeId}' for the 'cursor' value not found."); + } + $cursor->setValue($cursorDocument[0]); + } + + $filterQueries = Query::groupByType($queries)['filters']; $response->dynamic(new Document([ - 'total' => \count($attributes), - 'attributes' => $attributes + 'total' => $dbForProject->count('attributes', $filterQueries, APP_LIMIT_COUNT), + 'attributes' => $dbForProject->find('attributes', $queries), ]), Response::MODEL_ATTRIBUTE_LIST); }); @@ -2492,26 +2517,50 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ->label('sdk.response.model', Response::MODEL_INDEX_LIST) ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('queries', [], new Indexes(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). 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(', ', Indexes::ALLOWED_ATTRIBUTES), true) ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject) { + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) { $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $indexes = $collection->getAttribute('indexes'); + $queries = Query::parseQueries($queries); + \array_push($queries, Query::equal('collectionId', [$collectionId]), Query::equal('databaseId', [$databaseId])); + // Get cursor document if there was a cursor query + $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = reset($cursor); + + if ($cursor) { + $indexId = $cursor->getValue(); + $cursorDocument = Authorization::skip(fn() => $dbForProject->find('indexes', [ + Query::equal('collectionId', [$collectionId]), + Query::equal('databaseId', [$databaseId]), + Query::equal('key', [$indexId]), + Query::limit(1) + ])); + + if (empty($cursorDocument) || $cursorDocument[0]->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Index '{$indexId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument[0]); + } + + $filterQueries = Query::groupByType($queries)['filters']; $response->dynamic(new Document([ - 'total' => \count($indexes), - 'indexes' => $indexes, + 'total' => $dbForProject->count('indexes', $filterQueries, APP_LIMIT_COUNT), + 'indexes' => $dbForProject->find('indexes', $queries), ]), Response::MODEL_INDEX_LIST); }); diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 1bca7d47dc..2bbac41811 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -256,10 +256,11 @@ App::init() $deletes->setProject($project); $database->setProject($project); + $calculateUsage = fn ($event, Document $document) => $databaseListener($event, $document, $project, $queueForUsage, $dbForProject); + $dbForProject - ->on(Database::EVENT_DOCUMENT_CREATE, fn ($event, $document) => $databaseListener($event, $document, $project, $queueForUsage, $dbForProject)) - ->on(Database::EVENT_DOCUMENT_DELETE, fn ($event, $document) => $databaseListener($event, $document, $project, $queueForUsage, $dbForProject)) - ; + ->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', $calculateUsage) + ->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', $calculateUsage); $useCache = $route->getLabel('cache', false); diff --git a/app/init.php b/app/init.php index 2e59869993..1b74b81bc4 100644 --- a/app/init.php +++ b/app/init.php @@ -947,7 +947,7 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons if (APP_MODE_ADMIN !== $mode) { if ($project->isEmpty()) { - $user = new Document(['$id' => ID::custom(''), '$collection' => 'users']); + $user = new Document([]); } else { $user = $dbForProject->getDocument('users', Auth::$unique); } @@ -959,14 +959,14 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons $user->isEmpty() // Check a document has been found in the DB || !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret, $authDuration) ) { // Validate user has valid login token - $user = new Document(['$id' => ID::custom(''), '$collection' => 'users']); + $user = new Document([]); } if (APP_MODE_ADMIN === $mode) { if ($user->find('teamId', $project->getAttribute('teamId'), 'memberships')) { Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. } else { - $user = new Document(['$id' => ID::custom(''), '$collection' => 'users']); + $user = new Document([]); } } @@ -989,7 +989,7 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons } if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token - $user = new Document(['$id' => ID::custom(''), '$collection' => 'users']); + $user = new Document([]); } } diff --git a/composer.lock b/composer.lock index 3f5903e88d..4a0862679d 100644 --- a/composer.lock +++ b/composer.lock @@ -1516,16 +1516,16 @@ }, { "name": "utopia-php/database", - "version": "0.42.1", + "version": "0.42.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "9ff69a9b9eadc581771798833d423829c9d8cc90" + "reference": "5631f151ce2b454517f11c1914e50a1fa70c7bd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/9ff69a9b9eadc581771798833d423829c9d8cc90", - "reference": "9ff69a9b9eadc581771798833d423829c9d8cc90", + "url": "https://api.github.com/repos/utopia-php/database/zipball/5631f151ce2b454517f11c1914e50a1fa70c7bd1", + "reference": "5631f151ce2b454517f11c1914e50a1fa70c7bd1", "shasum": "" }, "require": { @@ -1566,9 +1566,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.42.1" + "source": "https://github.com/utopia-php/database/tree/0.42.0" }, - "time": "2023-08-14T16:09:09+00:00" + "time": "2023-08-10T23:18:38+00:00" }, { "name": "utopia-php/domains", @@ -2993,16 +2993,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.17.1", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + "reference": "19526a33fb561ef417e822e85f08a00db4059c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17", "shasum": "" }, "require": { @@ -3043,9 +3043,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" }, - "time": "2023-08-13T19:53:39+00:00" + "time": "2023-06-25T14:52:30+00:00" }, { "name": "phar-io/manifest", @@ -3270,16 +3270,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.7.3", + "version": "1.7.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419" + "reference": "b2fe4d22a5426f38e014855322200b97b5362c0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", - "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b2fe4d22a5426f38e014855322200b97b5362c0d", + "reference": "b2fe4d22a5426f38e014855322200b97b5362c0d", "shasum": "" }, "require": { @@ -3322,9 +3322,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.3" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.2" }, - "time": "2023-08-12T11:01:26+00:00" + "time": "2023-05-30T18:13:47+00:00" }, { "name": "phpspec/prophecy", @@ -5278,5 +5278,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 75b99dede2..dbcc1c1328 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,7 +84,7 @@ services: - ./docs:/usr/src/code/docs - ./public:/usr/src/code/public - ./src:/usr/src/code/src - - ./dev:/usr/local/dev + - ./dev:/usr/src/code/dev depends_on: - mariadb - redis diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php index 9d2705baa7..0be60d93dc 100644 --- a/src/Appwrite/GraphQL/Types/Mapper.php +++ b/src/Appwrite/GraphQL/Types/Mapper.php @@ -252,6 +252,8 @@ class Mapper case 'Appwrite\Utopia\Database\Validator\Queries\Base': case 'Appwrite\Utopia\Database\Validator\Queries\Buckets': case 'Appwrite\Utopia\Database\Validator\Queries\Collections': + case 'Appwrite\Utopia\Database\Validator\Queries\Attributes': + case 'Appwrite\Utopia\Database\Validator\Queries\Indexes': case 'Appwrite\Utopia\Database\Validator\Queries\Databases': case 'Appwrite\Utopia\Database\Validator\Queries\Deployments': case 'Utopia\Database\Validator\Queries\Documents': diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php index bf0293d5af..81999c06ad 100644 --- a/src/Appwrite/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -343,6 +343,8 @@ class OpenAPI3 extends Format case 'Utopia\Validator\ArrayList': case 'Appwrite\Utopia\Database\Validator\Queries\Buckets': case 'Appwrite\Utopia\Database\Validator\Queries\Collections': + case 'Appwrite\Utopia\Database\Validator\Queries\Indexes': + case 'Appwrite\Utopia\Database\Validator\Queries\Attributes': case 'Appwrite\Utopia\Database\Validator\Queries\Databases': case 'Appwrite\Utopia\Database\Validator\Queries\Deployments': case 'Utopia\Database\Validator\Queries\Documents': diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index 60974bef85..ac489864eb 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -342,6 +342,8 @@ class Swagger2 extends Format case 'Utopia\Validator\ArrayList': case 'Appwrite\Utopia\Database\Validator\Queries\Buckets': case 'Appwrite\Utopia\Database\Validator\Queries\Collections': + case 'Appwrite\Utopia\Database\Validator\Queries\Indexes': + case 'Appwrite\Utopia\Database\Validator\Queries\Attributes': case 'Appwrite\Utopia\Database\Validator\Queries\Databases': case 'Appwrite\Utopia\Database\Validator\Queries\Deployments': case 'Utopia\Database\Validator\Queries\Documents': diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Attributes.php b/src/Appwrite/Utopia/Database/Validator/Queries/Attributes.php new file mode 100644 index 0000000000..1463316ad1 --- /dev/null +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Attributes.php @@ -0,0 +1,27 @@ +getAttribute('options'); + if (!\is_null($options)) { + $document->setAttribute('relatedCollection', $options['relatedCollection']); + $document->setAttribute('relationType', $options['relationType']); + $document->setAttribute('twoWay', $options['twoWay']); + $document->setAttribute('twoWayKey', $options['twoWayKey']); + $document->setAttribute('side', $options['side']); + $document->setAttribute('onDelete', $options['onDelete']); + } + return $document; + } } diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index ebf1e8ade9..9e8477e570 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -3,9 +3,13 @@ namespace Tests\E2E\Services\Databases; use Tests\E2E\Client; +use Utopia\Database\Database; +use Utopia\Database\Document; +use Utopia\Database\DateTime; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; +use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; trait DatabasesBase @@ -313,6 +317,32 @@ trait DatabasesBase return $data; } + /** + * @depends testCreateAttributes + */ + public function testListAttributes(array $data): void + { + $databaseId = $data['databaseId']; + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/attributes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'queries' => ['equal("type", "string")', 'limit(2)', 'cursorAfter(title)'], + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(2, \count($response['body']['attributes'])); + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/attributes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'queries' => ['select(["key"])'], + ]); + $this->assertEquals(Exception::GENERAL_ARGUMENT_INVALID, $response['body']['type']); + $this->assertEquals(400, $response['headers']['status-code']); + } + /** * @depends testCreateAttributes */ @@ -695,7 +725,6 @@ trait DatabasesBase $this->assertEquals(10, $attributes['body']['total']); $attributes = $attributes['body']['attributes']; - $this->assertIsArray($attributes); $this->assertCount(10, $attributes); @@ -1045,6 +1074,32 @@ trait DatabasesBase return $data; } + /** + * @depends testCreateIndexes + */ + public function testListIndexes(array $data): void + { + $databaseId = $data['databaseId']; + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'queries' => ['equal("type", "key")', 'limit(2)'], + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(2, \count($response['body']['indexes'])); + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'queries' => ['select(["key"])'], + ]); + $this->assertEquals(Exception::GENERAL_ARGUMENT_INVALID, $response['body']['type']); + $this->assertEquals(400, $response['headers']['status-code']); + } + /** * @depends testCreateIndexes */ @@ -1405,7 +1460,7 @@ trait DatabasesBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [ - 'select(["title", "releaseYear"])', + 'select(["title", "releaseYear", "$id"])', ], ]); @@ -4125,10 +4180,9 @@ trait DatabasesBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [ - 'select(["libraries.*"])', + 'select(["libraries.*", "$id"])', ], ]); - $document = $response['body']['documents'][0]; $this->assertEquals(200, $response['headers']['status-code']); $this->assertArrayHasKey('libraries', $document); @@ -4138,7 +4192,7 @@ trait DatabasesBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [ - 'select(["fullName"])' + 'select(["fullName", "$id"])' ], ]);