Skip to content
Open
114 changes: 114 additions & 0 deletions src/Migration/Destinations/Appwrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
use Appwrite\Enums\Framework;
use Appwrite\Enums\PasswordHash;
use Appwrite\Enums\ProjectProtocolId;
use Appwrite\Enums\ProxyResourceType;
use Appwrite\Enums\Runtime;
use Appwrite\Enums\SmtpEncryption;
use Appwrite\Enums\StatusCode;
use Appwrite\InputFile;
use Appwrite\Services\Functions;
use Appwrite\Services\Messaging;
use Appwrite\Services\Project;
use Appwrite\Services\Proxy;
use Appwrite\Services\Sites;
use Appwrite\Services\Storage;
use Appwrite\Services\Teams;
Expand Down Expand Up @@ -51,6 +54,7 @@
use Utopia\Migration\Resources\Database\Index;
use Utopia\Migration\Resources\Database\Row;
use Utopia\Migration\Resources\Database\Table;
use Utopia\Migration\Resources\Domains\Rule;
use Utopia\Migration\Resources\Functions\Deployment;
use Utopia\Migration\Resources\Functions\EnvVar;
use Utopia\Migration\Resources\Functions\Func;
Expand Down Expand Up @@ -106,6 +110,7 @@ class Appwrite extends Destination
private Functions $functions;
private Messaging $messaging;
private Project $project;
private Proxy $proxy;
private Sites $sites;
private Storage $storage;
private Teams $teams;
Expand Down Expand Up @@ -191,6 +196,7 @@ public function __construct(
$this->functions = new Functions($this->client);
$this->messaging = new Messaging($this->client);
$this->project = new Project($this->client);
$this->proxy = new Proxy($this->client);
$this->sites = new Sites($this->client);
$this->storage = new Storage($this->client);
$this->teams = new Teams($this->client);
Expand Down Expand Up @@ -302,6 +308,9 @@ public static function getSupportedResources(): array

// Backups
Resource::TYPE_BACKUP_POLICY,

// Domains
Resource::TYPE_RULE,
];
}

Expand Down Expand Up @@ -461,6 +470,7 @@ protected function import(array $resources, callable $callback): void
Transfer::GROUP_INTEGRATIONS => $this->importIntegrationsResource($resource),
Transfer::GROUP_BACKUPS => $this->importBackupResource($resource),
Transfer::GROUP_PROJECTS => $this->importProjectsResource($resource),
Transfer::GROUP_DOMAINS => $this->importDomainsResource($resource),
default => throw new \Exception('Invalid resource group', Exception::CODE_VALIDATION),
};
} catch (\Throwable $e) {
Expand Down Expand Up @@ -3156,6 +3166,25 @@ public function importProjectsResource(Resource $resource): Resource
return $resource;
}

public function importDomainsResource(Resource $resource): Resource
{
switch ($resource->getName()) {
case Resource::TYPE_RULE:
/** @var Rule $resource */
$success = $this->createRule($resource);
if (!$success) {
return $resource;
}
break;
}

if ($resource->getStatus() !== Resource::STATUS_SKIPPED) {
$resource->setStatus(Resource::STATUS_SUCCESS);
}

return $resource;
}

protected function createProjectVariable(ProjectVariable $resource): bool
{
$existing = $this->dbForProject->findOne('variables', [
Expand Down Expand Up @@ -3280,6 +3309,91 @@ protected function createSMTP(SMTP $resource): bool
return true;
}

/**
* Auto-generated rules (default `.appwrite.network` domains for functions/sites)
* are recreated automatically on the destination when the parent Function/Site
* is migrated, so only manual rules need to be imported.
*
* Function/site IDs are preserved across migration, so the source
* `deploymentResourceId` is passed through directly.
*/
protected function createRule(Rule $resource): bool
{
if ($resource->getTrigger() !== 'manual') {
$resource->setStatus(Resource::STATUS_SKIPPED, 'Auto-generated rule, recreated by parent resource migration');
return false;
}

$type = $resource->getType();
$deploymentResourceType = $resource->getDeploymentResourceType();
$branch = $resource->getDeploymentVcsProviderBranch();

try {
switch ($type) {
case 'api':
$this->proxy->createAPIRule($resource->getDomain());
break;

case 'redirect':
$statusCode = match ($resource->getRedirectStatusCode()) {
301 => StatusCode::MOVEDPERMANENTLY301(),
302 => StatusCode::FOUND302(),
307 => StatusCode::TEMPORARYREDIRECT307(),
308 => StatusCode::PERMANENTREDIRECT308(),
default => StatusCode::MOVEDPERMANENTLY301(),
};

$resourceType = $deploymentResourceType === 'site'
? ProxyResourceType::SITE()
: ProxyResourceType::FUNCTIONMODEL();

$this->proxy->createRedirectRule(
$resource->getDomain(),
$resource->getRedirectUrl(),
$statusCode,
$resource->getDeploymentResourceId(),
$resourceType,
);
break;

case 'deployment':
if ($deploymentResourceType === 'function') {
$this->proxy->createFunctionRule(
$resource->getDomain(),
$resource->getDeploymentResourceId(),
$branch !== '' ? $branch : null,
);
} elseif ($deploymentResourceType === 'site') {
$this->proxy->createSiteRule(
$resource->getDomain(),
$resource->getDeploymentResourceId(),
$branch !== '' ? $branch : null,
);
} else {
$resource->setStatus(Resource::STATUS_SKIPPED, 'Unsupported deployment resource type "' . $deploymentResourceType . '"');
return false;
}
break;

default:
$resource->setStatus(Resource::STATUS_SKIPPED, 'Unsupported rule type "' . $type . '"');
return false;
}
} catch (AppwriteException $e) {
// 409 means the domain is owned by another project/organization — the
// user has to release it there before re-running. Surface as a warning,
// not an error, so the rest of the migration continues.
if ($e->getCode() === 409) {
$resource->setStatus(Resource::STATUS_WARNING, 'Domain "' . $resource->getDomain() . '" is owned by another project. Remove it there and re-run the migration.');
return false;
}

throw $e;
}

return true;
}

protected function createWebhook(Webhook $resource): bool
{
$existing = $this->dbForPlatform->findOne('webhooks', [
Expand Down
4 changes: 4 additions & 0 deletions src/Migration/Resource.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ abstract class Resource implements \JsonSerializable
public const TYPE_PROJECT_LABELS = 'project-labels';
public const TYPE_PROJECT_SERVICES = 'project-services';

// Domains
public const TYPE_RULE = 'rule';

// Messaging
public const TYPE_SUBSCRIBER = 'subscriber';
public const TYPE_MESSAGE = 'message';
Expand Down Expand Up @@ -135,6 +138,7 @@ abstract class Resource implements \JsonSerializable
self::TYPE_PROJECT_PROTOCOLS,
self::TYPE_PROJECT_LABELS,
self::TYPE_PROJECT_SERVICES,
self::TYPE_RULE,
self::TYPE_PROVIDER,
self::TYPE_TOPIC,
self::TYPE_SUBSCRIBER,
Expand Down
117 changes: 117 additions & 0 deletions src/Migration/Resources/Domains/Rule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace Utopia\Migration\Resources\Domains;

use Utopia\Migration\Resource;
use Utopia\Migration\Transfer;

class Rule extends Resource
{
public function __construct(
string $id,
private readonly string $domain,
private readonly string $type,
private readonly string $trigger,
private readonly string $redirectUrl = '',
private readonly int $redirectStatusCode = 0,
private readonly string $deploymentResourceType = '',
private readonly string $deploymentResourceId = '',
private readonly string $deploymentVcsProviderBranch = '',
string $createdAt = '',
string $updatedAt = '',
) {
$this->id = $id;
$this->createdAt = $createdAt;
$this->updatedAt = $updatedAt;
}

/**
* @param array<string, mixed> $array
*/
public static function fromArray(array $array): self
{
return new self(
$array['id'],
$array['domain'],
$array['type'],
$array['trigger'] ?? 'manual',
(string) ($array['redirectUrl'] ?? ''),
(int) ($array['redirectStatusCode'] ?? 0),
(string) ($array['deploymentResourceType'] ?? ''),
(string) ($array['deploymentResourceId'] ?? ''),
(string) ($array['deploymentVcsProviderBranch'] ?? ''),
createdAt: $array['createdAt'] ?? '',
updatedAt: $array['updatedAt'] ?? '',
);
}

/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'domain' => $this->domain,
'type' => $this->type,
'trigger' => $this->trigger,
'redirectUrl' => $this->redirectUrl,
'redirectStatusCode' => $this->redirectStatusCode,
'deploymentResourceType' => $this->deploymentResourceType,
'deploymentResourceId' => $this->deploymentResourceId,
'deploymentVcsProviderBranch' => $this->deploymentVcsProviderBranch,
'createdAt' => $this->createdAt,
'updatedAt' => $this->updatedAt,
];
}

public static function getName(): string
{
return Resource::TYPE_RULE;
}

public function getGroup(): string
{
return Transfer::GROUP_DOMAINS;
}

public function getDomain(): string
{
return $this->domain;
}

public function getType(): string
{
return $this->type;
}

public function getTrigger(): string
{
return $this->trigger;
}

public function getRedirectUrl(): string
{
return $this->redirectUrl;
}

public function getRedirectStatusCode(): int
{
return $this->redirectStatusCode;
}

public function getDeploymentResourceType(): string
{
return $this->deploymentResourceType;
}

public function getDeploymentResourceId(): string
{
return $this->deploymentResourceId;
}

public function getDeploymentVcsProviderBranch(): string
{
return $this->deploymentVcsProviderBranch;
}
}
17 changes: 17 additions & 0 deletions src/Migration/Source.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public function getProjectsBatchSize(): int
return static::$defaultBatchSize;
}

public function getDomainsBatchSize(): int
{
return static::$defaultBatchSize;
}

/**
* @param array<Resource> $resources
* @return void
Expand Down Expand Up @@ -127,6 +132,7 @@ public function exportResources(array $resources): void
Transfer::GROUP_INTEGRATIONS => Transfer::GROUP_INTEGRATIONS_RESOURCES,
Transfer::GROUP_BACKUPS => Transfer::GROUP_BACKUPS_RESOURCES,
Transfer::GROUP_PROJECTS => Transfer::GROUP_PROJECTS_RESOURCES,
Transfer::GROUP_DOMAINS => Transfer::GROUP_DOMAINS_RESOURCES,
];

foreach ($mapping as $group => $resources) {
Expand Down Expand Up @@ -170,6 +176,9 @@ public function exportResources(array $resources): void
case Transfer::GROUP_PROJECTS:
$this->exportGroupProjects($this->getProjectsBatchSize(), $resources);
break;
case Transfer::GROUP_DOMAINS:
$this->exportGroupDomains($this->getDomainsBatchSize(), $resources);
break;
}
}
}
Expand Down Expand Up @@ -245,4 +254,12 @@ abstract protected function exportGroupBackups(int $batchSize, array $resources)
* @param array<string> $resources Resources to export
*/
abstract protected function exportGroupProjects(int $batchSize, array $resources): void;

/**
* Export Domains Group
*
* @param int $batchSize
* @param array<string> $resources Resources to export
*/
abstract protected function exportGroupDomains(int $batchSize, array $resources): void;
}
Loading
Loading