@njdamstra/appwrite-utils-cli 1.8.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +1133 -0
- package/dist/adapters/AdapterFactory.d.ts +94 -0
- package/dist/adapters/AdapterFactory.js +405 -0
- package/dist/adapters/DatabaseAdapter.d.ts +233 -0
- package/dist/adapters/DatabaseAdapter.js +50 -0
- package/dist/adapters/LegacyAdapter.d.ts +50 -0
- package/dist/adapters/LegacyAdapter.js +612 -0
- package/dist/adapters/TablesDBAdapter.d.ts +45 -0
- package/dist/adapters/TablesDBAdapter.js +571 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.js +12 -0
- package/dist/backups/operations/bucketBackup.d.ts +19 -0
- package/dist/backups/operations/bucketBackup.js +197 -0
- package/dist/backups/operations/collectionBackup.d.ts +30 -0
- package/dist/backups/operations/collectionBackup.js +201 -0
- package/dist/backups/operations/comprehensiveBackup.d.ts +25 -0
- package/dist/backups/operations/comprehensiveBackup.js +238 -0
- package/dist/backups/schemas/bucketManifest.d.ts +93 -0
- package/dist/backups/schemas/bucketManifest.js +33 -0
- package/dist/backups/schemas/comprehensiveManifest.d.ts +108 -0
- package/dist/backups/schemas/comprehensiveManifest.js +32 -0
- package/dist/backups/tracking/centralizedTracking.d.ts +34 -0
- package/dist/backups/tracking/centralizedTracking.js +274 -0
- package/dist/cli/commands/configCommands.d.ts +8 -0
- package/dist/cli/commands/configCommands.js +166 -0
- package/dist/cli/commands/databaseCommands.d.ts +13 -0
- package/dist/cli/commands/databaseCommands.js +554 -0
- package/dist/cli/commands/functionCommands.d.ts +7 -0
- package/dist/cli/commands/functionCommands.js +330 -0
- package/dist/cli/commands/schemaCommands.d.ts +7 -0
- package/dist/cli/commands/schemaCommands.js +169 -0
- package/dist/cli/commands/storageCommands.d.ts +5 -0
- package/dist/cli/commands/storageCommands.js +143 -0
- package/dist/cli/commands/transferCommands.d.ts +5 -0
- package/dist/cli/commands/transferCommands.js +384 -0
- package/dist/collections/attributes.d.ts +13 -0
- package/dist/collections/attributes.js +1364 -0
- package/dist/collections/indexes.d.ts +12 -0
- package/dist/collections/indexes.js +217 -0
- package/dist/collections/methods.d.ts +19 -0
- package/dist/collections/methods.js +682 -0
- package/dist/collections/tableOperations.d.ts +86 -0
- package/dist/collections/tableOperations.js +434 -0
- package/dist/collections/transferOperations.d.ts +8 -0
- package/dist/collections/transferOperations.js +412 -0
- package/dist/collections/wipeOperations.d.ts +16 -0
- package/dist/collections/wipeOperations.js +233 -0
- package/dist/config/ConfigManager.d.ts +445 -0
- package/dist/config/ConfigManager.js +625 -0
- package/dist/config/configMigration.d.ts +87 -0
- package/dist/config/configMigration.js +390 -0
- package/dist/config/configValidation.d.ts +66 -0
- package/dist/config/configValidation.js +358 -0
- package/dist/config/index.d.ts +8 -0
- package/dist/config/index.js +7 -0
- package/dist/config/services/ConfigDiscoveryService.d.ts +126 -0
- package/dist/config/services/ConfigDiscoveryService.js +374 -0
- package/dist/config/services/ConfigLoaderService.d.ts +129 -0
- package/dist/config/services/ConfigLoaderService.js +540 -0
- package/dist/config/services/ConfigMergeService.d.ts +208 -0
- package/dist/config/services/ConfigMergeService.js +308 -0
- package/dist/config/services/ConfigValidationService.d.ts +214 -0
- package/dist/config/services/ConfigValidationService.js +310 -0
- package/dist/config/services/SessionAuthService.d.ts +225 -0
- package/dist/config/services/SessionAuthService.js +456 -0
- package/dist/config/services/__tests__/ConfigMergeService.test.d.ts +1 -0
- package/dist/config/services/__tests__/ConfigMergeService.test.js +271 -0
- package/dist/config/services/index.d.ts +13 -0
- package/dist/config/services/index.js +10 -0
- package/dist/config/yamlConfig.d.ts +722 -0
- package/dist/config/yamlConfig.js +702 -0
- package/dist/databases/methods.d.ts +6 -0
- package/dist/databases/methods.js +35 -0
- package/dist/databases/setup.d.ts +5 -0
- package/dist/databases/setup.js +45 -0
- package/dist/examples/yamlTerminologyExample.d.ts +42 -0
- package/dist/examples/yamlTerminologyExample.js +272 -0
- package/dist/functions/deployments.d.ts +4 -0
- package/dist/functions/deployments.js +146 -0
- package/dist/functions/fnConfigDiscovery.d.ts +3 -0
- package/dist/functions/fnConfigDiscovery.js +108 -0
- package/dist/functions/methods.d.ts +16 -0
- package/dist/functions/methods.js +162 -0
- package/dist/functions/pathResolution.d.ts +37 -0
- package/dist/functions/pathResolution.js +185 -0
- package/dist/functions/templates/count-docs-in-collection/README.md +54 -0
- package/dist/functions/templates/count-docs-in-collection/src/main.ts +159 -0
- package/dist/functions/templates/count-docs-in-collection/src/request.ts +9 -0
- package/dist/functions/templates/hono-typescript/README.md +286 -0
- package/dist/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
- package/dist/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
- package/dist/functions/templates/hono-typescript/src/app.ts +180 -0
- package/dist/functions/templates/hono-typescript/src/context.ts +103 -0
- package/dist/functions/templates/hono-typescript/src/index.ts +54 -0
- package/dist/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
- package/dist/functions/templates/typescript-node/README.md +32 -0
- package/dist/functions/templates/typescript-node/src/context.ts +103 -0
- package/dist/functions/templates/typescript-node/src/index.ts +29 -0
- package/dist/functions/templates/uv/README.md +31 -0
- package/dist/functions/templates/uv/pyproject.toml +30 -0
- package/dist/functions/templates/uv/src/__init__.py +0 -0
- package/dist/functions/templates/uv/src/context.py +125 -0
- package/dist/functions/templates/uv/src/index.py +46 -0
- package/dist/init.d.ts +2 -0
- package/dist/init.js +57 -0
- package/dist/interactiveCLI.d.ts +31 -0
- package/dist/interactiveCLI.js +898 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +1172 -0
- package/dist/migrations/afterImportActions.d.ts +17 -0
- package/dist/migrations/afterImportActions.js +306 -0
- package/dist/migrations/appwriteToX.d.ts +211 -0
- package/dist/migrations/appwriteToX.js +491 -0
- package/dist/migrations/comprehensiveTransfer.d.ts +147 -0
- package/dist/migrations/comprehensiveTransfer.js +1317 -0
- package/dist/migrations/dataLoader.d.ts +753 -0
- package/dist/migrations/dataLoader.js +1250 -0
- package/dist/migrations/importController.d.ts +23 -0
- package/dist/migrations/importController.js +268 -0
- package/dist/migrations/importDataActions.d.ts +50 -0
- package/dist/migrations/importDataActions.js +230 -0
- package/dist/migrations/relationships.d.ts +29 -0
- package/dist/migrations/relationships.js +204 -0
- package/dist/migrations/services/DataTransformationService.d.ts +55 -0
- package/dist/migrations/services/DataTransformationService.js +158 -0
- package/dist/migrations/services/FileHandlerService.d.ts +75 -0
- package/dist/migrations/services/FileHandlerService.js +236 -0
- package/dist/migrations/services/ImportOrchestrator.d.ts +97 -0
- package/dist/migrations/services/ImportOrchestrator.js +485 -0
- package/dist/migrations/services/RateLimitManager.d.ts +138 -0
- package/dist/migrations/services/RateLimitManager.js +279 -0
- package/dist/migrations/services/RelationshipResolver.d.ts +120 -0
- package/dist/migrations/services/RelationshipResolver.js +332 -0
- package/dist/migrations/services/UserMappingService.d.ts +109 -0
- package/dist/migrations/services/UserMappingService.js +277 -0
- package/dist/migrations/services/ValidationService.d.ts +74 -0
- package/dist/migrations/services/ValidationService.js +260 -0
- package/dist/migrations/transfer.d.ts +26 -0
- package/dist/migrations/transfer.js +608 -0
- package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +131 -0
- package/dist/migrations/yaml/YamlImportConfigLoader.js +383 -0
- package/dist/migrations/yaml/YamlImportIntegration.d.ts +93 -0
- package/dist/migrations/yaml/YamlImportIntegration.js +341 -0
- package/dist/migrations/yaml/generateImportSchemas.d.ts +30 -0
- package/dist/migrations/yaml/generateImportSchemas.js +1327 -0
- package/dist/schemas/authUser.d.ts +24 -0
- package/dist/schemas/authUser.js +17 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +5 -0
- package/dist/setupCommands.d.ts +58 -0
- package/dist/setupCommands.js +490 -0
- package/dist/setupController.d.ts +9 -0
- package/dist/setupController.js +34 -0
- package/dist/shared/attributeMapper.d.ts +20 -0
- package/dist/shared/attributeMapper.js +203 -0
- package/dist/shared/backupMetadataSchema.d.ts +94 -0
- package/dist/shared/backupMetadataSchema.js +38 -0
- package/dist/shared/backupTracking.d.ts +18 -0
- package/dist/shared/backupTracking.js +176 -0
- package/dist/shared/confirmationDialogs.d.ts +75 -0
- package/dist/shared/confirmationDialogs.js +236 -0
- package/dist/shared/errorUtils.d.ts +54 -0
- package/dist/shared/errorUtils.js +95 -0
- package/dist/shared/functionManager.d.ts +48 -0
- package/dist/shared/functionManager.js +336 -0
- package/dist/shared/indexManager.d.ts +24 -0
- package/dist/shared/indexManager.js +151 -0
- package/dist/shared/jsonSchemaGenerator.d.ts +50 -0
- package/dist/shared/jsonSchemaGenerator.js +290 -0
- package/dist/shared/logging.d.ts +61 -0
- package/dist/shared/logging.js +116 -0
- package/dist/shared/messageFormatter.d.ts +39 -0
- package/dist/shared/messageFormatter.js +162 -0
- package/dist/shared/migrationHelpers.d.ts +61 -0
- package/dist/shared/migrationHelpers.js +145 -0
- package/dist/shared/operationLogger.d.ts +10 -0
- package/dist/shared/operationLogger.js +12 -0
- package/dist/shared/operationQueue.d.ts +40 -0
- package/dist/shared/operationQueue.js +311 -0
- package/dist/shared/operationsTable.d.ts +26 -0
- package/dist/shared/operationsTable.js +286 -0
- package/dist/shared/operationsTableSchema.d.ts +48 -0
- package/dist/shared/operationsTableSchema.js +35 -0
- package/dist/shared/progressManager.d.ts +62 -0
- package/dist/shared/progressManager.js +215 -0
- package/dist/shared/pydanticModelGenerator.d.ts +17 -0
- package/dist/shared/pydanticModelGenerator.js +615 -0
- package/dist/shared/relationshipExtractor.d.ts +56 -0
- package/dist/shared/relationshipExtractor.js +138 -0
- package/dist/shared/schemaGenerator.d.ts +40 -0
- package/dist/shared/schemaGenerator.js +556 -0
- package/dist/shared/selectionDialogs.d.ts +214 -0
- package/dist/shared/selectionDialogs.js +544 -0
- package/dist/storage/backupCompression.d.ts +20 -0
- package/dist/storage/backupCompression.js +67 -0
- package/dist/storage/methods.d.ts +32 -0
- package/dist/storage/methods.js +472 -0
- package/dist/storage/schemas.d.ts +842 -0
- package/dist/storage/schemas.js +175 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +3 -0
- package/dist/users/methods.d.ts +16 -0
- package/dist/users/methods.js +277 -0
- package/dist/utils/ClientFactory.d.ts +87 -0
- package/dist/utils/ClientFactory.js +212 -0
- package/dist/utils/configDiscovery.d.ts +78 -0
- package/dist/utils/configDiscovery.js +472 -0
- package/dist/utils/configMigration.d.ts +1 -0
- package/dist/utils/configMigration.js +261 -0
- package/dist/utils/constantsGenerator.d.ts +31 -0
- package/dist/utils/constantsGenerator.js +321 -0
- package/dist/utils/dataConverters.d.ts +46 -0
- package/dist/utils/dataConverters.js +139 -0
- package/dist/utils/directoryUtils.d.ts +22 -0
- package/dist/utils/directoryUtils.js +59 -0
- package/dist/utils/getClientFromConfig.d.ts +39 -0
- package/dist/utils/getClientFromConfig.js +199 -0
- package/dist/utils/helperFunctions.d.ts +63 -0
- package/dist/utils/helperFunctions.js +156 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/loadConfigs.d.ts +50 -0
- package/dist/utils/loadConfigs.js +358 -0
- package/dist/utils/pathResolvers.d.ts +53 -0
- package/dist/utils/pathResolvers.js +72 -0
- package/dist/utils/projectConfig.d.ts +119 -0
- package/dist/utils/projectConfig.js +171 -0
- package/dist/utils/retryFailedPromises.d.ts +2 -0
- package/dist/utils/retryFailedPromises.js +23 -0
- package/dist/utils/sessionAuth.d.ts +48 -0
- package/dist/utils/sessionAuth.js +164 -0
- package/dist/utils/setupFiles.d.ts +4 -0
- package/dist/utils/setupFiles.js +1192 -0
- package/dist/utils/typeGuards.d.ts +35 -0
- package/dist/utils/typeGuards.js +57 -0
- package/dist/utils/validationRules.d.ts +43 -0
- package/dist/utils/validationRules.js +42 -0
- package/dist/utils/versionDetection.d.ts +58 -0
- package/dist/utils/versionDetection.js +251 -0
- package/dist/utils/yamlConverter.d.ts +100 -0
- package/dist/utils/yamlConverter.js +428 -0
- package/dist/utils/yamlLoader.d.ts +70 -0
- package/dist/utils/yamlLoader.js +267 -0
- package/dist/utilsController.d.ts +106 -0
- package/dist/utilsController.js +863 -0
- package/package.json +75 -0
- package/scripts/copy-templates.ts +23 -0
- package/src/adapters/AdapterFactory.ts +510 -0
- package/src/adapters/DatabaseAdapter.ts +306 -0
- package/src/adapters/LegacyAdapter.ts +841 -0
- package/src/adapters/TablesDBAdapter.ts +773 -0
- package/src/adapters/index.ts +37 -0
- package/src/backups/operations/bucketBackup.ts +277 -0
- package/src/backups/operations/collectionBackup.ts +310 -0
- package/src/backups/operations/comprehensiveBackup.ts +342 -0
- package/src/backups/schemas/bucketManifest.ts +78 -0
- package/src/backups/schemas/comprehensiveManifest.ts +76 -0
- package/src/backups/tracking/centralizedTracking.ts +352 -0
- package/src/cli/commands/configCommands.ts +201 -0
- package/src/cli/commands/databaseCommands.ts +749 -0
- package/src/cli/commands/functionCommands.ts +418 -0
- package/src/cli/commands/schemaCommands.ts +200 -0
- package/src/cli/commands/storageCommands.ts +152 -0
- package/src/cli/commands/transferCommands.ts +457 -0
- package/src/collections/attributes.ts +2054 -0
- package/src/collections/attributes.ts.backup +1555 -0
- package/src/collections/indexes.ts +352 -0
- package/src/collections/methods.ts +745 -0
- package/src/collections/tableOperations.ts +506 -0
- package/src/collections/transferOperations.ts +590 -0
- package/src/collections/wipeOperations.ts +346 -0
- package/src/config/ConfigManager.ts +808 -0
- package/src/config/README.md +274 -0
- package/src/config/configMigration.ts +575 -0
- package/src/config/configValidation.ts +445 -0
- package/src/config/index.ts +10 -0
- package/src/config/services/ConfigDiscoveryService.ts +463 -0
- package/src/config/services/ConfigLoaderService.ts +740 -0
- package/src/config/services/ConfigMergeService.ts +388 -0
- package/src/config/services/ConfigValidationService.ts +394 -0
- package/src/config/services/SessionAuthService.ts +565 -0
- package/src/config/services/__tests__/ConfigMergeService.test.ts +351 -0
- package/src/config/services/index.ts +29 -0
- package/src/config/yamlConfig.ts +761 -0
- package/src/databases/methods.ts +49 -0
- package/src/databases/setup.ts +77 -0
- package/src/examples/yamlTerminologyExample.ts +346 -0
- package/src/functions/deployments.ts +220 -0
- package/src/functions/fnConfigDiscovery.ts +103 -0
- package/src/functions/methods.ts +271 -0
- package/src/functions/pathResolution.ts +227 -0
- package/src/functions/templates/count-docs-in-collection/README.md +54 -0
- package/src/functions/templates/count-docs-in-collection/src/main.ts +159 -0
- package/src/functions/templates/count-docs-in-collection/src/request.ts +9 -0
- package/src/functions/templates/hono-typescript/README.md +286 -0
- package/src/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
- package/src/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
- package/src/functions/templates/hono-typescript/src/app.ts +180 -0
- package/src/functions/templates/hono-typescript/src/context.ts +103 -0
- package/src/functions/templates/hono-typescript/src/index.ts +54 -0
- package/src/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
- package/src/functions/templates/typescript-node/README.md +32 -0
- package/src/functions/templates/typescript-node/src/context.ts +103 -0
- package/src/functions/templates/typescript-node/src/index.ts +29 -0
- package/src/functions/templates/uv/README.md +31 -0
- package/src/functions/templates/uv/pyproject.toml +30 -0
- package/src/functions/templates/uv/src/__init__.py +0 -0
- package/src/functions/templates/uv/src/context.py +125 -0
- package/src/functions/templates/uv/src/index.py +46 -0
- package/src/init.ts +62 -0
- package/src/interactiveCLI.ts +1136 -0
- package/src/main.ts +1661 -0
- package/src/migrations/afterImportActions.ts +580 -0
- package/src/migrations/appwriteToX.ts +664 -0
- package/src/migrations/comprehensiveTransfer.ts +2285 -0
- package/src/migrations/dataLoader.ts +1702 -0
- package/src/migrations/importController.ts +428 -0
- package/src/migrations/importDataActions.ts +315 -0
- package/src/migrations/relationships.ts +334 -0
- package/src/migrations/services/DataTransformationService.ts +196 -0
- package/src/migrations/services/FileHandlerService.ts +311 -0
- package/src/migrations/services/ImportOrchestrator.ts +666 -0
- package/src/migrations/services/RateLimitManager.ts +363 -0
- package/src/migrations/services/RelationshipResolver.ts +461 -0
- package/src/migrations/services/UserMappingService.ts +345 -0
- package/src/migrations/services/ValidationService.ts +349 -0
- package/src/migrations/transfer.ts +1068 -0
- package/src/migrations/yaml/YamlImportConfigLoader.ts +439 -0
- package/src/migrations/yaml/YamlImportIntegration.ts +446 -0
- package/src/migrations/yaml/generateImportSchemas.ts +1354 -0
- package/src/schemas/authUser.ts +23 -0
- package/src/setup.ts +8 -0
- package/src/setupCommands.ts +603 -0
- package/src/setupController.ts +43 -0
- package/src/shared/attributeMapper.ts +229 -0
- package/src/shared/backupMetadataSchema.ts +93 -0
- package/src/shared/backupTracking.ts +211 -0
- package/src/shared/confirmationDialogs.ts +327 -0
- package/src/shared/errorUtils.ts +110 -0
- package/src/shared/functionManager.ts +525 -0
- package/src/shared/indexManager.ts +254 -0
- package/src/shared/jsonSchemaGenerator.ts +383 -0
- package/src/shared/logging.ts +149 -0
- package/src/shared/messageFormatter.ts +208 -0
- package/src/shared/migrationHelpers.ts +232 -0
- package/src/shared/operationLogger.ts +20 -0
- package/src/shared/operationQueue.ts +377 -0
- package/src/shared/operationsTable.ts +338 -0
- package/src/shared/operationsTableSchema.ts +60 -0
- package/src/shared/progressManager.ts +278 -0
- package/src/shared/pydanticModelGenerator.ts +618 -0
- package/src/shared/relationshipExtractor.ts +214 -0
- package/src/shared/schemaGenerator.ts +644 -0
- package/src/shared/selectionDialogs.ts +749 -0
- package/src/storage/backupCompression.ts +88 -0
- package/src/storage/methods.ts +698 -0
- package/src/storage/schemas.ts +205 -0
- package/src/types/node-appwrite-tablesdb.d.ts +44 -0
- package/src/types.ts +9 -0
- package/src/users/methods.ts +359 -0
- package/src/utils/ClientFactory.ts +240 -0
- package/src/utils/configDiscovery.ts +557 -0
- package/src/utils/configMigration.ts +348 -0
- package/src/utils/constantsGenerator.ts +369 -0
- package/src/utils/dataConverters.ts +159 -0
- package/src/utils/directoryUtils.ts +61 -0
- package/src/utils/getClientFromConfig.ts +257 -0
- package/src/utils/helperFunctions.ts +228 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/loadConfigs.ts +449 -0
- package/src/utils/pathResolvers.ts +81 -0
- package/src/utils/projectConfig.ts +299 -0
- package/src/utils/retryFailedPromises.ts +29 -0
- package/src/utils/sessionAuth.ts +230 -0
- package/src/utils/setupFiles.ts +1238 -0
- package/src/utils/typeGuards.ts +65 -0
- package/src/utils/validationRules.ts +88 -0
- package/src/utils/versionDetection.ts +292 -0
- package/src/utils/yamlConverter.ts +542 -0
- package/src/utils/yamlLoader.ts +371 -0
- package/src/utilsController.ts +1203 -0
- package/tests/README.md +497 -0
- package/tests/adapters/AdapterFactory.test.ts +277 -0
- package/tests/integration/syncOperations.test.ts +463 -0
- package/tests/jest.config.js +25 -0
- package/tests/migration/configMigration.test.ts +546 -0
- package/tests/setup.ts +62 -0
- package/tests/testUtils.ts +340 -0
- package/tests/utils/loadConfigs.test.ts +350 -0
- package/tests/validation/configValidation.test.ts +412 -0
- package/tsconfig.json +44 -0
|
@@ -0,0 +1,2285 @@
|
|
|
1
|
+
import {
|
|
2
|
+
converterFunctions,
|
|
3
|
+
tryAwaitWithRetry,
|
|
4
|
+
parseAttribute,
|
|
5
|
+
objectNeedsUpdate,
|
|
6
|
+
} from "@njdamstra/appwrite-utils";
|
|
7
|
+
import {
|
|
8
|
+
Client,
|
|
9
|
+
Databases,
|
|
10
|
+
Storage,
|
|
11
|
+
Users,
|
|
12
|
+
Functions,
|
|
13
|
+
Teams,
|
|
14
|
+
type Models,
|
|
15
|
+
Query,
|
|
16
|
+
AppwriteException,
|
|
17
|
+
} from "node-appwrite";
|
|
18
|
+
import { InputFile } from "node-appwrite/file";
|
|
19
|
+
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
20
|
+
import { processQueue, queuedOperations } from "../shared/operationQueue.js";
|
|
21
|
+
import { ProgressManager } from "../shared/progressManager.js";
|
|
22
|
+
import { getClient } from "../utils/getClientFromConfig.js";
|
|
23
|
+
import {
|
|
24
|
+
transferDatabaseLocalToLocal,
|
|
25
|
+
transferDatabaseLocalToRemote,
|
|
26
|
+
transferStorageLocalToLocal,
|
|
27
|
+
transferStorageLocalToRemote,
|
|
28
|
+
transferUsersLocalToRemote,
|
|
29
|
+
} from "./transfer.js";
|
|
30
|
+
import { deployLocalFunction } from "../functions/deployments.js";
|
|
31
|
+
import {
|
|
32
|
+
listFunctions,
|
|
33
|
+
downloadLatestFunctionDeployment,
|
|
34
|
+
} from "../functions/methods.js";
|
|
35
|
+
import pLimit from "p-limit";
|
|
36
|
+
import chalk from "chalk";
|
|
37
|
+
import { join } from "node:path";
|
|
38
|
+
import fs from "node:fs";
|
|
39
|
+
import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
|
|
40
|
+
import { getAdapter } from "../utils/getClientFromConfig.js";
|
|
41
|
+
import { mapToCreateAttributeParams } from "../shared/attributeMapper.js";
|
|
42
|
+
|
|
43
|
+
export interface ComprehensiveTransferOptions {
|
|
44
|
+
sourceEndpoint: string;
|
|
45
|
+
sourceProject: string;
|
|
46
|
+
sourceKey: string;
|
|
47
|
+
targetEndpoint: string;
|
|
48
|
+
targetProject: string;
|
|
49
|
+
targetKey: string;
|
|
50
|
+
transferUsers?: boolean;
|
|
51
|
+
transferTeams?: boolean;
|
|
52
|
+
transferDatabases?: boolean;
|
|
53
|
+
transferBuckets?: boolean;
|
|
54
|
+
transferFunctions?: boolean;
|
|
55
|
+
concurrencyLimit?: number; // 5-100 in steps of 5
|
|
56
|
+
dryRun?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TransferResults {
|
|
60
|
+
users: { transferred: number; skipped: number; failed: number };
|
|
61
|
+
teams: { transferred: number; skipped: number; failed: number };
|
|
62
|
+
databases: { transferred: number; skipped: number; failed: number };
|
|
63
|
+
buckets: { transferred: number; skipped: number; failed: number };
|
|
64
|
+
functions: { transferred: number; skipped: number; failed: number };
|
|
65
|
+
totalTime: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class ComprehensiveTransfer {
|
|
69
|
+
private sourceClient: Client;
|
|
70
|
+
private targetClient: Client;
|
|
71
|
+
private sourceUsers: Users;
|
|
72
|
+
private targetUsers: Users;
|
|
73
|
+
private sourceTeams: Teams;
|
|
74
|
+
private targetTeams: Teams;
|
|
75
|
+
private sourceDatabases: Databases;
|
|
76
|
+
private targetDatabases: Databases;
|
|
77
|
+
private sourceStorage: Storage;
|
|
78
|
+
private targetStorage: Storage;
|
|
79
|
+
private sourceFunctions: Functions;
|
|
80
|
+
private targetFunctions: Functions;
|
|
81
|
+
private limit: ReturnType<typeof pLimit>;
|
|
82
|
+
private userLimit: ReturnType<typeof pLimit>;
|
|
83
|
+
private fileLimit: ReturnType<typeof pLimit>;
|
|
84
|
+
private results: TransferResults;
|
|
85
|
+
private startTime: number;
|
|
86
|
+
private tempDir: string;
|
|
87
|
+
private cachedMaxFileSize?: number; // Cache successful maximumFileSize for subsequent buckets
|
|
88
|
+
private sourceAdapter?: DatabaseAdapter;
|
|
89
|
+
private targetAdapter?: DatabaseAdapter;
|
|
90
|
+
|
|
91
|
+
constructor(private options: ComprehensiveTransferOptions) {
|
|
92
|
+
this.sourceClient = getClient(
|
|
93
|
+
options.sourceEndpoint,
|
|
94
|
+
options.sourceProject,
|
|
95
|
+
options.sourceKey
|
|
96
|
+
);
|
|
97
|
+
this.targetClient = getClient(
|
|
98
|
+
options.targetEndpoint,
|
|
99
|
+
options.targetProject,
|
|
100
|
+
options.targetKey
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
this.sourceUsers = new Users(this.sourceClient);
|
|
104
|
+
this.targetUsers = new Users(this.targetClient);
|
|
105
|
+
this.sourceTeams = new Teams(this.sourceClient);
|
|
106
|
+
this.targetTeams = new Teams(this.targetClient);
|
|
107
|
+
this.sourceDatabases = new Databases(this.sourceClient);
|
|
108
|
+
this.targetDatabases = new Databases(this.targetClient);
|
|
109
|
+
this.sourceStorage = new Storage(this.sourceClient);
|
|
110
|
+
this.targetStorage = new Storage(this.targetClient);
|
|
111
|
+
this.sourceFunctions = new Functions(this.sourceClient);
|
|
112
|
+
this.targetFunctions = new Functions(this.targetClient);
|
|
113
|
+
|
|
114
|
+
const baseLimit = options.concurrencyLimit || 10;
|
|
115
|
+
this.limit = pLimit(baseLimit);
|
|
116
|
+
|
|
117
|
+
// Different rate limits for different operations to prevent API throttling
|
|
118
|
+
// Users: Half speed (more sensitive operations)
|
|
119
|
+
// Files: Quarter speed (most bandwidth intensive)
|
|
120
|
+
this.userLimit = pLimit(Math.max(1, Math.floor(baseLimit / 2)));
|
|
121
|
+
this.fileLimit = pLimit(Math.max(1, Math.floor(baseLimit / 4)));
|
|
122
|
+
this.results = {
|
|
123
|
+
users: { transferred: 0, skipped: 0, failed: 0 },
|
|
124
|
+
teams: { transferred: 0, skipped: 0, failed: 0 },
|
|
125
|
+
databases: { transferred: 0, skipped: 0, failed: 0 },
|
|
126
|
+
buckets: { transferred: 0, skipped: 0, failed: 0 },
|
|
127
|
+
functions: { transferred: 0, skipped: 0, failed: 0 },
|
|
128
|
+
totalTime: 0,
|
|
129
|
+
};
|
|
130
|
+
this.startTime = Date.now();
|
|
131
|
+
this.tempDir = join(process.cwd(), ".appwrite-transfer-temp");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async execute(): Promise<TransferResults> {
|
|
135
|
+
try {
|
|
136
|
+
MessageFormatter.info("Starting comprehensive transfer", {
|
|
137
|
+
prefix: "Transfer",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Initialize adapters for unified API (TablesDB or legacy via adapter)
|
|
141
|
+
const source = await getAdapter(
|
|
142
|
+
this.options.sourceEndpoint,
|
|
143
|
+
this.options.sourceProject,
|
|
144
|
+
this.options.sourceKey,
|
|
145
|
+
'auto'
|
|
146
|
+
);
|
|
147
|
+
const target = await getAdapter(
|
|
148
|
+
this.options.targetEndpoint,
|
|
149
|
+
this.options.targetProject,
|
|
150
|
+
this.options.targetKey,
|
|
151
|
+
'auto'
|
|
152
|
+
);
|
|
153
|
+
this.sourceAdapter = source.adapter;
|
|
154
|
+
this.targetAdapter = target.adapter;
|
|
155
|
+
|
|
156
|
+
if (this.options.dryRun) {
|
|
157
|
+
MessageFormatter.info("DRY RUN MODE - No actual changes will be made", {
|
|
158
|
+
prefix: "Transfer",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Show rate limiting configuration
|
|
163
|
+
const baseLimit = this.options.concurrencyLimit || 10;
|
|
164
|
+
const userLimit = Math.max(1, Math.floor(baseLimit / 2));
|
|
165
|
+
const fileLimit = Math.max(1, Math.floor(baseLimit / 4));
|
|
166
|
+
|
|
167
|
+
MessageFormatter.info(
|
|
168
|
+
`Rate limits: General=${baseLimit}, Users=${userLimit}, Files=${fileLimit}`,
|
|
169
|
+
{ prefix: "Transfer" }
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Ensure temp directory exists
|
|
173
|
+
if (!fs.existsSync(this.tempDir)) {
|
|
174
|
+
fs.mkdirSync(this.tempDir, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Execute transfers in the correct order
|
|
178
|
+
if (this.options.transferUsers !== false) {
|
|
179
|
+
await this.transferAllUsers();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this.options.transferTeams !== false) {
|
|
183
|
+
await this.transferAllTeams();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (this.options.transferDatabases !== false) {
|
|
187
|
+
await this.transferAllDatabases();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (this.options.transferBuckets !== false) {
|
|
191
|
+
await this.transferAllBuckets();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (this.options.transferFunctions !== false) {
|
|
195
|
+
await this.transferAllFunctions();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.results.totalTime = Date.now() - this.startTime;
|
|
199
|
+
this.printSummary();
|
|
200
|
+
|
|
201
|
+
return this.results;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
MessageFormatter.error(
|
|
204
|
+
"Comprehensive transfer failed",
|
|
205
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
206
|
+
{ prefix: "Transfer" }
|
|
207
|
+
);
|
|
208
|
+
throw error;
|
|
209
|
+
} finally {
|
|
210
|
+
// Clean up temp directory
|
|
211
|
+
if (fs.existsSync(this.tempDir)) {
|
|
212
|
+
fs.rmSync(this.tempDir, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private async transferAllUsers(): Promise<void> {
|
|
218
|
+
MessageFormatter.info("Starting user transfer phase", {
|
|
219
|
+
prefix: "Transfer",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (this.options.dryRun) {
|
|
223
|
+
const usersList = await this.sourceUsers.list([Query.limit(1)]);
|
|
224
|
+
MessageFormatter.info(
|
|
225
|
+
`DRY RUN: Would transfer ${usersList.total} users`,
|
|
226
|
+
{ prefix: "Transfer" }
|
|
227
|
+
);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
// Use the existing user transfer function
|
|
233
|
+
// Note: The rate limiting is handled at the API level, not per-user
|
|
234
|
+
// since user operations are already sequential in the existing implementation
|
|
235
|
+
await transferUsersLocalToRemote(
|
|
236
|
+
this.sourceUsers,
|
|
237
|
+
this.options.targetEndpoint,
|
|
238
|
+
this.options.targetProject,
|
|
239
|
+
this.options.targetKey
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Get actual count for results
|
|
243
|
+
const usersList = await this.sourceUsers.list([Query.limit(1)]);
|
|
244
|
+
this.results.users.transferred = usersList.total;
|
|
245
|
+
|
|
246
|
+
MessageFormatter.success(`User transfer completed`, {
|
|
247
|
+
prefix: "Transfer",
|
|
248
|
+
});
|
|
249
|
+
} catch (error) {
|
|
250
|
+
MessageFormatter.error(
|
|
251
|
+
"User transfer failed",
|
|
252
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
253
|
+
{ prefix: "Transfer" }
|
|
254
|
+
);
|
|
255
|
+
this.results.users.failed = 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async transferAllTeams(): Promise<void> {
|
|
260
|
+
MessageFormatter.info("Starting team transfer phase", {
|
|
261
|
+
prefix: "Transfer",
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
// Fetch all teams from source with pagination
|
|
266
|
+
const allSourceTeams = await this.fetchAllTeams(this.sourceTeams);
|
|
267
|
+
const allTargetTeams = await this.fetchAllTeams(this.targetTeams);
|
|
268
|
+
|
|
269
|
+
if (this.options.dryRun) {
|
|
270
|
+
let totalMemberships = 0;
|
|
271
|
+
for (const team of allSourceTeams) {
|
|
272
|
+
const memberships = await this.sourceTeams.listMemberships(team.$id, [
|
|
273
|
+
Query.limit(1),
|
|
274
|
+
]);
|
|
275
|
+
totalMemberships += memberships.total;
|
|
276
|
+
}
|
|
277
|
+
MessageFormatter.info(
|
|
278
|
+
`DRY RUN: Would transfer ${allSourceTeams.length} teams with ${totalMemberships} memberships`,
|
|
279
|
+
{ prefix: "Transfer" }
|
|
280
|
+
);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const transferTasks = allSourceTeams.map((team) =>
|
|
285
|
+
this.limit(async () => {
|
|
286
|
+
try {
|
|
287
|
+
// Check if team exists in target
|
|
288
|
+
const existingTeam = allTargetTeams.find(
|
|
289
|
+
(tt) => tt.$id === team.$id
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (!existingTeam) {
|
|
293
|
+
// Fetch all memberships to extract unique roles before creating team
|
|
294
|
+
MessageFormatter.info(
|
|
295
|
+
`Fetching memberships for team ${team.name} to extract roles`,
|
|
296
|
+
{ prefix: "Transfer" }
|
|
297
|
+
);
|
|
298
|
+
const memberships = await this.fetchAllMemberships(team.$id);
|
|
299
|
+
|
|
300
|
+
// Extract unique roles from all memberships
|
|
301
|
+
const allRoles = new Set<string>();
|
|
302
|
+
memberships.forEach((membership) => {
|
|
303
|
+
membership.roles.forEach((role) => allRoles.add(role));
|
|
304
|
+
});
|
|
305
|
+
const uniqueRoles = Array.from(allRoles);
|
|
306
|
+
|
|
307
|
+
MessageFormatter.info(
|
|
308
|
+
`Found ${uniqueRoles.length} unique roles for team ${
|
|
309
|
+
team.name
|
|
310
|
+
}: ${uniqueRoles.join(", ")}`,
|
|
311
|
+
{ prefix: "Transfer" }
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Create team in target with the collected roles
|
|
315
|
+
await this.targetTeams.create(team.$id, team.name, uniqueRoles);
|
|
316
|
+
MessageFormatter.success(
|
|
317
|
+
`Created team: ${team.name} with roles: ${uniqueRoles.join(
|
|
318
|
+
", "
|
|
319
|
+
)}`,
|
|
320
|
+
{ prefix: "Transfer" }
|
|
321
|
+
);
|
|
322
|
+
} else {
|
|
323
|
+
MessageFormatter.info(
|
|
324
|
+
`Team ${team.name} already exists, updating if needed`,
|
|
325
|
+
{ prefix: "Transfer" }
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Update team if needed
|
|
329
|
+
if (existingTeam.name !== team.name) {
|
|
330
|
+
await this.targetTeams.updateName(team.$id, team.name);
|
|
331
|
+
MessageFormatter.success(`Updated team name: ${team.name}`, {
|
|
332
|
+
prefix: "Transfer",
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Transfer team memberships
|
|
338
|
+
await this.transferTeamMemberships(team.$id);
|
|
339
|
+
|
|
340
|
+
this.results.teams.transferred++;
|
|
341
|
+
MessageFormatter.success(
|
|
342
|
+
`Team ${team.name} transferred successfully`,
|
|
343
|
+
{ prefix: "Transfer" }
|
|
344
|
+
);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
MessageFormatter.error(
|
|
347
|
+
`Team ${team.name} transfer failed`,
|
|
348
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
349
|
+
{ prefix: "Transfer" }
|
|
350
|
+
);
|
|
351
|
+
this.results.teams.failed++;
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
await Promise.all(transferTasks);
|
|
357
|
+
MessageFormatter.success("Team transfer phase completed", {
|
|
358
|
+
prefix: "Transfer",
|
|
359
|
+
});
|
|
360
|
+
} catch (error) {
|
|
361
|
+
MessageFormatter.error(
|
|
362
|
+
"Team transfer phase failed",
|
|
363
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
364
|
+
{ prefix: "Transfer" }
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private async transferAllDatabases(): Promise<void> {
|
|
370
|
+
MessageFormatter.info("Starting database transfer phase", {
|
|
371
|
+
prefix: "Transfer",
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const sourceDatabases = await this.sourceDatabases.list();
|
|
376
|
+
const targetDatabases = await this.targetDatabases.list();
|
|
377
|
+
|
|
378
|
+
if (this.options.dryRun) {
|
|
379
|
+
MessageFormatter.info(
|
|
380
|
+
`DRY RUN: Would transfer ${sourceDatabases.databases.length} databases`,
|
|
381
|
+
{ prefix: "Transfer" }
|
|
382
|
+
);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Phase 1: Create all databases and collections (structure only)
|
|
387
|
+
MessageFormatter.info(
|
|
388
|
+
"Phase 1: Creating database structures (databases, collections, attributes, indexes)",
|
|
389
|
+
{ prefix: "Transfer" }
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const structureCreationTasks = sourceDatabases.databases.map((db) =>
|
|
393
|
+
this.limit(async () => {
|
|
394
|
+
try {
|
|
395
|
+
// Check if database exists in target
|
|
396
|
+
const existingDb = targetDatabases.databases.find(
|
|
397
|
+
(tdb) => tdb.$id === db.$id
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (!existingDb) {
|
|
401
|
+
// Create database in target
|
|
402
|
+
await this.targetDatabases.create(db.$id, db.name, db.enabled);
|
|
403
|
+
MessageFormatter.success(`Created database: ${db.name}`, {
|
|
404
|
+
prefix: "Transfer",
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Create collections, attributes, and indexes WITHOUT transferring documents
|
|
409
|
+
await this.createDatabaseStructure(db.$id);
|
|
410
|
+
|
|
411
|
+
MessageFormatter.success(`Database structure created: ${db.name}`, {
|
|
412
|
+
prefix: "Transfer",
|
|
413
|
+
});
|
|
414
|
+
} catch (error) {
|
|
415
|
+
MessageFormatter.error(
|
|
416
|
+
`Database structure creation failed for ${db.name}`,
|
|
417
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
418
|
+
{ prefix: "Transfer" }
|
|
419
|
+
);
|
|
420
|
+
this.results.databases.failed++;
|
|
421
|
+
}
|
|
422
|
+
})
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
await Promise.all(structureCreationTasks);
|
|
426
|
+
|
|
427
|
+
// Phase 2: Transfer all documents after all structures are created
|
|
428
|
+
MessageFormatter.info(
|
|
429
|
+
"Phase 2: Transferring documents to all collections",
|
|
430
|
+
{ prefix: "Transfer" }
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const documentTransferTasks = sourceDatabases.databases.map((db) =>
|
|
434
|
+
this.limit(async () => {
|
|
435
|
+
try {
|
|
436
|
+
// Transfer documents for this database
|
|
437
|
+
await this.transferDatabaseDocuments(db.$id);
|
|
438
|
+
|
|
439
|
+
this.results.databases.transferred++;
|
|
440
|
+
MessageFormatter.success(
|
|
441
|
+
`Database documents transferred: ${db.name}`,
|
|
442
|
+
{ prefix: "Transfer" }
|
|
443
|
+
);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
MessageFormatter.error(
|
|
446
|
+
`Document transfer failed for ${db.name}`,
|
|
447
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
448
|
+
{ prefix: "Transfer" }
|
|
449
|
+
);
|
|
450
|
+
this.results.databases.failed++;
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
await Promise.all(documentTransferTasks);
|
|
456
|
+
MessageFormatter.success("Database transfer phase completed", {
|
|
457
|
+
prefix: "Transfer",
|
|
458
|
+
});
|
|
459
|
+
} catch (error) {
|
|
460
|
+
MessageFormatter.error(
|
|
461
|
+
"Database transfer phase failed",
|
|
462
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
463
|
+
{ prefix: "Transfer" }
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Phase 1: Create database structure (collections, attributes, indexes) without transferring documents
|
|
470
|
+
*/
|
|
471
|
+
private async createDatabaseStructure(dbId: string): Promise<void> {
|
|
472
|
+
MessageFormatter.info(`Creating database structure for ${dbId}`, {
|
|
473
|
+
prefix: "Transfer",
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
// Get all collections from source database
|
|
478
|
+
const sourceCollections = await this.fetchAllCollections(
|
|
479
|
+
dbId,
|
|
480
|
+
this.sourceDatabases
|
|
481
|
+
);
|
|
482
|
+
MessageFormatter.info(
|
|
483
|
+
`Found ${sourceCollections.length} collections in source database ${dbId}`,
|
|
484
|
+
{ prefix: "Transfer" }
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// Process each collection
|
|
488
|
+
for (const collection of sourceCollections) {
|
|
489
|
+
MessageFormatter.info(
|
|
490
|
+
`Processing collection: ${collection.name} (${collection.$id})`,
|
|
491
|
+
{ prefix: "Transfer" }
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
// Create or update collection in target
|
|
496
|
+
let targetCollection: Models.Collection;
|
|
497
|
+
const existingCollection = await tryAwaitWithRetry(async () =>
|
|
498
|
+
this.targetDatabases.listCollections(dbId, [
|
|
499
|
+
Query.equal("$id", collection.$id),
|
|
500
|
+
])
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (existingCollection.collections.length > 0) {
|
|
504
|
+
targetCollection = existingCollection.collections[0];
|
|
505
|
+
MessageFormatter.info(
|
|
506
|
+
`Collection ${collection.name} exists in target database`,
|
|
507
|
+
{ prefix: "Transfer" }
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
// Update collection if needed
|
|
511
|
+
if (
|
|
512
|
+
targetCollection.name !== collection.name ||
|
|
513
|
+
JSON.stringify(targetCollection.$permissions) !==
|
|
514
|
+
JSON.stringify(collection.$permissions) ||
|
|
515
|
+
targetCollection.documentSecurity !==
|
|
516
|
+
collection.documentSecurity ||
|
|
517
|
+
targetCollection.enabled !== collection.enabled
|
|
518
|
+
) {
|
|
519
|
+
targetCollection = await tryAwaitWithRetry(async () =>
|
|
520
|
+
this.targetDatabases.updateCollection(
|
|
521
|
+
dbId,
|
|
522
|
+
collection.$id,
|
|
523
|
+
collection.name,
|
|
524
|
+
collection.$permissions,
|
|
525
|
+
collection.documentSecurity,
|
|
526
|
+
collection.enabled
|
|
527
|
+
)
|
|
528
|
+
);
|
|
529
|
+
MessageFormatter.success(
|
|
530
|
+
`Collection ${collection.name} updated`,
|
|
531
|
+
{ prefix: "Transfer" }
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
} else {
|
|
535
|
+
MessageFormatter.info(
|
|
536
|
+
`Creating collection ${collection.name} in target database...`,
|
|
537
|
+
{ prefix: "Transfer" }
|
|
538
|
+
);
|
|
539
|
+
targetCollection = await tryAwaitWithRetry(async () =>
|
|
540
|
+
this.targetDatabases.createCollection(
|
|
541
|
+
dbId,
|
|
542
|
+
collection.$id,
|
|
543
|
+
collection.name,
|
|
544
|
+
collection.$permissions,
|
|
545
|
+
collection.documentSecurity,
|
|
546
|
+
collection.enabled
|
|
547
|
+
)
|
|
548
|
+
);
|
|
549
|
+
MessageFormatter.success(`Collection ${collection.name} created`, {
|
|
550
|
+
prefix: "Transfer",
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Handle attributes with enhanced status checking
|
|
555
|
+
MessageFormatter.info(
|
|
556
|
+
`Creating attributes for collection ${collection.name} with enhanced monitoring...`,
|
|
557
|
+
{ prefix: "Transfer" }
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
const attributesToCreate = collection.attributes.map((attr) =>
|
|
561
|
+
parseAttribute(attr as any)
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
const attributesSuccess =
|
|
565
|
+
await this.createCollectionAttributesWithStatusCheck(
|
|
566
|
+
this.targetDatabases,
|
|
567
|
+
dbId,
|
|
568
|
+
targetCollection,
|
|
569
|
+
attributesToCreate
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
if (!attributesSuccess) {
|
|
573
|
+
MessageFormatter.error(
|
|
574
|
+
`Failed to create some attributes for collection ${collection.name}`,
|
|
575
|
+
undefined,
|
|
576
|
+
{ prefix: "Transfer" }
|
|
577
|
+
);
|
|
578
|
+
MessageFormatter.error(
|
|
579
|
+
`Skipping index creation and document transfer for collection ${collection.name} due to attribute failures`,
|
|
580
|
+
undefined,
|
|
581
|
+
{ prefix: "Transfer" }
|
|
582
|
+
);
|
|
583
|
+
// Skip indexes and document transfer if attributes failed
|
|
584
|
+
continue;
|
|
585
|
+
} else {
|
|
586
|
+
MessageFormatter.success(
|
|
587
|
+
`All attributes created successfully for collection ${collection.name}`,
|
|
588
|
+
{ prefix: "Transfer" }
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Handle indexes with enhanced status checking
|
|
593
|
+
MessageFormatter.info(
|
|
594
|
+
`Creating indexes for collection ${collection.name} with enhanced monitoring...`,
|
|
595
|
+
{ prefix: "Transfer" }
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
let indexesSuccess = true;
|
|
599
|
+
// Check if indexes need to be created ahead of time
|
|
600
|
+
if (
|
|
601
|
+
collection.indexes.some(
|
|
602
|
+
(index) =>
|
|
603
|
+
!targetCollection.indexes.some(
|
|
604
|
+
(ti) =>
|
|
605
|
+
ti.key === index.key ||
|
|
606
|
+
ti.attributes.sort().join(",") ===
|
|
607
|
+
index.attributes.sort().join(",")
|
|
608
|
+
)
|
|
609
|
+
) ||
|
|
610
|
+
collection.indexes.length !== targetCollection.indexes.length
|
|
611
|
+
) {
|
|
612
|
+
indexesSuccess = await this.createCollectionIndexesWithStatusCheck(
|
|
613
|
+
dbId,
|
|
614
|
+
this.targetDatabases,
|
|
615
|
+
targetCollection.$id,
|
|
616
|
+
targetCollection,
|
|
617
|
+
collection.indexes as any
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!indexesSuccess) {
|
|
622
|
+
MessageFormatter.error(
|
|
623
|
+
`Failed to create some indexes for collection ${collection.name}`,
|
|
624
|
+
undefined,
|
|
625
|
+
{ prefix: "Transfer" }
|
|
626
|
+
);
|
|
627
|
+
MessageFormatter.warning(
|
|
628
|
+
`Proceeding with document transfer despite index failures for collection ${collection.name}`,
|
|
629
|
+
{ prefix: "Transfer" }
|
|
630
|
+
);
|
|
631
|
+
} else {
|
|
632
|
+
MessageFormatter.success(
|
|
633
|
+
`All indexes created successfully for collection ${collection.name}`,
|
|
634
|
+
{ prefix: "Transfer" }
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
MessageFormatter.success(
|
|
639
|
+
`Structure complete for collection ${collection.name}`,
|
|
640
|
+
{ prefix: "Transfer" }
|
|
641
|
+
);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
MessageFormatter.error(
|
|
644
|
+
`Error processing collection ${collection.name}`,
|
|
645
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
646
|
+
{ prefix: "Transfer" }
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// After processing all collections' attributes and indexes, process any queued
|
|
651
|
+
// relationship attributes so dependencies are resolved within this phase.
|
|
652
|
+
if (queuedOperations.length > 0) {
|
|
653
|
+
MessageFormatter.info(
|
|
654
|
+
`Processing ${queuedOperations.length} queued relationship operations`,
|
|
655
|
+
{ prefix: "Transfer" }
|
|
656
|
+
);
|
|
657
|
+
await processQueue(this.targetDatabases, dbId);
|
|
658
|
+
} else {
|
|
659
|
+
MessageFormatter.info("No queued relationship operations to process", {
|
|
660
|
+
prefix: "Transfer",
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
} catch (error) {
|
|
664
|
+
MessageFormatter.error(
|
|
665
|
+
`Failed to create database structure for ${dbId}`,
|
|
666
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
667
|
+
{ prefix: "Transfer" }
|
|
668
|
+
);
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Phase 2: Transfer documents to all collections in the database
|
|
675
|
+
*/
|
|
676
|
+
private async transferDatabaseDocuments(dbId: string): Promise<void> {
|
|
677
|
+
MessageFormatter.info(`Transferring documents for database ${dbId}`, {
|
|
678
|
+
prefix: "Transfer",
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
// Get all collections from source database
|
|
683
|
+
const sourceCollections = await this.fetchAllCollections(
|
|
684
|
+
dbId,
|
|
685
|
+
this.sourceDatabases
|
|
686
|
+
);
|
|
687
|
+
MessageFormatter.info(
|
|
688
|
+
`Transferring documents for ${sourceCollections.length} collections in database ${dbId}`,
|
|
689
|
+
{ prefix: "Transfer" }
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
// Process each collection
|
|
693
|
+
for (const collection of sourceCollections) {
|
|
694
|
+
MessageFormatter.info(
|
|
695
|
+
`Transferring documents for collection: ${collection.name} (${collection.$id})`,
|
|
696
|
+
{ prefix: "Transfer" }
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
// Transfer documents
|
|
701
|
+
await this.transferDocumentsBetweenDatabases(
|
|
702
|
+
this.sourceDatabases,
|
|
703
|
+
this.targetDatabases,
|
|
704
|
+
dbId,
|
|
705
|
+
dbId,
|
|
706
|
+
collection.$id,
|
|
707
|
+
collection.$id
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
MessageFormatter.success(
|
|
711
|
+
`Documents transferred for collection ${collection.name}`,
|
|
712
|
+
{ prefix: "Transfer" }
|
|
713
|
+
);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
MessageFormatter.error(
|
|
716
|
+
`Error transferring documents for collection ${collection.name}`,
|
|
717
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
718
|
+
{ prefix: "Transfer" }
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
} catch (error) {
|
|
723
|
+
MessageFormatter.error(
|
|
724
|
+
`Failed to transfer documents for database ${dbId}`,
|
|
725
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
726
|
+
{ prefix: "Transfer" }
|
|
727
|
+
);
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private async transferAllBuckets(): Promise<void> {
|
|
733
|
+
MessageFormatter.info("Starting bucket transfer phase", {
|
|
734
|
+
prefix: "Transfer",
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
// Get all buckets from source with pagination
|
|
739
|
+
const allSourceBuckets = await this.fetchAllBuckets(this.sourceStorage);
|
|
740
|
+
const allTargetBuckets = await this.fetchAllBuckets(this.targetStorage);
|
|
741
|
+
|
|
742
|
+
if (this.options.dryRun) {
|
|
743
|
+
let totalFiles = 0;
|
|
744
|
+
for (const bucket of allSourceBuckets) {
|
|
745
|
+
const files = await this.sourceStorage.listFiles(bucket.$id, [
|
|
746
|
+
Query.limit(1),
|
|
747
|
+
]);
|
|
748
|
+
totalFiles += files.total;
|
|
749
|
+
}
|
|
750
|
+
MessageFormatter.info(
|
|
751
|
+
`DRY RUN: Would transfer ${allSourceBuckets.length} buckets with ${totalFiles} files`,
|
|
752
|
+
{ prefix: "Transfer" }
|
|
753
|
+
);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const transferTasks = allSourceBuckets.map((bucket) =>
|
|
758
|
+
this.limit(async () => {
|
|
759
|
+
try {
|
|
760
|
+
// Check if bucket exists in target
|
|
761
|
+
const existingBucket = allTargetBuckets.find(
|
|
762
|
+
(tb) => tb.$id === bucket.$id
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
if (!existingBucket) {
|
|
766
|
+
// Create bucket with fallback strategy for maximumFileSize
|
|
767
|
+
await this.createBucketWithFallback(bucket);
|
|
768
|
+
MessageFormatter.success(`Created bucket: ${bucket.name}`, {
|
|
769
|
+
prefix: "Transfer",
|
|
770
|
+
});
|
|
771
|
+
} else {
|
|
772
|
+
// Compare bucket permissions and update if needed
|
|
773
|
+
const sourcePermissions = JSON.stringify(
|
|
774
|
+
bucket.$permissions?.sort() || []
|
|
775
|
+
);
|
|
776
|
+
const targetPermissions = JSON.stringify(
|
|
777
|
+
existingBucket.$permissions?.sort() || []
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
if (
|
|
781
|
+
sourcePermissions !== targetPermissions ||
|
|
782
|
+
existingBucket.name !== bucket.name ||
|
|
783
|
+
existingBucket.fileSecurity !== bucket.fileSecurity ||
|
|
784
|
+
existingBucket.enabled !== bucket.enabled
|
|
785
|
+
) {
|
|
786
|
+
MessageFormatter.warning(
|
|
787
|
+
`Bucket ${bucket.name} exists but has different settings. Updating to match source.`,
|
|
788
|
+
{ prefix: "Transfer" }
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
await this.targetStorage.updateBucket(
|
|
793
|
+
bucket.$id,
|
|
794
|
+
bucket.name,
|
|
795
|
+
bucket.$permissions,
|
|
796
|
+
bucket.fileSecurity,
|
|
797
|
+
bucket.enabled,
|
|
798
|
+
bucket.maximumFileSize,
|
|
799
|
+
bucket.allowedFileExtensions,
|
|
800
|
+
bucket.compression as any,
|
|
801
|
+
bucket.encryption,
|
|
802
|
+
bucket.antivirus
|
|
803
|
+
);
|
|
804
|
+
MessageFormatter.success(
|
|
805
|
+
`Updated bucket ${bucket.name} to match source`,
|
|
806
|
+
{ prefix: "Transfer" }
|
|
807
|
+
);
|
|
808
|
+
} catch (updateError) {
|
|
809
|
+
MessageFormatter.error(
|
|
810
|
+
`Failed to update bucket ${bucket.name}`,
|
|
811
|
+
updateError instanceof Error
|
|
812
|
+
? updateError
|
|
813
|
+
: new Error(String(updateError)),
|
|
814
|
+
{ prefix: "Transfer" }
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
} else {
|
|
818
|
+
MessageFormatter.info(
|
|
819
|
+
`Bucket ${bucket.name} already exists with matching settings`,
|
|
820
|
+
{ prefix: "Transfer" }
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Transfer bucket files with enhanced validation
|
|
826
|
+
await this.transferBucketFiles(bucket.$id, bucket.$id);
|
|
827
|
+
|
|
828
|
+
this.results.buckets.transferred++;
|
|
829
|
+
MessageFormatter.success(
|
|
830
|
+
`Bucket ${bucket.name} transferred successfully`,
|
|
831
|
+
{ prefix: "Transfer" }
|
|
832
|
+
);
|
|
833
|
+
} catch (error) {
|
|
834
|
+
MessageFormatter.error(
|
|
835
|
+
`Bucket ${bucket.name} transfer failed`,
|
|
836
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
837
|
+
{ prefix: "Transfer" }
|
|
838
|
+
);
|
|
839
|
+
this.results.buckets.failed++;
|
|
840
|
+
}
|
|
841
|
+
})
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
await Promise.all(transferTasks);
|
|
845
|
+
MessageFormatter.success("Bucket transfer phase completed", {
|
|
846
|
+
prefix: "Transfer",
|
|
847
|
+
});
|
|
848
|
+
} catch (error) {
|
|
849
|
+
MessageFormatter.error(
|
|
850
|
+
"Bucket transfer phase failed",
|
|
851
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
852
|
+
{ prefix: "Transfer" }
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private async createBucketWithFallback(bucket: Models.Bucket): Promise<void> {
|
|
858
|
+
// Determine the optimal size to try first
|
|
859
|
+
let sizeToTry: number;
|
|
860
|
+
|
|
861
|
+
if (this.cachedMaxFileSize) {
|
|
862
|
+
// Use cached size if it's smaller than or equal to the bucket's original size
|
|
863
|
+
if (bucket.maximumFileSize >= this.cachedMaxFileSize) {
|
|
864
|
+
sizeToTry = this.cachedMaxFileSize;
|
|
865
|
+
MessageFormatter.info(
|
|
866
|
+
`Bucket ${bucket.name}: Using cached maximumFileSize ${sizeToTry} (${(
|
|
867
|
+
sizeToTry / 1_000_000_000
|
|
868
|
+
).toFixed(1)}GB)`,
|
|
869
|
+
{ prefix: "Transfer" }
|
|
870
|
+
);
|
|
871
|
+
} else {
|
|
872
|
+
// Original size is smaller than cached size, try original first
|
|
873
|
+
sizeToTry = bucket.maximumFileSize;
|
|
874
|
+
}
|
|
875
|
+
} else {
|
|
876
|
+
// No cached size yet, try original size first
|
|
877
|
+
sizeToTry = bucket.maximumFileSize;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Try the optimal size first
|
|
881
|
+
try {
|
|
882
|
+
await this.targetStorage.createBucket(
|
|
883
|
+
bucket.$id,
|
|
884
|
+
bucket.name,
|
|
885
|
+
bucket.$permissions,
|
|
886
|
+
bucket.fileSecurity,
|
|
887
|
+
bucket.enabled,
|
|
888
|
+
sizeToTry,
|
|
889
|
+
bucket.allowedFileExtensions,
|
|
890
|
+
bucket.compression as any,
|
|
891
|
+
bucket.encryption,
|
|
892
|
+
bucket.antivirus
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
// Success - cache this size if it's not already cached or is smaller than cached
|
|
896
|
+
if (!this.cachedMaxFileSize || sizeToTry < this.cachedMaxFileSize) {
|
|
897
|
+
this.cachedMaxFileSize = sizeToTry;
|
|
898
|
+
MessageFormatter.info(
|
|
899
|
+
`Bucket ${
|
|
900
|
+
bucket.name
|
|
901
|
+
}: Cached successful maximumFileSize ${sizeToTry} (${(
|
|
902
|
+
sizeToTry / 1_000_000_000
|
|
903
|
+
).toFixed(1)}GB)`,
|
|
904
|
+
{ prefix: "Transfer" }
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Log if we used a different size than original
|
|
909
|
+
if (sizeToTry !== bucket.maximumFileSize) {
|
|
910
|
+
MessageFormatter.warning(
|
|
911
|
+
`Bucket ${
|
|
912
|
+
bucket.name
|
|
913
|
+
}: maximumFileSize used ${sizeToTry} instead of original ${
|
|
914
|
+
bucket.maximumFileSize
|
|
915
|
+
} (${(sizeToTry / 1_000_000_000).toFixed(1)}GB)`,
|
|
916
|
+
{ prefix: "Transfer" }
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return; // Success, exit the function
|
|
921
|
+
} catch (error) {
|
|
922
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
923
|
+
|
|
924
|
+
// Check if the error is related to maximumFileSize validation
|
|
925
|
+
if (
|
|
926
|
+
err.message.includes("maximumFileSize") ||
|
|
927
|
+
err.message.includes("valid range")
|
|
928
|
+
) {
|
|
929
|
+
MessageFormatter.warning(
|
|
930
|
+
`Bucket ${bucket.name}: Failed with maximumFileSize ${sizeToTry}, falling back to smaller sizes...`,
|
|
931
|
+
{ prefix: "Transfer" }
|
|
932
|
+
);
|
|
933
|
+
// Continue to fallback logic below
|
|
934
|
+
} else {
|
|
935
|
+
// Different error, don't retry
|
|
936
|
+
throw err;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Fallback to progressively smaller sizes
|
|
941
|
+
const fallbackSizes = [
|
|
942
|
+
5_000_000_000, // 5GB
|
|
943
|
+
2_500_000_000, // 2.5GB
|
|
944
|
+
2_000_000_000, // 2GB
|
|
945
|
+
1_000_000_000, // 1GB
|
|
946
|
+
500_000_000, // 500MB
|
|
947
|
+
100_000_000, // 100MB
|
|
948
|
+
];
|
|
949
|
+
|
|
950
|
+
// Remove sizes that are larger than or equal to the already-tried size
|
|
951
|
+
const validSizes = fallbackSizes
|
|
952
|
+
.filter((size) => size < sizeToTry)
|
|
953
|
+
.sort((a, b) => b - a); // Sort descending
|
|
954
|
+
|
|
955
|
+
let lastError: Error | null = null;
|
|
956
|
+
|
|
957
|
+
for (const fileSize of validSizes) {
|
|
958
|
+
try {
|
|
959
|
+
await this.targetStorage.createBucket(
|
|
960
|
+
bucket.$id,
|
|
961
|
+
bucket.name,
|
|
962
|
+
bucket.$permissions,
|
|
963
|
+
bucket.fileSecurity,
|
|
964
|
+
bucket.enabled,
|
|
965
|
+
fileSize,
|
|
966
|
+
bucket.allowedFileExtensions,
|
|
967
|
+
bucket.compression as any,
|
|
968
|
+
bucket.encryption,
|
|
969
|
+
bucket.antivirus
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
// Success - cache this size if it's not already cached or is smaller than cached
|
|
973
|
+
if (!this.cachedMaxFileSize || fileSize < this.cachedMaxFileSize) {
|
|
974
|
+
this.cachedMaxFileSize = fileSize;
|
|
975
|
+
MessageFormatter.info(
|
|
976
|
+
`Bucket ${
|
|
977
|
+
bucket.name
|
|
978
|
+
}: Cached successful maximumFileSize ${fileSize} (${(
|
|
979
|
+
fileSize / 1_000_000_000
|
|
980
|
+
).toFixed(1)}GB)`,
|
|
981
|
+
{ prefix: "Transfer" }
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Log if we had to reduce the file size
|
|
986
|
+
if (fileSize !== bucket.maximumFileSize) {
|
|
987
|
+
MessageFormatter.warning(
|
|
988
|
+
`Bucket ${bucket.name}: maximumFileSize reduced from ${
|
|
989
|
+
bucket.maximumFileSize
|
|
990
|
+
} to ${fileSize} (${(fileSize / 1_000_000_000).toFixed(1)}GB)`,
|
|
991
|
+
{ prefix: "Transfer" }
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return; // Success, exit the function
|
|
996
|
+
} catch (error) {
|
|
997
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
998
|
+
|
|
999
|
+
// Check if the error is related to maximumFileSize validation
|
|
1000
|
+
if (
|
|
1001
|
+
lastError.message.includes("maximumFileSize") ||
|
|
1002
|
+
lastError.message.includes("valid range")
|
|
1003
|
+
) {
|
|
1004
|
+
MessageFormatter.warning(
|
|
1005
|
+
`Bucket ${bucket.name}: Failed with maximumFileSize ${fileSize}, trying smaller size...`,
|
|
1006
|
+
{ prefix: "Transfer" }
|
|
1007
|
+
);
|
|
1008
|
+
continue; // Try next smaller size
|
|
1009
|
+
} else {
|
|
1010
|
+
// Different error, don't retry
|
|
1011
|
+
throw lastError;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// If we get here, all fallback sizes failed
|
|
1017
|
+
MessageFormatter.error(
|
|
1018
|
+
`Bucket ${bucket.name}: All fallback file sizes failed. Last error: ${lastError?.message}`,
|
|
1019
|
+
lastError || undefined,
|
|
1020
|
+
{ prefix: "Transfer" }
|
|
1021
|
+
);
|
|
1022
|
+
throw lastError || new Error("All fallback file sizes failed");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
private async transferBucketFiles(
|
|
1026
|
+
sourceBucketId: string,
|
|
1027
|
+
targetBucketId: string
|
|
1028
|
+
): Promise<void> {
|
|
1029
|
+
let lastFileId: string | undefined;
|
|
1030
|
+
let transferredFiles = 0;
|
|
1031
|
+
|
|
1032
|
+
while (true) {
|
|
1033
|
+
const queries = [Query.limit(50)]; // Smaller batch size for better rate limiting
|
|
1034
|
+
if (lastFileId) {
|
|
1035
|
+
queries.push(Query.cursorAfter(lastFileId));
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const files = await this.sourceStorage.listFiles(sourceBucketId, queries);
|
|
1039
|
+
if (files.files.length === 0) break;
|
|
1040
|
+
|
|
1041
|
+
// Process files with rate limiting
|
|
1042
|
+
const fileTasks = files.files.map((file) =>
|
|
1043
|
+
this.fileLimit(async () => {
|
|
1044
|
+
try {
|
|
1045
|
+
// Check if file already exists and compare permissions
|
|
1046
|
+
let existingFile: Models.File | null = null;
|
|
1047
|
+
try {
|
|
1048
|
+
existingFile = await this.targetStorage.getFile(
|
|
1049
|
+
targetBucketId,
|
|
1050
|
+
file.$id
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
// Compare permissions between source and target file
|
|
1054
|
+
const sourcePermissions = JSON.stringify(
|
|
1055
|
+
file.$permissions?.sort() || []
|
|
1056
|
+
);
|
|
1057
|
+
const targetPermissions = JSON.stringify(
|
|
1058
|
+
existingFile.$permissions?.sort() || []
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
if (sourcePermissions !== targetPermissions) {
|
|
1062
|
+
MessageFormatter.warning(
|
|
1063
|
+
`File ${file.name} (${file.$id}) exists but has different permissions. Source: ${sourcePermissions}, Target: ${targetPermissions}`,
|
|
1064
|
+
{ prefix: "Transfer" }
|
|
1065
|
+
);
|
|
1066
|
+
|
|
1067
|
+
// Update file permissions to match source
|
|
1068
|
+
try {
|
|
1069
|
+
await this.targetStorage.updateFile(
|
|
1070
|
+
targetBucketId,
|
|
1071
|
+
file.$id,
|
|
1072
|
+
file.name,
|
|
1073
|
+
file.$permissions
|
|
1074
|
+
);
|
|
1075
|
+
MessageFormatter.success(
|
|
1076
|
+
`Updated file ${file.name} permissions to match source`,
|
|
1077
|
+
{ prefix: "Transfer" }
|
|
1078
|
+
);
|
|
1079
|
+
} catch (updateError) {
|
|
1080
|
+
MessageFormatter.error(
|
|
1081
|
+
`Failed to update permissions for file ${file.name}`,
|
|
1082
|
+
updateError instanceof Error
|
|
1083
|
+
? updateError
|
|
1084
|
+
: new Error(String(updateError)),
|
|
1085
|
+
{ prefix: "Transfer" }
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
} else {
|
|
1089
|
+
MessageFormatter.info(
|
|
1090
|
+
`File ${file.name} already exists with matching permissions, skipping`,
|
|
1091
|
+
{ prefix: "Transfer" }
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
return;
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
// File doesn't exist, proceed with transfer
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Download file with validation
|
|
1100
|
+
const fileData = await this.validateAndDownloadFile(
|
|
1101
|
+
sourceBucketId,
|
|
1102
|
+
file.$id
|
|
1103
|
+
);
|
|
1104
|
+
if (!fileData) {
|
|
1105
|
+
MessageFormatter.warning(
|
|
1106
|
+
`File ${file.name} failed validation, skipping`,
|
|
1107
|
+
{ prefix: "Transfer" }
|
|
1108
|
+
);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Upload file to target
|
|
1113
|
+
const fileToCreate = InputFile.fromBuffer(
|
|
1114
|
+
new Uint8Array(fileData),
|
|
1115
|
+
file.name
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
await this.targetStorage.createFile(
|
|
1119
|
+
targetBucketId,
|
|
1120
|
+
file.$id,
|
|
1121
|
+
fileToCreate,
|
|
1122
|
+
file.$permissions
|
|
1123
|
+
);
|
|
1124
|
+
|
|
1125
|
+
transferredFiles++;
|
|
1126
|
+
MessageFormatter.success(`Transferred file: ${file.name}`, {
|
|
1127
|
+
prefix: "Transfer",
|
|
1128
|
+
});
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
MessageFormatter.error(
|
|
1131
|
+
`Failed to transfer file ${file.name}`,
|
|
1132
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1133
|
+
{ prefix: "Transfer" }
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
})
|
|
1137
|
+
);
|
|
1138
|
+
|
|
1139
|
+
await Promise.all(fileTasks);
|
|
1140
|
+
|
|
1141
|
+
if (files.files.length < 50) break;
|
|
1142
|
+
lastFileId = files.files[files.files.length - 1].$id;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
MessageFormatter.info(
|
|
1146
|
+
`Transferred ${transferredFiles} files from bucket ${sourceBucketId}`,
|
|
1147
|
+
{ prefix: "Transfer" }
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
private async validateAndDownloadFile(
|
|
1152
|
+
bucketId: string,
|
|
1153
|
+
fileId: string
|
|
1154
|
+
): Promise<ArrayBuffer | null> {
|
|
1155
|
+
let attempts = 3;
|
|
1156
|
+
while (attempts > 0) {
|
|
1157
|
+
try {
|
|
1158
|
+
const fileData = await this.sourceStorage.getFileDownload(
|
|
1159
|
+
bucketId,
|
|
1160
|
+
fileId
|
|
1161
|
+
);
|
|
1162
|
+
|
|
1163
|
+
// Basic validation - ensure file is not empty and not too large
|
|
1164
|
+
if (fileData.byteLength === 0) {
|
|
1165
|
+
MessageFormatter.warning(`File ${fileId} is empty`, {
|
|
1166
|
+
prefix: "Transfer",
|
|
1167
|
+
});
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
if (fileData.byteLength > 50 * 1024 * 1024) {
|
|
1172
|
+
// 50MB limit
|
|
1173
|
+
MessageFormatter.warning(
|
|
1174
|
+
`File ${fileId} is too large (${fileData.byteLength} bytes)`,
|
|
1175
|
+
{ prefix: "Transfer" }
|
|
1176
|
+
);
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return fileData;
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
attempts--;
|
|
1183
|
+
MessageFormatter.warning(
|
|
1184
|
+
`Error downloading file ${fileId}, attempts left: ${attempts}`,
|
|
1185
|
+
{ prefix: "Transfer" }
|
|
1186
|
+
);
|
|
1187
|
+
if (attempts === 0) {
|
|
1188
|
+
MessageFormatter.error(
|
|
1189
|
+
`Failed to download file ${fileId} after all attempts`,
|
|
1190
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1191
|
+
{ prefix: "Transfer" }
|
|
1192
|
+
);
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
// Wait before retry
|
|
1196
|
+
await new Promise((resolve) =>
|
|
1197
|
+
setTimeout(resolve, 1000 * (4 - attempts))
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
private async transferAllFunctions(): Promise<void> {
|
|
1205
|
+
MessageFormatter.info("Starting function transfer phase", {
|
|
1206
|
+
prefix: "Transfer",
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
try {
|
|
1210
|
+
const sourceFunctions = await listFunctions(this.sourceClient, [
|
|
1211
|
+
Query.limit(1000),
|
|
1212
|
+
]);
|
|
1213
|
+
const targetFunctions = await listFunctions(this.targetClient, [
|
|
1214
|
+
Query.limit(1000),
|
|
1215
|
+
]);
|
|
1216
|
+
|
|
1217
|
+
if (this.options.dryRun) {
|
|
1218
|
+
MessageFormatter.info(
|
|
1219
|
+
`DRY RUN: Would transfer ${sourceFunctions.functions.length} functions`,
|
|
1220
|
+
{ prefix: "Transfer" }
|
|
1221
|
+
);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const transferTasks = sourceFunctions.functions.map((func) =>
|
|
1226
|
+
this.limit(async () => {
|
|
1227
|
+
try {
|
|
1228
|
+
// Check if function exists in target
|
|
1229
|
+
const existingFunc = targetFunctions.functions.find(
|
|
1230
|
+
(tf) => tf.$id === func.$id
|
|
1231
|
+
);
|
|
1232
|
+
|
|
1233
|
+
if (existingFunc) {
|
|
1234
|
+
MessageFormatter.info(
|
|
1235
|
+
`Function ${func.name} already exists, skipping creation`,
|
|
1236
|
+
{ prefix: "Transfer" }
|
|
1237
|
+
);
|
|
1238
|
+
this.results.functions.skipped++;
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Download function from source
|
|
1243
|
+
const functionPath = await this.downloadFunction(func);
|
|
1244
|
+
if (!functionPath) {
|
|
1245
|
+
MessageFormatter.error(
|
|
1246
|
+
`Failed to download function ${func.name}`,
|
|
1247
|
+
undefined,
|
|
1248
|
+
{ prefix: "Transfer" }
|
|
1249
|
+
);
|
|
1250
|
+
this.results.functions.failed++;
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Deploy function to target
|
|
1255
|
+
const functionConfig = {
|
|
1256
|
+
$id: func.$id,
|
|
1257
|
+
name: func.name,
|
|
1258
|
+
runtime: func.runtime as any,
|
|
1259
|
+
execute: func.execute,
|
|
1260
|
+
events: func.events,
|
|
1261
|
+
enabled: func.enabled,
|
|
1262
|
+
logging: func.logging,
|
|
1263
|
+
entrypoint: func.entrypoint,
|
|
1264
|
+
commands: func.commands,
|
|
1265
|
+
scopes: func.scopes as any,
|
|
1266
|
+
timeout: func.timeout,
|
|
1267
|
+
schedule: func.schedule,
|
|
1268
|
+
installationId: func.installationId,
|
|
1269
|
+
providerRepositoryId: func.providerRepositoryId,
|
|
1270
|
+
providerBranch: func.providerBranch,
|
|
1271
|
+
providerSilentMode: func.providerSilentMode,
|
|
1272
|
+
providerRootDirectory: func.providerRootDirectory,
|
|
1273
|
+
specification: func.specification as any,
|
|
1274
|
+
dirPath: functionPath,
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
await deployLocalFunction(
|
|
1278
|
+
this.targetClient,
|
|
1279
|
+
func.name,
|
|
1280
|
+
functionConfig
|
|
1281
|
+
);
|
|
1282
|
+
|
|
1283
|
+
this.results.functions.transferred++;
|
|
1284
|
+
MessageFormatter.success(
|
|
1285
|
+
`Function ${func.name} transferred successfully`,
|
|
1286
|
+
{ prefix: "Transfer" }
|
|
1287
|
+
);
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
MessageFormatter.error(
|
|
1290
|
+
`Function ${func.name} transfer failed`,
|
|
1291
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1292
|
+
{ prefix: "Transfer" }
|
|
1293
|
+
);
|
|
1294
|
+
this.results.functions.failed++;
|
|
1295
|
+
}
|
|
1296
|
+
})
|
|
1297
|
+
);
|
|
1298
|
+
|
|
1299
|
+
await Promise.all(transferTasks);
|
|
1300
|
+
MessageFormatter.success("Function transfer phase completed", {
|
|
1301
|
+
prefix: "Transfer",
|
|
1302
|
+
});
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
MessageFormatter.error(
|
|
1305
|
+
"Function transfer phase failed",
|
|
1306
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1307
|
+
{ prefix: "Transfer" }
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
private async downloadFunction(
|
|
1313
|
+
func: Models.Function
|
|
1314
|
+
): Promise<string | null> {
|
|
1315
|
+
try {
|
|
1316
|
+
const { path } = await downloadLatestFunctionDeployment(
|
|
1317
|
+
this.sourceClient,
|
|
1318
|
+
func.$id,
|
|
1319
|
+
this.tempDir
|
|
1320
|
+
);
|
|
1321
|
+
return path;
|
|
1322
|
+
} catch (error) {
|
|
1323
|
+
MessageFormatter.error(
|
|
1324
|
+
`Failed to download function ${func.name}`,
|
|
1325
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1326
|
+
{ prefix: "Transfer" }
|
|
1327
|
+
);
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Helper method to fetch all collections from a database
|
|
1334
|
+
*/
|
|
1335
|
+
private async fetchAllCollections(
|
|
1336
|
+
dbId: string,
|
|
1337
|
+
databases: Databases
|
|
1338
|
+
): Promise<Models.Collection[]> {
|
|
1339
|
+
const collections: Models.Collection[] = [];
|
|
1340
|
+
let lastId: string | undefined;
|
|
1341
|
+
|
|
1342
|
+
while (true) {
|
|
1343
|
+
const queries = [Query.limit(100)];
|
|
1344
|
+
if (lastId) {
|
|
1345
|
+
queries.push(Query.cursorAfter(lastId));
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const result = await tryAwaitWithRetry(async () =>
|
|
1349
|
+
databases.listCollections(dbId, queries)
|
|
1350
|
+
);
|
|
1351
|
+
|
|
1352
|
+
if (result.collections.length === 0) {
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
collections.push(...result.collections);
|
|
1357
|
+
|
|
1358
|
+
if (result.collections.length < 100) {
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
lastId = result.collections[result.collections.length - 1].$id;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
return collections;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Helper method to fetch all buckets with pagination
|
|
1370
|
+
*/
|
|
1371
|
+
private async fetchAllBuckets(storage: Storage): Promise<Models.Bucket[]> {
|
|
1372
|
+
const buckets: Models.Bucket[] = [];
|
|
1373
|
+
let lastId: string | undefined;
|
|
1374
|
+
|
|
1375
|
+
while (true) {
|
|
1376
|
+
const queries = [Query.limit(100)];
|
|
1377
|
+
if (lastId) {
|
|
1378
|
+
queries.push(Query.cursorAfter(lastId));
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const result = await tryAwaitWithRetry(async () =>
|
|
1382
|
+
storage.listBuckets(queries)
|
|
1383
|
+
);
|
|
1384
|
+
|
|
1385
|
+
if (result.buckets.length === 0) {
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
buckets.push(...result.buckets);
|
|
1390
|
+
|
|
1391
|
+
if (result.buckets.length < 100) {
|
|
1392
|
+
break;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
lastId = result.buckets[result.buckets.length - 1].$id;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
return buckets;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Helper method to parse attribute objects (simplified version of parseAttribute)
|
|
1403
|
+
*/
|
|
1404
|
+
private parseAttribute(attr: any): any {
|
|
1405
|
+
// This is a simplified version - in production you'd use the actual parseAttribute from appwrite-utils
|
|
1406
|
+
return {
|
|
1407
|
+
key: attr.key,
|
|
1408
|
+
type: attr.type,
|
|
1409
|
+
size: attr.size,
|
|
1410
|
+
required: attr.required,
|
|
1411
|
+
array: attr.array,
|
|
1412
|
+
default: attr.default,
|
|
1413
|
+
format: attr.format,
|
|
1414
|
+
elements: attr.elements,
|
|
1415
|
+
min: attr.min,
|
|
1416
|
+
max: attr.max,
|
|
1417
|
+
relatedCollection: attr.relatedCollection,
|
|
1418
|
+
relationType: attr.relationType,
|
|
1419
|
+
twoWay: attr.twoWay,
|
|
1420
|
+
twoWayKey: attr.twoWayKey,
|
|
1421
|
+
onDelete: attr.onDelete,
|
|
1422
|
+
side: attr.side,
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Helper method to create collection attributes with status checking
|
|
1428
|
+
*/
|
|
1429
|
+
private async createCollectionAttributesWithStatusCheck(
|
|
1430
|
+
databases: Databases,
|
|
1431
|
+
dbId: string,
|
|
1432
|
+
collection: Models.Collection,
|
|
1433
|
+
attributes: any[]
|
|
1434
|
+
): Promise<boolean> {
|
|
1435
|
+
if (!this.targetAdapter) {
|
|
1436
|
+
throw new Error('Target adapter not initialized');
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
try {
|
|
1440
|
+
// Create non-relationship attributes first
|
|
1441
|
+
const nonRel = (attributes || []).filter((a: any) => a.type !== 'relationship');
|
|
1442
|
+
for (const attr of nonRel) {
|
|
1443
|
+
const params = mapToCreateAttributeParams(attr as any, { databaseId: dbId, tableId: collection.$id });
|
|
1444
|
+
await this.targetAdapter.createAttribute(params);
|
|
1445
|
+
// Small delay between creations
|
|
1446
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Wait for attributes to become available
|
|
1450
|
+
for (const attr of nonRel) {
|
|
1451
|
+
const maxWait = 60000; // 60s
|
|
1452
|
+
const start = Date.now();
|
|
1453
|
+
let lastStatus = '';
|
|
1454
|
+
while (Date.now() - start < maxWait) {
|
|
1455
|
+
try {
|
|
1456
|
+
const tableRes = await this.targetAdapter.getTable({ databaseId: dbId, tableId: collection.$id });
|
|
1457
|
+
const cols = (tableRes as any).attributes || (tableRes as any).columns || [];
|
|
1458
|
+
const col = cols.find((c: any) => c.key === attr.key);
|
|
1459
|
+
if (col) {
|
|
1460
|
+
if (col.status === 'available') break;
|
|
1461
|
+
if (col.status === 'failed' || col.status === 'stuck') {
|
|
1462
|
+
throw new Error(col.error || `Attribute ${attr.key} failed`);
|
|
1463
|
+
}
|
|
1464
|
+
lastStatus = col.status;
|
|
1465
|
+
}
|
|
1466
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1467
|
+
} catch {
|
|
1468
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (Date.now() - start >= maxWait) {
|
|
1472
|
+
MessageFormatter.warning(
|
|
1473
|
+
`Attribute ${attr.key} did not become available within 60s (last status: ${lastStatus})`,
|
|
1474
|
+
{ prefix: 'Attributes' }
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// Create relationship attributes
|
|
1480
|
+
const rels = (attributes || []).filter((a: any) => a.type === 'relationship');
|
|
1481
|
+
for (const attr of rels) {
|
|
1482
|
+
const params = mapToCreateAttributeParams(attr as any, { databaseId: dbId, tableId: collection.$id });
|
|
1483
|
+
await this.targetAdapter.createAttribute(params);
|
|
1484
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
return true;
|
|
1488
|
+
} catch (e) {
|
|
1489
|
+
MessageFormatter.error('Failed creating attributes via adapter', e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
|
|
1490
|
+
return false;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* Helper method to create collection indexes with status checking
|
|
1496
|
+
*/
|
|
1497
|
+
private async createCollectionIndexesWithStatusCheck(
|
|
1498
|
+
dbId: string,
|
|
1499
|
+
databases: Databases,
|
|
1500
|
+
collectionId: string,
|
|
1501
|
+
collection: Models.Collection,
|
|
1502
|
+
indexes: any[]
|
|
1503
|
+
): Promise<boolean> {
|
|
1504
|
+
if (!this.targetAdapter) {
|
|
1505
|
+
throw new Error('Target adapter not initialized');
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
try {
|
|
1509
|
+
for (const idx of indexes || []) {
|
|
1510
|
+
await this.targetAdapter.createIndex({
|
|
1511
|
+
databaseId: dbId,
|
|
1512
|
+
tableId: collectionId,
|
|
1513
|
+
key: idx.key,
|
|
1514
|
+
type: idx.type,
|
|
1515
|
+
attributes: idx.attributes,
|
|
1516
|
+
orders: idx.orders || []
|
|
1517
|
+
});
|
|
1518
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1519
|
+
}
|
|
1520
|
+
return true;
|
|
1521
|
+
} catch (e) {
|
|
1522
|
+
MessageFormatter.error('Failed creating indexes via adapter', e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
|
|
1523
|
+
return false;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* Helper method to transfer documents between databases using bulk operations with content and permission-based filtering
|
|
1529
|
+
*/
|
|
1530
|
+
private async transferDocumentsBetweenDatabases(
|
|
1531
|
+
sourceDb: Databases,
|
|
1532
|
+
targetDb: Databases,
|
|
1533
|
+
sourceDbId: string,
|
|
1534
|
+
targetDbId: string,
|
|
1535
|
+
sourceCollectionId: string,
|
|
1536
|
+
targetCollectionId: string
|
|
1537
|
+
): Promise<void> {
|
|
1538
|
+
MessageFormatter.info(
|
|
1539
|
+
`Transferring documents from ${sourceCollectionId} to ${targetCollectionId} with bulk operations, content comparison, and permission filtering`,
|
|
1540
|
+
{ prefix: "Transfer" }
|
|
1541
|
+
);
|
|
1542
|
+
|
|
1543
|
+
let lastId: string | undefined;
|
|
1544
|
+
let totalTransferred = 0;
|
|
1545
|
+
let totalSkipped = 0;
|
|
1546
|
+
let totalUpdated = 0;
|
|
1547
|
+
|
|
1548
|
+
// Check if bulk operations are supported
|
|
1549
|
+
const bulkEnabled = false;
|
|
1550
|
+
// Temporarily disable to see if it fixes my permissions issues
|
|
1551
|
+
const supportsBulk = bulkEnabled ? this.options.targetEndpoint.includes("cloud.appwrite.io") : false;
|
|
1552
|
+
|
|
1553
|
+
if (supportsBulk) {
|
|
1554
|
+
MessageFormatter.info(`Using bulk operations for enhanced performance`, {
|
|
1555
|
+
prefix: "Transfer",
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
while (true) {
|
|
1560
|
+
// Fetch source documents in larger batches (1000 instead of 50)
|
|
1561
|
+
const queries = [Query.limit(1000)];
|
|
1562
|
+
if (lastId) {
|
|
1563
|
+
queries.push(Query.cursorAfter(lastId));
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const sourceDocuments = await tryAwaitWithRetry(async () =>
|
|
1567
|
+
sourceDb.listDocuments(sourceDbId, sourceCollectionId, queries)
|
|
1568
|
+
);
|
|
1569
|
+
|
|
1570
|
+
if (sourceDocuments.documents.length === 0) {
|
|
1571
|
+
break;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
MessageFormatter.info(
|
|
1575
|
+
`Processing batch of ${sourceDocuments.documents.length} source documents`,
|
|
1576
|
+
{ prefix: "Transfer" }
|
|
1577
|
+
);
|
|
1578
|
+
|
|
1579
|
+
// Extract document IDs from the current batch
|
|
1580
|
+
const sourceDocIds = sourceDocuments.documents.map((doc) => doc.$id);
|
|
1581
|
+
|
|
1582
|
+
// Fetch existing documents from target in a single query
|
|
1583
|
+
const existingTargetDocs = await this.fetchTargetDocumentsBatch(
|
|
1584
|
+
targetDb,
|
|
1585
|
+
targetDbId,
|
|
1586
|
+
targetCollectionId,
|
|
1587
|
+
sourceDocIds
|
|
1588
|
+
);
|
|
1589
|
+
|
|
1590
|
+
// Create a map for quick lookup of existing documents
|
|
1591
|
+
const existingDocsMap = new Map<string, Models.Document>();
|
|
1592
|
+
existingTargetDocs.forEach((doc) => {
|
|
1593
|
+
existingDocsMap.set(doc.$id, doc);
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
// Filter documents based on existence, content comparison, and permission comparison
|
|
1597
|
+
const documentsToTransfer: Models.Document[] = [];
|
|
1598
|
+
const documentsToUpdate: {
|
|
1599
|
+
doc: Models.Document;
|
|
1600
|
+
targetDoc: Models.Document;
|
|
1601
|
+
reason: string;
|
|
1602
|
+
}[] = [];
|
|
1603
|
+
|
|
1604
|
+
for (const sourceDoc of sourceDocuments.documents) {
|
|
1605
|
+
const existingTargetDoc = existingDocsMap.get(sourceDoc.$id);
|
|
1606
|
+
|
|
1607
|
+
if (!existingTargetDoc) {
|
|
1608
|
+
// Document doesn't exist in target, needs to be transferred
|
|
1609
|
+
documentsToTransfer.push(sourceDoc);
|
|
1610
|
+
} else {
|
|
1611
|
+
// Document exists, compare both content and permissions
|
|
1612
|
+
const sourcePermissions = Array.from(
|
|
1613
|
+
new Set(sourceDoc.$permissions || [])
|
|
1614
|
+
).sort();
|
|
1615
|
+
const targetPermissions = Array.from(
|
|
1616
|
+
new Set(existingTargetDoc.$permissions || [])
|
|
1617
|
+
).sort();
|
|
1618
|
+
const permissionsDiffer =
|
|
1619
|
+
sourcePermissions.join(",") !== targetPermissions.join(",") ||
|
|
1620
|
+
sourcePermissions.length !== targetPermissions.length;
|
|
1621
|
+
|
|
1622
|
+
// Use objectNeedsUpdate to compare document content (excluding system fields)
|
|
1623
|
+
const contentDiffers = objectNeedsUpdate(
|
|
1624
|
+
existingTargetDoc,
|
|
1625
|
+
sourceDoc
|
|
1626
|
+
);
|
|
1627
|
+
|
|
1628
|
+
if (contentDiffers && permissionsDiffer) {
|
|
1629
|
+
// Both content and permissions differ
|
|
1630
|
+
documentsToUpdate.push({
|
|
1631
|
+
doc: sourceDoc,
|
|
1632
|
+
targetDoc: existingTargetDoc,
|
|
1633
|
+
reason: "content and permissions differ",
|
|
1634
|
+
});
|
|
1635
|
+
} else if (contentDiffers) {
|
|
1636
|
+
// Only content differs
|
|
1637
|
+
documentsToUpdate.push({
|
|
1638
|
+
doc: sourceDoc,
|
|
1639
|
+
targetDoc: existingTargetDoc,
|
|
1640
|
+
reason: "content differs",
|
|
1641
|
+
});
|
|
1642
|
+
} else if (permissionsDiffer) {
|
|
1643
|
+
// Only permissions differ
|
|
1644
|
+
documentsToUpdate.push({
|
|
1645
|
+
doc: sourceDoc,
|
|
1646
|
+
targetDoc: existingTargetDoc,
|
|
1647
|
+
reason: "permissions differ",
|
|
1648
|
+
});
|
|
1649
|
+
} else {
|
|
1650
|
+
// Document exists with identical content AND permissions, skip
|
|
1651
|
+
totalSkipped++;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
MessageFormatter.info(
|
|
1657
|
+
`Batch analysis: ${documentsToTransfer.length} to create, ${documentsToUpdate.length} to update, ${totalSkipped} skipped so far`,
|
|
1658
|
+
{ prefix: "Transfer" }
|
|
1659
|
+
);
|
|
1660
|
+
|
|
1661
|
+
// Process new documents with bulk operations if supported and available
|
|
1662
|
+
if (documentsToTransfer.length > 0) {
|
|
1663
|
+
if (supportsBulk && documentsToTransfer.length >= 10) {
|
|
1664
|
+
// Use bulk operations for large batches
|
|
1665
|
+
await this.transferDocumentsBulk(
|
|
1666
|
+
targetDb,
|
|
1667
|
+
targetDbId,
|
|
1668
|
+
targetCollectionId,
|
|
1669
|
+
documentsToTransfer
|
|
1670
|
+
);
|
|
1671
|
+
totalTransferred += documentsToTransfer.length;
|
|
1672
|
+
} else {
|
|
1673
|
+
// Use individual transfers for smaller batches or non-bulk endpoints
|
|
1674
|
+
const transferCount = await this.transferDocumentsIndividual(
|
|
1675
|
+
targetDb,
|
|
1676
|
+
targetDbId,
|
|
1677
|
+
targetCollectionId,
|
|
1678
|
+
documentsToTransfer
|
|
1679
|
+
);
|
|
1680
|
+
totalTransferred += transferCount;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// Process document updates (always individual since bulk update with permissions needs special handling)
|
|
1685
|
+
if (documentsToUpdate.length > 0) {
|
|
1686
|
+
const updateCount = await this.updateDocumentsIndividual(
|
|
1687
|
+
targetDb,
|
|
1688
|
+
targetDbId,
|
|
1689
|
+
targetCollectionId,
|
|
1690
|
+
documentsToUpdate
|
|
1691
|
+
);
|
|
1692
|
+
totalUpdated += updateCount;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
if (sourceDocuments.documents.length < 1000) {
|
|
1696
|
+
break;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
lastId =
|
|
1700
|
+
sourceDocuments.documents[sourceDocuments.documents.length - 1].$id;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
MessageFormatter.info(
|
|
1704
|
+
`Transfer complete: ${totalTransferred} new, ${totalUpdated} updated, ${totalSkipped} skipped from ${sourceCollectionId} to ${targetCollectionId}`,
|
|
1705
|
+
{ prefix: "Transfer" }
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* Fetch target documents by IDs in batches to check existence and permissions
|
|
1711
|
+
*/
|
|
1712
|
+
private async fetchTargetDocumentsBatch(
|
|
1713
|
+
targetDb: Databases,
|
|
1714
|
+
targetDbId: string,
|
|
1715
|
+
targetCollectionId: string,
|
|
1716
|
+
docIds: string[]
|
|
1717
|
+
): Promise<Models.Document[]> {
|
|
1718
|
+
const documents: Models.Document[] = [];
|
|
1719
|
+
|
|
1720
|
+
// Split IDs into chunks of 100 for Query.equal limitations
|
|
1721
|
+
const idChunks = this.chunkArray(docIds, 100);
|
|
1722
|
+
|
|
1723
|
+
for (const chunk of idChunks) {
|
|
1724
|
+
try {
|
|
1725
|
+
const result = await tryAwaitWithRetry(async () =>
|
|
1726
|
+
targetDb.listDocuments(targetDbId, targetCollectionId, [
|
|
1727
|
+
Query.equal("$id", chunk),
|
|
1728
|
+
Query.limit(100),
|
|
1729
|
+
])
|
|
1730
|
+
);
|
|
1731
|
+
documents.push(...result.documents);
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
// If query fails, fall back to individual gets (less efficient but more reliable)
|
|
1734
|
+
MessageFormatter.warning(
|
|
1735
|
+
`Batch query failed for ${chunk.length} documents, falling back to individual checks`,
|
|
1736
|
+
{ prefix: "Transfer" }
|
|
1737
|
+
);
|
|
1738
|
+
|
|
1739
|
+
for (const docId of chunk) {
|
|
1740
|
+
try {
|
|
1741
|
+
const doc = await targetDb.getDocument(
|
|
1742
|
+
targetDbId,
|
|
1743
|
+
targetCollectionId,
|
|
1744
|
+
docId
|
|
1745
|
+
);
|
|
1746
|
+
documents.push(doc);
|
|
1747
|
+
} catch (getError) {
|
|
1748
|
+
// Document doesn't exist, which is fine
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
return documents;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* Transfer documents using bulk operations with proper batch size handling
|
|
1759
|
+
*/
|
|
1760
|
+
private async transferDocumentsBulk(
|
|
1761
|
+
targetDb: Databases,
|
|
1762
|
+
targetDbId: string,
|
|
1763
|
+
targetCollectionId: string,
|
|
1764
|
+
documents: Models.Document[]
|
|
1765
|
+
): Promise<void> {
|
|
1766
|
+
// Prepare documents for bulk upsert
|
|
1767
|
+
const preparedDocs = documents.map((doc) => {
|
|
1768
|
+
const {
|
|
1769
|
+
$id,
|
|
1770
|
+
$createdAt,
|
|
1771
|
+
$updatedAt,
|
|
1772
|
+
$permissions,
|
|
1773
|
+
$databaseId,
|
|
1774
|
+
$collectionId,
|
|
1775
|
+
$sequence,
|
|
1776
|
+
...docData
|
|
1777
|
+
} = doc;
|
|
1778
|
+
return {
|
|
1779
|
+
$id,
|
|
1780
|
+
$permissions,
|
|
1781
|
+
...docData,
|
|
1782
|
+
};
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
// Process in smaller chunks for bulk operations (1000 for Pro, 100 for Free tier)
|
|
1786
|
+
const batchSizes = [1000, 100]; // Start with Pro plan, fallback to Free
|
|
1787
|
+
let processed = false;
|
|
1788
|
+
|
|
1789
|
+
for (const maxBatchSize of batchSizes) {
|
|
1790
|
+
const documentBatches = this.chunkArray(preparedDocs, maxBatchSize);
|
|
1791
|
+
|
|
1792
|
+
try {
|
|
1793
|
+
for (const batch of documentBatches) {
|
|
1794
|
+
await this.bulkUpsertDocuments(
|
|
1795
|
+
this.targetClient,
|
|
1796
|
+
targetDbId,
|
|
1797
|
+
targetCollectionId,
|
|
1798
|
+
batch
|
|
1799
|
+
);
|
|
1800
|
+
|
|
1801
|
+
MessageFormatter.success(
|
|
1802
|
+
`✅ Bulk upserted ${batch.length} documents`,
|
|
1803
|
+
{ prefix: "Transfer" }
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
processed = true;
|
|
1808
|
+
break; // Success, exit batch size loop
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
MessageFormatter.warning(
|
|
1811
|
+
`Bulk upsert with batch size ${maxBatchSize} failed, trying smaller size...`,
|
|
1812
|
+
{ prefix: "Transfer" }
|
|
1813
|
+
);
|
|
1814
|
+
continue; // Try next smaller batch size
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
if (!processed) {
|
|
1819
|
+
MessageFormatter.warning(
|
|
1820
|
+
`All bulk operations failed, falling back to individual transfers`,
|
|
1821
|
+
{ prefix: "Transfer" }
|
|
1822
|
+
);
|
|
1823
|
+
|
|
1824
|
+
// Fall back to individual transfers
|
|
1825
|
+
await this.transferDocumentsIndividual(
|
|
1826
|
+
targetDb,
|
|
1827
|
+
targetDbId,
|
|
1828
|
+
targetCollectionId,
|
|
1829
|
+
documents
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/**
|
|
1835
|
+
* Direct HTTP implementation of bulk upsert API
|
|
1836
|
+
*/
|
|
1837
|
+
private async bulkUpsertDocuments(
|
|
1838
|
+
client: any,
|
|
1839
|
+
dbId: string,
|
|
1840
|
+
collectionId: string,
|
|
1841
|
+
documents: any[]
|
|
1842
|
+
): Promise<any> {
|
|
1843
|
+
const apiPath = `/databases/${dbId}/collections/${collectionId}/documents`;
|
|
1844
|
+
const url = new URL(client.config.endpoint + apiPath);
|
|
1845
|
+
|
|
1846
|
+
const headers = {
|
|
1847
|
+
"Content-Type": "application/json",
|
|
1848
|
+
"X-Appwrite-Project": client.config.project,
|
|
1849
|
+
"X-Appwrite-Key": client.config.key,
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
const response = await fetch(url.toString(), {
|
|
1853
|
+
method: "PUT",
|
|
1854
|
+
headers,
|
|
1855
|
+
body: JSON.stringify({ documents }),
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
if (!response.ok) {
|
|
1859
|
+
const errorData: any = await response
|
|
1860
|
+
.json()
|
|
1861
|
+
.catch(() => ({ message: "Unknown error" }));
|
|
1862
|
+
throw new Error(
|
|
1863
|
+
`Bulk upsert failed: ${response.status} - ${
|
|
1864
|
+
errorData.message || "Unknown error"
|
|
1865
|
+
}`
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
return await response.json();
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/**
|
|
1873
|
+
* Transfer documents individually with rate limiting
|
|
1874
|
+
*/
|
|
1875
|
+
private async transferDocumentsIndividual(
|
|
1876
|
+
targetDb: Databases,
|
|
1877
|
+
targetDbId: string,
|
|
1878
|
+
targetCollectionId: string,
|
|
1879
|
+
documents: Models.Document[]
|
|
1880
|
+
): Promise<number> {
|
|
1881
|
+
let successCount = 0;
|
|
1882
|
+
|
|
1883
|
+
const transferTasks = documents.map((doc) =>
|
|
1884
|
+
this.limit(async () => {
|
|
1885
|
+
try {
|
|
1886
|
+
const {
|
|
1887
|
+
$id,
|
|
1888
|
+
$createdAt,
|
|
1889
|
+
$updatedAt,
|
|
1890
|
+
$permissions,
|
|
1891
|
+
$databaseId,
|
|
1892
|
+
$collectionId,
|
|
1893
|
+
$sequence,
|
|
1894
|
+
...docData
|
|
1895
|
+
} = doc;
|
|
1896
|
+
|
|
1897
|
+
await tryAwaitWithRetry(async () =>
|
|
1898
|
+
targetDb.createDocument(
|
|
1899
|
+
targetDbId,
|
|
1900
|
+
targetCollectionId,
|
|
1901
|
+
doc.$id,
|
|
1902
|
+
docData,
|
|
1903
|
+
doc.$permissions
|
|
1904
|
+
)
|
|
1905
|
+
);
|
|
1906
|
+
|
|
1907
|
+
successCount++;
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
if (
|
|
1910
|
+
error instanceof AppwriteException &&
|
|
1911
|
+
error.message.includes("already exists")
|
|
1912
|
+
) {
|
|
1913
|
+
try {
|
|
1914
|
+
// Update it! It's here because it needs an update or a create
|
|
1915
|
+
const {
|
|
1916
|
+
$id,
|
|
1917
|
+
$createdAt,
|
|
1918
|
+
$updatedAt,
|
|
1919
|
+
$permissions,
|
|
1920
|
+
$databaseId,
|
|
1921
|
+
$collectionId,
|
|
1922
|
+
$sequence,
|
|
1923
|
+
...docData
|
|
1924
|
+
} = doc;
|
|
1925
|
+
await tryAwaitWithRetry(async () =>
|
|
1926
|
+
targetDb.updateDocument(
|
|
1927
|
+
targetDbId,
|
|
1928
|
+
targetCollectionId,
|
|
1929
|
+
doc.$id,
|
|
1930
|
+
docData,
|
|
1931
|
+
doc.$permissions
|
|
1932
|
+
)
|
|
1933
|
+
);
|
|
1934
|
+
successCount++;
|
|
1935
|
+
} catch (updateError) {
|
|
1936
|
+
// just send the error to the formatter
|
|
1937
|
+
MessageFormatter.error(
|
|
1938
|
+
`Failed to transfer document ${doc.$id}`,
|
|
1939
|
+
updateError instanceof Error
|
|
1940
|
+
? updateError
|
|
1941
|
+
: new Error(String(updateError)),
|
|
1942
|
+
{ prefix: "Transfer" }
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
MessageFormatter.error(
|
|
1948
|
+
`Failed to transfer document ${doc.$id}`,
|
|
1949
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1950
|
+
{ prefix: "Transfer" }
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
})
|
|
1954
|
+
);
|
|
1955
|
+
|
|
1956
|
+
await Promise.all(transferTasks);
|
|
1957
|
+
return successCount;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
/**
|
|
1961
|
+
* Update documents individually with content and/or permission changes
|
|
1962
|
+
*/
|
|
1963
|
+
private async updateDocumentsIndividual(
|
|
1964
|
+
targetDb: Databases,
|
|
1965
|
+
targetDbId: string,
|
|
1966
|
+
targetCollectionId: string,
|
|
1967
|
+
documentPairs: {
|
|
1968
|
+
doc: Models.Document;
|
|
1969
|
+
targetDoc: Models.Document;
|
|
1970
|
+
reason: string;
|
|
1971
|
+
}[]
|
|
1972
|
+
): Promise<number> {
|
|
1973
|
+
let successCount = 0;
|
|
1974
|
+
|
|
1975
|
+
const updateTasks = documentPairs.map(({ doc, targetDoc, reason }) =>
|
|
1976
|
+
this.limit(async () => {
|
|
1977
|
+
try {
|
|
1978
|
+
const {
|
|
1979
|
+
$id,
|
|
1980
|
+
$createdAt,
|
|
1981
|
+
$updatedAt,
|
|
1982
|
+
$permissions,
|
|
1983
|
+
$databaseId,
|
|
1984
|
+
$collectionId,
|
|
1985
|
+
$sequence,
|
|
1986
|
+
...docData
|
|
1987
|
+
} = doc;
|
|
1988
|
+
|
|
1989
|
+
await tryAwaitWithRetry(async () =>
|
|
1990
|
+
targetDb.updateDocument(
|
|
1991
|
+
targetDbId,
|
|
1992
|
+
targetCollectionId,
|
|
1993
|
+
doc.$id,
|
|
1994
|
+
docData,
|
|
1995
|
+
$permissions,
|
|
1996
|
+
)
|
|
1997
|
+
);
|
|
1998
|
+
|
|
1999
|
+
successCount++;
|
|
2000
|
+
} catch (error) {
|
|
2001
|
+
MessageFormatter.error(
|
|
2002
|
+
`Failed to update document ${doc.$id} (${reason})`,
|
|
2003
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
2004
|
+
{ prefix: "Transfer" }
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
})
|
|
2008
|
+
);
|
|
2009
|
+
|
|
2010
|
+
await Promise.all(updateTasks);
|
|
2011
|
+
return successCount;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Utility method to chunk arrays
|
|
2016
|
+
*/
|
|
2017
|
+
private chunkArray<T>(array: T[], size: number): T[][] {
|
|
2018
|
+
const chunks: T[][] = [];
|
|
2019
|
+
for (let i = 0; i < array.length; i += size) {
|
|
2020
|
+
chunks.push(array.slice(i, i + size));
|
|
2021
|
+
}
|
|
2022
|
+
return chunks;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/**
|
|
2026
|
+
* Helper method to fetch all teams with pagination
|
|
2027
|
+
*/
|
|
2028
|
+
private async fetchAllTeams(
|
|
2029
|
+
teams: Teams
|
|
2030
|
+
): Promise<Models.Team<Models.Preferences>[]> {
|
|
2031
|
+
const teamsList: Models.Team<Models.Preferences>[] = [];
|
|
2032
|
+
let lastId: string | undefined;
|
|
2033
|
+
|
|
2034
|
+
while (true) {
|
|
2035
|
+
const queries = [Query.limit(100)];
|
|
2036
|
+
if (lastId) {
|
|
2037
|
+
queries.push(Query.cursorAfter(lastId));
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
const result = await tryAwaitWithRetry(async () => teams.list(queries));
|
|
2041
|
+
|
|
2042
|
+
if (result.teams.length === 0) {
|
|
2043
|
+
break;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
teamsList.push(...result.teams);
|
|
2047
|
+
|
|
2048
|
+
if (result.teams.length < 100) {
|
|
2049
|
+
break;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
lastId = result.teams[result.teams.length - 1].$id;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
return teamsList;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
/**
|
|
2059
|
+
* Helper method to fetch all memberships for a team with pagination
|
|
2060
|
+
*/
|
|
2061
|
+
private async fetchAllMemberships(
|
|
2062
|
+
teamId: string
|
|
2063
|
+
): Promise<Models.Membership[]> {
|
|
2064
|
+
const membershipsList: Models.Membership[] = [];
|
|
2065
|
+
let lastId: string | undefined;
|
|
2066
|
+
|
|
2067
|
+
while (true) {
|
|
2068
|
+
const queries = [Query.limit(100)];
|
|
2069
|
+
if (lastId) {
|
|
2070
|
+
queries.push(Query.cursorAfter(lastId));
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
const result = await tryAwaitWithRetry(async () =>
|
|
2074
|
+
this.sourceTeams.listMemberships(teamId, queries)
|
|
2075
|
+
);
|
|
2076
|
+
|
|
2077
|
+
if (result.memberships.length === 0) {
|
|
2078
|
+
break;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
membershipsList.push(...result.memberships);
|
|
2082
|
+
|
|
2083
|
+
if (result.memberships.length < 100) {
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
lastId = result.memberships[result.memberships.length - 1].$id;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
return membershipsList;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
/**
|
|
2094
|
+
* Helper method to transfer team memberships
|
|
2095
|
+
*/
|
|
2096
|
+
private async transferTeamMemberships(teamId: string): Promise<void> {
|
|
2097
|
+
MessageFormatter.info(`Transferring memberships for team ${teamId}`, {
|
|
2098
|
+
prefix: "Transfer",
|
|
2099
|
+
});
|
|
2100
|
+
|
|
2101
|
+
try {
|
|
2102
|
+
// Fetch all memberships for this team
|
|
2103
|
+
const memberships = await this.fetchAllMemberships(teamId);
|
|
2104
|
+
|
|
2105
|
+
if (memberships.length === 0) {
|
|
2106
|
+
MessageFormatter.info(`No memberships found for team ${teamId}`, {
|
|
2107
|
+
prefix: "Transfer",
|
|
2108
|
+
});
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
MessageFormatter.info(
|
|
2113
|
+
`Found ${memberships.length} memberships for team ${teamId}`,
|
|
2114
|
+
{ prefix: "Transfer" }
|
|
2115
|
+
);
|
|
2116
|
+
|
|
2117
|
+
let totalTransferred = 0;
|
|
2118
|
+
|
|
2119
|
+
// Transfer memberships with rate limiting
|
|
2120
|
+
const transferTasks = memberships.map((membership) =>
|
|
2121
|
+
this.userLimit(async () => {
|
|
2122
|
+
// Use userLimit for team operations (more sensitive)
|
|
2123
|
+
try {
|
|
2124
|
+
// Check if membership already exists and compare roles
|
|
2125
|
+
let existingMembership: Models.Membership | null = null;
|
|
2126
|
+
try {
|
|
2127
|
+
existingMembership = await this.targetTeams.getMembership(
|
|
2128
|
+
teamId,
|
|
2129
|
+
membership.$id
|
|
2130
|
+
);
|
|
2131
|
+
|
|
2132
|
+
// Compare roles between source and target membership
|
|
2133
|
+
const sourceRoles = JSON.stringify(
|
|
2134
|
+
membership.roles?.sort() || []
|
|
2135
|
+
);
|
|
2136
|
+
const targetRoles = JSON.stringify(
|
|
2137
|
+
existingMembership.roles?.sort() || []
|
|
2138
|
+
);
|
|
2139
|
+
|
|
2140
|
+
if (sourceRoles !== targetRoles) {
|
|
2141
|
+
MessageFormatter.warning(
|
|
2142
|
+
`Membership ${membership.$id} exists but has different roles. Source: ${sourceRoles}, Target: ${targetRoles}`,
|
|
2143
|
+
{ prefix: "Transfer" }
|
|
2144
|
+
);
|
|
2145
|
+
|
|
2146
|
+
// Update membership roles to match source
|
|
2147
|
+
try {
|
|
2148
|
+
await this.targetTeams.updateMembership(
|
|
2149
|
+
teamId,
|
|
2150
|
+
membership.$id,
|
|
2151
|
+
membership.roles
|
|
2152
|
+
);
|
|
2153
|
+
MessageFormatter.success(
|
|
2154
|
+
`Updated membership ${membership.$id} roles to match source`,
|
|
2155
|
+
{ prefix: "Transfer" }
|
|
2156
|
+
);
|
|
2157
|
+
} catch (updateError) {
|
|
2158
|
+
MessageFormatter.error(
|
|
2159
|
+
`Failed to update roles for membership ${membership.$id}`,
|
|
2160
|
+
updateError instanceof Error
|
|
2161
|
+
? updateError
|
|
2162
|
+
: new Error(String(updateError)),
|
|
2163
|
+
{ prefix: "Transfer" }
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
} else {
|
|
2167
|
+
MessageFormatter.info(
|
|
2168
|
+
`Membership ${membership.$id} already exists with matching roles, skipping`,
|
|
2169
|
+
{ prefix: "Transfer" }
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
2172
|
+
return;
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
// Membership doesn't exist, proceed with creation
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// Get user data from target (users should already be transferred)
|
|
2178
|
+
let userData: Models.User<Record<string, any>> | null = null;
|
|
2179
|
+
try {
|
|
2180
|
+
userData = await this.targetUsers.get(membership.userId);
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
MessageFormatter.warning(
|
|
2183
|
+
`User ${membership.userId} not found in target, membership ${membership.$id} may fail`,
|
|
2184
|
+
{ prefix: "Transfer" }
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// Create membership using the comprehensive user data
|
|
2189
|
+
await tryAwaitWithRetry(async () =>
|
|
2190
|
+
this.targetTeams.createMembership(
|
|
2191
|
+
teamId,
|
|
2192
|
+
membership.roles,
|
|
2193
|
+
userData?.email || membership.userEmail, // Use target user email if available, fallback to membership email
|
|
2194
|
+
membership.userId, // User ID
|
|
2195
|
+
userData?.phone || undefined, // Use target user phone if available
|
|
2196
|
+
undefined, // Invitation URL placeholder
|
|
2197
|
+
userData?.name || membership.userName // Use target user name if available, fallback to membership name
|
|
2198
|
+
)
|
|
2199
|
+
);
|
|
2200
|
+
|
|
2201
|
+
totalTransferred++;
|
|
2202
|
+
MessageFormatter.success(
|
|
2203
|
+
`Transferred membership ${membership.$id} for user ${
|
|
2204
|
+
userData?.name || membership.userName
|
|
2205
|
+
}`,
|
|
2206
|
+
{ prefix: "Transfer" }
|
|
2207
|
+
);
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
MessageFormatter.error(
|
|
2210
|
+
`Failed to transfer membership ${membership.$id}`,
|
|
2211
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
2212
|
+
{ prefix: "Transfer" }
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
})
|
|
2216
|
+
);
|
|
2217
|
+
|
|
2218
|
+
await Promise.all(transferTasks);
|
|
2219
|
+
MessageFormatter.info(
|
|
2220
|
+
`Transferred ${totalTransferred} memberships for team ${teamId}`,
|
|
2221
|
+
{ prefix: "Transfer" }
|
|
2222
|
+
);
|
|
2223
|
+
} catch (error) {
|
|
2224
|
+
MessageFormatter.error(
|
|
2225
|
+
`Failed to transfer memberships for team ${teamId}`,
|
|
2226
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
2227
|
+
{ prefix: "Transfer" }
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
private printSummary(): void {
|
|
2233
|
+
const duration = Math.round((Date.now() - this.startTime) / 1000);
|
|
2234
|
+
|
|
2235
|
+
MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", {
|
|
2236
|
+
prefix: "Transfer",
|
|
2237
|
+
});
|
|
2238
|
+
MessageFormatter.info(`Total Time: ${duration}s`, { prefix: "Transfer" });
|
|
2239
|
+
MessageFormatter.info(
|
|
2240
|
+
`Users: ${this.results.users.transferred} transferred, ${this.results.users.skipped} skipped, ${this.results.users.failed} failed`,
|
|
2241
|
+
{ prefix: "Transfer" }
|
|
2242
|
+
);
|
|
2243
|
+
MessageFormatter.info(
|
|
2244
|
+
`Teams: ${this.results.teams.transferred} transferred, ${this.results.teams.skipped} skipped, ${this.results.teams.failed} failed`,
|
|
2245
|
+
{ prefix: "Transfer" }
|
|
2246
|
+
);
|
|
2247
|
+
MessageFormatter.info(
|
|
2248
|
+
`Databases: ${this.results.databases.transferred} transferred, ${this.results.databases.skipped} skipped, ${this.results.databases.failed} failed`,
|
|
2249
|
+
{ prefix: "Transfer" }
|
|
2250
|
+
);
|
|
2251
|
+
MessageFormatter.info(
|
|
2252
|
+
`Buckets: ${this.results.buckets.transferred} transferred, ${this.results.buckets.skipped} skipped, ${this.results.buckets.failed} failed`,
|
|
2253
|
+
{ prefix: "Transfer" }
|
|
2254
|
+
);
|
|
2255
|
+
MessageFormatter.info(
|
|
2256
|
+
`Functions: ${this.results.functions.transferred} transferred, ${this.results.functions.skipped} skipped, ${this.results.functions.failed} failed`,
|
|
2257
|
+
{ prefix: "Transfer" }
|
|
2258
|
+
);
|
|
2259
|
+
|
|
2260
|
+
const totalTransferred =
|
|
2261
|
+
this.results.users.transferred +
|
|
2262
|
+
this.results.teams.transferred +
|
|
2263
|
+
this.results.databases.transferred +
|
|
2264
|
+
this.results.buckets.transferred +
|
|
2265
|
+
this.results.functions.transferred;
|
|
2266
|
+
const totalFailed =
|
|
2267
|
+
this.results.users.failed +
|
|
2268
|
+
this.results.teams.failed +
|
|
2269
|
+
this.results.databases.failed +
|
|
2270
|
+
this.results.buckets.failed +
|
|
2271
|
+
this.results.functions.failed;
|
|
2272
|
+
|
|
2273
|
+
if (totalFailed === 0) {
|
|
2274
|
+
MessageFormatter.success(
|
|
2275
|
+
`All ${totalTransferred} items transferred successfully!`,
|
|
2276
|
+
{ prefix: "Transfer" }
|
|
2277
|
+
);
|
|
2278
|
+
} else {
|
|
2279
|
+
MessageFormatter.warning(
|
|
2280
|
+
`${totalTransferred} items transferred, ${totalFailed} failed`,
|
|
2281
|
+
{ prefix: "Transfer" }
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|