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 c8330e52..2b89e1be 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -9,11 +9,13 @@ use Appwrite\Enums\Compression; use Appwrite\Enums\Framework; use Appwrite\Enums\PasswordHash; +use Appwrite\Enums\ProjectProtocolId; 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 +39,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 +60,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 +98,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 +178,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 +189,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 +250,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, @@ -281,8 +292,11 @@ public static function getSupportedResources(): array Resource::TYPE_API_KEY, Resource::TYPE_WEBHOOK, - // Settings + // Project Resource::TYPE_PROJECT_VARIABLE, + Resource::TYPE_PROJECT_PROTOCOLS, + Resource::TYPE_PROJECT_LABELS, + Resource::TYPE_PROJECT_SERVICES, // Backups Resource::TYPE_BACKUP_POLICY, @@ -444,7 +458,7 @@ protected function import(array $resources, callable $callback): void Transfer::GROUP_SITES => $this->importSiteResource($resource), Transfer::GROUP_INTEGRATIONS => $this->importIntegrationsResource($resource), Transfer::GROUP_BACKUPS => $this->importBackupResource($resource), - Transfer::GROUP_SETTINGS => $this->importSettingsResource($resource), + Transfer::GROUP_PROJECTS => $this->importProjectsResource($resource), default => throw new \Exception('Invalid resource group', Exception::CODE_VALIDATION), }; } catch (\Throwable $e) { @@ -2160,6 +2174,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); @@ -3100,13 +3122,25 @@ public function importIntegrationsResource(Resource $resource): Resource return $resource; } - public function importSettingsResource(Resource $resource): Resource + public function importProjectsResource(Resource $resource): Resource { switch ($resource->getName()) { case Resource::TYPE_PROJECT_VARIABLE: /** @var ProjectVariable $resource */ $this->createProjectVariable($resource); break; + case Resource::TYPE_PROJECT_PROTOCOLS: + /** @var Protocols $resource */ + $this->createProtocols($resource); + break; + case Resource::TYPE_PROJECT_LABELS: + /** @var Labels $resource */ + $this->createLabels($resource); + break; + case Resource::TYPE_PROJECT_SERVICES: + /** @var ServicesResource $resource */ + $this->createServices($resource); + break; } if ($resource->getStatus() !== Resource::STATUS_SKIPPED) { @@ -3155,6 +3189,61 @@ 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', []); + + foreach ($resource->getServices() as $serviceId => $enabled) { + $services[$serviceId] = (bool) $enabled; + } + + $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 +3264,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 +3283,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 +3294,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 +3312,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 +3326,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 +3418,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 +3434,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..0edb63bd 100644 --- a/src/Migration/Resource.php +++ b/src/Migration/Resource.php @@ -69,15 +69,22 @@ 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 public const TYPE_PLATFORM = 'platform'; public const TYPE_API_KEY = 'api-key'; + public const TYPE_WEBHOOK = 'webhook'; - // Settings + // Project (per-project singleton/settings resources) public const TYPE_PROJECT_VARIABLE = 'project-variable'; - public const TYPE_WEBHOOK = 'webhook'; + public const TYPE_PROJECT_PROTOCOLS = 'project-protocols'; + public const TYPE_PROJECT_LABELS = 'project-labels'; + public const TYPE_PROJECT_SERVICES = 'project-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_PROJECT_VARIABLE, + self::TYPE_PROJECT_PROTOCOLS, + self::TYPE_PROJECT_LABELS, + self::TYPE_PROJECT_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..cc8c075a --- /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_PROJECT_LABELS; + } + + public function getGroup(): string + { + return Transfer::GROUP_PROJECTS; + } + + /** + * @return array + */ + public function getLabels(): array + { + return $this->labels; + } +} diff --git a/src/Migration/Resources/Settings/ProjectVariable.php b/src/Migration/Resources/Settings/ProjectVariable.php index 1585695d..58146cf0 100644 --- a/src/Migration/Resources/Settings/ProjectVariable.php +++ b/src/Migration/Resources/Settings/ProjectVariable.php @@ -58,7 +58,7 @@ public static function getName(): string public function getGroup(): string { - return Transfer::GROUP_SETTINGS; + return Transfer::GROUP_PROJECTS; } public function getKey(): string diff --git a/src/Migration/Resources/Settings/Protocols.php b/src/Migration/Resources/Settings/Protocols.php new file mode 100644 index 00000000..1d652516 --- /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_PROJECT_PROTOCOLS; + } + + public function getGroup(): string + { + return Transfer::GROUP_PROJECTS; + } + + 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..b4a17fa6 --- /dev/null +++ b/src/Migration/Resources/Settings/Services.php @@ -0,0 +1,72 @@ + $services Map of ServiceId string → enabled flag. + */ + public function __construct( + string $id, + private readonly array $services = [], + 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['services'] ?? []), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'services' => $this->services, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_PROJECT_SERVICES; + } + + public function getGroup(): string + { + return Transfer::GROUP_PROJECTS; + } + + /** + * @return array + */ + public function getServices(): array + { + return $this->services; + } +} diff --git a/src/Migration/Source.php b/src/Migration/Source.php index fe53f2cd..9cf1d080 100644 --- a/src/Migration/Source.php +++ b/src/Migration/Source.php @@ -56,7 +56,7 @@ public function getBackupsBatchSize(): int return static::$defaultBatchSize; } - public function getSettingsBatchSize(): int + public function getProjectsBatchSize(): int { return static::$defaultBatchSize; } @@ -126,7 +126,7 @@ public function exportResources(array $resources): void Transfer::GROUP_SITES => Transfer::GROUP_SITES_RESOURCES, Transfer::GROUP_INTEGRATIONS => Transfer::GROUP_INTEGRATIONS_RESOURCES, Transfer::GROUP_BACKUPS => Transfer::GROUP_BACKUPS_RESOURCES, - Transfer::GROUP_SETTINGS => Transfer::GROUP_SETTINGS_RESOURCES, + Transfer::GROUP_PROJECTS => Transfer::GROUP_PROJECTS_RESOURCES, ]; foreach ($mapping as $group => $resources) { @@ -167,8 +167,8 @@ public function exportResources(array $resources): void case Transfer::GROUP_BACKUPS: $this->exportGroupBackups($this->getBackupsBatchSize(), $resources); break; - case Transfer::GROUP_SETTINGS: - $this->exportGroupSettings($this->getSettingsBatchSize(), $resources); + case Transfer::GROUP_PROJECTS: + $this->exportGroupProjects($this->getProjectsBatchSize(), $resources); break; } } @@ -239,10 +239,10 @@ abstract protected function exportGroupIntegrations(int $batchSize, array $resou abstract protected function exportGroupBackups(int $batchSize, array $resources): void; /** - * Export Settings Group + * Export Projects Group * * @param int $batchSize * @param array $resources Resources to export */ - abstract protected function exportGroupSettings(int $batchSize, array $resources): void; + abstract protected function exportGroupProjects(int $batchSize, array $resources): void; } diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 4114bb2e..c3397ac8 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -4,6 +4,9 @@ use Appwrite\AppwriteException; use Appwrite\Client; +use Appwrite\Enums\ProjectAuthMethodId; +use Appwrite\Enums\ProjectPolicyId; +use Appwrite\Enums\ProjectProtocolId; use Appwrite\Query; use Appwrite\Services\Functions; use Appwrite\Services\Messaging; @@ -19,8 +22,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 +67,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 +182,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, @@ -220,8 +230,11 @@ public static function getSupportedResources(): array // Backups Resource::TYPE_BACKUP_POLICY, - // Settings + // Project Resource::TYPE_PROJECT_VARIABLE, + Resource::TYPE_PROJECT_PROTOCOLS, + Resource::TYPE_PROJECT_LABELS, + Resource::TYPE_PROJECT_SERVICES, ]; } @@ -262,7 +275,7 @@ public function report(array $resources = [], array $resourceIds = []): array $this->reportSites($resources, $report, $resourceIds); $this->reportIntegrations($resources, $report, $resourceIds); $this->reportBackups($resources, $report, $resourceIds); - $this->reportSettings($resources, $report, $resourceIds); + $this->reportProjects($resources, $report, $resourceIds); $report['version'] = $this->call( 'GET', @@ -359,6 +372,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 +622,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]); } /** @@ -1458,7 +1566,7 @@ protected function reportBackups(array $resources, array &$report, array $resour } } - private function reportSettings(array $resources, array &$report, array $resourceIds = []): void + private function reportProjects(array $resources, array &$report, array $resourceIds = []): void { if (\in_array(Resource::TYPE_PROJECT_VARIABLE, $resources)) { $variableQueries = $this->buildQueries( @@ -1472,13 +1580,28 @@ private function reportSettings(array $resources, array &$report, array $resourc $report[Resource::TYPE_PROJECT_VARIABLE] = 0; } } + + if (\in_array(Resource::TYPE_PROJECT_PROTOCOLS, $resources)) { + // Singleton — there is exactly one protocols config per project. + $report[Resource::TYPE_PROJECT_PROTOCOLS] = 1; + } + + if (\in_array(Resource::TYPE_PROJECT_LABELS, $resources)) { + // Singleton — one labels array per project. + $report[Resource::TYPE_PROJECT_LABELS] = 1; + } + + if (\in_array(Resource::TYPE_PROJECT_SERVICES, $resources)) { + // Singleton — one services config per project. + $report[Resource::TYPE_PROJECT_SERVICES] = 1; + } } /** * @param int $batchSize * @param array $resources */ - protected function exportGroupSettings(int $batchSize, array $resources): void + protected function exportGroupProjects(int $batchSize, array $resources): void { if (\in_array(Resource::TYPE_PROJECT_VARIABLE, $resources)) { try { @@ -1486,13 +1609,109 @@ protected function exportGroupSettings(int $batchSize, array $resources): void } catch (\Throwable $e) { $this->addError(new Exception( Resource::TYPE_PROJECT_VARIABLE, - Transfer::GROUP_SETTINGS, + Transfer::GROUP_PROJECTS, message: $e->getMessage(), - code: $e->getCode(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, previous: $e )); } } + + try { + if (\in_array(Resource::TYPE_PROJECT_PROTOCOLS, $resources)) { + $this->exportProtocols(); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_PROJECT_PROTOCOLS, + Transfer::GROUP_PROJECTS, + message: $e->getMessage(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, + previous: $e + )); + } + + try { + if (\in_array(Resource::TYPE_PROJECT_LABELS, $resources)) { + $this->exportLabels(); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_PROJECT_LABELS, + Transfer::GROUP_PROJECTS, + message: $e->getMessage(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, + previous: $e + )); + } + + try { + if (\in_array(Resource::TYPE_PROJECT_SERVICES, $resources)) { + $this->exportServices(); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_PROJECT_SERVICES, + Transfer::GROUP_PROJECTS, + 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] = (bool) $service->enabled; + } + + $services = new ServicesResource( + $this->projectId, + $byId, + 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/Sources/CSV.php b/src/Migration/Sources/CSV.php index 22c9399b..2ddd31db 100644 --- a/src/Migration/Sources/CSV.php +++ b/src/Migration/Sources/CSV.php @@ -440,7 +440,7 @@ protected function exportGroupBackups(int $batchSize, array $resources): void throw new \Exception('Not Implemented'); } - protected function exportGroupSettings(int $batchSize, array $resources): void + protected function exportGroupProjects(int $batchSize, array $resources): void { throw new \Exception('Not Implemented'); } diff --git a/src/Migration/Sources/Firebase.php b/src/Migration/Sources/Firebase.php index 2bea50de..510b86d0 100644 --- a/src/Migration/Sources/Firebase.php +++ b/src/Migration/Sources/Firebase.php @@ -828,7 +828,7 @@ protected function exportGroupBackups(int $batchSize, array $resources): void throw new \Exception('Not implemented'); } - protected function exportGroupSettings(int $batchSize, array $resources): void + protected function exportGroupProjects(int $batchSize, array $resources): void { throw new \Exception('Not implemented'); } diff --git a/src/Migration/Sources/JSON.php b/src/Migration/Sources/JSON.php index 149ad66f..d1d8ccbc 100644 --- a/src/Migration/Sources/JSON.php +++ b/src/Migration/Sources/JSON.php @@ -214,7 +214,7 @@ protected function exportGroupBackups(int $batchSize, array $resources): void throw new \Exception('Not Implemented'); } - protected function exportGroupSettings(int $batchSize, array $resources): void + protected function exportGroupProjects(int $batchSize, array $resources): void { throw new \Exception('Not Implemented'); } diff --git a/src/Migration/Sources/NHost.php b/src/Migration/Sources/NHost.php index 6c8fe0ac..6fd011e9 100644 --- a/src/Migration/Sources/NHost.php +++ b/src/Migration/Sources/NHost.php @@ -963,7 +963,7 @@ protected function exportGroupBackups(int $batchSize, array $resources): void throw new \Exception('Not Implemented'); } - protected function exportGroupSettings(int $batchSize, array $resources): void + protected function exportGroupProjects(int $batchSize, array $resources): void { throw new \Exception('Not Implemented'); } diff --git a/src/Migration/Transfer.php b/src/Migration/Transfer.php index f7a4a1b2..f139196f 100644 --- a/src/Migration/Transfer.php +++ b/src/Migration/Transfer.php @@ -28,13 +28,15 @@ class Transfer public const GROUP_BACKUPS = 'backups'; - public const GROUP_SETTINGS = 'settings'; + public const GROUP_PROJECTS = 'projects'; public const GROUP_AUTH_RESOURCES = [ 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 = [ @@ -95,8 +97,11 @@ class Transfer Resource::TYPE_ATTRIBUTE ]; - public const GROUP_SETTINGS_RESOURCES = [ + public const GROUP_PROJECTS_RESOURCES = [ Resource::TYPE_PROJECT_VARIABLE, + Resource::TYPE_PROJECT_PROTOCOLS, + Resource::TYPE_PROJECT_LABELS, + Resource::TYPE_PROJECT_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, @@ -138,8 +145,11 @@ class Transfer Resource::TYPE_API_KEY, Resource::TYPE_WEBHOOK, - // Settings + // Project Resource::TYPE_PROJECT_VARIABLE, + Resource::TYPE_PROJECT_PROTOCOLS, + Resource::TYPE_PROJECT_LABELS, + Resource::TYPE_PROJECT_SERVICES, // legacy Resource::TYPE_DOCUMENT, @@ -426,7 +436,7 @@ public static function extractServices(array $services): array self::GROUP_DATABASES_VECTOR_DB => array_merge($resources, self::GROUP_VECTORSDB_RESOURCES), self::GROUP_MESSAGING => array_merge($resources, self::GROUP_MESSAGING_RESOURCES), self::GROUP_BACKUPS => array_merge($resources, self::GROUP_BACKUPS_RESOURCES), - self::GROUP_SETTINGS => array_merge($resources, self::GROUP_SETTINGS_RESOURCES), + self::GROUP_PROJECTS => array_merge($resources, self::GROUP_PROJECTS_RESOURCES), default => throw new \Exception('No service group found'), }; } diff --git a/tests/Migration/Unit/Adapters/MockDestination.php b/tests/Migration/Unit/Adapters/MockDestination.php index 640aba08..84b6f414 100644 --- a/tests/Migration/Unit/Adapters/MockDestination.php +++ b/tests/Migration/Unit/Adapters/MockDestination.php @@ -55,6 +55,11 @@ public static function getSupportedResources(): array Resource::TYPE_API_KEY, Resource::TYPE_PROJECT_VARIABLE, Resource::TYPE_WEBHOOK, + Resource::TYPE_AUTH_METHODS, + Resource::TYPE_POLICIES, + Resource::TYPE_PROJECT_PROTOCOLS, + Resource::TYPE_PROJECT_LABELS, + Resource::TYPE_PROJECT_SERVICES, Resource::TYPE_PROVIDER, Resource::TYPE_TOPIC, Resource::TYPE_SUBSCRIBER, diff --git a/tests/Migration/Unit/Adapters/MockSource.php b/tests/Migration/Unit/Adapters/MockSource.php index 4b0118c1..4b4d75b9 100644 --- a/tests/Migration/Unit/Adapters/MockSource.php +++ b/tests/Migration/Unit/Adapters/MockSource.php @@ -84,6 +84,11 @@ public static function getSupportedResources(): array Resource::TYPE_API_KEY, Resource::TYPE_PROJECT_VARIABLE, Resource::TYPE_WEBHOOK, + Resource::TYPE_AUTH_METHODS, + Resource::TYPE_POLICIES, + Resource::TYPE_PROJECT_PROTOCOLS, + Resource::TYPE_PROJECT_LABELS, + Resource::TYPE_PROJECT_SERVICES, Resource::TYPE_PROVIDER, Resource::TYPE_TOPIC, Resource::TYPE_SUBSCRIBER, @@ -232,14 +237,14 @@ protected function exportGroupBackups(int $batchSize, array $resources): void } } - protected function exportGroupSettings(int $batchSize, array $resources): void + protected function exportGroupProjects(int $batchSize, array $resources): void { - foreach (Transfer::GROUP_SETTINGS_RESOURCES as $resource) { + foreach (Transfer::GROUP_PROJECTS_RESOURCES as $resource) { if (!\in_array($resource, $resources)) { continue; } - $this->handleResourceTransfer(Transfer::GROUP_SETTINGS, $resource); + $this->handleResourceTransfer(Transfer::GROUP_PROJECTS, $resource); } } }