appwrite/app/controllers/api/migrations.php

987 lines
40 KiB
PHP
Raw Normal View History

2023-08-04 16:21:41 +00:00
<?php
use Appwrite\Event\Event;
use Appwrite\Event\Migration;
use Appwrite\Extend\Exception;
2025-04-21 06:34:36 +00:00
use Appwrite\OpenSSL\OpenSSL;
2025-01-17 04:31:39 +00:00
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
2025-04-12 11:40:06 +00:00
use Appwrite\Utopia\Database\Validator\CompoundUID;
2023-08-04 16:21:41 +00:00
use Appwrite\Utopia\Database\Validator\Queries\Migrations;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
2024-02-12 16:02:04 +00:00
use Utopia\Database\Exception\Query as QueryException;
2023-08-04 16:21:41 +00:00
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
2025-10-28 09:29:14 +00:00
use Utopia\Database\Validator\Queries\Documents;
2025-10-28 13:01:08 +00:00
use Utopia\Database\Validator\Query\Cursor;
2023-08-04 16:21:41 +00:00
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
2025-04-08 05:49:39 +00:00
use Utopia\Migration\Resource;
2023-08-09 17:08:10 +00:00
use Utopia\Migration\Sources\Appwrite;
2025-04-16 06:25:04 +00:00
use Utopia\Migration\Sources\CSV;
2023-08-09 17:08:10 +00:00
use Utopia\Migration\Sources\Firebase;
use Utopia\Migration\Sources\NHost;
use Utopia\Migration\Sources\Supabase;
2025-04-09 11:05:27 +00:00
use Utopia\Migration\Transfer;
2025-04-21 04:31:13 +00:00
use Utopia\Storage\Compression\Algorithms\GZIP;
use Utopia\Storage\Compression\Algorithms\Zstd;
2025-04-09 11:05:27 +00:00
use Utopia\Storage\Compression\Compression;
2025-04-08 05:49:39 +00:00
use Utopia\Storage\Device;
2025-04-21 06:34:36 +00:00
use Utopia\System\System;
2024-10-08 07:54:40 +00:00
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
2024-10-08 07:54:40 +00:00
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
2023-08-04 16:21:41 +00:00
include_once __DIR__ . '/../shared/api.php';
2026-02-04 05:30:22 +00:00
Http::post('/v1/migrations/appwrite')
2023-08-04 16:21:41 +00:00
->groups(['api', 'migrations'])
->desc('Create Appwrite migration')
2023-08-04 16:21:41 +00:00
->label('scope', 'migrations.write')
2023-08-16 15:01:56 +00:00
->label('event', 'migrations.[migrationId].create')
2023-08-04 16:21:41 +00:00
->label('audits.event', 'migration.create')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'migrations',
group: null,
2025-01-17 04:31:39 +00:00
name: 'createAppwriteMigration',
description: '/docs/references/migrations/migration-appwrite.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
2023-08-04 16:21:41 +00:00
->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate')
->param('endpoint', '', new URL(), 'Source Appwrite endpoint')
->param('projectId', '', new UID(), 'Source Project ID')
->param('apiKey', '', new Text(512), 'Source API Key')
2023-08-04 16:21:41 +00:00
->inject('response')
->inject('dbForProject')
->inject('project')
2025-12-17 12:11:14 +00:00
->inject('platform')
2023-08-04 16:21:41 +00:00
->inject('user')
2023-10-01 17:39:26 +00:00
->inject('queueForEvents')
->inject('queueForMigrations')
2025-12-17 12:11:14 +00:00
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
2023-08-04 16:21:41 +00:00
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Appwrite::getName(),
2024-05-27 15:56:28 +00:00
'destination' => Appwrite::getName(),
2023-08-04 16:21:41 +00:00
'credentials' => [
'endpoint' => $endpoint,
'projectId' => $projectId,
'apiKey' => $apiKey,
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
]));
2023-10-01 17:39:26 +00:00
$queueForEvents->setParam('migrationId', $migration->getId());
2023-08-04 16:21:41 +00:00
// Trigger Transfer
2023-10-01 17:39:26 +00:00
$queueForMigrations
2023-08-04 16:21:41 +00:00
->setMigration($migration)
->setProject($project)
2025-12-17 12:11:14 +00:00
->setPlatform($platform)
2023-08-04 16:21:41 +00:00
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
2026-02-04 05:30:22 +00:00
Http::post('/v1/migrations/firebase')
2023-08-09 22:46:23 +00:00
->groups(['api', 'migrations'])
->desc('Create Firebase migration')
2023-08-09 22:46:23 +00:00
->label('scope', 'migrations.write')
2023-08-16 15:01:56 +00:00
->label('event', 'migrations.[migrationId].create')
2023-08-09 22:46:23 +00:00
->label('audits.event', 'migration.create')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'migrations',
group: null,
2025-01-17 04:31:39 +00:00
name: 'createFirebaseMigration',
description: '/docs/references/migrations/migration-firebase.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
2023-08-09 22:46:23 +00:00
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
->param('serviceAccount', '', new Text(65536), 'JSON of the Firebase service account credentials')
->inject('response')
->inject('dbForProject')
->inject('project')
2025-12-17 12:11:14 +00:00
->inject('platform')
2023-08-09 22:46:23 +00:00
->inject('user')
2023-10-01 17:39:26 +00:00
->inject('queueForEvents')
->inject('queueForMigrations')
2025-12-17 12:11:14 +00:00
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
$serviceAccountData = json_decode($serviceAccount, true);
if (empty($serviceAccountData)) {
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
}
if (!isset($serviceAccountData['project_id']) || !isset($serviceAccountData['client_email']) || !isset($serviceAccountData['private_key'])) {
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
}
2023-10-13 15:23:20 +00:00
2023-08-09 22:46:23 +00:00
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Firebase::getName(),
2024-05-27 15:56:28 +00:00
'destination' => Appwrite::getName(),
2023-08-09 22:46:23 +00:00
'credentials' => [
'serviceAccount' => $serviceAccount,
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
]));
2023-10-01 17:39:26 +00:00
$queueForEvents->setParam('migrationId', $migration->getId());
2023-08-09 22:46:23 +00:00
// Trigger Transfer
2023-10-01 17:39:26 +00:00
$queueForMigrations
->setMigration($migration)
->setProject($project)
2025-12-17 12:11:14 +00:00
->setPlatform($platform)
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
2023-08-04 16:21:41 +00:00
});
2026-02-04 05:30:22 +00:00
Http::post('/v1/migrations/supabase')
2023-08-04 16:21:41 +00:00
->groups(['api', 'migrations'])
->desc('Create Supabase migration')
2023-08-04 16:21:41 +00:00
->label('scope', 'migrations.write')
2023-08-16 15:01:56 +00:00
->label('event', 'migrations.[migrationId].create')
2023-08-04 16:21:41 +00:00
->label('audits.event', 'migration.create')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'migrations',
group: null,
2025-01-17 04:31:39 +00:00
name: 'createSupabaseMigration',
description: '/docs/references/migrations/migration-supabase.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
2023-08-04 16:21:41 +00:00
->param('resources', [], new ArrayList(new WhiteList(Supabase::getSupportedResources(), true)), 'List of resources to migrate')
->param('endpoint', '', new URL(), 'Source\'s Supabase Endpoint')
->param('apiKey', '', new Text(512), 'Source\'s API Key')
->param('databaseHost', '', new Text(512), 'Source\'s Database Host')
->param('username', '', new Text(512), 'Source\'s Database Username')
->param('password', '', new Text(512), 'Source\'s Database Password')
->param('port', 5432, new Integer(true), 'Source\'s Database Port', true)
->inject('response')
->inject('dbForProject')
->inject('project')
2025-12-17 12:11:14 +00:00
->inject('platform')
2023-08-04 16:21:41 +00:00
->inject('user')
2023-10-01 17:39:26 +00:00
->inject('queueForEvents')
->inject('queueForMigrations')
2025-12-17 12:11:14 +00:00
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
2023-08-04 16:21:41 +00:00
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Supabase::getName(),
2024-05-27 15:56:28 +00:00
'destination' => Appwrite::getName(),
2023-08-04 16:21:41 +00:00
'credentials' => [
'endpoint' => $endpoint,
'apiKey' => $apiKey,
'databaseHost' => $databaseHost,
'username' => $username,
'password' => $password,
'port' => $port,
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
]));
2023-10-01 17:39:26 +00:00
$queueForEvents->setParam('migrationId', $migration->getId());
2023-08-04 16:21:41 +00:00
// Trigger Transfer
2023-10-01 17:39:26 +00:00
$queueForMigrations
2023-08-04 16:21:41 +00:00
->setMigration($migration)
->setProject($project)
2025-12-17 12:11:14 +00:00
->setPlatform($platform)
2023-08-04 16:21:41 +00:00
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
2026-02-04 05:30:22 +00:00
Http::post('/v1/migrations/nhost')
2023-08-04 16:21:41 +00:00
->groups(['api', 'migrations'])
->desc('Create NHost migration')
2023-08-04 16:21:41 +00:00
->label('scope', 'migrations.write')
2023-08-16 15:01:56 +00:00
->label('event', 'migrations.[migrationId].create')
2023-08-04 16:21:41 +00:00
->label('audits.event', 'migration.create')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'migrations',
group: null,
2025-01-17 04:31:39 +00:00
name: 'createNHostMigration',
description: '/docs/references/migrations/migration-nhost.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
2023-08-04 16:21:41 +00:00
->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate')
2023-08-16 15:01:56 +00:00
->param('subdomain', '', new Text(512), 'Source\'s Subdomain')
2023-08-04 16:21:41 +00:00
->param('region', '', new Text(512), 'Source\'s Region')
->param('adminSecret', '', new Text(512), 'Source\'s Admin Secret')
->param('database', '', new Text(512), 'Source\'s Database Name')
->param('username', '', new Text(512), 'Source\'s Database Username')
->param('password', '', new Text(512), 'Source\'s Database Password')
->param('port', 5432, new Integer(true), 'Source\'s Database Port', true)
->inject('response')
->inject('dbForProject')
->inject('project')
2025-12-17 12:11:14 +00:00
->inject('platform')
2023-08-04 16:21:41 +00:00
->inject('user')
2023-10-01 17:39:26 +00:00
->inject('queueForEvents')
->inject('queueForMigrations')
2025-12-17 12:11:14 +00:00
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
2023-08-04 16:21:41 +00:00
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => NHost::getName(),
2024-05-27 15:56:28 +00:00
'destination' => Appwrite::getName(),
2023-08-04 16:21:41 +00:00
'credentials' => [
'subdomain' => $subdomain,
'region' => $region,
'adminSecret' => $adminSecret,
'database' => $database,
'username' => $username,
'password' => $password,
'port' => $port,
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
]));
2023-10-01 17:39:26 +00:00
$queueForEvents->setParam('migrationId', $migration->getId());
2023-08-04 16:21:41 +00:00
// Trigger Transfer
2023-10-01 17:39:26 +00:00
$queueForMigrations
2023-08-04 16:21:41 +00:00
->setMigration($migration)
->setProject($project)
2025-12-17 12:11:14 +00:00
->setPlatform($platform)
2023-08-04 16:21:41 +00:00
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
2026-02-04 05:30:22 +00:00
Http::post('/v1/migrations/csv/imports')
2025-08-05 12:40:39 +00:00
->alias('/v1/migrations/csv')
2025-04-08 05:49:39 +00:00
->groups(['api', 'migrations'])
->desc('Import documents from a CSV')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
->label('sdk', new Method(
namespace: 'migrations',
2025-04-25 10:26:10 +00:00
group: null,
2025-10-21 12:26:01 +00:00
name: 'createCSVImport',
2025-08-05 05:04:31 +00:00
description: '/docs/references/migrations/migration-csv-import.md',
2025-04-08 05:49:39 +00:00
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File ID.')
2025-04-12 11:40:06 +00:00
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.')
->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true)
2025-04-08 05:49:39 +00:00
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
2025-04-08 05:49:39 +00:00
->inject('project')
2025-12-17 12:11:14 +00:00
->inject('platform')
2025-04-08 05:49:39 +00:00
->inject('deviceForFiles')
2025-08-05 12:40:39 +00:00
->inject('deviceForMigrations')
2025-04-09 11:05:27 +00:00
->inject('queueForEvents')
2025-04-08 05:49:39 +00:00
->inject('queueForMigrations')
2025-11-27 16:17:04 +00:00
->action(function (
string $bucketId,
string $fileId,
string $resourceId,
bool $internalFile,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization,
2025-11-27 16:17:04 +00:00
Document $project,
2025-12-17 12:11:14 +00:00
array $platform,
2025-11-27 16:17:04 +00:00
Device $deviceForFiles,
Device $deviceForMigrations,
Event $queueForEvents,
Migration $queueForMigrations
) {
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
if ($internalFile) {
return $dbForPlatform->getDocument('buckets', 'default');
}
return $dbForProject->getDocument('buckets', $bucketId);
});
2025-04-08 05:49:39 +00:00
2025-09-24 10:37:12 +00:00
if ($bucket->isEmpty()) {
2025-04-08 05:49:39 +00:00
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$file = $authorization->skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
2025-04-08 05:49:39 +00:00
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
$path = $file->getAttribute('path', '');
if (!$deviceForFiles->exists($path)) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path);
}
2025-09-24 10:37:12 +00:00
// No encryption or compression on files above 20MB.
2025-04-21 06:34:36 +00:00
$hasEncryption = !empty($file->getAttribute('openSSLCipher'));
2025-04-21 04:31:13 +00:00
$compression = $file->getAttribute('algorithm', Compression::NONE);
2025-04-21 06:34:36 +00:00
$hasCompression = $compression !== Compression::NONE;
2025-04-21 04:31:13 +00:00
2025-04-09 11:05:27 +00:00
$migrationId = ID::unique();
2025-08-05 12:40:39 +00:00
$newPath = $deviceForMigrations->getPath($migrationId . '_' . $fileId . '.csv');
2025-04-21 04:31:13 +00:00
2025-04-21 06:34:36 +00:00
if ($hasEncryption || $hasCompression) {
2025-04-21 04:31:13 +00:00
$source = $deviceForFiles->read($path);
2025-04-21 06:34:36 +00:00
if ($hasEncryption) {
$source = OpenSSL::decrypt(
$source,
$file->getAttribute('openSSLCipher'),
System::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')),
0,
hex2bin($file->getAttribute('openSSLIV')),
hex2bin($file->getAttribute('openSSLTag'))
);
}
if ($hasCompression) {
switch ($compression) {
case Compression::ZSTD:
$source = (new Zstd())->decompress($source);
break;
case Compression::GZIP:
$source = (new GZIP())->decompress($source);
break;
}
2025-04-21 04:31:13 +00:00
}
2025-09-24 10:37:12 +00:00
// Manual write after decryption and/or decompression
if (!$deviceForMigrations->write($newPath, $source, 'text/csv')) {
throw new \Exception('Unable to copy file');
2025-04-21 04:31:13 +00:00
}
2025-09-24 10:37:12 +00:00
} elseif (!$deviceForFiles->transfer($path, $newPath, $deviceForMigrations)) {
throw new \Exception('Unable to copy file');
2025-04-14 09:33:30 +00:00
}
2025-08-05 12:40:39 +00:00
$fileSize = $deviceForMigrations->getFileSize($newPath);
2025-04-09 11:05:27 +00:00
$resources = Transfer::extractServices([Transfer::GROUP_DATABASES]);
2025-04-08 05:49:39 +00:00
$migration = $dbForProject->createDocument('migrations', new Document([
2025-04-09 11:05:27 +00:00
'$id' => $migrationId,
2025-04-08 05:49:39 +00:00
'status' => 'pending',
'stage' => 'init',
2025-04-16 06:25:04 +00:00
'source' => CSV::getName(),
2025-04-17 05:24:23 +00:00
'destination' => Appwrite::getName(),
2025-04-09 11:05:27 +00:00
'resources' => $resources,
2025-04-08 05:49:39 +00:00
'resourceId' => $resourceId,
'resourceType' => Resource::TYPE_DATABASE,
2025-07-02 09:02:16 +00:00
'statusCounters' => '{}',
'resourceData' => '{}',
2025-04-08 05:49:39 +00:00
'errors' => [],
2025-04-14 09:33:30 +00:00
'options' => [
'path' => $newPath,
2025-04-09 11:05:27 +00:00
'size' => $fileSize,
],
2025-04-08 05:49:39 +00:00
]));
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
2025-12-17 12:11:14 +00:00
->setProject($project)
2025-04-08 05:49:39 +00:00
->trigger();
2025-04-09 11:05:27 +00:00
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
2025-04-08 05:49:39 +00:00
});
2026-02-04 05:30:22 +00:00
Http::post('/v1/migrations/csv/exports')
2025-08-05 05:04:31 +00:00
->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,
2025-10-21 12:26:01 +00:00
name: 'createCSVExport',
2025-08-05 05:04:31 +00:00
description: '/docs/references/migrations/migration-csv-export.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.')
2025-09-24 10:37:12 +00:00
->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)
2025-10-21 12:12:18 +00:00
->param('queries', [], new ArrayList(new Text(0)), 'Array of query strings generated using the Query class provided by the SDK to filter documents to export. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
2025-10-22 03:41:00 +00:00
->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 double quotes.', true)
2025-09-24 10:37:12 +00:00
->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')
2025-08-05 05:04:31 +00:00
->inject('response')
->inject('dbForProject')
2025-11-12 09:18:25 +00:00
->inject('dbForPlatform')
->inject('authorization')
2025-08-05 05:04:31 +00:00
->inject('project')
2025-12-17 12:11:14 +00:00
->inject('platform')
2025-08-05 05:04:31 +00:00
->inject('queueForEvents')
->inject('queueForMigrations')
2025-09-24 10:37:12 +00:00
->action(function (
string $resourceId,
string $filename,
array $columns,
2025-10-21 12:12:18 +00:00
array $queries,
2025-09-24 10:37:12 +00:00
string $delimiter,
string $enclosure,
string $escape,
bool $header,
bool $notify,
Document $user,
Response $response,
Database $dbForProject,
2025-11-12 09:18:25 +00:00
Database $dbForPlatform,
Authorization $authorization,
2025-09-24 10:37:12 +00:00
Document $project,
2025-12-17 12:11:14 +00:00
array $platform,
2025-09-24 10:37:12 +00:00
Event $queueForEvents,
Migration $queueForMigrations
) {
2025-10-21 12:12:18 +00:00
try {
2025-10-22 12:28:06 +00:00
$parsedQueries = Query::parseQueries($queries);
2025-10-21 12:12:18 +00:00
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
2025-09-24 10:37:12 +00:00
if ($bucket->isEmpty()) {
2025-08-05 05:04:31 +00:00
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
2025-09-24 10:37:12 +00:00
[$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));
2025-09-24 10:37:12 +00:00
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
2025-09-24 10:37:12 +00:00
if ($collection->isEmpty()) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
2025-08-05 05:04:31 +00:00
2025-10-21 12:12:18 +00:00
$validator = new Documents(
attributes: $collection->getAttribute('attributes', []),
indexes: $collection->getAttribute('indexes', []),
idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
);
2025-10-22 12:28:06 +00:00
if (!$validator->isValid($parsedQueries)) {
2025-10-21 12:12:18 +00:00
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
2025-08-05 05:04:31 +00:00
$migration = $dbForProject->createDocument('migrations', new Document([
2025-09-24 10:37:12 +00:00
'$id' => ID::unique(),
2025-08-05 05:04:31 +00:00
'status' => 'pending',
'stage' => 'init',
'source' => Appwrite::getName(),
'destination' => CSV::getName(),
2025-09-24 10:37:12 +00:00
'resources' => Transfer::extractServices([Transfer::GROUP_DATABASES]),
2025-08-05 05:04:31 +00:00
'resourceId' => $resourceId,
'resourceType' => Resource::TYPE_DATABASE,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
'options' => [
2025-11-12 09:18:25 +00:00
'bucketId' => 'default', // Always use internal bucket
2025-09-24 10:37:12 +00:00
'filename' => $filename,
2025-08-05 12:40:39 +00:00
'columns' => $columns,
2025-10-21 12:12:18 +00:00
'queries' => $queries,
2025-09-24 10:37:12 +00:00
'delimiter' => $delimiter,
'enclosure' => $enclosure,
'escape' => $escape,
'header' => $header,
'notify' => $notify,
'userInternalId' => $user->getSequence(),
2025-08-05 05:04:31 +00:00
],
]));
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
2025-12-17 12:11:14 +00:00
->setPlatform($platform)
2025-08-05 05:04:31 +00:00
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
2026-02-04 05:30:22 +00:00
Http::get('/v1/migrations')
2023-08-04 16:21:41 +00:00
->groups(['api', 'migrations'])
->desc('List migrations')
2023-08-04 16:21:41 +00:00
->label('scope', 'migrations.read')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'migrations',
group: null,
2025-01-17 04:31:39 +00:00
name: 'list',
description: '/docs/references/migrations/list-migrations.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION_LIST,
)
]
))
2023-08-04 16:21:41 +00:00
->param('queries', [], new Migrations(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Migrations::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
2023-08-04 16:21:41 +00:00
->inject('response')
->inject('dbForProject')
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
2024-02-12 16:02:04 +00:00
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
2023-08-04 16:21:41 +00:00
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
2026-01-28 12:53:24 +00:00
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
2026-01-28 12:53:24 +00:00
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
2023-08-04 16:21:41 +00:00
$migrationId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('migrations', $migrationId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Migration '{$migrationId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$migrations = $dbForProject->find('migrations', $queries);
$total = $includeTotal ? $dbForProject->count('migrations', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
2023-08-04 16:21:41 +00:00
$response->dynamic(new Document([
'migrations' => $migrations,
'total' => $total,
2023-08-04 16:21:41 +00:00
]), Response::MODEL_MIGRATION_LIST);
});
2026-02-04 05:30:22 +00:00
Http::get('/v1/migrations/:migrationId')
2023-08-04 16:21:41 +00:00
->groups(['api', 'migrations'])
->desc('Get migration')
2023-08-04 16:21:41 +00:00
->label('scope', 'migrations.read')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'migrations',
group: null,
2025-01-17 04:31:39 +00:00
name: 'get',
description: '/docs/references/migrations/get-migration.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION,
)
]
))
2023-08-04 16:21:41 +00:00
->param('migrationId', '', new UID(), 'Migration unique ID.')
->inject('response')
->inject('dbForProject')
->action(function (string $migrationId, Response $response, Database $dbForProject) {
$migration = $dbForProject->getDocument('migrations', $migrationId);
if ($migration->isEmpty()) {
throw new Exception(Exception::MIGRATION_NOT_FOUND);
}
$response->dynamic($migration, Response::MODEL_MIGRATION);
});
2026-02-04 05:30:22 +00:00
Http::get('/v1/migrations/appwrite/report')
2023-08-04 16:21:41 +00:00
->groups(['api', 'migrations'])
->desc('Get Appwrite migration report')
2023-08-04 16:21:41 +00:00
->label('scope', 'migrations.write')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'migrations',
group: null,
2025-01-17 04:31:39 +00:00
name: 'getAppwriteReport',
description: '/docs/references/migrations/migration-appwrite-report.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION_REPORT,
)
]
))
2023-08-04 16:21:41 +00:00
->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate')
->param('endpoint', '', new URL(), "Source's Appwrite Endpoint")
->param('projectID', '', new Text(512), "Source's Project ID")
->param('key', '', new Text(512), "Source's API Key")
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('user')
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
2025-04-11 14:52:19 +00:00
$appwrite = new Appwrite($projectID, $endpoint, $key);
2023-08-04 16:21:41 +00:00
try {
$report = $appwrite->report($resources);
} catch (\Throwable $e) {
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
2023-08-04 16:21:41 +00:00
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
2023-08-04 16:21:41 +00:00
});
2026-02-04 05:30:22 +00:00
Http::get('/v1/migrations/firebase/report')
2023-08-04 16:21:41 +00:00
->groups(['api', 'migrations'])
->desc('Get Firebase migration report')
2023-08-04 16:21:41 +00:00
->label('scope', 'migrations.write')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'migrations',
group: null,
2025-01-17 04:31:39 +00:00
name: 'getFirebaseReport',
description: '/docs/references/migrations/migration-firebase-report.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION_REPORT,
)
]
))
2023-08-04 16:21:41 +00:00
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
->param('serviceAccount', '', new Text(65536), 'JSON of the Firebase service account credentials')
->inject('response')
->action(function (array $resources, string $serviceAccount, Response $response) {
$serviceAccount = json_decode($serviceAccount, true);
if (empty($serviceAccount)) {
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
}
if (!isset($serviceAccount['project_id']) || !isset($serviceAccount['client_email']) || !isset($serviceAccount['private_key'])) {
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
}
$firebase = new Firebase($serviceAccount);
2023-08-04 16:21:41 +00:00
try {
$report = $firebase->report($resources);
} catch (\Throwable $e) {
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
2023-08-04 16:21:41 +00:00
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
2023-08-04 16:21:41 +00:00
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
2023-08-04 16:21:41 +00:00
});
2026-02-04 05:30:22 +00:00
Http::get('/v1/migrations/supabase/report')
2023-08-04 16:21:41 +00:00
->groups(['api', 'migrations'])
->desc('Get Supabase migration report')
2023-08-04 16:21:41 +00:00
->label('scope', 'migrations.write')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'migrations',
group: null,
2025-01-17 04:31:39 +00:00
name: 'getSupabaseReport',
description: '/docs/references/migrations/migration-supabase-report.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION_REPORT,
)
]
))
2023-08-04 16:21:41 +00:00
->param('resources', [], new ArrayList(new WhiteList(Supabase::getSupportedResources(), true)), 'List of resources to migrate')
->param('endpoint', '', new URL(), 'Source\'s Supabase Endpoint.')
->param('apiKey', '', new Text(512), 'Source\'s API Key.')
->param('databaseHost', '', new Text(512), 'Source\'s Database Host.')
->param('username', '', new Text(512), 'Source\'s Database Username.')
->param('password', '', new Text(512), 'Source\'s Database Password.')
->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true)
2023-08-04 16:21:41 +00:00
->inject('response')
->inject('dbForProject')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response) {
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
2023-08-04 16:21:41 +00:00
try {
$report = $supabase->report($resources);
} catch (\Throwable $e) {
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
2023-08-04 16:21:41 +00:00
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
2023-08-04 16:21:41 +00:00
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
2023-08-04 16:21:41 +00:00
});
2026-02-04 05:30:22 +00:00
Http::get('/v1/migrations/nhost/report')
2023-08-04 16:21:41 +00:00
->groups(['api', 'migrations'])
->desc('Get NHost migration report')
2023-08-04 16:21:41 +00:00
->label('scope', 'migrations.write')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'migrations',
group: null,
2025-01-17 04:31:39 +00:00
name: 'getNHostReport',
description: '/docs/references/migrations/migration-nhost-report.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION_REPORT,
)
]
))
->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate.')
->param('subdomain', '', new Text(512), 'Source\'s Subdomain.')
->param('region', '', new Text(512), 'Source\'s Region.')
->param('adminSecret', '', new Text(512), 'Source\'s Admin Secret.')
->param('database', '', new Text(512), 'Source\'s Database Name.')
->param('username', '', new Text(512), 'Source\'s Database Username.')
->param('password', '', new Text(512), 'Source\'s Database Password.')
->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true)
2023-08-04 16:21:41 +00:00
->inject('response')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response) {
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
2023-08-04 16:21:41 +00:00
try {
$report = $nhost->report($resources);
} catch (\Throwable $e) {
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
2023-08-04 16:21:41 +00:00
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
2023-08-04 16:21:41 +00:00
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
2023-08-04 16:21:41 +00:00
});
2026-02-04 05:30:22 +00:00
Http::patch('/v1/migrations/:migrationId')
2023-08-04 16:21:41 +00:00
->groups(['api', 'migrations'])
->desc('Update retry migration')
2023-08-04 16:21:41 +00:00
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].retry')
->label('audits.event', 'migration.retry')
->label('audits.resource', 'migrations/{request.migrationId}')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'migrations',
group: null,
2025-01-17 04:31:39 +00:00
name: 'retry',
description: '/docs/references/migrations/retry-migration.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
2023-08-04 16:21:41 +00:00
->param('migrationId', '', new UID(), 'Migration unique ID.')
->inject('response')
->inject('dbForProject')
->inject('project')
2025-12-17 12:11:14 +00:00
->inject('platform')
2023-08-04 16:21:41 +00:00
->inject('user')
2023-10-01 17:39:26 +00:00
->inject('queueForMigrations')
2025-12-17 12:11:14 +00:00
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Migration $queueForMigrations) {
2023-08-04 16:21:41 +00:00
$migration = $dbForProject->getDocument('migrations', $migrationId);
if ($migration->isEmpty()) {
throw new Exception(Exception::MIGRATION_NOT_FOUND);
}
if ($migration->getAttribute('status') !== 'failed') {
throw new Exception(Exception::MIGRATION_IN_PROGRESS, 'Migration not failed yet');
}
$migration
->setAttribute('status', 'pending')
->setAttribute('dateUpdated', \time());
// Trigger Migration
2023-10-01 17:39:26 +00:00
$queueForMigrations
2023-08-04 16:21:41 +00:00
->setMigration($migration)
->setProject($project)
2025-12-17 12:11:14 +00:00
->setPlatform($platform)
2023-08-04 16:21:41 +00:00
->setUser($user)
->trigger();
$response->noContent();
});
2026-02-04 05:30:22 +00:00
Http::delete('/v1/migrations/:migrationId')
2023-08-04 16:21:41 +00:00
->groups(['api', 'migrations'])
->desc('Delete migration')
2023-08-04 16:21:41 +00:00
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].delete')
->label('audits.event', 'migrationId.delete')
->label('audits.resource', 'migrations/{request.migrationId}')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'migrations',
group: null,
2025-01-17 04:31:39 +00:00
name: 'delete',
description: '/docs/references/migrations/delete-migration.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
2023-08-04 16:21:41 +00:00
->param('migrationId', '', new UID(), 'Migration ID.')
->inject('response')
->inject('dbForProject')
2023-10-01 17:39:26 +00:00
->inject('queueForEvents')
->action(function (string $migrationId, Response $response, Database $dbForProject, Event $queueForEvents) {
2023-08-04 16:21:41 +00:00
$migration = $dbForProject->getDocument('migrations', $migrationId);
if ($migration->isEmpty()) {
throw new Exception(Exception::MIGRATION_NOT_FOUND);
}
if (!$dbForProject->deleteDocument('migrations', $migration->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove migration from DB');
2023-08-04 16:21:41 +00:00
}
2023-10-01 17:39:26 +00:00
$queueForEvents->setParam('migrationId', $migration->getId());
2023-08-04 16:21:41 +00:00
$response->noContent();
2025-01-17 04:39:16 +00:00
});