1
0
Fork 0
mirror of synced 2024-06-14 00:34:51 +12:00

Move to new branch

This commit is contained in:
Bradley Schofield 2023-08-04 17:21:41 +01:00
parent 9f7f99ba83
commit 8f5d79e668
21 changed files with 2168 additions and 33 deletions

View file

@ -177,7 +177,8 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/worker-builds && \
chmod +x /usr/local/bin/worker-mails && \
chmod +x /usr/local/bin/worker-messaging && \
chmod +x /usr/local/bin/worker-webhooks
chmod +x /usr/local/bin/worker-webhooks && \
chmod +x /usr/local/bin/worker-migrations
# Letsencrypt Permissions
RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/

View file

@ -3664,6 +3664,143 @@ $collections = [
],
],
],
'migrations' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('migrations'),
'name' => 'Migrations',
'attributes' => [
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('stage'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('source'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 8192,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('credentials'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65536,
'signed' => true,
'required' => false,
'default' => [],
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('resources'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => [],
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('statusCounters'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 3000,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('resourceData'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 131070,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('errors'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => true,
'default' => null,
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
]
],
'indexes' => [
[
'$id' => '_key_status',
'type' => Database::INDEX_KEY,
'attributes' => ['status'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_stage',
'type' => Database::INDEX_KEY,
'attributes' => ['stage'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_source',
'type' => Database::INDEX_KEY,
'attributes' => ['source'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_fulltext_search'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
]
],
],
];
return $collections;

View file

@ -629,4 +629,21 @@ return [
'description' => 'Too many queries.',
'code' => 400,
],
/** Migrations */
Exception::MIGRATION_NOT_FOUND => [
'name' => Exception::MIGRATION_NOT_FOUND,
'description' => 'Migration with the requested ID could not be found.',
'code' => 404,
],
Exception::MIGRATION_ALREADY_EXISTS => [
'name' => Exception::MIGRATION_ALREADY_EXISTS,
'description' => 'Migration with the requested ID already exists.',
'code' => 409,
],
Exception::MIGRATION_IN_PROGRESS => [
'name' => Exception::MIGRATION_IN_PROGRESS,
'description' => 'Migration is already in progress.',
'code' => 409,
],
];

View file

@ -51,6 +51,8 @@ $admins = [
'functions.write',
'execution.read',
'execution.write',
'migrations.read',
'migrations.write',
];
return [

View file

@ -76,4 +76,10 @@ return [ // List of publicly visible scopes
'health.read' => [
'description' => 'Access to read your project\'s health status',
],
'migrations.read' => [
'description' => 'Access to read your project\'s migrations',
],
'migrations.write' => [
'description' => 'Access to create, update, and delete your project\'s migrations.',
]
];

View file

@ -199,4 +199,17 @@ return [
'optional' => false,
'icon' => '',
],
'migrations' => [
'key' => 'migrations',
'name' => 'Migrations',
'subtitle' => 'The Migrations service allows you to migrate third-party data to your Appwrite server.',
'description' => '/docs/services/migrations.md',
'controller' => 'api/migrations.php',
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/migrations',
'tests' => true,
'optional' => true,
'icon' => '/images/services/migrations.png',
],
];

View file

@ -0,0 +1,888 @@
<?php
use Appwrite\Auth\OAuth2\Firebase as OAuth2Firebase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Migration;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Database\Validator\Queries\Migrations;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Transfer\Sources\Appwrite;
use Utopia\Transfer\Sources\Firebase;
use Utopia\Transfer\Sources\NHost;
use Utopia\Transfer\Sources\Supabase;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Host;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
include_once __DIR__ . '/../shared/api.php';
App::post('/v1/migrations/appwrite')
->groups(['api', 'migrations'])
->desc('Migrate Appwrite Data')
->label('scope', 'migrations.write')
->label('event', 'migrations.create')
->label('audits.event', 'migration.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'createAppwriteMigration')
->label('sdk.description', '/docs/references/migrations/migration-appwrite.md')
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION)
->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate')
->param('endpoint', '', new URL(), "Source's Appwrite Endpoint")
->param('projectId', '', new UID(), "Source's Project ID")
->param('apiKey', '', new Text(512), "Source's API Key")
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('user')
->inject('events')
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Appwrite::getName(),
'credentials' => [
'endpoint' => $endpoint,
'projectId' => $projectId,
'apiKey' => $apiKey,
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
]));
$events->setParam('migrationId', $migration->getId());
// Trigger Transfer
$event = new Migration();
$event
->setMigration($migration)
->setProject($project)
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/firebase')
->groups(['api', 'migrations'])
->desc('Migrate Firebase Data (Service Account)')
->label('scope', 'migrations.write')
->label('event', 'migrations.create')
->label('audits.event', 'migration.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'createFirebaseMigration')
->label('sdk.description', '/docs/references/migrations/migration-firebase.md')
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION)
->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')
->inject('user')
->inject('events')
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Firebase::getName(),
'credentials' => [
'serviceAccount' => $serviceAccount,
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
]));
$events->setParam('migrationId', $migration->getId());
// Trigger Transfer
$event = new Migration();
$event
->setMigration($migration)
->setProject($project)
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/firebase/oauth')
->groups(['api', 'migrations'])
->desc('Migrate Firebase Data (OAuth)')
->label('scope', 'migrations.write')
->label('event', 'migrations.create')
->label('audits.event', 'migration.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'createFirebaseOAuthMigration')
->label('sdk.description', '/docs/references/migrations/migration-firebase.md')
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION)
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
->param('projectId', '', new Text(65536), 'Project ID of the Firebase Project')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->inject('project')
->inject('user')
->inject('events')
->inject('request')
->action(function (array $resources, string $projectId, Response $response, Database $dbForProject, Database $dbForConsole, Document $project, Document $user, Event $events, Request $request) {
$firebase = new OAuth2Firebase(
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$accessToken = $user->getAttribute('migrationsFirebaseAccessToken');
$refreshToken = $user->getAttribute('migrationsFirebaseRefreshToken');
$accessTokenExpiry = $user->getAttribute('migrationsFirebaseAccessTokenExpiry');
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
$firebase->refreshTokens($refreshToken);
$accessToken = $firebase->getAccessToken('');
$refreshToken = $firebase->getRefreshToken('');
$verificationId = $firebase->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
}
$user = $user
->setAttribute('migrationsFirebaseAccessToken', $accessToken)
->setAttribute('migrationsFirebaseRefreshToken', $refreshToken)
->setAttribute('migrationsFirebaseAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $firebase->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('users', $user->getId(), $user);
}
$serviceAccount = $firebase->createServiceAccount($accessToken, $projectId);
// $migration = $dbForProject->createDocument('migrations', new Document([
// '$id' => ID::unique(),
// 'status' => 'pending',
// 'stage' => 'init',
// 'source' => Firebase::getName(),
// 'credentials' => [
// 'serviceAccount' => $serviceAccount,
// ],
// 'resources' => $resources,
// 'statusCounters' => '{}',
// 'resourceData' => '{}',
// 'errors' => []
// ]));
// $events->setParam('migrationId', $migration->getId());
// // Trigger Transfer
// $event = new Migration();
// $event
// ->setMigration($migration)
// ->setProject($project)
// ->setUser($user)
// ->trigger();
// $response
// ->setStatusCode(Response::STATUS_CODE_ACCEPTED)
// ->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/supabase')
->groups(['api', 'migrations'])
->desc('Migrate Supabase Data')
->label('scope', 'migrations.write')
->label('event', 'migrations.create')
->label('audits.event', 'migration.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'createSupabaseMigration')
->label('sdk.description', '/docs/references/migrations/migration-supabase.md')
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION)
->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')
->inject('user')
->inject('events')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Supabase::getName(),
'credentials' => [
'endpoint' => $endpoint,
'apiKey' => $apiKey,
'databaseHost' => $databaseHost,
'username' => $username,
'password' => $password,
'port' => $port,
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
]));
$events->setParam('migrationId', $migration->getId());
// Trigger Transfer
$event = new Migration();
$event
->setMigration($migration)
->setProject($project)
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/nhost')
->groups(['api', 'migrations'])
->desc('Migrate NHost Data')
->label('scope', 'migrations.write')
->label('event', 'migrations.create')
->label('audits.event', 'migration.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'createNHostMigration')
->label('sdk.description', '/docs/references/migrations/migration-nhost.md')
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION)
->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate')
->param('subdomain', '', new URL(), '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)
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('user')
->inject('events')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => NHost::getName(),
'credentials' => [
'subdomain' => $subdomain,
'region' => $region,
'adminSecret' => $adminSecret,
'database' => $database,
'username' => $username,
'password' => $password,
'port' => $port,
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
]));
$events->setParam('migrationId', $migration->getId());
// Trigger Transfer
$event = new Migration();
$event
->setMigration($migration)
->setProject($project)
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::get('/v1/migrations')
->groups(['api', 'migrations'])
->desc('List Migrations')
->label('scope', 'migrations.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'list')
->label('sdk.description', '/docs/references/migrations/list-migrations.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION_LIST)
->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)
->inject('response')
->inject('dbForProject')
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$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'];
$response->dynamic(new Document([
'migrations' => $dbForProject->find('migrations', $queries),
'total' => $dbForProject->count('migrations', $filterQueries, APP_LIMIT_COUNT),
]), Response::MODEL_MIGRATION_LIST);
});
App::get('/v1/migrations/:migrationId')
->groups(['api', 'migrations'])
->desc('Get Migration')
->label('scope', 'migrations.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'get')
->label('sdk.description', '/docs/references/migrations/get-migration.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION)
->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);
});
App::get('/v1/migrations/appwrite/report')
->groups(['api', 'migrations'])
->desc('Generate a report on Appwrite Data')
->label('scope', 'migrations.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'getAppwriteReport')
->label('sdk.description', '/docs/references/migrations/migration-appwrite-report.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
->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) {
try {
$appwrite = new Appwrite($projectID, $endpoint, $key);
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($appwrite->report($resources)), Response::MODEL_MIGRATION_REPORT);
} catch (\Exception $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
}
});
App::get('/v1/migrations/firebase/report')
->groups(['api', 'migrations'])
->desc('Generate a report on Firebase Data')
->label('scope', 'migrations.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'getFirebaseReport')
->label('sdk.description', '/docs/references/migrations/migration-firebase-report.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
->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) {
try {
$firebase = new Firebase(json_decode($serviceAccount, true));
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($firebase->report($resources)), Response::MODEL_MIGRATION_REPORT);
} catch (\Exception $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
}
});
App::get('/v1/migrations/firebase/report/oauth')
->groups(['api', 'migrations'])
->desc('Generate a report on Firebase Data using OAuth')
->label('scope', 'migrations.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'getFirebaseReport')
->label('sdk.description', '/docs/references/migrations/migration-firebase-report.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
->param('projectId', '', new Text(65536), 'Project ID')
->inject('response')
->inject('request')
->inject('user')
->inject('dbForConsole')
->action(function (array $resources, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) {
try {
$firebase = new OAuth2Firebase(
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$accessToken = $user->getAttribute('migrationsFirebaseAccessToken');
$refreshToken = $user->getAttribute('migrationsFirebaseRefreshToken');
$accessTokenExpiry = $user->getAttribute('migrationsFirebaseAccessTokenExpiry');
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
$firebase->refreshTokens($refreshToken);
$accessToken = $firebase->getAccessToken('');
$refreshToken = $firebase->getRefreshToken('');
$verificationId = $firebase->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
}
$user = $user
->setAttribute('migrationsFirebaseAccessToken', $accessToken)
->setAttribute('migrationsFirebaseRefreshToken', $refreshToken)
->setAttribute('migrationsFirebaseAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $firebase->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('users', $user->getId(), $user);
}
$serviceAccount = $firebase->createServiceAccount($accessToken, $projectId);
$firebase = new Firebase(json_decode($serviceAccount, true));
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($firebase->report($resources)), Response::MODEL_MIGRATION_REPORT);
} catch (\Exception $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
}
});
App::get('/v1/migrations/firebase/connect')
->desc('Authorize with firebase')
->groups(['api', 'migrations'])
->label('scope', 'public')
->label('origin', '*')
->label('sdk.auth', [])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'createFirebaseAuth')
->label('sdk.description', '')
->label('sdk.response.code', Response::STATUS_CODE_MOVED_PERMANENTLY)
->label('sdk.response.type', Response::CONTENT_TYPE_HTML)
->label('sdk.methodType', 'webAuth')
->param('redirect', '', fn ($clients) => new Host($clients), 'URL to redirect back to your Firebase authorization. Only console hostnames are allowed.', true, ['clients'])
->param('projectId', '', new UID(), 'Project ID')
->inject('response')
->inject('request')
->inject('user')
->inject('dbForConsole')
->action(function (string $redirect, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) {
$state = \json_encode([
'projectId' => $projectId,
'redirect' => $redirect,
]);
$prefs = $user->getAttribute('prefs', []);
$prefs['migrationState'] = $state;
$user->setAttribute('prefs', $prefs);
$dbForConsole->updateDocument('users', $user->getId(), $user);
$oauth2 = new OAuth2Firebase(
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$url = $oauth2->getLoginURL();
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($url);
});
App::get('/v1/migrations/firebase/redirect')
->desc('Capture and receive data on Firebase authorization')
->groups(['api', 'migrations'])
->label('scope', 'public')
->label('error', __DIR__ . '/../../views/general/error.phtml')
->param('code', '', new Text(2048), 'OAuth2 code.', true)
->inject('user')
->inject('project')
->inject('request')
->inject('response')
->inject('dbForConsole')
->action(function (string $code, Document $user, Document $project, Request $request, Response $response, Database $dbForConsole) {
$state = $user['prefs']['migrationState'] ?? '{}';
$prefs['migrationState'] = '';
$user->setAttribute('prefs', $prefs);
$dbForConsole->updateDocument('users', $user->getId(), $user);
if (empty($state)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Installation requests from organisation members for the Appwrite Google App are currently unsupported.');
}
$state = \json_decode($state, true);
$redirect = $state['redirect'] ?? '';
$projectId = $state['projectId'] ?? '';
$project = $dbForConsole->getDocument('projects', $projectId);
if (empty($redirect)) {
$redirect = $request->getProtocol() . '://' . $request->getHostname() . '/console/project-$projectId/settings/migrations';
}
if ($project->isEmpty()) {
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirect);
return;
}
// OAuth Authroization
if (!empty($code)) {
$oauth2 = new OAuth2Firebase(
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$accessToken = $oauth2->getAccessToken($code);
$refreshToken = $oauth2->getRefreshToken($code);
$accessTokenExpiry = $oauth2->getAccessTokenExpiry($code);
if (empty($accessToken)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token.');
}
if (empty($refreshToken)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get refresh token.');
}
if (empty($accessTokenExpiry)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token expiry.');
}
$user = $user
->setAttribute('migrationsFirebaseAccessToken', $accessToken)
->setAttribute('migrationsFirebaseRefreshToken', $refreshToken)
->setAttribute('migrationsFirebaseAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry));
$dbForConsole->updateDocument('users', $user->getId(), $user);
} else {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Missing OAuth2 code.');
}
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirect);
});
App::get('/v1/migrations/firebase/projects')
->desc('List Firebase Projects')
->groups(['api', 'migrations'])
->label('scope', 'public')
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'listFirebaseProjects')
->label('sdk.description', '')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST)
->inject('user')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('request')
->action(function (Document $user, Response $response, Document $project, Database $dbForConsole, Request $request) {
if (empty($user->getAttribute('migrationsFirebaseAccessToken')) || empty($user->getAttribute('migrationsFirebaseRefreshToken')) || empty($user->getAttribute('migrationsFirebaseAccessTokenExpiry'))) {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase');
}
if (App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Missing Google OAuth credentials');
}
$firebase = new OAuth2Firebase(
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$accessToken = $user->getAttribute('migrationsFirebaseAccessToken');
$refreshToken = $user->getAttribute('migrationsFirebaseRefreshToken');
$accessTokenExpiry = $user->getAttribute('migrationsFirebaseAccessTokenExpiry');
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
$firebase->refreshTokens($refreshToken);
$accessToken = $firebase->getAccessToken('');
$refreshToken = $firebase->getRefreshToken('');
$verificationId = $firebase->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
}
$user = $user
->setAttribute('migrationsFirebaseAccessToken', $accessToken)
->setAttribute('migrationsFirebaseRefreshToken', $refreshToken)
->setAttribute('migrationsFirebaseAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $firebase->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('users', $user->getId(), $user);
}
$projects = $firebase->getProjects($accessToken);
$output = [];
foreach ($projects as $project) {
$output[] = [
'displayName' => $project['displayName'],
'projectId' => $project['projectId'],
];
}
$response->dynamic(new Document([
'projects' => $output,
'total' => count($output),
]), Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST);
});
App::get('/v1/migrations/firebase/deauthorize')
->desc('Revoke Appwrite\'s authorization to access Firebase Projects')
->groups(['api', 'migrations'])
->label('scope', 'public')
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'deleteFirebaseAuth')
->label('sdk.description', '')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->inject('user')
->inject('response')
->inject('dbForConsole')
->action(function (Document $user, Response $response, Database $dbForConsole) {
$user = $user
->setAttribute('migrationsFirebaseAccessToken', '')
->setAttribute('migrationsFirebaseRefreshToken', '')
->setAttribute('migrationsFirebaseAccessTokenExpiry', '');
$dbForConsole->updateDocument('users', $user->getId(), $user);
$response->noContent();
});
App::get('/v1/migrations/supabase/report')
->groups(['api', 'migrations'])
->desc('Generate a report on Supabase Data')
->label('scope', 'migrations.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'getSupabaseReport')
->label('sdk.description', '/docs/references/migrations/migration-supabase-report.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
->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')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response) {
try {
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($supabase->report($resources)), Response::MODEL_MIGRATION_REPORT);
} catch (\Exception $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
}
});
App::get('/v1/migrations/nhost/report')
->groups(['api', 'migrations'])
->desc('Generate a report on NHost Data')
->label('scope', 'migrations.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'getNHostReport')
->label('sdk.description', '/docs/references/migrations/migration-nhost-report.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate')
->param('subdomain', '', new URL(), '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)
->inject('response')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response) {
try {
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($nhost->report($resources)), Response::MODEL_MIGRATION_REPORT);
} catch (\Exception $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
}
});
App::patch('/v1/migrations/:migrationId')
->groups(['api', 'migrations'])
->desc('Retry Migration')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].retry')
->label('audits.event', 'migration.retry')
->label('audits.resource', 'migrations/{request.migrationId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'retry')
->label('sdk.description', '/docs/references/migrations/retry-migration.md')
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION)
->param('migrationId', '', new UID(), 'Migration unique ID.')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('user')
->inject('events')
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, Document $user, Event $eventInstance) {
$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
$event = new Migration();
$event
->setMigration($migration)
->setProject($project)
->setUser($user)
->trigger();
$response->noContent();
});
App::delete('/v1/migrations/:migrationId')
->groups(['api', 'migrations'])
->desc('Delete Migration')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].delete')
->label('audits.event', 'migrationId.delete')
->label('audits.resource', 'migrations/{request.migrationId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'delete')
->label('sdk.description', '/docs/references/functions/delete-migration.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('migrationId', '', new UID(), 'Migration ID.')
->inject('response')
->inject('dbForProject')
->inject('deletes')
->inject('events')
->action(function (string $migrationId, Response $response, Database $dbForProject, Delete $deletes, Event $events) {
$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', 500);
}
$events->setParam('migrationId', $migration->getId());
$response->noContent();
});

315
app/workers/migrations.php Normal file
View file

@ -0,0 +1,315 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Permission;
use Appwrite\Resque\Worker;
use Appwrite\Role;
use Appwrite\Utopia\Response\Model\Migration;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Transfer\Destinations\Appwrite as DestinationsAppwrite;
use Utopia\Transfer\Resource;
use Utopia\Transfer\Source;
use Utopia\Transfer\Sources\Appwrite;
use Utopia\Transfer\Sources\Firebase;
use Utopia\Transfer\Sources\NHost;
use Utopia\Transfer\Sources\Supabase;
use Utopia\Transfer\Transfer;
require_once __DIR__.'/../init.php';
Console::title('Migrations V1 Worker');
Console::success(APP_NAME.' Migrations worker v1 has started');
class MigrationsV1 extends Worker
{
/**
* Database connection shared across all methods of this file
*
* @var Database
*/
private Database $dbForProject;
public function getName(): string
{
return 'migrations';
}
public function init(): void
{
}
public function run(): void
{
$type = $this->args['type'] ?? '';
$events = $this->args['events'] ?? [];
$project = new Document($this->args['project'] ?? []);
$user = new Document($this->args['user'] ?? []);
$payload = json_encode($this->args['payload'] ?? []);
if ($project->getId() === 'console') {
return;
}
/**
* Handle Event execution.
*/
if (! empty($events)) {
return;
}
$this->dbForProject = $this->getProjectDB($this->args['project']['$id']);
$this->processMigration();
}
/**
* Process Source
*
* @return Source
*
* @throws \Exception
*/
protected function processSource(string $source, array $credentials): Source
{
switch ($source) {
case Firebase::getName():
return new Firebase(
json_decode($credentials['serviceAccount'], true),
);
break;
case Supabase::getName():
return new Supabase(
$credentials['endpoint'],
$credentials['apiKey'],
$credentials['databaseHost'],
'postgres',
$credentials['username'],
$credentials['password'],
$credentials['port'],
);
break;
case NHost::getName():
return new NHost(
$credentials['subdomain'],
$credentials['region'],
$credentials['adminSecret'],
$credentials['database'],
$credentials['username'],
$credentials['password'],
$credentials['port'],
);
break;
case Appwrite::getName():
return new Appwrite($credentials['projectId'], str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], $credentials['apiKey']);
break;
default:
throw new \Exception('Invalid source type');
break;
}
}
protected function updateMigrationDocument(Document $migration, Document $project): Document
{
/** Trigger Realtime */
$allEvents = Event::generateEvents('migrations.[migrationId].update', [
'migrationId' => $migration->getId(),
]);
$target = Realtime::fromPayload(
event: $allEvents[0],
payload: $migration,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $migration->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles'],
);
Realtime::send(
projectId: $project->getId(),
payload: $migration->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles'],
);
return $this->dbForProject->updateDocument('migrations', $migration->getId(), $migration);
}
protected function removeAPIKey(Document $apiKey)
{
$consoleDB = $this->getConsoleDB();
$consoleDB->deleteDocument('keys', $apiKey->getId());
}
protected function generateAPIKey(Document $project): Document
{
$consoleDB = $this->getConsoleDB();
$generatedSecret = bin2hex(\random_bytes(128));
$key = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'name' => 'Transfer API Key',
'scopes' => [
'users.read',
'users.write',
'teams.read',
'teams.write',
'databases.read',
'databases.write',
'collections.read',
'collections.write',
'documents.read',
'documents.write',
'buckets.read',
'buckets.write',
'files.read',
'files.write',
'functions.read',
'functions.write',
],
'expire' => null,
'sdks' => [],
'accessedAt' => null,
'secret' => $generatedSecret,
]);
$consoleDB->createDocument('keys', $key);
$consoleDB->deleteCachedDocument('projects', $project->getId());
return $key;
}
/**
* Process Migration
*
* @return void
*/
protected function processMigration(): void
{
/**
* @var Document $migrationDocument
* @var Transfer $transfer
*/
$migrationDocument = null;
$transfer = null;
$projectDocument = $this->getConsoleDB()->getDocument('projects', $this->args['project']['$id']);
$tempAPIKey = $this->generateAPIKey($projectDocument);
try {
$migrationDocument = $this->dbForProject->getDocument('migrations', $this->args['migration']['$id']);
$migrationDocument->setAttribute('stage', 'processing');
$migrationDocument->setAttribute('status', 'processing');
$this->updateMigrationDocument($migrationDocument, $projectDocument);
$source = $this->processSource($migrationDocument->getAttribute('source'), $migrationDocument->getAttribute('credentials'));
$source->report();
$destination = new DestinationsAppwrite(
$projectDocument->getId(),
'http://appwrite/v1',
$tempAPIKey['secret'],
);
$transfer = new Transfer(
$source,
$destination
);
$migrationDocument->setAttribute('stage', 'source-check');
$this->updateMigrationDocument($migrationDocument, $projectDocument);
$migrationDocument->setAttribute('stage', 'destination-check');
$this->updateMigrationDocument($migrationDocument, $projectDocument);
/** Start Transfer */
$migrationDocument->setAttribute('stage', 'migrating');
$this->updateMigrationDocument($migrationDocument, $projectDocument);
$transfer->run($migrationDocument->getAttribute('resources'), function () use ($migrationDocument, $transfer, $projectDocument) {
$migrationDocument->setAttribute('resourceData', json_encode($transfer->getCache()));
$migrationDocument->setAttribute('statusCounters', json_encode($transfer->getStatusCounters()));
$this->updateMigrationDocument($migrationDocument, $projectDocument);
});
$errors = $transfer->getReport(Resource::STATUS_ERROR);
if (count($errors) > 0) {
$migrationDocument->setAttribute('status', 'failed');
$migrationDocument->setAttribute('stage', 'finished');
$errorMessages = [];
foreach ($errors as $error) {
$errorMessages[] = "Failed to transfer resource '{$error['id']}:{$error['resource']}' with message '{$error['message']}'";
}
$migrationDocument->setAttribute('errors', $errorMessages);
$this->updateMigrationDocument($migrationDocument, $projectDocument);
return;
}
$migrationDocument->setAttribute('status', 'completed');
$migrationDocument->setAttribute('stage', 'finished');
} catch (\Throwable $th) {
Console::error($th->getMessage());
if ($migrationDocument) {
Console::error($th->getMessage());
Console::error($th->getTraceAsString());
$migrationDocument->setAttribute('status', 'failed');
$migrationDocument->setAttribute('stage', 'finished');
$migrationDocument->setAttribute('errors', [$th->getMessage()]);
return;
}
if ($transfer) {
$errors = $transfer->getReport(Resource::STATUS_ERROR);
if (count($errors) > 0) {
$migrationDocument->setAttribute('status', 'failed');
$migrationDocument->setAttribute('stage', 'finished');
$migrationDocument->setAttribute('errors', $errors);
}
}
} finally {
if ($migrationDocument) {
$this->updateMigrationDocument($migrationDocument, $projectDocument);
}
if ($tempAPIKey) {
$this->removeAPIKey($tempAPIKey);
}
}
}
/**
* Process Verification
*
* @return void
*/
protected function processVerification(): void
{
}
public function shutdown(): void
{
}
}

10
bin/worker-migrations Normal file
View file

@ -0,0 +1,10 @@
#!/bin/sh
if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
INTERVAL=0.1 QUEUE='v1-migrations' APP_INCLUDE='/usr/src/code/app/workers/migrations.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php

View file

@ -62,6 +62,7 @@
"utopia-php/storage": "0.14.*",
"utopia-php/swoole": "0.8.*",
"utopia-php/websocket": "0.1.*",
"utopia-php/transfer": "dev-feat-improve-features",
"resque/php-resque": "1.3.6",
"matomo/device-detector": "6.1.*",
"dragonmantank/cron-expression": "3.3.2",
@ -76,6 +77,10 @@
{
"url": "https://github.com/appwrite/runtimes.git",
"type": "git"
},
{
"url": "https://github.com/utopia-php/transfer.git",
"type": "vcs"
}
],
"require-dev": {

177
composer.lock generated
View file

@ -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": "acb2fc370254dfd67f7d44aa1fa0e694",
"content-hash": "ad6ca93acb312945f1f0edc58c8002ec",
"packages": [
{
"name": "adhocore/jwt",
@ -63,6 +63,47 @@
],
"time": "2021-02-20T09:56:44+00:00"
},
{
"name": "appwrite/appwrite",
"version": "8.0.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
"reference": "2b9e966edf35c4061179ed98ea364698ab30de8b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/2b9e966edf35c4061179ed98ea364698ab30de8b",
"reference": "2b9e966edf35c4061179ed98ea364698ab30de8b",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"php": ">=7.1.0"
},
"require-dev": {
"phpunit/phpunit": "3.7.35"
},
"type": "library",
"autoload": {
"psr-4": {
"Appwrite\\": "src/Appwrite"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API",
"support": {
"email": "team@appwrite.io",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
"source": "https://github.com/appwrite/sdk-for-php/tree/8.0.0",
"url": "https://appwrite.io/support"
},
"time": "2023-04-12T10:16:28+00:00"
},
{
"name": "appwrite/php-clamav",
"version": "2.0.0",
@ -607,16 +648,16 @@
},
{
"name": "guzzlehttp/promises",
"version": "2.0.0",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6"
"reference": "111166291a0f8130081195ac4556a5587d7f1b5d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/3a494dc7dc1d7d12e511890177ae2d0e6c107da6",
"reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6",
"url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d",
"reference": "111166291a0f8130081195ac4556a5587d7f1b5d",
"shasum": ""
},
"require": {
@ -670,7 +711,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.0.0"
"source": "https://github.com/guzzle/promises/tree/2.0.1"
},
"funding": [
{
@ -686,20 +727,20 @@
"type": "tidelift"
}
],
"time": "2023-05-21T13:50:22+00:00"
"time": "2023-08-03T15:11:55+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.5.0",
"version": "2.6.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "b635f279edd83fc275f822a1188157ffea568ff6"
"reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6",
"reference": "b635f279edd83fc275f822a1188157ffea568ff6",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/8bd7c33a0734ae1c5d074360512beb716bef3f77",
"reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77",
"shasum": ""
},
"require": {
@ -786,7 +827,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.5.0"
"source": "https://github.com/guzzle/psr7/tree/2.6.0"
},
"funding": [
{
@ -802,7 +843,7 @@
"type": "tidelift"
}
],
"time": "2023-04-17T16:11:26+00:00"
"time": "2023-08-03T15:06:02+00:00"
},
{
"name": "influxdb/influxdb-php",
@ -994,16 +1035,16 @@
},
{
"name": "matomo/device-detector",
"version": "6.1.3",
"version": "6.1.4",
"source": {
"type": "git",
"url": "https://github.com/matomo-org/device-detector.git",
"reference": "3e0fac7e77f3faadc3858fea9f5fa7efeb9cf239"
"reference": "74f6c4f6732b3ad6cdf25560746841d522969112"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/matomo-org/device-detector/zipball/3e0fac7e77f3faadc3858fea9f5fa7efeb9cf239",
"reference": "3e0fac7e77f3faadc3858fea9f5fa7efeb9cf239",
"url": "https://api.github.com/repos/matomo-org/device-detector/zipball/74f6c4f6732b3ad6cdf25560746841d522969112",
"reference": "74f6c4f6732b3ad6cdf25560746841d522969112",
"shasum": ""
},
"require": {
@ -1059,7 +1100,7 @@
"source": "https://github.com/matomo-org/matomo",
"wiki": "https://dev.matomo.org/"
},
"time": "2023-06-06T11:58:07+00:00"
"time": "2023-08-02T08:48:53+00:00"
},
{
"name": "mongodb/mongodb",
@ -2846,6 +2887,76 @@
},
"time": "2022-11-07T13:51:59+00:00"
},
{
"name": "utopia-php/transfer",
"version": "dev-feat-improve-features",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/transfer.git",
"reference": "7b5fe7059a6e23f2a59a448d79ffb978fa3cc60a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/transfer/zipball/7b5fe7059a6e23f2a59a448d79ffb978fa3cc60a",
"reference": "7b5fe7059a6e23f2a59a448d79ffb978fa3cc60a",
"shasum": ""
},
"require": {
"appwrite/appwrite": "^8.0",
"php": ">=8.0",
"utopia-php/cli": "^0.13.0"
},
"require-dev": {
"laravel/pint": "^1.10",
"phpunit/phpunit": "^9.3",
"vlucas/phpdotenv": "^5.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Utopia\\Transfer\\": "src/Transfer"
}
},
"autoload-dev": {
"psr-4": {
"Utopia\\Tests\\": "tests/Transfer"
}
},
"scripts": {
"lint": [
"./vendor/bin/pint --test"
],
"format": [
"./vendor/bin/pint"
]
},
"license": [
"MIT"
],
"authors": [
{
"name": "Eldad Fux",
"email": "eldad@appwrite.io"
},
{
"name": "Bradley Schofield",
"email": "bradley@appwrite.io"
}
],
"description": "A simple library to transfer resources between services.",
"keywords": [
"framework",
"php",
"transfer",
"upf",
"utopia"
],
"support": {
"source": "https://github.com/utopia-php/transfer/tree/feat-improve-features",
"issues": "https://github.com/utopia-php/transfer/issues"
},
"time": "2023-08-04T16:09:30+00:00"
},
{
"name": "utopia-php/websocket",
"version": "0.1.0",
@ -3785,16 +3896,16 @@
},
{
"name": "phpstan/phpdoc-parser",
"version": "1.23.0",
"version": "1.23.1",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "a2b24135c35852b348894320d47b3902a94bc494"
"reference": "846ae76eef31c6d7790fac9bc399ecee45160b26"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a2b24135c35852b348894320d47b3902a94bc494",
"reference": "a2b24135c35852b348894320d47b3902a94bc494",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/846ae76eef31c6d7790fac9bc399ecee45160b26",
"reference": "846ae76eef31c6d7790fac9bc399ecee45160b26",
"shasum": ""
},
"require": {
@ -3826,9 +3937,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.23.0"
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.23.1"
},
"time": "2023-07-23T22:17:56+00:00"
"time": "2023-08-03T16:32:59+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -4758,16 +4869,16 @@
},
{
"name": "sebastian/global-state",
"version": "5.0.5",
"version": "5.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
"reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2"
"reference": "bde739e7565280bda77be70044ac1047bc007e34"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2",
"reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34",
"reference": "bde739e7565280bda77be70044ac1047bc007e34",
"shasum": ""
},
"require": {
@ -4810,7 +4921,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/global-state/issues",
"source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5"
"source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6"
},
"funding": [
{
@ -4818,7 +4929,7 @@
"type": "github"
}
],
"time": "2022-02-14T08:28:10+00:00"
"time": "2023-08-02T09:26:13+00:00"
},
{
"name": "sebastian/lines-of-code",
@ -5643,7 +5754,9 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"utopia-php/transfer": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@ -5667,5 +5780,5 @@
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.1.0"
}

View file

@ -576,6 +576,39 @@ services:
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-migrations:
entrypoint: worker-migrations
<<: *x-logging
container_name: appwrite-worker-migrations
restart: unless-stopped
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./tests:/usr/src/code/tests
- ./vendor:/usr/src/code/tests
depends_on:
- mariadb
environment:
- _APP_ENV
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-maintenance:
entrypoint: maintenance
<<: *x-logging

View file

@ -0,0 +1,217 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
class Firebase extends OAuth2
{
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @var array
*/
protected array $scopes = [
'https://www.googleapis.com/auth/firebase',
'https://www.googleapis.com/auth/datastore',
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/identitytoolkit',
'https://www.googleapis.com/auth/userinfo.profile'
];
/**
* @return string
*/
public function getName(): string
{
return 'firebase';
}
/**
* @return string
*/
public function getLoginURL(): string
{
return 'https://accounts.google.com/o/oauth2/v2/auth?' . \http_build_query([
'access_type' => 'offline',
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => \json_encode($this->state),
'response_type' => 'code',
'prompt' => 'consent',
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$response = $this->request(
'POST',
'https://oauth2.googleapis.com/token',
[],
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code,
'grant_type' => 'authorization_code'
])
);
$this->tokens = \json_decode($response, true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$response = $this->request(
'POST',
'https://github.com/login/oauth/access_token',
[],
\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
])
);
$output = [];
\parse_str($response, $output);
$this->tokens = $output;
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserID(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['id'] ?? '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['email'] ?? '';
}
/**
* Check if the OAuth email is verified
*
* @link https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
*
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
$user = $this->getUser($accessToken);
if ($user['verified'] ?? false) {
return true;
}
return false;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['name'] ?? '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken)
{
if (empty($this->user)) {
$response = $this->request(
'GET',
'https://www.googleapis.com/oauth2/v1/userinfo',
['Authorization: Bearer ' . \urlencode($accessToken)]
);
$this->user = \json_decode($response, true);
}
return $this->user;
}
public function getProjects(string $accessToken): array
{
$projects = $this->request('GET', 'https://firebase.googleapis.com/v1beta1/projects', ['Authorization: Bearer ' . \urlencode($accessToken)]);
$projects = \json_decode($projects, true);
return $projects['results'];
}
public function createServiceAccount(string $accessToken, string $projectID): string
{
$response = $this->request(
'POST',
"https://iam.googleapis.com/v1/projects/{$projectID}/serviceAccounts",
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
],
json_encode([
'accountId' => 'appwrite-migrations',
'serviceAccount' => [
'displayName' => 'Appwrite Migrations'
]
])
);
return $response;
}
}

View file

@ -35,6 +35,9 @@ class Event
public const MESSAGING_QUEUE_NAME = 'v1-messaging';
public const MESSAGING_CLASS_NAME = 'MessagingV1';
public const MIGRATIONS_QUEUE_NAME = 'v1-migrations';
public const MIGRATIONS_CLASS_NAME = 'MigrationsV1';
protected string $queue = '';
protected string $class = '';
protected string $event = '';

View file

@ -0,0 +1,98 @@
<?php
namespace Appwrite\Event;
use DateTime;
use Resque;
use ResqueScheduler;
use Utopia\Database\Document;
class Migration extends Event
{
protected string $type = '';
protected ?Document $migration = null;
public function __construct()
{
parent::__construct(Event::MIGRATIONS_QUEUE_NAME, Event::MIGRATIONS_CLASS_NAME);
}
/**
* Sets migration document for the migration event.
*
* @param Document $migration
* @return self
*/
public function setMigration(Document $migration): self
{
$this->migration = $migration;
return $this;
}
/**
* Returns set migration document for the function event.
*
* @return null|Document
*/
public function getMigration(): ?Document
{
return $this->migration;
}
/**
* Sets migration type for the migration event.
*
* @param string $type
*
* @return self
*/
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
/**
* Returns set migration type for the migration event.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Executes the migration event and sends it to the migrations worker.
*
* @return string|bool
* @throws \InvalidArgumentException
*/
public function trigger(): string|bool
{
return Resque::enqueue($this->queue, $this->class, [
'project' => $this->project,
'user' => $this->user,
'migration' => $this->migration
]);
}
/**
* Schedules the migration event and schedules it in the migrations worker queue.
*
* @param \DateTime|int $at
* @return void
* @throws \Resque_Exception
* @throws \ResqueScheduler_InvalidTimestampException
*/
public function schedule(DateTime|int $at): void
{
ResqueScheduler::enqueueAt($at, $this->queue, $this->class, [
'project' => $this->project,
'user' => $this->user,
'migration' => $this->migration
]);
}
}

View file

@ -32,6 +32,7 @@ class Exception extends \Exception
* - Platform
* - Domain
* - GraphQL
* - Migrations
*/
/** General */
@ -199,6 +200,11 @@ class Exception extends \Exception
public const GRAPHQL_NO_QUERY = 'graphql_no_query';
public const GRAPHQL_TOO_MANY_QUERIES = 'graphql_too_many_queries';
/** Migrations */
public const MIGRATION_NOT_FOUND = 'migration_not_found';
public const MIGRATION_ALREADY_EXISTS = 'migration_already_exists';
public const MIGRATION_IN_PROGRESS = 'migration_in_progress';
protected $type = '';
public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int $code = null, \Throwable $previous = null)

View file

@ -0,0 +1,25 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Migrations extends Base
{
public const ALLOWED_ATTRIBUTES = [
'status',
'stage',
'source',
'resources',
'statusCounters',
'resourceData',
'errors'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('migrations', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -88,6 +88,9 @@ use Appwrite\Utopia\Response\Model\UsageProject;
use Appwrite\Utopia\Response\Model\UsageStorage;
use Appwrite\Utopia\Response\Model\UsageUsers;
use Appwrite\Utopia\Response\Model\Variable;
use Appwrite\Utopia\Response\Model\Migration;
use Appwrite\Utopia\Response\Model\MigrationFirebaseProject;
use Appwrite\Utopia\Response\Model\MigrationReport;
/**
* @method int getStatusCode()
@ -198,6 +201,13 @@ class Response extends SwooleResponse
public const MODEL_BUILD_LIST = 'buildList'; // Not used anywhere yet
public const MODEL_FUNC_PERMISSIONS = 'funcPermissions';
// Migrations
public const MODEL_MIGRATION = 'migration';
public const MODEL_MIGRATION_LIST = 'migrationList';
public const MODEL_MIGRATION_REPORT = 'migrationReport';
public const MODEL_MIGRATION_FIREBASE_PROJECT = 'firebaseProject';
public const MODEL_MIGRATION_FIREBASE_PROJECT_LIST = 'firebaseProjectList';
// Project
public const MODEL_PROJECT = 'project';
public const MODEL_PROJECT_LIST = 'projectList';
@ -288,6 +298,8 @@ class Response extends SwooleResponse
->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false))
->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE))
->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE))
->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION))
->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT))
// Entities
->setModel(new Database())
->setModel(new Collection())
@ -360,6 +372,9 @@ class Response extends SwooleResponse
->setModel(new TemplateSMS())
->setModel(new TemplateEmail())
->setModel(new ConsoleVariables())
->setModel(new Migration())
->setModel(new MigrationReport())
->setModel(new MigrationFirebaseProject())
// Verification
// Recovery
// Tests (keep last)

View file

@ -0,0 +1,96 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Migration extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Migration ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Variable creation date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Variable creation date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('status', [
'type' => self::TYPE_STRING,
'description' => 'Migration status ( pending, processing, failed. completed ) ',
'default' => '',
'example' => 'pending',
])
->addRule('stage', [
'type' => self::TYPE_STRING,
'description' => 'Migration stage ( init, processing, source-check, destination-check, migrating, finished )',
'default' => '',
'example' => 'init',
])
->addRule('source', [
'type' => self::TYPE_STRING,
'description' => 'A string containing the type of source of the migration.',
'default' => '',
'example' => 'Appwrite',
])
->addRule('resources', [
'type' => self::TYPE_STRING,
'description' => 'Resources to migration.',
'default' => [],
'example' => ['user'],
'array' => true
])
->addRule('statusCounters', [
'type' => self::TYPE_JSON,
'description' => 'A group of counters that represent the total progress of the migration.',
'default' => [],
'example' => '{"Database": {"PENDING": 0, "SUCCESS": 1, "ERROR": 0, "SKIP": 0, "PROCESSING": 0, "WARNING": 0}}',
])
->addRule('resourceData', [
'type' => self::TYPE_JSON,
'description' => 'An array of objects containing the report data of the resources that were migrated.',
'default' => [],
'example' => '[{"resource":"Database","id":"public","status":"SUCCESS","message":""}]',
])
->addRule('errors', [
'type' => self::TYPE_STRING,
'description' => 'All errors that occurred during the migration process.',
'default' => [],
'example' => [],
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Migration';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_MIGRATION;
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class MigrationFirebaseProject extends Model
{
public function __construct()
{
$this
->addRule('projectId', [
'type' => self::TYPE_STRING,
'description' => 'Project ID.',
'default' => '',
'example' => 'my-project',
])
->addRule('displayName', [
'type' => self::TYPE_STRING,
'description' => 'Project display name.',
'default' => '',
'example' => 'My Project',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'MigrationFirebaseProject';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_MIGRATION_FIREBASE_PROJECT;
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Transfer\Resource;
class MigrationReport extends Model
{
public function __construct()
{
$this
->addRule(Resource::TYPE_USER, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of users to be migrated.',
'default' => 0,
'example' => 20,
])
->addRule(Resource::TYPE_TEAM, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of teams to be migrated.',
'default' => 0,
'example' => 20,
])
->addRule(Resource::TYPE_DATABASE, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of databases to be migrated.',
'default' => 0,
'example' => 20,
])
->addRule(Resource::TYPE_DOCUMENT, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of documents to be migrated.',
'default' => 0,
'example' => 20,
])
->addRule(Resource::TYPE_FILE, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of files to be migrated.',
'default' => 0,
'example' => 20,
])
->addRule(Resource::TYPE_BUCKET, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of buckets to be migrated.',
'default' => 0,
'example' => 20,
])
->addRule(Resource::TYPE_FUNCTION, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of functions to be migrated.',
'default' => 0,
'example' => 20,
])
->addRule('size', [
'type' => self::TYPE_INTEGER,
'description' => 'Size of files to be migrated in mb.',
'default' => 0,
'example' => 30000,
])
->addRule('version', [
'type' => self::TYPE_STRING,
'description' => 'Version of the Appwrite instance to be migrated.',
'default' => '',
'example' => '1.4.0',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Migration Report';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_MIGRATION_REPORT;
}
}