From 3271ef57695f56b2a3e0dfa4d229167dad7eb576 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 4 May 2025 17:16:37 +0530 Subject: [PATCH] update: move Index ops to module. --- app/controllers/api/databases.php | 389 ------------------ .../Modules/Databases/Http/Indexes/Create.php | 217 ++++++++++ .../Modules/Databases/Http/Indexes/Delete.php | 109 +++++ .../Modules/Databases/Http/Indexes/Get.php | 80 ++++ .../Modules/Databases/Http/Indexes/XList.php | 129 ++++++ 5 files changed, 535 insertions(+), 389 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Indexes/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Indexes/Delete.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Indexes/Get.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Indexes/XList.php diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index bc80502521..63a4e3fbb7 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2,7 +2,6 @@ use Appwrite\Auth\Auth; use Appwrite\Detector\Detector; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; @@ -11,7 +10,6 @@ use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\CustomId; -use Appwrite\Utopia\Database\Validator\Queries\Indexes; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; use Utopia\App; @@ -31,8 +29,6 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Index as IndexValidator; -use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Cursor; @@ -41,395 +37,10 @@ use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; use Utopia\Locale\Locale; use Utopia\Validator\ArrayList; -use Utopia\Validator\Boolean; use Utopia\Validator\JSON; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; -App::post('/v1/databases/:databaseId/tables/:tableId/indexes') - ->alias('/v1/databases/:databaseId/collections/:tableId/indexes') - ->desc('Create index') - ->groups(['api', 'database']) - ->label('event', 'databases.[databaseId].tables.[tableId].indexes.[indexId].create') - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('audits.event', 'index.create') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'tables', - name: 'createIndex', - description: '/docs/references/databases/create-index.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_ACCEPTED, - model: Response::MODEL_INDEX, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', null, new Key(), 'Index Key.') - ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.') - ->param('columns', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of columns to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' columns are allowed, each 32 characters long.') - ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, string $type, array $columns, array $orders, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - - $table = $dbForProject->getDocument('database_' . $db->getInternalId(), $tableId); - - if ($table->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $count = $dbForProject->count('indexes', [ - Query::equal('collectionInternalId', [$table->getInternalId()]), - Query::equal('databaseInternalId', [$db->getInternalId()]) - ], 61); - - $limit = $dbForProject->getLimitForIndexes(); - - if ($count >= $limit) { - throw new Exception(Exception::INDEX_LIMIT_EXCEEDED, 'Index limit exceeded'); - } - - // Convert Document[] to array of attribute metadata - $oldColumns = \array_map(fn ($a) => $a->getArrayCopy(), $table->getAttribute('attributes')); - - $oldColumns[] = [ - 'key' => '$id', - 'type' => Database::VAR_STRING, - 'status' => 'available', - 'required' => true, - 'array' => false, - 'default' => null, - 'size' => Database::LENGTH_KEY - ]; - - $oldColumns[] = [ - 'key' => '$createdAt', - 'type' => Database::VAR_DATETIME, - 'status' => 'available', - 'signed' => false, - 'required' => false, - 'array' => false, - 'default' => null, - 'size' => 0 - ]; - - $oldColumns[] = [ - 'key' => '$updatedAt', - 'type' => Database::VAR_DATETIME, - 'status' => 'available', - 'signed' => false, - 'required' => false, - 'array' => false, - 'default' => null, - 'size' => 0 - ]; - - // lengths hidden by default - $lengths = []; - - foreach ($columns as $i => $column) { - // find attribute metadata in collection document - $columnIndex = \array_search($column, array_column($oldColumns, 'key')); - - if ($columnIndex === false) { - throw new Exception(Exception::ATTRIBUTE_UNKNOWN, 'Unknown column: ' . $column . '. Verify the column name or create the column.'); - } - - $columnStatus = $oldColumns[$columnIndex]['status']; - $columnType = $oldColumns[$columnIndex]['type']; - $columnArray = $oldColumns[$columnIndex]['array'] ?? false; - - if ($columnType === Database::VAR_RELATIONSHIP) { - throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, 'Cannot create an index for a relationship column: ' . $oldColumns[$columnIndex]['key']); - } - - // ensure attribute is available - if ($columnStatus !== 'available') { - throw new Exception(Exception::ATTRIBUTE_NOT_AVAILABLE, 'Column not available: ' . $oldColumns[$columnIndex]['key']); - } - - $lengths[$i] = null; - - if ($columnArray === true) { - $lengths[$i] = Database::ARRAY_INDEX_LENGTH; - $orders[$i] = null; - } - } - - $index = new Document([ - '$id' => ID::custom($db->getInternalId() . '_' . $table->getInternalId() . '_' . $key), - 'key' => $key, - 'status' => 'processing', // processing, available, failed, deleting, stuck - 'databaseInternalId' => $db->getInternalId(), - 'databaseId' => $databaseId, - 'collectionInternalId' => $table->getInternalId(), - 'collectionId' => $tableId, - 'type' => $type, - 'attributes' => $columns, - 'lengths' => $lengths, - 'orders' => $orders, - ]); - - $validator = new IndexValidator( - $table->getAttribute('attributes'), - $dbForProject->getAdapter()->getMaxIndexLength(), - $dbForProject->getAdapter()->getInternalIndexesKeys(), - ); - if (!$validator->isValid($index)) { - throw new Exception(Exception::INDEX_INVALID, $validator->getDescription()); - } - - try { - $index = $dbForProject->createDocument('indexes', $index); - } catch (DuplicateException) { - throw new Exception(Exception::INDEX_ALREADY_EXISTS); - } - - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $tableId); - - $queueForDatabase - ->setType(DATABASE_TYPE_CREATE_INDEX) - ->setDatabase($db) - ->setTable($table) - ->setRow($index); - - $queueForEvents - ->setParam('databaseId', $databaseId) - ->setParam('tableId', $table->getId()) - ->setParam('indexId', $index->getId()) - ->setContext('table', $table) - ->setContext('database', $db); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($index, Response::MODEL_INDEX); - }); - -App::get('/v1/databases/:databaseId/tables/:tableId/indexes') - ->alias('/v1/databases/:databaseId/collections/:tableId/indexes') - ->desc('List indexes') - ->groups(['api', 'database']) - ->label('scope', 'collections.read') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('sdk', new Method( - namespace: 'databases', - group: 'indexes', - name: 'listIndexes', - description: '/docs/references/databases/list-indexes.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_INDEX_LIST, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table 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 $tableId, array $queries, Response $response, Database $dbForProject) { - /** @var Document $database */ - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($database->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - - $table = $dbForProject->getDocument('database_' . $database->getInternalId(), $tableId); - - if ($table->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $queries = Query::parseQueries($queries); - - \array_push( - $queries, - Query::equal('databaseId', [$databaseId]), - Query::equal('collectionId', [$tableId]), - ); - - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - - if ($cursor) { - $validator = new Cursor(); - if (!$validator->isValid($cursor)) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); - } - - $indexId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->find('indexes', [ - Query::equal('collectionInternalId', [$table->getInternalId()]), - Query::equal('databaseInternalId', [$database->getInternalId()]), - 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']; - try { - $total = $dbForProject->count('indexes', $filterQueries, APP_LIMIT_COUNT); - $indexes = $dbForProject->find('indexes', $queries); - } catch (OrderException $e) { - throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order column '{$e->getAttribute()}' had a null value. Cursor pagination requires all rows order column values are non-null."); - } - - $response->dynamic(new Document([ - 'total' => $total, - 'indexes' => $indexes, - ]), Response::MODEL_INDEX_LIST); - }); - -App::get('/v1/databases/:databaseId/tables/:tableId/indexes/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/indexes/:key') - ->desc('Get index') - ->groups(['api', 'database']) - ->label('scope', 'collections.read') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('sdk', new Method( - namespace: 'databases', - group: 'indexes', - name: 'getIndex', - description: '/docs/references/databases/get-index.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_INDEX, - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', null, new Key(), 'Index Key.') - ->inject('response') - ->inject('dbForProject') - ->action(function (string $databaseId, string $tableId, string $key, Response $response, Database $dbForProject) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($database->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - $table = $dbForProject->getDocument('database_' . $database->getInternalId(), $tableId); - - if ($table->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $index = $table->find('key', $key, 'indexes'); - if (empty($index)) { - throw new Exception(Exception::INDEX_NOT_FOUND); - } - - $response->dynamic($index, Response::MODEL_INDEX); - }); - - -App::delete('/v1/databases/:databaseId/tables/:tableId/indexes/:key') - ->alias('/v1/databases/:databaseId/collections/:tableId/indexes/:key') - ->desc('Delete index') - ->groups(['api', 'database']) - ->label('scope', 'collections.write') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('event', 'databases.[databaseId].tables.[tableId].indexes.[indexId].update') - ->label('audits.event', 'index.delete') - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') - ->label('sdk', new Method( - namespace: 'databases', - group: 'indexes', - name: 'deleteIndex', - description: '/docs/references/databases/delete-index.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('key', '', new Key(), 'Index Key.') - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDatabase') - ->inject('queueForEvents') - ->action(function (string $databaseId, string $tableId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - $table = $dbForProject->getDocument('database_' . $db->getInternalId(), $tableId); - - if ($table->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $index = $dbForProject->getDocument('indexes', $db->getInternalId() . '_' . $table->getInternalId() . '_' . $key); - - if (empty($index->getId())) { - throw new Exception(Exception::INDEX_NOT_FOUND); - } - - // Only update status if removing available index - if ($index->getAttribute('status') === 'available') { - $index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting')); - } - - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $tableId); - - $queueForDatabase - ->setType(DATABASE_TYPE_DELETE_INDEX) - ->setDatabase($db) - ->setTable($table) - ->setRow($index); - - $queueForEvents - ->setParam('databaseId', $databaseId) - ->setParam('tableId', $table->getId()) - ->setParam('indexId', $index->getId()) - ->setContext('table', $table) - ->setContext('database', $db) - ->setPayload($response->output($index, Response::MODEL_INDEX)); - - $response->noContent(); - }); - App::post('/v1/databases/:databaseId/tables/:tableId/rows') ->alias('/v1/databases/:databaseId/collections/:tableId/documents') ->desc('Create row') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Indexes/Create.php new file mode 100644 index 0000000000..49cb7157b0 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Indexes/Create.php @@ -0,0 +1,217 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/indexes') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/indexes') + ->desc('Create index') + ->groups(['api', 'database']) + ->label('event', 'databases.[databaseId].tables.[tableId].indexes.[indexId].create') + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('audits.event', 'index.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'tables', + name: 'createIndex', + description: '/docs/references/databases/create-index.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: UtopiaResponse::MODEL_INDEX, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('key', null, new Key(), 'Index Key.') + ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.') + ->param('columns', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of columns to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' columns are allowed, each 32 characters long.') + ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, string $tableId, string $key, string $type, array $columns, array $orders, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + { + $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + + if ($db->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $table = $dbForProject->getDocument('database_' . $db->getInternalId(), $tableId); + + if ($table->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $count = $dbForProject->count('indexes', [ + Query::equal('collectionInternalId', [$table->getInternalId()]), + Query::equal('databaseInternalId', [$db->getInternalId()]) + ], 61); + + $limit = $dbForProject->getLimitForIndexes(); + + if ($count >= $limit) { + throw new Exception(Exception::INDEX_LIMIT_EXCEEDED, 'Index limit exceeded'); + } + + // Convert Document[] to array of attribute metadata + $oldColumns = \array_map(fn ($a) => $a->getArrayCopy(), $table->getAttribute('attributes')); + + $oldColumns[] = [ + 'key' => '$id', + 'type' => Database::VAR_STRING, + 'status' => 'available', + 'required' => true, + 'array' => false, + 'default' => null, + 'size' => Database::LENGTH_KEY + ]; + + $oldColumns[] = [ + 'key' => '$createdAt', + 'type' => Database::VAR_DATETIME, + 'status' => 'available', + 'signed' => false, + 'required' => false, + 'array' => false, + 'default' => null, + 'size' => 0 + ]; + + $oldColumns[] = [ + 'key' => '$updatedAt', + 'type' => Database::VAR_DATETIME, + 'status' => 'available', + 'signed' => false, + 'required' => false, + 'array' => false, + 'default' => null, + 'size' => 0 + ]; + + // lengths hidden by default + $lengths = []; + + foreach ($columns as $i => $column) { + // find attribute metadata in collection document + $columnIndex = \array_search($column, array_column($oldColumns, 'key')); + + if ($columnIndex === false) { + throw new Exception(Exception::ATTRIBUTE_UNKNOWN, 'Unknown column: ' . $column . '. Verify the column name or create the column.'); + } + + $columnStatus = $oldColumns[$columnIndex]['status']; + $columnType = $oldColumns[$columnIndex]['type']; + $columnArray = $oldColumns[$columnIndex]['array'] ?? false; + + if ($columnType === Database::VAR_RELATIONSHIP) { + throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, 'Cannot create an index for a relationship column: ' . $oldColumns[$columnIndex]['key']); + } + + // ensure attribute is available + if ($columnStatus !== 'available') { + throw new Exception(Exception::ATTRIBUTE_NOT_AVAILABLE, 'Column not available: ' . $oldColumns[$columnIndex]['key']); + } + + $lengths[$i] = null; + + if ($columnArray === true) { + $lengths[$i] = Database::ARRAY_INDEX_LENGTH; + $orders[$i] = null; + } + } + + $index = new Document([ + '$id' => ID::custom($db->getInternalId() . '_' . $table->getInternalId() . '_' . $key), + 'key' => $key, + 'status' => 'processing', // processing, available, failed, deleting, stuck + 'databaseInternalId' => $db->getInternalId(), + 'databaseId' => $databaseId, + 'collectionInternalId' => $table->getInternalId(), + 'collectionId' => $tableId, + 'type' => $type, + 'attributes' => $columns, + 'lengths' => $lengths, + 'orders' => $orders, + ]); + + $validator = new IndexValidator( + $table->getAttribute('attributes'), + $dbForProject->getAdapter()->getMaxIndexLength(), + $dbForProject->getAdapter()->getInternalIndexesKeys(), + ); + if (!$validator->isValid($index)) { + throw new Exception(Exception::INDEX_INVALID, $validator->getDescription()); + } + + try { + $index = $dbForProject->createDocument('indexes', $index); + } catch (DuplicateException) { + throw new Exception(Exception::INDEX_ALREADY_EXISTS); + } + + $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $tableId); + + $queueForDatabase + ->setType(DATABASE_TYPE_CREATE_INDEX) + ->setDatabase($db) + ->setTable($table) + ->setRow($index); + + $queueForEvents + ->setParam('databaseId', $databaseId) + ->setParam('tableId', $table->getId()) + ->setParam('indexId', $index->getId()) + ->setContext('table', $table) + ->setContext('database', $db); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($index, UtopiaResponse::MODEL_INDEX); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Indexes/Delete.php new file mode 100644 index 0000000000..eeb772e0c3 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Indexes/Delete.php @@ -0,0 +1,109 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/indexes/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/indexes/:key') + ->desc('Delete index') + ->groups(['api', 'database']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].indexes.[indexId].update') + ->label('audits.event', 'index.delete') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: 'databases', + group: 'indexes', + name: 'deleteIndex', + description: '/docs/references/databases/delete-index.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_NOCONTENT, + model: UtopiaResponse::MODEL_NONE, + ) + ], + contentType: ContentType::NONE + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('key', '', new Key(), 'Index Key.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, string $tableId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + { + $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + + if ($db->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + $table = $dbForProject->getDocument('database_' . $db->getInternalId(), $tableId); + + if ($table->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $index = $dbForProject->getDocument('indexes', $db->getInternalId() . '_' . $table->getInternalId() . '_' . $key); + + if (empty($index->getId())) { + throw new Exception(Exception::INDEX_NOT_FOUND); + } + + // Only update status if removing available index + if ($index->getAttribute('status') === 'available') { + $index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting')); + } + + $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $tableId); + + $queueForDatabase + ->setType(DATABASE_TYPE_DELETE_INDEX) + ->setDatabase($db) + ->setTable($table) + ->setRow($index); + + $queueForEvents + ->setParam('databaseId', $databaseId) + ->setParam('tableId', $table->getId()) + ->setParam('indexId', $index->getId()) + ->setContext('table', $table) + ->setContext('database', $db) + ->setPayload($response->output($index, UtopiaResponse::MODEL_INDEX)); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Indexes/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Indexes/Get.php new file mode 100644 index 0000000000..72ab1a02b1 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Indexes/Get.php @@ -0,0 +1,80 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/indexes/:key') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/indexes/:key') + ->desc('Get index') + ->groups(['api', 'database']) + ->label('scope', 'collections.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'indexes', + name: 'getIndex', + description: '/docs/references/databases/get-index.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_INDEX, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('key', null, new Key(), 'Index Key.') + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, string $tableId, string $key, UtopiaResponse $response, Database $dbForProject): void + { + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + $table = $dbForProject->getDocument('database_' . $database->getInternalId(), $tableId); + + if ($table->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $index = $table->find('key', $key, 'indexes'); + if (empty($index)) { + throw new Exception(Exception::INDEX_NOT_FOUND); + } + + $response->dynamic($index, UtopiaResponse::MODEL_INDEX); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Indexes/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Indexes/XList.php new file mode 100644 index 0000000000..4920b679c1 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Indexes/XList.php @@ -0,0 +1,129 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/indexes') + ->httpAlias('/v1/databases/:databaseId/collections/:tableId/indexes') + ->desc('List indexes') + ->groups(['api', 'database']) + ->label('scope', 'collections.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'indexes', + name: 'listIndexes', + description: '/docs/references/databases/list-indexes.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_INDEX_LIST, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('tableId', '', new UID(), 'Table ID. You can create a new table 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') + ->callback([$this, 'action']); + } + + public function action(string $databaseId, string $tableId, array $queries, UtopiaResponse $response, Database $dbForProject): void + { + /** @var Document $database */ + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $table = $dbForProject->getDocument('database_' . $database->getInternalId(), $tableId); + + if ($table->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $queries = Query::parseQueries($queries); + + \array_push( + $queries, + Query::equal('databaseId', [$databaseId]), + Query::equal('collectionId', [$tableId]), + ); + + /** + * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries + */ + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); + }); + $cursor = reset($cursor); + + if ($cursor) { + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $indexId = $cursor->getValue(); + $cursorDocument = Authorization::skip(fn () => $dbForProject->find('indexes', [ + Query::equal('collectionInternalId', [$table->getInternalId()]), + Query::equal('databaseInternalId', [$database->getInternalId()]), + 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']; + try { + $total = $dbForProject->count('indexes', $filterQueries, APP_LIMIT_COUNT); + $indexes = $dbForProject->find('indexes', $queries); + } catch (OrderException $e) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order column '{$e->getAttribute()}' had a null value. Cursor pagination requires all rows order column values are non-null."); + } + + $response->dynamic(new Document([ + 'total' => $total, + 'indexes' => $indexes, + ]), UtopiaResponse::MODEL_INDEX_LIST); + } +}