diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index a72aaf66a3..675268b894 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 a73d41268f..322787cbd0 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -835,35 +835,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; }