Merge branch '1.9.x' into feat-public-platform-api

This commit is contained in:
Matej Bačo
2026-04-07 14:08:00 +02:00
8 changed files with 281 additions and 33 deletions
+7
View File
@@ -98,6 +98,9 @@ Http::init()
->inject('authorization')
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
/**
* Handle user authentication and session validation.
@@ -489,6 +492,10 @@ Http::init()
$request->setUser($user);
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
$path = $route->getMatchedPath();
$databaseType = match (true) {
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
@@ -1,20 +1,15 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Http\Projects\Labels;
namespace Appwrite\Platform\Modules\Project\Http\Project\Labels;
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\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
@@ -27,39 +22,37 @@ class Update extends Action
return 'updateProjectLabels';
}
protected function getQueriesValidator(): Validator
{
return new Projects();
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/projects/:projectId/labels')
->setHttpPath('/v1/project/labels')
->httpAlias('/v1/projects/:projectId/labels')
->desc('Update project labels')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'labels.*.update')
->label('audits.event', 'project.labels.update')
->label('audits.resource', 'project.labels/{response.$id}')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
namespace: 'project',
group: null,
name: 'updateLabels',
description: <<<EOT
Update the project labels by its unique ID. Labels can be used to easily filter projects in an organization.
Update the project labels. Labels can be used to easily filter projects in an organization.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT
model: Response::MODEL_PROJECT,
)
],
contentType: ContentType::JSON
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), APP_LIMIT_ARRAY_LABELS_SIZE), 'Array of project labels. Replaces the previous labels. Maximum of ' . APP_LIMIT_ARRAY_LABELS_SIZE . ' labels are allowed, each up to 36 alphanumeric characters long.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
->callback($this->action(...));
}
@@ -67,17 +60,11 @@ class Update extends Action
* @param array<string> $labels
*/
public function action(
string $projectId,
array $labels,
Response $response,
Database $dbForPlatform
Database $dbForPlatform,
Document $project
): void {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$labels = (array) \array_values(\array_unique($labels));
$project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels]));
@@ -16,6 +16,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Update as Updat
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Create as CreateWindowsPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Update as UpdateWindowsPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\XList as ListPlatforms;
use Appwrite\Platform\Modules\Project\Http\Project\Labels\Update as UpdateProjectLabels;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
@@ -31,6 +32,9 @@ class Http extends Service
// Hooks
$this->addAction(Init::getName(), new Init());
// Project
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
// Variables
$this->addAction(CreateVariable::getName(), new CreateVariable());
@@ -8,7 +8,6 @@ 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\Create as CreateProject;
use Appwrite\Platform\Modules\Projects\Http\Projects\Labels\Update as UpdateProjectLabels;
use Appwrite\Platform\Modules\Projects\Http\Projects\Team\Update as UpdateProjectTeam;
use Appwrite\Platform\Modules\Projects\Http\Projects\Update as UpdateProject;
use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects;
@@ -31,7 +30,6 @@ class Http extends Service
$this->addAction(CreateProject::getName(), new CreateProject());
$this->addAction(UpdateProject::getName(), new UpdateProject());
$this->addAction(ListProjects::getName(), new ListProjects());
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
$this->addAction(UpdateProjectTeam::getName(), new UpdateProjectTeam());
$this->addAction(CreateSchedule::getName(), new CreateSchedule());
+2 -2
View File
@@ -1324,8 +1324,8 @@ class UsageTest extends Scope
$this->assertEquals($requestsTotal, $response['body']['requests'][array_key_last($response['body']['requests'])]['value']);
$this->validateDates($response['body']['requests']);
// vectordbTotal should reflect only VectorsDB instances, not relational databases.
$this->assertEquals($vectordbTotal, $response['body']['vectordbDatabasesTotal']);
$this->assertEquals($documentsTotal, $response['body']['vectordbDocumentsTotal']);
$this->assertEquals($vectordbTotal, $response['body']['vectorsdbDatabasesTotal']);
$this->assertEquals($documentsTotal, $response['body']['vectorsdbDocumentsTotal']);
});
$response = $this->client->call(
+224
View File
@@ -0,0 +1,224 @@
<?php
namespace Tests\E2E\Services\Project;
trait LabelsBase
{
// Update labels tests
public function testUpdateLabels(): void
{
$response = $this->updateLabels(['frontend', 'backend']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['labels']);
$this->assertCount(2, $response['body']['labels']);
$this->assertContains('frontend', $response['body']['labels']);
$this->assertContains('backend', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsReplace(): void
{
$response = $this->updateLabels(['alpha', 'beta']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(2, $response['body']['labels']);
$this->assertContains('alpha', $response['body']['labels']);
$this->assertContains('beta', $response['body']['labels']);
// Replace with new labels
$response = $this->updateLabels(['gamma', 'delta', 'epsilon']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(3, $response['body']['labels']);
$this->assertContains('gamma', $response['body']['labels']);
$this->assertContains('delta', $response['body']['labels']);
$this->assertContains('epsilon', $response['body']['labels']);
$this->assertNotContains('alpha', $response['body']['labels']);
$this->assertNotContains('beta', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsEmpty(): void
{
// Set some labels first
$response = $this->updateLabels(['toRemove']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['labels']);
// Clear all labels
$response = $this->updateLabels([]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['labels']);
$this->assertCount(0, $response['body']['labels']);
}
public function testUpdateLabelsDeduplicated(): void
{
$response = $this->updateLabels(['duplicate', 'duplicate', 'unique']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(2, $response['body']['labels']);
$this->assertContains('duplicate', $response['body']['labels']);
$this->assertContains('unique', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsSingleLabel(): void
{
$response = $this->updateLabels(['solo']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['labels']);
$this->assertContains('solo', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsWithoutAuthentication(): void
{
$response = $this->updateLabels(['unauthorized'], false);
$this->assertSame(401, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidLabelTooLong(): void
{
$response = $this->updateLabels([str_repeat('a', 37)]);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidLabelCharacters(): void
{
$response = $this->updateLabels(['invalid-label!']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsAlphanumericOnly(): void
{
$response = $this->updateLabels(['ABC123', 'lowercase', 'UPPERCASE', '0123456789']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(4, $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsMaxLength(): void
{
$label = str_repeat('a', 36);
$response = $this->updateLabels([$label]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['labels']);
$this->assertContains($label, $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsIdempotent(): void
{
$labels = ['stable', 'production'];
$first = $this->updateLabels($labels);
$this->assertSame(200, $first['headers']['status-code']);
$second = $this->updateLabels($labels);
$this->assertSame(200, $second['headers']['status-code']);
$this->assertSame($first['body']['labels'], $second['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsDeduplicatedOrder(): void
{
$response = $this->updateLabels(['b', 'a', 'b']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(2, $response['body']['labels']);
$this->assertSame('b', $response['body']['labels'][0]);
$this->assertSame('a', $response['body']['labels'][1]);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsInvalidHyphen(): void
{
$response = $this->updateLabels(['my-label']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidUnderscore(): void
{
$response = $this->updateLabels(['my_label']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidSpace(): void
{
$response = $this->updateLabels(['my label']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidEmptyString(): void
{
$response = $this->updateLabels(['']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsResponseModel(): void
{
$response = $this->updateLabels(['test']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertArrayHasKey('$id', $response['body']);
$this->assertArrayHasKey('name', $response['body']);
$this->assertArrayHasKey('labels', $response['body']);
$this->assertIsArray($response['body']['labels']);
$this->assertContains('test', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
// Helpers
/**
* @param array<string> $labels
*/
protected function updateLabels(array $labels, bool $authenticated = true): mixed
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
if ($authenticated) {
$headers = array_merge($headers, $this->getHeaders());
}
return $this->client->call(\Tests\E2E\Client::METHOD_PUT, '/project/labels', $headers, [
'labels' => $labels,
]);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
class LabelsConsoleClientTest extends Scope
{
use LabelsBase;
use ProjectCustom;
use SideConsole;
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class LabelsCustomServerTest extends Scope
{
use LabelsBase;
use ProjectCustom;
use SideServer;
}