add: indexes api to tables.

This commit is contained in:
Darshan
2025-05-08 14:15:00 +05:30
parent da70da985b
commit fe693a9469
16 changed files with 624 additions and 111 deletions
+27
View File
@@ -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,
+7 -1
View File
@@ -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;
}
+2
View File
@@ -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';
+7
View File
@@ -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';
@@ -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
{
@@ -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.");
}
}
@@ -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') {
@@ -0,0 +1,163 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Extend\Exception;
use Utopia\Platform\Action as UtopiaAction;
abstract class Action extends UtopiaAction
{
/**
* The current API context (either 'columnIndex' or 'index').
*/
private ?string $context = DATABASE_INDEX_CONTEXT;
/**
* Get the response model used in the SDK and HTTP responses.
*/
abstract protected function getResponseModel(): string;
/**
* Set the current API context.
*
* @param string $context Must be either `DATABASE_INDEX_CONTEXT` or `DATABASE_COLUMN_INDEX_CONTEXT`.
*/
final protected function setContext(string $context): void
{
if (!\in_array($context, [DATABASE_INDEX_CONTEXT, DATABASE_COLUMN_INDEX_CONTEXT], true)) {
throw new \InvalidArgumentException("Invalid context '$context'. Must be either `DATABASE_COLUMN_INDEX_CONTEXT` or `DATABASE_INDEX_CONTEXT`.");
}
$this->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;
}
}
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Indexes;
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
@@ -19,7 +19,6 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Index as IndexValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
@@ -34,38 +33,42 @@ class Create extends Action
return 'createIndex';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_INDEX;
}
public function __construct()
{
$this
->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());
}
}
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Indexes;
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
@@ -14,7 +14,6 @@ use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
@@ -27,23 +26,31 @@ class Delete extends Action
return 'deleteIndex';
}
/**
* 1. `SDKResponse` uses `UtopiaResponse::MODEL_NONE`.
* 2. But we later need the actual return type for events queue below!
*/
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_INDEX;
}
public function __construct()
{
$this
->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();
}
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Indexes;
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
@@ -12,7 +12,6 @@ use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
@@ -25,55 +24,61 @@ class Get extends Action
return 'getIndex';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_INDEX;
}
public function __construct()
{
$this
->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());
}
}
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Indexes;
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
@@ -16,7 +16,6 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
@@ -29,38 +28,43 @@ class XList extends Action
return 'listIndexes';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_INDEX_LIST;
}
public function __construct()
{
$this
->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());
}
}
@@ -0,0 +1,72 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Indexes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes\Create as IndexCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
class Create extends IndexCreate
{
use HTTP;
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLUMN_INDEX;
}
public function __construct()
{
$this->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);
});
}
}
@@ -0,0 +1,71 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Indexes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes\Delete as IndexDelete;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends IndexDelete
{
use HTTP;
/**
* 1. `SDKResponse` uses `UtopiaResponse::MODEL_NONE`.
* 2. But we later need the actual return type for events queue below!
*/
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLUMN_INDEX;
}
public function __construct()
{
$this->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);
});
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Indexes;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes\Get as IndexGet;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
class Get extends IndexGet
{
use HTTP;
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLUMN_INDEX;
}
public function __construct()
{
$this->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);
});
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Tables\Indexes;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes\XList as IndexXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Indexes;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
class XList extends IndexXList
{
use HTTP;
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLUMN_INDEX_LIST;
}
public function __construct()
{
$this->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);
});
}
}