diff --git a/composer.json b/composer.json index 6bd64041..5dc3f557 100644 --- a/composer.json +++ b/composer.json @@ -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.*", diff --git a/composer.lock b/composer.lock index ab0a68c7..d4a75bb0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "44746ecb1183e23d963fc90b1481541a", + "content-hash": "d500f176f703c97758a1fc200f0ace55", "packages": [ { "name": "appwrite/appwrite", - "version": "23.1.0", + "version": "24.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "2f275921f10ceb7cff99f2d463f7328b296234fa" + "reference": "dcb3550a3332de1c1665a015a09e9c73ff515e4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/2f275921f10ceb7cff99f2d463f7328b296234fa", - "reference": "2f275921f10ceb7cff99f2d463f7328b296234fa", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/dcb3550a3332de1c1665a015a09e9c73ff515e4f", + "reference": "dcb3550a3332de1c1665a015a09e9c73ff515e4f", "shasum": "" }, "require": { @@ -43,10 +43,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/23.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/24.1.0", "url": "https://appwrite.io/support" }, - "time": "2026-05-08T13:44:58+00:00" + "time": "2026-05-20T09:37:03+00:00" }, { "name": "brick/math", diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 90f596ee..7af35c2d 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -9,11 +9,14 @@ use Appwrite\Enums\Compression; use Appwrite\Enums\Framework; use Appwrite\Enums\PasswordHash; +use Appwrite\Enums\ProjectProtocolId; +use Appwrite\Enums\ProjectServiceId; use Appwrite\Enums\Runtime; 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; @@ -37,8 +40,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; @@ -56,7 +61,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; @@ -91,12 +99,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; @@ -170,7 +179,7 @@ public function __construct( protected OnDuplicate $onDuplicate = OnDuplicate::Fail, ?callable $getDatabaseDSN = null, ) { - $this->project = $project; + $this->projectId = $project; $this->endpoint = $endpoint; $this->key = $key; @@ -181,6 +190,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); @@ -241,6 +251,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, @@ -283,6 +295,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, @@ -2160,6 +2175,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); @@ -3107,6 +3130,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) { @@ -3155,6 +3190,75 @@ protected function createProjectVariable(ProjectVariable $resource): bool return true; } + protected function createProtocols(Protocols $resource): bool + { + $project = $this->dbForPlatform->getDocument('projects', $this->projectId); + $apis = $project->getAttribute('apis', []); + + $apis[(string) ProjectProtocolId::REST()] = $resource->getRest(); + $apis[(string) ProjectProtocolId::GRAPHQL()] = $resource->getGraphql(); + $apis[(string) ProjectProtocolId::WEBSOCKET()] = $resource->getWebsocket(); + + $this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument( + 'projects', + $this->projectId, + new UtopiaDocument(['apis' => $apis]), + )); + + $this->dbForPlatform->purgeCachedDocument('projects', $this->projectId); + + return true; + } + + protected function createLabels(Labels $resource): bool + { + $labels = \array_values(\array_unique($resource->getLabels())); + + $this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument( + 'projects', + $this->projectId, + new UtopiaDocument(['labels' => $labels]), + )); + + $this->dbForPlatform->purgeCachedDocument('projects', $this->projectId); + + return true; + } + + protected function createServices(ServicesResource $resource): bool + { + $project = $this->dbForPlatform->getDocument('projects', $this->projectId); + $services = $project->getAttribute('services', []); + + $services[(string) ProjectServiceId::ACCOUNT()] = $resource->getAccount(); + $services[(string) ProjectServiceId::AVATARS()] = $resource->getAvatars(); + $services[(string) ProjectServiceId::DATABASES()] = $resource->getDatabases(); + $services[(string) ProjectServiceId::TABLESDB()] = $resource->getTablesdb(); + $services[(string) ProjectServiceId::LOCALE()] = $resource->getLocale(); + $services[(string) ProjectServiceId::HEALTH()] = $resource->getHealth(); + $services[(string) ProjectServiceId::PROJECT()] = $resource->getProject(); + $services[(string) ProjectServiceId::STORAGE()] = $resource->getStorage(); + $services[(string) ProjectServiceId::TEAMS()] = $resource->getTeams(); + $services[(string) ProjectServiceId::USERS()] = $resource->getUsers(); + $services[(string) ProjectServiceId::VCS()] = $resource->getVcs(); + $services[(string) ProjectServiceId::SITES()] = $resource->getSites(); + $services[(string) ProjectServiceId::FUNCTIONS()] = $resource->getFunctions(); + $services[(string) ProjectServiceId::PROXY()] = $resource->getProxy(); + $services[(string) ProjectServiceId::GRAPHQL()] = $resource->getGraphql(); + $services[(string) ProjectServiceId::MIGRATIONS()] = $resource->getMigrations(); + $services[(string) ProjectServiceId::MESSAGING()] = $resource->getMessaging(); + + $this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument( + 'projects', + $this->projectId, + new UtopiaDocument(['services' => $services]), + )); + + $this->dbForPlatform->purgeCachedDocument('projects', $this->projectId); + + return true; + } + protected function createWebhook(Webhook $resource): bool { $existing = $this->dbForPlatform->findOne('webhooks', [ @@ -3175,7 +3279,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(), @@ -3194,7 +3298,7 @@ protected function createWebhook(Webhook $resource): bool return false; } - $this->dbForPlatform->purgeCachedDocument('projects', $this->project); + $this->dbForPlatform->purgeCachedDocument('projects', $this->projectId); return true; } @@ -3205,7 +3309,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()]), ]); @@ -3223,7 +3327,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(), @@ -3237,7 +3341,72 @@ protected function createPlatform(Platform $resource): bool return false; } - $this->dbForPlatform->purgeCachedDocument('projects', $this->project); + $this->dbForPlatform->purgeCachedDocument('projects', $this->projectId); + + return true; + } + + /** + * Storage keys mirror app/config/auth.php, not the SDK enum values. + * Shares the `auths` map with createPolicies — read-then-merge. + */ + protected function createAuthMethods(AuthMethods $resource): bool + { + $project = $this->dbForPlatform->getDocument('projects', $this->projectId); + $auths = $project->getAttribute('auths', []); + + $auths['emailPassword'] = $resource->getEmailPassword(); + $auths['usersAuthMagicURL'] = $resource->getMagicURL(); + $auths['emailOtp'] = $resource->getEmailOtp(); + $auths['anonymous'] = $resource->getAnonymous(); + $auths['invites'] = $resource->getInvites(); + $auths['JWT'] = $resource->getJwt(); + $auths['phone'] = $resource->getPhone(); + + $this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument( + 'projects', + $this->projectId, + new UtopiaDocument(['auths' => $auths]), + )); + + $this->dbForPlatform->purgeCachedDocument('projects', $this->projectId); + + return true; + } + + /** + * Direct DB write — SDK policy setters reject `total: 0` but `0` is the + * storage value for "disabled". Shares the `auths` map with + * createAuthMethods — read-then-merge. + */ + protected function createPolicies(Policies $resource): bool + { + $project = $this->dbForPlatform->getDocument('projects', $this->projectId); + $auths = $project->getAttribute('auths', []); + + $auths['passwordHistory'] = $resource->getPasswordHistory(); + $auths['duration'] = $resource->getSessionDuration(); + $auths['maxSessions'] = $resource->getSessionsLimit(); + $auths['limit'] = $resource->getUserLimit(); + + $auths['passwordDictionary'] = $resource->getPasswordDictionary(); + $auths['personalDataCheck'] = $resource->getPersonalDataCheck(); + $auths['sessionAlerts'] = $resource->getSessionAlerts(); + $auths['invalidateSessions'] = $resource->getSessionInvalidation(); + + $auths['membershipsUserId'] = $resource->getMembershipsUserId(); + $auths['membershipsUserEmail'] = $resource->getMembershipsUserEmail(); + $auths['membershipsUserName'] = $resource->getMembershipsUserName(); + $auths['membershipsMfa'] = $resource->getMembershipsUserMfa(); + $auths['membershipsUserPhone'] = $resource->getMembershipsUserPhone(); + + $this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument( + 'projects', + $this->projectId, + new UtopiaDocument(['auths' => $auths]), + )); + + $this->dbForPlatform->purgeCachedDocument('projects', $this->projectId); return true; } @@ -3264,7 +3433,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(), @@ -3280,7 +3449,7 @@ protected function createApiKey(ApiKey $resource): bool return false; } - $this->dbForPlatform->purgeCachedDocument('projects', $this->project); + $this->dbForPlatform->purgeCachedDocument('projects', $this->projectId); return true; } diff --git a/src/Migration/Resource.php b/src/Migration/Resource.php index 9dc936ac..f77b3d8d 100644 --- a/src/Migration/Resource.php +++ b/src/Migration/Resource.php @@ -69,6 +69,10 @@ abstract class Resource implements \JsonSerializable public const TYPE_HASH = 'hash'; + public const TYPE_AUTH_METHODS = 'auth-methods'; + + public const TYPE_POLICIES = 'policies'; + public const TYPE_ENVIRONMENT_VARIABLE = 'environment-variable'; // Integrations @@ -78,6 +82,9 @@ abstract class Resource implements \JsonSerializable // Settings public const TYPE_PROJECT_VARIABLE = 'project-variable'; public const TYPE_WEBHOOK = 'webhook'; + public const TYPE_PROTOCOLS = 'protocols'; + public const TYPE_LABELS = 'labels'; + public const TYPE_SERVICES = 'services'; // Messaging public const TYPE_SUBSCRIBER = 'subscriber'; @@ -117,10 +124,15 @@ abstract class Resource implements \JsonSerializable self::TYPE_ENVIRONMENT_VARIABLE, self::TYPE_TEAM, self::TYPE_MEMBERSHIP, + self::TYPE_AUTH_METHODS, + self::TYPE_POLICIES, self::TYPE_PLATFORM, self::TYPE_API_KEY, self::TYPE_PROJECT_VARIABLE, self::TYPE_WEBHOOK, + self::TYPE_PROTOCOLS, + self::TYPE_LABELS, + self::TYPE_SERVICES, self::TYPE_PROVIDER, self::TYPE_TOPIC, self::TYPE_SUBSCRIBER, diff --git a/src/Migration/Resources/Auth/AuthMethods.php b/src/Migration/Resources/Auth/AuthMethods.php new file mode 100644 index 00000000..bc1adbe4 --- /dev/null +++ b/src/Migration/Resources/Auth/AuthMethods.php @@ -0,0 +1,113 @@ +id = $id; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + } + + /** + * @param array $array + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + (bool) ($array['emailPassword'] ?? true), + (bool) ($array['magicURL'] ?? true), + (bool) ($array['emailOtp'] ?? true), + (bool) ($array['anonymous'] ?? true), + (bool) ($array['invites'] ?? true), + (bool) ($array['jwt'] ?? true), + (bool) ($array['phone'] ?? true), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'emailPassword' => $this->emailPassword, + 'magicURL' => $this->magicURL, + 'emailOtp' => $this->emailOtp, + 'anonymous' => $this->anonymous, + 'invites' => $this->invites, + 'jwt' => $this->jwt, + 'phone' => $this->phone, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_AUTH_METHODS; + } + + public function getGroup(): string + { + return Transfer::GROUP_AUTH; + } + + public function getEmailPassword(): bool + { + return $this->emailPassword; + } + + public function getMagicURL(): bool + { + return $this->magicURL; + } + + public function getEmailOtp(): bool + { + return $this->emailOtp; + } + + public function getAnonymous(): bool + { + return $this->anonymous; + } + + public function getInvites(): bool + { + return $this->invites; + } + + public function getJwt(): bool + { + return $this->jwt; + } + + public function getPhone(): bool + { + return $this->phone; + } +} diff --git a/src/Migration/Resources/Auth/Policies.php b/src/Migration/Resources/Auth/Policies.php new file mode 100644 index 00000000..1e264751 --- /dev/null +++ b/src/Migration/Resources/Auth/Policies.php @@ -0,0 +1,163 @@ +id = $id; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + } + + /** + * @param array $array + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + (int) ($array['passwordHistory'] ?? 0), + (int) ($array['sessionDuration'] ?? 31536000), + (int) ($array['sessionsLimit'] ?? 100), + (int) ($array['userLimit'] ?? 0), + (bool) ($array['passwordDictionary'] ?? false), + (bool) ($array['personalDataCheck'] ?? false), + (bool) ($array['sessionAlerts'] ?? false), + (bool) ($array['sessionInvalidation'] ?? false), + (bool) ($array['membershipsUserId'] ?? true), + (bool) ($array['membershipsUserEmail'] ?? true), + (bool) ($array['membershipsUserName'] ?? true), + (bool) ($array['membershipsUserMfa'] ?? true), + (bool) ($array['membershipsUserPhone'] ?? true), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'passwordHistory' => $this->passwordHistory, + 'sessionDuration' => $this->sessionDuration, + 'sessionsLimit' => $this->sessionsLimit, + 'userLimit' => $this->userLimit, + 'passwordDictionary' => $this->passwordDictionary, + 'personalDataCheck' => $this->personalDataCheck, + 'sessionAlerts' => $this->sessionAlerts, + 'sessionInvalidation' => $this->sessionInvalidation, + 'membershipsUserId' => $this->membershipsUserId, + 'membershipsUserEmail' => $this->membershipsUserEmail, + 'membershipsUserName' => $this->membershipsUserName, + 'membershipsUserMfa' => $this->membershipsUserMfa, + 'membershipsUserPhone' => $this->membershipsUserPhone, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_POLICIES; + } + + public function getGroup(): string + { + return Transfer::GROUP_AUTH; + } + + public function getPasswordHistory(): int + { + return $this->passwordHistory; + } + + public function getSessionDuration(): int + { + return $this->sessionDuration; + } + + public function getSessionsLimit(): int + { + return $this->sessionsLimit; + } + + public function getUserLimit(): int + { + return $this->userLimit; + } + + public function getPasswordDictionary(): bool + { + return $this->passwordDictionary; + } + + public function getPersonalDataCheck(): bool + { + return $this->personalDataCheck; + } + + public function getSessionAlerts(): bool + { + return $this->sessionAlerts; + } + + public function getSessionInvalidation(): bool + { + return $this->sessionInvalidation; + } + + public function getMembershipsUserId(): bool + { + return $this->membershipsUserId; + } + + public function getMembershipsUserEmail(): bool + { + return $this->membershipsUserEmail; + } + + public function getMembershipsUserName(): bool + { + return $this->membershipsUserName; + } + + public function getMembershipsUserMfa(): bool + { + return $this->membershipsUserMfa; + } + + public function getMembershipsUserPhone(): bool + { + return $this->membershipsUserPhone; + } +} diff --git a/src/Migration/Resources/Settings/Labels.php b/src/Migration/Resources/Settings/Labels.php new file mode 100644 index 00000000..a2da658d --- /dev/null +++ b/src/Migration/Resources/Settings/Labels.php @@ -0,0 +1,72 @@ + $labels + */ + public function __construct( + string $id, + private readonly array $labels = [], + string $createdAt = '', + string $updatedAt = '', + ) { + $this->id = $id; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + } + + /** + * @param array $array + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + (array) ($array['labels'] ?? []), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'labels' => $this->labels, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_LABELS; + } + + public function getGroup(): string + { + return Transfer::GROUP_SETTINGS; + } + + /** + * @return array + */ + public function getLabels(): array + { + return $this->labels; + } +} diff --git a/src/Migration/Resources/Settings/Protocols.php b/src/Migration/Resources/Settings/Protocols.php new file mode 100644 index 00000000..de0609a4 --- /dev/null +++ b/src/Migration/Resources/Settings/Protocols.php @@ -0,0 +1,82 @@ +id = $id; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + } + + /** + * @param array $array + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + (bool) ($array['rest'] ?? true), + (bool) ($array['graphql'] ?? true), + (bool) ($array['websocket'] ?? true), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'rest' => $this->rest, + 'graphql' => $this->graphql, + 'websocket' => $this->websocket, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_PROTOCOLS; + } + + public function getGroup(): string + { + return Transfer::GROUP_SETTINGS; + } + + public function getRest(): bool + { + return $this->rest; + } + + public function getGraphql(): bool + { + return $this->graphql; + } + + public function getWebsocket(): bool + { + return $this->websocket; + } +} diff --git a/src/Migration/Resources/Settings/Services.php b/src/Migration/Resources/Settings/Services.php new file mode 100644 index 00000000..3b7474c4 --- /dev/null +++ b/src/Migration/Resources/Settings/Services.php @@ -0,0 +1,194 @@ +id = $id; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + } + + /** + * @param array $array + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + (bool) ($array['account'] ?? true), + (bool) ($array['avatars'] ?? true), + (bool) ($array['databases'] ?? true), + (bool) ($array['tablesdb'] ?? true), + (bool) ($array['locale'] ?? true), + (bool) ($array['health'] ?? true), + (bool) ($array['project'] ?? true), + (bool) ($array['storage'] ?? true), + (bool) ($array['teams'] ?? true), + (bool) ($array['users'] ?? true), + (bool) ($array['vcs'] ?? true), + (bool) ($array['sites'] ?? true), + (bool) ($array['functions'] ?? true), + (bool) ($array['proxy'] ?? true), + (bool) ($array['graphql'] ?? true), + (bool) ($array['migrations'] ?? true), + (bool) ($array['messaging'] ?? true), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'account' => $this->account, + 'avatars' => $this->avatars, + 'databases' => $this->databases, + 'tablesdb' => $this->tablesdb, + 'locale' => $this->locale, + 'health' => $this->health, + 'project' => $this->project, + 'storage' => $this->storage, + 'teams' => $this->teams, + 'users' => $this->users, + 'vcs' => $this->vcs, + 'sites' => $this->sites, + 'functions' => $this->functions, + 'proxy' => $this->proxy, + 'graphql' => $this->graphql, + 'migrations' => $this->migrations, + 'messaging' => $this->messaging, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_SERVICES; + } + + public function getGroup(): string + { + return Transfer::GROUP_SETTINGS; + } + + public function getAccount(): bool + { + return $this->account; + } + + public function getAvatars(): bool + { + return $this->avatars; + } + + public function getDatabases(): bool + { + return $this->databases; + } + + public function getTablesdb(): bool + { + return $this->tablesdb; + } + + public function getLocale(): bool + { + return $this->locale; + } + + public function getHealth(): bool + { + return $this->health; + } + + public function getProject(): bool + { + return $this->project; + } + + public function getStorage(): bool + { + return $this->storage; + } + + public function getTeams(): bool + { + return $this->teams; + } + + public function getUsers(): bool + { + return $this->users; + } + + public function getVcs(): bool + { + return $this->vcs; + } + + public function getSites(): bool + { + return $this->sites; + } + + public function getFunctions(): bool + { + return $this->functions; + } + + public function getProxy(): bool + { + return $this->proxy; + } + + public function getGraphql(): bool + { + return $this->graphql; + } + + public function getMigrations(): bool + { + return $this->migrations; + } + + public function getMessaging(): bool + { + return $this->messaging; + } +} diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index fd03b7d0..1b9c601e 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -4,6 +4,10 @@ use Appwrite\AppwriteException; use Appwrite\Client; +use Appwrite\Enums\ProjectAuthMethodId; +use Appwrite\Enums\ProjectPolicyId; +use Appwrite\Enums\ProjectProtocolId; +use Appwrite\Enums\ProjectServiceId; use Appwrite\Query; use Appwrite\Services\Functions; use Appwrite\Services\Messaging; @@ -19,8 +23,10 @@ use Utopia\Database\Document as UtopiaDocument; 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; @@ -62,7 +68,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; @@ -174,6 +183,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, @@ -222,6 +233,9 @@ public static function getSupportedResources(): array // Settings Resource::TYPE_PROJECT_VARIABLE, Resource::TYPE_WEBHOOK, + Resource::TYPE_PROTOCOLS, + Resource::TYPE_LABELS, + Resource::TYPE_SERVICES, ]; } @@ -359,6 +373,16 @@ private function reportAuth(array $resources, array &$report, array $resourceIds )->total; } } + + if (\in_array(Resource::TYPE_AUTH_METHODS, $resources)) { + // Singleton — there is exactly one auth-methods config per project. + $report[Resource::TYPE_AUTH_METHODS] = 1; + } + + if (\in_array(Resource::TYPE_POLICIES, $resources)) { + // Singleton — one policies config per project. + $report[Resource::TYPE_POLICIES] = 1; + } } /** @@ -599,6 +623,91 @@ protected function exportGroupAuth(int $batchSize, array $resources): void previous: $e )); } + + try { + if (\in_array(Resource::TYPE_AUTH_METHODS, $resources)) { + $this->exportAuthMethods(); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_AUTH_METHODS, + Transfer::GROUP_AUTH, + message: $e->getMessage(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, + previous: $e + )); + } + + try { + if (\in_array(Resource::TYPE_POLICIES, $resources)) { + $this->exportPolicies(); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_POLICIES, + Transfer::GROUP_AUTH, + message: $e->getMessage(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, + previous: $e + )); + } + } + + private function exportPolicies(): void + { + $passwordHistory = $this->project->getPolicy(ProjectPolicyId::PASSWORDHISTORY()); + $passwordDictionary = $this->project->getPolicy(ProjectPolicyId::PASSWORDDICTIONARY()); + $passwordPersonalData = $this->project->getPolicy(ProjectPolicyId::PASSWORDPERSONALDATA()); + $sessionAlert = $this->project->getPolicy(ProjectPolicyId::SESSIONALERT()); + $sessionDuration = $this->project->getPolicy(ProjectPolicyId::SESSIONDURATION()); + $sessionInvalidation = $this->project->getPolicy(ProjectPolicyId::SESSIONINVALIDATION()); + $sessionLimit = $this->project->getPolicy(ProjectPolicyId::SESSIONLIMIT()); + $userLimit = $this->project->getPolicy(ProjectPolicyId::USERLIMIT()); + $membershipPrivacy = $this->project->getPolicy(ProjectPolicyId::MEMBERSHIPPRIVACY()); + + $policies = new Policies( + $this->projectId, + $passwordHistory->total, + $sessionDuration->duration, + $sessionLimit->total, + $userLimit->total, + $passwordDictionary->enabled, + $passwordPersonalData->enabled, + $sessionAlert->enabled, + $sessionInvalidation->enabled, + $membershipPrivacy->userId, + $membershipPrivacy->userEmail, + $membershipPrivacy->userName, + $membershipPrivacy->userMFA, + $membershipPrivacy->userPhone, + ); + + $this->callback([$policies]); + } + + private function exportAuthMethods(): void + { + $project = $this->project->get(); + + $byId = []; + foreach ($project->authMethods as $method) { + $byId[(string) $method->id] = $method->enabled; + } + + $authMethods = new AuthMethods( + $this->projectId, + $byId[(string) ProjectAuthMethodId::EMAILPASSWORD()] ?? true, + $byId[(string) ProjectAuthMethodId::MAGICURL()] ?? true, + $byId[(string) ProjectAuthMethodId::EMAILOTP()] ?? true, + $byId[(string) ProjectAuthMethodId::ANONYMOUS()] ?? true, + $byId[(string) ProjectAuthMethodId::INVITES()] ?? true, + $byId[(string) ProjectAuthMethodId::JWT()] ?? true, + $byId[(string) ProjectAuthMethodId::PHONE()] ?? true, + createdAt: $project->createdAt, + updatedAt: $project->updatedAt, + ); + + $this->callback([$authMethods]); } /** @@ -1485,6 +1594,21 @@ private function reportSettings(array $resources, array &$report, array $resourc $report[Resource::TYPE_WEBHOOK] = 0; } } + + if (\in_array(Resource::TYPE_PROTOCOLS, $resources)) { + // Singleton — there is exactly one protocols config per project. + $report[Resource::TYPE_PROTOCOLS] = 1; + } + + if (\in_array(Resource::TYPE_LABELS, $resources)) { + // Singleton — one labels array per project. + $report[Resource::TYPE_LABELS] = 1; + } + + if (\in_array(Resource::TYPE_SERVICES, $resources)) { + // Singleton — one services config per project. + $report[Resource::TYPE_SERVICES] = 1; + } } /** @@ -1501,7 +1625,7 @@ protected function exportGroupSettings(int $batchSize, array $resources): void Resource::TYPE_PROJECT_VARIABLE, Transfer::GROUP_SETTINGS, message: $e->getMessage(), - code: $e->getCode(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, previous: $e )); } @@ -1515,11 +1639,123 @@ protected function exportGroupSettings(int $batchSize, array $resources): void Resource::TYPE_WEBHOOK, Transfer::GROUP_SETTINGS, message: $e->getMessage(), - code: $e->getCode(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, previous: $e )); } } + + try { + if (\in_array(Resource::TYPE_PROTOCOLS, $resources)) { + $this->exportProtocols(); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_PROTOCOLS, + Transfer::GROUP_SETTINGS, + message: $e->getMessage(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, + previous: $e + )); + } + + try { + if (\in_array(Resource::TYPE_LABELS, $resources)) { + $this->exportLabels(); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_LABELS, + Transfer::GROUP_SETTINGS, + message: $e->getMessage(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, + previous: $e + )); + } + + try { + if (\in_array(Resource::TYPE_SERVICES, $resources)) { + $this->exportServices(); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_SERVICES, + Transfer::GROUP_SETTINGS, + message: $e->getMessage(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, + previous: $e + )); + } + } + + private function exportServices(): void + { + $project = $this->project->get(); + + $byId = []; + foreach ($project->services as $service) { + $byId[(string) $service->id] = $service->enabled; + } + + $services = new ServicesResource( + $this->projectId, + $byId[(string) ProjectServiceId::ACCOUNT()] ?? true, + $byId[(string) ProjectServiceId::AVATARS()] ?? true, + $byId[(string) ProjectServiceId::DATABASES()] ?? true, + $byId[(string) ProjectServiceId::TABLESDB()] ?? true, + $byId[(string) ProjectServiceId::LOCALE()] ?? true, + $byId[(string) ProjectServiceId::HEALTH()] ?? true, + $byId[(string) ProjectServiceId::PROJECT()] ?? true, + $byId[(string) ProjectServiceId::STORAGE()] ?? true, + $byId[(string) ProjectServiceId::TEAMS()] ?? true, + $byId[(string) ProjectServiceId::USERS()] ?? true, + $byId[(string) ProjectServiceId::VCS()] ?? true, + $byId[(string) ProjectServiceId::SITES()] ?? true, + $byId[(string) ProjectServiceId::FUNCTIONS()] ?? true, + $byId[(string) ProjectServiceId::PROXY()] ?? true, + $byId[(string) ProjectServiceId::GRAPHQL()] ?? true, + $byId[(string) ProjectServiceId::MIGRATIONS()] ?? true, + $byId[(string) ProjectServiceId::MESSAGING()] ?? true, + createdAt: $project->createdAt, + updatedAt: $project->updatedAt, + ); + + $this->callback([$services]); + } + + private function exportLabels(): void + { + $project = $this->project->get(); + + $labels = new Labels( + $this->projectId, + $project->labels, + createdAt: $project->createdAt, + updatedAt: $project->updatedAt, + ); + + $this->callback([$labels]); + } + + private function exportProtocols(): void + { + $project = $this->project->get(); + + $byId = []; + foreach ($project->protocols as $protocol) { + $byId[(string) $protocol->id] = $protocol->enabled; + } + + $protocols = new Protocols( + $this->projectId, + $byId[(string) ProjectProtocolId::REST()] ?? true, + $byId[(string) ProjectProtocolId::GRAPHQL()] ?? true, + $byId[(string) ProjectProtocolId::WEBSOCKET()] ?? true, + createdAt: $project->createdAt, + updatedAt: $project->updatedAt, + ); + + $this->callback([$protocols]); } /** diff --git a/src/Migration/Transfer.php b/src/Migration/Transfer.php index 05c61f40..c22d1c26 100644 --- a/src/Migration/Transfer.php +++ b/src/Migration/Transfer.php @@ -34,7 +34,9 @@ class Transfer Resource::TYPE_USER, Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP, - Resource::TYPE_HASH + Resource::TYPE_HASH, + Resource::TYPE_AUTH_METHODS, + Resource::TYPE_POLICIES, ]; public const GROUP_STORAGE_RESOURCES = [ @@ -97,6 +99,9 @@ class Transfer public const GROUP_SETTINGS_RESOURCES = [ Resource::TYPE_PROJECT_VARIABLE, Resource::TYPE_WEBHOOK, + Resource::TYPE_PROTOCOLS, + Resource::TYPE_LABELS, + Resource::TYPE_SERVICES, ]; public const GROUP_BACKUPS_RESOURCES = [ @@ -114,6 +119,8 @@ class Transfer Resource::TYPE_USER, Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP, + Resource::TYPE_AUTH_METHODS, + Resource::TYPE_POLICIES, Resource::TYPE_FILE, Resource::TYPE_BUCKET, Resource::TYPE_FUNCTION, @@ -140,6 +147,9 @@ class Transfer // Settings Resource::TYPE_PROJECT_VARIABLE, Resource::TYPE_WEBHOOK, + Resource::TYPE_PROTOCOLS, + Resource::TYPE_LABELS, + Resource::TYPE_SERVICES, // legacy Resource::TYPE_DOCUMENT,