From 4cf3d0b87bb40624314defca5ba2050460e97b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 5 Sep 2025 09:12:35 +0200 Subject: [PATCH 1/5] move projects.list() to module --- app/controllers/api/projects.php | 74 ------------ docs/references/projects/list.md | 1 - .../Projects/Http/DevKeys/Projects/XList.php | 110 ++++++++++++++++++ .../Modules/Projects/Services/Http.php | 3 + 4 files changed, 113 insertions(+), 75 deletions(-) delete mode 100644 docs/references/projects/list.md create mode 100644 src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 4f34156115..ea3a00dcb6 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -30,14 +30,11 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; -use Utopia\Database\Exception\Order as OrderException; -use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; -use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; use Utopia\Domains\Validator\PublicDomain; use Utopia\DSN\DSN; @@ -304,77 +301,6 @@ App::post('/v1/projects') ->dynamic($project, Response::MODEL_PROJECT); }); -App::get('/v1/projects') - ->desc('List projects') - ->groups(['api', 'projects']) - ->label('scope', 'projects.read') - ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', - name: 'list', - description: '/docs/references/projects/list.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROJECT_LIST, - ) - ] - )) - ->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) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (array $queries, string $search, Response $response, Database $dbForPlatform) { - - try { - $queries = Query::parseQueries($queries); - } catch (QueryException $e) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); - } - - if (!empty($search)) { - $queries[] = Query::search('search', $search); - } - - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ - - $validator = new Cursor(); - if (!$validator->isValid($cursor)) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); - } - - $projectId = $cursor->getValue(); - $cursorDocument = $dbForPlatform->getDocument('projects', $projectId); - - if ($cursorDocument->isEmpty()) { - throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Project '{$projectId}' for the 'cursor' value not found."); - } - - $cursor->setValue($cursorDocument); - } - - $filterQueries = Query::groupByType($queries)['filters']; - try { - $projects = $dbForPlatform->find('projects', $queries); - $total = $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT); - } catch (OrderException $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."); - } - $response->dynamic(new Document([ - 'projects' => $projects, - 'total' => $total, - ]), Response::MODEL_PROJECT_LIST); - }); - App::get('/v1/projects/:projectId') ->desc('Get project') ->groups(['api', 'projects']) diff --git a/docs/references/projects/list.md b/docs/references/projects/list.md deleted file mode 100644 index 576a4b79ae..0000000000 --- a/docs/references/projects/list.md +++ /dev/null @@ -1 +0,0 @@ -Get a list of all projects. You can use the query params to filter your results. \ No newline at end of file diff --git a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php new file mode 100644 index 0000000000..c42b847a19 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php @@ -0,0 +1,110 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/projects') + ->desc('List projects') + ->groups(['api', 'projects']) + ->label('scope', 'projects.read') + ->label('sdk', new Method( + namespace: 'projects', + group: 'projects', + name: 'list', + description: <<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) + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action(array $queries, string $search, Response $response, Database $dbForPlatform) + { + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + /** + * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries + */ + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); + }); + $cursor = reset($cursor); + if ($cursor) { + /** @var Query $cursor */ + + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $projectId = $cursor->getValue(); + $cursorDocument = $dbForPlatform->getDocument('projects', $projectId); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Project '{$projectId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + try { + $projects = $dbForPlatform->find('projects', $queries); + $total = $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT); + } 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."); + } + $response->dynamic(new Document([ + 'projects' => $projects, + 'total' => $total, + ]), Response::MODEL_PROJECT_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Projects/Services/Http.php b/src/Appwrite/Platform/Modules/Projects/Services/Http.php index cec8ed6d16..2a0dd0aa60 100644 --- a/src/Appwrite/Platform/Modules/Projects/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Projects/Services/Http.php @@ -7,6 +7,7 @@ use Appwrite\Platform\Modules\Projects\Http\DevKeys\Delete as DeleteDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\Get as GetDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\Update as UpdateDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\XList as ListDevKeys; +use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects; use Utopia\Platform\Service; class Http extends Service @@ -19,5 +20,7 @@ class Http extends Service $this->addAction(GetDevKey::getName(), new GetDevKey()); $this->addAction(ListDevKeys::getName(), new ListDevKeys()); $this->addAction(DeleteDevKey::getName(), new DeleteDevKey()); + + $this->addAction(ListProjects::getName(), new ListProjects()); } } From c894b0ab6a12fa6162626cf177ab68aa00cb1f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 5 Sep 2025 09:25:13 +0200 Subject: [PATCH 2/5] Simplify validator override --- .../Modules/Projects/Http/DevKeys/Projects/XList.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php index c42b847a19..9a140cbdf8 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php @@ -17,6 +17,7 @@ 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\Text; class XList extends Action @@ -26,9 +27,14 @@ class XList extends Action { return 'listProjects'; } + + // Stored as property intentionally for extensibility + protected Validator $queriesValidator; public function __construct() { + $this->queriesValidator = new Projects(); + $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/projects') @@ -51,7 +57,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->queriesValidator, '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) ->inject('response') ->inject('dbForPlatform') From cc50302530226c8c586873278320c3ac22dc25f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 5 Sep 2025 09:25:45 +0200 Subject: [PATCH 3/5] Formatting fix --- .../Platform/Modules/Projects/Http/DevKeys/Projects/XList.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php index 9a140cbdf8..f190990e00 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php @@ -27,14 +27,14 @@ class XList extends Action { return 'listProjects'; } - + // Stored as property intentionally for extensibility protected Validator $queriesValidator; public function __construct() { $this->queriesValidator = new Projects(); - + $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/projects') From bf59f1ba7d777bc1ccbc49c09a563186c4264b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 5 Sep 2025 09:30:07 +0200 Subject: [PATCH 4/5] Fix path structure --- .../Modules/Projects/Http/{DevKeys => }/Projects/XList.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Appwrite/Platform/Modules/Projects/Http/{DevKeys => }/Projects/XList.php (100%) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php similarity index 100% rename from src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Projects/XList.php rename to src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php From ca6cf01773b49b9ca16659ccc8b92b5b6de0a849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 5 Sep 2025 10:12:07 +0200 Subject: [PATCH 5/5] Improve customization of queries validator --- .../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 f190990e00..3d06103e75 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php @@ -28,13 +28,13 @@ class XList extends Action return 'listProjects'; } - // Stored as property intentionally for extensibility - protected Validator $queriesValidator; + protected function getQueriesValidator(): Validator + { + return new Projects(); + } public function __construct() { - $this->queriesValidator = new Projects(); - $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/projects') @@ -57,7 +57,7 @@ class XList extends Action ], contentType: ContentType::JSON )) - ->param('queries', [], $this->queriesValidator, '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) ->inject('response') ->inject('dbForPlatform')