From c70915306d6cbf6aaca7c9b1e20a7f3a0920b448 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 5 Aug 2025 17:04:31 +1200 Subject: [PATCH 001/333] WIP export --- app/controllers/api/migrations.php | 74 +++++++++++++++++++- src/Appwrite/Platform/Workers/Migrations.php | 7 ++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 85751811ba..51f305d16e 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -306,7 +306,7 @@ App::post('/v1/migrations/nhost') ->dynamic($migration, Response::MODEL_MIGRATION); }); -App::post('/v1/migrations/csv') +App::post('/v1/migrations/csv/imports') ->groups(['api', 'migrations']) ->desc('Import documents from a CSV') ->label('scope', 'migrations.write') @@ -315,8 +315,8 @@ App::post('/v1/migrations/csv') ->label('sdk', new Method( namespace: 'migrations', group: null, - name: 'createCsvMigration', - description: '/docs/references/migrations/migration-csv.md', + name: 'createCsvImportMigration', + description: '/docs/references/migrations/migration-csv-import.md', auth: [AuthType::ADMIN], responses: [ new SDKResponse( @@ -431,6 +431,74 @@ App::post('/v1/migrations/csv') ->dynamic($migration, Response::MODEL_MIGRATION); }); +App::post('/v1/migrations/csv/exports') + ->groups(['api', 'migrations']) + ->desc('Export documents to CSV') + ->label('scope', 'migrations.write') + ->label('event', 'migrations.[migrationId].create') + ->label('audits.event', 'migration.create') + ->label('sdk', new Method( + namespace: 'migrations', + group: null, + name: 'createCsvExportMigration', + description: '/docs/references/migrations/migration-csv-export.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_ACCEPTED, + model: Response::MODEL_MIGRATION, + ) + ] + )) + ->param('bucketId', '', new UID(), 'Storage bucket unique ID where the exported CSV will be stored.') + ->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.') + ->inject('response') + ->inject('dbForProject') + ->inject('project') + ->inject('queueForEvents') + ->inject('queueForMigrations') + ->action(function (string $bucketId, string $resourceId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Migration $queueForMigrations) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + + if ($bucket->isEmpty() || (!$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + } + + $migrationId = ID::unique(); + $resources = Transfer::extractServices([Transfer::GROUP_DATABASES]); + + $migration = $dbForProject->createDocument('migrations', new Document([ + '$id' => $migrationId, + 'status' => 'pending', + 'stage' => 'init', + 'source' => Appwrite::getName(), + 'destination' => CSV::getName(), + 'resources' => $resources, + 'resourceId' => $resourceId, + 'resourceType' => Resource::TYPE_DATABASE, + 'statusCounters' => '{}', + 'resourceData' => '{}', + 'errors' => [], + 'options' => [ + 'bucketId' => $bucketId, + ], + ])); + + $queueForEvents->setParam('migrationId', $migration->getId()); + + $queueForMigrations + ->setMigration($migration) + ->setProject($project) + ->trigger(); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($migration, Response::MODEL_MIGRATION); + }); + App::get('/v1/migrations') ->groups(['api', 'migrations']) ->desc('List migrations') diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 807cf5ec9d..f878853ed6 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -172,6 +172,13 @@ class Migrations extends Action $this->dbForProject, Config::getParam('collections', [])['databases']['collections'], ), + DestinationCSV::getName() => new DestinationCSV( + $this->project, + $this->dbForProject, + $migration->getAttribute('resourceId'), + $migration->getAttribute('options', []), + $this->deviceForImports + ), default => throw new \Exception('Invalid destination type'), }; } From bf1af094c139f7de04c26d6ea4ecf3f400e63b41 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 6 Aug 2025 00:40:39 +1200 Subject: [PATCH 002/333] Add specific column selection --- app/controllers/api/migrations.php | 17 ++++++++++------- app/init/resources.php | 2 +- app/worker.php | 2 +- composer.lock | 12 ++++++------ src/Appwrite/Platform/Workers/Migrations.php | 18 ++++++++---------- 5 files changed, 26 insertions(+), 25 deletions(-) diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 51f305d16e..f6dc56a5eb 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -307,6 +307,7 @@ App::post('/v1/migrations/nhost') }); App::post('/v1/migrations/csv/imports') + ->alias('/v1/migrations/csv') ->groups(['api', 'migrations']) ->desc('Import documents from a CSV') ->label('scope', 'migrations.write') @@ -332,10 +333,10 @@ App::post('/v1/migrations/csv/imports') ->inject('dbForProject') ->inject('project') ->inject('deviceForFiles') - ->inject('deviceForImports') + ->inject('deviceForMigrations') ->inject('queueForEvents') ->inject('queueForMigrations') - ->action(function (string $bucketId, string $fileId, string $resourceId, Response $response, Database $dbForProject, Document $project, Device $deviceForFiles, Device $deviceForImports, Event $queueForEvents, Migration $queueForMigrations) { + ->action(function (string $bucketId, string $fileId, string $resourceId, Response $response, Database $dbForProject, Document $project, Device $deviceForFiles, Device $deviceForMigrations, Event $queueForEvents, Migration $queueForMigrations) { $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -361,7 +362,7 @@ App::post('/v1/migrations/csv/imports') $hasCompression = $compression !== Compression::NONE; $migrationId = ID::unique(); - $newPath = $deviceForImports->getPath($migrationId . '_' . $fileId . '.csv'); + $newPath = $deviceForMigrations->getPath($migrationId . '_' . $fileId . '.csv'); if ($hasEncryption || $hasCompression) { $source = $deviceForFiles->read($path); @@ -391,14 +392,14 @@ App::post('/v1/migrations/csv/imports') } // manual write after decryption and/or decompression - if (! $deviceForImports->write($newPath, $source, 'text/csv')) { + if (! $deviceForMigrations->write($newPath, $source, 'text/csv')) { throw new \Exception("Unable to copy file"); } - } elseif (! $deviceForFiles->transfer($path, $newPath, $deviceForImports)) { + } elseif (! $deviceForFiles->transfer($path, $newPath, $deviceForMigrations)) { throw new \Exception("Unable to copy file"); } - $fileSize = $deviceForImports->getFileSize($newPath); + $fileSize = $deviceForMigrations->getFileSize($newPath); $resources = Transfer::extractServices([Transfer::GROUP_DATABASES]); $migration = $dbForProject->createDocument('migrations', new Document([ @@ -452,12 +453,13 @@ App::post('/v1/migrations/csv/exports') )) ->param('bucketId', '', new UID(), 'Storage bucket unique ID where the exported CSV will be stored.') ->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.') + ->param('columns', [], new ArrayList(new Text(255)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.') ->inject('response') ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') ->inject('queueForMigrations') - ->action(function (string $bucketId, string $resourceId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Migration $queueForMigrations) { + ->action(function (string $bucketId, string $resourceId, array $columns, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Migration $queueForMigrations) { $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -484,6 +486,7 @@ App::post('/v1/migrations/csv/exports') 'errors' => [], 'options' => [ 'bucketId' => $bucketId, + 'columns' => $columns, ], ])); diff --git a/app/init/resources.php b/app/init/resources.php index 162eab1973..8b7ec941f8 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -514,7 +514,7 @@ App::setResource('deviceForFiles', function ($project, Telemetry $telemetry) { App::setResource('deviceForSites', function ($project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); }, ['project', 'telemetry']); -App::setResource('deviceForImports', function ($project, Telemetry $telemetry) { +App::setResource('deviceForMigrations', function ($project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); }, ['project', 'telemetry']); App::setResource('deviceForFunctions', function ($project, Telemetry $telemetry) { diff --git a/app/worker.php b/app/worker.php index 90f3368fe7..9429cb853f 100644 --- a/app/worker.php +++ b/app/worker.php @@ -341,7 +341,7 @@ Server::setResource('deviceForSites', function (Document $project, Telemetry $te return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); }, ['project', 'telemetry']); -Server::setResource('deviceForImports', function (Document $project, Telemetry $telemetry) { +Server::setResource('deviceForMigrations', function (Document $project, Telemetry $telemetry) { return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); }, ['project', 'telemetry']); diff --git a/composer.lock b/composer.lock index e3b3d2208f..2cd331ff6d 100644 --- a/composer.lock +++ b/composer.lock @@ -3542,16 +3542,16 @@ }, { "name": "utopia-php/database", - "version": "0.71.11", + "version": "0.71.12", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "644ed827aace63cbdf8c6c64a3998c11b43e3383" + "reference": "72c2a9c185f0f606e4792913a071f744cca21d42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/644ed827aace63cbdf8c6c64a3998c11b43e3383", - "reference": "644ed827aace63cbdf8c6c64a3998c11b43e3383", + "url": "https://api.github.com/repos/utopia-php/database/zipball/72c2a9c185f0f606e4792913a071f744cca21d42", + "reference": "72c2a9c185f0f606e4792913a071f744cca21d42", "shasum": "" }, "require": { @@ -3592,9 +3592,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.71.11" + "source": "https://github.com/utopia-php/database/tree/0.71.12" }, - "time": "2025-08-05T08:35:29+00:00" + "time": "2025-08-05T09:38:25+00:00" }, { "name": "utopia-php/detector", diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index f878853ed6..4e20c966b1 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -4,6 +4,7 @@ namespace Appwrite\Platform\Workers; use Ahc\Jwt\JWT; use Appwrite\Event\Realtime; +use Appwrite\Migration\CSV as DestinationCSV; use Exception; use Utopia\CLI\Console; use Utopia\Config\Config; @@ -34,7 +35,7 @@ class Migrations extends Action protected Database $dbForPlatform; - protected Device $deviceForImports; + protected Device $deviceForMigrations; protected Document $project; @@ -68,17 +69,17 @@ class Migrations extends Action ->inject('dbForPlatform') ->inject('logError') ->inject('queueForRealtime') - ->inject('deviceForImports') + ->inject('deviceForMigrations') ->callback($this->action(...)); } /** * @throws Exception */ - public function action(Message $message, Document $project, Database $dbForProject, Database $dbForPlatform, callable $logError, Realtime $queueForRealtime, Device $deviceForImports): void + public function action(Message $message, Document $project, Database $dbForProject, Database $dbForPlatform, callable $logError, Realtime $queueForRealtime, Device $deviceForMigrations): void { $payload = $message->getPayload() ?? []; - $this->deviceForImports = $deviceForImports; + $this->deviceForMigrations = $deviceForMigrations; if (empty($payload)) { throw new Exception('Missing payload'); @@ -146,7 +147,7 @@ class Migrations extends Action CSV::getName() => new CSV( $resourceId, $migrationOptions['path'], - $this->deviceForImports, + $this->deviceForMigrations, $this->dbForProject ), default => throw new \Exception('Invalid source type'), @@ -173,11 +174,9 @@ class Migrations extends Action Config::getParam('collections', [])['databases']['collections'], ), DestinationCSV::getName() => new DestinationCSV( - $this->project, - $this->dbForProject, + $this->deviceForMigrations, $migration->getAttribute('resourceId'), - $migration->getAttribute('options', []), - $this->deviceForImports + $migration->getAttribute('options', [])['columns'] ?? [], ), default => throw new \Exception('Invalid destination type'), }; @@ -211,7 +210,6 @@ class Migrations extends Action // set the errors back without trace $clonedMigrationDocument->setAttribute('errors', $errorMessages); - /** Trigger Realtime Events */ $queueForRealtime ->setProject($project) From 892cfe01b2ebb34e144caccb95f729ee4d607377 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 7 Aug 2025 00:47:11 +1200 Subject: [PATCH 003/333] Optional columns --- app/controllers/api/migrations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index f6dc56a5eb..36a920a56e 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -453,7 +453,7 @@ App::post('/v1/migrations/csv/exports') )) ->param('bucketId', '', new UID(), 'Storage bucket unique ID where the exported CSV will be stored.') ->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.') - ->param('columns', [], new ArrayList(new Text(255)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.') + ->param('columns', [], new ArrayList(new Text(255)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true) ->inject('response') ->inject('dbForProject') ->inject('project') From 57a46b98ecd92e630b70d81986da92db917f97eb Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 22 Aug 2025 18:01:38 +1200 Subject: [PATCH 004/333] Use utopia adapter --- composer.json | 2 +- composer.lock | 27 +++++++++++++------- src/Appwrite/Platform/Workers/Migrations.php | 2 +- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 0c662c775f..fcc0acb213 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,7 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.18.*", - "utopia-php/migration": "1.*", + "utopia-php/migration": "dev-feat-csv-export as 1.0.0", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.8.*", diff --git a/composer.lock b/composer.lock index b7e9a76088..0dffee1bde 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0da713ee5642eba1d30bc51c1a04a723", + "content-hash": "954529a36566209d6687df9f41a0f2e6", "packages": [ { "name": "adhocore/jwt", @@ -4109,16 +4109,16 @@ }, { "name": "utopia-php/migration", - "version": "1.0.0", + "version": "dev-feat-csv-export", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "0e4499d9dd2c90c2be188cc5fb7a32d9a892b569" + "reference": "8435f1db0db4854ca27cb4c9cf275b905fcb3b41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/0e4499d9dd2c90c2be188cc5fb7a32d9a892b569", - "reference": "0e4499d9dd2c90c2be188cc5fb7a32d9a892b569", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/8435f1db0db4854ca27cb4c9cf275b905fcb3b41", + "reference": "8435f1db0db4854ca27cb4c9cf275b905fcb3b41", "shasum": "" }, "require": { @@ -4159,9 +4159,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.0.0" + "source": "https://github.com/utopia-php/migration/tree/feat-csv-export" }, - "time": "2025-08-13T09:15:53+00:00" + "time": "2025-08-21T12:56:18+00:00" }, { "name": "utopia-php/orchestration", @@ -8425,9 +8425,18 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/migration", + "version": "dev-feat-csv-export", + "alias": "1.0.0", + "alias_normalized": "1.0.0.0" + } + ], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": { + "utopia-php/migration": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 23b8cf7ba3..33af785ab5 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -4,7 +4,7 @@ namespace Appwrite\Platform\Workers; use Ahc\Jwt\JWT; use Appwrite\Event\Realtime; -use Appwrite\Migration\CSV as DestinationCSV; +use Utopia\Migration\Destinations\CSV as DestinationCSV; use Exception; use Utopia\CLI\Console; use Utopia\Config\Config; From 1722e9e416ad998fed889c098526a01f76493848 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 22 Aug 2025 18:03:10 +1200 Subject: [PATCH 005/333] Fix DB read source --- src/Appwrite/Platform/Workers/Migrations.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 33af785ab5..ce9b7e2881 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -18,6 +18,7 @@ use Utopia\Migration\Destination; use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite; use Utopia\Migration\Exception as MigrationException; use Utopia\Migration\Source; +use Utopia\Migration\Sources\Appwrite; use Utopia\Migration\Sources\Appwrite as SourceAppwrite; use Utopia\Migration\Sources\CSV; use Utopia\Migration\Sources\Firebase; @@ -113,9 +114,17 @@ class Migrations extends Action protected function processSource(Document $migration): Source { $source = $migration->getAttribute('source'); + $destination = $migration->getAttribute('destination'); $resourceId = $migration->getAttribute('resourceId'); $credentials = $migration->getAttribute('credentials'); $migrationOptions = $migration->getAttribute('options'); + $dataSource = Appwrite::SOURCE_API; + $database = null; + + if ($source === Appwrite::getName() && $destination === DestinationCSV::getName()) { + $dataSource = Appwrite::SOURCE_DATABASE; + $database = $this->dbForProject; + } $migrationSource = match ($source) { Firebase::getName() => new Firebase( @@ -143,6 +152,8 @@ class Migrations extends Action $credentials['projectId'], $credentials['endpoint'] === 'http://localhost/v1' ? 'http://appwrite/v1' : $credentials['endpoint'], $credentials['apiKey'], + $dataSource, + $database ), CSV::getName() => new CSV( $resourceId, @@ -251,7 +262,9 @@ class Migrations extends Action 'functions.write', 'databases.read', 'collections.read', + 'collections.write', 'tables.read', + 'tables.write', 'documents.read', 'documents.write', 'rows.read', From f68d49c4d63fce8a663b5f0c7d6b3852dae3feef Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 29 Aug 2025 18:19:29 +0530 Subject: [PATCH 006/333] updated the migration error size attribute --- app/config/collections/projects.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 7fc82b7441..4fd44c7725 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -2345,7 +2345,7 @@ return [ '$id' => ID::custom('errors'), 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 65535, + 'size' => 131070, 'signed' => true, 'required' => true, 'default' => null, From ac604b11e3b241e504d14f14b2e09750e855c705 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 1 Sep 2025 13:13:52 +0530 Subject: [PATCH 007/333] added migration script --- src/Appwrite/Migration/Version/V23.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Appwrite/Migration/Version/V23.php b/src/Appwrite/Migration/Version/V23.php index d5caf2ab3c..c26c45d732 100644 --- a/src/Appwrite/Migration/Version/V23.php +++ b/src/Appwrite/Migration/Version/V23.php @@ -6,6 +6,7 @@ use Appwrite\Migration\Migration; use Exception; use Throwable; use Utopia\CLI\Console; +use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; @@ -29,6 +30,9 @@ class V23 extends Migration Console::info('Migrating databases'); $this->migrateDatabases(); + + Console::info('Migrating migration collection'); + $this->updateMigrateErrorSize(); } /** @@ -49,4 +53,26 @@ class V23 extends Migration $this->dbForProject->updateDocuments('databases', new Document(['type' => 'legacy'])); } + /** + * Update migration collection error attribute + * + * @return void + * @throws Exception|Throwable + */ + + private function updateMigrateErrorSize(): void + { + if ($this->project->getId() === 'console') { + return; + } + + $collection = Config::getParam('collections', [])['projects'] ?? []; + $migrationAttributes = $collection['migrations']['attributes']; + $attributeKey = \array_search('errors', \array_column($migrationAttributes, '$id')); + $migrationAttributes[$attributeKey]['size'] = 131070; + $migration = $this->dbForProject->getCollection('migrations'); + $migration->setAttribute('attributes', $migrationAttributes); + $this->dbForProject->updateDocument($migration->getCollection(), $migration->getId(), $migration); + $this->dbForProject->purgeCachedCollection('migrations'); + } } From 6e16340f16c7388e3e2fdfe1c73659052d392c11 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Sat, 13 Sep 2025 17:56:19 +0530 Subject: [PATCH 008/333] feat: add branch deployments to appwrite --- .../specs/open-api3-latest-console.json | 151 ++++++++++-- app/config/specs/open-api3-latest-server.json | 126 ++++++++-- app/config/specs/swagger2-latest-console.json | 159 ++++++++++-- app/config/specs/swagger2-latest-server.json | 134 ++++++++-- .../Modules/Functions/Workers/Builds.php | 12 +- .../Sites/Http/Deployments/Direct/Create.php | 231 ++++++++++++++++++ .../Platform/Modules/Sites/Services/Http.php | 2 + 7 files changed, 731 insertions(+), 84 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 02d97fffc7..0de652e3fb 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -4888,7 +4888,7 @@ "x-appwrite": { "method": "getResource", "group": null, - "weight": 496, + "weight": 497, "cookies": false, "type": "", "demo": "console\/get-resource.md", @@ -28209,7 +28209,7 @@ "x-appwrite": { "method": "listRules", "group": null, - "weight": 502, + "weight": 503, "cookies": false, "type": "", "demo": "proxy\/list-rules.md", @@ -28283,7 +28283,7 @@ "x-appwrite": { "method": "createAPIRule", "group": null, - "weight": 497, + "weight": 498, "cookies": false, "type": "", "demo": "proxy\/create-api-rule.md", @@ -28350,7 +28350,7 @@ "x-appwrite": { "method": "createFunctionRule", "group": null, - "weight": 499, + "weight": 500, "cookies": false, "type": "", "demo": "proxy\/create-function-rule.md", @@ -28428,7 +28428,7 @@ "x-appwrite": { "method": "createRedirectRule", "group": null, - "weight": 500, + "weight": 501, "cookies": false, "type": "", "demo": "proxy\/create-redirect-rule.md", @@ -28541,7 +28541,7 @@ "x-appwrite": { "method": "createSiteRule", "group": null, - "weight": 498, + "weight": 499, "cookies": false, "type": "", "demo": "proxy\/create-site-rule.md", @@ -28619,7 +28619,7 @@ "x-appwrite": { "method": "getRule", "group": null, - "weight": 501, + "weight": 502, "cookies": false, "type": "", "demo": "proxy\/get-rule.md", @@ -28670,7 +28670,7 @@ "x-appwrite": { "method": "deleteRule", "group": null, - "weight": 503, + "weight": 504, "cookies": false, "type": "", "demo": "proxy\/delete-rule.md", @@ -28730,7 +28730,7 @@ "x-appwrite": { "method": "updateRuleVerification", "group": null, - "weight": 504, + "weight": 505, "cookies": false, "type": "", "demo": "proxy\/update-rule-verification.md", @@ -29085,6 +29085,103 @@ } } }, + "\/sites\/direct": { + "post": { + "summary": "Create direct deployment", + "operationId": "sitesCreateDirectDeployment", + "tags": [ + "sites" + ], + "description": "Create a deployment directly from a repository branch.", + "responses": { + "202": { + "description": "Deployment", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/deployment" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createDirectDeployment", + "group": "deployments", + "weight": 483, + "cookies": false, + "type": "", + "demo": "sites\/create-direct-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "siteId": { + "type": "string", + "description": "Site ID.", + "x-example": "" + }, + "repository": { + "type": "string", + "description": "Repository name of the template.", + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to site code in the template repo.", + "x-example": "" + }, + "branch": { + "type": "string", + "description": "Branch to create deployment from.", + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "x-example": false + } + }, + "required": [ + "siteId", + "repository", + "owner", + "rootDirectory", + "branch" + ] + } + } + } + } + } + }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -29158,7 +29255,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -29208,7 +29305,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 491, + "weight": 492, "cookies": false, "type": "", "demo": "sites\/list-templates.md", @@ -29308,7 +29405,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 492, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/get-template.md", @@ -29368,7 +29465,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 493, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/list-usage.md", @@ -30637,7 +30734,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -30708,7 +30805,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -30770,7 +30867,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -30841,7 +30938,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 494, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/get-usage.md", @@ -30923,7 +31020,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -30982,7 +31079,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -31073,7 +31170,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -31142,7 +31239,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -31233,7 +31330,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -39984,7 +40081,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -40064,7 +40161,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -40153,7 +40250,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -40213,7 +40310,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -40283,7 +40380,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 510, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 09d53dbdf0..d8d7178eb2 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -20014,6 +20014,104 @@ } } }, + "\/sites\/direct": { + "post": { + "summary": "Create direct deployment", + "operationId": "sitesCreateDirectDeployment", + "tags": [ + "sites" + ], + "description": "Create a deployment directly from a repository branch.", + "responses": { + "202": { + "description": "Deployment", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/deployment" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createDirectDeployment", + "group": "deployments", + "weight": 483, + "cookies": false, + "type": "", + "demo": "sites\/create-direct-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "siteId": { + "type": "string", + "description": "Site ID.", + "x-example": "" + }, + "repository": { + "type": "string", + "description": "Repository name of the template.", + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to site code in the template repo.", + "x-example": "" + }, + "branch": { + "type": "string", + "description": "Branch to create deployment from.", + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "x-example": false + } + }, + "required": [ + "siteId", + "repository", + "owner", + "rootDirectory", + "branch" + ] + } + } + } + } + } + }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -20088,7 +20186,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -21349,7 +21447,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -21421,7 +21519,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -21484,7 +21582,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -21556,7 +21654,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -21616,7 +21714,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -21708,7 +21806,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -21778,7 +21876,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -21870,7 +21968,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -30024,7 +30122,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -30105,7 +30203,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -30195,7 +30293,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -30256,7 +30354,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -30327,7 +30425,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 510, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 6d5721c73b..22823cd40e 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -5052,7 +5052,7 @@ "x-appwrite": { "method": "getResource", "group": null, - "weight": 496, + "weight": 497, "cookies": false, "type": "", "demo": "console\/get-resource.md", @@ -28351,7 +28351,7 @@ "x-appwrite": { "method": "listRules", "group": null, - "weight": 502, + "weight": 503, "cookies": false, "type": "", "demo": "proxy\/list-rules.md", @@ -28424,7 +28424,7 @@ "x-appwrite": { "method": "createAPIRule", "group": null, - "weight": 497, + "weight": 498, "cookies": false, "type": "", "demo": "proxy\/create-api-rule.md", @@ -28494,7 +28494,7 @@ "x-appwrite": { "method": "createFunctionRule", "group": null, - "weight": 499, + "weight": 500, "cookies": false, "type": "", "demo": "proxy\/create-function-rule.md", @@ -28577,7 +28577,7 @@ "x-appwrite": { "method": "createRedirectRule", "group": null, - "weight": 500, + "weight": 501, "cookies": false, "type": "", "demo": "proxy\/create-redirect-rule.md", @@ -28697,7 +28697,7 @@ "x-appwrite": { "method": "createSiteRule", "group": null, - "weight": 498, + "weight": 499, "cookies": false, "type": "", "demo": "proxy\/create-site-rule.md", @@ -28778,7 +28778,7 @@ "x-appwrite": { "method": "getRule", "group": null, - "weight": 501, + "weight": 502, "cookies": false, "type": "", "demo": "proxy\/get-rule.md", @@ -28831,7 +28831,7 @@ "x-appwrite": { "method": "deleteRule", "group": null, - "weight": 503, + "weight": 504, "cookies": false, "type": "", "demo": "proxy\/delete-rule.md", @@ -28891,7 +28891,7 @@ "x-appwrite": { "method": "updateRuleVerification", "group": null, - "weight": 504, + "weight": 505, "cookies": false, "type": "", "demo": "proxy\/update-rule-verification.md", @@ -29264,6 +29264,111 @@ ] } }, + "\/sites\/direct": { + "post": { + "summary": "Create direct deployment", + "operationId": "sitesCreateDirectDeployment", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "sites" + ], + "description": "Create a deployment directly from a repository branch.", + "responses": { + "202": { + "description": "Deployment", + "schema": { + "$ref": "#\/definitions\/deployment" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createDirectDeployment", + "group": "deployments", + "weight": 483, + "cookies": false, + "type": "", + "demo": "sites\/create-direct-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "siteId": { + "type": "string", + "description": "Site ID.", + "default": null, + "x-example": "" + }, + "repository": { + "type": "string", + "description": "Repository name of the template.", + "default": null, + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "default": null, + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to site code in the template repo.", + "default": null, + "x-example": "" + }, + "branch": { + "type": "string", + "description": "Branch to create deployment from.", + "default": null, + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "default": true, + "x-example": false + } + }, + "required": [ + "siteId", + "repository", + "owner", + "rootDirectory", + "branch" + ] + } + } + ] + } + }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -29337,7 +29442,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -29387,7 +29492,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 491, + "weight": 492, "cookies": false, "type": "", "demo": "sites\/list-templates.md", @@ -29481,7 +29586,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 492, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/get-template.md", @@ -29539,7 +29644,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 493, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/list-usage.md", @@ -30811,7 +30916,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -30882,7 +30987,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -30946,7 +31051,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -31013,7 +31118,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 494, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/get-usage.md", @@ -31091,7 +31196,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -31150,7 +31255,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -31240,7 +31345,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -31307,7 +31412,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -31399,7 +31504,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -39916,7 +40021,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -39996,7 +40101,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -40080,7 +40185,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -40140,7 +40245,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -40211,7 +40316,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 510, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index 98077f1050..c7a5a20c24 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -20226,6 +20226,112 @@ ] } }, + "\/sites\/direct": { + "post": { + "summary": "Create direct deployment", + "operationId": "sitesCreateDirectDeployment", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "sites" + ], + "description": "Create a deployment directly from a repository branch.", + "responses": { + "202": { + "description": "Deployment", + "schema": { + "$ref": "#\/definitions\/deployment" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createDirectDeployment", + "group": "deployments", + "weight": 483, + "cookies": false, + "type": "", + "demo": "sites\/create-direct-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "siteId": { + "type": "string", + "description": "Site ID.", + "default": null, + "x-example": "" + }, + "repository": { + "type": "string", + "description": "Repository name of the template.", + "default": null, + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "default": null, + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to site code in the template repo.", + "default": null, + "x-example": "" + }, + "branch": { + "type": "string", + "description": "Branch to create deployment from.", + "default": null, + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "default": true, + "x-example": false + } + }, + "required": [ + "siteId", + "repository", + "owner", + "rootDirectory", + "branch" + ] + } + } + ] + } + }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -20300,7 +20406,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -21566,7 +21672,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -21638,7 +21744,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -21703,7 +21809,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -21771,7 +21877,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -21831,7 +21937,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -21922,7 +22028,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -21990,7 +22096,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -22083,7 +22189,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -30036,7 +30142,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -30117,7 +30223,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -30202,7 +30308,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -30263,7 +30369,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -30335,7 +30441,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 510, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 9547a752ef..2ac05eae77 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -311,19 +311,27 @@ class Builds extends Action $templateRepositoryName = $template->getAttribute('repositoryName', ''); $templateOwnerName = $template->getAttribute('ownerName', ''); $templateVersion = $template->getAttribute('version', ''); + $templateBranch = $template->getAttribute('branch', ''); $templateRootDirectory = $template->getAttribute('rootDirectory', ''); $templateRootDirectory = \rtrim($templateRootDirectory, '/'); $templateRootDirectory = \ltrim($templateRootDirectory, '.'); $templateRootDirectory = \ltrim($templateRootDirectory, '/'); - if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) { + if (!empty($templateRepositoryName) && !empty($templateOwnerName) && (!empty($templateVersion) || !empty($templateBranch))) { $stdout = ''; $stderr = ''; // Clone template repo $tmpTemplateDirectory = '/tmp/builds/' . $deploymentId . '-template'; - $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); + + if(empty($templateVersion)) { + $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateBranch, GitHub::CLONE_TYPE_BRANCH, $tmpTemplateDirectory, $templateRootDirectory); + } else { + // True template + $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); + } + $exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr); if ($exit !== 0) { diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php new file mode 100644 index 0000000000..092a7702df --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php @@ -0,0 +1,231 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/sites/direct') + ->desc('Create direct deployment') + ->groups(['api', 'sites']) + ->label('scope', 'sites.write') + ->label('resourceType', RESOURCE_TYPE_SITES) + ->label('event', 'sites.[siteId].deployments.[deploymentId].create') + ->label('audits.event', 'deployment.create') + ->label('audits.resource', 'site/{request.siteId}') + ->label('sdk', new Method( + namespace: 'sites', + group: 'deployments', + name: 'createDirectDeployment', + description: <<param('siteId', '', new UID(), 'Site ID.') + ->param('repository', '', new Text(128, 0), 'Repository name of the template.') + ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') + ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') + ->param('branch', '', new Text(128, 0), 'Branch to create deployment from.') + ->param('activate', true, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('project') + ->inject('queueForEvents') + ->inject('queueForBuilds') + ->inject('gitHub') + ->callback($this->action(...)); + } + + public function action( + string $siteId, + string $repository, + string $owner, + string $rootDirectory, + string $branch, + bool $activate, + Request $request, + Response $response, + Database $dbForProject, + Database $dbForPlatform, + Document $project, + Event $queueForEvents, + Build $queueForBuilds, + GitHub $github + ) { + $site = $dbForProject->getDocument('sites', $siteId); + + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + $template = new Document([ + 'repositoryName' => $repository, + 'ownerName' => $owner, + 'rootDirectory' => $rootDirectory, + 'branch' => $branch + ]); + + + if (!empty($site->getAttribute('providerRepositoryId'))) { + $installation = $dbForPlatform->getDocument('installations', $site->getAttribute('installationId')); + + $deployment = $this->redeployVcsSite( + request: $request, + site: $site, + project: $project, + installation: $installation, + dbForProject: $dbForProject, + dbForPlatform: $dbForPlatform, + queueForBuilds: $queueForBuilds, + template: $template, + github: $github, + activate: $activate, + ); + + $queueForEvents + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + + return; + } + + $branchUrl = "https://github.com/$owner/$repository/tree/$branch"; + $repositoryUrl = "https://github.com/$owner/$repository"; + + try { + $commitDetails = $github->getLatestCommit($owner, $repository, $branch); + } catch (\Throwable $error) { + // Ignore; deployment can continue + } + + $commands = []; + if (!empty($site->getAttribute('installCommand', ''))) { + $commands[] = $site->getAttribute('installCommand', ''); + } + if (!empty($site->getAttribute('buildCommand', ''))) { + $commands[] = $site->getAttribute('buildCommand', ''); + } + + $deploymentId = ID::unique(); + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceId' => $site->getId(), + 'resourceInternalId' => $site->getSequence(), + 'resourceType' => 'sites', + 'buildCommands' => \implode(' && ', $commands), + 'buildOutput' => $site->getAttribute('outputDirectory', ''), + 'adapter' => $site->getAttribute('adapter', ''), + 'fallbackFile' => $site->getAttribute('fallbackFile', ''), + 'providerRepositoryName' => $repository, + 'providerRepositoryOwner' => $owner, + 'providerRepositoryUrl' => $repositoryUrl, + 'providerBranchUrl' => $branchUrl, + 'providerBranch' => $branch, + 'providerCommitHash' => $commitDetails['commitHash'] ?? '', + 'providerCommitAuthorUrl' => $commitDetails['commitAuthorUrl'] ?? '', + 'providerCommitAuthor' => $commitDetails['commitAuthor'] ?? '', + 'providerCommitMessage' => mb_strimwidth($commitDetails['commitMessage'] ?? '', 0, 255, '...'), + 'providerCommitUrl' => $commitDetails['commitUrl'] ?? '', + 'type' => 'vcs', + 'activate' => $activate, + ])); + + $site = $site + ->setAttribute('latestDeploymentId', $deployment->getId()) + ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) + ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) + ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); + $dbForProject->updateDocument('sites', $site->getId(), $site); + + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $domain = ID::unique() . "." . $sitesDomain; + + // TODO: @christyjacob remove once we migrate the rules in 1.7.x + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique(); + + Authorization::skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getSequence(), + 'domain' => $domain, + 'type' => 'deployment', + 'trigger' => 'deployment', + 'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(), + 'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(), + 'deploymentResourceType' => 'site', + 'deploymentResourceId' => $site->getId(), + 'deploymentResourceInternalId' => $site->getSequence(), + 'status' => 'verified', + 'certificateId' => '', + 'owner' => 'Appwrite', + 'region' => $project->getAttribute('region') + ])) + ); + + $queueForBuilds + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($site) + ->setDeployment($deployment) + ->setTemplate($template); + + $queueForEvents + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php index 6bd151f97e..f19b9b6d71 100644 --- a/src/Appwrite/Platform/Modules/Sites/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -6,6 +6,7 @@ use Appwrite\Platform\Modules\Sites\Http\Deployments\Create as CreateDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Delete as DeleteDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Download\Get as DownloadDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Duplicate\Create as CreateDuplicateDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\Direct\Create as CreateDirectDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Get as GetDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Status\Update as UpdateDeploymentStatus; use Appwrite\Platform\Modules\Sites\Http\Deployments\Template\Create as CreateTemplateDeployment; @@ -60,6 +61,7 @@ class Http extends Service $this->addAction(DownloadDeployment::getName(), new DownloadDeployment()); $this->addAction(CreateDuplicateDeployment::getName(), new CreateDuplicateDeployment()); $this->addAction(UpdateDeploymentStatus::getName(), new UpdateDeploymentStatus()); + $this->addAction(CreateDirectDeployment::getName(), new CreateDirectDeployment()); // Logs $this->addAction(GetLog::getName(), new GetLog()); From 4858317891faa2a5a0bdcaf4ea3f976d223ae125 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Sat, 13 Sep 2025 18:02:13 +0530 Subject: [PATCH 009/333] fix: format, lints --- .../Platform/Modules/Functions/Workers/Builds.php | 4 ++-- .../Modules/Sites/Http/Deployments/Direct/Create.php | 8 ++++---- src/Appwrite/Platform/Modules/Sites/Services/Http.php | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 2ac05eae77..62ee96da47 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -324,8 +324,8 @@ class Builds extends Action // Clone template repo $tmpTemplateDirectory = '/tmp/builds/' . $deploymentId . '-template'; - - if(empty($templateVersion)) { + + if (empty($templateVersion)) { $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateBranch, GitHub::CLONE_TYPE_BRANCH, $tmpTemplateDirectory, $templateRootDirectory); } else { // True template diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php index 092a7702df..d1df994149 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php @@ -106,7 +106,7 @@ class Create extends Base 'rootDirectory' => $rootDirectory, 'branch' => $branch ]); - + if (!empty($site->getAttribute('providerRepositoryId'))) { $installation = $dbForPlatform->getDocument('installations', $site->getAttribute('installationId')); @@ -137,11 +137,11 @@ class Create extends Base $branchUrl = "https://github.com/$owner/$repository/tree/$branch"; $repositoryUrl = "https://github.com/$owner/$repository"; - + try { - $commitDetails = $github->getLatestCommit($owner, $repository, $branch); + $commitDetails = $github->getLatestCommit($owner, $repository, $branch); } catch (\Throwable $error) { - // Ignore; deployment can continue + // Ignore; deployment can continue } $commands = []; diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php index f19b9b6d71..437356d4e2 100644 --- a/src/Appwrite/Platform/Modules/Sites/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -4,9 +4,9 @@ namespace Appwrite\Platform\Modules\Sites\Services; use Appwrite\Platform\Modules\Sites\Http\Deployments\Create as CreateDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Delete as DeleteDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\Direct\Create as CreateDirectDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Download\Get as DownloadDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Duplicate\Create as CreateDuplicateDeployment; -use Appwrite\Platform\Modules\Sites\Http\Deployments\Direct\Create as CreateDirectDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Get as GetDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Status\Update as UpdateDeploymentStatus; use Appwrite\Platform\Modules\Sites\Http\Deployments\Template\Create as CreateTemplateDeployment; From 2b1e3bd9477941431f3f5055ea118b1fe62e9067 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Sun, 14 Sep 2025 15:15:44 +0530 Subject: [PATCH 010/333] delete the new route and just do it in template route --- .../Http/Deployments/Template/Create.php | 26 ++++++++++++++++--- .../Modules/Functions/Workers/Builds.php | 13 +++------- .../Http/Deployments/Template/Create.php | 26 ++++++++++++++++--- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php index 4d93c8e8cd..885e23bfa1 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -65,7 +65,9 @@ class Create extends Base ->param('repository', '', new Text(128, 0), 'Repository name of the template.') ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.') - ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.') + ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) + ->param('type', '', new Text(128, 0), 'Type for the reference provided. Can be commit, branch, or version', true) + ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag', true) ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') ->inject('response') @@ -84,6 +86,8 @@ class Create extends Base string $owner, string $rootDirectory, string $version, + string $type, + string $reference, bool $activate, Request $request, Response $response, @@ -100,11 +104,22 @@ class Create extends Base throw new Exception(Exception::FUNCTION_NOT_FOUND); } + if (empty($version) && empty($type) && empty($reference)) { + throw new Exception("Either version or type & reference must be provided"); + } + + $referenceType = !empty($version) ? GitHub::CLONE_TYPE_TAG : $type; + $referenceValue = !empty($version) ? $version : $reference; + + $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$referenceValue" : ""; + $repositoryUrl = "https://github.com/$owner/$repository"; + $template = new Document([ 'repositoryName' => $repository, 'ownerName' => $owner, 'rootDirectory' => $rootDirectory, - 'version' => $version + 'referenceType' => $referenceType, + 'referenceValue' => $referenceValue, ]); if (!empty($function->getAttribute('providerRepositoryId'))) { @@ -146,7 +161,12 @@ class Create extends Base 'resourceType' => 'functions', 'entrypoint' => $function->getAttribute('entrypoint', ''), 'buildCommands' => $function->getAttribute('commands', ''), - 'type' => 'manual', + 'providerRepositoryName' => $repository, + 'providerRepositoryOwner' => $owner, + 'providerRepositoryUrl' => $repositoryUrl, + 'providerBranchUrl' => $branchUrl, + 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $referenceValue : '', + 'type' => 'vcs', 'activate' => $activate, ])); diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 62ee96da47..4bb49a42c8 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -310,27 +310,22 @@ class Builds extends Action // Non-VCS + Template $templateRepositoryName = $template->getAttribute('repositoryName', ''); $templateOwnerName = $template->getAttribute('ownerName', ''); - $templateVersion = $template->getAttribute('version', ''); - $templateBranch = $template->getAttribute('branch', ''); + $templateReferenceType = $template->getAttribute('referenceType', ''); + $templateReferenceValue = $template->getAttribute('referenceValue', ''); $templateRootDirectory = $template->getAttribute('rootDirectory', ''); $templateRootDirectory = \rtrim($templateRootDirectory, '/'); $templateRootDirectory = \ltrim($templateRootDirectory, '.'); $templateRootDirectory = \ltrim($templateRootDirectory, '/'); - if (!empty($templateRepositoryName) && !empty($templateOwnerName) && (!empty($templateVersion) || !empty($templateBranch))) { + if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateReferenceType) && !empty($templateReferenceValue)) { $stdout = ''; $stderr = ''; // Clone template repo $tmpTemplateDirectory = '/tmp/builds/' . $deploymentId . '-template'; - if (empty($templateVersion)) { - $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateBranch, GitHub::CLONE_TYPE_BRANCH, $tmpTemplateDirectory, $templateRootDirectory); - } else { - // True template - $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); - } + $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateReferenceValue, $templateReferenceType, $tmpTemplateDirectory, $templateRootDirectory); $exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index a2040d830b..0799e0b51f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -67,7 +67,9 @@ class Create extends Base ->param('repository', '', new Text(128, 0), 'Repository name of the template.') ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') - ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the site template.') + ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) + ->param('type', '', new Text(128, 0), 'Type for the reference provided. Can be commit, branch, or version', true) + ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag', true) ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') ->inject('response') @@ -86,6 +88,8 @@ class Create extends Base string $owner, string $rootDirectory, string $version, + string $type, + string $reference, bool $activate, Request $request, Response $response, @@ -102,11 +106,22 @@ class Create extends Base throw new Exception(Exception::SITE_NOT_FOUND); } + if (empty($version) && empty($type) && empty($reference)) { + throw new Exception("Either version or type & reference must be provided"); + } + + $referenceType = !empty($version) ? GitHub::CLONE_TYPE_TAG : $type; + $referenceValue = !empty($version) ? $version : $reference; + + $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$referenceValue" : ""; + $repositoryUrl = "https://github.com/$owner/$repository"; + $template = new Document([ 'repositoryName' => $repository, 'ownerName' => $owner, 'rootDirectory' => $rootDirectory, - 'version' => $version + 'referenceType' => $referenceType, + 'referenceValue' => $referenceValue ]); if (!empty($site->getAttribute('providerRepositoryId'))) { @@ -157,9 +172,14 @@ class Create extends Base 'resourceType' => 'sites', 'buildCommands' => \implode(' && ', $commands), 'buildOutput' => $site->getAttribute('outputDirectory', ''), + 'providerRepositoryName' => $repository, + 'providerRepositoryOwner' => $owner, + 'providerRepositoryUrl' => $repositoryUrl, + 'providerBranchUrl' => $branchUrl, + 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $referenceValue : '', 'adapter' => $site->getAttribute('adapter', ''), 'fallbackFile' => $site->getAttribute('fallbackFile', ''), - 'type' => 'manual', + 'type' => 'vcs', 'activate' => $activate, ])); From cb03bfe74ccd1ac43737053113b27a82e8bd3cbd Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Sun, 14 Sep 2025 15:22:51 +0530 Subject: [PATCH 011/333] specs and sdk --- .../specs/open-api3-latest-console.json | 179 ++++---------- app/config/specs/open-api3-latest-server.json | 154 +++--------- app/config/specs/swagger2-latest-console.json | 195 +++++---------- app/config/specs/swagger2-latest-server.json | 170 ++++--------- .../Sites/Http/Deployments/Direct/Create.php | 231 ------------------ .../Platform/Modules/Sites/Services/Http.php | 2 - 6 files changed, 186 insertions(+), 745 deletions(-) delete mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 0de652e3fb..96cec5c5a6 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -4888,7 +4888,7 @@ "x-appwrite": { "method": "getResource", "group": null, - "weight": 497, + "weight": 496, "cookies": false, "type": "", "demo": "console\/get-resource.md", @@ -12932,6 +12932,16 @@ "description": "Version (tag) for the repo linked to the function template.", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -12941,8 +12951,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -28209,7 +28218,7 @@ "x-appwrite": { "method": "listRules", "group": null, - "weight": 503, + "weight": 502, "cookies": false, "type": "", "demo": "proxy\/list-rules.md", @@ -28283,7 +28292,7 @@ "x-appwrite": { "method": "createAPIRule", "group": null, - "weight": 498, + "weight": 497, "cookies": false, "type": "", "demo": "proxy\/create-api-rule.md", @@ -28350,7 +28359,7 @@ "x-appwrite": { "method": "createFunctionRule", "group": null, - "weight": 500, + "weight": 499, "cookies": false, "type": "", "demo": "proxy\/create-function-rule.md", @@ -28428,7 +28437,7 @@ "x-appwrite": { "method": "createRedirectRule", "group": null, - "weight": 501, + "weight": 500, "cookies": false, "type": "", "demo": "proxy\/create-redirect-rule.md", @@ -28541,7 +28550,7 @@ "x-appwrite": { "method": "createSiteRule", "group": null, - "weight": 499, + "weight": 498, "cookies": false, "type": "", "demo": "proxy\/create-site-rule.md", @@ -28619,7 +28628,7 @@ "x-appwrite": { "method": "getRule", "group": null, - "weight": 502, + "weight": 501, "cookies": false, "type": "", "demo": "proxy\/get-rule.md", @@ -28670,7 +28679,7 @@ "x-appwrite": { "method": "deleteRule", "group": null, - "weight": 504, + "weight": 503, "cookies": false, "type": "", "demo": "proxy\/delete-rule.md", @@ -28730,7 +28739,7 @@ "x-appwrite": { "method": "updateRuleVerification", "group": null, - "weight": 505, + "weight": 504, "cookies": false, "type": "", "demo": "proxy\/update-rule-verification.md", @@ -29085,103 +29094,6 @@ } } }, - "\/sites\/direct": { - "post": { - "summary": "Create direct deployment", - "operationId": "sitesCreateDirectDeployment", - "tags": [ - "sites" - ], - "description": "Create a deployment directly from a repository branch.", - "responses": { - "202": { - "description": "Deployment", - "content": { - "application\/json": { - "schema": { - "$ref": "#\/components\/schemas\/deployment" - } - } - } - } - }, - "deprecated": false, - "x-appwrite": { - "method": "createDirectDeployment", - "group": "deployments", - "weight": 483, - "cookies": false, - "type": "", - "demo": "sites\/create-direct-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", - "rate-limit": 0, - "rate-time": 3600, - "rate-key": "url:{url},ip:{ip}", - "scope": "sites.write", - "platforms": [ - "server" - ], - "packaging": false, - "auth": { - "Project": [] - } - }, - "security": [ - { - "Project": [], - "Key": [] - } - ], - "requestBody": { - "content": { - "application\/json": { - "schema": { - "type": "object", - "properties": { - "siteId": { - "type": "string", - "description": "Site ID.", - "x-example": "" - }, - "repository": { - "type": "string", - "description": "Repository name of the template.", - "x-example": "" - }, - "owner": { - "type": "string", - "description": "The name of the owner of the template.", - "x-example": "" - }, - "rootDirectory": { - "type": "string", - "description": "Path to site code in the template repo.", - "x-example": "" - }, - "branch": { - "type": "string", - "description": "Branch to create deployment from.", - "x-example": "" - }, - "activate": { - "type": "boolean", - "description": "Automatically activate the deployment when it is finished building.", - "x-example": false - } - }, - "required": [ - "siteId", - "repository", - "owner", - "rootDirectory", - "branch" - ] - } - } - } - } - } - }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -29255,7 +29167,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 496, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -29305,7 +29217,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 492, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/list-templates.md", @@ -29405,7 +29317,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 493, + "weight": 492, "cookies": false, "type": "", "demo": "sites\/get-template.md", @@ -29465,7 +29377,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 494, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/list-usage.md", @@ -30304,9 +30216,19 @@ }, "version": { "type": "string", - "description": "Version (tag) for the repo linked to the site template.", + "description": "Version (tag) for the repo linked to the function template.", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -30316,8 +30238,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -30734,7 +30655,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 485, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -30805,7 +30726,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 484, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -30867,7 +30788,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 486, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -30938,7 +30859,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 495, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/get-usage.md", @@ -31020,7 +30941,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 489, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -31079,7 +31000,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 487, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -31170,7 +31091,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 488, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -31239,7 +31160,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 490, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -31330,7 +31251,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 491, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -40081,7 +40002,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 508, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -40161,7 +40082,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 506, + "weight": 505, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -40250,7 +40171,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 507, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -40310,7 +40231,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 509, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -40380,7 +40301,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 510, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index d8d7178eb2..3a1afab16b 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -11714,6 +11714,16 @@ "description": "Version (tag) for the repo linked to the function template.", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -11723,8 +11733,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -20014,104 +20023,6 @@ } } }, - "\/sites\/direct": { - "post": { - "summary": "Create direct deployment", - "operationId": "sitesCreateDirectDeployment", - "tags": [ - "sites" - ], - "description": "Create a deployment directly from a repository branch.", - "responses": { - "202": { - "description": "Deployment", - "content": { - "application\/json": { - "schema": { - "$ref": "#\/components\/schemas\/deployment" - } - } - } - } - }, - "deprecated": false, - "x-appwrite": { - "method": "createDirectDeployment", - "group": "deployments", - "weight": 483, - "cookies": false, - "type": "", - "demo": "sites\/create-direct-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", - "rate-limit": 0, - "rate-time": 3600, - "rate-key": "url:{url},ip:{ip}", - "scope": "sites.write", - "platforms": [ - "server" - ], - "packaging": false, - "auth": { - "Project": [], - "Key": [] - } - }, - "security": [ - { - "Project": [], - "Key": [] - } - ], - "requestBody": { - "content": { - "application\/json": { - "schema": { - "type": "object", - "properties": { - "siteId": { - "type": "string", - "description": "Site ID.", - "x-example": "" - }, - "repository": { - "type": "string", - "description": "Repository name of the template.", - "x-example": "" - }, - "owner": { - "type": "string", - "description": "The name of the owner of the template.", - "x-example": "" - }, - "rootDirectory": { - "type": "string", - "description": "Path to site code in the template repo.", - "x-example": "" - }, - "branch": { - "type": "string", - "description": "Branch to create deployment from.", - "x-example": "" - }, - "activate": { - "type": "boolean", - "description": "Automatically activate the deployment when it is finished building.", - "x-example": false - } - }, - "required": [ - "siteId", - "repository", - "owner", - "rootDirectory", - "branch" - ] - } - } - } - } - } - }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -20186,7 +20097,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 496, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -21012,9 +20923,19 @@ }, "version": { "type": "string", - "description": "Version (tag) for the repo linked to the site template.", + "description": "Version (tag) for the repo linked to the function template.", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -21024,8 +20945,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -21447,7 +21367,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 485, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -21519,7 +21439,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 484, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -21582,7 +21502,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 486, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -21654,7 +21574,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 489, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -21714,7 +21634,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 487, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -21806,7 +21726,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 488, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -21876,7 +21796,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 490, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -21968,7 +21888,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 491, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -30122,7 +30042,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 508, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -30203,7 +30123,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 506, + "weight": 505, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -30293,7 +30213,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 507, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -30354,7 +30274,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 509, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -30425,7 +30345,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 510, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 22823cd40e..27604767b9 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -5052,7 +5052,7 @@ "x-appwrite": { "method": "getResource", "group": null, - "weight": 497, + "weight": 496, "cookies": false, "type": "", "demo": "console\/get-resource.md", @@ -12928,9 +12928,21 @@ "version": { "type": "string", "description": "Version (tag) for the repo linked to the function template.", - "default": null, + "default": "", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "default": "", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "default": "", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -12941,8 +12953,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -28351,7 +28362,7 @@ "x-appwrite": { "method": "listRules", "group": null, - "weight": 503, + "weight": 502, "cookies": false, "type": "", "demo": "proxy\/list-rules.md", @@ -28424,7 +28435,7 @@ "x-appwrite": { "method": "createAPIRule", "group": null, - "weight": 498, + "weight": 497, "cookies": false, "type": "", "demo": "proxy\/create-api-rule.md", @@ -28494,7 +28505,7 @@ "x-appwrite": { "method": "createFunctionRule", "group": null, - "weight": 500, + "weight": 499, "cookies": false, "type": "", "demo": "proxy\/create-function-rule.md", @@ -28577,7 +28588,7 @@ "x-appwrite": { "method": "createRedirectRule", "group": null, - "weight": 501, + "weight": 500, "cookies": false, "type": "", "demo": "proxy\/create-redirect-rule.md", @@ -28697,7 +28708,7 @@ "x-appwrite": { "method": "createSiteRule", "group": null, - "weight": 499, + "weight": 498, "cookies": false, "type": "", "demo": "proxy\/create-site-rule.md", @@ -28778,7 +28789,7 @@ "x-appwrite": { "method": "getRule", "group": null, - "weight": 502, + "weight": 501, "cookies": false, "type": "", "demo": "proxy\/get-rule.md", @@ -28831,7 +28842,7 @@ "x-appwrite": { "method": "deleteRule", "group": null, - "weight": 504, + "weight": 503, "cookies": false, "type": "", "demo": "proxy\/delete-rule.md", @@ -28891,7 +28902,7 @@ "x-appwrite": { "method": "updateRuleVerification", "group": null, - "weight": 505, + "weight": 504, "cookies": false, "type": "", "demo": "proxy\/update-rule-verification.md", @@ -29264,111 +29275,6 @@ ] } }, - "\/sites\/direct": { - "post": { - "summary": "Create direct deployment", - "operationId": "sitesCreateDirectDeployment", - "consumes": [ - "application\/json" - ], - "produces": [ - "application\/json" - ], - "tags": [ - "sites" - ], - "description": "Create a deployment directly from a repository branch.", - "responses": { - "202": { - "description": "Deployment", - "schema": { - "$ref": "#\/definitions\/deployment" - } - } - }, - "deprecated": false, - "x-appwrite": { - "method": "createDirectDeployment", - "group": "deployments", - "weight": 483, - "cookies": false, - "type": "", - "demo": "sites\/create-direct-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", - "rate-limit": 0, - "rate-time": 3600, - "rate-key": "url:{url},ip:{ip}", - "scope": "sites.write", - "platforms": [ - "server" - ], - "packaging": false, - "auth": { - "Project": [] - } - }, - "security": [ - { - "Project": [], - "Key": [] - } - ], - "parameters": [ - { - "name": "payload", - "in": "body", - "schema": { - "type": "object", - "properties": { - "siteId": { - "type": "string", - "description": "Site ID.", - "default": null, - "x-example": "" - }, - "repository": { - "type": "string", - "description": "Repository name of the template.", - "default": null, - "x-example": "" - }, - "owner": { - "type": "string", - "description": "The name of the owner of the template.", - "default": null, - "x-example": "" - }, - "rootDirectory": { - "type": "string", - "description": "Path to site code in the template repo.", - "default": null, - "x-example": "" - }, - "branch": { - "type": "string", - "description": "Branch to create deployment from.", - "default": null, - "x-example": "" - }, - "activate": { - "type": "boolean", - "description": "Automatically activate the deployment when it is finished building.", - "default": true, - "x-example": false - } - }, - "required": [ - "siteId", - "repository", - "owner", - "rootDirectory", - "branch" - ] - } - } - ] - } - }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -29442,7 +29348,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 496, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -29492,7 +29398,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 492, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/list-templates.md", @@ -29586,7 +29492,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 493, + "weight": 492, "cookies": false, "type": "", "demo": "sites\/get-template.md", @@ -29644,7 +29550,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 494, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/list-usage.md", @@ -30490,10 +30396,22 @@ }, "version": { "type": "string", - "description": "Version (tag) for the repo linked to the site template.", - "default": null, + "description": "Version (tag) for the repo linked to the function template.", + "default": "", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "default": "", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "default": "", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -30504,8 +30422,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -30916,7 +30833,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 485, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -30987,7 +30904,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 484, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -31051,7 +30968,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 486, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -31118,7 +31035,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 495, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/get-usage.md", @@ -31196,7 +31113,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 489, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -31255,7 +31172,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 487, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -31345,7 +31262,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 488, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -31412,7 +31329,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 490, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -31504,7 +31421,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 491, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -40021,7 +39938,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 508, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -40101,7 +40018,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 506, + "weight": 505, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -40185,7 +40102,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 507, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -40245,7 +40162,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 509, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -40316,7 +40233,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 510, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index c7a5a20c24..a13c8324a4 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -11735,9 +11735,21 @@ "version": { "type": "string", "description": "Version (tag) for the repo linked to the function template.", - "default": null, + "default": "", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "default": "", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "default": "", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -11748,8 +11760,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -20226,112 +20237,6 @@ ] } }, - "\/sites\/direct": { - "post": { - "summary": "Create direct deployment", - "operationId": "sitesCreateDirectDeployment", - "consumes": [ - "application\/json" - ], - "produces": [ - "application\/json" - ], - "tags": [ - "sites" - ], - "description": "Create a deployment directly from a repository branch.", - "responses": { - "202": { - "description": "Deployment", - "schema": { - "$ref": "#\/definitions\/deployment" - } - } - }, - "deprecated": false, - "x-appwrite": { - "method": "createDirectDeployment", - "group": "deployments", - "weight": 483, - "cookies": false, - "type": "", - "demo": "sites\/create-direct-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", - "rate-limit": 0, - "rate-time": 3600, - "rate-key": "url:{url},ip:{ip}", - "scope": "sites.write", - "platforms": [ - "server" - ], - "packaging": false, - "auth": { - "Project": [], - "Key": [] - } - }, - "security": [ - { - "Project": [], - "Key": [] - } - ], - "parameters": [ - { - "name": "payload", - "in": "body", - "schema": { - "type": "object", - "properties": { - "siteId": { - "type": "string", - "description": "Site ID.", - "default": null, - "x-example": "" - }, - "repository": { - "type": "string", - "description": "Repository name of the template.", - "default": null, - "x-example": "" - }, - "owner": { - "type": "string", - "description": "The name of the owner of the template.", - "default": null, - "x-example": "" - }, - "rootDirectory": { - "type": "string", - "description": "Path to site code in the template repo.", - "default": null, - "x-example": "" - }, - "branch": { - "type": "string", - "description": "Branch to create deployment from.", - "default": null, - "x-example": "" - }, - "activate": { - "type": "boolean", - "description": "Automatically activate the deployment when it is finished building.", - "default": true, - "x-example": false - } - }, - "required": [ - "siteId", - "repository", - "owner", - "rootDirectory", - "branch" - ] - } - } - ] - } - }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -20406,7 +20311,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 496, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -21241,10 +21146,22 @@ }, "version": { "type": "string", - "description": "Version (tag) for the repo linked to the site template.", - "default": null, + "description": "Version (tag) for the repo linked to the function template.", + "default": "", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "default": "", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "default": "", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -21255,8 +21172,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -21672,7 +21588,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 485, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -21744,7 +21660,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 484, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -21809,7 +21725,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 486, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -21877,7 +21793,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 489, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -21937,7 +21853,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 487, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -22028,7 +21944,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 488, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -22096,7 +22012,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 490, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -22189,7 +22105,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 491, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -30142,7 +30058,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 508, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -30223,7 +30139,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 506, + "weight": 505, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -30308,7 +30224,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 507, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -30369,7 +30285,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 509, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -30441,7 +30357,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 510, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php deleted file mode 100644 index d1df994149..0000000000 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php +++ /dev/null @@ -1,231 +0,0 @@ -setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) - ->setHttpPath('/v1/sites/direct') - ->desc('Create direct deployment') - ->groups(['api', 'sites']) - ->label('scope', 'sites.write') - ->label('resourceType', RESOURCE_TYPE_SITES) - ->label('event', 'sites.[siteId].deployments.[deploymentId].create') - ->label('audits.event', 'deployment.create') - ->label('audits.resource', 'site/{request.siteId}') - ->label('sdk', new Method( - namespace: 'sites', - group: 'deployments', - name: 'createDirectDeployment', - description: <<param('siteId', '', new UID(), 'Site ID.') - ->param('repository', '', new Text(128, 0), 'Repository name of the template.') - ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') - ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') - ->param('branch', '', new Text(128, 0), 'Branch to create deployment from.') - ->param('activate', true, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) - ->inject('request') - ->inject('response') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('project') - ->inject('queueForEvents') - ->inject('queueForBuilds') - ->inject('gitHub') - ->callback($this->action(...)); - } - - public function action( - string $siteId, - string $repository, - string $owner, - string $rootDirectory, - string $branch, - bool $activate, - Request $request, - Response $response, - Database $dbForProject, - Database $dbForPlatform, - Document $project, - Event $queueForEvents, - Build $queueForBuilds, - GitHub $github - ) { - $site = $dbForProject->getDocument('sites', $siteId); - - if ($site->isEmpty()) { - throw new Exception(Exception::SITE_NOT_FOUND); - } - - $template = new Document([ - 'repositoryName' => $repository, - 'ownerName' => $owner, - 'rootDirectory' => $rootDirectory, - 'branch' => $branch - ]); - - - if (!empty($site->getAttribute('providerRepositoryId'))) { - $installation = $dbForPlatform->getDocument('installations', $site->getAttribute('installationId')); - - $deployment = $this->redeployVcsSite( - request: $request, - site: $site, - project: $project, - installation: $installation, - dbForProject: $dbForProject, - dbForPlatform: $dbForPlatform, - queueForBuilds: $queueForBuilds, - template: $template, - github: $github, - activate: $activate, - ); - - $queueForEvents - ->setParam('siteId', $site->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($deployment, Response::MODEL_DEPLOYMENT); - - return; - } - - $branchUrl = "https://github.com/$owner/$repository/tree/$branch"; - $repositoryUrl = "https://github.com/$owner/$repository"; - - try { - $commitDetails = $github->getLatestCommit($owner, $repository, $branch); - } catch (\Throwable $error) { - // Ignore; deployment can continue - } - - $commands = []; - if (!empty($site->getAttribute('installCommand', ''))) { - $commands[] = $site->getAttribute('installCommand', ''); - } - if (!empty($site->getAttribute('buildCommand', ''))) { - $commands[] = $site->getAttribute('buildCommand', ''); - } - - $deploymentId = ID::unique(); - $deployment = $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceId' => $site->getId(), - 'resourceInternalId' => $site->getSequence(), - 'resourceType' => 'sites', - 'buildCommands' => \implode(' && ', $commands), - 'buildOutput' => $site->getAttribute('outputDirectory', ''), - 'adapter' => $site->getAttribute('adapter', ''), - 'fallbackFile' => $site->getAttribute('fallbackFile', ''), - 'providerRepositoryName' => $repository, - 'providerRepositoryOwner' => $owner, - 'providerRepositoryUrl' => $repositoryUrl, - 'providerBranchUrl' => $branchUrl, - 'providerBranch' => $branch, - 'providerCommitHash' => $commitDetails['commitHash'] ?? '', - 'providerCommitAuthorUrl' => $commitDetails['commitAuthorUrl'] ?? '', - 'providerCommitAuthor' => $commitDetails['commitAuthor'] ?? '', - 'providerCommitMessage' => mb_strimwidth($commitDetails['commitMessage'] ?? '', 0, 255, '...'), - 'providerCommitUrl' => $commitDetails['commitUrl'] ?? '', - 'type' => 'vcs', - 'activate' => $activate, - ])); - - $site = $site - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); - - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = ID::unique() . "." . $sitesDomain; - - // TODO: @christyjacob remove once we migrate the rules in 1.7.x - $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique(); - - Authorization::skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getSequence(), - 'domain' => $domain, - 'type' => 'deployment', - 'trigger' => 'deployment', - 'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(), - 'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(), - 'deploymentResourceType' => 'site', - 'deploymentResourceId' => $site->getId(), - 'deploymentResourceInternalId' => $site->getSequence(), - 'status' => 'verified', - 'certificateId' => '', - 'owner' => 'Appwrite', - 'region' => $project->getAttribute('region') - ])) - ); - - $queueForBuilds - ->setType(BUILD_TYPE_DEPLOYMENT) - ->setResource($site) - ->setDeployment($deployment) - ->setTemplate($template); - - $queueForEvents - ->setParam('siteId', $site->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($deployment, Response::MODEL_DEPLOYMENT); - } -} diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php index 437356d4e2..6bd151f97e 100644 --- a/src/Appwrite/Platform/Modules/Sites/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -4,7 +4,6 @@ namespace Appwrite\Platform\Modules\Sites\Services; use Appwrite\Platform\Modules\Sites\Http\Deployments\Create as CreateDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Delete as DeleteDeployment; -use Appwrite\Platform\Modules\Sites\Http\Deployments\Direct\Create as CreateDirectDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Download\Get as DownloadDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Duplicate\Create as CreateDuplicateDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Get as GetDeployment; @@ -61,7 +60,6 @@ class Http extends Service $this->addAction(DownloadDeployment::getName(), new DownloadDeployment()); $this->addAction(CreateDuplicateDeployment::getName(), new CreateDuplicateDeployment()); $this->addAction(UpdateDeploymentStatus::getName(), new UpdateDeploymentStatus()); - $this->addAction(CreateDirectDeployment::getName(), new CreateDirectDeployment()); // Logs $this->addAction(GetLog::getName(), new GetLog()); From fca27279660b7f13ca652a0f7ae9a417fe087cf6 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Sun, 14 Sep 2025 15:28:25 +0530 Subject: [PATCH 012/333] add validation for type --- .../Modules/Functions/Http/Deployments/Template/Create.php | 3 ++- .../Modules/Sites/Http/Deployments/Template/Create.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php index 885e23bfa1..6312c24d86 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -21,6 +21,7 @@ use Utopia\Platform\Scope\HTTP; use Utopia\Swoole\Request; use Utopia\Validator\Boolean; use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; class Create extends Base @@ -66,7 +67,7 @@ class Create extends Base ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.') ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) - ->param('type', '', new Text(128, 0), 'Type for the reference provided. Can be commit, branch, or version', true) + ->param('type', '', new WhiteList(['commit', 'branch', 'tag']), 'Type for the reference provided. Can be commit, branch, or version', true) ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag', true) ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index 0799e0b51f..3721740e7f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -23,6 +23,7 @@ use Utopia\Swoole\Request; use Utopia\System\System; use Utopia\Validator\Boolean; use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; class Create extends Base @@ -68,7 +69,7 @@ class Create extends Base ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) - ->param('type', '', new Text(128, 0), 'Type for the reference provided. Can be commit, branch, or version', true) + ->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type for the reference provided. Can be commit, branch, or version', true) ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag', true) ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') From de8538b6222ec78e87dc9eaa81aa5d40b0b7e4cc Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Thu, 18 Sep 2025 12:27:24 +0530 Subject: [PATCH 013/333] use request filters for 1.8.0 to convert into and --- app/controllers/general.php | 4 +++ .../Http/Deployments/Template/Create.php | 21 ++++-------- .../Http/Deployments/Template/Create.php | 21 ++++-------- src/Appwrite/Utopia/Request/Filters/V21.php | 34 +++++++++++++++++++ 4 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 src/Appwrite/Utopia/Request/Filters/V21.php diff --git a/app/controllers/general.php b/app/controllers/general.php index 8abca96742..c663091677 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -23,6 +23,7 @@ use Appwrite\Utopia\Request\Filters\V17 as RequestV17; use Appwrite\Utopia\Request\Filters\V18 as RequestV18; use Appwrite\Utopia\Request\Filters\V19 as RequestV19; use Appwrite\Utopia\Request\Filters\V20 as RequestV20; +use Appwrite\Utopia\Request\Filters\V21 as RequestV21; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filters\V16 as ResponseV16; use Appwrite\Utopia\Response\Filters\V17 as ResponseV17; @@ -906,6 +907,9 @@ App::init() $dbForProject = $getProjectDB($project); $request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request))); } + if (version_compare($requestFormat, '1.8.1', '<')) { + $request->addFilter(new RequestV21()); + } } $domain = $request->getHostname(); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php index 6312c24d86..5e0266a09e 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -66,9 +66,8 @@ class Create extends Base ->param('repository', '', new Text(128, 0), 'Repository name of the template.') ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.') - ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) - ->param('type', '', new WhiteList(['commit', 'branch', 'tag']), 'Type for the reference provided. Can be commit, branch, or version', true) - ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag', true) + ->param('type', '', new WhiteList(['commit', 'branch', 'tag']), 'Type for the reference provided. Can be commit, branch, or tag') + ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag') ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') ->inject('response') @@ -86,7 +85,6 @@ class Create extends Base string $repository, string $owner, string $rootDirectory, - string $version, string $type, string $reference, bool $activate, @@ -105,22 +103,15 @@ class Create extends Base throw new Exception(Exception::FUNCTION_NOT_FOUND); } - if (empty($version) && empty($type) && empty($reference)) { - throw new Exception("Either version or type & reference must be provided"); - } - - $referenceType = !empty($version) ? GitHub::CLONE_TYPE_TAG : $type; - $referenceValue = !empty($version) ? $version : $reference; - - $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$referenceValue" : ""; + $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$reference" : ""; $repositoryUrl = "https://github.com/$owner/$repository"; $template = new Document([ 'repositoryName' => $repository, 'ownerName' => $owner, 'rootDirectory' => $rootDirectory, - 'referenceType' => $referenceType, - 'referenceValue' => $referenceValue, + 'referenceType' => $type, + 'referenceValue' => $reference, ]); if (!empty($function->getAttribute('providerRepositoryId'))) { @@ -166,7 +157,7 @@ class Create extends Base 'providerRepositoryOwner' => $owner, 'providerRepositoryUrl' => $repositoryUrl, 'providerBranchUrl' => $branchUrl, - 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $referenceValue : '', + 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $reference : '', 'type' => 'vcs', 'activate' => $activate, ])); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index 3721740e7f..d7196eb3d5 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -68,9 +68,8 @@ class Create extends Base ->param('repository', '', new Text(128, 0), 'Repository name of the template.') ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') - ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) - ->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type for the reference provided. Can be commit, branch, or version', true) - ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag', true) + ->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type for the reference provided. Can be commit, branch, or tag') + ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag') ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') ->inject('response') @@ -88,7 +87,6 @@ class Create extends Base string $repository, string $owner, string $rootDirectory, - string $version, string $type, string $reference, bool $activate, @@ -107,22 +105,15 @@ class Create extends Base throw new Exception(Exception::SITE_NOT_FOUND); } - if (empty($version) && empty($type) && empty($reference)) { - throw new Exception("Either version or type & reference must be provided"); - } - - $referenceType = !empty($version) ? GitHub::CLONE_TYPE_TAG : $type; - $referenceValue = !empty($version) ? $version : $reference; - - $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$referenceValue" : ""; + $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$reference" : ""; $repositoryUrl = "https://github.com/$owner/$repository"; $template = new Document([ 'repositoryName' => $repository, 'ownerName' => $owner, 'rootDirectory' => $rootDirectory, - 'referenceType' => $referenceType, - 'referenceValue' => $referenceValue + 'referenceType' => $type, + 'referenceValue' => $reference ]); if (!empty($site->getAttribute('providerRepositoryId'))) { @@ -177,7 +168,7 @@ class Create extends Base 'providerRepositoryOwner' => $owner, 'providerRepositoryUrl' => $repositoryUrl, 'providerBranchUrl' => $branchUrl, - 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $referenceValue : '', + 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $reference : '', 'adapter' => $site->getAttribute('adapter', ''), 'fallbackFile' => $site->getAttribute('fallbackFile', ''), 'type' => 'vcs', diff --git a/src/Appwrite/Utopia/Request/Filters/V21.php b/src/Appwrite/Utopia/Request/Filters/V21.php new file mode 100644 index 0000000000..3ef0becf1d --- /dev/null +++ b/src/Appwrite/Utopia/Request/Filters/V21.php @@ -0,0 +1,34 @@ +convertVersionToTypeAndReference($content); + break; + } + return $content; + } + + /** + * Convert version parameter to type and reference for backwards compatibility + * with 1.8.0 template deployment endpoints + */ + protected function convertVersionToTypeAndReference(array $content): array + { + if (!empty($content['version'])) { + $content['type'] = 'tag'; + $content['reference'] = $content['version']; + unset($content['version']); + } + return $content; + } +} From 8e32fb05d1361778e64b583f468e2cd6d7de77f1 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Thu, 18 Sep 2025 12:28:08 +0530 Subject: [PATCH 014/333] specs --- .../specs/open-api3-latest-console.json | 40 +++++++++------ app/config/specs/open-api3-latest-server.json | 40 +++++++++------ app/config/specs/swagger2-latest-console.json | 50 +++++++++++-------- app/config/specs/swagger2-latest-server.json | 50 +++++++++++-------- 4 files changed, 104 insertions(+), 76 deletions(-) diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 96cec5c5a6..e052f542c9 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -12927,15 +12927,17 @@ "description": "Path to function code in the template repo.", "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "x-example": "commit", + "enum": [ + "commit", + "branch", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", @@ -12951,7 +12953,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } @@ -30214,15 +30218,17 @@ "description": "Path to site code in the template repo.", "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "x-example": "branch", + "enum": [ + "branch", + "commit", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", @@ -30238,7 +30244,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 3a1afab16b..b111f8e3e8 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -11709,15 +11709,17 @@ "description": "Path to function code in the template repo.", "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "x-example": "commit", + "enum": [ + "commit", + "branch", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", @@ -11733,7 +11735,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } @@ -20921,15 +20925,17 @@ "description": "Path to site code in the template repo.", "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "x-example": "branch", + "enum": [ + "branch", + "commit", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", @@ -20945,7 +20951,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 27604767b9..b359eaf310 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -12925,22 +12925,23 @@ "default": null, "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "default": "", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "default": "", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "default": null, + "x-example": "commit", + "enum": [ + "commit", + "branch", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", "description": "Reference value, can be a commit hash, branch name, or release tag", - "default": "", + "default": null, "x-example": "" }, "activate": { @@ -12953,7 +12954,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } @@ -30394,22 +30397,23 @@ "default": null, "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "default": "", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "default": "", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "default": null, + "x-example": "branch", + "enum": [ + "branch", + "commit", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", "description": "Reference value, can be a commit hash, branch name, or release tag", - "default": "", + "default": null, "x-example": "" }, "activate": { @@ -30422,7 +30426,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index a13c8324a4..b806a9e6c7 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -11732,22 +11732,23 @@ "default": null, "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "default": "", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "default": "", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "default": null, + "x-example": "commit", + "enum": [ + "commit", + "branch", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", "description": "Reference value, can be a commit hash, branch name, or release tag", - "default": "", + "default": null, "x-example": "" }, "activate": { @@ -11760,7 +11761,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } @@ -21144,22 +21147,23 @@ "default": null, "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "default": "", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "default": "", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "default": null, + "x-example": "branch", + "enum": [ + "branch", + "commit", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", "description": "Reference value, can be a commit hash, branch name, or release tag", - "default": "", + "default": null, "x-example": "" }, "activate": { @@ -21172,7 +21176,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } From 9fe3e94a669240e3a8669d9da28cdea51561358a Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Thu, 18 Sep 2025 13:12:46 +0530 Subject: [PATCH 015/333] e2e tests --- .../e2e/Services/Functions/FunctionsBase.php | 28 ++++ .../Functions/FunctionsCustomServerTest.php | 117 +++++++++++++- tests/e2e/Services/Sites/SitesBase.php | 29 ++++ .../Services/Sites/SitesCustomServerTest.php | 149 +++++++++++++++++- 4 files changed, 321 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 27b67d851d..4eb31c08ac 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -268,6 +268,34 @@ trait FunctionsBase 'x-appwrite-project' => $this->getProject()['$id'], ])); + // Fetch latest commit from GitHub API if template has provider info + if ( + isset($template['body']['providerOwner']) && + isset($template['body']['providerRepositoryId']) + ) { + $owner = $template['body']['providerOwner']; + $repo = $template['body']['providerRepositoryId']; + + // GitHub API to get latest commit from main branch + $ch = curl_init("https://api.github.com/repos/{$owner}/{$repo}/commits/main"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'User-Agent: Appwrite', + 'Accept: application/vnd.github.v3+json' + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + $commitData = json_decode($response, true); + if (isset($commitData['sha'])) { + $template['body']['latestCommit'] = $commitData['sha']; + } + } + } + return $template; } diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 0d63791151..8a774ed8bb 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -400,7 +400,8 @@ class FunctionsCustomServerTest extends Scope 'repository' => $starterTemplate['body']['providerRepositoryId'], 'owner' => $starterTemplate['body']['providerOwner'], 'rootDirectory' => $phpRuntime['providerRootDirectory'], - 'version' => $starterTemplate['body']['providerVersion'], + 'type' => 'tag', + 'reference' => $starterTemplate['body']['providerVersion'], ] ); @@ -502,6 +503,120 @@ class FunctionsCustomServerTest extends Scope $function = $this->cleanupFunction($functionId); } + public function testCreateFunctionAndDeploymentFromTemplateBranch() + { + $starterTemplate = $this->getTemplate('starter'); + $this->assertEquals(200, $starterTemplate['headers']['status-code']); + + $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + return $runtime['name'] === 'node-22'; + }))[0]; + + // If this fails, the template has variables, and this test needs to be updated + $this->assertEmpty($starterTemplate['body']['variables']); + + $function = $this->createFunction( + [ + 'functionId' => ID::unique(), + 'name' => $starterTemplate['body']['name'] . ' - Branch Test', + 'runtime' => 'node-22', + 'execute' => $starterTemplate['body']['permissions'], + 'entrypoint' => $phpRuntime['entrypoint'], + 'events' => $starterTemplate['body']['events'], + 'schedule' => $starterTemplate['body']['cron'], + 'timeout' => $starterTemplate['body']['timeout'], + 'commands' => $phpRuntime['commands'], + 'scopes' => $starterTemplate['body']['scopes'], + ] + ); + + $this->assertEquals(201, $function['headers']['status-code']); + $this->assertNotEmpty($function['body']['$id']); + + $functionId = $function['body']['$id'] ?? ''; + + // Deploy using branch + $deployment = $this->createTemplateDeployment( + $functionId, + [ + 'resourceId' => ID::unique(), + 'activate' => true, + 'repository' => $starterTemplate['body']['providerRepositoryId'], + 'owner' => $starterTemplate['body']['providerOwner'], + 'rootDirectory' => $phpRuntime['providerRootDirectory'], + 'type' => 'branch', + 'reference' => 'main', + ] + ); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deployment = $this->getDeployment($functionId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + + $function = $this->cleanupFunction($functionId); + } + + public function testCreateFunctionAndDeploymentFromTemplateCommit() + { + $starterTemplate = $this->getTemplate('starter'); + $this->assertEquals(200, $starterTemplate['headers']['status-code']); + + // Ensure we have the latest commit + $this->assertArrayHasKey('latestCommit', $starterTemplate['body']); + $latestCommit = $starterTemplate['body']['latestCommit']; + + $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + return $runtime['name'] === 'node-22'; + }))[0]; + + // If this fails, the template has variables, and this test needs to be updated + $this->assertEmpty($starterTemplate['body']['variables']); + + $function = $this->createFunction( + [ + 'functionId' => ID::unique(), + 'name' => $starterTemplate['body']['name'] . ' - Commit Test', + 'runtime' => 'node-22', + 'execute' => $starterTemplate['body']['permissions'], + 'entrypoint' => $phpRuntime['entrypoint'], + 'events' => $starterTemplate['body']['events'], + 'schedule' => $starterTemplate['body']['cron'], + 'timeout' => $starterTemplate['body']['timeout'], + 'commands' => $phpRuntime['commands'], + 'scopes' => $starterTemplate['body']['scopes'], + ] + ); + + $this->assertEquals(201, $function['headers']['status-code']); + $this->assertNotEmpty($function['body']['$id']); + + $functionId = $function['body']['$id'] ?? ''; + + // Deploy using commit + $deployment = $this->createTemplateDeployment( + $functionId, + [ + 'resourceId' => ID::unique(), + 'activate' => true, + 'repository' => $starterTemplate['body']['providerRepositoryId'], + 'owner' => $starterTemplate['body']['providerOwner'], + 'rootDirectory' => $phpRuntime['providerRootDirectory'], + 'type' => 'commit', + 'reference' => $latestCommit, + ] + ); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deployment = $this->getDeployment($functionId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + + $function = $this->cleanupFunction($functionId); + } + /** * @depends testUpdateFunction */ diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index 93c55b82b7..004032452b 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -329,6 +329,35 @@ trait SitesBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ]); + + // Fetch latest commit from GitHub API if template has provider info + if ( + isset($template['body']['providerOwner']) && + isset($template['body']['providerRepositoryId']) + ) { + $owner = $template['body']['providerOwner']; + $repo = $template['body']['providerRepositoryId']; + + // GitHub API to get latest commit from main branch + $ch = curl_init("https://api.github.com/repos/{$owner}/{$repo}/commits/main"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'User-Agent: Appwrite', + 'Accept: application/vnd.github.v3+json' + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + $commitData = json_decode($response, true); + if (isset($commitData['sha'])) { + $template['body']['latestCommit'] = $commitData['sha']; + } + } + } + return $template; } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index c8301b9428..524e8a09f3 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1563,7 +1563,154 @@ class SitesCustomServerTest extends Scope 'repository' => $template['providerRepositoryId'], 'owner' => $template['providerOwner'], 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], - 'version' => $template['providerVersion'], + 'type' => 'tag', + 'reference' => $template['providerVersion'], + 'activate' => true + ]); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals(0, $deployment['body']['sourceSize']); + $this->assertEquals(0, $deployment['body']['buildSize']); + $this->assertEquals(0, $deployment['body']['totalSize']); + + $this->assertEventually(function () use ($siteId) { + $site = $this->getSite($siteId); + $this->assertNotEmpty($site['body']['deploymentId']); + }, 50000, 500); + + $domain = $this->setupSiteDomain($siteId); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Astro Blog", $response['body']); + $this->assertStringContainsString("Hello, Astronaut!", $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/about'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Astro Blog", $response['body']); + $this->assertStringContainsString("About Me", $response['body']); + + $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); + + $this->cleanupSite($siteId); + } + + public function testCreateSiteFromTemplateBranch() + { + $template = $this->getTemplate('playground-for-astro'); + $this->assertEquals(200, $template['headers']['status-code']); + + $template = $template['body']; + + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Astro Blog - Branch Test', + 'framework' => $template['frameworks'][0]['key'], + 'adapter' => $template['frameworks'][0]['adapter'], + 'buildRuntime' => $template['frameworks'][0]['buildRuntime'], + 'outputDirectory' => $template['frameworks'][0]['outputDirectory'], + 'buildCommand' => $template['frameworks'][0]['buildCommand'], + 'installCommand' => $template['frameworks'][0]['installCommand'], + 'fallbackFile' => $template['frameworks'][0]['fallbackFile'], + ]); + + $this->assertNotEmpty($siteId); + + // Deploy using branch + $deployment = $this->createTemplateDeployment($siteId, [ + 'repository' => $template['providerRepositoryId'], + 'owner' => $template['providerOwner'], + 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], + 'type' => 'branch', + 'reference' => 'main', + 'activate' => true + ]); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals(0, $deployment['body']['sourceSize']); + $this->assertEquals(0, $deployment['body']['buildSize']); + $this->assertEquals(0, $deployment['body']['totalSize']); + + $this->assertEventually(function () use ($siteId) { + $site = $this->getSite($siteId); + $this->assertNotEmpty($site['body']['deploymentId']); + }, 50000, 500); + + $domain = $this->setupSiteDomain($siteId); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Astro Blog", $response['body']); + $this->assertStringContainsString("Hello, Astronaut!", $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/about'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Astro Blog", $response['body']); + $this->assertStringContainsString("About Me", $response['body']); + + $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); + + $this->cleanupSite($siteId); + } + + public function testCreateSiteFromTemplateCommit() + { + $template = $this->getTemplate('playground-for-astro'); + $this->assertEquals(200, $template['headers']['status-code']); + + // Ensure we have the latest commit + $this->assertArrayHasKey('latestCommit', $template['body']); + $latestCommit = $template['body']['latestCommit']; + + $template = $template['body']; + + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Astro Blog - Commit Test', + 'framework' => $template['frameworks'][0]['key'], + 'adapter' => $template['frameworks'][0]['adapter'], + 'buildRuntime' => $template['frameworks'][0]['buildRuntime'], + 'outputDirectory' => $template['frameworks'][0]['outputDirectory'], + 'buildCommand' => $template['frameworks'][0]['buildCommand'], + 'installCommand' => $template['frameworks'][0]['installCommand'], + 'fallbackFile' => $template['frameworks'][0]['fallbackFile'], + ]); + + $this->assertNotEmpty($siteId); + + // Deploy using commit + $deployment = $this->createTemplateDeployment($siteId, [ + 'repository' => $template['providerRepositoryId'], + 'owner' => $template['providerOwner'], + 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], + 'type' => 'commit', + 'reference' => $latestCommit, 'activate' => true ]); From 2a145bc284d1edff27014187b316493af85448b6 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Fri, 19 Sep 2025 18:18:44 +0530 Subject: [PATCH 016/333] address reviews regarding tests --- .../e2e/Services/Functions/FunctionsBase.php | 41 ++++----- .../Functions/FunctionsCustomServerTest.php | 87 ++++++++++++------- tests/e2e/Services/Sites/SitesBase.php | 41 ++++----- .../Services/Sites/SitesCustomServerTest.php | 9 +- 4 files changed, 97 insertions(+), 81 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 4eb31c08ac..7403b23a73 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -268,35 +268,30 @@ trait FunctionsBase 'x-appwrite-project' => $this->getProject()['$id'], ])); - // Fetch latest commit from GitHub API if template has provider info - if ( - isset($template['body']['providerOwner']) && - isset($template['body']['providerRepositoryId']) - ) { - $owner = $template['body']['providerOwner']; - $repo = $template['body']['providerRepositoryId']; + return $template; + } - // GitHub API to get latest commit from main branch - $ch = curl_init("https://api.github.com/repos/{$owner}/{$repo}/commits/main"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'User-Agent: Appwrite', - 'Accept: application/vnd.github.v3+json' - ]); + protected function helperGetLatestCommit(string $owner, string $repository): ?string + { + $ch = curl_init("https://api.github.com/repos/{$owner}/{$repository}/commits/main"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'User-Agent: Appwrite', + 'Accept: application/vnd.github.v3+json' + ]); - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); - if ($httpCode === 200) { - $commitData = json_decode($response, true); - if (isset($commitData['sha'])) { - $template['body']['latestCommit'] = $commitData['sha']; - } + if ($httpCode === 200) { + $commitData = json_decode($response, true); + if (isset($commitData['sha'])) { + return $commitData['sha']; } } - return $template; + return null; } protected function createExecution(string $functionId, mixed $params = []): mixed diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 8a774ed8bb..5672bd9817 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -361,7 +361,7 @@ class FunctionsCustomServerTest extends Scope $starterTemplate = $this->getTemplate('starter'); $this->assertEquals(200, $starterTemplate['headers']['status-code']); - $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + $runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { return $runtime['name'] === 'node-22'; }))[0]; @@ -374,15 +374,15 @@ class FunctionsCustomServerTest extends Scope 'name' => $starterTemplate['body']['name'], 'runtime' => 'node-22', 'execute' => $starterTemplate['body']['permissions'], - 'entrypoint' => $phpRuntime['entrypoint'], + 'entrypoint' => $runtime['entrypoint'], 'events' => $starterTemplate['body']['events'], 'schedule' => $starterTemplate['body']['cron'], 'timeout' => $starterTemplate['body']['timeout'], - 'commands' => $phpRuntime['commands'], + 'commands' => $runtime['commands'], 'scopes' => $starterTemplate['body']['scopes'], 'templateRepository' => $starterTemplate['body']['providerRepositoryId'], 'templateOwner' => $starterTemplate['body']['providerOwner'], - 'templateRootDirectory' => $phpRuntime['providerRootDirectory'], + 'templateRootDirectory' => $runtime['providerRootDirectory'], 'templateVersion' => $starterTemplate['body']['providerVersion'], ] ); @@ -399,7 +399,7 @@ class FunctionsCustomServerTest extends Scope 'activate' => true, 'repository' => $starterTemplate['body']['providerRepositoryId'], 'owner' => $starterTemplate['body']['providerOwner'], - 'rootDirectory' => $phpRuntime['providerRootDirectory'], + 'rootDirectory' => $runtime['providerRootDirectory'], 'type' => 'tag', 'reference' => $starterTemplate['body']['providerVersion'], ] @@ -408,11 +408,20 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); - $deployment = $this->getDeployment($functionId, $deployment['body']['$id']); + // Wait for deployment to be ready + $deploymentId = $deployment['body']['$id']; + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals('ready', $deployment['body']['status']); + }, 50000, 500); + + // Verify deployment sizes + $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals(0, $deployment['body']['sourceSize']); - $this->assertEquals(0, $deployment['body']['buildSize']); - $this->assertEquals(0, $deployment['body']['totalSize']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); $deployments = $this->listDeployments($functionId); @@ -422,16 +431,7 @@ class FunctionsCustomServerTest extends Scope $lastDeployment = $deployments['body']['deployments'][0]; $this->assertNotEmpty($lastDeployment['$id']); - $this->assertEquals(0, $lastDeployment['sourceSize']); - - $deploymentId = $lastDeployment['$id']; - - $this->assertEventually(function () use ($functionId, $deploymentId) { - $deployment = $this->getDeployment($functionId, $deploymentId); - - $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals('ready', $deployment['body']['status']); - }, 50000, 1000); + $this->assertGreaterThan(0, $lastDeployment['sourceSize']); $function = $this->getFunction($functionId); @@ -508,7 +508,7 @@ class FunctionsCustomServerTest extends Scope $starterTemplate = $this->getTemplate('starter'); $this->assertEquals(200, $starterTemplate['headers']['status-code']); - $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + $runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { return $runtime['name'] === 'node-22'; }))[0]; @@ -521,11 +521,11 @@ class FunctionsCustomServerTest extends Scope 'name' => $starterTemplate['body']['name'] . ' - Branch Test', 'runtime' => 'node-22', 'execute' => $starterTemplate['body']['permissions'], - 'entrypoint' => $phpRuntime['entrypoint'], + 'entrypoint' => $runtime['entrypoint'], 'events' => $starterTemplate['body']['events'], 'schedule' => $starterTemplate['body']['cron'], 'timeout' => $starterTemplate['body']['timeout'], - 'commands' => $phpRuntime['commands'], + 'commands' => $runtime['commands'], 'scopes' => $starterTemplate['body']['scopes'], ] ); @@ -543,7 +543,7 @@ class FunctionsCustomServerTest extends Scope 'activate' => true, 'repository' => $starterTemplate['body']['providerRepositoryId'], 'owner' => $starterTemplate['body']['providerOwner'], - 'rootDirectory' => $phpRuntime['providerRootDirectory'], + 'rootDirectory' => $runtime['providerRootDirectory'], 'type' => 'branch', 'reference' => 'main', ] @@ -552,8 +552,18 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); - $deployment = $this->getDeployment($functionId, $deployment['body']['$id']); + $deploymentId = $deployment['body']['$id']; + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals('ready', $deployment['body']['status']); + }, 50000, 500); + + $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); $function = $this->cleanupFunction($functionId); } @@ -563,11 +573,14 @@ class FunctionsCustomServerTest extends Scope $starterTemplate = $this->getTemplate('starter'); $this->assertEquals(200, $starterTemplate['headers']['status-code']); - // Ensure we have the latest commit - $this->assertArrayHasKey('latestCommit', $starterTemplate['body']); - $latestCommit = $starterTemplate['body']['latestCommit']; + // Get latest commit using helper function + $latestCommit = $this->helperGetLatestCommit( + $starterTemplate['body']['providerOwner'], + $starterTemplate['body']['providerRepositoryId'] + ); + $this->assertNotNull($latestCommit); - $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + $runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { return $runtime['name'] === 'node-22'; }))[0]; @@ -580,11 +593,11 @@ class FunctionsCustomServerTest extends Scope 'name' => $starterTemplate['body']['name'] . ' - Commit Test', 'runtime' => 'node-22', 'execute' => $starterTemplate['body']['permissions'], - 'entrypoint' => $phpRuntime['entrypoint'], + 'entrypoint' => $runtime['entrypoint'], 'events' => $starterTemplate['body']['events'], 'schedule' => $starterTemplate['body']['cron'], 'timeout' => $starterTemplate['body']['timeout'], - 'commands' => $phpRuntime['commands'], + 'commands' => $runtime['commands'], 'scopes' => $starterTemplate['body']['scopes'], ] ); @@ -602,7 +615,7 @@ class FunctionsCustomServerTest extends Scope 'activate' => true, 'repository' => $starterTemplate['body']['providerRepositoryId'], 'owner' => $starterTemplate['body']['providerOwner'], - 'rootDirectory' => $phpRuntime['providerRootDirectory'], + 'rootDirectory' => $runtime['providerRootDirectory'], 'type' => 'commit', 'reference' => $latestCommit, ] @@ -611,8 +624,18 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); - $deployment = $this->getDeployment($functionId, $deployment['body']['$id']); + $deploymentId = $deployment['body']['$id']; + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals('ready', $deployment['body']['status']); + }, 50000, 500); + + $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); $function = $this->cleanupFunction($functionId); } diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index 004032452b..7eb5d9699c 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -330,35 +330,30 @@ trait SitesBase 'x-appwrite-project' => $this->getProject()['$id'], ]); - // Fetch latest commit from GitHub API if template has provider info - if ( - isset($template['body']['providerOwner']) && - isset($template['body']['providerRepositoryId']) - ) { - $owner = $template['body']['providerOwner']; - $repo = $template['body']['providerRepositoryId']; + return $template; + } - // GitHub API to get latest commit from main branch - $ch = curl_init("https://api.github.com/repos/{$owner}/{$repo}/commits/main"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'User-Agent: Appwrite', - 'Accept: application/vnd.github.v3+json' - ]); + protected function helperGetLatestCommit(string $owner, string $repository): ?string + { + $ch = curl_init("https://api.github.com/repos/{$owner}/{$repository}/commits/main"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'User-Agent: Appwrite', + 'Accept: application/vnd.github.v3+json' + ]); - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); - if ($httpCode === 200) { - $commitData = json_decode($response, true); - if (isset($commitData['sha'])) { - $template['body']['latestCommit'] = $commitData['sha']; - } + if ($httpCode === 200) { + $commitData = json_decode($response, true); + if (isset($commitData['sha'])) { + return $commitData['sha']; } } - return $template; + return null; } protected function deleteSite(string $siteId): mixed diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 524e8a09f3..f7015ccb48 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1684,9 +1684,12 @@ class SitesCustomServerTest extends Scope $template = $this->getTemplate('playground-for-astro'); $this->assertEquals(200, $template['headers']['status-code']); - // Ensure we have the latest commit - $this->assertArrayHasKey('latestCommit', $template['body']); - $latestCommit = $template['body']['latestCommit']; + // Get latest commit using helper function + $latestCommit = $this->helperGetLatestCommit( + $template['body']['providerOwner'], + $template['body']['providerRepositoryId'] + ); + $this->assertNotNull($latestCommit); $template = $template['body']; From 1a19e01d69d563aff5942ea836cfdb8b484875ee Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Fri, 19 Sep 2025 18:30:42 +0530 Subject: [PATCH 017/333] attempt to fix tests --- tests/e2e/General/UsageTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index 8f5477331a..dc49d27aea 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -28,6 +28,7 @@ class UsageTest extends Scope FunctionsBase::createVariable insteadof SitesBase; FunctionsBase::getVariable insteadof SitesBase; FunctionsBase::listVariables insteadof SitesBase; + FunctionsBase::helperGetLatestCommit insteadof SitesBase; FunctionsBase::updateVariable insteadof SitesBase; FunctionsBase::deleteVariable insteadof SitesBase; FunctionsBase::getDeployment insteadof SitesBase; From 7d4486b9e691789bed9a68e62df2f455e18d57ce Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Fri, 19 Sep 2025 18:42:25 +0530 Subject: [PATCH 018/333] more fixes to tests --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 5672bd9817..a3f8768676 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -500,7 +500,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals($deployment['body']['$id'], $function['body']['deploymentId']); $this->assertEquals($deployment['body']['$createdAt'], $function['body']['deploymentCreatedAt']); - $function = $this->cleanupFunction($functionId); + $this->cleanupFunction($functionId); } public function testCreateFunctionAndDeploymentFromTemplateBranch() @@ -565,7 +565,7 @@ class FunctionsCustomServerTest extends Scope $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; $this->assertEquals($totalSize, $deployment['body']['totalSize']); - $function = $this->cleanupFunction($functionId); + $this->cleanupFunction($functionId); } public function testCreateFunctionAndDeploymentFromTemplateCommit() @@ -637,7 +637,7 @@ class FunctionsCustomServerTest extends Scope $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; $this->assertEquals($totalSize, $deployment['body']['totalSize']); - $function = $this->cleanupFunction($functionId); + $this->cleanupFunction($functionId); } /** From 5963b2baf799d99c5bcb7610595b5ed70fc4ef48 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Tue, 23 Sep 2025 18:00:14 +0530 Subject: [PATCH 019/333] change request filter to versions below 1.9.0 --- app/controllers/general.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index c663091677..f0294e9274 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -907,7 +907,7 @@ App::init() $dbForProject = $getProjectDB($project); $request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request))); } - if (version_compare($requestFormat, '1.8.1', '<')) { + if (version_compare($requestFormat, '1.9.0', '<')) { $request->addFilter(new RequestV21()); } } From c8993d7f713a073ead1bfca7c54ee130863ea30c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 24 Sep 2025 22:37:12 +1200 Subject: [PATCH 020/333] Add additional export params --- app/controllers/api/migrations.php | 101 +++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 27 deletions(-) diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index d5bcfc2fd7..6e0ecd218d 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -339,12 +339,20 @@ App::post('/v1/migrations/csv/imports') ->inject('deviceForMigrations') ->inject('queueForEvents') ->inject('queueForMigrations') - ->action(function (string $bucketId, string $fileId, string $resourceId, bool $internalFile, Response $response, Database $dbForProject, Document $project, Device $deviceForFiles, Device $deviceForMigrations, Event $queueForEvents, Migration $queueForMigrations) { - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - if ($internalFile && !$isPrivilegedUser) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } + ->action(function ( + string $bucketId, + string $fileId, + string $resourceId, + bool $internalFile, + Response $response, + Database $dbForProject, + Database $dbForPlatform, + Document $project, + Device $deviceForFiles, + Device $deviceForMigrations, + Event $queueForEvents, + Migration $queueForMigrations + ) { $bucket = Authorization::skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) { if ($internalFile) { return $dbForPlatform->getDocument('buckets', 'default'); @@ -352,7 +360,7 @@ App::post('/v1/migrations/csv/imports') return $dbForProject->getDocument('buckets', $bucketId); }); - if ($bucket->isEmpty() || (!$isAPIKey && !$isPrivilegedUser)) { + if ($bucket->isEmpty()) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } @@ -366,7 +374,7 @@ App::post('/v1/migrations/csv/imports') throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path); } - // no encryption, compression on files above 20MB. + // No encryption or compression on files above 20MB. $hasEncryption = !empty($file->getAttribute('openSSLCipher')); $compression = $file->getAttribute('algorithm', Compression::NONE); $hasCompression = $compression !== Compression::NONE; @@ -377,7 +385,6 @@ App::post('/v1/migrations/csv/imports') if ($hasEncryption || $hasCompression) { $source = $deviceForFiles->read($path); - // 1. decrypt if ($hasEncryption) { $source = OpenSSL::decrypt( $source, @@ -389,7 +396,6 @@ App::post('/v1/migrations/csv/imports') ); } - // 2. decompress if ($hasCompression) { switch ($compression) { case Compression::ZSTD: @@ -401,12 +407,12 @@ App::post('/v1/migrations/csv/imports') } } - // manual write after decryption and/or decompression - if (! $deviceForMigrations->write($newPath, $source, 'text/csv')) { - throw new \Exception("Unable to copy file"); + // Manual write after decryption and/or decompression + if (!$deviceForMigrations->write($newPath, $source, 'text/csv')) { + throw new \Exception('Unable to copy file'); } - } elseif (! $deviceForFiles->transfer($path, $newPath, $deviceForMigrations)) { - throw new \Exception("Unable to copy file"); + } elseif (!$deviceForFiles->transfer($path, $newPath, $deviceForMigrations)) { + throw new \Exception('Unable to copy file'); } $fileSize = $deviceForMigrations->getFileSize($newPath); @@ -461,34 +467,68 @@ App::post('/v1/migrations/csv/exports') ) ] )) - ->param('bucketId', '', new UID(), 'Storage bucket unique ID where the exported CSV will be stored.') ->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.') - ->param('columns', [], new ArrayList(new Text(255)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true) + ->param('bucketId', '', new UID(), 'Storage bucket unique ID where the exported CSV will be stored.') + ->param('filename', '', new Text(255), 'The name of the file to be created for the export, excluding the .csv extension.') + ->param('columns', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true) + ->param('delimiter', ',', new Text(1), 'The character that separates each column value. Default is comma ",".', true) + ->param('enclosure', '"', new Text(1), 'The character that encloses each column value. Default is double quotes \'"\'.', true) + ->param('escape', '\\', new Text(1), 'The escape character for the enclosure character. Default is backslash "\\".', true) + ->param('header', true, new Boolean(), 'Whether to include the header row with column names. Default is true.', true) + ->param('notify', true, new Boolean(), 'Set to true to receive an email when the export is complete. Default is true.', true) + ->inject('user') ->inject('response') ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') ->inject('queueForMigrations') - ->action(function (string $bucketId, string $resourceId, array $columns, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Migration $queueForMigrations) { - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - + ->action(function ( + string $resourceId, + string $bucketId, + string $filename, + array $columns, + string $delimiter, + string $enclosure, + string $escape, + bool $header, + bool $notify, + Document $user, + Response $response, + Database $dbForProject, + Document $project, + Event $queueForEvents, + Migration $queueForMigrations + ) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - - if ($bucket->isEmpty() || (!$isAPIKey && !$isPrivilegedUser)) { + if ($bucket->isEmpty()) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $migrationId = ID::unique(); - $resources = Transfer::extractServices([Transfer::GROUP_DATABASES]); + [$databaseId, $collectionId] = \explode(':', $resourceId, 2); + if (empty($databaseId)) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + if (empty($collectionId)) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); + if ($collection->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } $migration = $dbForProject->createDocument('migrations', new Document([ - '$id' => $migrationId, + '$id' => ID::unique(), 'status' => 'pending', 'stage' => 'init', 'source' => Appwrite::getName(), 'destination' => CSV::getName(), - 'resources' => $resources, + 'resources' => Transfer::extractServices([Transfer::GROUP_DATABASES]), 'resourceId' => $resourceId, 'resourceType' => Resource::TYPE_DATABASE, 'statusCounters' => '{}', @@ -496,7 +536,14 @@ App::post('/v1/migrations/csv/exports') 'errors' => [], 'options' => [ 'bucketId' => $bucketId, + 'filename' => $filename, 'columns' => $columns, + 'delimiter' => $delimiter, + 'enclosure' => $enclosure, + 'escape' => $escape, + 'header' => $header, + 'notify' => $notify, + 'userInternalId' => $user->getSequence(), ], ])); From 5e8951fbe02dce5ac3f7841b45ff2328ca868794 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 24 Sep 2025 22:37:49 +1200 Subject: [PATCH 021/333] Save export to bucket on complete --- src/Appwrite/Platform/Workers/Migrations.php | 224 +++++++++++++------ 1 file changed, 153 insertions(+), 71 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index ce9b7e2881..bbdf537157 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -3,19 +3,23 @@ namespace Appwrite\Platform\Workers; use Ahc\Jwt\JWT; +use Appwrite\Event\Mail; use Appwrite\Event\Realtime; -use Utopia\Migration\Destinations\CSV as DestinationCSV; +use Appwrite\Template\Template; use Exception; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Locale\Locale; use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; +use Utopia\Database\Query; use Utopia\Migration\Destination; use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite; +use Utopia\Migration\Destinations\CSV as DestinationCSV; use Utopia\Migration\Exception as MigrationException; use Utopia\Migration\Source; use Utopia\Migration\Sources\Appwrite; @@ -27,6 +31,7 @@ use Utopia\Migration\Sources\Supabase; use Utopia\Migration\Transfer; use Utopia\Platform\Action; use Utopia\Queue\Message; +use Utopia\Storage\Compression\Compression; use Utopia\Storage\Device; use Utopia\System\System; @@ -37,6 +42,7 @@ class Migrations extends Action protected Database $dbForPlatform; protected Device $deviceForMigrations; + protected Device $deviceForFiles; protected Document $project; @@ -71,16 +77,28 @@ class Migrations extends Action ->inject('logError') ->inject('queueForRealtime') ->inject('deviceForMigrations') + ->inject('deviceForFiles') + ->inject('queueForMails') ->callback($this->action(...)); } /** * @throws Exception */ - public function action(Message $message, Document $project, Database $dbForProject, Database $dbForPlatform, callable $logError, Realtime $queueForRealtime, Device $deviceForMigrations): void - { + public function action( + Message $message, + Document $project, + Database $dbForProject, + Database $dbForPlatform, + callable $logError, + Realtime $queueForRealtime, + Device $deviceForMigrations, + Device $deviceForFiles, + Mail $queueForMails, + ): void { $payload = $message->getPayload() ?? []; $this->deviceForMigrations = $deviceForMigrations; + $this->deviceForFiles = $deviceForFiles; if (empty($payload)) { throw new Exception('Missing payload'); @@ -105,7 +123,7 @@ class Migrations extends Action return; } - $this->processMigration($migration, $queueForRealtime); + $this->processMigration($migration, $queueForRealtime, $queueForMails); } /** @@ -175,6 +193,7 @@ class Migrations extends Action protected function processDestination(Document $migration, string $apiKey): Destination { $destination = $migration->getAttribute('destination'); + $options = $migration->getAttribute('options', []); return match ($destination) { DestinationAppwrite::getName() => new DestinationAppwrite( @@ -185,14 +204,32 @@ class Migrations extends Action Config::getParam('collections', [])['databases']['collections'], ), DestinationCSV::getName() => new DestinationCSV( - $this->deviceForMigrations, + $this->deviceForFiles, $migration->getAttribute('resourceId'), - $migration->getAttribute('options', [])['columns'] ?? [], + $options['bucketId'], + $options['filename'], + $options['columns'], + $options['delimiter'], + $options['enclosure'], + $options['escape'], + $options['header'], ), default => throw new \Exception('Invalid destination type'), }; } + /** + * Sanitize a filename to make it filesystem-safe + */ + protected function sanitizeFilename(string $filename): string + { + // Replace problematic characters with underscores + $sanitized = \preg_replace('/[:\/<>"|*?]/', '_', $filename); + $sanitized = \preg_replace('/[^\x20-\x7E]/', '_', $sanitized); + $sanitized = \trim($sanitized); + return empty($sanitized) ? 'export' : $sanitized; + } + /** * @throws Authorization * @throws Structure @@ -202,24 +239,18 @@ class Migrations extends Action */ protected function updateMigrationDocument(Document $migration, Document $project, Realtime $queueForRealtime): Document { - $errorMessages = []; - $clonedMigrationDocument = clone $migration; - - // we cannot use #sensitive because - // `errors` is nested which requires an override. - $errors = $clonedMigrationDocument->getAttribute('errors', []); + $messages = []; + $errors = $migration->getAttribute('errors', []); foreach ($errors as $error) { - $decoded = json_decode($error, true); - - if (is_array($decoded) && isset($decoded['trace'])) { + $decoded = \json_decode($error, true); + if (\is_array($decoded) && isset($decoded['trace'])) { unset($decoded['trace']); - $errorMessages[] = json_encode($decoded); + $messages[] = json_encode($decoded); } } - // set the errors back without trace - $clonedMigrationDocument->setAttribute('errors', $errorMessages); + $migration->setAttribute('errors', $messages); /** Trigger Realtime Events */ $queueForRealtime @@ -227,10 +258,14 @@ class Migrations extends Action ->setSubscribers(['console', $project->getId()]) ->setEvent('migrations.[migrationId].update') ->setParam('migrationId', $migration->getId()) - ->setPayload($clonedMigrationDocument->getArrayCopy(), ['options', 'credentials']) + ->setPayload($migration->getArrayCopy(), sensitive: ['options', 'credentials']) ->trigger(); - return $this->dbForProject->updateDocument('migrations', $migration->getId(), $migration); + return $this->dbForProject->updateDocument( + 'migrations', + $migration->getId(), + $migration + ); } /** @@ -285,11 +320,13 @@ class Migrations extends Action * @throws \Utopia\Database\Exception * @throws Exception */ - protected function processMigration(Document $migration, Realtime $queueForRealtime): void - { - $project = $this->project; - $projectDocument = $this->dbForPlatform->getDocument('projects', $project->getId()); - $tempAPIKey = $this->generateAPIKey($projectDocument); + protected function processMigration( + Document $migration, + Realtime $queueForRealtime, + Mail $queueForMails, + ): void { + $project = $this->dbForPlatform->getDocument('projects', $this->project->getId()); + $tempAPIKey = $this->generateAPIKey($project); $transfer = $source = $destination = null; @@ -299,17 +336,15 @@ class Migrations extends Action empty($migration->getAttribute('credentials', [])) ) { $credentials = $migration->getAttribute('credentials', []); - - $credentials['projectId'] = $credentials['projectId'] ?? $projectDocument->getId(); + $credentials['projectId'] = $credentials['projectId'] ?? $project->getId(); $credentials['endpoint'] = $credentials['endpoint'] ?? 'http://appwrite/v1'; $credentials['apiKey'] = $credentials['apiKey'] ?? $tempAPIKey; - $migration->setAttribute('credentials', $credentials); } $migration->setAttribute('stage', 'processing'); $migration->setAttribute('status', 'processing'); - $this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime); + $this->updateMigrationDocument($migration, $project, $queueForRealtime); $source = $this->processSource($migration); $destination = $this->processDestination($migration, $tempAPIKey); @@ -322,40 +357,44 @@ class Migrations extends Action /** Start Transfer */ if (empty($source->getErrors())) { $migration->setAttribute('stage', 'migrating'); - $this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime); + $this->updateMigrationDocument($migration, $project, $queueForRealtime); $transfer->run( $migration->getAttribute('resources'), - function () use ($migration, $transfer, $projectDocument, $queueForRealtime) { + function () use ($migration, $transfer, $project, $queueForRealtime) { $migration->setAttribute('resourceData', json_encode($transfer->getCache())); $migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); - $this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime); + $this->updateMigrationDocument($migration, $project, $queueForRealtime); }, $migration->getAttribute('resourceId'), $migration->getAttribute('resourceType') ); } - $destination->shutDown(); - $source->shutDown(); + // Debug logging for CSV exports before shutdown + if ($migration->getAttribute('destination') === DestinationCSV::getName()) { + $statusCounters = $transfer->getStatusCounters(); + Console::info('CSV export transfer completed. Status counters: ' . json_encode($statusCounters)); + Console::info('CSV export options: ' . json_encode($migration->getAttribute('options'))); + Console::info('CSV export errors: ' . json_encode($destination->getErrors())); + } + + $destination->shutdown(); + $source->shutdown(); $sourceErrors = $source->getErrors(); $destinationErrors = $destination->getErrors(); - if (! empty($sourceErrors) || ! empty($destinationErrors)) { + if (!empty($sourceErrors) || ! empty($destinationErrors)) { $migration->setAttribute('status', 'failed'); $migration->setAttribute('stage', 'finished'); - $errorMessages = []; - foreach ($sourceErrors as $error) { - $errorMessages[] = json_encode($error); - } - foreach ($destinationErrors as $error) { - $errorMessages[] = json_encode($error); + $errors = []; + foreach ([...$sourceErrors, ...$destinationErrors] as $error) { + $errors[] = \json_encode($error); } - $migration->setAttribute('errors', $errorMessages); - + $migration->setAttribute('errors', $errors); return; } @@ -382,57 +421,100 @@ class Migrations extends Action $sourceErrors = $source->getErrors(); $destinationErrors = $destination->getErrors(); - $errorMessages = []; - foreach ($sourceErrors as $error) { - $errorMessages[] = json_encode($error); - } - foreach ($destinationErrors as $error) { - $errorMessages[] = json_encode($error); + $errors = []; + foreach ([...$sourceErrors, ...$destinationErrors] as $error) { + $errors[] = \json_encode($error); } - $migration->setAttribute('errors', $errorMessages); + $migration->setAttribute('errors', $errors); } } finally { - $this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime); + $this->updateMigrationDocument($migration, $project, $queueForRealtime); if ($migration->getAttribute('status', '') === 'failed') { Console::error('Migration('.$migration->getSequence().':'.$migration->getId().') failed, Project('.$this->project->getSequence().':'.$this->project->getId().')'); - if ($destination) { - $destination->error(); + $sourceErrors = $source?->getErrors() ?? []; + $destinationErrors = $destination?->getErrors() ?? []; - foreach ($destination->getErrors() as $error) { - /** @var MigrationException $error */ - call_user_func($this->logError, $error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [ + foreach ([...$sourceErrors, ...$destinationErrors] as $error) { + /** @var MigrationException $error */ + if ($error->getCode() === 0 || $error->getCode() >= 500) { + ($this->logError)($error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [ 'migrationId' => $migration->getId(), 'source' => $migration->getAttribute('source') ?? '', 'destination' => $migration->getAttribute('destination') ?? '', 'resourceName' => $error->getResourceName(), - 'resourceGroup' => $error->getResourceGroup() + 'resourceGroup' => $error->getResourceGroup(), ]); } } - if ($source) { - $source->error(); - - foreach ($source->getErrors() as $error) { - /** @var MigrationException $error */ - call_user_func($this->logError, $error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [ - 'migrationId' => $migration->getId(), - 'source' => $migration->getAttribute('source') ?? '', - 'destination' => $migration->getAttribute('destination') ?? '', - 'resourceName' => $error->getResourceName(), - 'resourceGroup' => $error->getResourceGroup() - ]); - } - } + $source?->error(); + $destination?->error(); } if ($migration->getAttribute('status', '') === 'completed') { $destination?->success(); $source?->success(); + + if ($migration->getAttribute('destination') === DestinationCSV::getName()) { + $this->handleCSVExportComplete($project, $migration, $queueForMails); + } } } } + + protected function handleCSVExportComplete(Document $project, Document $migration, Mail $queueForMails): void + { + $options = $migration->getAttribute('options', []); + $bucketId = $options['bucketId'] ?? null; + $filename = $options['filename'] ?? 'export.csv'; + $userInternalId = $options['userInternalId'] ?? ''; + $resourceId = $migration->getAttribute('resourceId'); + + // Save file to bucket + $bucket = $this->dbForProject->getDocument('buckets', $bucketId); + if ($bucket->isEmpty()) { + throw new \Exception("Bucket not found: $bucketId"); + } + + $path = $this->deviceForFiles->getPath($bucketId . '/' . $this->sanitizeFilename($filename) . '.csv'); + $size = $this->deviceForFiles->getFileSize($path); + $mime = $this->deviceForFiles->getFileMimeType($path); + $hash = $this->deviceForFiles->getFileHash($path); + $algorithm = Compression::NONE; + $fileId = \md5($resourceId); + + $this->dbForProject->createDocument('bucket_' . $bucket->getSequence(), new Document([ + '$id' => $fileId, + '$permissions' => [], + 'bucketId' => $bucket->getId(), + 'bucketInternalId' => $bucket->getSequence(), + 'name' => $filename, + 'path' => $path, + 'signature' => $hash, + 'mimeType' => $mime, + 'sizeOriginal' => $size, + 'sizeActual' => $size, + 'algorithm' => $algorithm, + 'comment' => '', + 'chunksTotal' => 1, + 'chunksUploaded' => 1, + 'openSSLVersion' => null, + 'openSSLCipher' => null, + 'openSSLTag' => null, + 'openSSLIV' => null, + 'search' => \implode(' ', [$fileId, $filename]), + 'metadata' => ['content_type' => $mime] + ])); + + Console::info("Created file document in bucket: $fileId"); + + // No notification required, skip email sending + if (!($options['notify'] ?? false)) { + return; + } + + } } From 4970aa7426925e3c4d0567e5d8d3bc4c37a9ce33 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 24 Sep 2025 22:38:12 +1200 Subject: [PATCH 022/333] Send email if notify on complete is set --- app/config/locale/translations/en.json | 8 +++ src/Appwrite/Platform/Workers/Migrations.php | 66 ++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/app/config/locale/translations/en.json b/app/config/locale/translations/en.json index e2ee20b2d7..c194819744 100644 --- a/app/config/locale/translations/en.json +++ b/app/config/locale/translations/en.json @@ -54,6 +54,14 @@ "emails.recovery.thanks": "Thanks,", "emails.recovery.buttonText": "Reset password", "emails.recovery.signature": "{{project}} team", + "emails.csvExport.subject": "Your CSV export is ready", + "emails.csvExport.preview": "Your data export has been completed successfully.", + "emails.csvExport.hello": "Hello {{user}},", + "emails.csvExport.body": "Your CSV export is ready for download. Click the link below to download your data export.", + "emails.csvExport.footer": "This download link will expire in 1 hour.", + "emails.csvExport.thanks": "Thanks,", + "emails.csvExport.buttonText": "Download CSV", + "emails.csvExport.signature": "{{project}} team", "emails.invitation.subject": "Invitation to {{team}} Team at {{project}}", "emails.invitation.preview": "{{owner}} invited you to join {{team}} at {{project}}", "emails.invitation.hello": "Hello {{user}},", diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index bbdf537157..ab8c7091cb 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -516,5 +516,71 @@ class Migrations extends Action return; } + $user = $this->dbForPlatform->findOne('users', [ + Query::equal('$sequence', [$userInternalId]) + ]); + + // Set up locale + $locale = new Locale(System::getEnv('_APP_LOCALE', 'en')); + $locale->setFallback(System::getEnv('_APP_LOCALE', 'en')); + + // Generate JWT + $expiry = (new \DateTime())->add(new \DateInterval('PT1H'))->format('U'); + $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', \intval($expiry), 0); + $jwt = $encoder->encode([ + 'bucketId' => $bucketId, + 'fileId' => $fileId, + 'projectId' => $project->getId(), + ]); + + // Generate download URL with JWT + $endpoint = System::getEnv('_APP_DOMAIN', ''); + $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled' ? 'https' : 'http'; + $downloadUrl = "{$protocol}://{$endpoint}/v1/storage/buckets/{$bucketId}/files/{$fileId}/push?project={$project->getId()}&jwt={$jwt}"; + + // Get localized email content + $subject = $locale->getText('emails.csvExport.subject'); + $preview = $locale->getText('emails.csvExport.preview'); + $hello = $locale->getText('emails.csvExport.hello'); + $body = $locale->getText('emails.csvExport.body'); + $footer = $locale->getText('emails.csvExport.footer'); + $thanks = $locale->getText('emails.csvExport.thanks'); + $buttonText = $locale->getText('emails.csvExport.buttonText'); + $signature = $locale->getText('emails.csvExport.signature'); + + // Build email body using inner template + $message = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-inner-base.tpl'); + $message + ->setParam('{{body}}', $body, escapeHtml: false) + ->setParam('{{hello}}', $hello) + ->setParam('{{footer}}', $footer) + ->setParam('{{thanks}}', $thanks) + ->setParam('{{buttonText}}', $buttonText) + ->setParam('{{signature}}', $signature) + ->setParam('{{direction}}', $locale->getText('settings.direction')) + ->setParam('{{project}}', $project->getAttribute('name')) + ->setParam('{{user}}', $user->getAttribute('name', $user->getAttribute('email'))) + ->setParam('{{redirect}}', $downloadUrl); + + $emailBody = $message->render(); + + $emailVariables = [ + 'direction' => $locale->getText('settings.direction'), + 'project' => $project->getAttribute('name'), + 'user' => $user->getAttribute('name', $user->getAttribute('email')), + 'redirect' => $downloadUrl, + ]; + + $queueForMails + ->setSubject($subject) + ->setPreview($preview) + ->setBody($emailBody) + ->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl') + ->setVariables($emailVariables) + ->setName($user->getAttribute('name', $user->getAttribute('email'))) + ->setRecipient($user->getAttribute('email')) + ->trigger(); + + Console::info('CSV export notification email sent to ' . $user->getAttribute('email')); } } From 61d9db8c67500fadda6bf65805ae47168a5e265e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 24 Sep 2025 22:38:35 +1200 Subject: [PATCH 023/333] Cover export + notify in tests --- .../Services/Migrations/MigrationsBase.php | 231 +++++++++++++++++- 1 file changed, 230 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index 7a57b7f8f9..4d27cf2828 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -900,7 +900,7 @@ trait MigrationsBase /** * Import documents from a CSV file. */ - public function testCreateCsvMigration(): void + public function testCreateCSVImport(): void { // Make a database $response = $this->client->call(Client::METHOD_POST, '/databases', [ @@ -1194,4 +1194,233 @@ trait MigrationsBase 'x-appwrite-project' => $this->getProject()['$id'], ], $body); } + + /** + * Test CSV export with email notification + */ + public function testCreateCSVExport(): void + { + // Create a database + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Test Export Database' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create a collection + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'collectionId' => ID::unique(), + 'name' => 'Test Export Collection', + 'permissions' => [] + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Create a simple attribute like the basic test + $name = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'name', + 'size' => 255, + 'required' => true, + ]); + + $this->assertEquals(202, $name['headers']['status-code']); + + // Create a simple attribute like the basic test + $email = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'email', + 'size' => 255, + 'required' => false, + ]); + + $this->assertEquals(202, $email['headers']['status-code']); + + \sleep(3); + + // Create sample documents + for ($i = 1; $i <= 10; $i++) { + $doc = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'documentId' => ID::unique(), + 'data' => [ + 'name' => 'Test User ' . $i, + 'email' => 'user' . $i . '@appwrite.io' + ] + ]); + + $this->assertEquals(201, $doc['headers']['status-code'], 'Failed to create document ' . $i); + } + + // Verify documents were created + $docs = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]); + + $this->assertEquals(200, $docs['headers']['status-code']); + $this->assertEquals(10, $docs['body']['total'], 'Expected 10 documents but got ' . $docs['body']['total']); + + // Create a storage bucket for the export + $bucketIdUnique = ID::unique(); + $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'bucketId' => $bucketIdUnique, + 'name' => 'Test Export Bucket', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'fileSecurity' => false, + 'enabled' => true, + 'maximumFileSize' => 10485760, // 10MB + 'allowedFileExtensions' => ['csv'], + 'compression' => 'none', + 'encryption' => false, + 'antivirus' => false + ]); + + $this->assertEquals(201, $bucket['headers']['status-code']); + $bucketId = $bucket['body']['$id']; + + // Perform CSV export with notification enabled + $migration = $this->client->call(Client::METHOD_POST, '/migrations/csv/exports', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), [ + 'bucketId' => $bucketId, + 'resourceId' => $databaseId . ':' . $collectionId, + 'filename' => 'test-export', + 'columns' => [], + 'delimiter' => ',', + 'enclosure' => '"', + 'escape' => '\\', + 'header' => true, + 'notify' => true + ]); + + $this->assertEquals(202, $migration['headers']['status-code']); + $this->assertNotEmpty($migration['body']['$id']); + $migrationId = $migration['body']['$id']; + + $this->assertEventually(function () use ($migrationId) { + $response = $this->client->call(Client::METHOD_GET, '/migrations/' . $migrationId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('finished', $response['body']['stage']); + $this->assertEquals('completed', $response['body']['status']); + $this->assertEquals('Appwrite', $response['body']['source']); + $this->assertEquals('CSV', $response['body']['destination']); + + return true; + }, 30000, 500); + + // Check that the file was created in the bucket + // File ID is MD5 of the resourceId (not the filename) + $fileId = \md5($databaseId . ':' . $collectionId); + + $file = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]); + + $this->assertEquals(200, $file['headers']['status-code']); + $this->assertEquals($fileId, $file['body']['$id']); + $this->assertEquals($bucketId, $file['body']['bucketId']); + $this->assertEquals('test-export', $file['body']['name']); + $this->assertEquals('text/csv', $file['body']['mimeType']); + $this->assertGreaterThan(0, $file['body']['sizeOriginal']); + + // Download and verify CSV content + $download = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/download', \array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $download['headers']['status-code']); + + $csvContent = $download['body']; + $lines = explode("\n", trim($csvContent)); + $this->assertCount(11, $lines); + $this->assertStringContainsString('$id', $lines[0]); + $this->assertStringContainsString('$permissions', $lines[0]); + $this->assertStringContainsString('$createdAt', $lines[0]); + $this->assertStringContainsString('$updatedAt', $lines[0]); + $this->assertStringContainsString('name', $lines[0]); + $this->assertStringContainsString('email', $lines[0]); + + $this->assertStringContainsString('Test User 1', $lines[1]); + $this->assertStringContainsString('user1@appwrite.io', $lines[1]); + + // Check that email was sent with download link + $lastEmail = $this->getLastEmail(); + $this->assertNotEmpty($lastEmail); + $this->assertEquals('Your CSV export is ready', $lastEmail['subject']); + $this->assertStringContainsStringIgnoringCase('Your data export has been completed successfully', $lastEmail['text']); + + // Extract download URL from email HTML + \preg_match('/href="([^"]*\/storage\/buckets\/[^"]*\/push[^"]*)"/', $lastEmail['html'], $matches); + $this->assertNotEmpty($matches[1], 'Download URL not found in email'); + $downloadUrl = html_entity_decode($matches[1]); + + // Parse the URL to extract components + $components = \parse_url($downloadUrl); + $this->assertNotEmpty($components); + \parse_str($components['query'] ?? '', $queryParams); + $this->assertArrayHasKey('jwt', $queryParams, 'JWT not found in download URL'); + $this->assertNotEmpty($queryParams['jwt']); + + // Test download with JWT + $path = \str_replace('/v1', '', $components['path']); + $downloadWithJwt = $this->client->call(Client::METHOD_GET, $path . '?project=' . $queryParams['project'] . '&jwt=' . $queryParams['jwt']); + $this->assertEquals(200, $downloadWithJwt['headers']['status-code'], 'Failed to download file with JWT'); + $this->assertEquals($csvContent, $downloadWithJwt['body'], 'Downloaded content differs from original'); + + // Test that download without JWT fails + $downloadWithoutJwt = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/download'); + $this->assertEquals(404, $downloadWithoutJwt['headers']['status-code'], 'File should not be downloadable without JWT'); + + $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId . '/files/' . $fileId, [ + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]); + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [ + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]); + $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId, [ + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]); + } } From 223523f722f4a5b34ccc93e475d364334d1dd5ae Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 24 Sep 2025 22:38:55 +1200 Subject: [PATCH 024/333] Mount uploads to migrations worker --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index da6362b4c4..0c187dd762 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -698,6 +698,7 @@ services: - appwrite volumes: - appwrite-imports:/storage/imports:rw + - appwrite-uploads:/storage/uploads:rw - ./app:/usr/src/code/app - ./src:/usr/src/code/src - ./tests:/usr/src/code/tests From 2494c9afb376e89311fd11e4904465b7c40f228c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 24 Sep 2025 22:39:06 +1200 Subject: [PATCH 025/333] Update to release migrations --- composer.json | 2 +- composer.lock | 133 ++++++++++++++++++++++++++------------------------ 2 files changed, 69 insertions(+), 66 deletions(-) diff --git a/composer.json b/composer.json index 8a1e325f7f..6f89312223 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,7 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.18.*", - "utopia-php/migration": "dev-feat-csv-export as 1.0.0", + "utopia-php/migration": "1.2.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.8.*", diff --git a/composer.lock b/composer.lock index 73fb57f6f8..0fc58749f5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "954529a36566209d6687df9f41a0f2e6", + "content-hash": "5150254cf0d6aa361a31244b7f7d1eb7", "packages": [ { "name": "adhocore/jwt", @@ -1159,20 +1159,20 @@ }, { "name": "open-telemetry/api", - "version": "1.5.0", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "7692075f486c14d8cfd37fba98a08a5667f089e5" + "reference": "ee17d937652eca06c2341b6fadc0f74c1c1a5af2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/7692075f486c14d8cfd37fba98a08a5667f089e5", - "reference": "7692075f486c14d8cfd37fba98a08a5667f089e5", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/ee17d937652eca06c2341b6fadc0f74c1c1a5af2", + "reference": "ee17d937652eca06c2341b6fadc0f74c1c1a5af2", "shasum": "" }, "require": { - "open-telemetry/context": "^1.0", + "open-telemetry/context": "^1.4", "php": "^8.1", "psr/log": "^1.1|^2.0|^3.0", "symfony/polyfill-php82": "^1.26" @@ -1225,20 +1225,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-08-07T23:07:38+00:00" + "time": "2025-09-19T00:05:49+00:00" }, { "name": "open-telemetry/context", - "version": "1.3.1", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/context.git", - "reference": "438f71812242db3f196fb4c717c6f92cbc819be6" + "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/438f71812242db3f196fb4c717c6f92cbc819be6", - "reference": "438f71812242db3f196fb4c717c6f92cbc819be6", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/d4c4470b541ce72000d18c339cfee633e4c8e0cf", + "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf", "shasum": "" }, "require": { @@ -1284,7 +1284,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-08-13T01:12:00+00:00" + "time": "2025-09-19T00:05:49+00:00" }, { "name": "open-telemetry/exporter-otlp", @@ -1415,23 +1415,23 @@ }, { "name": "open-telemetry/sdk", - "version": "1.7.1", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "52690d4b37ae4f091af773eef3c238ed2bc0aa06" + "reference": "105c6e81e3d86150bd5704b00c7e4e165e957b89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/52690d4b37ae4f091af773eef3c238ed2bc0aa06", - "reference": "52690d4b37ae4f091af773eef3c238ed2bc0aa06", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/105c6e81e3d86150bd5704b00c7e4e165e957b89", + "reference": "105c6e81e3d86150bd5704b00c7e4e165e957b89", "shasum": "" }, "require": { "ext-json": "*", "nyholm/psr7-server": "^1.1", - "open-telemetry/api": "^1.4", - "open-telemetry/context": "^1.0", + "open-telemetry/api": "^1.6", + "open-telemetry/context": "^1.4", "open-telemetry/sem-conv": "^1.0", "php": "^8.1", "php-http/discovery": "^1.14", @@ -1508,7 +1508,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-09-05T07:17:06+00:00" + "time": "2025-09-19T00:05:49+00:00" }, { "name": "open-telemetry/sem-conv", @@ -1569,16 +1569,16 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v2.7.0", + "version": "v2.8.1", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105" + "reference": "e6352b9f43318821f148c1e8c2d9e944aa9accb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105", - "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/e6352b9f43318821f148c1e8c2d9e944aa9accb5", + "reference": "e6352b9f43318821f148c1e8c2d9e944aa9accb5", "shasum": "" }, "require": { @@ -1632,7 +1632,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2024-05-08T12:18:48+00:00" + "time": "2025-09-24T01:40:13+00:00" }, { "name": "paragonie/random_compat", @@ -4187,16 +4187,16 @@ }, { "name": "utopia-php/migration", - "version": "dev-feat-csv-export", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "8435f1db0db4854ca27cb4c9cf275b905fcb3b41" + "reference": "42ff497c5231f5a727d1e229419ff1d2195d8093" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/8435f1db0db4854ca27cb4c9cf275b905fcb3b41", - "reference": "8435f1db0db4854ca27cb4c9cf275b905fcb3b41", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/42ff497c5231f5a727d1e229419ff1d2195d8093", + "reference": "42ff497c5231f5a727d1e229419ff1d2195d8093", "shasum": "" }, "require": { @@ -4237,9 +4237,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/feat-csv-export" + "source": "https://github.com/utopia-php/migration/tree/1.2.0" }, - "time": "2025-08-21T12:56:18+00:00" + "time": "2025-09-24T10:32:24+00:00" }, { "name": "utopia-php/orchestration", @@ -5004,16 +5004,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.3.5", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "6fda9e58b37c9872c1a2a424e5467de8de1bc567" + "reference": "3583fa6fddb1d1a902b37ff2048527a5827fc008" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6fda9e58b37c9872c1a2a424e5467de8de1bc567", - "reference": "6fda9e58b37c9872c1a2a424e5467de8de1bc567", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/3583fa6fddb1d1a902b37ff2048527a5827fc008", + "reference": "3583fa6fddb1d1a902b37ff2048527a5827fc008", "shasum": "" }, "require": { @@ -5049,9 +5049,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.3.5" + "source": "https://github.com/appwrite/sdk-generator/tree/1.4.0" }, - "time": "2025-09-15T04:19:40+00:00" + "time": "2025-09-23T02:27:10+00:00" }, { "name": "doctrine/annotations", @@ -5278,16 +5278,16 @@ }, { "name": "laravel/pint", - "version": "v1.25.0", + "version": "v1.25.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96" + "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96", - "reference": "595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96", + "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", + "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", "shasum": "" }, "require": { @@ -5340,7 +5340,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-09-17T01:36:44+00:00" + "time": "2025-09-19T02:57:12+00:00" }, { "name": "matthiasmullie/minify", @@ -6230,16 +6230,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.27", + "version": "9.6.29", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0a9aa4440b6a9528cf360071502628d717af3e0a" + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0a9aa4440b6a9528cf360071502628d717af3e0a", - "reference": "0a9aa4440b6a9528cf360071502628d717af3e0a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", "shasum": "" }, "require": { @@ -6264,7 +6264,7 @@ "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", + "sebastian/exporter": "^4.0.8", "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", @@ -6313,7 +6313,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.27" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" }, "funding": [ { @@ -6337,7 +6337,7 @@ "type": "tidelift" } ], - "time": "2025-09-14T06:18:03+00:00" + "time": "2025-09-24T06:29:11+00:00" }, { "name": "psr/cache", @@ -6829,16 +6829,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -6894,15 +6894,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", @@ -8504,18 +8516,9 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [ - { - "package": "utopia-php/migration", - "version": "dev-feat-csv-export", - "alias": "1.0.0", - "alias_normalized": "1.0.0.0" - } - ], + "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/migration": 20 - }, + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { From 16821a28fcad97655b94c48b02caa6970adc0ab9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 24 Sep 2025 22:53:13 +1200 Subject: [PATCH 026/333] Format --- app/controllers/api/migrations.php | 1 - src/Appwrite/Platform/Workers/Migrations.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 6e0ecd218d..c69185d0e8 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -1,6 +1,5 @@ Date: Tue, 30 Sep 2025 08:52:21 +0000 Subject: [PATCH 027/333] fix: sites deployment activation instructions in createDeployment --- src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index 65a0fcf143..4bd3afa1f5 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -57,7 +57,7 @@ class Create extends Action group: 'deployments', name: 'createDeployment', description: << Date: Wed, 1 Oct 2025 08:44:14 +0000 Subject: [PATCH 028/333] add specs files --- app/config/specs/open-api3-1.8.x-console.json | 4 ++-- app/config/specs/open-api3-1.8.x-server.json | 4 ++-- app/config/specs/open-api3-latest-console.json | 4 ++-- app/config/specs/open-api3-latest-server.json | 4 ++-- app/config/specs/swagger2-1.8.x-console.json | 4 ++-- app/config/specs/swagger2-1.8.x-server.json | 4 ++-- app/config/specs/swagger2-latest-console.json | 4 ++-- app/config/specs/swagger2-latest-server.json | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/config/specs/open-api3-1.8.x-console.json b/app/config/specs/open-api3-1.8.x-console.json index bdff664cbc..a2266067af 100644 --- a/app/config/specs/open-api3-1.8.x-console.json +++ b/app/config/specs/open-api3-1.8.x-console.json @@ -29951,7 +29951,7 @@ "tags": [ "sites" ], - "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "responses": { "202": { "description": "Deployment", @@ -29972,7 +29972,7 @@ "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", diff --git a/app/config/specs/open-api3-1.8.x-server.json b/app/config/specs/open-api3-1.8.x-server.json index 6b766dbdee..efff48e940 100644 --- a/app/config/specs/open-api3-1.8.x-server.json +++ b/app/config/specs/open-api3-1.8.x-server.json @@ -20655,7 +20655,7 @@ "tags": [ "sites" ], - "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "responses": { "202": { "description": "Deployment", @@ -20676,7 +20676,7 @@ "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index bdff664cbc..a2266067af 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -29951,7 +29951,7 @@ "tags": [ "sites" ], - "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "responses": { "202": { "description": "Deployment", @@ -29972,7 +29972,7 @@ "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 6b766dbdee..efff48e940 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -20655,7 +20655,7 @@ "tags": [ "sites" ], - "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "responses": { "202": { "description": "Deployment", @@ -20676,7 +20676,7 @@ "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", diff --git a/app/config/specs/swagger2-1.8.x-console.json b/app/config/specs/swagger2-1.8.x-console.json index ee3702d27d..47cde0e7a1 100644 --- a/app/config/specs/swagger2-1.8.x-console.json +++ b/app/config/specs/swagger2-1.8.x-console.json @@ -30135,7 +30135,7 @@ "tags": [ "sites" ], - "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "responses": { "202": { "description": "Deployment", @@ -30152,7 +30152,7 @@ "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", diff --git a/app/config/specs/swagger2-1.8.x-server.json b/app/config/specs/swagger2-1.8.x-server.json index ff5056b35a..48d8708216 100644 --- a/app/config/specs/swagger2-1.8.x-server.json +++ b/app/config/specs/swagger2-1.8.x-server.json @@ -20882,7 +20882,7 @@ "tags": [ "sites" ], - "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "responses": { "202": { "description": "Deployment", @@ -20899,7 +20899,7 @@ "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index ee3702d27d..47cde0e7a1 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -30135,7 +30135,7 @@ "tags": [ "sites" ], - "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "responses": { "202": { "description": "Deployment", @@ -30152,7 +30152,7 @@ "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index ff5056b35a..48d8708216 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -20882,7 +20882,7 @@ "tags": [ "sites" ], - "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "description": "Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "responses": { "202": { "description": "Deployment", @@ -20899,7 +20899,7 @@ "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.", "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", From 92ccb6e4ea9d11c70055b87ba2a8231ea174f57b Mon Sep 17 00:00:00 2001 From: Harsh Mahajan Date: Thu, 2 Oct 2025 17:43:30 +0000 Subject: [PATCH 029/333] feat(vcs): expand GitHub comment tips --- docker-compose.yml | 2 +- src/Appwrite/Vcs/Comment.php | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 58b78fcd8e..afc32a518b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1033,7 +1033,7 @@ services: volumes: - appwrite-mariadb:/var/lib/mysql:rw ports: - - "3306:3306" + - "3307:3306" environment: - MYSQL_ROOT_PASSWORD=${_APP_DB_ROOT_PASS} - MYSQL_DATABASE=${_APP_DB_SCHEMA} diff --git a/src/Appwrite/Vcs/Comment.php b/src/Appwrite/Vcs/Comment.php index a66706a4a2..f81abca858 100644 --- a/src/Appwrite/Vcs/Comment.php +++ b/src/Appwrite/Vcs/Comment.php @@ -11,10 +11,37 @@ class Comment { // TODO: Add more tips protected array $tips = [ - 'Appwrite has a Discord community with over 16 000 members.', - 'You can use Avatars API to generate QR code for any text or URLs.', - 'Cursor pagination performs better than offset pagination when loading further pages.', - ]; + 'Appwrite has crossed the 50K GitHub stars milestone with hundreds of active contributors', + 'Discord community has grown to 24K passionate developers and counting', + 'Sites auto-generate unique domains with the pattern https://randomstring.appwrite.network', + 'Every Git commit and branch gets its own deployment URL automatically', + 'Custom domains work with both CNAME for subdomains and NS records for apex domains', + 'HTTPS and SSL certificates are handled automagically for all your Sites', + 'Functions can run for up to 15 minutes before timing out', + 'Schedule functions to run as often as every minute with cron expressions', + 'Environment variables can be scoped per function or shared across your project', + 'Function scopes give you fine-grained control over API permissions', + 'Sites support three domain rule types: Active deployment, Git branch, and Redirect', + 'Preview deployments create instant URLs for every branch and commit', + 'Trigger functions via HTTP, SDKs, events, webhooks, or scheduled cron jobs', + 'Each function runs in its own isolated container with custom environment variables', + 'Build commands execute in runtime containers during deployment', + 'Dynamic API keys are generated automatically for each function execution', + 'JWT tokens let functions act on behalf of users while preserving their permissions', + 'Storage files get ClamAV malware scanning and encryption by default', + 'Roll back Sites deployments instantly by switching between versions', + 'Git integration provides automatic deployments with optional PR comments', + 'Silent mode disables those chatty PR comments if you prefer peace and quiet', + 'Environment variable changes require redeployment to take effect', + 'SSR frameworks are fully supported with configurable build runtimes', + 'Global CDN and DDoS protection come free with every Sites deployment', + 'Deploy functions via zip upload or connect directly to your Git repo', + 'Realtime gives you live updates for users, storage, functions, and databases', + 'GraphQL API works alongside REST and WebSocket protocols', + 'Messaging handles push notifications, emails, and SMS through one unified API', + 'Teams feature lets you group users with membership management and role permissions', + 'MCP server integration brings LLM superpowers to Claude Desktop and Cursor IDE', + ]; protected string $statePrefix = '[appwrite]: #'; From 61a8f589ac11a82faac766bf4bbbe34c274e84f8 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan Date: Thu, 2 Oct 2025 17:44:21 +0000 Subject: [PATCH 030/333] port issue --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index afc32a518b..58b78fcd8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1033,7 +1033,7 @@ services: volumes: - appwrite-mariadb:/var/lib/mysql:rw ports: - - "3307:3306" + - "3306:3306" environment: - MYSQL_ROOT_PASSWORD=${_APP_DB_ROOT_PASS} - MYSQL_DATABASE=${_APP_DB_SCHEMA} From 64c3b7a2ffe4a7907f90eda92accfed2432dc2d2 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Thu, 2 Oct 2025 23:19:34 +0530 Subject: [PATCH 031/333] Update src/Appwrite/Vcs/Comment.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/Appwrite/Vcs/Comment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Vcs/Comment.php b/src/Appwrite/Vcs/Comment.php index f81abca858..325f7d30d1 100644 --- a/src/Appwrite/Vcs/Comment.php +++ b/src/Appwrite/Vcs/Comment.php @@ -41,7 +41,7 @@ class Comment 'Messaging handles push notifications, emails, and SMS through one unified API', 'Teams feature lets you group users with membership management and role permissions', 'MCP server integration brings LLM superpowers to Claude Desktop and Cursor IDE', - ]; + ]; protected string $statePrefix = '[appwrite]: #'; From c525e15ea55a9792fb53bed2a934312868c7adf6 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Fri, 10 Oct 2025 23:01:26 +0530 Subject: [PATCH 032/333] Dark mode styles for emails --- .../locale/templates/email-base-styled.tpl | 33 ++++++++++++++++++- app/config/locale/templates/email-base.tpl | 32 ++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/app/config/locale/templates/email-base-styled.tpl b/app/config/locale/templates/email-base-styled.tpl index 37ca630d43..c47d276ea8 100644 --- a/app/config/locale/templates/email-base-styled.tpl +++ b/app/config/locale/templates/email-base-styled.tpl @@ -2,6 +2,38 @@ + + +