mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
extract prepareListContext helper so explain reuses listRows setup
This commit is contained in:
+105
@@ -8,10 +8,15 @@ use Appwrite\Event\Publisher\Func as FunctionPublisher;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Action as DatabasesAction;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\NotFound as NotFoundException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
|
||||
abstract class Action extends DatabasesAction
|
||||
{
|
||||
@@ -504,4 +509,104 @@ abstract class Action extends DatabasesAction
|
||||
$queueForRealtime->reset();
|
||||
$queueForWebhooks->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared setup for any list-style read on the documents/rows API surface.
|
||||
*
|
||||
* Both listRows/listDocuments and explainRows/explainDocuments need the
|
||||
* same auth checks, database+collection lookup, query parse, cursor
|
||||
* resolution, and find-closure construction. Centralising it here is the
|
||||
* single source of truth so the explain endpoint stays byte-identical to
|
||||
* the real read it's explaining.
|
||||
*
|
||||
* Returned bundle:
|
||||
* 'database' Document the user-facing database doc
|
||||
* 'collection' Document the user-facing collection doc
|
||||
* 'dbForDatabases' Database the per-database adapter the read runs against
|
||||
* 'queries' Query[] parsed Query objects (cursor value resolved)
|
||||
* 'collectionTableId' string physical table id (`database_X_collection_Y`)
|
||||
* 'hasSelects' bool whether the queries include any select
|
||||
* 'find' callable closure that runs the find — wraps in skipRelationships when there are no related selects
|
||||
*
|
||||
* @param array<string> $queries raw stringified queries from the HTTP request
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function prepareListContext(
|
||||
string $databaseId,
|
||||
string $collectionId,
|
||||
array $queries,
|
||||
Database $dbForProject,
|
||||
User $user,
|
||||
callable $getDatabasesDB,
|
||||
Authorization $authorization,
|
||||
): array {
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($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, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty() || (! $collection->getAttribute('enabled', false) && ! $isAPIKey && ! $isPrivilegedUser)) {
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$dbForDatabases = $getDatabasesDB($database);
|
||||
|
||||
$cursor = Query::getCursorQueries($queries, false);
|
||||
$cursor = \reset($cursor);
|
||||
|
||||
if ($cursor !== false) {
|
||||
$validator = new Cursor;
|
||||
if (! $validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$documentId = $cursor->getValue();
|
||||
|
||||
try {
|
||||
$cursorDocument = $authorization->skip(fn () => $dbForDatabases->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
|
||||
} catch (NotFoundException) {
|
||||
// The collection metadata document exists but the backing
|
||||
// store has no table for it. Treat as collection not-found
|
||||
// so the caller sees a 404 instead of a 500.
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
$type = ucfirst($this->getContext());
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "$type '{$documentId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
$hasSelects = ! empty(Query::groupByType($queries)['selections']);
|
||||
|
||||
// When there are no select queries, relationship loading is skipped on
|
||||
// the underlying find() to avoid pulling related documents the caller
|
||||
// did not ask for.
|
||||
$find = $hasSelects
|
||||
? fn () => $dbForDatabases->find($collectionTableId, $queries)
|
||||
: fn () => $dbForDatabases->skipRelationships(fn () => $dbForDatabases->find($collectionTableId, $queries));
|
||||
|
||||
return [
|
||||
'database' => $database,
|
||||
'collection' => $collection,
|
||||
'dbForDatabases' => $dbForDatabases,
|
||||
'queries' => $queries,
|
||||
'collectionTableId' => $collectionTableId,
|
||||
'hasSelects' => $hasSelects,
|
||||
'find' => $find,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+10
-49
@@ -17,7 +17,6 @@ use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Exception\Timeout;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\ArrayList;
|
||||
@@ -85,54 +84,16 @@ abstract class Get extends Action
|
||||
callable $getDatabasesDB,
|
||||
Authorization $authorization,
|
||||
): void {
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($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, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_'.$database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty() || (! $collection->getAttribute('enabled', false) && ! $isAPIKey && ! $isPrivilegedUser)) {
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$dbForDatabases = $getDatabasesDB($database);
|
||||
|
||||
$cursor = Query::getCursorQueries($queries, false);
|
||||
$cursor = \reset($cursor);
|
||||
|
||||
if ($cursor !== false) {
|
||||
$validator = new Cursor;
|
||||
if (! $validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$documentId = $cursor->getValue();
|
||||
|
||||
$cursorDocument = $authorization->skip(fn () => $dbForDatabases->getDocument('database_'.$database->getSequence().'_collection_'.$collection->getSequence(), $documentId));
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
$type = ucfirst($this->getContext());
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "$type '{$documentId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$collectionTableId = 'database_'.$database->getSequence().'_collection_'.$collection->getSequence();
|
||||
$hasSelects = ! empty(Query::groupByType($queries)['selections']);
|
||||
|
||||
$find = $hasSelects
|
||||
? fn () => $dbForDatabases->find($collectionTableId, $queries)
|
||||
: fn () => $dbForDatabases->skipRelationships(fn () => $dbForDatabases->find($collectionTableId, $queries));
|
||||
// Reuses the same prep listRows runs (auth, lookups, query parse,
|
||||
// cursor, find closure) so the explain endpoint stays byte-identical
|
||||
// to the read it's explaining.
|
||||
$context = $this->prepareListContext($databaseId, $collectionId, $queries, $dbForProject, $user, $getDatabasesDB, $authorization);
|
||||
$database = $context['database'];
|
||||
$collection = $context['collection'];
|
||||
$dbForDatabases = $context['dbForDatabases'];
|
||||
$queries = $context['queries'];
|
||||
$collectionTableId = $context['collectionTableId'];
|
||||
$find = $context['find'];
|
||||
|
||||
// listRows fires both find() and count() when `total: true` (the default).
|
||||
// Mirror that exactly so explain reflects real listRows read volume.
|
||||
|
||||
+7
-55
@@ -88,65 +88,17 @@ class XList extends Action
|
||||
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, User $user, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization, ?Http $utopia = null): void
|
||||
{
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($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, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$dbForDatabases = $getDatabasesDB($database);
|
||||
$cursor = Query::getCursorQueries($queries, false);
|
||||
$cursor = \reset($cursor);
|
||||
|
||||
if ($cursor !== false) {
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$documentId = $cursor->getValue();
|
||||
|
||||
try {
|
||||
$cursorDocument = $authorization->skip(fn () => $dbForDatabases->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
|
||||
} catch (NotFoundException) {
|
||||
// The collection metadata document exists but the backing store (e.g. a
|
||||
// dedicated DocumentsDB shard) has no table for it. Treat this as a
|
||||
// not-found on the collection so the caller sees a 404 instead of a 500.
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
$type = ucfirst($this->getContext());
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "$type '{$documentId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
$context = $this->prepareListContext($databaseId, $collectionId, $queries, $dbForProject, $user, $getDatabasesDB, $authorization);
|
||||
$database = $context['database'];
|
||||
$collection = $context['collection'];
|
||||
$dbForDatabases = $context['dbForDatabases'];
|
||||
$queries = $context['queries'];
|
||||
$collectionTableId = $context['collectionTableId'];
|
||||
$find = $context['find'];
|
||||
|
||||
$dbStart = \microtime(true);
|
||||
|
||||
try {
|
||||
$hasSelects = ! empty(Query::groupByType($queries)['selections']);
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
// When there are no select queries, relationship loading is skipped on the
|
||||
// underlying find() to avoid pulling related documents the caller did not ask for.
|
||||
$find = $hasSelects
|
||||
? fn () => $dbForDatabases->find($collectionTableId, $queries)
|
||||
: fn () => $dbForDatabases->skipRelationships(fn () => $dbForDatabases->find($collectionTableId, $queries));
|
||||
|
||||
// Use transaction-aware document retrieval if transactionId is provided
|
||||
if ($transactionId !== null) {
|
||||
$documents = $transactionState->listDocuments($database, $collectionTableId, $transactionId, $queries);
|
||||
|
||||
Reference in New Issue
Block a user