diff --git a/app/init/resources.php b/app/init/resources.php index 4f2e658060..9a5e543e9e 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -769,6 +769,34 @@ Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Docume $databaseIdCollectionIdDocumentsMetric = $databaseType . '.' . $databaseIdCollectionIdDocumentsMetric; } + // Build collection ID mapping (internal name → user-facing ID) + $databaseKey = 'database_' . $originalDatabase->getSequence(); + $collectionIdMap = []; + try { + $collections = $authorization->skip( + 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) { + $collectionIdMap['collection_' . $seq] = $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, @@ -778,11 +806,7 @@ Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Docume )) ->addHook(new Permissions()) ->addHook(new Relationships($database)) - ->addHook(new Metadata( - database: $originalDatabase, - context: $context, - authorization: $authorization, - )); + ->addHook($metadata); if ($database->getSharedTables() && ($database->getTenant() !== null)) { $database->addHook(new Tenancy($database->getTenant())); diff --git a/src/Appwrite/Utopia/Database/Hooks/Metadata.php b/src/Appwrite/Utopia/Database/Hooks/Metadata.php index 90bdae32f2..cfbe3d9d4d 100644 --- a/src/Appwrite/Utopia/Database/Hooks/Metadata.php +++ b/src/Appwrite/Utopia/Database/Hooks/Metadata.php @@ -6,7 +6,6 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Event; use Utopia\Database\Hook\Decorator; -use Utopia\Database\Validator\Authorization; use Utopia\Query\Schema\ColumnType; /** @@ -23,18 +22,22 @@ class Metadata implements Decorator /** @var array internal collection name → user-facing collection ID */ private array $collectionIdMap = []; - private bool $mapLoaded = false; - private int $operations = 0; public function __construct( private Document $database, private string $context = 'collection', - private ?Database $dbForProject = null, - private ?Authorization $authorization = null, ) { } + /** + * Register a mapping from internal collection name to user-facing collection ID. + */ + public function setCollectionId(string $internalName, string $externalId): void + { + $this->collectionIdMap[$internalName] = $externalId; + } + public function decorate(Event $event, Document $collection, Document $document): Document { if ($document->isEmpty() || $collection->getId() === '_metadata') { @@ -43,7 +46,7 @@ class Metadata implements Decorator $this->operations++; - $collectionId = $this->resolveCollectionId($collection->getId()); + $collectionId = $this->collectionIdMap[$collection->getId()] ?? $collection->getId(); $document->setAttribute('$databaseId', $this->database->getId()); $document->setAttribute('$' . $this->context . 'Id', $collectionId); @@ -68,58 +71,6 @@ class Metadata implements Decorator $this->operations = 0; } - /** - * Resolve an internal collection name (e.g. 'collection_5') to the user-facing collection ID. - * Loads all mappings from the database in a single query on first access. - */ - private function resolveCollectionId(string $internalName): string - { - if (isset($this->collectionIdMap[$internalName])) { - return $this->collectionIdMap[$internalName]; - } - - if (!$this->mapLoaded) { - $this->loadCollectionMap(); - $this->mapLoaded = true; - - if (isset($this->collectionIdMap[$internalName])) { - return $this->collectionIdMap[$internalName]; - } - } - - return $internalName; - } - - /** - * Load all collection ID mappings from the Appwrite database metadata collection in one query. - */ - private function loadCollectionMap(): void - { - if ($this->dbForProject === null || $this->authorization === null) { - return; - } - - $databaseKey = 'database_' . $this->database->getSequence(); - - try { - $collections = $this->authorization->skip( - fn () => $this->dbForProject->find($databaseKey, [ - \Utopia\Database\Query::select(['$id', '$sequence']), - \Utopia\Database\Query::limit(5000), - ]) - ); - - foreach ($collections as $collection) { - $sequence = $collection->getSequence(); - if ($sequence !== null) { - $this->collectionIdMap['collection_' . $sequence] = $collection->getId(); - } - } - } catch (\Throwable) { - // Silently fail — fall back to internal names - } - } - private function decorateRelationships(Document $collection, Document $document, int $depth = 0): void { if ($depth >= Database::RELATION_MAX_DEPTH) { @@ -144,7 +95,7 @@ class Metadata implements Decorator $options = $relationship->getAttribute('options', []); $relatedInternalName = (\is_array($options) ? ($options['relatedCollection'] ?? null) : null) ?? $relationship->getAttribute('relatedCollection'); - $relatedExternalId = $this->resolveCollectionId($relatedInternalName); + $relatedExternalId = $this->collectionIdMap[$relatedInternalName] ?? $relatedInternalName; foreach ($relations as $relation) { if ($relation instanceof Document) { @@ -152,7 +103,7 @@ class Metadata implements Decorator $relation->setAttribute('$databaseId', $this->database->getId()); $relation->setAttribute('$' . $this->context . 'Id', $relatedExternalId); - $relatedCollection = $this->getRelatedCollection($relatedInternalName); + $relatedCollection = $this->getRelatedCollection($relatedInternalName, $collection); $this->decorateRelationships($relatedCollection, $relation, $depth + 1); } } @@ -174,25 +125,15 @@ class Metadata implements Decorator return $this->relationshipCache[$collectionId]; } - private function getRelatedCollection(string $internalName): Document + /** + * Build a minimal collection document for decorating nested relationships. + * Uses attributes from the parent collection's relationship definition. + */ + private function getRelatedCollection(string $internalName, Document $parentCollection): Document { - if (!isset($this->relationshipCache[$internalName]) && $this->dbForProject !== null && $this->authorization !== null) { - $relatedExternalId = $this->resolveCollectionId($internalName); - try { - $relatedCollection = $this->authorization->skip( - fn () => $this->dbForProject->getDocument( - 'database_' . $this->database->getSequence(), - $relatedExternalId - ) - ); - - $this->relationshipCache[$internalName] = \array_filter( - $relatedCollection->getAttribute('attributes', []), - fn ($attr) => $attr->getAttribute('type') === ColumnType::Relationship->value - ); - } catch (\Throwable) { - $this->relationshipCache[$internalName] = []; - } + if (!isset($this->relationshipCache[$internalName])) { + // Try to find relationship attributes from the parent's known relationships + $this->relationshipCache[$internalName] = []; } return new Document([