From c958f3f1cbfc1db0af3a2ba75fae947c171e63ef Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 19 Dec 2025 14:13:52 +0530 Subject: [PATCH 1/7] update: allow queries on projects xlist. --- src/Appwrite/Platform/Action.php | 6 +- .../Modules/Projects/Http/Projects/XList.php | 101 +++++++- .../Database/Validator/Queries/Projects.php | 5 + .../Utopia/Response/Model/Project.php | 42 +++- .../Projects/ProjectsConsoleClientTest.php | 230 ++++++++++++++++++ 5 files changed, 365 insertions(+), 19 deletions(-) diff --git a/src/Appwrite/Platform/Action.php b/src/Appwrite/Platform/Action.php index 5699a67ff2..939f12f28d 100644 --- a/src/Appwrite/Platform/Action.php +++ b/src/Appwrite/Platform/Action.php @@ -107,9 +107,11 @@ class Action extends UtopiaAction } } - public function disableSubqueries() + public function disableSubqueries(array $filters = []): void { - $filters = $this->filters; + if (empty($filters)) { + $filters = $this->filters; + } foreach ($filters as $filter) { Database::addFilter( diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php index 692b467282..a5f5e5c9f3 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php @@ -3,37 +3,37 @@ namespace Appwrite\Platform\Modules\Projects\Http\Projects; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\Projects; +use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; +use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Order; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Query; use Utopia\Database\Validator\Query\Cursor; -use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; -use Utopia\Validator; use Utopia\Validator\Boolean; use Utopia\Validator\Text; class XList extends Action { use HTTP; + + // cached mapping of columns to their subQuery filters + private static ?array $attributeToSubQueryFilters = null; + public static function getName() { return 'listProjects'; } - protected function getQueriesValidator(): Validator - { - return new Projects(); - } - public function __construct() { $this @@ -58,15 +58,16 @@ class XList extends Action ], contentType: ContentType::JSON )) - ->param('queries', [], $this->getQueriesValidator(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true) + ->param('queries', [], new Projects(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) + ->inject('request') ->inject('response') ->inject('dbForPlatform') ->callback($this->action(...)); } - public function action(array $queries, string $search, bool $includeTotal, Response $response, Database $dbForPlatform) + public function action(array $queries, string $search, bool $includeTotal, Request $request, Response $response, Database $dbForPlatform) { try { $queries = Query::parseQueries($queries); @@ -103,16 +104,94 @@ class XList extends Action $cursor->setValue($cursorDocument); } - $filterQueries = Query::groupByType($queries)['filters']; try { - $projects = $dbForPlatform->find('projects', $queries); + $selectQueries = Query::groupByType($queries)['selections'] ?? []; + $filterQueries = Query::groupByType($queries)['filters']; + + if (!empty($selectQueries)) { + // has selects, skip unnecessary filters + $projects = $this->findWithSelect($dbForPlatform, $queries, $selectQueries); + } else { + // has no selects, load all columns + $projects = $dbForPlatform->find('projects', $queries); + } + $total = $includeTotal ? $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT) : 0; } catch (Order $e) { throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); } + + $this->applySelectQueries($request, $response, Response::MODEL_PROJECT); $response->dynamic(new Document([ 'projects' => $projects, 'total' => $total, ]), Response::MODEL_PROJECT_LIST); } + + // Build mapping of columns to their subQuery filters + private static function getAttributeToSubQueryFilters(): array + { + if (self::$attributeToSubQueryFilters !== null) { + return self::$attributeToSubQueryFilters; + } + + self::$attributeToSubQueryFilters = []; + + $collections = Config::getParam('collections', []); + $projectAttributes = $collections['platform']['projects']['attributes'] ?? []; + + foreach ($projectAttributes as $attribute) { + $attributeId = $attribute['$id'] ?? null; + $filters = $attribute['filters'] ?? []; + + if ($attributeId === null || empty($filters)) { + continue; + } + + // extract only subQuery filters + $subQueryFilters = \array_filter($filters, function ($filter) { + return \str_starts_with($filter, 'subQuery'); + }); + + if (!empty($subQueryFilters)) { + self::$attributeToSubQueryFilters[$attributeId] = \array_values($subQueryFilters); + } + } + + return self::$attributeToSubQueryFilters; + } + + // Find projects with a given select query + private function findWithSelect(Database $db, array $queries, array $selectQueries): array + { + $selectedAttributes = []; + foreach ($selectQueries as $query) { + // nested selects aren't handled atm! + foreach ($query->getValues() as $value) { + $selectedAttributes[] = $value; + } + } + + if (\in_array('*', $selectedAttributes)) { + return $db->find('projects', $queries); + } + + $filtersToSkipMap = []; + $selectedAttributesMap = \array_flip($selectedAttributes); + $attributeToSubQueryFilters = self::getAttributeToSubQueryFilters(); + + foreach ($attributeToSubQueryFilters as $attributeName => $subQueryFilters) { + if (!isset($selectedAttributesMap[$attributeName])) { + foreach ($subQueryFilters as $filter) { + $filtersToSkipMap[$filter] = true; + } + } + } + + $filtersToSkip = \array_keys($filtersToSkipMap); + + return empty($filtersToSkip) + ? $db->find('projects', $queries) + : $db->skipFilters(fn () => $db->find('projects', $queries), $filtersToSkip); + } } diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php b/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php index 5a0befb739..d179703274 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php @@ -17,4 +17,9 @@ class Projects extends Base { parent::__construct('projects', self::ALLOWED_ATTRIBUTES); } + + public function isSelectQueryAllowed(): bool + { + return true; + } } diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 65f9f7685b..7641e96090 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -341,6 +341,20 @@ class Project extends Model */ public function filter(Document $document): Document { + $this->expandSmtpFields($document); + $this->expandServiceFields($document); + $this->expandAuthFields($document); + $this->expandOAuthProviders($document); + + return $document; + } + + private function expandSmtpFields(Document $document): void + { + if (!$document->isSet('smtp')) { + return; + } + // SMTP $smtp = $document->getAttribute('smtp', []); $document->setAttribute('smtpEnabled', $smtp['enabled'] ?? false); @@ -352,8 +366,14 @@ class Project extends Model $document->setAttribute('smtpUsername', $smtp['username'] ?? ''); $document->setAttribute('smtpPassword', $smtp['password'] ?? ''); $document->setAttribute('smtpSecure', $smtp['secure'] ?? ''); + } + + private function expandServiceFields(Document $document): void + { + if (!$document->isSet('services')) { + return; + } - // Services $values = $document->getAttribute('services', []); $services = Config::getParam('services', []); @@ -365,8 +385,14 @@ class Project extends Model $value = $values[$key] ?? true; $document->setAttribute('serviceStatusFor' . ucfirst($key), $value); } + } + + private function expandAuthFields(Document $document): void + { + if (!$document->isSet('auths')) { + return; + } - // Auth $authValues = $document->getAttribute('auths', []); $auth = Config::getParam('auth', []); @@ -383,13 +409,19 @@ class Project extends Model $document->setAttribute('authMembershipsMfa', $authValues['membershipsMfa'] ?? true); $document->setAttribute('authInvalidateSessions', $authValues['invalidateSessions'] ?? false); - foreach ($auth as $index => $method) { + foreach ($auth as $method) { $key = $method['key']; $value = $authValues[$key] ?? true; $document->setAttribute('auth' . ucfirst($key), $value); } + } + + private function expandOAuthProviders(Document $document): void + { + if (!$document->isSet('oAuthProviders')) { + return; + } - // OAuth Providers $providers = Config::getParam('oAuthProviders', []); $providerValues = $document->getAttribute('oAuthProviders', []); $projectProviders = []; @@ -410,7 +442,5 @@ class Project extends Model } $document->setAttribute('oAuthProviders', $projectProviders); - - return $document; } } diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 9526c5a4da..e0ee67ed26 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -436,6 +436,236 @@ class ProjectsConsoleClientTest extends Scope return $data; } + /** + * @group projectsCRUD + */ + public function testListProjectsQuerySelect(): void + { + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => ID::unique(), + 'name' => 'Query Select Test Team', + ]); + + $this->assertEquals(201, $team['headers']['status-code']); + $teamId = $team['body']['$id']; + + $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Query Select Test Project', + 'teamId' => $teamId, + 'region' => System::getEnv('_APP_REGION', 'default') + ]); + + $this->assertEquals(201, $project['headers']['status-code']); + $projectId = $project['body']['$id']; + + /** + * Test Query.select - basic fields + */ + $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['$id', 'name'])->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertGreaterThan(0, count($response['body']['projects'])); + + $project = $response['body']['projects'][0]; + $this->assertArrayHasKey('$id', $project); + $this->assertArrayHasKey('name', $project); + $this->assertArrayNotHasKey('platforms', $project); + $this->assertArrayNotHasKey('webhooks', $project); + $this->assertArrayNotHasKey('keys', $project); + $this->assertArrayNotHasKey('devKeys', $project); + $this->assertArrayNotHasKey('oAuthProviders', $project); + $this->assertArrayNotHasKey('smtpEnabled', $project); + $this->assertArrayNotHasKey('smtpHost', $project); + $this->assertArrayNotHasKey('authLimit', $project); + $this->assertArrayNotHasKey('authDuration', $project); + + /** + * Test Query.select - multiple fields + */ + $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['$id', 'name', 'teamId', 'description', '$createdAt', '$updatedAt'])->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertGreaterThan(0, count($response['body']['projects'])); + + $project = $response['body']['projects'][0]; + $this->assertArrayHasKey('$id', $project); + $this->assertArrayHasKey('name', $project); + $this->assertArrayHasKey('teamId', $project); + $this->assertArrayHasKey('description', $project); + $this->assertArrayHasKey('$createdAt', $project); + $this->assertArrayHasKey('$updatedAt', $project); + $this->assertArrayNotHasKey('platforms', $project); + $this->assertArrayNotHasKey('webhooks', $project); + $this->assertArrayNotHasKey('keys', $project); + $this->assertArrayNotHasKey('devKeys', $project); + $this->assertArrayNotHasKey('oAuthProviders', $project); + $this->assertArrayNotHasKey('smtpEnabled', $project); + $this->assertArrayNotHasKey('authLimit', $project); + + /** + * Test Query.select combined with filters + */ + $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['$id', 'name', 'teamId'])->toString(), + Query::equal('name', ['Query Select Test Project'])->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertCount(1, $response['body']['projects']); + + $project = $response['body']['projects'][0]; + $this->assertArrayHasKey('$id', $project); + $this->assertArrayHasKey('name', $project); + $this->assertArrayHasKey('teamId', $project); + $this->assertEquals('Query Select Test Project', $project['name']); + $this->assertEquals($teamId, $project['teamId']); + $this->assertArrayNotHasKey('platforms', $project); + $this->assertArrayNotHasKey('webhooks', $project); + $this->assertArrayNotHasKey('keys', $project); + $this->assertArrayNotHasKey('devKeys', $project); + $this->assertArrayNotHasKey('oAuthProviders', $project); + $this->assertArrayNotHasKey('smtpEnabled', $project); + $this->assertArrayNotHasKey('authLimit', $project); + + /** + * Test Query.select combined with limit + */ + $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['$id', 'name'])->toString(), + Query::limit(2)->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertLessThanOrEqual(2, count($response['body']['projects'])); + + foreach ($response['body']['projects'] as $p) { + $this->assertArrayHasKey('$id', $p); + $this->assertArrayHasKey('name', $p); + $this->assertArrayNotHasKey('platforms', $p); + $this->assertArrayNotHasKey('webhooks', $p); + $this->assertArrayNotHasKey('keys', $p); + $this->assertArrayNotHasKey('devKeys', $p); + $this->assertArrayNotHasKey('oAuthProviders', $p); + $this->assertArrayNotHasKey('smtpEnabled', $p); + $this->assertArrayNotHasKey('authLimit', $p); + } + + /** + * Test Query.select with subquery attributes (platforms, webhooks, etc.) + * When explicitly selected, subqueries should still run + */ + $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['$id', 'name', 'platforms'])->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertGreaterThan(0, count($response['body']['projects'])); + + $project = $response['body']['projects'][0]; + $this->assertArrayHasKey('$id', $project); + $this->assertArrayHasKey('name', $project); + $this->assertArrayHasKey('platforms', $project); + $this->assertIsArray($project['platforms']); + $this->assertArrayNotHasKey('webhooks', $project); + $this->assertArrayNotHasKey('keys', $project); + $this->assertArrayNotHasKey('devKeys', $project); + $this->assertArrayNotHasKey('oAuthProviders', $project); + $this->assertArrayNotHasKey('smtpEnabled', $project); + $this->assertArrayNotHasKey('authLimit', $project); + + /** + * Test Query.select with expanded attributes + * webhooks and keys should load their subquery data when selected + */ + $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['$id', 'name', 'webhooks', 'keys'])->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertGreaterThan(0, count($response['body']['projects'])); + + $project = $response['body']['projects'][0]; + $this->assertArrayHasKey('$id', $project); + $this->assertArrayHasKey('name', $project); + $this->assertArrayHasKey('webhooks', $project); + $this->assertArrayHasKey('keys', $project); + $this->assertIsArray($project['webhooks']); + $this->assertIsArray($project['keys']); + $this->assertArrayNotHasKey('platforms', $project); + $this->assertArrayNotHasKey('devKeys', $project); + $this->assertArrayNotHasKey('smtpEnabled', $project); + $this->assertArrayNotHasKey('authLimit', $project); + + /** + * Test Query.select with invalid attribute + */ + $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['$id', 'invalidAttribute'])->toString(), + ], + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals('Invalid `queries` param: Invalid query: Attribute not found in schema: invalidAttribute', $response['body']['message']); + + $response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $response['headers']['status-code']); + } + public function testGetProject(): void { // Create a team From 3ddfd9c3b0f2b2498dfc65fca52a9494e1b56f15 Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 19 Dec 2025 15:52:19 +0530 Subject: [PATCH 2/7] update: address comment. --- src/Appwrite/Platform/Action.php | 5 +++ .../Projects/ProjectsConsoleClientTest.php | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/Appwrite/Platform/Action.php b/src/Appwrite/Platform/Action.php index 939f12f28d..3db0c74d45 100644 --- a/src/Appwrite/Platform/Action.php +++ b/src/Appwrite/Platform/Action.php @@ -191,6 +191,11 @@ class Action extends UtopiaAction } } + // found a wildcard, return! + if (\in_array('*', $attributes)) { + return; + } + $responseModel = $response->getModel($model); foreach ($responseModel->getRules() as $ruleName => $rule) { if (\str_starts_with($ruleName, '$')) { diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 69b8413fdd..769d3a4c85 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -643,6 +643,37 @@ class ProjectsConsoleClientTest extends Scope $this->assertArrayNotHasKey('smtpEnabled', $project); $this->assertArrayNotHasKey('authLimit', $project); + /** + * Test Query.select with wildcard '*' + * Should return all fields like no select query + */ + $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['*'])->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertGreaterThan(0, count($response['body']['projects'])); + + $project = $response['body']['projects'][0]; + $this->assertArrayHasKey('$id', $project); + $this->assertArrayHasKey('name', $project); + $this->assertArrayHasKey('teamId', $project); + $this->assertArrayHasKey('platforms', $project); + $this->assertArrayHasKey('webhooks', $project); + $this->assertArrayHasKey('keys', $project); + $this->assertArrayHasKey('devKeys', $project); + $this->assertArrayHasKey('oAuthProviders', $project); + $this->assertArrayHasKey('smtpEnabled', $project); + $this->assertArrayHasKey('smtpHost', $project); + $this->assertArrayHasKey('authLimit', $project); + $this->assertArrayHasKey('authDuration', $project); + /** * Test Query.select with invalid attribute */ From f814fd980b6a79a50e2628deb4b17ec5da47b34d Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 19 Dec 2025 16:16:56 +0530 Subject: [PATCH 3/7] update: address comments. --- .../Modules/Projects/Http/Projects/XList.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php index a5f5e5c9f3..0c124d31ce 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php @@ -108,14 +108,7 @@ class XList extends Action $selectQueries = Query::groupByType($queries)['selections'] ?? []; $filterQueries = Query::groupByType($queries)['filters']; - if (!empty($selectQueries)) { - // has selects, skip unnecessary filters - $projects = $this->findWithSelect($dbForPlatform, $queries, $selectQueries); - } else { - // has no selects, load all columns - $projects = $dbForPlatform->find('projects', $queries); - } - + $projects = $this->find($dbForPlatform, $queries, $selectQueries); $total = $includeTotal ? $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT) : 0; } catch (Order $e) { throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); @@ -161,12 +154,14 @@ class XList extends Action return self::$attributeToSubQueryFilters; } - // Find projects with a given select query - private function findWithSelect(Database $db, array $queries, array $selectQueries): array + private function find(Database $db, array $queries, array $selectQueries): array { + if (empty($selectQueries)) { + return $db->find('projects', $queries); + } + $selectedAttributes = []; foreach ($selectQueries as $query) { - // nested selects aren't handled atm! foreach ($query->getValues() as $value) { $selectedAttributes[] = $value; } From 35bf13cfab98f93a308751608ae77b726d2af3fe Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 19 Dec 2025 16:19:15 +0530 Subject: [PATCH 4/7] update: variable name :D --- .../Platform/Modules/Projects/Http/Projects/XList.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php index 0c124d31ce..5f2996aff4 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php @@ -154,10 +154,10 @@ class XList extends Action return self::$attributeToSubQueryFilters; } - private function find(Database $db, array $queries, array $selectQueries): array + private function find(Database $dbForPlatform, array $queries, array $selectQueries): array { if (empty($selectQueries)) { - return $db->find('projects', $queries); + return $dbForPlatform->find('projects', $queries); } $selectedAttributes = []; @@ -168,7 +168,7 @@ class XList extends Action } if (\in_array('*', $selectedAttributes)) { - return $db->find('projects', $queries); + return $dbForPlatform->find('projects', $queries); } $filtersToSkipMap = []; @@ -186,7 +186,7 @@ class XList extends Action $filtersToSkip = \array_keys($filtersToSkipMap); return empty($filtersToSkip) - ? $db->find('projects', $queries) - : $db->skipFilters(fn () => $db->find('projects', $queries), $filtersToSkip); + ? $dbForPlatform->find('projects', $queries) + : $dbForPlatform->skipFilters(fn () => $dbForPlatform->find('projects', $queries), $filtersToSkip); } } From e43292052cfad1468b19a811fc8fb0d75b15b3fc Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 19 Dec 2025 18:02:07 +0530 Subject: [PATCH 5/7] ci: empty commit From 406181e88773d7ba84087a51b05b0edae8d7b5db Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 19 Dec 2025 19:31:05 +0530 Subject: [PATCH 6/7] revert: to method call --- .../Platform/Modules/Projects/Http/Projects/XList.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php index 5f2996aff4..1726321a90 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php @@ -19,6 +19,7 @@ use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Query; use Utopia\Database\Validator\Query\Cursor; use Utopia\Platform\Scope\HTTP; +use Utopia\Validator; use Utopia\Validator\Boolean; use Utopia\Validator\Text; @@ -29,6 +30,11 @@ class XList extends Action // cached mapping of columns to their subQuery filters private static ?array $attributeToSubQueryFilters = null; + protected function getQueriesValidator(): Validator + { + return new Projects(); + } + public static function getName() { return 'listProjects'; @@ -58,7 +64,7 @@ class XList extends Action ], contentType: ContentType::JSON )) - ->param('queries', [], new Projects(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true) + ->param('queries', [], $this->getQueriesValidator(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('request') From bf50fbeb6c6479ebfda64211f0d3013b31495b46 Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 19 Dec 2025 19:36:05 +0530 Subject: [PATCH 7/7] bump: order for easier review. --- .../Platform/Modules/Projects/Http/Projects/XList.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php index 1726321a90..32318dd189 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php @@ -30,16 +30,16 @@ class XList extends Action // cached mapping of columns to their subQuery filters private static ?array $attributeToSubQueryFilters = null; - protected function getQueriesValidator(): Validator - { - return new Projects(); - } - public static function getName() { return 'listProjects'; } + protected function getQueriesValidator(): Validator + { + return new Projects(); + } + public function __construct() { $this