diff --git a/app/init/resources.php b/app/init/resources.php index 5a9d4e6d26..0c77d935f7 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -30,7 +30,6 @@ use Appwrite\Usage\Context as UsageContext; use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Database\Hooks\DocumentUsage; use Appwrite\Utopia\Database\Hooks\FunctionCache; -use Appwrite\Utopia\Database\Hooks\Metadata; use Appwrite\Utopia\Database\Hooks\Usage; use Appwrite\Utopia\Database\Hooks\UserEvents; use Appwrite\Utopia\Request; @@ -769,39 +768,6 @@ Http::setResource('getDatabasesDB', function (Group $pools, Database $dbForProje $databaseIdCollectionIdDocumentsMetric = $databaseType . '.' . $databaseIdCollectionIdDocumentsMetric; } - // Pre-compute collection ID mapping (internal name → user-facing ID) - $collectionIdMap = []; - try { - $databaseKey = 'database_' . $originalDatabase->getSequence(); - $dbPrefix = $databaseKey . '_'; - $collections = $authorization->skip( - fn () => $dbForProject->silent( - fn () => $dbForProject->find($databaseKey, [ - \Utopia\Database\Query::select(['$id', '$sequence']), - \Utopia\Database\Query::limit(5000), - ]) - ) - ); - foreach ($collections as $col) { - $seq = $col->getSequence(); - if ($seq !== null) { - $key = 'collection_' . $seq; - $collectionIdMap[$key] = $col->getId(); - $collectionIdMap[$dbPrefix . $key] = $col->getId(); - } - } - } catch (\Throwable) { - // Database may not have collections yet - } - - $metadata = new Metadata( - database: $originalDatabase, - context: $context, - ); - foreach ($collectionIdMap as $internal => $external) { - $metadata->setCollectionId($internal, $external); - } - $database ->addHook(new DocumentUsage( $usage, @@ -810,8 +776,7 @@ Http::setResource('getDatabasesDB', function (Group $pools, Database $dbForProje $databaseIdCollectionIdDocumentsMetric, )) ->addHook(new Permissions()) - ->addHook(new Relationships($database)) - ->addHook($metadata); + ->addHook(new Relationships($database)); if ($database->getSharedTables() && ($database->getTenant() !== null)) { $database->addHook(new Tenancy($database->getTenant())); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index 33fcfc1520..1ae0130927 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -401,4 +401,103 @@ abstract class Action extends DatabasesAction $queueForFunctions->reset(); $queueForWebhooks->reset(); } + + /** + * Stamp database/collection metadata onto a document and recursively + * process relationship documents. Called from endpoint actions where + * the user-facing collection ID is available. + * + * @param array> $collectionsCache + */ + protected function processDocument( + Document $database, + Document $collection, + Document $document, + Database $dbForProject, + array &$collectionsCache, + \Utopia\Database\Validator\Authorization $authorization, + ?int &$operations = null, + int $depth = 0, + ): bool { + if ($operations !== null && $document->isEmpty()) { + return false; + } + + if ($operations !== null) { + $operations++; + } + + $collectionId = $collection->getId(); + $document->removeAttribute('$collection'); + $document->setAttribute('$databaseId', $database->getId()); + $document->setAttribute('$' . $this->getCollectionsEventsContext() . 'Id', $collectionId); + + if ($depth >= Database::RELATION_MAX_DEPTH) { + return true; + } + + $relationships = $collectionsCache[$collectionId] ??= \array_filter( + $collection->getAttribute('attributes', []), + fn ($attr) => $attr->getAttribute('type') === \Utopia\Query\Schema\ColumnType::Relationship->value + ); + + foreach ($relationships as $relationship) { + $key = $relationship->getAttribute('key'); + $related = $document->getAttribute($key); + + if (empty($related)) { + if (\in_array(\gettype($related), ['array', 'object']) && $operations !== null) { + $operations++; + } + continue; + } + + $relations = \is_array($related) ? $related : [$related]; + $options = $relationship->getAttribute('options', []); + $relatedCollectionId = (\is_array($options) ? ($options['relatedCollection'] ?? null) : null) + ?? $relationship->getAttribute('relatedCollection'); + + if (!isset($collectionsCache[$relatedCollectionId])) { + $relatedCollectionDoc = $authorization->skip( + fn () => $dbForProject->getDocument( + 'database_' . $database->getSequence(), + $relatedCollectionId + ) + ); + + $collectionsCache[$relatedCollectionId] = \array_filter( + $relatedCollectionDoc->getAttribute('attributes', []), + fn ($attr) => $attr->getAttribute('type') === \Utopia\Query\Schema\ColumnType::Relationship->value + ); + } + + foreach ($relations as $relation) { + if ($relation instanceof Document) { + $relatedCollection = new Document([ + '$id' => $relatedCollectionId, + 'attributes' => $collectionsCache[$relatedCollectionId], + ]); + + $this->processDocument( + database: $database, + collection: $relatedCollection, + document: $relation, + dbForProject: $dbForProject, + collectionsCache: $collectionsCache, + authorization: $authorization, + operations: $operations, + depth: $depth + 1 + ); + } + } + + if (\is_array($related)) { + $document->setAttribute($relationship->getAttribute('key'), \array_values($relations)); + } elseif (empty($relations)) { + $document->setAttribute($relationship->getAttribute('key'), null); + } + } + + return true; + } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 478834f656..413f700286 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -389,6 +389,18 @@ class Create extends Action ->setParam('tableId', $collection->getId()) ->setContext($this->getCollectionsEventsContext(), $collection); + $collectionsCache = []; + foreach ($created as $document) { + $this->processDocument( + database: $database, + collection: $collection, + document: $document, + dbForProject: $dbForProject, + collectionsCache: $collectionsCache, + authorization: $authorization + ); + } + $usage ->addMetric($this->getDatabasesOperationWriteMetric(), 1) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1); // per collection diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php index c82910d217..8b7b3021e6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php @@ -122,11 +122,23 @@ class Get extends Action throw new Exception($this->getNotFoundException(), params: [$documentId]); } - $usage - ->addMetric($this->getDatabasesOperationReadMetric(), 1) - ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationReadMetric()), 1); + $operations = 0; + $collectionsCache = []; + $this->processDocument( + database: $database, + collection: $collection, + document: $document, + dbForProject: $dbForProject, + collectionsCache: $collectionsCache, + authorization: $authorization, + operations: $operations + ); - $response->addHeader('X-Debug-Operations', 1); + $usage + ->addMetric($this->getDatabasesOperationReadMetric(), max($operations, 1)) + ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationReadMetric()), $operations); + + $response->addHeader('X-Debug-Operations', $operations); $response->dynamic($document, $this->getResponseModel()); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php index fa35089022..5115fb28dc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php @@ -262,6 +262,16 @@ class Update extends Action throw new Exception($this->getStructureException(), $e->getMessage()); } + $collectionsCache = []; + $this->processDocument( + database: $database, + collection: $collection, + document: $document, + dbForProject: $dbForProject, + collectionsCache: $collectionsCache, + authorization: $authorization, + ); + $usage ->addMetric($this->getDatabasesOperationWriteMetric(), 1) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php index c95206c58b..d216e72ca3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php @@ -283,6 +283,16 @@ class Upsert extends Action $document = $upserted[0]; + $collectionsCache = []; + $this->processDocument( + database: $database, + collection: $collection, + document: $document, + dbForProject: $dbForProject, + collectionsCache: $collectionsCache, + authorization: $authorization + ); + $usage ->addMetric($this->getDatabasesOperationWriteMetric(), 1) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index eca66714c7..e54fbaf15d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -218,9 +218,23 @@ class XList extends Action throw new Exception(Exception::DATABASE_TIMEOUT); } + $operations = 0; + $collectionsCache = []; + foreach ($documents as $document) { + $this->processDocument( + database: $database, + collection: $collection, + document: $document, + dbForProject: $dbForProject, + collectionsCache: $collectionsCache, + authorization: $authorization, + operations: $operations + ); + } + $usage - ->addMetric($this->getDatabasesOperationReadMetric(), 1) - ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationReadMetric()), 1); + ->addMetric($this->getDatabasesOperationReadMetric(), max($operations, 1)) + ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationReadMetric()), $operations); $response->dynamic(new Document([ 'total' => $total,