update: move Index ops to module.

This commit is contained in:
Darshan 2025-05-04 17:16:37 +05:30
parent 8efc1543e9
commit 3271ef5769
5 changed files with 535 additions and 389 deletions

View file

@ -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')

View file

@ -0,0 +1,217 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Indexes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
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\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
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\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
class Create extends Action
{
use HTTP;
public static function getName(): string
{
return 'createIndex';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Indexes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
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\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;
class Delete extends Action
{
use HTTP;
public static function getName(): string
{
return 'deleteIndex';
}
public function __construct()
{
$this
->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();
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Indexes;
use Appwrite\Extend\Exception;
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\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;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getIndex';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -0,0 +1,129 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Indexes;
use Appwrite\Extend\Exception;
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\Document;
use Utopia\Database\Exception\Order as OrderException;
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;
class XList extends Action
{
use HTTP;
public static function getName(): string
{
return 'listIndexes';
}
public function __construct()
{
$this
->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);
}
}