From d577fd129415500163f638b4d86f866488698639 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 17 May 2025 20:23:46 +1200 Subject: [PATCH 1/5] Sync 1.6.x --- app/config/storage/inputs.php | 1 - app/config/storage/outputs.php | 1 - app/controllers/api/databases.php | 595 +++++++++++----------- app/controllers/general.php | 28 +- app/controllers/shared/api.php | 4 +- app/init/resources.php | 7 +- app/worker.php | 12 +- src/Appwrite/Platform/Workers/Deletes.php | 50 +- src/Appwrite/Utopia/Request.php | 13 + 9 files changed, 344 insertions(+), 367 deletions(-) diff --git a/app/config/storage/inputs.php b/app/config/storage/inputs.php index 713801cd7c..edcf667d86 100644 --- a/app/config/storage/inputs.php +++ b/app/config/storage/inputs.php @@ -4,7 +4,6 @@ return [ // Accepted inputs files "jpg" => "image/jpeg", "jpeg" => "image/jpeg", - "gif" => "image/gif", "png" => "image/png", "heic" => "image/heic", "webp" => "image/webp", diff --git a/app/config/storage/outputs.php b/app/config/storage/outputs.php index 49548dda50..519ff825fe 100644 --- a/app/config/storage/outputs.php +++ b/app/config/storage/outputs.php @@ -4,7 +4,6 @@ return [ // Accepted outputs files "jpg" => "image/jpeg", "jpeg" => "image/jpeg", - "gif" => "image/gif", "png" => "image/png", "webp" => "image/webp", "heic" => "image/heic", diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 2e847ae11c..62ddd41daa 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -34,9 +34,9 @@ use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Order as OrderException; use Utopia\Database\Exception\Query as QueryException; +use Utopia\Database\Exception\Relationship as RelationshipException; use Utopia\Database\Exception\Restricted as RestrictedException; use Utopia\Database\Exception\Structure as StructureException; -use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; @@ -101,13 +101,13 @@ function createAttribute(string $databaseId, string $collectionId, Document $att $default = $attribute->getAttribute('default'); $options = $attribute->getAttribute('options', []); - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); - if ($db->isEmpty()) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -130,7 +130,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att if ($type === Database::VAR_RELATIONSHIP) { $options['side'] = Database::RELATION_SIDE_PARENT; - $relatedCollection = $dbForProject->getDocument('database_' . $db->getInternalId(), $options['relatedCollection'] ?? ''); + $relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection'] ?? ''); if ($relatedCollection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND, 'The related collection was not found.'); } @@ -138,10 +138,10 @@ function createAttribute(string $databaseId, string $collectionId, Document $att try { $attribute = new Document([ - '$id' => ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key), + '$id' => ID::custom($database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key), 'key' => $key, - 'databaseInternalId' => $db->getInternalId(), - 'databaseId' => $db->getId(), + 'databaseInternalId' => $database->getInternalId(), + 'databaseId' => $database->getId(), 'collectionInternalId' => $collection->getInternalId(), 'collectionId' => $collectionId, 'type' => $type, @@ -164,13 +164,13 @@ function createAttribute(string $databaseId, string $collectionId, Document $att } catch (LimitException) { throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); } catch (\Throwable $e) { - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId()); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId); + $dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); throw $e; } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId()); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId); + $dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) { $twoWayKey = $options['twoWayKey']; @@ -179,53 +179,58 @@ function createAttribute(string $databaseId, string $collectionId, Document $att $options['side'] = Database::RELATION_SIDE_CHILD; try { - $twoWayAttribute = new Document([ - '$id' => ID::custom($db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $twoWayKey), - 'key' => $twoWayKey, - 'databaseInternalId' => $db->getInternalId(), - 'databaseId' => $db->getId(), - 'collectionInternalId' => $relatedCollection->getInternalId(), - 'collectionId' => $relatedCollection->getId(), - 'type' => $type, - 'status' => 'processing', // processing, available, failed, deleting, stuck - 'size' => $size, - 'required' => $required, - 'signed' => $signed, - 'default' => $default, - 'array' => $array, - 'format' => $format, - 'formatOptions' => $formatOptions, - 'filters' => $filters, - 'options' => $options, - ]); + try { + $twoWayAttribute = new Document([ + '$id' => ID::custom($database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $twoWayKey), + 'key' => $twoWayKey, + 'databaseInternalId' => $database->getInternalId(), + 'databaseId' => $database->getId(), + 'collectionInternalId' => $relatedCollection->getInternalId(), + 'collectionId' => $relatedCollection->getId(), + 'type' => $type, + 'status' => 'processing', // processing, available, failed, deleting, stuck + 'size' => $size, + 'required' => $required, + 'signed' => $signed, + 'default' => $default, + 'array' => $array, + 'format' => $format, + 'formatOptions' => $formatOptions, + 'filters' => $filters, + 'options' => $options, + ]); - $dbForProject->checkAttribute($relatedCollection, $twoWayAttribute); - $dbForProject->createDocument('attributes', $twoWayAttribute); - } catch (DuplicateException) { - $dbForProject->deleteDocument('attributes', $attribute->getId()); - throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS); - } catch (LimitException) { - $dbForProject->deleteDocument('attributes', $attribute->getId()); - throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + $dbForProject->checkAttribute($relatedCollection, $twoWayAttribute); + $dbForProject->createDocument('attributes', $twoWayAttribute); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (LimitException) { + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } } catch (\Throwable $e) { - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId()); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); + $dbForProject->deleteDocument('attributes', $attribute->getId()); throw $e; + } finally { + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId); + $dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId()); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); + // If operation succeeded, purge the cache for the related collection too + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId()); + $dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); } $queueForDatabase ->setType(DATABASE_TYPE_CREATE_ATTRIBUTE) - ->setDatabase($db) + ->setDatabase($database) ->setCollection($collection) ->setDocument($attribute); $queueForEvents ->setContext('collection', $collection) - ->setContext('database', $db) + ->setContext('database', $database) ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setParam('attributeId', $attribute->getId()); @@ -252,20 +257,17 @@ function updateAttribute( array $options = [], string $newKey = null, ): Document { - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { + $database = $dbForProject->getDocument('databases', $databaseId); + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); - + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $attribute = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); - + $attribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); if ($attribute->isEmpty()) { throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); } @@ -290,7 +292,7 @@ function updateAttribute( throw new Exception(Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED, 'Cannot set default value for array attributes'); } - $collectionId = 'database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId(); + $collectionId = 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(); $attribute ->setAttribute('default', $default) @@ -365,14 +367,19 @@ function updateAttribute( newKey: $newKey, onDelete: $primaryDocumentOptions['onDelete'], ); - } catch (NotFoundException) { - throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); + } catch (IndexException) { + throw new Exception(Exception::INDEX_INVALID); + } catch (LimitException) { + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); } if ($primaryDocumentOptions['twoWay']) { - $relatedCollection = $dbForProject->getDocument('database_' . $db->getInternalId(), $primaryDocumentOptions['relatedCollection']); - - $relatedAttribute = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $primaryDocumentOptions['twoWayKey']); + $relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $primaryDocumentOptions['relatedCollection']); + $relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $primaryDocumentOptions['twoWayKey']); if (!empty($newKey) && $newKey !== $key) { $options['twoWayKey'] = $newKey; @@ -380,9 +387,10 @@ function updateAttribute( $relatedOptions = \array_merge($relatedAttribute->getAttribute('options'), $options); $relatedAttribute->setAttribute('options', $relatedOptions); - $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $primaryDocumentOptions['twoWayKey'], $relatedAttribute); - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId()); + + $dbForProject->updateDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $primaryDocumentOptions['twoWayKey'], $relatedAttribute); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId()); } } else { try { @@ -395,14 +403,14 @@ function updateAttribute( formatOptions: $options, newKey: $newKey ?? null ); - } catch (TruncateException) { - throw new Exception(Exception::ATTRIBUTE_INVALID_RESIZE); - } catch (NotFoundException) { - throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); - } catch (LimitException) { - throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); } catch (IndexException $e) { throw new Exception(Exception::INDEX_INVALID, $e->getMessage()); + } catch (LimitException) { + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + } catch (TruncateException) { + throw new Exception(Exception::ATTRIBUTE_INVALID_RESIZE); } } @@ -410,10 +418,14 @@ function updateAttribute( $originalUid = $attribute->getId(); $attribute - ->setAttribute('$id', ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $newKey)) + ->setAttribute('$id', ID::custom($database->getInternalId() . '_' . $collection->getInternalId() . '_' . $newKey)) ->setAttribute('key', $newKey); - $dbForProject->updateDocument('attributes', $originalUid, $attribute); + try { + $dbForProject->updateDocument('attributes', $originalUid, $attribute); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } /** * @var Document $index @@ -432,14 +444,14 @@ function updateAttribute( } } } else { - $attribute = $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key, $attribute); + $attribute = $dbForProject->updateDocument('attributes', $database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key, $attribute); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collection->getId()); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collection->getId()); $queueForEvents ->setContext('collection', $collection) - ->setContext('database', $db) + ->setContext('database', $database) ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setParam('attributeId', $attribute->getId()); @@ -487,10 +499,11 @@ App::post('/v1/databases') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('queueForStatsUsage') - ->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $queueForEvents) { - $databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId; + $databaseId = $databaseId === 'unique()' + ? ID::unique() + : $databaseId; try { $dbForProject->createDocument('databases', new Document([ @@ -499,42 +512,37 @@ App::post('/v1/databases') 'enabled' => $enabled, 'search' => implode(' ', [$databaseId, $name]), ])); - $database = $dbForProject->getDocument('databases', $databaseId); + } catch (DuplicateException) { + throw new Exception(Exception::DATABASE_ALREADY_EXISTS); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } - $collections = (Config::getParam('collections', [])['databases'] ?? [])['collections'] ?? []; - if (empty($collections)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'The "collections" collection is not configured.'); - } + $database = $dbForProject->getDocument('databases', $databaseId); - $attributes = []; - $indexes = []; + $collections = (Config::getParam('collections', [])['databases'] ?? [])['collections'] ?? []; + if (empty($collections)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'The "collections" collection is not configured.'); + } - foreach ($collections['attributes'] as $attribute) { - $attributes[] = new Document([ - '$id' => $attribute['$id'], - 'type' => $attribute['type'], - 'size' => $attribute['size'], - 'required' => $attribute['required'], - 'signed' => $attribute['signed'], - 'array' => $attribute['array'], - 'filters' => $attribute['filters'], - 'default' => $attribute['default'] ?? null, - 'format' => $attribute['format'] ?? '' - ]); - } + $attributes = []; + foreach ($collections['attributes'] as $attribute) { + $attributes[] = new Document($attribute); + } - foreach ($collections['indexes'] as $index) { - $indexes[] = new Document([ - '$id' => $index['$id'], - 'type' => $index['type'], - 'attributes' => $index['attributes'], - 'lengths' => $index['lengths'], - 'orders' => $index['orders'], - ]); - } + $indexes = []; + foreach ($collections['indexes'] as $index) { + $indexes[] = new Document($index); + } + + try { $dbForProject->createCollection('database_' . $database->getInternalId(), $attributes, $indexes); } catch (DuplicateException) { throw new Exception(Exception::DATABASE_ALREADY_EXISTS); + } catch (IndexException) { + throw new Exception(Exception::INDEX_INVALID); + } catch (LimitException) { + throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED); } $queueForEvents->setParam('databaseId', $database->getId()); @@ -580,6 +588,7 @@ App::get('/v1/databases') $cursor = \array_filter($queries, function ($query) { return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); }); + $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ @@ -590,8 +599,8 @@ App::get('/v1/databases') } $databaseId = $cursor->getValue(); - $cursorDocument = $dbForProject->getDocument('databases', $databaseId); + $cursorDocument = $dbForProject->getDocument('databases', $databaseId); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Database '{$databaseId}' for the 'cursor' value not found."); } @@ -599,14 +608,15 @@ App::get('/v1/databases') $cursor->setValue($cursorDocument); } - $filterQueries = Query::groupByType($queries)['filters']; - try { $databases = $dbForProject->find('databases', $queries); - $total = $dbForProject->count('databases', $filterQueries, APP_LIMIT_COUNT); - } catch (OrderException $e) { - throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + $total = $dbForProject->count('databases', $queries, APP_LIMIT_COUNT); + } catch (OrderException) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL); + } catch (QueryException) { + throw new Exception(Exception::GENERAL_QUERY_INVALID); } + $response->dynamic(new Document([ 'databases' => $databases, 'total' => $total, @@ -636,9 +646,7 @@ App::get('/v1/databases/:databaseId') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, Response $response, Database $dbForProject) { - $database = $dbForProject->getDocument('databases', $databaseId); - if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } @@ -672,9 +680,7 @@ App::get('/v1/databases/:databaseId/logs') ->inject('locale') ->inject('geodb') ->action(function (string $databaseId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { - $database = $dbForProject->getDocument('databases', $databaseId); - if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } @@ -776,17 +782,18 @@ App::put('/v1/databases/:databaseId') ->inject('dbForProject') ->inject('queueForEvents') ->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $queueForEvents) { - $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $database = $dbForProject->updateDocument('databases', $databaseId, $database + $database ->setAttribute('name', $name) ->setAttribute('enabled', $enabled) - ->setAttribute('search', implode(' ', [$databaseId, $name]))); + ->setAttribute('search', implode(' ', [$databaseId, $name])); + + $database = $dbForProject->updateDocument('databases', $databaseId, $database); $queueForEvents->setParam('databaseId', $database->getId()); @@ -820,9 +827,7 @@ App::delete('/v1/databases/:databaseId') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('queueForStatsUsage') - ->action(function (string $databaseId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, StatsUsage $queueForStatsUsage) { - + ->action(function (string $databaseId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { @@ -830,7 +835,7 @@ App::delete('/v1/databases/:databaseId') } if (!$dbForProject->deleteDocument('databases', $databaseId)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove collection from DB'); + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove database from database'); } $dbForProject->purgeCachedDocument('databases', $database->getId()); @@ -880,14 +885,15 @@ App::post('/v1/databases/:databaseId/collections') ->inject('mode') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collectionId = $collectionId == 'unique()' ? ID::unique() : $collectionId; + $collectionId = $collectionId === 'unique()' + ? ID::unique() + : $collectionId; // Map aggregate permissions into the multiple permissions they represent. $permissions = Permission::aggregate($permissions) ?? []; @@ -903,12 +909,26 @@ App::post('/v1/databases/:databaseId/collections') 'name' => $name, 'search' => implode(' ', [$collectionId, $name]), ])); - - $dbForProject->createCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), permissions: $permissions, documentSecurity: $documentSecurity); } catch (DuplicateException) { throw new Exception(Exception::COLLECTION_ALREADY_EXISTS); } catch (LimitException) { throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED); + } catch (NotFoundException) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + try { + $dbForProject->createCollection( + id: 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), + permissions: $permissions, + documentSecurity: $documentSecurity + ); + } catch (DuplicateException) { + throw new Exception(Exception::COLLECTION_ALREADY_EXISTS); + } catch (IndexException) { + throw new Exception(Exception::INDEX_INVALID); + } catch (LimitException) { + throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED); } $queueForEvents @@ -946,16 +966,18 @@ App::get('/v1/databases/:databaseId/collections') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('response') ->inject('dbForProject') - ->inject('mode') - ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject) { + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $queries = Query::parseQueries($queries); + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } if (!empty($search)) { $queries[] = Query::search('search', $search); @@ -977,6 +999,7 @@ App::get('/v1/databases/:databaseId/collections') } $collectionId = $cursor->getValue(); + $cursorDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($cursorDocument->isEmpty()) { @@ -986,14 +1009,17 @@ App::get('/v1/databases/:databaseId/collections') $cursor->setValue($cursorDocument); } - $filterQueries = Query::groupByType($queries)['filters']; + $collectionId = 'database_' . $database->getInternalId(); try { - $collections = $dbForProject->find('database_' . $database->getInternalId(), $queries); - $total = $dbForProject->count('database_' . $database->getInternalId(), $filterQueries, APP_LIMIT_COUNT); - } catch (OrderException $e) { - throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + $collections = $dbForProject->find($collectionId, $queries); + $total = $dbForProject->count($collectionId, $queries, APP_LIMIT_COUNT); + } catch (OrderException) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL); + } catch (QueryException) { + throw new Exception(Exception::GENERAL_QUERY_INVALID); } + $response->dynamic(new Document([ 'collections' => $collections, 'total' => $total, @@ -1024,10 +1050,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId') ->param('collectionId', '', new UID(), 'Collection ID.') ->inject('response') ->inject('dbForProject') - ->inject('mode') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, string $mode) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject) { + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1070,8 +1094,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs') ->inject('locale') ->inject('geodb') ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1080,7 +1103,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs') $collectionDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); $collection = $dbForProject->getCollection('database_' . $database->getInternalId() . '_collection_' . $collectionDocument->getInternalId()); - if ($collection->isEmpty()) { + if ($collectionDocument->isEmpty() || $collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } @@ -1186,8 +1209,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') ->inject('mode') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1206,15 +1228,17 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') $enabled ??= $collection->getAttribute('enabled', true); - $collection = $dbForProject->updateDocument( - 'database_' . $database->getInternalId(), - $collectionId, - $collection + $collection ->setAttribute('name', $name) ->setAttribute('$permissions', $permissions) ->setAttribute('documentSecurity', $documentSecurity) ->setAttribute('enabled', $enabled) - ->setAttribute('search', \implode(' ', [$collectionId, $name])) + ->setAttribute('search', \implode(' ', [$collectionId, $name])); + + $collection = $dbForProject->updateDocument( + 'database_' . $database->getInternalId(), + $collectionId, + $collection ); $dbForProject->updateCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $permissions, $documentSecurity); @@ -1256,10 +1280,8 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('mode') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, string $mode) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1326,7 +1348,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, bool $encrypt, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - // Ensure attribute default is within required size $validator = new Text($size, 0); if (!is_null($default) && !$validator->isValid($default)) { @@ -1334,7 +1355,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string } $filters = []; - if ($encrypt) { $filters[] = 'encrypt'; } @@ -1387,7 +1407,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_STRING, @@ -1490,7 +1509,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_STRING, @@ -1539,7 +1557,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_STRING, @@ -1590,7 +1607,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - // Ensure attribute default is within range $min ??= PHP_INT_MIN; $max ??= PHP_INT_MAX; @@ -1668,7 +1684,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - // Ensure attribute default is within range $min ??= -PHP_FLOAT_MAX; $max ??= PHP_FLOAT_MAX; @@ -1742,7 +1757,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_BOOLEAN, @@ -1790,7 +1804,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - $filters[] = 'datetime'; $attribute = createAttribute($databaseId, $collectionId, new Document([ @@ -1859,7 +1872,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati $key ??= $relatedCollectionId; $twoWayKey ??= $collectionId; - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1880,6 +1893,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati } $attributes = $collection->getAttribute('attributes', []); + /** @var Document[] $attributes */ foreach ($attributes as $attribute) { if ($attribute->getAttribute('type') !== Database::VAR_RELATIONSHIP) { @@ -1968,20 +1982,21 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) { - /** @var Document $database */ - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); - if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $queries = Query::parseQueries($queries); + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } \array_push( $queries, @@ -2005,26 +2020,31 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') } $attributeId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->find('attributes', [ - Query::equal('databaseInternalId', [$database->getInternalId()]), - Query::equal('collectionInternalId', [$collection->getInternalId()]), - Query::equal('key', [$attributeId]), - Query::limit(1), - ])); - if (empty($cursorDocument) || $cursorDocument[0]->isEmpty()) { + try { + $cursorDocument = $dbForProject->findOne('attributes', [ + Query::equal('databaseInternalId', [$database->getInternalId()]), + Query::equal('collectionInternalId', [$collection->getInternalId()]), + Query::equal('key', [$attributeId]), + ]); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Attribute '{$attributeId}' for the 'cursor' value not found."); } - $cursor->setValue($cursorDocument[0]); + $cursor->setValue($cursorDocument); } - $filters = Query::groupByType($queries)['filters']; try { $attributes = $dbForProject->find('attributes', $queries); - $total = $dbForProject->count('attributes', $filters, APP_LIMIT_COUNT); - } catch (OrderException $e) { - throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + $total = $dbForProject->count('attributes', $queries, APP_LIMIT_COUNT); + } catch (OrderException) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL); + } catch (QueryException) { + throw new Exception(Exception::GENERAL_QUERY_INVALID); } $response->dynamic(new Document([ @@ -2069,8 +2089,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2149,7 +2168,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/strin ->inject('dbForProject') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?int $size, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { - $attribute = updateAttribute( databaseId: $databaseId, collectionId: $collectionId, @@ -2683,21 +2701,20 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('queueForStatsUsage') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + $database = $dbForProject->getDocument('databases', $databaseId); - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); + + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $attribute = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); + $attribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); if ($attribute->isEmpty()) { throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); @@ -2720,19 +2737,19 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key $attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting')); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId()); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId); + $dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) { $options = $attribute->getAttribute('options'); if ($options['twoWay']) { - $relatedCollection = $dbForProject->getDocument('database_' . $db->getInternalId(), $options['relatedCollection']); + $relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']); if ($relatedCollection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $relatedAttribute = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']); + $relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']); if ($relatedAttribute->isEmpty()) { throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); @@ -2742,15 +2759,15 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key $dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'deleting')); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $options['relatedCollection']); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $options['relatedCollection']); + $dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); } } $queueForDatabase ->setType(DATABASE_TYPE_DELETE_ATTRIBUTE) ->setCollection($collection) - ->setDatabase($db) + ->setDatabase($database) ->setDocument($attribute); // Select response model based on type and format @@ -2778,7 +2795,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->setParam('collectionId', $collection->getId()) ->setParam('attributeId', $attribute->getId()) ->setContext('collection', $collection) - ->setContext('database', $db) + ->setContext('database', $database) ->setPayload($response->output($attribute, $model)); $response->noContent(); @@ -2819,31 +2836,31 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } + $limit = $dbForProject->getLimitForIndexes(); + $count = $dbForProject->count('indexes', [ Query::equal('collectionInternalId', [$collection->getInternalId()]), - Query::equal('databaseInternalId', [$db->getInternalId()]) - ], 61); + Query::equal('databaseInternalId', [$database->getInternalId()]) + ], max: $limit); - $limit = $dbForProject->getLimitForIndexes(); if ($count >= $limit) { throw new Exception(Exception::INDEX_LIMIT_EXCEEDED, 'Index limit exceeded'); } - // Convert Document[] to array of attribute metadata + // Convert Document array to array of attribute metadata $oldAttributes = \array_map(fn ($a) => $a->getArrayCopy(), $collection->getAttribute('attributes')); $oldAttributes[] = [ @@ -2855,7 +2872,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') 'default' => null, 'size' => Database::LENGTH_KEY ]; - $oldAttributes[] = [ 'key' => '$createdAt', 'type' => Database::VAR_DATETIME, @@ -2866,7 +2882,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') 'default' => null, 'size' => 0 ]; - $oldAttributes[] = [ 'key' => '$updatedAt', 'type' => Database::VAR_DATETIME, @@ -2879,7 +2894,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ]; foreach ($attributes as $i => $attribute) { - // find attribute metadata in collection document + // Find attribute metadata in collection document $attributeIndex = \array_search($attribute, array_column($oldAttributes, 'key')); if ($attributeIndex === false) { @@ -2907,10 +2922,10 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') } $index = new Document([ - '$id' => ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key), + '$id' => ID::custom($database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key), 'key' => $key, 'status' => 'processing', // processing, available, failed, deleting, stuck - 'databaseInternalId' => $db->getInternalId(), + 'databaseInternalId' => $database->getInternalId(), 'databaseId' => $databaseId, 'collectionInternalId' => $collection->getInternalId(), 'collectionId' => $collectionId, @@ -2925,6 +2940,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') $dbForProject->getAdapter()->getMaxIndexLength(), $dbForProject->getAdapter()->getInternalIndexesKeys(), ); + if (!$validator->isValid($index)) { throw new Exception(Exception::INDEX_INVALID, $validator->getDescription()); } @@ -2935,11 +2951,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') throw new Exception(Exception::INDEX_ALREADY_EXISTS); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId); $queueForDatabase ->setType(DATABASE_TYPE_CREATE_INDEX) - ->setDatabase($db) + ->setDatabase($database) ->setCollection($collection) ->setDocument($index); @@ -2948,7 +2964,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->setParam('collectionId', $collection->getId()) ->setParam('indexId', $index->getId()) ->setContext('collection', $collection) - ->setContext('database', $db); + ->setContext('database', $database); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) @@ -2981,8 +2997,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) { - /** @var Document $database */ - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -3008,6 +3023,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') $cursor = \array_filter($queries, function ($query) { return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); }); + $cursor = reset($cursor); if ($cursor) { @@ -3031,12 +3047,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') $cursor->setValue($cursorDocument[0]); } - $filterQueries = Query::groupByType($queries)['filters']; try { - $total = $dbForProject->count('indexes', $filterQueries, APP_LIMIT_COUNT); + $total = $dbForProject->count('indexes', $queries, APP_LIMIT_COUNT); $indexes = $dbForProject->find('indexes', $queries); } catch (OrderException $e) { throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } $response->dynamic(new Document([ @@ -3046,7 +3063,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') }); App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') - ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/indexes/:key') ->desc('Get index') ->groups(['api', 'database']) ->label('scope', 'collections.read') @@ -3071,12 +3088,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { @@ -3084,6 +3101,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') } $index = $collection->find('key', $key, 'indexes'); + if (empty($index)) { throw new Exception(Exception::INDEX_NOT_FOUND); } @@ -3093,7 +3111,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') - ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/indexes/:key') ->desc('Delete index') ->groups(['api', 'database']) ->label('scope', 'collections.write') @@ -3123,21 +3141,21 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + $database = $dbForProject->getDocument('databases', $databaseId); - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); + + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $index = $dbForProject->getDocument('indexes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); + $index = $dbForProject->getDocument('indexes', $database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); - if (empty($index->getId())) { + if ($index->isEmpty()) { throw new Exception(Exception::INDEX_NOT_FOUND); } @@ -3146,11 +3164,11 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') $index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting')); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId); $queueForDatabase ->setType(DATABASE_TYPE_DELETE_INDEX) - ->setDatabase($db) + ->setDatabase($database) ->setCollection($collection) ->setDocument($index); @@ -3159,14 +3177,14 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->setParam('collectionId', $collection->getId()) ->setParam('indexId', $index->getId()) ->setContext('collection', $collection) - ->setContext('database', $db) + ->setContext('database', $database) ->setPayload($response->output($index, Response::MODEL_INDEX)); $response->noContent(); }); App::post('/v1/databases/:databaseId/collections/:collectionId/documents') - ->alias('/v1/database/collections/:collectionId/documents', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/documents') ->desc('Create document') ->groups(['api', 'database']) ->label('scope', 'documents.write') @@ -3273,6 +3291,10 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + if ($isBulk && !$isAPIKey && !$isPrivilegedUser) { + throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE); + } + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -3459,16 +3481,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documents ); - } catch (StructureException $e) { - throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); } catch (DuplicateException) { throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); } catch (NotFoundException) { throw new Exception(Exception::COLLECTION_NOT_FOUND); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } catch (TimeoutException) { - throw new Exception(Exception::DATABASE_TIMEOUT); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); } $queueForEvents @@ -3541,7 +3561,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') }); App::get('/v1/databases/:databaseId/collections/:collectionId/documents') - ->alias('/v1/database/collections/:collectionId/documents', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/documents') ->desc('List documents') ->groups(['api', 'database']) ->label('scope', 'documents.read') @@ -3567,16 +3587,15 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->inject('dbForProject') ->inject('queueForStatsUsage') ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); - if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } @@ -3617,6 +3636,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') $total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, APP_LIMIT_COUNT); } catch (OrderException $e) { throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } $operations = 0; @@ -3718,7 +3739,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') }); App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') - ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Get document') ->groups(['api', 'database']) ->label('scope', 'documents.read') @@ -3745,29 +3766,26 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->inject('dbForProject') ->inject('queueForStatsUsage') ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); - if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } try { $queries = Query::parseQueries($queries); - $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId, $queries); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); } catch (QueryException $e) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } + $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId, $queries); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } @@ -3828,7 +3846,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen }); App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/logs') - ->alias('/v1/database/collections/:collectionId/documents/:documentId/logs', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/documents/:documentId/logs') ->desc('List document logs') ->groups(['api', 'database']) ->label('scope', 'documents.read') @@ -3856,15 +3874,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->inject('locale') ->inject('geodb') ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); - if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } @@ -3985,11 +4000,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum throw new Exception(Exception::DOCUMENT_MISSING_PAYLOAD); } - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } @@ -4000,9 +4014,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum } // Read permission should not be required for update - /** @var Document $document */ $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); - if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } @@ -4126,14 +4138,14 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $document->getId(), $newDocument ); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); } catch (DuplicateException) { throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); } catch (StructureException $e) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); - } catch (NotFoundException $e) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); } // Add $collectionId and $databaseId for all documents @@ -4277,14 +4289,12 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents') } }, ); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); } catch (StructureException $e) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); - } catch (NotFoundException) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } catch (TimeoutException) { - throw new Exception(Exception::DATABASE_TIMEOUT); } foreach ($documents as $document) { @@ -4359,15 +4369,25 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents') $upserted = []; - $modified = $dbForProject->createOrUpdateDocuments( - 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), - $documents, - onNext: function (Document $document) use ($plan, &$upserted) { - if (\count($upserted) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) { - $upserted[] = $document; - } - }, - ); + try { + $modified = $dbForProject->createOrUpdateDocuments( + 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), + $documents, + onNext: function (Document $document) use ($plan, &$upserted) { + if (\count($upserted) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) { + $upserted[] = $document; + } + }, + ); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } foreach ($upserted as $document) { $document->setAttribute('$databaseId', $database->getId()); @@ -4385,7 +4405,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents') }); App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') - ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Delete document') ->groups(['api', 'database']) ->label('scope', 'documents.write') @@ -4419,24 +4439,21 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->inject('queueForEvents') ->inject('queueForStatsUsage') ->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); - if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } // Read permission should not be required for delete $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); - if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } @@ -4446,8 +4463,10 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId ); - } catch (NotFoundException $e) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (RestrictedException) { + throw new Exception(Exception::DOCUMENT_DELETE_RESTRICTED); } $operations = 0; @@ -4582,12 +4601,10 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents') } }, ); - } catch (NotFoundException) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } catch (TimeoutException) { - throw new Exception(Exception::DATABASE_TIMEOUT); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (RestrictedException) { + throw new Exception(Exception::DOCUMENT_DELETE_RESTRICTED); } foreach ($documents as $document) { @@ -4628,7 +4645,6 @@ App::get('/v1/databases/usage') ->inject('response') ->inject('dbForProject') ->action(function (string $range, Response $response, Database $dbForProject) { - $periods = Config::getParam('usage', []); $stats = $usage = []; $days = $periods[$range]; @@ -4725,9 +4741,7 @@ App::get('/v1/databases/:databaseId/usage') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $range, Response $response, Database $dbForProject) { - $database = $dbForProject->getDocument('databases', $databaseId); - if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } @@ -4803,7 +4817,7 @@ App::get('/v1/databases/:databaseId/usage') }); App::get('/v1/databases/:databaseId/collections/:collectionId/usage') - ->alias('/v1/database/:collectionId/usage', ['databaseId' => 'default']) + ->alias('/v1/database/:collectionId/usage') ->desc('Get collection usage stats') ->groups(['api', 'database', 'usage']) ->label('scope', 'collections.read') @@ -4828,7 +4842,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $range, string $collectionId, Response $response, Database $dbForProject) { - $database = $dbForProject->getDocument('databases', $databaseId); $collectionDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); $collection = $dbForProject->getCollection('database_' . $database->getInternalId() . '_collection_' . $collectionDocument->getInternalId()); diff --git a/app/controllers/general.php b/app/controllers/general.php index dbc07f950b..13433b98c8 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1160,35 +1160,11 @@ App::error() break; } break; - case 'Utopia\Database\Exception\Conflict': - $error = new AppwriteException(AppwriteException::DOCUMENT_UPDATE_CONFLICT, previous: $error); - break; - case 'Utopia\Database\Exception\Timeout': - $error = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $error); - break; - case 'Utopia\Database\Exception\Query': - $error = new AppwriteException(AppwriteException::GENERAL_QUERY_INVALID, $error->getMessage(), previous: $error); - break; - case 'Utopia\Database\Exception\Structure': - $error = new AppwriteException(AppwriteException::DOCUMENT_INVALID_STRUCTURE, $error->getMessage(), previous: $error); - break; - case 'Utopia\Database\Exception\Duplicate': - $error = new AppwriteException(AppwriteException::DOCUMENT_ALREADY_EXISTS); - break; - case 'Utopia\Database\Exception\Restricted': - $error = new AppwriteException(AppwriteException::DOCUMENT_DELETE_RESTRICTED); - break; case 'Utopia\Database\Exception\Authorization': $error = new AppwriteException(AppwriteException::USER_UNAUTHORIZED); break; - case 'Utopia\Database\Exception\Relationship': - $error = new AppwriteException(AppwriteException::RELATIONSHIP_VALUE_INVALID, $error->getMessage(), previous: $error); - break; - case 'Utopia\Database\Exception\NotFound': - $error = new AppwriteException(AppwriteException::COLLECTION_NOT_FOUND, $error->getMessage(), previous: $error); - break; - case 'Utopia\Database\Exception\Dependency': - $error = new AppwriteException(AppwriteException::INDEX_DEPENDENCY, null, previous: $error); + case 'Utopia\Database\Exception\Timeout': + $error = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $error); break; } diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 94eaa2c916..937f245099 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -558,7 +558,7 @@ App::init() $isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview'; $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !Auth::isPrivilegedUser(Authorization::getRoles()); - $key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); + $key = $request->cacheIdentifier(); $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); $cache = new Cache( new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId()) @@ -824,7 +824,7 @@ App::shutdown() $resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user); } - $key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); + $key = $request->cacheIdentifier(); $signature = md5($data['payload']); $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); $accessedAt = $cacheLog->getAttribute('accessedAt', 0); diff --git a/app/init/resources.php b/app/init/resources.php index fdbb426458..5083a38332 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -509,9 +509,9 @@ App::setResource('timelimit', function (\Redis $redis) { }; }, ['redis']); -App::setResource('deviceForLocal', function (Telemetry $telemetry) { - return new Device\Telemetry($telemetry, new Local()); -}, ['telemetry']); +App::setResource('deviceForLocal', function () { + return new Local(); +}); App::setResource('deviceForFiles', function ($project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); @@ -525,7 +525,6 @@ App::setResource('deviceForImports', function ($project, Telemetry $telemetry) { App::setResource('deviceForFunctions', function ($project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); }, ['project', 'telemetry']); - App::setResource('deviceForBuilds', function ($project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); }, ['project', 'telemetry']); diff --git a/app/worker.php b/app/worker.php index ad0e92bf6a..1ffb3165ff 100644 --- a/app/worker.php +++ b/app/worker.php @@ -37,10 +37,7 @@ use Utopia\Queue\Message; use Utopia\Queue\Publisher; use Utopia\Queue\Server; use Utopia\Registry\Registry; -use Utopia\Storage\Device; use Utopia\System\System; -use Utopia\Telemetry\Adapter as Telemetry; -use Utopia\Telemetry\Adapter\None as NoTelemetry; Authorization::disable(); Runtime::enableCoroutine(SWOOLE_HOOK_ALL); @@ -337,7 +334,6 @@ Server::setResource('pools', function (Registry $register) { return $register->get('pools'); }, ['register']); -Server::setResource('telemetry', fn () => new NoTelemetry()); Server::setResource('deviceForSites', function (Document $project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); @@ -351,15 +347,11 @@ Server::setResource('deviceForFunctions', function (Document $project, Telemetry return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); }, ['project', 'telemetry']); -Server::setResource('deviceForFiles', function (Document $project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); - Server::setResource('deviceForBuilds', function (Document $project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); + return new Device\Telemetry($telemetry getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); }, ['project', 'telemetry']); -Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry) { +Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry ) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId())); }, ['project', 'telemetry']); diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index f8f7be95f8..12143b4106 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -515,47 +515,33 @@ class Deletes extends Action AbuseDatabase::COLLECTION, ]; - $limit = \count($projectCollectionIds) + 25; - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); $sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', '')); $projectTables = !\in_array($dsn->getHost(), $sharedTables); $sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1); $sharedTablesV2 = !$projectTables && !$sharedTablesV1; - $sharedTables = $sharedTablesV1 || $sharedTablesV2; - while (true) { - $collections = $dbForProject->listCollections($limit); - - foreach ($collections as $collection) { - try { - if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { - $dbForProject->deleteCollection($collection->getId()); - } else { - $this->deleteByGroup( - $collection->getId(), - [ - Query::orderAsc() - ], - database: $dbForProject - ); - } - } catch (Throwable $e) { - Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); + /** + * @var $dbForProject Database + */ + $dbForProject->foreach(Database::METADATA, function (Document $collection) use ($dbForProject, $projectTables, $projectCollectionIds) { + try { + if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { + $dbForProject->deleteCollection($collection->getId()); + } else { + $this->deleteByGroup( + $collection->getId(), + [ + Query::orderAsc() + ], + database: $dbForProject + ); } + } catch (Throwable $e) { + Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); } - - if ($sharedTables) { - $collectionsIds = \array_map(fn ($collection) => $collection->getId(), $collections); - - if (empty(\array_diff($collectionsIds, $projectCollectionIds))) { - break; - } - } elseif (empty($collections)) { - break; - } - } + }); // Delete Platforms $this->deleteByGroup('platforms', [ diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index c50dea2713..558f0cdf09 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -220,4 +220,17 @@ class Request extends UtopiaRequest return UtopiaRequest::getUserAgent($default); } + + /** + * Creates a unique stable cache identifier for this GET request. + * Stable-sorts query params, use `serialize` to ensure key&value are part of cache keys. + * + * @return string + */ + public function cacheIdentifier(): string + { + $params = $this->getParams(); + ksort($params); + return md5($this->getURI() . '*' . serialize($params) . '*' . APP_CACHE_BUSTER); + } } From b41dfb49b490590486187bbcf0ac6d7e887ff628 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 17 May 2025 20:27:45 +1200 Subject: [PATCH 2/5] Fix merge --- app/worker.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/worker.php b/app/worker.php index 1ffb3165ff..b06f9b7eae 100644 --- a/app/worker.php +++ b/app/worker.php @@ -348,10 +348,10 @@ Server::setResource('deviceForFunctions', function (Document $project, Telemetry }, ['project', 'telemetry']); Server::setResource('deviceForBuilds', function (Document $project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); + return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); }, ['project', 'telemetry']); -Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry ) { +Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId())); }, ['project', 'telemetry']); From 678578ad64451720905bc79b36f32e6bfe6f3ea9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 17 May 2025 20:33:32 +1200 Subject: [PATCH 3/5] Fix merge --- app/worker.php | 16 +++++++++++----- composer.lock | 12 ++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/worker.php b/app/worker.php index b06f9b7eae..b342d4c7af 100644 --- a/app/worker.php +++ b/app/worker.php @@ -37,7 +37,9 @@ use Utopia\Queue\Message; use Utopia\Queue\Publisher; use Utopia\Queue\Server; use Utopia\Registry\Registry; +use Utopia\Storage\Device\Telemetry as TelemetryDevice; use Utopia\System\System; +use Utopia\Telemetry\Adapter as Telemetry; Authorization::disable(); Runtime::enableCoroutine(SWOOLE_HOOK_ALL); @@ -336,23 +338,27 @@ Server::setResource('pools', function (Registry $register) { Server::setResource('deviceForSites', function (Document $project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); + return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); }, ['project', 'telemetry']); Server::setResource('deviceForImports', function (Document $project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); + return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); }, ['project', 'telemetry']); Server::setResource('deviceForFunctions', function (Document $project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); + return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); +}, ['project', 'telemetry']); + +Server::setResource('deviceForFiles', function (Document $project, Telemetry $telemetry) { + return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); }, ['project', 'telemetry']); Server::setResource('deviceForBuilds', function (Document $project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); + return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); }, ['project', 'telemetry']); Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId())); + return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId())); }, ['project', 'telemetry']); Server::setResource( diff --git a/composer.lock b/composer.lock index 9873511697..a495c50034 100644 --- a/composer.lock +++ b/composer.lock @@ -3499,16 +3499,16 @@ }, { "name": "utopia-php/database", - "version": "0.69.4", + "version": "0.69.5", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "31f913a9c5c363e6427e69a0b8bbb9b8d901e061" + "reference": "4abe53609dfc23b2ea82884d12b149df6a8af2f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/31f913a9c5c363e6427e69a0b8bbb9b8d901e061", - "reference": "31f913a9c5c363e6427e69a0b8bbb9b8d901e061", + "url": "https://api.github.com/repos/utopia-php/database/zipball/4abe53609dfc23b2ea82884d12b149df6a8af2f5", + "reference": "4abe53609dfc23b2ea82884d12b149df6a8af2f5", "shasum": "" }, "require": { @@ -3549,9 +3549,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.69.4" + "source": "https://github.com/utopia-php/database/tree/0.69.5" }, - "time": "2025-05-16T13:51:43+00:00" + "time": "2025-05-17T08:01:51+00:00" }, { "name": "utopia-php/detector", From a96f770e844c143b25d000f1c5519070498b91c4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 17 May 2025 22:43:32 +1200 Subject: [PATCH 4/5] Fix merge --- app/init/resources.php | 6 +++--- app/worker.php | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index 5083a38332..2e6b8bc78e 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -509,9 +509,9 @@ App::setResource('timelimit', function (\Redis $redis) { }; }, ['redis']); -App::setResource('deviceForLocal', function () { - return new Local(); -}); +App::setResource('deviceForFiles', function (Telemetry $telemetry) { + return new Device\Telemetry($telemetry, new Local()); +}, ['telemetry']); App::setResource('deviceForFiles', function ($project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); diff --git a/app/worker.php b/app/worker.php index b342d4c7af..dfa645478b 100644 --- a/app/worker.php +++ b/app/worker.php @@ -40,6 +40,7 @@ use Utopia\Registry\Registry; use Utopia\Storage\Device\Telemetry as TelemetryDevice; use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; +use Utopia\Telemetry\Adapter\None as NoTelemetry; Authorization::disable(); Runtime::enableCoroutine(SWOOLE_HOOK_ALL); @@ -336,6 +337,7 @@ Server::setResource('pools', function (Registry $register) { return $register->get('pools'); }, ['register']); +Server::setResource('telemetry', fn () => new NoTelemetry()); Server::setResource('deviceForSites', function (Document $project, Telemetry $telemetry) { return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); From a4845661b90de2950031bb3151037b0a9f60aa9c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 17 May 2025 22:50:30 +1200 Subject: [PATCH 5/5] Fix merge --- app/init/resources.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/init/resources.php b/app/init/resources.php index 2e6b8bc78e..2f7c17ab7f 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -509,7 +509,7 @@ App::setResource('timelimit', function (\Redis $redis) { }; }, ['redis']); -App::setResource('deviceForFiles', function (Telemetry $telemetry) { +App::setResource('deviceForLocal', function (Telemetry $telemetry) { return new Device\Telemetry($telemetry, new Local()); }, ['telemetry']);