mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
(fix): load collection ID mapping in getDatabasesDB instead of decorator
Move the collection mapping query from inside the Metadata decorator to the getDatabasesDB resource factory. This avoids decorator-level database queries that can fail in various contexts and cause cascading issues. The mapping is loaded once when getDatabasesDB creates the database instance, and the results are passed to the Metadata decorator via setCollectionId(). The decorator itself is now stateless with respect to database queries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f8b3e687a0
commit
dca59528df
+29
-5
@@ -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()));
|
||||
|
||||
@@ -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<string, string> 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([
|
||||
|
||||
Reference in New Issue
Block a user