diff --git a/src/Migration/Destination.php b/src/Migration/Destination.php index f3206533..b3e42612 100644 --- a/src/Migration/Destination.php +++ b/src/Migration/Destination.php @@ -26,13 +26,16 @@ public function setSource(Source $source): self * * @param array $resources Resources to transfer * @param callable(array): void $callback to run after transfer - * @param string $rootResourceId Root resource ID, If enabled you can only transfer a single root resource + * @param string $rootResourceId Root resource ID. If set, only this root resource is transferred. + * @param string $rootResourceType Resource type for $rootResourceId. + * @param string $rootResourceChildId Optional child filter under the root resource. For database roots, this is the collection/table ID. */ public function run( array $resources, callable $callback, string $rootResourceId = '', string $rootResourceType = '', + string $rootResourceChildId = '', ): void { $this->source->run( $resources, @@ -41,6 +44,7 @@ function (array $resources) use ($callback) { }, $rootResourceId, $rootResourceType, + $rootResourceChildId, ); } diff --git a/src/Migration/Destinations/CSV.php b/src/Migration/Destinations/CSV.php index 790e6a0a..d6d4b506 100644 --- a/src/Migration/Destinations/CSV.php +++ b/src/Migration/Destinations/CSV.php @@ -17,7 +17,8 @@ class CSV extends Destination { protected Device $deviceForFiles; - protected string $resourceId; + protected string $databaseId; + protected string $tableId; protected string $directory; protected string $outputFile; protected Local $local; @@ -32,7 +33,8 @@ class CSV extends Destination */ public function __construct( Device $deviceForFiles, - string $resourceId, + string $databaseId, + string $tableId, string $directory, string $filename, array $allowedColumns = [], @@ -42,7 +44,8 @@ public function __construct( private readonly bool $includeHeaders = true, ) { $this->deviceForFiles = $deviceForFiles; - $this->resourceId = $resourceId; + $this->databaseId = $databaseId; + $this->tableId = $tableId; $this->directory = $directory; $this->outputFile = $this->sanitizeFilename($filename); $this->local = new Local(\sys_get_temp_dir() . '/csv_export_' . uniqid()); @@ -168,7 +171,7 @@ public function shutdown(): void $destPath = $this->deviceForFiles->getPath($this->directory . '/' . $filename); if (!$this->local->exists($sourcePath)) { - throw new \Exception("No data to export for resource: $this->resourceId", MigrationException::CODE_NOT_FOUND); + throw new \Exception("No data to export for table {$this->tableId} in database {$this->databaseId}", MigrationException::CODE_NOT_FOUND); } try { @@ -193,7 +196,7 @@ public function shutdown(): void UtopiaResource::TYPE_ROW, Transfer::GROUP_DATABASES, 'Error cleaning up: ' . $this->local->getRoot(), - $this->resourceId + $this->tableId )); } } diff --git a/src/Migration/Destinations/JSON.php b/src/Migration/Destinations/JSON.php index 6660ab5a..3cafc999 100644 --- a/src/Migration/Destinations/JSON.php +++ b/src/Migration/Destinations/JSON.php @@ -18,7 +18,8 @@ class JSON extends Destination { protected Device $deviceForFiles; - protected string $resourceId; + protected string $databaseId; + protected string $tableId; protected string $directory; protected string $outputFile; protected Local $local; @@ -36,13 +37,15 @@ class JSON extends Destination */ public function __construct( Device $deviceForFiles, - string $resourceId, + string $databaseId, + string $tableId, string $directory, string $filename, array $allowedColumns = [], ) { $this->deviceForFiles = $deviceForFiles; - $this->resourceId = $resourceId; + $this->databaseId = $databaseId; + $this->tableId = $tableId; $this->directory = $directory; $this->outputFile = $this->sanitizeFilename($filename); @@ -178,7 +181,7 @@ public function shutdown(): void $destPath = $this->deviceForFiles->getPath($this->directory . '/' . $filename); if (!$this->local->exists($sourcePath)) { - throw new Exception("No data to export for resource: $this->resourceId"); + throw new Exception("No data to export for table {$this->tableId} in database {$this->databaseId}"); } $handle = null; @@ -217,7 +220,7 @@ public function shutdown(): void UtopiaResource::TYPE_ROW, Transfer::GROUP_DATABASES, 'Error cleaning up: ' . $this->local->getRoot(), - $this->resourceId + $this->tableId )); } } diff --git a/src/Migration/Source.php b/src/Migration/Source.php index cd38ebbd..2191c5a2 100644 --- a/src/Migration/Source.php +++ b/src/Migration/Source.php @@ -65,12 +65,15 @@ public function callback(array $resources): void * * @param array $resources Resources to transfer * @param callable $callback Callback to run after transfer - * @param string $rootResourceId Root resource ID, If enabled you can only transfer a single root resource + * @param string $rootResourceId Root resource ID. If set, only this root resource is transferred. + * @param string $rootResourceType Resource type for $rootResourceId. Required when $rootResourceId is set. + * @param string $rootResourceChildId Optional child filter under the root resource. For database roots, this is the collection/table ID. */ - public function run(array $resources, callable $callback, string $rootResourceId = '', string $rootResourceType = ''): void + public function run(array $resources, callable $callback, string $rootResourceId = '', string $rootResourceType = '', string $rootResourceChildId = ''): void { $this->rootResourceId = $rootResourceId; $this->rootResourceType = $rootResourceType; + $this->rootResourceChildId = $rootResourceChildId; $this->transferCallback = function (array $returnedResources) use ($callback, $resources) { $prunedResources = []; diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 738a8093..913f28b4 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -830,17 +830,7 @@ private function exportDatabases(int $batchSize, array $resources = []): void $queries = [$this->reader->queryLimit($batchSize)]; if ($this->rootResourceId !== '' && ($this->rootResourceType === Resource::TYPE_DATABASE || $this->rootResourceType === Resource::TYPE_DATABASE_DOCUMENTSDB)) { - $targetDatabaseId = $this->rootResourceId; - - // Handle database:collection format - extract database ID - if (\str_contains($this->rootResourceId, ':')) { - $parts = \explode(':', $this->rootResourceId, 2); - if (\count($parts) === 2) { - $targetDatabaseId = $parts[0]; - } - } - - $queries[] = $this->reader->queryEqual('$id', [$targetDatabaseId]); + $queries[] = $this->reader->queryEqual('$id', [$this->rootResourceId]); $queries[] = $this->reader->queryLimit(1); } @@ -904,24 +894,19 @@ private function exportEntities(string $databaseName, int $batchSize): void $queries = [$this->reader->queryLimit($batchSize)]; $tables = []; - // Filter to specific table if rootResourceType is database with database:collection format + // Filter to a specific table when the root is a database with a child set, or + // when the root itself is a table. if ( - $this->rootResourceId !== '' && - $this->rootResourceType === Resource::TYPE_DATABASE && - \str_contains($this->rootResourceId, ':') + $this->rootResourceChildId !== '' && + $this->rootResourceType === Resource::TYPE_DATABASE ) { - $parts = \explode(':', $this->rootResourceId, 2); - if (\count($parts) === 2) { - $targetTableId = $parts[1]; // table ID - $queries[] = $this->reader->queryEqual('$id', [$targetTableId]); - $queries[] = $this->reader->queryLimit(1); - } + $queries[] = $this->reader->queryEqual('$id', [$this->rootResourceChildId]); + $queries[] = $this->reader->queryLimit(1); } elseif ( $this->rootResourceId !== '' && $this->rootResourceType === Resource::TYPE_TABLE ) { - $targetTableId = $this->rootResourceId; - $queries[] = $this->reader->queryEqual('$id', [$targetTableId]); + $queries[] = $this->reader->queryEqual('$id', [$this->rootResourceId]); $queries[] = $this->reader->queryLimit(1); } elseif ($lastTable) { $queries[] = $this->reader->queryCursorAfter($lastTable); diff --git a/src/Migration/Sources/CSV.php b/src/Migration/Sources/CSV.php index 0595b956..21cf2bae 100644 --- a/src/Migration/Sources/CSV.php +++ b/src/Migration/Sources/CSV.php @@ -27,10 +27,9 @@ class CSV extends Source private string $filePath; - /** - * format: `{databaseId:tableId}` - */ - private string $resourceId; + private string $databaseId; + + private string $tableId; private Device $device; @@ -39,7 +38,8 @@ class CSV extends Source private bool $downloaded = false; public function __construct( - string $resourceId, + string $databaseId, + string $tableId, string $filePath, Device $device, ?UtopiaDatabase $dbForProject, @@ -47,7 +47,8 @@ public function __construct( ) { $this->device = $device; $this->filePath = $filePath; - $this->resourceId = $resourceId; + $this->databaseId = $databaseId; + $this->tableId = $tableId; $this->database = new DatabaseReader($dbForProject, $getDatabasesDB); } @@ -131,10 +132,8 @@ private function exportRows(int $batchSize): void $columns = []; $lastColumn = null; - [$databaseId, $tableId] = \explode(':', $this->resourceId); - $databases = $this->database->listDatabases([ - $this->database->queryEqual('$id', [$databaseId]), + $this->database->queryEqual('$id', [$this->databaseId]), $this->database->queryLimit(1), ]); @@ -157,8 +156,8 @@ private function exportRows(int $batchSize): void ]; $tablePayload = [ - 'id' => $tableId, - 'name' => $tableId, + 'id' => $this->tableId, + 'name' => $this->tableId, 'documentSecurity' => false, 'rowSecurity' => false, 'permissions' => [], @@ -504,7 +503,7 @@ private function validateCSVHeaders(array $headers, array $columnTypes, array $r UtopiaResource::TYPE_ROW, Transfer::GROUP_DATABASES, \implode(', ', $messages), - $this->resourceId + $this->tableId )); } } diff --git a/src/Migration/Sources/JSON.php b/src/Migration/Sources/JSON.php index 8f2a81ae..5f0c1602 100644 --- a/src/Migration/Sources/JSON.php +++ b/src/Migration/Sources/JSON.php @@ -22,10 +22,9 @@ class JSON extends Source { private string $filePath; - /** - * format: `{databaseId:tableId}` - */ - private string $resourceId; + private string $databaseId; + + private string $tableId; private Device $device; @@ -35,14 +34,16 @@ class JSON extends Source private bool $downloaded = false; public function __construct( - string $resourceId, + string $databaseId, + string $tableId, string $filePath, Device $device, ?UtopiaDatabase $dbForProject ) { $this->device = $device; $this->filePath = $filePath; - $this->resourceId = $resourceId; + $this->databaseId = $databaseId; + $this->tableId = $tableId; /* kept for composer check */ $this->dbForProject = $dbForProject; @@ -120,9 +121,8 @@ protected function exportGroupDatabases(int $batchSize, array $resources): void */ private function exportRows(int $batchSize): void { - [$databaseId, $tableId] = \explode(':', $this->resourceId); - $database = new Database($databaseId, ''); - $table = new Table($database, '', $tableId); + $database = new Database($this->databaseId, ''); + $table = new Table($database, '', $this->tableId); $this->withJsonItems(function ($items) use ($table, $batchSize) { $buffer = []; diff --git a/src/Migration/Target.php b/src/Migration/Target.php index 777609f2..8f76e24e 100644 --- a/src/Migration/Target.php +++ b/src/Migration/Target.php @@ -33,6 +33,8 @@ abstract class Target protected string $rootResourceId = ''; + protected string $rootResourceChildId = ''; + protected string $rootResourceType = ''; abstract public static function getName(): string; @@ -49,9 +51,10 @@ public function registerCache(Cache &$cache): void * * @param array $resources Resources to transfer * @param callable $callback Callback to run after transfer - * @param string $rootResourceId Root resource ID, If enabled you can only transfer a single root resource + * @param string $rootResourceId Root resource ID. If set, only this root resource is transferred. + * @param string $rootResourceChildId Optional child filter under the root resource. For database roots, this is the collection/table ID. */ - abstract public function run(array $resources, callable $callback, string $rootResourceId = ''): void; + abstract public function run(array $resources, callable $callback, string $rootResourceId = '', string $rootResourceChildId = ''): void; /** * Report Resources diff --git a/src/Migration/Transfer.php b/src/Migration/Transfer.php index e8c5ee5d..fe28fac0 100644 --- a/src/Migration/Transfer.php +++ b/src/Migration/Transfer.php @@ -267,7 +267,9 @@ public function getStatusCounters(): array * * @param array $resources Resources to transfer * @param callable $callback Callback to run after transfer - * @param string|null $rootResourceId Root resource ID, If enabled you can only transfer a single root resource + * @param string|null $rootResourceId Root resource ID. If set, only this root resource is transferred. + * @param string|null $rootResourceType Resource type for $rootResourceId. Required when $rootResourceId is set. + * @param string|null $rootResourceChildId Optional child filter under the root resource. For database roots, this is the collection/table ID. * @throws \Exception */ public function run( @@ -275,11 +277,13 @@ public function run( callable $callback, ?string $rootResourceId = null, ?string $rootResourceType = null, + ?string $rootResourceChildId = null, ): void { // Allows you to push entire groups if you want. $computedResources = []; $rootResourceId = $rootResourceId ?? ''; $rootResourceType = $rootResourceType ?? ''; + $rootResourceChildId = $rootResourceChildId ?? ''; foreach ($resources as $resource) { if (is_array($resource)) { @@ -320,6 +324,7 @@ public function run( $callback, $rootResourceId, $rootResourceType, + $rootResourceChildId, ); } diff --git a/tests/Migration/Unit/General/CSVTest.php b/tests/Migration/Unit/General/CSVTest.php index e4fd3ec1..e7ef661d 100644 --- a/tests/Migration/Unit/General/CSVTest.php +++ b/tests/Migration/Unit/General/CSVTest.php @@ -81,7 +81,7 @@ public function testCSVExportBasic() $exportDevice = new Local($tempDir); // Create CSV destination - $csvDestination = new TestCSV($exportDevice, 'test_db:test_table_id', '', 'test_db_test_table_id'); + $csvDestination = new TestCSV($exportDevice, 'test_db', 'test_table_id', '', 'test_db_test_table_id'); // Create test data $database = new Database('test_db'); @@ -156,7 +156,7 @@ public function testCSVExportWithSpecialCharacters() $tempDir = sys_get_temp_dir() . '/csv_test_special_' . uniqid(); $exportDevice = new Local($tempDir); - $csvDestination = new TestCSV($exportDevice, 'test_db:test_table_id', '', 'test_db_test_table_id'); + $csvDestination = new TestCSV($exportDevice, 'test_db', 'test_table_id', '', 'test_db_test_table_id'); $database = new Database('test_db'); $table = new Table($database, 'test_table', 'test_table_id'); @@ -204,7 +204,7 @@ public function testCSVExportWithArrays() $tempDir = sys_get_temp_dir() . '/csv_test_arrays_' . uniqid(); $exportDevice = new Local($tempDir); - $csvDestination = new TestCSV($exportDevice, 'test_db:test_table_id', '', 'test_db_test_table_id'); + $csvDestination = new TestCSV($exportDevice, 'test_db', 'test_table_id', '', 'test_db_test_table_id'); $database = new Database('test_db'); $table = new Table($database, 'test_table', 'test_table_id'); @@ -251,7 +251,7 @@ public function testCSVExportWithNullValues() $tempDir = sys_get_temp_dir() . '/csv_test_nulls_' . uniqid(); $exportDevice = new Local($tempDir); - $csvDestination = new TestCSV($exportDevice, 'test_db:test_table_id', '', 'test_db_test_table_id'); + $csvDestination = new TestCSV($exportDevice, 'test_db', 'test_table_id', '', 'test_db_test_table_id'); $database = new Database('test_db'); $table = new Table($database, 'test_table', 'test_table_id'); @@ -300,7 +300,7 @@ public function testCSVExportWithAllowedAttributes() $exportDevice = new Local($tempDir); // Only allow specific attributes - $csvDestination = new TestCSV($exportDevice, 'test_db:test_table_id', '', 'test_db_test_table_id', ['name', 'email']); + $csvDestination = new TestCSV($exportDevice, 'test_db', 'test_table_id', '', 'test_db_test_table_id', ['name', 'email']); $database = new Database('test_db'); $table = new Table($database, 'test_table', 'test_table_id'); @@ -351,7 +351,7 @@ public function testCSVExportImportCompatibility() $exportDevice = new Local($tempDir); // Export data - $csvDestination = new TestCSV($exportDevice, 'test_db:test_table_id', '', 'test_db_test_table_id'); + $csvDestination = new TestCSV($exportDevice, 'test_db', 'test_table_id', '', 'test_db_test_table_id'); $database = new Database('test_db'); $table = new Table($database, 'test_table', 'test_table_id'); diff --git a/tests/Migration/Unit/General/JSONTest.php b/tests/Migration/Unit/General/JSONTest.php index 1a880ec7..4b425df4 100644 --- a/tests/Migration/Unit/General/JSONTest.php +++ b/tests/Migration/Unit/General/JSONTest.php @@ -198,7 +198,8 @@ public function testJSONExportWithAllowedAttributes() $jsonDestination = new DestinationJSON( new Local($tempDir), - 'test_db:test_table_id', + 'test_db', + 'test_table_id', '', 'test_db_test_table_id', ['name', 'email'] @@ -236,7 +237,8 @@ private function createDestination(string $tempDir): DestinationJSON { return new DestinationJSON( new Local($tempDir), - 'test_db:test_table_id', + 'test_db', + 'test_table_id', '', 'test_db_test_table_id' );