*/ public static array $versions = [ '1.0.0-RC1' => 'V15', '1.0.0' => 'V15', '1.0.1' => 'V15', '1.0.3' => 'V15', '1.1.0' => 'V16', '1.1.1' => 'V16', '1.1.2' => 'V16', '1.2.0' => 'V17', '1.2.1' => 'V17', '1.3.0' => 'V18', '1.3.1' => 'V18', '1.3.2' => 'V18', '1.3.3' => 'V18', '1.3.4' => 'V18', '1.3.5' => 'V18', '1.3.6' => 'V18', '1.3.7' => 'V18', '1.3.8' => 'V18', '1.4.0' => 'V19', '1.4.1' => 'V19', '1.4.2' => 'V19', '1.4.3' => 'V19', '1.4.4' => 'V19', '1.4.5' => 'V19', '1.4.6' => 'V19', '1.4.7' => 'V19', '1.4.8' => 'V19', '1.4.9' => 'V19', '1.4.10' => 'V19', '1.4.11' => 'V19', '1.4.12' => 'V19', '1.4.13' => 'V19', '1.5.0' => 'V20', '1.5.1' => 'V20', '1.5.2' => 'V20', '1.5.3' => 'V20', '1.5.4' => 'V20', '1.5.5' => 'V20', '1.5.6' => 'V20', '1.5.7' => 'V20', '1.5.8' => 'V20', '1.5.9' => 'V20', '1.5.10' => 'V20', '1.5.11' => 'V20', '1.6.0' => 'V21', '1.6.1' => 'V21', '1.6.2' => 'V21', '1.7.0-RC1' => 'V22', '1.7.0' => 'V22', '1.7.1' => 'V22', '1.7.2' => 'V22', '1.7.3' => 'V22', '1.7.4' => 'V22', '1.8.0' => 'V23' ]; /** * @var array> */ protected array $collections; public function __construct() { Authorization::disable(); Authorization::setDefaultStatus(false); $this->collections = Config::getParam('collections', []); $this->collections['projects']['_metadata'] = [ '$id' => ID::custom('_metadata'), '$collection' => Database::METADATA, ]; $this->collections['projects']['audit'] = [ '$id' => ID::custom('audit'), '$collection' => Database::METADATA, ]; } /** * Set project for migration. * * @param Document $project * @param Database $dbForProject * @param Database $dbForPlatform * @param callable|null $getProjectDB * @return self */ public function setProject( Document $project, Database $dbForProject, Database $dbForPlatform, ?callable $getProjectDB = null ): self { $this->project = $project; $this->dbForProject = $dbForProject; $this->dbForPlatform = $dbForPlatform; $this->getProjectDB = $getProjectDB; return $this; } /** * Set PDO for Migration. * * @param PDO $pdo * @return Migration */ public function setPDO(PDO $pdo): self { $this->pdo = $pdo; return $this; } /** * Iterates through every document. * * @param callable $callback * @throws Exception */ public function forEachDocument(callable $callback): void { $projectInternalId = $this->project->getSequence(); $collections = match ($projectInternalId) { 'console' => $this->collections['console'], default => $this->collections['projects'], }; foreach ($collections as $collection) { // Only migrate top-level collections if ($collection['$collection'] !== Database::METADATA) { continue; } Console::log('Migrating documents for collection "' . $collection['$id'] . '"'); $this->dbForProject->foreach($collection['$id'], function (Document $document) use ($collection, $callback) { if (empty($document->getId()) || empty($document->getCollection())) { return; } $old = $document->getArrayCopy(); $new = $callback($document); if ($new === null || $new->getArrayCopy() == $old) { return; } try { $this->dbForProject->updateDocument( $document->getCollection(), $document->getId(), $document ); } catch (\Throwable $th) { Console::error("Failed to update document \"{$document->getId()}\" in collection \"{$collection['$id']}\":" . $th->getMessage()); return; } }); } } /** * Creates collection from the config collection. * * @param string $id * @param string|null $name * @return void * @throws \Throwable */ protected function createCollection(string $id, string $name = null): void { $name ??= $id; $collectionType = match ($this->project->getSequence()) { 'console' => 'console', default => 'projects', }; if (!$this->dbForProject->getCollection($id)->isEmpty()) { return; } $collection = $this->collections[$collectionType][$id]; $attributes = []; foreach ($collection['attributes'] as $attribute) { $attributes[] = new Document($attribute); } $indexes = []; foreach ($collection['indexes'] as $index) { $indexes[] = new Document($index); } try { $this->dbForProject->createCollection($name, $attributes, $indexes); } catch (Duplicate) { Console::warning('Failed to create collection "' . $name . '": Collection already exists'); } } /** * Creates attributes from collections.php * * @param Database $database * @param string $collectionId * @param array $attributeIds * @param string|null $from * @return void * @throws \Utopia\Database\Exception * @throws \Utopia\Database\Exception\Authorization * @throws Conflict * @throws Duplicate * @throws Limit * @throws Structure */ public function createAttributesFromCollection( Database $database, string $collectionId, array $attributeIds, string $from = null ): void { $from ??= $collectionId; $collectionType = match ($this->project->getSequence()) { 'console' => 'console', default => 'projects', }; if ($from === 'files') { $collectionType = 'buckets'; } $collection = $this->collections[$collectionType][$from] ?? null; if ($collection === null) { throw new Exception("Collection {$from} not found"); } $attributesToCreate = []; $attributes = $collection['attributes']; $attributeKeys = \array_column($collection['attributes'], '$id'); foreach ($attributeIds as $attributeId) { $attributeKey = \array_search($attributeId, $attributeKeys); if ($attributeKey === false) { throw new Exception("Attribute {$attributeId} not found"); } $attribute = $attributes[$attributeKey]; $attribute['filters'] ??= []; $attribute['default'] ??= null; $attribute['default'] = \in_array('json', $attribute['filters']) ? \json_encode($attribute['default']) : $attribute['default']; $attributesToCreate[] = $attribute; } $database->createAttributes( collection: $collectionId, attributes: $attributesToCreate, ); } /** * Creates attribute from collections.php * * @param Database $database * @param string $collectionId * @param string $attributeId * @param string|null $from * @return void * @throws \Utopia\Database\Exception * @throws \Utopia\Database\Exception\Authorization * @throws Conflict * @throws Duplicate * @throws Limit * @throws Structure */ public function createAttributeFromCollection( Database $database, string $collectionId, string $attributeId, string $from = null ): void { $from ??= $collectionId; $collectionType = match ($this->project->getSequence()) { 'console' => 'console', default => 'projects', }; if ($from === 'files') { $collectionType = 'buckets'; } $collection = $this->collections[$collectionType][$from] ?? null; if ($collection === null) { throw new Exception("Collection {$from} not found"); } $attributes = $collection['attributes']; $attributeKey = \array_search($attributeId, \array_column($attributes, '$id')); if ($attributeKey === false) { throw new Exception("Attribute {$attributeId} not found"); } $attribute = $attributes[$attributeKey]; $filters = $attribute['filters'] ?? []; $default = $attribute['default'] ?? null; $database->createAttribute( collection: $collectionId, id: $attributeId, type: $attribute['type'], size: $attribute['size'], required: $attribute['required'], default: \in_array('json', $filters) ? \json_encode($default) : $default, signed: $attribute['signed'] ?? true, array: $attribute['array'] ?? false, format: $attribute['format'] ?? '', formatOptions: $attribute['formatOptions'] ?? [], filters: $filters, ); } /** * Creates index from collections.php * * @param Database $database * @param string $collectionId * @param string $indexId * @param string|null $from * @return void * @throws \Exception * @throws Duplicate * @throws Limit */ public function createIndexFromCollection(Database $database, string $collectionId, string $indexId, string $from = null): void { $from ??= $collectionId; $collectionType = match ($this->project->getSequence()) { 'console' => 'console', default => 'projects', }; if ($from === 'files') { $collectionType = 'buckets'; } $collection = $this->collections[$collectionType][$from] ?? null; if ($collection === null) { throw new Exception("Collection {$collectionId} not found"); } $indexes = $collection['indexes']; $indexKey = \array_search($indexId, \array_column($indexes, '$id')); if ($indexKey === false) { throw new Exception("Index {$indexId} not found"); } $index = $indexes[$indexKey]; $database->createIndex( collection: $collectionId, id: $indexId, type: $index['type'], attributes: $index['attributes'], lengths: $index['lengths'] ?? [], orders: $index['orders'] ?? [] ); } /** * Change a collection attribute's internal type * * @param string $collection * @param string $attribute * @param string $type * @return void * @throws \Utopia\Database\Exception */ protected function changeAttributeInternalType(string $collection, string $attribute, string $type): void { $stmt = $this->pdo->prepare("ALTER TABLE `{$this->dbForProject->getDatabase()}`.`_{$this->project->getSequence()}_{$collection}` MODIFY `$attribute` $type;"); try { $stmt->execute(); } catch (\Throwable $e) { Console::warning($e->getMessage()); } } /** * Executes migration for set project. */ abstract public function execute(): void; }