diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 1b7c4d23..2d46c9f6 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -56,6 +56,7 @@ use Utopia\Migration\Resources\Messaging\Provider; use Utopia\Migration\Resources\Messaging\Subscriber; use Utopia\Migration\Resources\Messaging\Topic; +use Utopia\Migration\Resources\Settings\ProjectVariable; use Utopia\Migration\Resources\Sites\Deployment as SiteDeployment; use Utopia\Migration\Resources\Sites\EnvVar as SiteEnvVar; use Utopia\Migration\Resources\Sites\Site; @@ -278,6 +279,9 @@ public static function getSupportedResources(): array Resource::TYPE_PLATFORM, Resource::TYPE_API_KEY, + // Settings + Resource::TYPE_PROJECT_VARIABLE, + // Backups Resource::TYPE_BACKUP_POLICY, ]; @@ -438,6 +442,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), default => throw new \Exception('Invalid resource group', Exception::CODE_VALIDATION), }; } catch (\Throwable $e) { @@ -3089,6 +3094,61 @@ public function importIntegrationsResource(Resource $resource): Resource return $resource; } + public function importSettingsResource(Resource $resource): Resource + { + switch ($resource->getName()) { + case Resource::TYPE_PROJECT_VARIABLE: + /** @var ProjectVariable $resource */ + $this->createProjectVariable($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', [ + Query::equal('resourceType', ['project']), + Query::equal('key', [$resource->getKey()]), + ]); + + if ($existing !== false && !$existing->isEmpty()) { + $resource->setStatus(Resource::STATUS_SKIPPED, 'Project variable already exists'); + return false; + } + + $createdAt = $this->normalizeDateTime($resource->getCreatedAt()); + $updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt); + $variableId = ID::unique(); + $key = $resource->getKey(); + + try { + $this->dbForProject->createDocument('variables', new UtopiaDocument([ + '$id' => $variableId, + '$permissions' => $resource->getPermissions(), + 'resourceInternalId' => '', + 'resourceId' => '', + 'resourceType' => 'project', + 'key' => $key, + 'value' => $resource->getValue(), + 'secret' => $resource->isSecret(), + 'search' => \implode(' ', [$variableId, $key, 'project']), + '$createdAt' => $createdAt, + '$updatedAt' => $updatedAt, + ])); + } catch (DuplicateException) { + $resource->setStatus(Resource::STATUS_SKIPPED, 'Project variable already exists'); + return false; + } + + return true; + } + /** * @throws \Throwable */ diff --git a/src/Migration/Resource.php b/src/Migration/Resource.php index 14452570..6dd1df78 100644 --- a/src/Migration/Resource.php +++ b/src/Migration/Resource.php @@ -74,8 +74,12 @@ abstract class Resource implements \JsonSerializable // Integrations public const TYPE_PLATFORM = 'platform'; public const TYPE_API_KEY = 'api-key'; - public const TYPE_SUBSCRIBER = 'subscriber'; + // Settings + public const TYPE_PROJECT_VARIABLE = 'project-variable'; + + // Messaging + public const TYPE_SUBSCRIBER = 'subscriber'; public const TYPE_MESSAGE = 'message'; // Backups @@ -114,6 +118,7 @@ abstract class Resource implements \JsonSerializable self::TYPE_MEMBERSHIP, self::TYPE_PLATFORM, self::TYPE_API_KEY, + self::TYPE_PROJECT_VARIABLE, self::TYPE_PROVIDER, self::TYPE_TOPIC, self::TYPE_SUBSCRIBER, diff --git a/src/Migration/Resources/Settings/ProjectVariable.php b/src/Migration/Resources/Settings/ProjectVariable.php new file mode 100644 index 00000000..1585695d --- /dev/null +++ b/src/Migration/Resources/Settings/ProjectVariable.php @@ -0,0 +1,78 @@ +id = $id; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + } + + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + $array['key'], + $array['value'] ?? '', + (bool) ($array['secret'] ?? false), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'key' => $this->key, + 'value' => $this->value, + 'secret' => $this->secret, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_PROJECT_VARIABLE; + } + + public function getGroup(): string + { + return Transfer::GROUP_SETTINGS; + } + + public function getKey(): string + { + return $this->key; + } + + public function getValue(): string + { + return $this->value; + } + + public function isSecret(): bool + { + return $this->secret; + } +} diff --git a/src/Migration/Source.php b/src/Migration/Source.php index bf129b2b..fe53f2cd 100644 --- a/src/Migration/Source.php +++ b/src/Migration/Source.php @@ -56,6 +56,11 @@ public function getBackupsBatchSize(): int return static::$defaultBatchSize; } + public function getSettingsBatchSize(): int + { + return static::$defaultBatchSize; + } + /** * @param array $resources * @return void @@ -121,6 +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, ]; foreach ($mapping as $group => $resources) { @@ -161,6 +167,9 @@ 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); + break; } } } @@ -228,4 +237,12 @@ abstract protected function exportGroupIntegrations(int $batchSize, array $resou * @param array $resources Resources to export */ abstract protected function exportGroupBackups(int $batchSize, array $resources): void; + + /** + * Export Settings Group + * + * @param int $batchSize + * @param array $resources Resources to export + */ + abstract protected function exportGroupSettings(int $batchSize, array $resources): void; } diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index a2196520..17be781f 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -61,6 +61,7 @@ use Utopia\Migration\Resources\Messaging\Provider; use Utopia\Migration\Resources\Messaging\Subscriber; use Utopia\Migration\Resources\Messaging\Topic; +use Utopia\Migration\Resources\Settings\ProjectVariable; use Utopia\Migration\Resources\Sites\Deployment as SiteDeployment; use Utopia\Migration\Resources\Sites\EnvVar as SiteEnvVar; use Utopia\Migration\Resources\Sites\Site; @@ -214,6 +215,7 @@ public static function getSupportedResources(): array Resource::TYPE_BACKUP_POLICY, // Settings + Resource::TYPE_PROJECT_VARIABLE, ]; } @@ -254,6 +256,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); $report['version'] = $this->call( 'GET', @@ -1449,6 +1452,90 @@ protected function reportBackups(array $resources, array &$report, array $resour } } + private function reportSettings(array $resources, array &$report, array $resourceIds = []): void + { + if (\in_array(Resource::TYPE_PROJECT_VARIABLE, $resources)) { + $variableQueries = $this->buildQueries( + resourceType: Resource::TYPE_PROJECT_VARIABLE, + resourceIds: $resourceIds, + limit: 1 + ); + try { + $report[Resource::TYPE_PROJECT_VARIABLE] = $this->project->listVariables($variableQueries)->total; + } catch (\Throwable) { + $report[Resource::TYPE_PROJECT_VARIABLE] = 0; + } + } + } + + /** + * @param int $batchSize + * @param array $resources + */ + protected function exportGroupSettings(int $batchSize, array $resources): void + { + if (\in_array(Resource::TYPE_PROJECT_VARIABLE, $resources)) { + try { + $this->exportProjectVariables($batchSize); + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_PROJECT_VARIABLE, + Transfer::GROUP_SETTINGS, + message: $e->getMessage(), + code: $e->getCode(), + previous: $e + )); + } + } + } + + /** + * @throws AppwriteException + */ + private function exportProjectVariables(int $batchSize): void + { + $lastId = null; + + while (true) { + $queries = [Query::limit($batchSize)]; + + if ($this->rootResourceId !== '' && $this->rootResourceType === Resource::TYPE_PROJECT_VARIABLE) { + $queries[] = Query::equal('$id', $this->rootResourceId); + $queries[] = Query::limit(1); + } + + if ($lastId !== null) { + $queries[] = Query::cursorAfter($lastId); + } + + $response = $this->project->listVariables($queries); + if ($response->total === 0) { + break; + } + + $variables = []; + + foreach ($response->variables as $variable) { + $variables[] = new ProjectVariable( + $variable->id, + $variable->key, + $variable->value, + $variable->secret, + createdAt: $variable->createdAt, + updatedAt: $variable->updatedAt, + ); + + $lastId = $variable->id; + } + + $this->callback($variables); + + if (\count($response->variables) < $batchSize) { + break; + } + } + } + /** * @throws AppwriteException */ diff --git a/src/Migration/Sources/CSV.php b/src/Migration/Sources/CSV.php index 678b3b9e..22c9399b 100644 --- a/src/Migration/Sources/CSV.php +++ b/src/Migration/Sources/CSV.php @@ -440,6 +440,11 @@ protected function exportGroupBackups(int $batchSize, array $resources): void throw new \Exception('Not Implemented'); } + protected function exportGroupSettings(int $batchSize, array $resources): void + { + throw new \Exception('Not Implemented'); + } + /** * @param callable(resource $stream, string $delimiter): void $callback * @return void diff --git a/src/Migration/Sources/Firebase.php b/src/Migration/Sources/Firebase.php index 007797b5..2bea50de 100644 --- a/src/Migration/Sources/Firebase.php +++ b/src/Migration/Sources/Firebase.php @@ -827,4 +827,9 @@ protected function exportGroupBackups(int $batchSize, array $resources): void { throw new \Exception('Not implemented'); } + + protected function exportGroupSettings(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 0b3e7829..149ad66f 100644 --- a/src/Migration/Sources/JSON.php +++ b/src/Migration/Sources/JSON.php @@ -214,6 +214,11 @@ protected function exportGroupBackups(int $batchSize, array $resources): void throw new \Exception('Not Implemented'); } + protected function exportGroupSettings(int $batchSize, array $resources): void + { + throw new \Exception('Not Implemented'); + } + /** * @throws \Exception */ diff --git a/src/Migration/Sources/NHost.php b/src/Migration/Sources/NHost.php index c72ca812..6c8fe0ac 100644 --- a/src/Migration/Sources/NHost.php +++ b/src/Migration/Sources/NHost.php @@ -962,4 +962,9 @@ protected function exportGroupBackups(int $batchSize, array $resources): void { throw new \Exception('Not Implemented'); } + + protected function exportGroupSettings(int $batchSize, array $resources): void + { + throw new \Exception('Not Implemented'); + } } diff --git a/src/Migration/Transfer.php b/src/Migration/Transfer.php index 15a0c8ae..069a780b 100644 --- a/src/Migration/Transfer.php +++ b/src/Migration/Transfer.php @@ -28,6 +28,8 @@ class Transfer public const GROUP_BACKUPS = 'backups'; + public const GROUP_SETTINGS = 'settings'; + public const GROUP_AUTH_RESOURCES = [ Resource::TYPE_USER, Resource::TYPE_TEAM, @@ -92,7 +94,9 @@ class Transfer Resource::TYPE_ATTRIBUTE ]; - public const GROUP_SETTINGS_RESOURCES = []; + public const GROUP_SETTINGS_RESOURCES = [ + Resource::TYPE_PROJECT_VARIABLE, + ]; public const GROUP_BACKUPS_RESOURCES = [ Resource::TYPE_BACKUP_POLICY, @@ -132,6 +136,9 @@ class Transfer Resource::TYPE_PLATFORM, Resource::TYPE_API_KEY, + // Settings + Resource::TYPE_PROJECT_VARIABLE, + // legacy Resource::TYPE_DOCUMENT, Resource::TYPE_ATTRIBUTE, diff --git a/tests/Migration/Unit/Adapters/MockSource.php b/tests/Migration/Unit/Adapters/MockSource.php index 27ac181b..28002376 100644 --- a/tests/Migration/Unit/Adapters/MockSource.php +++ b/tests/Migration/Unit/Adapters/MockSource.php @@ -229,4 +229,15 @@ protected function exportGroupBackups(int $batchSize, array $resources): void $this->handleResourceTransfer(Transfer::GROUP_BACKUPS, $resource); } } + + protected function exportGroupSettings(int $batchSize, array $resources): void + { + foreach (Transfer::GROUP_SETTINGS_RESOURCES as $resource) { + if (!\in_array($resource, $resources)) { + continue; + } + + $this->handleResourceTransfer(Transfer::GROUP_SETTINGS, $resource); + } + } }