Merge pull request #10380 from appwrite/feat-select-queries-on-deployments

Feat: Deployment select queries
This commit is contained in:
Matej Bačo 2025-09-02 12:12:53 +02:00 committed by GitHub
commit b3fa60b2d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 177 additions and 32 deletions

View file

@ -2,6 +2,8 @@
namespace Appwrite\Platform; namespace Appwrite\Platform;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Coroutine as Co; use Swoole\Coroutine as Co;
use Utopia\CLI\Console; use Utopia\CLI\Console;
use Utopia\Database\Database; use Utopia\Database\Database;
@ -157,4 +159,45 @@ class Action extends UtopiaAction
Console::info("[" . DateTime::now() . "] " . $method . ' ' . $type . ' ' . $project->getSequence() . ' ' . $project->getId() . ' ' . $collectionId . ' ' . $log); Console::info("[" . DateTime::now() . "] " . $method . ' ' . $type . ' ' . $project->getSequence() . ' ' . $project->getId() . ' ' . $collectionId . ' ' . $log);
} }
} }
/**
* Helper to apply (request) select queries to response model.
*
* This prevents default values of rules to be presnet for not-selected attributes
*
* @param Request $request
* @param Document $document
* @return void
*/
public function applySelectQueries(Request $request, Response $response, string $model): void
{
$queries = $request->getParam('queries', []);
$queries = Query::parseQueries($queries);
$selectQueries = Query::groupByType($queries)['selections'] ?? [];
// No select queries means no filtering out
if (empty($selectQueries)) {
return;
}
$attributes = [];
foreach ($selectQueries as $query) {
foreach ($query->getValues() as $attribute) {
$attributes[] = $attribute;
}
}
$responseModel = $response->getModel($model);
foreach ($responseModel->getRules() as $ruleName => $rule) {
if (\str_starts_with($ruleName, '$')) {
continue;
}
if (!\in_array($ruleName, $attributes)) {
$responseModel->removeRule($ruleName);
}
}
}
} }

View file

@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Compute;
use Appwrite\Event\Build; use Appwrite\Event\Build;
use Appwrite\Extend\Exception; use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Duplicate;
@ -11,7 +12,6 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role; use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Swoole\Request; use Utopia\Swoole\Request;
use Utopia\System\System; use Utopia\System\System;
use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Adapter\Git\GitHub;

View file

@ -3,10 +3,12 @@
namespace Appwrite\Platform\Modules\Functions\Http\Deployments; namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
use Appwrite\Extend\Exception; use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType; use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method; use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Deployments; use Appwrite\Utopia\Database\Validator\Queries\Deployments;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response; use Appwrite\Utopia\Response;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
@ -19,7 +21,7 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP; use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text; use Utopia\Validator\Text;
class XList extends Action class XList extends Base
{ {
use HTTP; use HTTP;
@ -55,6 +57,7 @@ class XList extends Action
->param('functionId', '', new UID(), 'Function ID.') ->param('functionId', '', new UID(), 'Function ID.')
->param('queries', [], new Deployments(), '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(', ', Deployments::ALLOWED_ATTRIBUTES), true) ->param('queries', [], new Deployments(), '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(', ', Deployments::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('request')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->callback($this->action(...)); ->callback($this->action(...));
@ -64,6 +67,7 @@ class XList extends Action
string $functionId, string $functionId,
array $queries, array $queries,
string $search, string $search,
Request $request,
Response $response, Response $response,
Database $dbForProject Database $dbForProject
) { ) {
@ -121,6 +125,7 @@ class XList extends Action
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."); 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_DEPLOYMENT);
$response->dynamic(new Document([ $response->dynamic(new Document([
'deployments' => $results, 'deployments' => $results,
'total' => $total, 'total' => $total,

View file

@ -3,10 +3,12 @@
namespace Appwrite\Platform\Modules\Sites\Http\Deployments; namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Extend\Exception; use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType; use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method; use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Deployments; use Appwrite\Utopia\Database\Validator\Queries\Deployments;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response; use Appwrite\Utopia\Response;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
@ -19,7 +21,7 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP; use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text; use Utopia\Validator\Text;
class XList extends Action class XList extends Base
{ {
use HTTP; use HTTP;
@ -55,13 +57,20 @@ class XList extends Action
->param('siteId', '', new UID(), 'Site ID.') ->param('siteId', '', new UID(), 'Site ID.')
->param('queries', [], new Deployments(), '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(', ', Deployments::ALLOWED_ATTRIBUTES), true) ->param('queries', [], new Deployments(), '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(', ', Deployments::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('request')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->callback($this->action(...)); ->callback($this->action(...));
} }
public function action(string $siteId, array $queries, string $search, Response $response, Database $dbForProject) public function action(
{ string $siteId,
array $queries,
string $search,
Request $request,
Response $response,
Database $dbForProject
) {
$site = $dbForProject->getDocument('sites', $siteId); $site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) { if ($site->isEmpty()) {
@ -116,6 +125,7 @@ class XList extends Action
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."); 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_DEPLOYMENT);
$response->dynamic(new Document([ $response->dynamic(new Document([
'deployments' => $results, 'deployments' => $results,
'total' => $total, 'total' => $total,

View file

@ -11,6 +11,7 @@ use Utopia\Database\Validator\Query\Filter;
use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\Query\Order; use Utopia\Database\Validator\Query\Order;
use Utopia\Database\Validator\Query\Select;
class Base extends Queries class Base extends Queries
{ {
@ -40,41 +41,51 @@ class Base extends Queries
$allowedAttributesLookup[$attribute] = true; $allowedAttributesLookup[$attribute] = true;
} }
$allAttributes = [];
$attributes = []; $attributes = [];
foreach ($collection['attributes'] as $attribute) { foreach ($collection['attributes'] as $attribute) {
$key = $attribute['$id']; $key = $attribute['$id'];
if (!isset($allowedAttributesLookup[$key])) { $attributeDocument = new Document([
continue;
}
$attributes[] = new Document([
'key' => $key, 'key' => $key,
'type' => $attribute['type'], 'type' => $attribute['type'],
'array' => $attribute['array'], 'array' => $attribute['array'],
]); ]);
$allAttributes[] = $attributeDocument;
if (isset($allowedAttributesLookup[$key])) {
$attributes[] = $attributeDocument;
}
} }
$attributes[] = new Document([ $internalAttributes = [
'key' => '$id', new Document([
'type' => Database::VAR_STRING, 'key' => '$id',
'array' => false, 'type' => Database::VAR_STRING,
]); 'array' => false,
$attributes[] = new Document([ ]),
'key' => '$createdAt', new Document([
'type' => Database::VAR_DATETIME, 'key' => '$createdAt',
'array' => false, 'type' => Database::VAR_DATETIME,
]); 'array' => false,
$attributes[] = new Document([ ]),
'key' => '$updatedAt', new Document([
'type' => Database::VAR_DATETIME, 'key' => '$updatedAt',
'array' => false, 'type' => Database::VAR_DATETIME,
]); 'array' => false,
$attributes[] = new Document([ ]),
'key' => '$sequence', new Document([
'type' => Database::VAR_INTEGER, 'key' => '$sequence',
'array' => false, 'type' => Database::VAR_INTEGER,
]); 'array' => false,
])
];
foreach ($internalAttributes as $attribute) {
$attributes[] = $attribute;
$allAttributes[] = $attribute;
}
$validators = [ $validators = [
new Limit(), new Limit(),
@ -84,6 +95,15 @@ class Base extends Queries
new Order($attributes), new Order($attributes),
]; ];
if ($this->isSelectQueryAllowed()) {
$validators[] = new Select($allAttributes);
}
parent::__construct($validators); parent::__construct($validators);
} }
public function isSelectQueryAllowed(): bool
{
return false;
}
} }

View file

@ -22,4 +22,9 @@ class Deployments extends Base
{ {
parent::__construct('deployments', self::ALLOWED_ATTRIBUTES); parent::__construct('deployments', self::ALLOWED_ATTRIBUTES);
} }
public function isSelectQueryAllowed(): bool
{
return true;
}
} }

View file

@ -402,7 +402,7 @@ class Response extends SwooleResponse
/** /**
* Response constructor. * Response constructor.
* *
* @param float $time * @param SwooleHTTPResponse $response Native response to be passed to parent constructor
*/ */
public function __construct(SwooleHTTPResponse $response) public function __construct(SwooleHTTPResponse $response)
{ {

View file

@ -44,6 +44,7 @@ abstract class Model
/** /**
* Filter Document Structure * Filter Document Structure
* @param Document $document Document to apply filter on
* *
* @return Document * @return Document
*/ */
@ -105,7 +106,7 @@ abstract class Model
* @param string $key * @param string $key
* @return Model * @return Model
*/ */
protected function removeRule(string $key): self public function removeRule(string $key): self
{ {
if (isset($this->rules[$key])) { if (isset($this->rules[$key])) {
unset($this->rules[$key]); unset($this->rules[$key]);

View file

@ -721,6 +721,30 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals($deployments['headers']['status-code'], 200); $this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertCount(1, $deployments['body']['deployments']); $this->assertCount(1, $deployments['body']['deployments']);
$deployments = $this->listDeployments($functionId, [
'queries' => [
Query::select(['status'])->toString(),
],
]);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertArrayHasKey('status', $deployments['body']['deployments'][0]);
$this->assertArrayHasKey('status', $deployments['body']['deployments'][1]);
$this->assertArrayNotHasKey('sourceSize', $deployments['body']['deployments'][0]);
$this->assertArrayNotHasKey('sourceSize', $deployments['body']['deployments'][1]);
// Extra select query check, for attribute not allowed by filter queries
$deployments = $this->listDeployments($functionId, [
'queries' => [
Query::select(['buildLogs'])->toString(),
],
]);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertArrayHasKey('buildLogs', $deployments['body']['deployments'][0]);
$this->assertArrayHasKey('buildLogs', $deployments['body']['deployments'][1]);
$this->assertArrayNotHasKey('sourceSize', $deployments['body']['deployments'][0]);
$this->assertArrayNotHasKey('sourceSize', $deployments['body']['deployments'][1]);
$deployments = $this->listDeployments($functionId, [ $deployments = $this->listDeployments($functionId, [
'queries' => [ 'queries' => [
Query::offset(1)->toString(), Query::offset(1)->toString(),

View file

@ -1052,6 +1052,30 @@ class SitesCustomServerTest extends Scope
$this->assertEquals($deployments['headers']['status-code'], 200); $this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertCount(1, $deployments['body']['deployments']); $this->assertCount(1, $deployments['body']['deployments']);
$deployments = $this->listDeployments($siteId, [
'queries' => [
Query::select(['status'])->toString(),
],
]);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertArrayHasKey('status', $deployments['body']['deployments'][0]);
$this->assertArrayHasKey('status', $deployments['body']['deployments'][1]);
$this->assertArrayNotHasKey('sourceSize', $deployments['body']['deployments'][0]);
$this->assertArrayNotHasKey('sourceSize', $deployments['body']['deployments'][1]);
// Extra select query check, for attribute not allowed by filter queries
$deployments = $this->listDeployments($siteId, [
'queries' => [
Query::select(['buildLogs'])->toString(),
],
]);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertArrayHasKey('buildLogs', $deployments['body']['deployments'][0]);
$this->assertArrayHasKey('buildLogs', $deployments['body']['deployments'][1]);
$this->assertArrayNotHasKey('sourceSize', $deployments['body']['deployments'][0]);
$this->assertArrayNotHasKey('sourceSize', $deployments['body']['deployments'][1]);
$deployments = $this->listDeployments($siteId, [ $deployments = $this->listDeployments($siteId, [
'queries' => [ 'queries' => [
Query::offset(1)->toString(), Query::offset(1)->toString(),

View file

@ -445,6 +445,19 @@ trait UsersBase
$user1 = $response['body']['users'][1]; $user1 = $response['body']['users'][1];
// This test ensures that by default, endpoints dont support select queries
// If we add select query to this endpoint, you will need to remove this test
// Please make sure to add it to another place, unless all endpoints support select queries
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::select(['name'])->toString()
]
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([ $response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json', 'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-project' => $this->getProject()['$id'],