Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"ext-curl": "*",
"ext-openssl": "*",

"appwrite/appwrite": "23.*",
"appwrite/appwrite": "24.*",
"utopia-php/database": "5.*",
"utopia-php/storage": "2.*",
"utopia-php/dsn": "0.2.*",
Expand Down
14 changes: 7 additions & 7 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

201 changes: 192 additions & 9 deletions src/Migration/Destinations/Appwrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
use Appwrite\AppwriteException;
use Appwrite\Client;
use Appwrite\Enums\Adapter;
use Appwrite\Enums\ProjectAuthMethodId;
use Appwrite\Enums\BuildRuntime;
use Appwrite\Enums\Compression;
use Appwrite\Enums\Framework;
use Appwrite\Enums\PasswordHash;
use Appwrite\Enums\ProjectProtocolId;
use Appwrite\Enums\Runtime;
use Appwrite\Enums\ProjectServiceId;
use Appwrite\Enums\SmtpEncryption;
use Appwrite\InputFile;
use Appwrite\Services\Functions;
use Appwrite\Services\Messaging;
use Appwrite\Services\Project;
use Appwrite\Services\Sites;
use Appwrite\Services\Storage;
use Appwrite\Services\Teams;
Expand All @@ -37,8 +41,10 @@
use Utopia\Migration\Destination;
use Utopia\Migration\Exception;
use Utopia\Migration\Resource;
use Utopia\Migration\Resources\Auth\AuthMethods;
use Utopia\Migration\Resources\Auth\Hash;
use Utopia\Migration\Resources\Auth\Membership;
use Utopia\Migration\Resources\Auth\Policies;
use Utopia\Migration\Resources\Auth\Team;
use Utopia\Migration\Resources\Auth\User;
use Utopia\Migration\Resources\Database\Attribute;
Expand All @@ -56,7 +62,10 @@
use Utopia\Migration\Resources\Messaging\Provider;
use Utopia\Migration\Resources\Messaging\Subscriber;
use Utopia\Migration\Resources\Messaging\Topic;
use Utopia\Migration\Resources\Settings\Labels;
use Utopia\Migration\Resources\Settings\ProjectVariable;
use Utopia\Migration\Resources\Settings\Protocols;
use Utopia\Migration\Resources\Settings\Services as ServicesResource;
use Utopia\Migration\Resources\Settings\Webhook;
use Utopia\Migration\Resources\Sites\Deployment as SiteDeployment;
use Utopia\Migration\Resources\Sites\EnvVar as SiteEnvVar;
Expand Down Expand Up @@ -91,12 +100,13 @@ class Appwrite extends Destination
];

protected Client $client;
protected string $project;
protected string $projectId;

protected string $key;

private Functions $functions;
private Messaging $messaging;
private Project $project;
private Sites $sites;
private Storage $storage;
private Teams $teams;
Expand Down Expand Up @@ -170,7 +180,7 @@ public function __construct(
protected OnDuplicate $onDuplicate = OnDuplicate::Fail,
?callable $getDatabaseDSN = null,
) {
$this->project = $project;
$this->projectId = $project;
$this->endpoint = $endpoint;
$this->key = $key;

Expand All @@ -181,6 +191,7 @@ public function __construct(

$this->functions = new Functions($this->client);
$this->messaging = new Messaging($this->client);
$this->project = new Project($this->client);
$this->sites = new Sites($this->client);
$this->storage = new Storage($this->client);
$this->teams = new Teams($this->client);
Expand Down Expand Up @@ -241,6 +252,8 @@ public static function getSupportedResources(): array
Resource::TYPE_USER,
Resource::TYPE_TEAM,
Resource::TYPE_MEMBERSHIP,
Resource::TYPE_AUTH_METHODS,
Resource::TYPE_POLICIES,

// Database
Resource::TYPE_DATABASE,
Expand Down Expand Up @@ -283,6 +296,9 @@ public static function getSupportedResources(): array
// Settings
Resource::TYPE_PROJECT_VARIABLE,
Resource::TYPE_WEBHOOK,
Resource::TYPE_PROTOCOLS,
Resource::TYPE_LABELS,
Resource::TYPE_SERVICES,

// Backups
Resource::TYPE_BACKUP_POLICY,
Expand Down Expand Up @@ -2160,6 +2176,14 @@ public function importAuthResource(Resource $resource): Resource
userId: $user->getId(),
);
break;
case Resource::TYPE_AUTH_METHODS:
/** @var AuthMethods $resource */
$this->createAuthMethods($resource);
break;
case Resource::TYPE_POLICIES:
/** @var Policies $resource */
$this->createPolicies($resource);
break;
}

$resource->setStatus(Resource::STATUS_SUCCESS);
Expand Down Expand Up @@ -3107,6 +3131,18 @@ public function importSettingsResource(Resource $resource): Resource
/** @var Webhook $resource */
$this->createWebhook($resource);
break;
case Resource::TYPE_PROTOCOLS:
/** @var Protocols $resource */
$this->createProtocols($resource);
break;
case Resource::TYPE_LABELS:
/** @var Labels $resource */
$this->createLabels($resource);
break;
case Resource::TYPE_SERVICES:
/** @var ServicesResource $resource */
$this->createServices($resource);
break;
}

if ($resource->getStatus() !== Resource::STATUS_SKIPPED) {
Expand Down Expand Up @@ -3155,6 +3191,71 @@ protected function createProjectVariable(ProjectVariable $resource): bool
return true;
}

/**
* Flip each protocol on the destination via the SDK. Three single-field
* updates rather than one bulk write — the SDK only exposes per-protocol
* setters, mirroring upstream's per-flag controllers.
*/
protected function createProtocols(Protocols $resource): bool
{
$flags = [
[ProjectProtocolId::REST(), $resource->getRest()],
[ProjectProtocolId::GRAPHQL(), $resource->getGraphql()],
[ProjectProtocolId::WEBSOCKET(), $resource->getWebsocket()],
];

foreach ($flags as [$protocol, $enabled]) {
$this->project->updateProtocol($protocol, $enabled);
}

return true;
}

/**
* Overwrite destination labels with the source array. Project::updateLabels
* is a wholesale replace, so the source's array is authoritative.
*/
protected function createLabels(Labels $resource): bool
{
$this->project->updateLabels($resource->getLabels());

return true;
}

/**
* Flip each service flag on the destination via the SDK. 17 single-field
* updates rather than one bulk write — the SDK only exposes per-service
* setters, matching upstream's per-flag controller.
*/
protected function createServices(ServicesResource $resource): bool
{
$flags = [
[ProjectServiceId::ACCOUNT(), $resource->getAccount()],
[ProjectServiceId::AVATARS(), $resource->getAvatars()],
[ProjectServiceId::DATABASES(), $resource->getDatabases()],
[ProjectServiceId::TABLESDB(), $resource->getTablesdb()],
[ProjectServiceId::LOCALE(), $resource->getLocale()],
[ProjectServiceId::HEALTH(), $resource->getHealth()],
[ProjectServiceId::PROJECT(), $resource->getProject()],
[ProjectServiceId::STORAGE(), $resource->getStorage()],
[ProjectServiceId::TEAMS(), $resource->getTeams()],
[ProjectServiceId::USERS(), $resource->getUsers()],
[ProjectServiceId::VCS(), $resource->getVcs()],
[ProjectServiceId::SITES(), $resource->getSites()],
[ProjectServiceId::FUNCTIONS(), $resource->getFunctions()],
[ProjectServiceId::PROXY(), $resource->getProxy()],
[ProjectServiceId::GRAPHQL(), $resource->getGraphql()],
[ProjectServiceId::MIGRATIONS(), $resource->getMigrations()],
[ProjectServiceId::MESSAGING(), $resource->getMessaging()],
];

foreach ($flags as [$service, $enabled]) {
$this->project->updateService($service, $enabled);
}

return true;
}

protected function createWebhook(Webhook $resource): bool
{
$existing = $this->dbForPlatform->findOne('webhooks', [
Expand All @@ -3175,7 +3276,7 @@ protected function createWebhook(Webhook $resource): bool
'$id' => ID::unique(),
'$permissions' => $resource->getPermissions(),
'projectInternalId' => $this->projectInternalId,
'projectId' => $this->project,
'projectId' => $this->projectId,
'name' => $resource->getWebhookName(),
'events' => $resource->getEvents(),
'url' => $resource->getUrl(),
Expand All @@ -3194,7 +3295,7 @@ protected function createWebhook(Webhook $resource): bool
return false;
}

$this->dbForPlatform->purgeCachedDocument('projects', $this->project);
$this->dbForPlatform->purgeCachedDocument('projects', $this->projectId);

return true;
}
Expand All @@ -3205,7 +3306,7 @@ protected function createWebhook(Webhook $resource): bool
protected function createPlatform(Platform $resource): bool
{
$existing = $this->dbForPlatform->findOne('platforms', [
Query::equal('projectId', [$this->project]),
Query::equal('projectId', [$this->projectId]),
Query::equal('type', [$resource->getType()]),
Query::equal('name', [$resource->getPlatformName()]),
]);
Expand All @@ -3223,7 +3324,7 @@ protected function createPlatform(Platform $resource): bool
'$id' => ID::unique(),
'$permissions' => $resource->getPermissions(),
'projectInternalId' => $this->projectInternalId,
'projectId' => $this->project,
'projectId' => $this->projectId,
'type' => $resource->getType(),
'name' => $resource->getPlatformName(),
'key' => $resource->getKey(),
Expand All @@ -3237,7 +3338,89 @@ protected function createPlatform(Platform $resource): bool
return false;
}

$this->dbForPlatform->purgeCachedDocument('projects', $this->project);
$this->dbForPlatform->purgeCachedDocument('projects', $this->projectId);

return true;
}

/**
* Flip each auth-method flag on the destination project via the SDK. Seven
* single-field updates rather than one bulk write — the SDK only exposes
* per-flag setters, and the destination needs to honor any server-side
* validation per flag (e.g. provider-specific guards).
*/
protected function createAuthMethods(AuthMethods $resource): bool
{
$flags = [
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
[ProjectAuthMethodId::EMAILPASSWORD(), $resource->getEmailPassword()],
[ProjectAuthMethodId::MAGICURL(), $resource->getMagicURL()],
[ProjectAuthMethodId::EMAILOTP(), $resource->getEmailOtp()],
[ProjectAuthMethodId::ANONYMOUS(), $resource->getAnonymous()],
[ProjectAuthMethodId::INVITES(), $resource->getInvites()],
[ProjectAuthMethodId::JWT(), $resource->getJwt()],
[ProjectAuthMethodId::PHONE(), $resource->getPhone()],
];

foreach ($flags as [$method, $enabled]) {
$this->project->updateAuthMethod($method, $enabled);
}

return true;
}

/**
* Write all 9 policies into the project document's `auths` attribute in a
* single `updateDocument` call. Matches the convention used by Webhook /
* Platform / ProjectVariable / ApiKey destinations.
*
* Direct DB write — chosen over the SDK setter methods because:
* - SDK methods (updatePasswordHistoryPolicy, etc.) return the full
* Project response model. Cloud's response emits `billingLimits: {}`
* which crashes typed-SDK deserialization on the required `bandwidth`
* field, so every per-policy call would crash even though its write
* succeeded server-side.
* - PasswordHistory/UserLimit/SessionLimit endpoints reject `total: 0`
* even though `0` is the storage value for "disabled" (see response
* model docs). A round-trip of disabled state goes through 0 in
* storage but the validator only accepts 1-N or null.
*
* Going direct to the underlying document sidesteps both. The data shape
* matches what the per-policy controllers themselves write into `auths`.
*/
protected function createPolicies(Policies $resource): bool
{
$project = $this->dbForPlatform->getDocument('projects', $this->projectId);
$auths = $project->getAttribute('auths', []);

// Ints. Source's 0 = disabled; the same convention applies in storage.
$auths['passwordHistory'] = $resource->getPasswordHistory();
$auths['duration'] = $resource->getSessionDuration();
$auths['maxSessions'] = $resource->getSessionsLimit();
$auths['limit'] = $resource->getUserLimit();

// Booleans.
$auths['passwordDictionary'] = $resource->getPasswordDictionary();
$auths['personalDataCheck'] = $resource->getPersonalDataCheck();
$auths['sessionAlerts'] = $resource->getSessionAlerts();
$auths['invalidateSessions'] = $resource->getSessionInvalidation();

// Membership-privacy bundle.
$auths['membershipsUserId'] = $resource->getMembershipsUserId();
$auths['membershipsUserEmail'] = $resource->getMembershipsUserEmail();
$auths['membershipsUserName'] = $resource->getMembershipsUserName();
$auths['membershipsMfa'] = $resource->getMembershipsUserMfa();
$auths['membershipsUserPhone'] = $resource->getMembershipsUserPhone();

// The projects document is restricted to team-owner role on update;
// we're running in the migration worker which has no team context, so
// skip authorization the same way the upstream policy controllers do.
$this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument(
'projects',
$this->projectId,
new UtopiaDocument(['auths' => $auths]),
));

$this->dbForPlatform->purgeCachedDocument('projects', $this->projectId);

return true;
}
Expand All @@ -3264,7 +3447,7 @@ protected function createApiKey(ApiKey $resource): bool
'$id' => ID::unique(),
'$permissions' => $resource->getPermissions(),
'resourceInternalId' => $this->projectInternalId,
'resourceId' => $this->project,
'resourceId' => $this->projectId,
'resourceType' => 'projects',
'name' => $resource->getApiKeyName(),
'scopes' => $resource->getScopes(),
Expand All @@ -3280,7 +3463,7 @@ protected function createApiKey(ApiKey $resource): bool
return false;
}

$this->dbForPlatform->purgeCachedDocument('projects', $this->project);
$this->dbForPlatform->purgeCachedDocument('projects', $this->projectId);

return true;
}
Expand Down
Loading
Loading