Merge branch '1.8.x' of https://github.com/appwrite/appwrite into migrations-cleanup

This commit is contained in:
fogelito 2026-01-06 10:45:19 +02:00
commit 2a679ead32
10 changed files with 335 additions and 8 deletions

View file

@ -330,7 +330,18 @@ $platformCollections = [
'default' => null,
'array' => false,
'filters' => ['datetime'],
]
],
[
'$id' => ID::custom('labels'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
],
'indexes' => [
[

View file

@ -204,6 +204,7 @@ App::post('/v1/projects')
'accessedAt' => DateTime::now(),
'search' => implode(' ', [$projectId, $name]),
'database' => $dsn,
'labels' => [],
]));
} catch (Duplicate) {
throw new Exception(Exception::PROJECT_ALREADY_EXISTS);

View file

@ -360,6 +360,8 @@ $register->set('smtp', function () {
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
$mail->SMTPAutoTLS = false;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10; /* Connection timeout */
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
$from = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$email = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);

View file

@ -0,0 +1,86 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Http\Projects\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\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectLabels';
}
protected function getQueriesValidator(): Validator
{
return new Projects();
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/projects/:projectId/labels')
->desc('Update project labels')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'updateLabels',
description: <<<EOT
Update the project labels by its unique ID. Labels can be used to easily filter projects in an organization.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
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')
->callback($this->action(...));
}
/**
* @param array<string> $labels
*/
public function action(
string $projectId,
array $labels,
Response $response,
Database $dbForPlatform
): void {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$project->setAttribute('labels', (array) \array_values(\array_unique($labels)));
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project);
$response->dynamic($project, Response::MODEL_PROJECT);
}
}

View file

@ -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\Labels\Update as UpdateProjectLabels;
use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects;
use Utopia\Platform\Service;
@ -22,5 +23,6 @@ class Http extends Service
$this->addAction(DeleteDevKey::getName(), new DeleteDevKey());
$this->addAction(ListProjects::getName(), new ListProjects());
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
}
}

View file

@ -148,7 +148,7 @@ class Deletes extends Action
break;
case DELETE_TYPE_AUDIT:
if (!$project->isEmpty()) {
$this->deleteAuditLogs($project, $auditRetention, $getAudit);
$this->deleteAuditLogs($project, $getAudit, $auditRetention);
}
break;
case DELETE_TYPE_REALTIME:
@ -783,14 +783,13 @@ class Deletes extends Action
}
/**
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $auditRetention
* @param Document $project
* @param callable $getAudit
* @param string $auditRetention
* @return void
* @throws Exception
*/
private function deleteAuditLogs(Document $project, string $auditRetention, callable $getAudit): void
private function deleteAuditLogs(Document $project, callable $getAudit, string $auditRetention): void
{
$projectId = $project->getId();
/** @var Audit $audit */

View file

@ -68,7 +68,8 @@ class Mails extends Action
throw new Exception('Skipped mail processing. No SMTP configuration has been set.');
}
$log->addTag('type', empty($smtp) ? 'cloud' : 'smtp');
$type = empty($smtp) ? 'cloud' : 'smtp';
$log->addTag('type', $type);
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN');
@ -182,6 +183,9 @@ class Mails extends Action
try {
$mail->send();
} catch (\Throwable $error) {
if ($type === 'smtp') {
throw new Exception('Error sending mail: ' . $error->getMessage(), 401);
}
throw new Exception('Error sending mail: ' . $error->getMessage(), 500);
}
}
@ -209,6 +213,8 @@ class Mails extends Action
$mail->SMTPSecure = $smtp['secure'];
$mail->SMTPAutoTLS = false;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10; /* Connection timeout */
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
$mail->setFrom($smtp['senderEmail'], $smtp['senderName']);

View file

@ -6,7 +6,8 @@ class Projects extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'teamId'
'teamId',
'labels',
];
/**

View file

@ -276,6 +276,13 @@ class Project extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('labels', [
'type' => self::TYPE_STRING,
'description' => 'Labels for the project.',
'default' => [],
'example' => ['vip'],
'array' => true,
])
;
$services = Config::getParam('services', []);

View file

@ -5382,4 +5382,216 @@ class ProjectsConsoleClientTest extends Scope
/**
* Devkeys Tests ends here ------------------------------------------------
*/
public function testProjectLabels(): void
{
// Setup: Prepare team
$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'];
// Setup: Prepare project
$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' => 'Test project - Labels 1',
'teamId' => $teamId,
'region' => System::getEnv('_APP_REGION', 'default')
]);
$this->assertEquals(201, $project['headers']['status-code']);
$this->assertIsArray($project['body']['labels']);
$this->assertCount(0, $project['body']['labels']);
$projectId = $project['body']['$id'];
// Apply labels
$project = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId . '/labels', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'labels' => ['vip', 'imagine', 'blocked']
]);
$this->assertEquals(200, $project['headers']['status-code']);
$this->assertIsArray($project['body']['labels']);
$this->assertCount(3, $project['body']['labels']);
$this->assertEquals('vip', $project['body']['labels'][0]);
$this->assertEquals('imagine', $project['body']['labels'][1]);
$this->assertEquals('blocked', $project['body']['labels'][2]);
// Update labels
$project = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId . '/labels', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'labels' => ['nonvip', 'imagine']
]);
$this->assertEquals(200, $project['headers']['status-code']);
$this->assertIsArray($project['body']['labels']);
$this->assertCount(2, $project['body']['labels']);
$this->assertEquals('nonvip', $project['body']['labels'][0]);
$this->assertEquals('imagine', $project['body']['labels'][1]);
// Filter by labels
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['nonvip'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(1, $projects['body']['total']);
$this->assertEquals($projectId, $projects['body']['projects'][0]['$id']);
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(0, $projects['body']['total']);
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['imagine'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(1, $projects['body']['total']);
$this->assertEquals($projectId, $projects['body']['projects'][0]['$id']);
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['nonvip', 'imagine'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(1, $projects['body']['total']);
$this->assertEquals($projectId, $projects['body']['projects'][0]['$id']);
// Setup: Second project with only imagine label
$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' => 'Test project - Labels 2',
'teamId' => $teamId,
'region' => System::getEnv('_APP_REGION', 'default')
]);
$this->assertEquals(201, $project['headers']['status-code']);
$this->assertIsArray($project['body']['labels']);
$this->assertCount(0, $project['body']['labels']);
$projectId2 = $project['body']['$id'];
$project = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId2 . '/labels', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'labels' => ['vip', 'imagine']
]);
$this->assertEquals(200, $project['headers']['status-code']);
$this->assertIsArray($project['body']['labels']);
$this->assertCount(2, $project['body']['labels']);
$this->assertEquals('vip', $project['body']['labels'][0]);
$this->assertEquals('imagine', $project['body']['labels'][1]);
// List of imagine has both
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['imagine'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(2, $projects['body']['total']);
$this->assertEquals($projectId, $projects['body']['projects'][0]['$id']);
$this->assertEquals($projectId2, $projects['body']['projects'][1]['$id']);
// List of vip only has second
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(1, $projects['body']['total']);
$this->assertEquals($projectId2, $projects['body']['projects'][0]['$id']);
// List of vip and imagine has second
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip'])->toString(),
Query::contains('labels', ['imagine'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(1, $projects['body']['total']);
$this->assertEquals($projectId2, $projects['body']['projects'][0]['$id']);
// List of vip or imagine has second
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip', 'imagine'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(2, $projects['body']['total']);
$this->assertEquals($projectId, $projects['body']['projects'][0]['$id']);
$this->assertEquals($projectId2, $projects['body']['projects'][1]['$id']);
// Cleanup
$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']);
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $projectId2, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
}
}