mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
(fix): replace Metadata decorator with processDocument() in endpoint actions
The Metadata decorator required a collection ID mapping query that added overhead and caused Redis OOM in CI. Instead, stamp $databaseId and $collectionId/$tableId directly in endpoint actions where user-facing IDs are available from request parameters — matching the 1.9.x approach. - Remove Metadata decorator from getDatabasesDB hooks - Restore processDocument() on Documents/Action base class - Add processDocument() calls in Get, Create, XList, Update, Upsert - Simplify Metadata.php to stateless map-only decorator (no 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
33b971a2c1
commit
1eb9c386f6
+1
-36
@@ -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()));
|
||||
|
||||
+99
@@ -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<string, array<Document>> $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;
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -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
|
||||
|
||||
+16
-4
@@ -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());
|
||||
}
|
||||
|
||||
+10
@@ -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);
|
||||
|
||||
+10
@@ -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);
|
||||
|
||||
+16
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user