From 566c6406ce5c08b4cbf6b1531840bd57d12df7e6 Mon Sep 17 00:00:00 2001 From: Darshan Date: Tue, 20 Jan 2026 12:57:43 +0530 Subject: [PATCH 1/3] feat: json. --- composer.json | 3 +- composer.lock | 64 +++- src/Migration/Destinations/JSON.php | 314 ++++++++++++++++++ src/Migration/Sources/JSON.php | 271 +++++++++++++++ tests/Migration/Unit/General/CSVTest.php | 6 +- .../Migration/Unit/General/JSONExportTest.php | 291 ++++++++++++++++ 6 files changed, 944 insertions(+), 5 deletions(-) create mode 100644 src/Migration/Destinations/JSON.php create mode 100644 src/Migration/Sources/JSON.php create mode 100644 tests/Migration/Unit/General/JSONExportTest.php diff --git a/composer.json b/composer.json index 4cefc3d..806fedb 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "utopia-php/database": "4.*", "utopia-php/storage": "0.18.*", "utopia-php/dsn": "0.2.*", - "utopia-php/console": "0.0.*" + "utopia-php/console": "0.0.*", + "halaxa/json-machine": "^1.2" }, "require-dev": { "ext-pdo": "*", diff --git a/composer.lock b/composer.lock index fb29c8a..48cd96b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cf3cc5c8ea77349cef37794a3a47f45e", + "content-hash": "5bece3d3d0a709fd79f03912b6b5ab6d", "packages": [ { "name": "appwrite/appwrite", @@ -229,6 +229,68 @@ }, "time": "2026-01-12T17:58:43+00:00" }, + { + "name": "halaxa/json-machine", + "version": "1.2.6", + "source": { + "type": "git", + "url": "https://github.com/halaxa/json-machine.git", + "reference": "8bf0b0ff6ff60ab480778eaa5ad7d505b442c2d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/halaxa/json-machine/zipball/8bf0b0ff6ff60ab480778eaa5ad7d505b442c2d4", + "reference": "8bf0b0ff6ff60ab480778eaa5ad7d505b442c2d4", + "shasum": "" + }, + "require": { + "php": "7.2 - 8.5" + }, + "require-dev": { + "ext-json": "*", + "friendsofphp/php-cs-fixer": "^3.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.0" + }, + "suggest": { + "ext-json": "To run JSON Machine out of the box without custom decoders.", + "guzzlehttp/guzzle": "To run example with GuzzleHttp" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "JsonMachine\\": "src/" + }, + "exclude-from-classmap": [ + "src/autoloader.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Filip Halaxa", + "email": "filip@halaxa.cz" + } + ], + "description": "Efficient, easy-to-use and fast JSON pull parser", + "support": { + "issues": "https://github.com/halaxa/json-machine/issues", + "source": "https://github.com/halaxa/json-machine/tree/1.2.6" + }, + "funding": [ + { + "url": "https://ko-fi.com/G2G57KTE4", + "type": "other" + } + ], + "time": "2025-12-05T14:53:09+00:00" + }, { "name": "mongodb/mongodb", "version": "2.1.2", diff --git a/src/Migration/Destinations/JSON.php b/src/Migration/Destinations/JSON.php new file mode 100644 index 0000000..cbb2d0c --- /dev/null +++ b/src/Migration/Destinations/JSON.php @@ -0,0 +1,314 @@ +deviceForFiles = $deviceForFiles; + $this->resourceId = $resourceId; + $this->directory = $directory; + $this->outputFile = $this->sanitizeFilename($filename); + + /* local settings */ + $this->local = new Local(\sys_get_temp_dir() . '/json_export_' . uniqid()); + $this->local->setTransferChunkSize(Transfer::STORAGE_MAX_CHUNK_SIZE); + $this->createDirectory($this->local->getRoot()); + + foreach ($allowedColumns as $attribute) { + $this->allowedColumns[$attribute] = true; + } + } + + public static function getName(): string + { + return 'JSON'; + } + + public static function getSupportedResources(): array + { + return [ + UtopiaResource::TYPE_ROW, + ]; + } + + public function report(array $resources = [], array $resourceIds = []): array + { + return []; + } + + /** + * @param array $resources + * @throws Exception + */ + protected function import(array $resources, callable $callback): void + { + $handle = null; + $buffer = ''; + $bufferSize = 0; + $bufferBytes = 1024 * 1024; // 1MB + $log = $this->local->getRoot() . '/' . $this->outputFile . '.json'; + + $openHandle = function () use (&$handle, $log): void { + if (isset($handle)) { + return; + } + $handle = \fopen($log, 'a'); + if ($handle === false) { + throw new Exception("Failed to open file for writing: $log"); + } + }; + + $flushBuffer = function () use (&$handle, &$buffer, &$bufferSize, $openHandle): void { + if ($buffer === '') { + return; + } + $openHandle(); + if (\fwrite($handle, $buffer) === false) { + throw new Exception('Failed to write JSON data to file.'); + } + $buffer = ''; + $bufferSize = 0; + }; + + $append = function (string $chunk) use (&$buffer, &$bufferSize, $bufferBytes, $flushBuffer): void { + $buffer .= $chunk; + $bufferSize += \strlen($chunk); + if ($bufferSize >= $bufferBytes) { + $flushBuffer(); + } + }; + + try { + if (!$this->jsonStarted) { + $append('['); + $this->jsonStarted = true; + } + + foreach ($resources as $resource) { + if (!($resource instanceof Row)) { + continue; + } + + $jsonData = $this->resourceToJSONData($resource); + try { + $json = \json_encode( + $jsonData, + JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE + ); + } catch (Exception $e) { + $resource->setStatus(UtopiaResource::STATUS_ERROR, $e->getMessage()); + $this->addError(new MigrationException( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: $e->getMessage(), + previous: $e, + )); + continue; + } + + if ($this->jsonHasItems) { + $append(','); + } + + $append($json); + $this->jsonHasItems = true; + + $resource->setStatus(UtopiaResource::STATUS_SUCCESS); + if (isset($this->cache)) { + $this->cache->update($resource); + } + } + + $flushBuffer(); + } finally { + if (\is_resource($handle)) { + \fclose($handle); + } + } + + $callback($resources); + } + + /** + * @throws Exception + */ + public function shutdown(): void + { + $filename = $this->outputFile . '.json'; + $sourcePath = $this->local->getPath($filename); + $destPath = $this->deviceForFiles->getPath($this->directory . '/' . $filename); + + if (!$this->local->exists($sourcePath)) { + throw new Exception("No data to export for resource: $this->resourceId"); + } + + $handle = null; + try { + $handle = \fopen($sourcePath, 'a'); + if ($handle === false) { + throw new Exception("Failed to open file for writing: $sourcePath"); + } + if (!$this->jsonStarted) { + \fwrite($handle, '['); + $this->jsonStarted = true; + } + \fwrite($handle, ']'); + } finally { + if (\is_resource($handle)) { + \fclose($handle); + } + } + + try { + $result = $this->local->transfer( + $sourcePath, + $destPath, + $this->deviceForFiles + ); + if ($result === false) { + throw new Exception('Error transferring to ' . $this->deviceForFiles->getRoot() . '/' . $filename); + } + if (!$this->deviceForFiles->exists($destPath)) { + throw new Exception('File not found on destination: ' . $destPath); + } + } finally { + // Clean up the temporary directory + if (!$this->local->deletePath('') || $this->local->exists($this->local->getRoot())) { + Console::error('Error cleaning up: ' . $this->local->getRoot()); + } + } + } + + /** + * Helper to ensure a directory exists. + * @throws Exception + */ + protected function createDirectory(string $path): void + { + if (!\file_exists($path)) { + if (!\mkdir($path, 0755, true)) { + throw new Exception('Error creating directory: ' . $path); + } + } + } + + /** + * Sanitize a filename to make it filesystem-safe + */ + protected function sanitizeFilename(string $filename): string + { + $sanitized = \preg_replace('/[:\\/<>"|*?]/', '_', $filename); + $sanitized = \preg_replace('/[^\x20-\x7E]/', '_', $sanitized); + $sanitized = \trim($sanitized); + return empty($sanitized) ? 'export' : $sanitized; + } + + /** + * Convert a resource to JSON-compatible data + */ + protected function resourceToJSONData(Row $resource): array + { + $rowData = $resource->getData(); + + $data = [ + '$id' => $resource->getId(), + '$permissions' => $resource->getPermissions(), + '$createdAt' => $rowData['$createdAt'] ?? '', + '$updatedAt' => $rowData['$updatedAt'] ?? '', + ]; + + unset( + $rowData['$createdAt'], + $rowData['$updatedAt'], + ); + + if (empty($this->allowedColumns)) { + $data = \array_merge($data, $rowData); + } else { + foreach ($rowData as $key => $value) { + if (isset($this->allowedColumns[$key])) { + $data[$key] = $value; + } + } + } + + foreach ($data as $key => $value) { + $data[$key] = $this->convertValueToJSON($value); + } + + return $data; + } + + /** + * Convert a single value to JSON-compatible format + */ + protected function convertValueToJSON(mixed $value): mixed + { + if (\is_null($value) || \is_bool($value) || \is_int($value) || \is_float($value) || \is_string($value)) { + return $value; + } + + if (\is_array($value)) { + return $this->convertArrayToJSON($value); + } + + if (\is_object($value)) { + return $value; + } + + return (string) $value; + } + + /** + * Convert array to JSON format + */ + protected function convertArrayToJSON(array $value): array + { + if (empty($value)) { + return []; + } + + return array_map(function ($item) { + return $this->convertValueToJSON($item); + }, $value); + } + +} diff --git a/src/Migration/Sources/JSON.php b/src/Migration/Sources/JSON.php new file mode 100644 index 0000000..7af4b89 --- /dev/null +++ b/src/Migration/Sources/JSON.php @@ -0,0 +1,271 @@ +device = $device; + $this->filePath = $filePath; + $this->resourceId = $resourceId; + } + + public static function getName(): string + { + return 'JSON'; + } + + public static function getSupportedResources(): array + { + return [ + UtopiaResource::TYPE_ROW, + ]; + } + + /** + * @throws \Exception + */ + public function report(array $resources = [], array $resourceIds = []): array + { + $report = []; + + if (!$this->device->exists($this->filePath)) { + return $report; + } + + $this->downloadToLocal( + $this->device, + $this->filePath, + ); + + $items = Items::fromFile($this->filePath, [ + 'decoder' => new PassThruDecoder(), + ]); + + $report[UtopiaResource::TYPE_ROW] = \iterator_count($items); + + return $report; + } + + /** + * @throws \Exception + */ + protected function exportGroupAuth(int $batchSize, array $resources): void + { + throw new \Exception('Not Implemented'); + } + + protected function exportGroupDatabases(int $batchSize, array $resources): void + { + try { + if (UtopiaResource::isSupported(UtopiaResource::TYPE_ROW, $resources)) { + $this->exportRows($batchSize); + } + } catch (\Throwable $e) { + $this->addError( + new Exception( + UtopiaResource::TYPE_ROW, + Transfer::GROUP_DATABASES, + message: $e->getMessage(), + code: $e->getCode(), + previous: $e + ) + ); + } finally { + // delete the temporary file! + $this->device->delete($this->filePath); + } + } + + /** + * @throws \Exception + */ + private function exportRows(int $batchSize): void + { + [$databaseId, $tableId] = \explode(':', $this->resourceId); + $database = new Database($databaseId, ''); + $table = new Table($database, '', $tableId); + + $this->withJsonItems(function ($items) use ($table, $batchSize) { + $buffer = []; + + foreach ($items as $index => $item) { + if (!\is_array($item)) { + throw new \Exception("JSON item at index $index is not an object."); + } + + $rowId = $item['$id'] ?? 'unique()'; + $permissions = []; + if (\array_key_exists('$permissions', $item)) { + $permissions = $this->validatePermissions($item['$permissions']); + } + + unset($item['$id'], $item['$permissions']); + + $row = new Row( + $rowId, + $table, + $item, + $permissions, + ); + + $buffer[] = $row; + + if (\count($buffer) === $batchSize) { + $this->callback($buffer); + $buffer = []; + } + } + + if (!empty($buffer)) { + $this->callback($buffer); + } + }); + } + + /** + * @throws \Exception + */ + protected function exportGroupStorage(int $batchSize, array $resources): void + { + throw new \Exception('Not Implemented'); + } + + /** + * @throws \Exception + */ + protected function exportBuckets(int $batchSize): void + { + throw new \Exception('Not Implemented'); + } + + /** + * @throws \Exception + */ + private function exportFiles(int $batchSize): void + { + throw new \Exception('Not Implemented'); + } + + /** + * @throws \Exception + */ + private function exportFile(File $file): void + { + throw new \Exception('Not Implemented'); + } + + /** + * @throws \Exception + */ + protected function exportGroupFunctions(int $batchSize, array $resources): void + { + throw new \Exception('Not Implemented'); + } + + /** + * @param callable(Items): void $callback + * @throws \Exception|JsonMachineException + */ + private function withJsonItems(callable $callback): void + { + if (!$this->device->exists($this->filePath)) { + return; + } + + if (!$this->downloaded) { + $this->downloadToLocal( + $this->device, + $this->filePath, + ); + } + + $items = Items::fromFile($this->filePath, [ + 'decoder' => new ExtJsonDecoder(true), + ]); + + $callback($items); + } + + /** + * @throws \Exception + */ + private function downloadToLocal( + Device $device, + string $filePath + ): void { + if ($this->downloaded + || $device->getType() === Storage::DEVICE_LOCAL + ) { + return; + } + + try { + $success = $device->transfer( + $filePath, + $filePath, + new Device\Local('/'), + ); + } catch (\Exception $e) { + $success = false; + } + + if (!$success) { + throw new \Exception('Failed to transfer JSON file from device to local storage.', previous: $e ?? null); + } + + $this->downloaded = true; + } + + /** + * @return array + * @throws \Exception + */ + private function validatePermissions(mixed $permissions): array + { + if (!\is_array($permissions)) { + throw new \Exception('Invalid permissions format; expected an array of strings.'); + } + + foreach ($permissions as $value) { + if (!\is_string($value)) { + throw new \Exception('Invalid permission value; expected string.'); + } + } + + return $permissions; + } +} diff --git a/tests/Migration/Unit/General/CSVTest.php b/tests/Migration/Unit/General/CSVTest.php index 6971991..530a596 100644 --- a/tests/Migration/Unit/General/CSVTest.php +++ b/tests/Migration/Unit/General/CSVTest.php @@ -92,14 +92,14 @@ public function testCSVExportBasic() 'age' => 30, 'email' => 'john@example.com' ]); - $row1->setPermissions(['read' => ['user:123']]); + $row1->setPermissions(['read("user:123")']); $row2 = new Row('row2', $table, [ 'name' => 'Jane Smith', 'age' => 25, 'email' => 'jane@example.com' ]); - $row2->setPermissions(['read' => ['user:456']]); + $row2->setPermissions(['read("user:456")']); // Export the data $csvDestination->testableImport([$row1, $row2], function ($resources) { @@ -363,7 +363,7 @@ public function testCSVExportImportCompatibility() ]; $row = new Row('compat_row', $table, $originalData); - $row->setPermissions(['read' => ['user:123']]); + $row->setPermissions(['read("user:123")']); $csvDestination->testableImport([$row], function ($resources) {}); $csvDestination->shutdown(); diff --git a/tests/Migration/Unit/General/JSONExportTest.php b/tests/Migration/Unit/General/JSONExportTest.php new file mode 100644 index 0000000..2db628b --- /dev/null +++ b/tests/Migration/Unit/General/JSONExportTest.php @@ -0,0 +1,291 @@ +createDestination($tempDir); + + $database = new Database('test_db'); + $table = new Table($database, 'test_table', 'test_table_id'); + + $row1 = new Row('row1', $table, [ + 'name' => 'John Doe', + 'age' => 30, + 'email' => 'john@example.com' + ]); + $row1->setPermissions(['read("user:123")']); + + $row2 = new Row('row2', $table, [ + 'name' => 'Jane Smith', + 'age' => 25, + 'email' => 'jane@example.com' + ]); + $row2->setPermissions(['read("user:456")']); + + $this->runExport([$row1, $row2], $jsonDestination); + + $data = $this->readJsonFile($tempDir); + + $this->assertIsArray($data); + $this->assertCount(2, $data); + $this->assertSame('row1', $data[0]['$id']); + $this->assertSame('John Doe', $data[0]['name']); + $this->assertSame(30, $data[0]['age']); + $this->assertSame('john@example.com', $data[0]['email']); + $this->assertIsArray($data[0]['$permissions']); + + if (is_dir($tempDir)) { + $this->recursiveDelete($tempDir); + } + } + + public function testJSONExportWithSpecialCharacters() + { + $tempDir = sys_get_temp_dir() . '/json_test_special_' . uniqid(); + mkdir($tempDir, 0755, true); + + $jsonDestination = $this->createDestination($tempDir); + + $database = new Database('test_db'); + $table = new Table($database, 'test_table', 'test_table_id'); + + $row = new Row('special_row', $table, [ + 'quote_field' => 'Text with "quotes"', + 'comma_field' => 'Text, with, commas', + 'newline_field' => "Text with\nnewlines", + 'mixed_field' => 'Text with "quotes", commas, and\nnewlines' + ]); + + $this->runExport([$row], $jsonDestination); + + $data = $this->readJsonFile($tempDir); + + $this->assertSame('Text with "quotes"', $data[0]['quote_field']); + $this->assertSame('Text, with, commas', $data[0]['comma_field']); + $this->assertSame("Text with\nnewlines", $data[0]['newline_field']); + $this->assertSame('Text with "quotes", commas, and\nnewlines', $data[0]['mixed_field']); + + if (is_dir($tempDir)) { + $this->recursiveDelete($tempDir); + } + } + + public function testJSONExportWithArrays() + { + $tempDir = sys_get_temp_dir() . '/json_test_arrays_' . uniqid(); + mkdir($tempDir, 0755, true); + + $jsonDestination = $this->createDestination($tempDir); + + $database = new Database('test_db'); + $table = new Table($database, 'test_table', 'test_table_id'); + + $row = new Row('array_row', $table, [ + 'tags' => ['php', 'json', 'export'], + 'metadata' => ['key1' => 'value1', 'key2' => 'value2'], + 'empty_array' => [], + 'nested' => [['id' => 1], ['id' => 2]] + ]); + + $this->runExport([$row], $jsonDestination); + + $data = $this->readJsonFile($tempDir); + + $this->assertSame(['php', 'json', 'export'], $data[0]['tags']); + $this->assertSame(['key1' => 'value1', 'key2' => 'value2'], $data[0]['metadata']); + $this->assertSame([], $data[0]['empty_array']); + $this->assertSame([['id' => 1], ['id' => 2]], $data[0]['nested']); + + if (is_dir($tempDir)) { + $this->recursiveDelete($tempDir); + } + } + + public function testJSONExportPreservesNestedObjectsWithIdKeys() + { + $tempDir = sys_get_temp_dir() . '/json_test_nested_ids_' . uniqid(); + mkdir($tempDir, 0755, true); + + $jsonDestination = $this->createDestination($tempDir); + + $database = new Database('test_db'); + $table = new Table($database, 'test_table', 'test_table_id'); + + $payload = [ + 'items' => [ + [ + '$id' => 'nested1', + 'value' => 'keep-me', + ], + [ + '$id' => 'nested2', + 'value' => ['deep' => true], + ], + ], + 'object' => [ + '$id' => 'nested-object', + 'meta' => ['foo' => 'bar'], + ], + ]; + + $row = new Row('nested_row', $table, $payload); + + $this->runExport([$row], $jsonDestination); + + $data = $this->readJsonFile($tempDir); + + $this->assertSame($payload['items'], $data[0]['items']); + $this->assertSame($payload['object'], $data[0]['object']); + + if (is_dir($tempDir)) { + $this->recursiveDelete($tempDir); + } + } + + public function testJSONExportWithNullValues() + { + $tempDir = sys_get_temp_dir() . '/json_test_nulls_' . uniqid(); + mkdir($tempDir, 0755, true); + + $jsonDestination = $this->createDestination($tempDir); + + $database = new Database('test_db'); + $table = new Table($database, 'test_table', 'test_table_id'); + + $row = new Row('null_row', $table, [ + 'name' => 'Test', + 'null_field' => null, + 'empty_string' => '', + 'zero' => 0, + 'false_bool' => false + ]); + + $this->runExport([$row], $jsonDestination); + + $data = $this->readJsonFile($tempDir); + + $this->assertSame('Test', $data[0]['name']); + $this->assertNull($data[0]['null_field']); + $this->assertSame('', $data[0]['empty_string']); + $this->assertSame(0, $data[0]['zero']); + $this->assertFalse($data[0]['false_bool']); + + if (is_dir($tempDir)) { + $this->recursiveDelete($tempDir); + } + } + + public function testJSONExportWithAllowedAttributes() + { + $tempDir = sys_get_temp_dir() . '/json_test_filtered_' . uniqid(); + mkdir($tempDir, 0755, true); + + $jsonDestination = new DestinationJSON( + new Local($tempDir), + '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'); + + $row = new Row('filtered_row', $table, [ + 'name' => 'John Doe', + 'age' => 30, + 'email' => 'john@example.com', + 'secret' => 'should_not_appear' + ]); + + $this->runExport([$row], $jsonDestination); + + $data = $this->readJsonFile($tempDir); + + $this->assertArrayHasKey('$id', $data[0]); + $this->assertArrayHasKey('$permissions', $data[0]); + $this->assertArrayHasKey('$createdAt', $data[0]); + $this->assertArrayHasKey('$updatedAt', $data[0]); + $this->assertArrayHasKey('name', $data[0]); + $this->assertArrayHasKey('email', $data[0]); + $this->assertArrayNotHasKey('age', $data[0]); + $this->assertArrayNotHasKey('secret', $data[0]); + + if (is_dir($tempDir)) { + $this->recursiveDelete($tempDir); + } + } + + private function createDestination(string $tempDir): DestinationJSON + { + return new DestinationJSON( + new Local($tempDir), + 'test_db:test_table_id', + '', + 'test_db_test_table_id' + ); + } + + /** + * @return array> + */ + private function readJsonFile(string $tempDir): array + { + $jsonFile = $tempDir . '/test_db_test_table_id.json'; + $data = json_decode(file_get_contents($jsonFile), true); + return is_array($data) ? $data : []; + } + + /** + * @param array $rows + */ + private function runExport(array $rows, DestinationJSON $destination): void + { + $source = new MockSource(); + foreach ($rows as $row) { + $source->pushMockResource($row); + } + + $transfer = new Transfer($source, $destination); + $transfer->run([UtopiaResource::TYPE_ROW], function () { + return; + }); + + $destination->shutdown(); + } + + private function recursiveDelete(string $dir): void + { + if (is_dir($dir)) { + $objects = scandir($dir); + if ($objects !== false) { + foreach ($objects as $object) { + if ($object != "." && $object != "..") { + if (is_dir($dir."/".$object)) { + $this->recursiveDelete($dir."/".$object); + } else { + unlink($dir."/".$object); + } + } + } + } + rmdir($dir); + } + } +} From 0b86ca67e72114594bd625991b5a498860d090c3 Mon Sep 17 00:00:00 2001 From: Darshan Date: Tue, 20 Jan 2026 13:15:37 +0530 Subject: [PATCH 2/3] fix: tests. --- src/Migration/Sources/JSON.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Migration/Sources/JSON.php b/src/Migration/Sources/JSON.php index 7af4b89..779e267 100644 --- a/src/Migration/Sources/JSON.php +++ b/src/Migration/Sources/JSON.php @@ -29,9 +29,11 @@ class JSON extends Source private Device $device; + /** @noinspection PhpPropertyOnlyWrittenInspection */ + private ?UtopiaDatabase $dbForProject; + private bool $downloaded = false; - /** @noinspection PhpUnusedParameterInspection */ public function __construct( string $resourceId, string $filePath, @@ -41,6 +43,9 @@ public function __construct( $this->device = $device; $this->filePath = $filePath; $this->resourceId = $resourceId; + + /* kept for composer check */ + $this->dbForProject = $dbForProject; } public static function getName(): string From 91cb1f2ff29791742e8e19aa6443a131b27d78e6 Mon Sep 17 00:00:00 2001 From: Darshan Date: Tue, 20 Jan 2026 13:17:13 +0530 Subject: [PATCH 3/3] fix: naming pattern. --- .../Migration/Unit/General/{JSONExportTest.php => JSONTest.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/Migration/Unit/General/{JSONExportTest.php => JSONTest.php} (99%) diff --git a/tests/Migration/Unit/General/JSONExportTest.php b/tests/Migration/Unit/General/JSONTest.php similarity index 99% rename from tests/Migration/Unit/General/JSONExportTest.php rename to tests/Migration/Unit/General/JSONTest.php index 2db628b..1a880ec 100644 --- a/tests/Migration/Unit/General/JSONExportTest.php +++ b/tests/Migration/Unit/General/JSONTest.php @@ -12,7 +12,7 @@ use Utopia\Storage\Device\Local; use Utopia\Tests\Unit\Adapters\MockSource; -class JSONExportTest extends TestCase +class JSONTest extends TestCase { public function testJSONExportBasic() {