diff --git a/app/config/errors.php b/app/config/errors.php index 182b6286e8..de26b077fc 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -903,6 +903,33 @@ return [ 'code' => 409, ], + /** Column Indexes, same as Indexes but with different type */ + Exception::COLUMN_INDEX_NOT_FOUND => [ + 'name' => Exception::COLUMN_INDEX_NOT_FOUND, + 'description' => 'Index with the requested ID could not be found.', + 'code' => 404, + ], + Exception::COLUMN_INDEX_LIMIT_EXCEEDED => [ + 'name' => Exception::COLUMN_INDEX_LIMIT_EXCEEDED, + 'description' => 'The maximum number of indexes has been reached.', + 'code' => 400, + ], + Exception::COLUMN_INDEX_ALREADY_EXISTS => [ + 'name' => Exception::COLUMN_INDEX_ALREADY_EXISTS, + 'description' => 'Index with the requested key already exists. Try again with a different key.', + 'code' => 409, + ], + Exception::COLUMN_INDEX_INVALID => [ + 'name' => Exception::COLUMN_INDEX_INVALID, + 'description' => 'Index invalid.', + 'code' => 400, + ], + Exception::COLUMN_INDEX_DEPENDENCY => [ + 'name' => Exception::COLUMN_INDEX_DEPENDENCY, + 'description' => 'Column cannot be renamed or deleted. Please remove the associated index first.', + 'code' => 409, + ], + /** Project Errors */ Exception::PROJECT_NOT_FOUND => [ 'name' => Exception::PROJECT_NOT_FOUND, diff --git a/app/controllers/general.php b/app/controllers/general.php index da68d4758c..b7ee606c69 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1216,7 +1216,13 @@ App::error() ); break; case 'Utopia\Database\Exception\Dependency': - $error = new AppwriteException(AppwriteException::INDEX_DEPENDENCY, null, previous: $error); + $error = new AppwriteException( + $isTablesAPI + ? AppwriteException::COLUMN_INDEX_DEPENDENCY + : AppwriteException::INDEX_DEPENDENCY, + null, + previous: $error + ); break; } diff --git a/app/init/constants.php b/app/init/constants.php index 29a861c45a..c3b6d4a058 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -271,7 +271,9 @@ const TOKENS_RESOURCE_TYPE_DATABASES = 'databases'; const DATABASE_ROWS_CONTEXT = 'row'; const DATABASE_TABLES_CONTEXT = 'table'; const DATABASE_COLUMNS_CONTEXT = 'column'; +const DATABASE_INDEX_CONTEXT = 'index'; const DATABASE_DOCUMENTS_CONTEXT = 'document'; const DATABASE_ATTRIBUTES_CONTEXT = 'attribute'; const DATABASE_COLLECTIONS_CONTEXT = 'collection'; +const DATABASE_COLUMN_INDEX_CONTEXT = 'columnIndex'; diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 15b964577d..38a562571c 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -253,6 +253,13 @@ class Exception extends \Exception public const INDEX_INVALID = 'index_invalid'; public const INDEX_DEPENDENCY = 'index_dependency'; + /** Column Indexes */ + public const COLUMN_INDEX_NOT_FOUND = 'column_index_not_found'; + public const COLUMN_INDEX_LIMIT_EXCEEDED = 'column_index_limit_exceeded'; + public const COLUMN_INDEX_ALREADY_EXISTS = 'column_index_already_exists'; + public const COLUMN_INDEX_INVALID = 'column_index_invalid'; + public const COLUMN_INDEX_DEPENDENCY = 'column_index_dependency'; + /** Projects */ public const PROJECT_NOT_FOUND = 'project_not_found'; public const PROJECT_PROVIDER_DISABLED = 'project_provider_disabled'; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Action.php index eaaa08d237..abec90f1a6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Action.php @@ -5,12 +5,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections; use Appwrite\Extend\Exception; use Utopia\Platform\Action as UtopiaAction; -/** - * Abstract base action for Collection and Table database routes. - * - * Provides shared utilities to determine API type, response model, - * SDK group, and context-specific exceptions. - */ abstract class Action extends UtopiaAction { /** @@ -39,8 +33,6 @@ abstract class Action extends UtopiaAction /** * Get the current API context. - * - * @throws \Exception if context has not been set. */ final protected function getContext(): string { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php index 1c779f4d3b..fff070d58f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php @@ -42,7 +42,7 @@ abstract class Action extends UtopiaAction final protected function setContext(string $context): void { if (!\in_array($context, [DATABASE_COLUMNS_CONTEXT, DATABASE_ATTRIBUTES_CONTEXT], true)) { - throw new \InvalidArgumentException("Invalid context '{$context}'. Use `DATABASE_COLUMNS_CONTEXT` or `DATABASE_ATTRIBUTES_CONTEXT`"); + throw new \InvalidArgumentException("Invalid context '$context'. Use `DATABASE_COLUMNS_CONTEXT` or `DATABASE_ATTRIBUTES_CONTEXT`"); } $this->context = $context; @@ -112,6 +112,16 @@ abstract class Action extends UtopiaAction : Exception::COLUMN_NOT_FOUND; } + /** + * Get the appropriate not found exception. + */ + final protected function getIndexDependencyException(): string + { + return $this->isCollectionsAPI() + ? Exception::INDEX_DEPENDENCY + : Exception::COLUMN_INDEX_DEPENDENCY; + } + /** * Get the appropriate already exists exception. */ @@ -275,7 +285,7 @@ abstract class Action extends UtopiaAction if (!empty($format)) { if (!Structure::hasFormat($format, $type)) { - throw new Exception($this->getFormatUnsupportedException(), "Format {$format} not available for {$type} columns."); + throw new Exception($this->getFormatUnsupportedException(), "Format $format not available for $type columns."); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php index 9597070be6..444fa41a7f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php @@ -91,7 +91,7 @@ class Delete extends Action $dbForProject->getAdapter()->getSupportForCastIndexArray(), ); if (!$validator->isValid($attribute)) { - throw new Exception(Exception::INDEX_DEPENDENCY); + throw new Exception($this->getIndexDependencyException()); } if ($attribute->getAttribute('status') === 'available') { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Action.php new file mode 100644 index 0000000000..102acdb639 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Action.php @@ -0,0 +1,163 @@ +context = $context; + } + + /** + * Get the current API's parent context. + */ + final protected function getParentContext(): string + { + return $this->getContext() === DATABASE_INDEX_CONTEXT + ? DATABASE_ATTRIBUTES_CONTEXT + : DATABASE_COLUMNS_CONTEXT; + } + + /** + * Get the current API context. + */ + final protected function getContext(): string + { + return $this->context; + } + + /** + * Get the correct grand parent param key (e.g. `tableId` or `collectionId`) + */ + final protected function getGrandParentEventsParamKey(): string + { + return $this->isCollectionsAPI() ? 'collectionId' : 'tableId'; + } + + /** + * Get the key used in event parameters. + */ + final protected function getEventsParamKey(): string + { + return 'indexId'; + } + + /** + * Determine if the current action is for the Collections API. + */ + final protected function isCollectionsAPI(): bool + { + return $this->getParentContext() === DATABASE_ATTRIBUTES_CONTEXT; + } + + /** + * Get the SDK group name for the current action. + */ + final protected function getSdkGroup(): string + { + return 'indexes'; + } + + /** + * Get the exception to throw when the parent is unknown. + */ + final protected function getParentUnknownException(): string + { + return $this->isCollectionsAPI() + ? Exception::ATTRIBUTE_UNKNOWN + : Exception::COLUMN_UNKNOWN; + } + + /** + * Get the appropriate grandparent level not found exception. + */ + final protected function getGrantParentNotFoundException(): string + { + return $this->isCollectionsAPI() + ? Exception::COLLECTION_NOT_FOUND + : Exception::TABLE_NOT_FOUND; + } + + /** + * Get the appropriate not found exception. + */ + final protected function getNotFoundException(): string + { + return $this->isCollectionsAPI() + ? Exception::INDEX_NOT_FOUND + : Exception::COLUMN_INDEX_NOT_FOUND; + } + + /** + * Get the exception to throw when the parent type is invalid. + */ + final protected function getParentInvalidTypeException(): string + { + return $this->isCollectionsAPI() + ? Exception::ATTRIBUTE_TYPE_INVALID + : Exception::COLUMN_TYPE_INVALID; + } + + /** + * Get the exception to throw when the index type is invalid. + */ + final protected function getInvalidTypeException(): string + { + return $this->isCollectionsAPI() + ? Exception::INDEX_INVALID + : Exception::COLUMN_INDEX_INVALID; + } + + /** + * Get the exception to throw when the resource already exists. + */ + final protected function getDuplicateException(): string + { + return $this->isCollectionsAPI() + ? Exception::INDEX_ALREADY_EXISTS + : Exception::COLUMN_INDEX_ALREADY_EXISTS; + } + + /** + * Get the exception to throw when the resource limit is exceeded. + */ + final protected function getLimitException(): string + { + return $this->isCollectionsAPI() + ? Exception::INDEX_LIMIT_EXCEEDED + : Exception::COLUMN_INDEX_LIMIT_EXCEEDED; + } + + /** + * Get the exception to throw when the parent attribute/column is not in `available` state. + */ + final protected function getParentNotAvailableException(): string + { + return $this->isCollectionsAPI() + ? Exception::ATTRIBUTE_NOT_AVAILABLE + : Exception::COLUMN_NOT_AVAILABLE; + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php index e9dd4c7092..aa4729e605 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php @@ -1,6 +1,6 @@ setHttpMethod(self::HTTP_REQUEST_METHOD_POST) - ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/indexes') + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/indexes') ->desc('Create index') ->groups(['api', 'database']) - ->label('event', 'databases.[databaseId].tables.[tableId].indexes.[indexId].create') + ->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].create') ->label('scope', 'collections.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('audits.event', 'index.create') - // TODO: audits table or collections, check the context type if possible, move into another module. - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( namespace: 'databases', - group: 'tables', - name: 'createIndex', + group: $this->getSdkGroup(), + name: self::getName(), description: '/docs/references/databases/create-index.md', auth: [AuthType::KEY], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_ACCEPTED, - model: UtopiaResponse::MODEL_INDEX, + model: $this->getResponseModel(), ) ], 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('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('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('attributes', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes 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') @@ -74,7 +77,7 @@ class Create extends Action ->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 + public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void { $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -82,27 +85,28 @@ class Create extends Action throw new Exception(Exception::DATABASE_NOT_FOUND); } - $table = $dbForProject->getDocument('database_' . $db->getInternalId(), $tableId); + $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); - if ($table->isEmpty()) { - throw new Exception(Exception::TABLE_NOT_FOUND); + if ($collection->isEmpty()) { + // table or collection. + throw new Exception($this->getGrantParentNotFoundException()); } $count = $dbForProject->count('indexes', [ - Query::equal('collectionInternalId', [$table->getInternalId()]), + Query::equal('collectionInternalId', [$collection->getInternalId()]), Query::equal('databaseInternalId', [$db->getInternalId()]) ], 61); $limit = $dbForProject->getLimitForIndexes(); if ($count >= $limit) { - throw new Exception(Exception::INDEX_LIMIT_EXCEEDED, 'Index limit exceeded'); + throw new Exception($this->getLimitException(), 'Index limit exceeded'); } // Convert Document[] to array of attribute metadata - $oldColumns = \array_map(fn ($a) => $a->getArrayCopy(), $table->getAttribute('attributes')); + $oldAttributes = \array_map(fn ($a) => $a->getArrayCopy(), $collection->getAttribute('attributes')); - $oldColumns[] = [ + $oldAttributes[] = [ 'key' => '$id', 'type' => Database::VAR_STRING, 'status' => 'available', @@ -112,7 +116,7 @@ class Create extends Action 'size' => Database::LENGTH_KEY ]; - $oldColumns[] = [ + $oldAttributes[] = [ 'key' => '$createdAt', 'type' => Database::VAR_DATETIME, 'status' => 'available', @@ -123,7 +127,7 @@ class Create extends Action 'size' => 0 ]; - $oldColumns[] = [ + $oldAttributes[] = [ 'key' => '$updatedAt', 'type' => Database::VAR_DATETIME, 'status' => 'available', @@ -137,25 +141,27 @@ class Create extends Action // lengths hidden by default $lengths = []; - foreach ($columns as $i => $column) { + $contextType = $this->getParentContext(); + foreach ($attributes as $i => $attribute) { // find attribute metadata in collection document - $columnIndex = \array_search($column, array_column($oldColumns, 'key')); + $attributeIndex = \array_search($attribute, array_column($oldAttributes, 'key')); - if ($columnIndex === false) { - throw new Exception(Exception::COLUMN_UNKNOWN, 'Unknown column: ' . $column . '. Verify the column name or create the column.'); + if ($attributeIndex === false) { + throw new Exception($this->getParentUnknownException(), "Unknown $contextType: " . $attribute . ". Verify the $contextType name or create the $contextType."); } - $columnStatus = $oldColumns[$columnIndex]['status']; - $columnType = $oldColumns[$columnIndex]['type']; - $columnArray = $oldColumns[$columnIndex]['array'] ?? false; + $columnStatus = $oldAttributes[$attributeIndex]['status']; + $columnType = $oldAttributes[$attributeIndex]['type']; + $columnArray = $oldAttributes[$attributeIndex]['array'] ?? false; if ($columnType === Database::VAR_RELATIONSHIP) { - throw new Exception(Exception::COLUMN_TYPE_INVALID, 'Cannot create an index for a relationship column: ' . $oldColumns[$columnIndex]['key']); + throw new Exception($this->getParentInvalidTypeException(), "Cannot create an index for a relationship $contextType: " . $oldAttributes[$attributeIndex]['key']); } // ensure attribute is available if ($columnStatus !== 'available') { - throw new Exception(Exception::COLUMN_NOT_AVAILABLE, 'Column not available: ' . $oldColumns[$columnIndex]['key']); + $contextType = ucfirst($contextType); + throw new Exception($this->getParentNotAvailableException(), "$contextType not available: " . $oldAttributes[$attributeIndex]['key']); } $lengths[$i] = null; @@ -167,51 +173,59 @@ class Create extends Action } $index = new Document([ - '$id' => ID::custom($db->getInternalId() . '_' . $table->getInternalId() . '_' . $key), + '$id' => ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key), 'key' => $key, 'status' => 'processing', // processing, available, failed, deleting, stuck 'databaseInternalId' => $db->getInternalId(), 'databaseId' => $databaseId, - 'collectionInternalId' => $table->getInternalId(), - 'collectionId' => $tableId, + 'collectionInternalId' => $collection->getInternalId(), + 'collectionId' => $collectionId, 'type' => $type, - 'attributes' => $columns, + 'attributes' => $attributes, 'lengths' => $lengths, 'orders' => $orders, ]); $validator = new IndexValidator( - $table->getAttribute('attributes'), + $collection->getAttribute('attributes'), $dbForProject->getAdapter()->getMaxIndexLength(), $dbForProject->getAdapter()->getInternalIndexesKeys(), ); if (!$validator->isValid($index)) { - throw new Exception(Exception::INDEX_INVALID, $validator->getDescription()); + throw new Exception($this->getInvalidTypeException(), $validator->getDescription()); } try { $index = $dbForProject->createDocument('indexes', $index); } catch (DuplicateException) { - throw new Exception(Exception::INDEX_ALREADY_EXISTS); + throw new Exception($this->getDuplicateException()); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $tableId); + $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId); $queueForDatabase ->setType(DATABASE_TYPE_CREATE_INDEX) - ->setDatabase($db) - ->setTable($table) - ->setRow($index); + ->setDatabase($db); + + if ($this->isCollectionsAPI()) { + $queueForDatabase + ->setCollection($collection) + ->setDocument($index); + } else { + $queueForDatabase + ->setTable($collection) + ->setRow($index); + } $queueForEvents + ->setContext('database', $db) ->setParam('databaseId', $databaseId) - ->setParam('tableId', $table->getId()) - ->setParam('indexId', $index->getId()) - ->setContext('table', $table) - ->setContext('database', $db); + ->setParam($this->getEventsParamKey(), $index->getId()) + ->setParam($this->getGrandParentEventsParamKey(), $collection->getId()) + ->setContext($this->isCollectionsAPI() ? 'collection' : 'table', $collection); $response ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) - ->dynamic($index, UtopiaResponse::MODEL_INDEX); + ->dynamic($index, $this->getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php index dd7c4bd5e7..157e4ef7de 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php @@ -1,6 +1,6 @@ setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE) - ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/indexes/:key') + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/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('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].update') ->label('audits.event', 'index.delete') - // TODO: audits table or collections, check the context type if possible - ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( namespace: 'databases', - group: 'indexes', - name: 'deleteIndex', + group: $this->getSdkGroup(), + name: self::getName(), description: '/docs/references/databases/delete-index.md', auth: [AuthType::KEY], responses: [ @@ -55,7 +62,7 @@ class Delete extends Action 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('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('key', '', new Key(), 'Index Key.') ->inject('response') ->inject('dbForProject') @@ -64,23 +71,24 @@ class Delete extends Action ->callback([$this, 'action']); } - public function action(string $databaseId, string $tableId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, 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); + $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); - if ($table->isEmpty()) { - throw new Exception(Exception::TABLE_NOT_FOUND); + if ($collection->isEmpty()) { + // table or collection. + throw new Exception($this->getGrantParentNotFoundException()); } - $index = $dbForProject->getDocument('indexes', $db->getInternalId() . '_' . $table->getInternalId() . '_' . $key); + $index = $dbForProject->getDocument('indexes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); if (empty($index->getId())) { - throw new Exception(Exception::INDEX_NOT_FOUND); + throw new Exception($this->getNotFoundException()); } // Only update status if removing available index @@ -88,21 +96,29 @@ class Delete extends Action $index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting')); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $tableId); + $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId); $queueForDatabase ->setType(DATABASE_TYPE_DELETE_INDEX) - ->setDatabase($db) - ->setTable($table) - ->setRow($index); + ->setDatabase($db); + + if ($this->isCollectionsAPI()) { + $queueForDatabase + ->setCollection($collection) + ->setDocument($index); + } else { + $queueForDatabase + ->setTable($collection) + ->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)); + ->setParam('databaseId', $databaseId) + ->setParam($this->getEventsParamKey(), $index->getId()) + ->setPayload($response->output($index, $this->getResponseModel())) + ->setParam($this->getGrandParentEventsParamKey(), $collection->getId()) + ->setContext($this->isCollectionsAPI() ? 'collection' : 'table', $collection); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Get.php index a89819d844..3e8a67b395 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Get.php @@ -1,6 +1,6 @@ setHttpMethod(self::HTTP_REQUEST_METHOD_GET) - ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/indexes/:key') + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/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', + group: $this->getSdkGroup(), + name: self::getName(), description: '/docs/references/databases/get-index.md', auth: [AuthType::KEY], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_OK, - model: UtopiaResponse::MODEL_INDEX, + model: $this->getResponseModel(), ) ], 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('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('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 + public function action(string $databaseId, string $collectionId, 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); + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); - if ($table->isEmpty()) { - throw new Exception(Exception::TABLE_NOT_FOUND); + if ($collection->isEmpty()) { + // table or collection. + throw new Exception($this->getGrantParentNotFoundException()); } - $index = $table->find('key', $key, 'indexes'); + $index = $collection->find('key', $key, 'indexes'); if (empty($index)) { - throw new Exception(Exception::INDEX_NOT_FOUND); + throw new Exception($this->getNotFoundException()); } - $response->dynamic($index, UtopiaResponse::MODEL_INDEX); + $response->dynamic($index, $this->getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php index b11806cfd0..45627654dc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php @@ -1,6 +1,6 @@ setHttpMethod(self::HTTP_REQUEST_METHOD_GET) - ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/indexes') + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/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', + group: $this->getSdkGroup(), + name: self::getName(), description: '/docs/references/databases/list-indexes.md', auth: [AuthType::KEY], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_OK, - model: UtopiaResponse::MODEL_INDEX_LIST, + model: $this->getResponseModel(), ) ], 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('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') ->callback([$this, 'action']); } - public function action(string $databaseId, string $tableId, array $queries, UtopiaResponse $response, Database $dbForProject): void + public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject): void { /** @var Document $database */ $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -69,10 +73,11 @@ class XList extends Action throw new Exception(Exception::DATABASE_NOT_FOUND); } - $table = $dbForProject->getDocument('database_' . $database->getInternalId(), $tableId); + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); - if ($table->isEmpty()) { - throw new Exception(Exception::TABLE_NOT_FOUND); + if ($collection->isEmpty()) { + // table or collection. + throw new Exception($this->getGrantParentNotFoundException()); } $queries = Query::parseQueries($queries); @@ -80,7 +85,7 @@ class XList extends Action \array_push( $queries, Query::equal('databaseId', [$databaseId]), - Query::equal('collectionId', [$tableId]), + Query::equal('collectionId', [$collectionId]), ); /** @@ -99,7 +104,7 @@ class XList extends Action $indexId = $cursor->getValue(); $cursorDocument = Authorization::skip(fn () => $dbForProject->find('indexes', [ - Query::equal('collectionInternalId', [$table->getInternalId()]), + Query::equal('collectionInternalId', [$collection->getInternalId()]), Query::equal('databaseInternalId', [$database->getInternalId()]), Query::equal('key', [$indexId]), Query::limit(1) @@ -117,12 +122,15 @@ class XList extends Action $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."); + $documents = $this->isCollectionsAPI() ? 'documents' : 'rows'; + $attribute = $this->isCollectionsAPI() ? 'attribute' : 'column'; + $message = "The order $attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all $documents order $attribute values are non-null."; + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, $message); } $response->dynamic(new Document([ 'total' => $total, 'indexes' => $indexes, - ]), UtopiaResponse::MODEL_INDEX_LIST); + ]), $this->getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Indexes/Create.php new file mode 100644 index 0000000000..844a317de7 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Indexes/Create.php @@ -0,0 +1,72 @@ +setContext(DATABASE_COLUMN_INDEX_CONTEXT); + + $this + ->setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/tables/:tables/indexes') + ->desc('Create index') + ->groups(['api', 'database']) + ->label('event', 'databases.[databaseId].tables.[tables].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: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/databases/create-index.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: $this->getResponseModel(), + ) + ], + 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(function (string $databaseId, string $tableId, string $key, string $type, array $columns, array $orders, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + parent::action($databaseId, $tableId, $key, $type, $columns, $orders, $response, $dbForProject, $queueForDatabase, $queueForEvents); + }); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Indexes/Delete.php new file mode 100644 index 0000000000..d4185cfcbe --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Indexes/Delete.php @@ -0,0 +1,71 @@ +setContext(DATABASE_COLUMN_INDEX_CONTEXT); + + $this + ->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/databases/:databaseId/tables/: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: $this->getSdkGroup(), + name: self::getName(), + 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(function (string $databaseId, string $tableId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + parent::action($databaseId, $tableId, $key, $response, $dbForProject, $queueForDatabase, $queueForEvents); + }); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Indexes/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Indexes/Get.php new file mode 100644 index 0000000000..ee0432522c --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Indexes/Get.php @@ -0,0 +1,60 @@ +setContext(DATABASE_COLUMN_INDEX_CONTEXT); + + $this + ->setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/databases/:databaseId/tables/: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: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/databases/get-index.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: $this->getResponseModel(), + ) + ], + 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(function (string $databaseId, string $tableId, string $key, UtopiaResponse $response, Database $dbForProject) { + parent::action($databaseId, $tableId, $key, $response, $dbForProject); + }); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Indexes/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Indexes/XList.php new file mode 100644 index 0000000000..50c1eb11d1 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Indexes/XList.php @@ -0,0 +1,60 @@ +setContext(DATABASE_COLUMN_INDEX_CONTEXT); + + $this + ->setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/databases/:databaseId/tables/:tableId/indexes') + ->desc('List indexes') + ->groups(['api', 'database']) + ->label('scope', 'collections.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: $this->getSdkGroup(), + name: self::getName(), + description: '/docs/references/databases/list-indexes.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: $this->getResponseModel(), + ) + ], + 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(function (string $databaseId, string $tableId, array $queries, UtopiaResponse $response, Database $dbForProject) { + parent::action($databaseId, $tableId, $queries, $response, $dbForProject); + }); + } +}