Compare commits

...
10 changed files with 298 additions and 110 deletions
+19 -71
View File
@@ -21,7 +21,7 @@ on:
jobs:
dependencies:
name: Checks / Dependencies
if: github.event_name == 'pull_request'
if: false
permissions:
actions: read
security-events: write
@@ -30,7 +30,7 @@ jobs:
security:
name: Checks / Image
if: github.event_name == 'pull_request'
if: false
runs-on: ubuntu-latest
permissions:
contents: read
@@ -85,6 +85,7 @@ jobs:
composer:
name: Checks / Composer
if: false
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -110,6 +111,7 @@ jobs:
format:
name: Checks / Format
if: false
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -135,6 +137,7 @@ jobs:
analyze:
name: Checks / Analyze
if: false
runs-on: ubuntu-latest
steps:
- name: Check out the repo
@@ -163,6 +166,7 @@ jobs:
locale:
name: Checks / Locale
if: false
runs-on: ubuntu-latest
steps:
- name: Check out the repo
@@ -188,38 +192,8 @@ jobs:
uses: actions/github-script@v8
with:
script: |
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
const defaultDatabases = ['MongoDB'];
const defaultModes = ['dedicated'];
const pr = context.payload.pull_request;
if (!pr) {
core.setOutput('databases', JSON.stringify(allDatabases));
core.setOutput('modes', JSON.stringify(allModes));
return;
}
const getContent = (ref) => github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: 'composer.lock',
ref,
});
const getDbVersion = (lock) => lock.packages?.find(p => p.name === 'utopia-php/database')?.version;
const [{ data: base }, { data: head }] = await Promise.all([
getContent(pr.base.sha),
getContent(pr.head.sha),
]);
const decode = (content) => JSON.parse(Buffer.from(content, 'base64').toString());
const databaseChanged = getDbVersion(decode(base.content)) !== getDbVersion(decode(head.content));
core.setOutput('databases', JSON.stringify(databaseChanged ? allDatabases : defaultDatabases));
core.setOutput('modes', JSON.stringify(databaseChanged ? allModes : defaultModes));
core.setOutput('databases', JSON.stringify(['MariaDB', 'PostgreSQL']));
core.setOutput('modes', JSON.stringify(['shared_v1', 'shared_v2']));
build:
name: Build
@@ -264,6 +238,7 @@ jobs:
unit:
name: Tests / Unit
if: false
runs-on: ubuntu-latest
needs: build
permissions:
@@ -311,6 +286,7 @@ jobs:
e2e_general:
name: Tests / E2E / General
if: false
runs-on: ubuntu-latest
needs: build
permissions:
@@ -377,45 +353,14 @@ jobs:
strategy:
fail-fast: false
matrix:
database: ${{ fromJSON(needs.matrix.outputs.databases) }}
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
service: [
Account,
Avatars,
Console,
Databases,
TablesDB,
Functions,
FunctionsSchedule,
GraphQL,
Health,
Locale,
Projects,
Realtime,
Sites,
Proxy,
Storage,
Tokens,
Teams,
Users,
ProjectWebhooks,
Webhooks,
VCS,
Messaging,
Migrations,
Project
]
include:
- service: Databases
runner: blacksmith-4vcpu-ubuntu-2404
- service: Sites
runner: blacksmith-4vcpu-ubuntu-2404
- service: Functions
runner: blacksmith-4vcpu-ubuntu-2404
- service: Avatars
runner: blacksmith-4vcpu-ubuntu-2404
- service: Realtime
- database: MariaDB
mode: shared_v1
service: Databases
runner: blacksmith-4vcpu-ubuntu-2404
- database: PostgreSQL
mode: shared_v2
service: TablesDB
- service: TablesDB
runner: blacksmith-4vcpu-ubuntu-2404
steps:
@@ -506,6 +451,7 @@ jobs:
e2e_abuse:
name: Tests / E2E / Abuse (${{ matrix.mode }})
if: false
runs-on: ubuntu-latest
needs: [build, matrix]
permissions:
@@ -568,6 +514,7 @@ jobs:
e2e_screenshots:
name: Tests / E2E / Screenshots (${{ matrix.mode }})
if: false
runs-on: ubuntu-latest
needs: [build, matrix]
permissions:
@@ -637,6 +584,7 @@ jobs:
benchmark:
name: Benchmark
if: false
runs-on: ubuntu-latest
needs: build
permissions:
+1 -1
View File
@@ -1346,7 +1346,7 @@ return function (Container $container): void {
} elseif (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int) $project->getSequence())
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
+1 -1
View File
@@ -237,7 +237,7 @@ return function (Container $container): void {
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
$database
->setSharedTables(true)
->setTenant((int) $projectDocument->getSequence())
->setTenant($projectDocument->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
Generated
+6 -6
View File
@@ -4271,16 +4271,16 @@
},
{
"name": "utopia-php/framework",
"version": "0.34.17",
"version": "0.34.18",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "d3e4143b8b06d9823d0c29a06dacefa5a1b93677"
"reference": "c8e7e8fc9b9b68aa874e365c83010fefe8ae8ccc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/d3e4143b8b06d9823d0c29a06dacefa5a1b93677",
"reference": "d3e4143b8b06d9823d0c29a06dacefa5a1b93677",
"url": "https://api.github.com/repos/utopia-php/http/zipball/c8e7e8fc9b9b68aa874e365c83010fefe8ae8ccc",
"reference": "c8e7e8fc9b9b68aa874e365c83010fefe8ae8ccc",
"shasum": ""
},
"require": {
@@ -4319,9 +4319,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.34.17"
"source": "https://github.com/utopia-php/http/tree/0.34.18"
},
"time": "2026-04-06T04:40:23+00:00"
"time": "2026-04-07T08:06:39+00:00"
},
{
"name": "utopia-php/http",
@@ -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]));
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Project\Services;
use Appwrite\Platform\Modules\Project\Http\Init;
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;
@@ -25,5 +26,7 @@ class Http extends Service
$this->addAction(GetVariable::getName(), new GetVariable());
$this->addAction(DeleteVariable::getName(), new DeleteVariable());
$this->addAction(UpdateVariable::getName(), new UpdateVariable());
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
}
}
@@ -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());
+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;
}