From be626ad0fc39195bc9b366a9f1f8f675f83b72fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 15 Dec 2025 17:33:10 +0100 Subject: [PATCH 1/6] Add deployment retention to sites and functions --- app/config/collections/projects.php | 22 +++ app/controllers/mock.php | 43 +++++ app/init/constants.php | 1 + .../Functions/Http/Functions/Create.php | 3 + .../Functions/Http/Functions/Update.php | 5 +- .../Modules/Sites/Http/Sites/Create.php | 3 + .../Modules/Sites/Http/Sites/Update.php | 3 + src/Appwrite/Platform/Tasks/Maintenance.php | 25 ++- src/Appwrite/Platform/Workers/Deletes.php | 58 ++++++- src/Appwrite/Utopia/Response/Model/Func.php | 6 + src/Appwrite/Utopia/Response/Model/Site.php | 6 + .../Functions/FunctionsCustomServerTest.php | 160 ++++++++++++++++++ 12 files changed, 328 insertions(+), 7 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 65441be5f4..5e1a9e81ea 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -567,6 +567,17 @@ return [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('deploymentRetention'), + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('deploymentInternalId'), 'type' => Database::VAR_STRING, @@ -1080,6 +1091,17 @@ return [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('deploymentRetention'), + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => 0, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('deploymentInternalId'), 'type' => Database::VAR_STRING, diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 40ddae8f30..78e0a3bc65 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -2,6 +2,7 @@ global $utopia, $request, $response; +use Utopia\Database\Validator\Datetime as DatetimeValidator; use Appwrite\Extend\Exception; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; @@ -198,6 +199,48 @@ App::post('/v1/mock/api-key-unprefixed') ->dynamic($key, Response::MODEL_KEY); }); +App::post('/v1/mock/time-travels') + ->desc('Create a time-travel to chane $createdAt') + ->groups(['mock', 'api', 'projects']) + ->label('scope', 'public') + ->label('docs', false) + ->param('projectId', '', new UID(), 'Project ID.') + ->param('resourceType', '', new WhiteList(['function', 'site']), 'Type of resource.') + ->param('resourceId', '', new UID(), 'ID of resource.') + ->param('createdAt', '', new DatetimeValidator(), 'New value for $createdAt') + ->inject('response') + ->inject('getProjectDB') + ->inject('dbForPlatform') + ->action(function (string $projectId, string $resourceType, string $resourceId, string $createdAt, Response $response, callable $getProjectDB, Database $dbForPlatform) { + $isDevelopment = System::getEnv('_APP_ENV', 'development') === 'development'; + + if (!$isDevelopment) { + throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); + } + + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $collection = match($resourceType) { + 'function' => 'functions', + 'site' => 'sites', + default => throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED) + }; + + /** @var Database $dbForProject */ + $dbForProject = $getProjectDB($project); + + $dbForProject->withPreserveDates(fn () => $dbForProject->updateDocument($collection, $resourceId, new Document([ + '$createdAt' => $createdAt + ])) + ); + + $response->noContent(); + }); + App::get('/v1/mock/github/callback') ->desc('Create installation document using GitHub installation id') ->groups(['mock', 'api', 'vcs']) diff --git a/app/init/constants.php b/app/init/constants.php index e11fdf9a54..9467b7275d 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -85,6 +85,7 @@ const APP_HOSTNAME_INTERNAL = 'appwrite'; const APP_COMPUTE_CPUS_DEFAULT = 0.5; const APP_COMPUTE_MEMORY_DEFAULT = 512; const APP_COMPUTE_SPECIFICATION_DEFAULT = Specification::S_1VCPU_512MB; +const APP_COMPUTE_DEPLOYMENT_MAX_RETENTION = 100 * 365; // 100 years const APP_PLATFORM_SERVER = 'server'; const APP_PLATFORM_CLIENT = 'client'; const APP_PLATFORM_CONSOLE = 'console'; diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 48947ac3e6..21cc9a4d31 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -109,6 +109,7 @@ class Create extends Base ->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true, deprecated: true) ->param('templateRootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.', true, deprecated: true) ->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true, deprecated: true) + ->param('deploymentRetention', 0, new Range(0, APP_COMPUTE_DEPLOYMENT_MAX_RETENTION), 'Days to keep non-active deployments before deletion. Value 0 means all deployments will be kept.', true) ->inject('response') ->inject('dbForProject') ->inject('timelimit') @@ -148,6 +149,7 @@ class Create extends Base string $templateOwner, string $templateRootDirectory, string $templateVersion, + int $deploymentRetention, Response $response, Database $dbForProject, callable $timelimit, @@ -218,6 +220,7 @@ class Create extends Base 'logging' => $logging, 'name' => $name, 'runtime' => $runtime, + 'deploymentRetention' => $deploymentRetention, 'deploymentInternalId' => '', 'deploymentId' => '', 'events' => $events, diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index df5dd3adf5..7e7878df28 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -101,6 +101,7 @@ class Update extends Base System::getEnv('_APP_COMPUTE_CPUS', 0), System::getEnv('_APP_COMPUTE_MEMORY', 0) ), 'Runtime specification for the function executions.', true, ['plan']) + ->param('deploymentRetention', 0, new Range(0, APP_COMPUTE_DEPLOYMENT_MAX_RETENTION), 'Days to keep non-active deployments before deletion. Value 0 means all deployments will be kept.', true) ->inject('request') ->inject('response') ->inject('dbForProject') @@ -133,6 +134,7 @@ class Update extends Base string $providerRootDirectory, string $buildSpecification, string $runtimeSpecification, + int $deploymentRetention, Request $request, Response $response, Database $dbForProject, @@ -222,7 +224,7 @@ class Update extends Base 'resourceId' => $function->getId(), 'resourceInternalId' => $function->getSequence(), 'resourceType' => 'function', - 'providerPullRequestIds' => [] + 'providerPullRequestIds' => [], ])); $repositoryId = $repository->getId(); @@ -275,6 +277,7 @@ class Update extends Base 'entrypoint' => $entrypoint, 'commands' => $commands, 'scopes' => $scopes, + 'deploymentRetention' => $deploymentRetention, 'installationId' => $installation->getId(), 'installationInternalId' => $installation->getSequence(), 'providerRepositoryId' => $providerRepositoryId, diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index f535c90de6..5243a88469 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -91,6 +91,7 @@ class Create extends Base System::getEnv('_APP_COMPUTE_CPUS', 0), System::getEnv('_APP_COMPUTE_MEMORY', 0) ), 'Runtime specification for the function SSR executions.', true, ['plan']) + ->param('deploymentRetention', 0, new Range(0, APP_COMPUTE_DEPLOYMENT_MAX_RETENTION), 'Days to keep non-active deployments before deletion. Value 0 means all deployments will be kept.', true) ->inject('response') ->inject('dbForProject') ->inject('project') @@ -120,6 +121,7 @@ class Create extends Base string $providerRootDirectory, string $buildSpecification, string $runtimeSpecification, + int $deploymentRetention, Response $response, Database $dbForProject, Document $project, @@ -154,6 +156,7 @@ class Create extends Base 'logging' => $logging, 'name' => $name, 'framework' => $framework, + 'deploymentRetention' => $deploymentRetention, 'deploymentInternalId' => '', 'deploymentId' => '', 'timeout' => $timeout, diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index e7c2df0c53..887b7f4aa6 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -95,6 +95,7 @@ class Update extends Base System::getEnv('_APP_COMPUTE_CPUS', 0), System::getEnv('_APP_COMPUTE_MEMORY', 0) ), 'Runtime specification for the function SSR executions.', true, ['plan']) + ->param('deploymentRetention', 0, new Range(0, APP_COMPUTE_DEPLOYMENT_MAX_RETENTION), 'Days to keep non-active deployments before deletion. Value 0 means all deployments will be kept.', true) ->inject('request') ->inject('response') ->inject('dbForProject') @@ -128,6 +129,7 @@ class Update extends Base string $providerRootDirectory, string $buildSpecification, string $runtimeSpecification, + int $deploymentRetention, Request $request, Response $response, Database $dbForProject, @@ -270,6 +272,7 @@ class Update extends Base 'logging' => $logging, 'live' => $live, 'timeout' => $timeout, + 'deploymentRetention' => $deploymentRetention, 'installCommand' => $installCommand, 'buildCommand' => $buildCommand, 'startCommand' => $startCommand, diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index f5785d0bb4..91a4860e7f 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -13,6 +13,7 @@ use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Platform\Action; use Utopia\System\System; +use Utopia\Validator\WhiteList; class Maintenance extends Action { @@ -25,6 +26,7 @@ class Maintenance extends Action { $this ->desc('Schedules maintenance tasks and publishes them to our queues') + ->param('type', 'loop', new WhiteList(['loop', 'trigger']), 'How to run task. "loop" is meant for container entrypoint, and "trigger" for manual execution.') ->inject('dbForPlatform') ->inject('console') ->inject('queueForCertificates') @@ -32,8 +34,13 @@ class Maintenance extends Action ->callback($this->action(...)); } - public function action(Database $dbForPlatform, Document $console, Certificate $queueForCertificates, Delete $queueForDeletes): void + public function action(string $type, Database $dbForPlatform, Document $console, Certificate $queueForCertificates, Delete $queueForDeletes): void { + $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; + if ($type === 'trigger' && $isProduction) { + throw new \Exception('Triggering maintenance task is not allowed in production. Please use type=loop instead.'); + } + Console::title('Maintenance V1'); Console::success(APP_NAME . ' maintenance process v1 has started'); @@ -57,9 +64,7 @@ class Maintenance extends Action $delay = $next->getTimestamp() - $now->getTimestamp(); } - Console::info('Setting loop start time to ' . $next->format("Y-m-d H:i:s.v") . '. Delaying for ' . $delay . ' seconds.'); - - Console::loop(function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $queueForCertificates) { + $action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $queueForCertificates) { $time = DatabaseDateTime::now(); Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds"); @@ -96,7 +101,17 @@ class Maintenance extends Action $this->notifyDeleteCache($cacheRetention, $queueForDeletes); $this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes); $this->notifyDeleteCSVExports($queueForDeletes); - }, $interval, $delay); + }; + + if ($type === 'loop') { + Console::info('Setting loop start time to ' . $next->format("Y-m-d H:i:s.v") . '. Delaying for ' . $delay . ' seconds.'); + + Console::loop(function () use ($action) { + $action(); + }, $interval, $delay); + } elseif ($type === 'trigger') { + $action(); + } } private function notifyDeleteConnections(Delete $queueForDeletes): void diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 7df2770ac6..9146e8e376 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -6,6 +6,7 @@ use Appwrite\Auth\Auth; use Appwrite\Certificates\Adapter as CertificatesAdapter; use Appwrite\Deletes\Identities; use Appwrite\Deletes\Targets; +use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Extend\Exception; use Executor\Executor; use Throwable; @@ -63,6 +64,7 @@ class Deletes extends Action ->inject('executionRetention') ->inject('auditRetention') ->inject('log') + ->inject('queueForDeletes') ->callback($this->action(...)); } @@ -85,7 +87,8 @@ class Deletes extends Action Executor $executor, string $executionRetention, string $auditRetention, - Log $log + Log $log, + DeleteEvent $queueForDeletes, ): void { $payload = $message->getPayload() ?? []; @@ -189,6 +192,7 @@ class Deletes extends Action $this->deleteUsageStats($project, $getProjectDB, $getLogsDB, $hourlyUsageRetentionDatetime); $this->deleteExpiredSessions($project, $getProjectDB); $this->deleteExpiredTransactions($project, $getProjectDB); + $this->deleteOldDeployments($queueForDeletes, $project, $getProjectDB); break; default: throw new \Exception('No delete operation for type: ' . \strval($type)); @@ -308,6 +312,58 @@ class Deletes extends Action Targets::delete($getProjectDB($project), Query::equal('sessionInternalId', [$session->getSequence()])); } + private function deleteOldDeployments(DeleteEvent $queueForDeletes, Document $project, callable $getProjectDB): void + { + /* @var $dbForProject Database */ + $dbForProject = $getProjectDB($project); + + $removalCallback = function (Document $resource) use ($dbForProject, $queueForDeletes) { + $retention = $resource->getAttribute('deploymentRetention', 0); + + // 0 means unlimited - never delete + if ($retention === 0) { + return; + } + + $activeDeploymentId = $resource->getAttribute('deploymentId', ''); + + $queries = [ + Query::createdBefore(DateTime::addSeconds(new \DateTime(), -1 * $retention * 24 * 60 * 60)), + Query::orderDesc('$createdAt'), + ]; + + if (!empty($activeDeploymentId)) { + $queries[] = Query::notEqual('$id', $activeDeploymentId); + } + + $this->deleteByGroup( + 'deployments', + $queries, + $dbForProject, + function (Document $deployment) use ($queueForDeletes) { + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($deployment) + ->trigger(); + } + ); + }; + + $this->listByGroup( + 'functions', + [], + $dbForProject, + $removalCallback + ); + + $this->listByGroup( + 'sites', + [], + $dbForProject, + $removalCallback + ); + } + /** * @param Document $project * @param callable $getProjectDB diff --git a/src/Appwrite/Utopia/Response/Model/Func.php b/src/Appwrite/Utopia/Response/Model/Func.php index 0d7e80849d..3aea364fe5 100644 --- a/src/Appwrite/Utopia/Response/Model/Func.php +++ b/src/Appwrite/Utopia/Response/Model/Func.php @@ -65,6 +65,12 @@ class Func extends Model 'default' => '', 'example' => 'python-3.8', ]) + ->addRule('deploymentRetention', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'How many days to keep the non-active deployments before they will be automatically deleted.', + 'default' => 0, + 'example' => 7, + ]) ->addRule('deploymentId', [ 'type' => self::TYPE_STRING, 'description' => 'Function\'s active deployment ID.', diff --git a/src/Appwrite/Utopia/Response/Model/Site.php b/src/Appwrite/Utopia/Response/Model/Site.php index 15605984f0..941b6104df 100644 --- a/src/Appwrite/Utopia/Response/Model/Site.php +++ b/src/Appwrite/Utopia/Response/Model/Site.php @@ -58,6 +58,12 @@ class Site extends Model 'default' => '', 'example' => 'react', ]) + ->addRule('deploymentRetention', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'How many days to keep the non-active deployments before they will be automatically deleted.', + 'default' => 0, + 'example' => 7, + ]) ->addRule('deploymentId', [ 'type' => self::TYPE_STRING, 'description' => 'Site\'s active deployment ID.', diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index d60737be09..e0a4c05ad2 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2629,4 +2629,164 @@ class FunctionsCustomServerTest extends Scope $this->cleanupFunction($functionId); } + + public function testFunctionDeploymentRetention(): void + { + $functionIds = []; + + // Default + $response = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test retention function', + 'runtime' => 'node-22', + ]); + $this->assertSame(201, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['deploymentRetention']); + $functionIds[] = $response['body']['$id']; + + $response = $this->getFunction($response['body']['$id']); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['deploymentRetention']); + + // Success values + $response = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test retention function', + 'runtime' => 'node-22', + 'deploymentRetention' => 0 + ]); + $this->assertSame(201, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['deploymentRetention']); + $functionIds[] = $response['body']['$id']; + + $response = $this->getFunction($response['body']['$id']); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['deploymentRetention']); + + $response = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test retention function', + 'runtime' => 'node-22', + 'deploymentRetention' => 180 + ]); + $this->assertSame(201, $response['headers']['status-code']); + $this->assertSame(180, $response['body']['deploymentRetention']); + $functionIds[] = $response['body']['$id']; + + $response = $this->getFunction($response['body']['$id']); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(180, $response['body']['deploymentRetention']); + + // Failure values + $response = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test retention function', + 'runtime' => 'node-22', + 'deploymentRetention' => 999999 + ]); + $this->assertSame(400, $response['headers']['status-code']); + + $response = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test retention function', + 'runtime' => 'node-22', + 'deploymentRetention' => -1 + ]); + $this->assertSame(400, $response['headers']['status-code']); + + // Update flow + $response = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test retention function', + 'runtime' => 'node-22', + 'deploymentRetention' => 180 + ]); + $this->assertSame(201, $response['headers']['status-code']); + $this->assertSame(180, $response['body']['deploymentRetention']); + $functionIds[] = $response['body']['$id']; + $functionIdForUpdate = $response['body']['$id']; + + $response = $this->updateFunction($functionIdForUpdate, [ + 'name' => 'Test retention function', + 'deploymentRetention' => 90 + ]); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(90, $response['body']['deploymentRetention']); + + $response = $this->getFunction($functionIdForUpdate); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(90, $response['body']['deploymentRetention']); + + $response = $this->updateFunction($functionIdForUpdate, [ + 'name' => 'Test retention function', + ]); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['deploymentRetention']); + + $response = $this->getFunction($functionIdForUpdate); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['deploymentRetention']); + + // Failed update flow + $response = $this->updateFunction($functionIdForUpdate, [ + 'name' => 'Test retention function', + 'deploymentRetention' => -1 + ]); + $this->assertSame(400, $response['headers']['status-code']); + + $response = $this->updateFunction($functionIdForUpdate, [ + 'name' => 'Test retention function', + 'deploymentRetention' => 999999 + ]); + $this->assertSame(400, $response['headers']['status-code']); + + foreach ($functionIds as $functionId) { + $this->cleanupFunction($functionId); + } + } + + public function testFunctionDeploymentRetentionWithMaintenance(): void + { + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test retention function', + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'deploymentRetention' => 180 + ]); + $this->assertNotEmpty($functionId); + + $deploymentIdInactive = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('node'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentIdInactive); + + $deploymentIdInactiveOld = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('node'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentIdInactiveOld); + + $deploymentIdActive = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('node'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentIdActive); + + $response = $this->client->call(Client::METHOD_POST, '/v1/mock/time-travels', array_merge([ + 'content-type' => 'application/json', + ]), [ + 'projectId' => $this->getProject()['$id'], + 'resourceType' => 'function', + 'resourceId' => $deploymentIdInactiveOld, + 'createdAt' => '2020-01-01T00:00:00Z' // More than 180 days ago + ]); + $this->assertSame(204, $response['headers']['status-code']); + + // TODO: Trigger maintenance + // TODO: Assert eventuelly, 2 deployments only + + $this->cleanupFunction($functionId); + } } From 0ec755911efbdc89e9c8d3e3f0c719c8dc1962d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 15 Dec 2025 18:44:27 +0100 Subject: [PATCH 2/6] Finish maintenance test --- app/controllers/mock.php | 30 +++++----- docker-compose.yml | 1 + src/Appwrite/Platform/Tasks/Maintenance.php | 5 -- .../Functions/FunctionsConsoleClientTest.php | 55 +++++++++++++++++++ .../Functions/FunctionsCustomServerTest.php | 45 --------------- 5 files changed, 71 insertions(+), 65 deletions(-) diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 78e0a3bc65..733341968a 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -2,7 +2,6 @@ global $utopia, $request, $response; -use Utopia\Database\Validator\Datetime as DatetimeValidator; use Appwrite\Extend\Exception; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; @@ -13,6 +12,7 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; +use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Locale\Locale; use Utopia\System\System; @@ -201,11 +201,11 @@ App::post('/v1/mock/api-key-unprefixed') App::post('/v1/mock/time-travels') ->desc('Create a time-travel to chane $createdAt') - ->groups(['mock', 'api', 'projects']) + ->groups(['mock', 'api']) ->label('scope', 'public') ->label('docs', false) ->param('projectId', '', new UID(), 'Project ID.') - ->param('resourceType', '', new WhiteList(['function', 'site']), 'Type of resource.') + ->param('resourceType', '', new WhiteList(['deployment']), 'Type of resource.') ->param('resourceId', '', new UID(), 'ID of resource.') ->param('createdAt', '', new DatetimeValidator(), 'New value for $createdAt') ->inject('response') @@ -213,30 +213,30 @@ App::post('/v1/mock/time-travels') ->inject('dbForPlatform') ->action(function (string $projectId, string $resourceType, string $resourceId, string $createdAt, Response $response, callable $getProjectDB, Database $dbForPlatform) { $isDevelopment = System::getEnv('_APP_ENV', 'development') === 'development'; - + if (!$isDevelopment) { throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); } $project = $dbForPlatform->getDocument('projects', $projectId); - + if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - + $collection = match($resourceType) { - 'function' => 'functions', - 'site' => 'sites', + 'deployment' => 'deployments', default => throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED) }; - - /** @var Database $dbForProject */ + + /** @var Database $dbForProject */ $dbForProject = $getProjectDB($project); - - $dbForProject->withPreserveDates(fn () => $dbForProject->updateDocument($collection, $resourceId, new Document([ - '$createdAt' => $createdAt - ])) - ); + + $update = new Document([ + '$createdAt' => $createdAt, + ]); + + $dbForProject->withPreserveDates(fn () => $dbForProject->updateDocument($collection, $resourceId, $update)); $response->noContent(); }); diff --git a/docker-compose.yml b/docker-compose.yml index ede1011af3..c50dc3adcc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,6 +71,7 @@ services: - traefik.http.routers.appwrite_api_https.service=appwrite_api - traefik.http.routers.appwrite_api_https.tls=true volumes: + - ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database - /var/run/docker.sock:/var/run/docker.sock # Only needed for tests - ./docker-compose.yml:/usr/src/code/docker-compose.yml # Only needed for tests - ./.env:/usr/src/code/.env # Only needed for tests diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index 91a4860e7f..7b9da50e30 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -36,11 +36,6 @@ class Maintenance extends Action public function action(string $type, Database $dbForPlatform, Document $console, Certificate $queueForCertificates, Delete $queueForDeletes): void { - $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; - if ($type === 'trigger' && $isProduction) { - throw new \Exception('Triggering maintenance task is not allowed in production. Please use type=loop instead.'); - } - Console::title('Maintenance V1'); Console::success(APP_NAME . ' maintenance process v1 has started'); diff --git a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php index 9dae8efdb4..2125e03bc0 100644 --- a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php @@ -6,6 +6,7 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideConsole; +use Utopia\CLI\Console; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; @@ -537,4 +538,58 @@ class FunctionsConsoleClientTest extends Scope $this->cleanupFunction($functionId); } + + public function testFunctionDeploymentRetentionWithMaintenance(): void + { + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test retention function', + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'deploymentRetention' => 180 + ]); + $this->assertNotEmpty($functionId); + + $deploymentIdInactive = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('node'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentIdInactive); + + $deploymentIdInactiveOld = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('node'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentIdInactiveOld); + + $deploymentIdActive = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('node'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentIdActive); + + $response = $this->client->call(Client::METHOD_POST, '/mock/time-travels', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'projectId' => $this->getProject()['$id'], + 'resourceType' => 'deployment', + 'resourceId' => $deploymentIdInactiveOld, + 'createdAt' => '2020-01-01T00:00:00Z' // More than 180 days ago + ]); + $this->assertSame(204, $response['headers']['status-code']); + + $stdout = ''; + $stderr = ''; + $code = Console::execute("docker exec appwrite-task-maintenance maintenance --type=trigger", '', $stdout, $stderr); + $this->assertSame(0, $code, "Maintenance command failed with code $code: $stderr ($stdout)"); + + $this->assertEventually(function () use ($functionId) { + $response = $this->listDeployments($functionId); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(2, $response['body']['total']); + }); + + $this->cleanupFunction($functionId); + } } diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index e0a4c05ad2..a3746ad362 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2744,49 +2744,4 @@ class FunctionsCustomServerTest extends Scope $this->cleanupFunction($functionId); } } - - public function testFunctionDeploymentRetentionWithMaintenance(): void - { - $functionId = $this->setupFunction([ - 'functionId' => ID::unique(), - 'name' => 'Test retention function', - 'runtime' => 'node-22', - 'entrypoint' => 'index.js', - 'deploymentRetention' => 180 - ]); - $this->assertNotEmpty($functionId); - - $deploymentIdInactive = $this->setupDeployment($functionId, [ - 'code' => $this->packageFunction('node'), - 'activate' => true - ]); - $this->assertNotEmpty($deploymentIdInactive); - - $deploymentIdInactiveOld = $this->setupDeployment($functionId, [ - 'code' => $this->packageFunction('node'), - 'activate' => true - ]); - $this->assertNotEmpty($deploymentIdInactiveOld); - - $deploymentIdActive = $this->setupDeployment($functionId, [ - 'code' => $this->packageFunction('node'), - 'activate' => true - ]); - $this->assertNotEmpty($deploymentIdActive); - - $response = $this->client->call(Client::METHOD_POST, '/v1/mock/time-travels', array_merge([ - 'content-type' => 'application/json', - ]), [ - 'projectId' => $this->getProject()['$id'], - 'resourceType' => 'function', - 'resourceId' => $deploymentIdInactiveOld, - 'createdAt' => '2020-01-01T00:00:00Z' // More than 180 days ago - ]); - $this->assertSame(204, $response['headers']['status-code']); - - // TODO: Trigger maintenance - // TODO: Assert eventuelly, 2 deployments only - - $this->cleanupFunction($functionId); - } } From 6cbc79026f83e5639298d608956b9816a6e17196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 15 Dec 2025 18:55:34 +0100 Subject: [PATCH 3/6] Finish site tests for auto deletion --- docker-compose.yml | 1 - .../Services/Sites/SitesConsoleClientTest.php | 55 ++++++++ .../Services/Sites/SitesCustomServerTest.php | 130 ++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index c50dc3adcc..ede1011af3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,7 +71,6 @@ services: - traefik.http.routers.appwrite_api_https.service=appwrite_api - traefik.http.routers.appwrite_api_https.tls=true volumes: - - ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database - /var/run/docker.sock:/var/run/docker.sock # Only needed for tests - ./docker-compose.yml:/usr/src/code/docker-compose.yml # Only needed for tests - ./.env:/usr/src/code/.env # Only needed for tests diff --git a/tests/e2e/Services/Sites/SitesConsoleClientTest.php b/tests/e2e/Services/Sites/SitesConsoleClientTest.php index 227e36a50e..e5ffc6d9c4 100644 --- a/tests/e2e/Services/Sites/SitesConsoleClientTest.php +++ b/tests/e2e/Services/Sites/SitesConsoleClientTest.php @@ -6,6 +6,7 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideConsole; +use Utopia\CLI\Console; use Utopia\Database\Helpers\ID; class SitesConsoleClientTest extends Scope @@ -139,4 +140,58 @@ class SitesConsoleClientTest extends Scope $this->cleanupSite($siteId); } + + public function testSiteDeploymentRetentionWithMaintenance(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Test retention site', + 'framework' => 'other', + 'deploymentRetention' => 180, + 'buildRuntime' => 'node-22', + ]); + $this->assertNotEmpty($siteId); + + $deploymentIdInactive = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentIdInactive); + + $deploymentIdInactiveOld = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentIdInactiveOld); + + $deploymentIdActive = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentIdActive); + + $response = $this->client->call(Client::METHOD_POST, '/mock/time-travels', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'projectId' => $this->getProject()['$id'], + 'resourceType' => 'deployment', + 'resourceId' => $deploymentIdInactiveOld, + 'createdAt' => '2020-01-01T00:00:00Z' // More than 180 days ago + ]); + $this->assertSame(204, $response['headers']['status-code']); + + $stdout = ''; + $stderr = ''; + $code = Console::execute("docker exec appwrite-task-maintenance maintenance --type=trigger", '', $stdout, $stderr); + $this->assertSame(0, $code, "Maintenance command failed with code $code: $stderr ($stdout)"); + + $this->assertEventually(function () use ($siteId) { + $response = $this->listDeployments($siteId); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(2, $response['body']['total']); + }); + + $this->cleanupSite($siteId); + } } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 82f1639836..05b259a9ae 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -3076,4 +3076,134 @@ class SitesCustomServerTest extends Scope $this->cleanupSite($siteId); } + + public function testSiteDeploymentRetention(): void + { + $siteIds = []; + + // Default + $response = $this->createSite([ + 'siteId' => ID::unique(), + 'name' => 'Test retention site', + 'framework' => 'other', + 'buildRuntime' => 'node-22', + ]); + $this->assertSame(201, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['deploymentRetention']); + $siteId[] = $response['body']['$id']; + + $response = $this->getSite($response['body']['$id']); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['deploymentRetention']); + + // Success values + $response = $this->createSite([ + 'siteId' => ID::unique(), + 'name' => 'Test retention site', + 'framework' => 'other', + 'buildRuntime' => 'node-22', + 'deploymentRetention' => 0 + ]); + $this->assertSame(201, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['deploymentRetention']); + $siteId[] = $response['body']['$id']; + + $response = $this->getSite($response['body']['$id']); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['deploymentRetention']); + + $response = $this->createSite([ + 'siteId' => ID::unique(), + 'name' => 'Test retention site', + 'framework' => 'other', + 'buildRuntime' => 'node-22', + 'deploymentRetention' => 180 + ]); + $this->assertSame(201, $response['headers']['status-code']); + $this->assertSame(180, $response['body']['deploymentRetention']); + $siteId[] = $response['body']['$id']; + + $response = $this->getSite($response['body']['$id']); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(180, $response['body']['deploymentRetention']); + + // Failure values + $response = $this->createSite([ + 'siteId' => ID::unique(), + 'name' => 'Test retention site', + 'framework' => 'other', + 'buildRuntime' => 'node-22', + 'deploymentRetention' => 999999 + ]); + $this->assertSame(400, $response['headers']['status-code']); + + $response = $this->createSite([ + 'siteId' => ID::unique(), + 'name' => 'Test retention site', + 'framework' => 'other', + 'buildRuntime' => 'node-22', + 'deploymentRetention' => -1 + ]); + $this->assertSame(400, $response['headers']['status-code']); + + // Update flow + $response = $this->createSite([ + 'siteId' => ID::unique(), + 'name' => 'Test retention site', + 'framework' => 'other', + 'buildRuntime' => 'node-22', + 'deploymentRetention' => 180 + ]); + $this->assertSame(201, $response['headers']['status-code']); + $this->assertSame(180, $response['body']['deploymentRetention']); + $siteId[] = $response['body']['$id']; + $siteIdToUpdate = $response['body']['$id']; + + $response = $this->updateSite([ + '$id' => $siteIdToUpdate, + 'name' => 'Test retention site', + 'framework' => 'other', + 'deploymentRetention' => 90 + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(90, $response['body']['deploymentRetention']); + + $response = $this->getSite($siteIdToUpdate); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(90, $response['body']['deploymentRetention']); + + $response = $this->updateSite([ + '$id' => $siteIdToUpdate, + 'name' => 'Test retention site', + 'framework' => 'other', + ]); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['deploymentRetention']); + + $response = $this->getSite($siteIdToUpdate); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['deploymentRetention']); + + // Failed update flow + $response = $this->updateSite([ + '$id' => $siteIdToUpdate, + 'name' => 'Test retention site', + 'framework' => 'other', + 'deploymentRetention' => -1 + ]); + $this->assertSame(400, $response['headers']['status-code']); + + $response = $this->updateSite([ + '$id' => $siteIdToUpdate, + 'name' => 'Test retention site', + 'framework' => 'other', + 'deploymentRetention' => 999999 + ]); + $this->assertSame(400, $response['headers']['status-code']); + + foreach ($siteIds as $siteId) { + $this->cleanupSite($siteId); + } + } } From bd2db5e2498bf41e5ce86ca626923b3b9d6f9793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 15 Dec 2025 19:45:57 +0100 Subject: [PATCH 4/6] AI review fixes --- app/controllers/mock.php | 2 +- src/Appwrite/Platform/Workers/Deletes.php | 13 +++++++++++-- tests/e2e/Services/Sites/SitesCustomServerTest.php | 8 ++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 733341968a..d129571e08 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -200,7 +200,7 @@ App::post('/v1/mock/api-key-unprefixed') }); App::post('/v1/mock/time-travels') - ->desc('Create a time-travel to chane $createdAt') + ->desc('Create a time-travel to change $createdAt') ->groups(['mock', 'api']) ->label('scope', 'public') ->label('docs', false) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 9146e8e376..40b57d4578 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -317,7 +317,7 @@ class Deletes extends Action /* @var $dbForProject Database */ $dbForProject = $getProjectDB($project); - $removalCallback = function (Document $resource) use ($dbForProject, $queueForDeletes) { + $removalCallback = function (Document $resource) use ($dbForProject, $queueForDeletes, $project) { $retention = $resource->getAttribute('deploymentRetention', 0); // 0 means unlimited - never delete @@ -327,8 +327,16 @@ class Deletes extends Action $activeDeploymentId = $resource->getAttribute('deploymentId', ''); + $resourceType = match ($resource->getCollection()) { + 'functions' => 'functions', + 'sites' => 'site', + default => null, + }; + $queries = [ Query::createdBefore(DateTime::addSeconds(new \DateTime(), -1 * $retention * 24 * 60 * 60)), + Query::equal('resourceInternalId', [$resource->getSequence()]), + Query::equal('resourceType', [$resourceType]), Query::orderDesc('$createdAt'), ]; @@ -340,10 +348,11 @@ class Deletes extends Action 'deployments', $queries, $dbForProject, - function (Document $deployment) use ($queueForDeletes) { + function (Document $deployment) use ($queueForDeletes, $project) { $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($deployment) + ->setProject($project) ->trigger(); } ); diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 05b259a9ae..eb0900ed5e 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -3090,7 +3090,7 @@ class SitesCustomServerTest extends Scope ]); $this->assertSame(201, $response['headers']['status-code']); $this->assertSame(0, $response['body']['deploymentRetention']); - $siteId[] = $response['body']['$id']; + $siteIds[] = $response['body']['$id']; $response = $this->getSite($response['body']['$id']); $this->assertSame(200, $response['headers']['status-code']); @@ -3106,7 +3106,7 @@ class SitesCustomServerTest extends Scope ]); $this->assertSame(201, $response['headers']['status-code']); $this->assertSame(0, $response['body']['deploymentRetention']); - $siteId[] = $response['body']['$id']; + $siteIds[] = $response['body']['$id']; $response = $this->getSite($response['body']['$id']); $this->assertSame(200, $response['headers']['status-code']); @@ -3121,7 +3121,7 @@ class SitesCustomServerTest extends Scope ]); $this->assertSame(201, $response['headers']['status-code']); $this->assertSame(180, $response['body']['deploymentRetention']); - $siteId[] = $response['body']['$id']; + $siteIds[] = $response['body']['$id']; $response = $this->getSite($response['body']['$id']); $this->assertSame(200, $response['headers']['status-code']); @@ -3156,7 +3156,7 @@ class SitesCustomServerTest extends Scope ]); $this->assertSame(201, $response['headers']['status-code']); $this->assertSame(180, $response['body']['deploymentRetention']); - $siteId[] = $response['body']['$id']; + $siteIds[] = $response['body']['$id']; $siteIdToUpdate = $response['body']['$id']; $response = $this->updateSite([ From a3a5e05b5c3b469bccdd1f9aac867e3781eff659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 15 Dec 2025 20:13:45 +0100 Subject: [PATCH 5/6] bug fix --- src/Appwrite/Platform/Workers/Deletes.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 40b57d4578..c3dff514e0 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -327,16 +327,10 @@ class Deletes extends Action $activeDeploymentId = $resource->getAttribute('deploymentId', ''); - $resourceType = match ($resource->getCollection()) { - 'functions' => 'functions', - 'sites' => 'site', - default => null, - }; - $queries = [ Query::createdBefore(DateTime::addSeconds(new \DateTime(), -1 * $retention * 24 * 60 * 60)), Query::equal('resourceInternalId', [$resource->getSequence()]), - Query::equal('resourceType', [$resourceType]), + Query::equal('resourceType', [$resource->getCollection()]), Query::orderDesc('$createdAt'), ]; From c88a77a31ca2affc9937f8fb969d404824f4ba7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 16 Dec 2025 12:53:06 +0100 Subject: [PATCH 6/6] AI suggestion fixes --- app/config/collections/projects.php | 4 ++-- src/Appwrite/Platform/Workers/Deletes.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 5e1a9e81ea..de3ed4c7f2 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -573,8 +573,8 @@ return [ 'format' => '', 'size' => 0, 'signed' => true, - 'required' => true, - 'default' => null, + 'required' => false, + 'default' => 0, 'array' => false, 'filters' => [], ], diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index c3dff514e0..fcfaa4cc9c 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -314,7 +314,7 @@ class Deletes extends Action private function deleteOldDeployments(DeleteEvent $queueForDeletes, Document $project, callable $getProjectDB): void { - /* @var $dbForProject Database */ + /** @var Database $dbForProject */ $dbForProject = $getProjectDB($project); $removalCallback = function (Document $resource) use ($dbForProject, $queueForDeletes, $project) {