appwrite-utils-cli 1.9.7 → 1.11.0

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.
Files changed (425) hide show
  1. package/CONFIG_TODO.md +1189 -1189
  2. package/README.md +1004 -1004
  3. package/SELECTION_DIALOGS.md +145 -145
  4. package/SERVICE_IMPLEMENTATION_REPORT.md +462 -462
  5. package/package.json +6 -3
  6. package/scripts/copy-templates.ts +23 -23
  7. package/src/adapters/index.ts +11 -37
  8. package/src/backups/operations/bucketBackup.ts +277 -277
  9. package/src/backups/operations/collectionBackup.ts +310 -310
  10. package/src/backups/operations/comprehensiveBackup.ts +342 -342
  11. package/src/backups/schemas/bucketManifest.ts +78 -78
  12. package/src/backups/schemas/comprehensiveManifest.ts +76 -76
  13. package/src/backups/tracking/centralizedTracking.ts +352 -352
  14. package/src/cli/commands/configCommands.ts +265 -201
  15. package/src/cli/commands/databaseCommands.ts +931 -879
  16. package/src/cli/commands/functionCommands.ts +333 -332
  17. package/src/cli/commands/importFileCommands.ts +815 -0
  18. package/src/cli/commands/schemaCommands.ts +141 -141
  19. package/src/cli/commands/storageCommands.ts +2 -3
  20. package/src/cli/commands/transferCommands.ts +454 -457
  21. package/src/collections/attributes.ts.backup +1555 -1555
  22. package/src/collections/{attributes.ts → columns.ts} +15 -10
  23. package/src/collections/indexes.ts +350 -352
  24. package/src/collections/methods.ts +714 -700
  25. package/src/collections/tableOperations.ts +29 -8
  26. package/src/collections/transferOperations.ts +376 -377
  27. package/src/collections/wipeOperations.ts +449 -346
  28. package/src/databases/methods.ts +49 -49
  29. package/src/databases/setup.ts +77 -77
  30. package/src/examples/yamlTerminologyExample.ts +346 -346
  31. package/src/functions/deployments.ts +221 -220
  32. package/src/functions/fnConfigDiscovery.ts +2 -2
  33. package/src/functions/methods.ts +284 -284
  34. package/src/functions/templates/count-docs-in-collection/README.md +53 -53
  35. package/src/functions/templates/count-docs-in-collection/src/main.ts +159 -159
  36. package/src/functions/templates/count-docs-in-collection/src/request.ts +8 -8
  37. package/src/functions/templates/hono-typescript/README.md +285 -285
  38. package/src/functions/templates/hono-typescript/src/adapters/request.ts +73 -73
  39. package/src/functions/templates/hono-typescript/src/adapters/response.ts +105 -105
  40. package/src/functions/templates/hono-typescript/src/app.ts +179 -179
  41. package/src/functions/templates/hono-typescript/src/context.ts +102 -102
  42. package/src/functions/templates/hono-typescript/src/{index.ts → main.ts} +53 -53
  43. package/src/functions/templates/hono-typescript/src/middleware/appwrite.ts +118 -118
  44. package/src/functions/templates/typescript-node/README.md +31 -31
  45. package/src/functions/templates/typescript-node/src/context.ts +102 -102
  46. package/src/functions/templates/typescript-node/src/{index.ts → main.ts} +29 -29
  47. package/src/functions/templates/uv/README.md +30 -30
  48. package/src/functions/templates/uv/pyproject.toml +29 -29
  49. package/src/functions/templates/uv/src/context.py +124 -124
  50. package/src/functions/templates/uv/src/{index.py → main.py} +45 -45
  51. package/src/init.ts +62 -62
  52. package/src/interactiveCLI.ts +1095 -1030
  53. package/src/main.ts +1517 -1670
  54. package/src/migrations/afterImportActions.ts +579 -580
  55. package/src/migrations/appwriteToX.ts +634 -630
  56. package/src/migrations/comprehensiveTransfer.ts +2149 -2149
  57. package/src/migrations/dataLoader.ts +1729 -1702
  58. package/src/migrations/importController.ts +440 -428
  59. package/src/migrations/importDataActions.ts +315 -315
  60. package/src/migrations/relationships.ts +333 -334
  61. package/src/migrations/services/DataTransformationService.ts +195 -195
  62. package/src/migrations/services/FileHandlerService.ts +310 -310
  63. package/src/migrations/services/ImportOrchestrator.ts +674 -665
  64. package/src/migrations/services/RateLimitManager.ts +362 -362
  65. package/src/migrations/services/RelationshipResolver.ts +460 -460
  66. package/src/migrations/services/UserMappingService.ts +344 -344
  67. package/src/migrations/services/ValidationService.ts +333 -333
  68. package/src/migrations/transfer.ts +987 -942
  69. package/src/migrations/yaml/YamlImportConfigLoader.ts +438 -438
  70. package/src/migrations/yaml/YamlImportIntegration.ts +438 -438
  71. package/src/migrations/yaml/generateImportSchemas.ts +1347 -1347
  72. package/src/schemas/authUser.ts +23 -23
  73. package/src/setup.ts +8 -8
  74. package/src/setupCommands.ts +5 -6
  75. package/src/setupController.ts +42 -42
  76. package/src/shared/backupMetadataSchema.ts +93 -93
  77. package/src/shared/backupTracking.ts +211 -211
  78. package/src/shared/confirmationDialogs.ts +326 -326
  79. package/src/shared/migrationHelpers.ts +232 -232
  80. package/src/shared/operationLogger.ts +20 -20
  81. package/src/shared/operationQueue.ts +326 -327
  82. package/src/shared/operationsTable.ts +338 -338
  83. package/src/shared/operationsTableSchema.ts +60 -60
  84. package/src/shared/progressManager.ts +277 -277
  85. package/src/shared/relationshipExtractor.ts +214 -214
  86. package/src/shared/selectionDialogs.ts +775 -722
  87. package/src/storage/backupCompression.ts +88 -88
  88. package/src/storage/methods.ts +695 -682
  89. package/src/storage/schemas.ts +205 -205
  90. package/src/tables/indexManager.ts +408 -408
  91. package/src/types/node-appwrite-tablesdb.d.ts +43 -43
  92. package/src/types.ts +9 -9
  93. package/src/users/methods.ts +358 -359
  94. package/src/utils/configMigration.ts +347 -347
  95. package/src/utils/index.ts +2 -2
  96. package/src/utils/loadConfigs.ts +457 -449
  97. package/src/utils/setupFiles.ts +1236 -1238
  98. package/src/utilsController.ts +1263 -1213
  99. package/tests/README.md +496 -496
  100. package/tests/adapters/AdapterFactory.test.ts +276 -276
  101. package/tests/integration/syncOperations.test.ts +462 -462
  102. package/tests/jest.config.js +24 -24
  103. package/tests/migration/configMigration.test.ts +545 -545
  104. package/tests/setup.ts +61 -61
  105. package/tests/testUtils.ts +339 -339
  106. package/tests/utils/loadConfigs.test.ts +349 -349
  107. package/tests/validation/configValidation.test.ts +411 -411
  108. package/tsconfig.json +44 -44
  109. package/.appwrite/.yaml_schemas/appwrite-config.schema.json +0 -380
  110. package/.appwrite/.yaml_schemas/collection.schema.json +0 -255
  111. package/.appwrite/collections/Categories.yaml +0 -182
  112. package/.appwrite/collections/ExampleCollection.yaml +0 -36
  113. package/.appwrite/collections/Posts.yaml +0 -227
  114. package/.appwrite/collections/Users.yaml +0 -149
  115. package/.appwrite/config.yaml +0 -109
  116. package/.appwrite/import/README.md +0 -148
  117. package/.appwrite/import/categories-import.yaml +0 -129
  118. package/.appwrite/import/posts-import.yaml +0 -208
  119. package/.appwrite/import/users-import.yaml +0 -130
  120. package/.appwrite/importData/categories.json +0 -194
  121. package/.appwrite/importData/posts.json +0 -270
  122. package/.appwrite/importData/users.json +0 -220
  123. package/.appwrite/schemas/categories.json +0 -128
  124. package/.appwrite/schemas/exampleCollection.json +0 -52
  125. package/.appwrite/schemas/posts.json +0 -173
  126. package/.appwrite/schemas/users.json +0 -125
  127. package/dist/adapters/AdapterFactory.d.ts +0 -94
  128. package/dist/adapters/AdapterFactory.js +0 -420
  129. package/dist/adapters/DatabaseAdapter.d.ts +0 -243
  130. package/dist/adapters/DatabaseAdapter.js +0 -50
  131. package/dist/adapters/LegacyAdapter.d.ts +0 -50
  132. package/dist/adapters/LegacyAdapter.js +0 -615
  133. package/dist/adapters/TablesDBAdapter.d.ts +0 -45
  134. package/dist/adapters/TablesDBAdapter.js +0 -611
  135. package/dist/adapters/index.d.ts +0 -11
  136. package/dist/adapters/index.js +0 -12
  137. package/dist/backups/operations/bucketBackup.d.ts +0 -19
  138. package/dist/backups/operations/bucketBackup.js +0 -197
  139. package/dist/backups/operations/collectionBackup.d.ts +0 -30
  140. package/dist/backups/operations/collectionBackup.js +0 -201
  141. package/dist/backups/operations/comprehensiveBackup.d.ts +0 -25
  142. package/dist/backups/operations/comprehensiveBackup.js +0 -238
  143. package/dist/backups/schemas/bucketManifest.d.ts +0 -93
  144. package/dist/backups/schemas/bucketManifest.js +0 -33
  145. package/dist/backups/schemas/comprehensiveManifest.d.ts +0 -108
  146. package/dist/backups/schemas/comprehensiveManifest.js +0 -32
  147. package/dist/backups/tracking/centralizedTracking.d.ts +0 -34
  148. package/dist/backups/tracking/centralizedTracking.js +0 -274
  149. package/dist/cli/commands/configCommands.d.ts +0 -8
  150. package/dist/cli/commands/configCommands.js +0 -166
  151. package/dist/cli/commands/databaseCommands.d.ts +0 -14
  152. package/dist/cli/commands/databaseCommands.js +0 -644
  153. package/dist/cli/commands/functionCommands.d.ts +0 -7
  154. package/dist/cli/commands/functionCommands.js +0 -330
  155. package/dist/cli/commands/schemaCommands.d.ts +0 -7
  156. package/dist/cli/commands/schemaCommands.js +0 -169
  157. package/dist/cli/commands/storageCommands.d.ts +0 -5
  158. package/dist/cli/commands/storageCommands.js +0 -143
  159. package/dist/cli/commands/transferCommands.d.ts +0 -5
  160. package/dist/cli/commands/transferCommands.js +0 -384
  161. package/dist/collections/attributes.d.ts +0 -13
  162. package/dist/collections/attributes.js +0 -1333
  163. package/dist/collections/indexes.d.ts +0 -12
  164. package/dist/collections/indexes.js +0 -217
  165. package/dist/collections/methods.d.ts +0 -19
  166. package/dist/collections/methods.js +0 -587
  167. package/dist/collections/tableOperations.d.ts +0 -86
  168. package/dist/collections/tableOperations.js +0 -447
  169. package/dist/collections/transferOperations.d.ts +0 -8
  170. package/dist/collections/transferOperations.js +0 -412
  171. package/dist/collections/wipeOperations.d.ts +0 -16
  172. package/dist/collections/wipeOperations.js +0 -233
  173. package/dist/config/ConfigManager.d.ts +0 -450
  174. package/dist/config/ConfigManager.js +0 -650
  175. package/dist/config/configMigration.d.ts +0 -87
  176. package/dist/config/configMigration.js +0 -390
  177. package/dist/config/configValidation.d.ts +0 -66
  178. package/dist/config/configValidation.js +0 -358
  179. package/dist/config/index.d.ts +0 -8
  180. package/dist/config/index.js +0 -7
  181. package/dist/config/services/ConfigDiscoveryService.d.ts +0 -122
  182. package/dist/config/services/ConfigDiscoveryService.js +0 -322
  183. package/dist/config/services/ConfigLoaderService.d.ts +0 -129
  184. package/dist/config/services/ConfigLoaderService.js +0 -535
  185. package/dist/config/services/ConfigMergeService.d.ts +0 -208
  186. package/dist/config/services/ConfigMergeService.js +0 -308
  187. package/dist/config/services/ConfigValidationService.d.ts +0 -214
  188. package/dist/config/services/ConfigValidationService.js +0 -310
  189. package/dist/config/services/SessionAuthService.d.ts +0 -225
  190. package/dist/config/services/SessionAuthService.js +0 -456
  191. package/dist/config/services/__tests__/ConfigMergeService.test.d.ts +0 -1
  192. package/dist/config/services/__tests__/ConfigMergeService.test.js +0 -271
  193. package/dist/config/services/index.d.ts +0 -13
  194. package/dist/config/services/index.js +0 -10
  195. package/dist/config/yamlConfig.d.ts +0 -722
  196. package/dist/config/yamlConfig.js +0 -702
  197. package/dist/databases/methods.d.ts +0 -6
  198. package/dist/databases/methods.js +0 -35
  199. package/dist/databases/setup.d.ts +0 -5
  200. package/dist/databases/setup.js +0 -45
  201. package/dist/examples/yamlTerminologyExample.d.ts +0 -42
  202. package/dist/examples/yamlTerminologyExample.js +0 -272
  203. package/dist/functions/deployments.d.ts +0 -4
  204. package/dist/functions/deployments.js +0 -146
  205. package/dist/functions/fnConfigDiscovery.d.ts +0 -3
  206. package/dist/functions/fnConfigDiscovery.js +0 -108
  207. package/dist/functions/methods.d.ts +0 -16
  208. package/dist/functions/methods.js +0 -174
  209. package/dist/functions/pathResolution.d.ts +0 -37
  210. package/dist/functions/pathResolution.js +0 -185
  211. package/dist/functions/templates/count-docs-in-collection/README.md +0 -54
  212. package/dist/functions/templates/count-docs-in-collection/package.json +0 -25
  213. package/dist/functions/templates/count-docs-in-collection/src/main.ts +0 -159
  214. package/dist/functions/templates/count-docs-in-collection/src/request.ts +0 -9
  215. package/dist/functions/templates/count-docs-in-collection/tsconfig.json +0 -28
  216. package/dist/functions/templates/hono-typescript/README.md +0 -286
  217. package/dist/functions/templates/hono-typescript/package.json +0 -26
  218. package/dist/functions/templates/hono-typescript/src/adapters/request.ts +0 -74
  219. package/dist/functions/templates/hono-typescript/src/adapters/response.ts +0 -106
  220. package/dist/functions/templates/hono-typescript/src/app.ts +0 -180
  221. package/dist/functions/templates/hono-typescript/src/context.ts +0 -103
  222. package/dist/functions/templates/hono-typescript/src/index.ts +0 -54
  223. package/dist/functions/templates/hono-typescript/src/middleware/appwrite.ts +0 -119
  224. package/dist/functions/templates/hono-typescript/tsconfig.json +0 -20
  225. package/dist/functions/templates/typescript-node/README.md +0 -32
  226. package/dist/functions/templates/typescript-node/package.json +0 -25
  227. package/dist/functions/templates/typescript-node/src/context.ts +0 -103
  228. package/dist/functions/templates/typescript-node/src/index.ts +0 -29
  229. package/dist/functions/templates/typescript-node/tsconfig.json +0 -28
  230. package/dist/functions/templates/uv/README.md +0 -31
  231. package/dist/functions/templates/uv/pyproject.toml +0 -30
  232. package/dist/functions/templates/uv/src/__init__.py +0 -0
  233. package/dist/functions/templates/uv/src/context.py +0 -125
  234. package/dist/functions/templates/uv/src/index.py +0 -46
  235. package/dist/init.d.ts +0 -2
  236. package/dist/init.js +0 -57
  237. package/dist/interactiveCLI.d.ts +0 -31
  238. package/dist/interactiveCLI.js +0 -898
  239. package/dist/main.d.ts +0 -2
  240. package/dist/main.js +0 -1180
  241. package/dist/migrations/afterImportActions.d.ts +0 -17
  242. package/dist/migrations/afterImportActions.js +0 -306
  243. package/dist/migrations/appwriteToX.d.ts +0 -211
  244. package/dist/migrations/appwriteToX.js +0 -491
  245. package/dist/migrations/comprehensiveTransfer.d.ts +0 -147
  246. package/dist/migrations/comprehensiveTransfer.js +0 -1317
  247. package/dist/migrations/dataLoader.d.ts +0 -753
  248. package/dist/migrations/dataLoader.js +0 -1250
  249. package/dist/migrations/importController.d.ts +0 -23
  250. package/dist/migrations/importController.js +0 -268
  251. package/dist/migrations/importDataActions.d.ts +0 -50
  252. package/dist/migrations/importDataActions.js +0 -230
  253. package/dist/migrations/relationships.d.ts +0 -29
  254. package/dist/migrations/relationships.js +0 -204
  255. package/dist/migrations/services/DataTransformationService.d.ts +0 -55
  256. package/dist/migrations/services/DataTransformationService.js +0 -158
  257. package/dist/migrations/services/FileHandlerService.d.ts +0 -75
  258. package/dist/migrations/services/FileHandlerService.js +0 -236
  259. package/dist/migrations/services/ImportOrchestrator.d.ts +0 -97
  260. package/dist/migrations/services/ImportOrchestrator.js +0 -485
  261. package/dist/migrations/services/RateLimitManager.d.ts +0 -138
  262. package/dist/migrations/services/RateLimitManager.js +0 -279
  263. package/dist/migrations/services/RelationshipResolver.d.ts +0 -120
  264. package/dist/migrations/services/RelationshipResolver.js +0 -332
  265. package/dist/migrations/services/UserMappingService.d.ts +0 -109
  266. package/dist/migrations/services/UserMappingService.js +0 -277
  267. package/dist/migrations/services/ValidationService.d.ts +0 -74
  268. package/dist/migrations/services/ValidationService.js +0 -260
  269. package/dist/migrations/transfer.d.ts +0 -26
  270. package/dist/migrations/transfer.js +0 -608
  271. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +0 -131
  272. package/dist/migrations/yaml/YamlImportConfigLoader.js +0 -383
  273. package/dist/migrations/yaml/YamlImportIntegration.d.ts +0 -93
  274. package/dist/migrations/yaml/YamlImportIntegration.js +0 -341
  275. package/dist/migrations/yaml/generateImportSchemas.d.ts +0 -30
  276. package/dist/migrations/yaml/generateImportSchemas.js +0 -1327
  277. package/dist/schemas/authUser.d.ts +0 -24
  278. package/dist/schemas/authUser.js +0 -17
  279. package/dist/setup.d.ts +0 -2
  280. package/dist/setup.js +0 -5
  281. package/dist/setupCommands.d.ts +0 -58
  282. package/dist/setupCommands.js +0 -490
  283. package/dist/setupController.d.ts +0 -9
  284. package/dist/setupController.js +0 -34
  285. package/dist/shared/attributeMapper.d.ts +0 -20
  286. package/dist/shared/attributeMapper.js +0 -203
  287. package/dist/shared/backupMetadataSchema.d.ts +0 -94
  288. package/dist/shared/backupMetadataSchema.js +0 -38
  289. package/dist/shared/backupTracking.d.ts +0 -18
  290. package/dist/shared/backupTracking.js +0 -176
  291. package/dist/shared/confirmationDialogs.d.ts +0 -75
  292. package/dist/shared/confirmationDialogs.js +0 -236
  293. package/dist/shared/errorUtils.d.ts +0 -54
  294. package/dist/shared/errorUtils.js +0 -95
  295. package/dist/shared/functionManager.d.ts +0 -48
  296. package/dist/shared/functionManager.js +0 -348
  297. package/dist/shared/jsonSchemaGenerator.d.ts +0 -50
  298. package/dist/shared/jsonSchemaGenerator.js +0 -290
  299. package/dist/shared/logging.d.ts +0 -61
  300. package/dist/shared/logging.js +0 -116
  301. package/dist/shared/messageFormatter.d.ts +0 -39
  302. package/dist/shared/messageFormatter.js +0 -162
  303. package/dist/shared/migrationHelpers.d.ts +0 -61
  304. package/dist/shared/migrationHelpers.js +0 -145
  305. package/dist/shared/operationLogger.d.ts +0 -10
  306. package/dist/shared/operationLogger.js +0 -12
  307. package/dist/shared/operationQueue.d.ts +0 -40
  308. package/dist/shared/operationQueue.js +0 -311
  309. package/dist/shared/operationsTable.d.ts +0 -26
  310. package/dist/shared/operationsTable.js +0 -286
  311. package/dist/shared/operationsTableSchema.d.ts +0 -48
  312. package/dist/shared/operationsTableSchema.js +0 -35
  313. package/dist/shared/progressManager.d.ts +0 -62
  314. package/dist/shared/progressManager.js +0 -215
  315. package/dist/shared/pydanticModelGenerator.d.ts +0 -17
  316. package/dist/shared/pydanticModelGenerator.js +0 -615
  317. package/dist/shared/relationshipExtractor.d.ts +0 -56
  318. package/dist/shared/relationshipExtractor.js +0 -138
  319. package/dist/shared/schemaGenerator.d.ts +0 -40
  320. package/dist/shared/schemaGenerator.js +0 -556
  321. package/dist/shared/selectionDialogs.d.ts +0 -214
  322. package/dist/shared/selectionDialogs.js +0 -544
  323. package/dist/storage/backupCompression.d.ts +0 -20
  324. package/dist/storage/backupCompression.js +0 -67
  325. package/dist/storage/methods.d.ts +0 -32
  326. package/dist/storage/methods.js +0 -472
  327. package/dist/storage/schemas.d.ts +0 -842
  328. package/dist/storage/schemas.js +0 -175
  329. package/dist/tables/indexManager.d.ts +0 -65
  330. package/dist/tables/indexManager.js +0 -294
  331. package/dist/types.d.ts +0 -4
  332. package/dist/types.js +0 -3
  333. package/dist/users/methods.d.ts +0 -16
  334. package/dist/users/methods.js +0 -277
  335. package/dist/utils/ClientFactory.d.ts +0 -87
  336. package/dist/utils/ClientFactory.js +0 -212
  337. package/dist/utils/configDiscovery.d.ts +0 -78
  338. package/dist/utils/configDiscovery.js +0 -472
  339. package/dist/utils/configMigration.d.ts +0 -1
  340. package/dist/utils/configMigration.js +0 -261
  341. package/dist/utils/constantsGenerator.d.ts +0 -31
  342. package/dist/utils/constantsGenerator.js +0 -321
  343. package/dist/utils/dataConverters.d.ts +0 -46
  344. package/dist/utils/dataConverters.js +0 -139
  345. package/dist/utils/directoryUtils.d.ts +0 -22
  346. package/dist/utils/directoryUtils.js +0 -59
  347. package/dist/utils/getClientFromConfig.d.ts +0 -39
  348. package/dist/utils/getClientFromConfig.js +0 -199
  349. package/dist/utils/helperFunctions.d.ts +0 -63
  350. package/dist/utils/helperFunctions.js +0 -156
  351. package/dist/utils/index.d.ts +0 -2
  352. package/dist/utils/index.js +0 -2
  353. package/dist/utils/loadConfigs.d.ts +0 -50
  354. package/dist/utils/loadConfigs.js +0 -358
  355. package/dist/utils/pathResolvers.d.ts +0 -53
  356. package/dist/utils/pathResolvers.js +0 -72
  357. package/dist/utils/projectConfig.d.ts +0 -122
  358. package/dist/utils/projectConfig.js +0 -206
  359. package/dist/utils/retryFailedPromises.d.ts +0 -2
  360. package/dist/utils/retryFailedPromises.js +0 -23
  361. package/dist/utils/sessionAuth.d.ts +0 -48
  362. package/dist/utils/sessionAuth.js +0 -164
  363. package/dist/utils/setupFiles.d.ts +0 -4
  364. package/dist/utils/setupFiles.js +0 -1192
  365. package/dist/utils/typeGuards.d.ts +0 -35
  366. package/dist/utils/typeGuards.js +0 -57
  367. package/dist/utils/validationRules.d.ts +0 -43
  368. package/dist/utils/validationRules.js +0 -42
  369. package/dist/utils/versionDetection.d.ts +0 -58
  370. package/dist/utils/versionDetection.js +0 -251
  371. package/dist/utils/yamlConverter.d.ts +0 -100
  372. package/dist/utils/yamlConverter.js +0 -428
  373. package/dist/utils/yamlLoader.d.ts +0 -70
  374. package/dist/utils/yamlLoader.js +0 -267
  375. package/dist/utilsController.d.ts +0 -107
  376. package/dist/utilsController.js +0 -873
  377. package/src/adapters/AdapterFactory.ts +0 -529
  378. package/src/adapters/DatabaseAdapter.ts +0 -319
  379. package/src/adapters/LegacyAdapter.ts +0 -844
  380. package/src/adapters/TablesDBAdapter.ts +0 -823
  381. package/src/config/ConfigManager.ts +0 -849
  382. package/src/config/README.md +0 -274
  383. package/src/config/configMigration.ts +0 -575
  384. package/src/config/configValidation.ts +0 -445
  385. package/src/config/index.ts +0 -10
  386. package/src/config/services/ConfigDiscoveryService.ts +0 -410
  387. package/src/config/services/ConfigLoaderService.ts +0 -732
  388. package/src/config/services/ConfigMergeService.ts +0 -388
  389. package/src/config/services/ConfigValidationService.ts +0 -394
  390. package/src/config/services/SessionAuthService.ts +0 -565
  391. package/src/config/services/__tests__/ConfigMergeService.test.ts +0 -351
  392. package/src/config/services/index.ts +0 -29
  393. package/src/config/yamlConfig.ts +0 -761
  394. package/src/functions/pathResolution.ts +0 -227
  395. package/src/functions/templates/count-docs-in-collection/package.json +0 -25
  396. package/src/functions/templates/count-docs-in-collection/tsconfig.json +0 -28
  397. package/src/functions/templates/hono-typescript/package.json +0 -26
  398. package/src/functions/templates/hono-typescript/tsconfig.json +0 -20
  399. package/src/functions/templates/typescript-node/package.json +0 -25
  400. package/src/functions/templates/typescript-node/tsconfig.json +0 -28
  401. package/src/shared/attributeMapper.ts +0 -229
  402. package/src/shared/errorUtils.ts +0 -110
  403. package/src/shared/functionManager.ts +0 -537
  404. package/src/shared/jsonSchemaGenerator.ts +0 -383
  405. package/src/shared/logging.ts +0 -149
  406. package/src/shared/messageFormatter.ts +0 -208
  407. package/src/shared/pydanticModelGenerator.ts +0 -618
  408. package/src/shared/schemaGenerator.ts +0 -644
  409. package/src/utils/ClientFactory.ts +0 -240
  410. package/src/utils/configDiscovery.ts +0 -557
  411. package/src/utils/constantsGenerator.ts +0 -369
  412. package/src/utils/dataConverters.ts +0 -159
  413. package/src/utils/directoryUtils.ts +0 -61
  414. package/src/utils/getClientFromConfig.ts +0 -257
  415. package/src/utils/helperFunctions.ts +0 -228
  416. package/src/utils/pathResolvers.ts +0 -81
  417. package/src/utils/projectConfig.ts +0 -340
  418. package/src/utils/retryFailedPromises.ts +0 -29
  419. package/src/utils/sessionAuth.ts +0 -230
  420. package/src/utils/typeGuards.ts +0 -65
  421. package/src/utils/validationRules.ts +0 -88
  422. package/src/utils/versionDetection.ts +0 -292
  423. package/src/utils/yamlConverter.ts +0 -542
  424. package/src/utils/yamlLoader.ts +0 -371
  425. package/tmp-sync-test/.appwrite/collections/TestCollection.yaml +0 -7
@@ -1,136 +1,134 @@
1
- import {
2
- converterFunctions,
3
- tryAwaitWithRetry,
4
- parseAttribute,
5
- objectNeedsUpdate,
6
- } from "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";
1
+ import {
2
+ converterFunctions,
3
+ tryAwaitWithRetry,
4
+ parseAttribute,
5
+ objectNeedsUpdate,
6
+ } from "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, getClient } from "appwrite-utils-helpers";
20
20
  import { processQueue, queuedOperations } from "../shared/operationQueue.js";
21
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";
22
+ import {
23
+ transferDatabaseLocalToLocal,
24
+ transferDatabaseLocalToRemote,
25
+ transferStorageLocalToLocal,
26
+ transferStorageLocalToRemote,
27
+ transferUsersLocalToRemote,
28
+ } from "./transfer.js";
29
+ import { deployLocalFunction } from "../functions/deployments.js";
30
+ import {
31
+ listFunctions,
32
+ downloadLatestFunctionDeployment,
33
+ } from "../functions/methods.js";
34
+ import pLimit from "p-limit";
35
+ import chalk from "chalk";
36
+ import { join } from "node:path";
38
37
  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>;
38
+ import type { DatabaseAdapter } from "appwrite-utils-helpers";
39
+ import { getAdapter, mapToCreateAttributeParams } from "appwrite-utils-helpers";
40
+
41
+ export interface ComprehensiveTransferOptions {
42
+ sourceEndpoint: string;
43
+ sourceProject: string;
44
+ sourceKey: string;
45
+ targetEndpoint: string;
46
+ targetProject: string;
47
+ targetKey: string;
48
+ transferUsers?: boolean;
49
+ transferTeams?: boolean;
50
+ transferDatabases?: boolean;
51
+ transferBuckets?: boolean;
52
+ transferFunctions?: boolean;
53
+ concurrencyLimit?: number; // 5-100 in steps of 5
54
+ dryRun?: boolean;
55
+ }
56
+
57
+ export interface TransferResults {
58
+ users: { transferred: number; skipped: number; failed: number };
59
+ teams: { transferred: number; skipped: number; failed: number };
60
+ databases: { transferred: number; skipped: number; failed: number };
61
+ buckets: { transferred: number; skipped: number; failed: number };
62
+ functions: { transferred: number; skipped: number; failed: number };
63
+ totalTime: number;
64
+ }
65
+
66
+ export class ComprehensiveTransfer {
67
+ private sourceClient: Client;
68
+ private targetClient: Client;
69
+ private sourceUsers: Users;
70
+ private targetUsers: Users;
71
+ private sourceTeams: Teams;
72
+ private targetTeams: Teams;
73
+ private sourceDatabases: Databases;
74
+ private targetDatabases: Databases;
75
+ private sourceStorage: Storage;
76
+ private targetStorage: Storage;
77
+ private sourceFunctions: Functions;
78
+ private targetFunctions: Functions;
79
+ private limit: ReturnType<typeof pLimit>;
80
+ private userLimit: ReturnType<typeof pLimit>;
81
+ private fileLimit: ReturnType<typeof pLimit>;
84
82
  private results: TransferResults;
85
83
  private startTime: number;
86
84
  private tempDir: string;
87
85
  private cachedMaxFileSize?: number; // Cache successful maximumFileSize for subsequent buckets
88
86
  private sourceAdapter?: DatabaseAdapter;
89
87
  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
-
88
+
89
+ constructor(private options: ComprehensiveTransferOptions) {
90
+ this.sourceClient = getClient(
91
+ options.sourceEndpoint,
92
+ options.sourceProject,
93
+ options.sourceKey
94
+ );
95
+ this.targetClient = getClient(
96
+ options.targetEndpoint,
97
+ options.targetProject,
98
+ options.targetKey
99
+ );
100
+
101
+ this.sourceUsers = new Users(this.sourceClient);
102
+ this.targetUsers = new Users(this.targetClient);
103
+ this.sourceTeams = new Teams(this.sourceClient);
104
+ this.targetTeams = new Teams(this.targetClient);
105
+ this.sourceDatabases = new Databases(this.sourceClient);
106
+ this.targetDatabases = new Databases(this.targetClient);
107
+ this.sourceStorage = new Storage(this.sourceClient);
108
+ this.targetStorage = new Storage(this.targetClient);
109
+ this.sourceFunctions = new Functions(this.sourceClient);
110
+ this.targetFunctions = new Functions(this.targetClient);
111
+
112
+ const baseLimit = options.concurrencyLimit || 10;
113
+ this.limit = pLimit(baseLimit);
114
+
115
+ // Different rate limits for different operations to prevent API throttling
116
+ // Users: Half speed (more sensitive operations)
117
+ // Files: Quarter speed (most bandwidth intensive)
118
+ this.userLimit = pLimit(Math.max(1, Math.floor(baseLimit / 2)));
119
+ this.fileLimit = pLimit(Math.max(1, Math.floor(baseLimit / 4)));
120
+ this.results = {
121
+ users: { transferred: 0, skipped: 0, failed: 0 },
122
+ teams: { transferred: 0, skipped: 0, failed: 0 },
123
+ databases: { transferred: 0, skipped: 0, failed: 0 },
124
+ buckets: { transferred: 0, skipped: 0, failed: 0 },
125
+ functions: { transferred: 0, skipped: 0, failed: 0 },
126
+ totalTime: 0,
127
+ };
128
+ this.startTime = Date.now();
129
+ this.tempDir = join(process.cwd(), ".appwrite-transfer-temp");
130
+ }
131
+
134
132
  async execute(): Promise<TransferResults> {
135
133
  try {
136
134
  MessageFormatter.info("Starting comprehensive transfer", {
@@ -152,1280 +150,1282 @@ export class ComprehensiveTransfer {
152
150
  );
153
151
  this.sourceAdapter = source.adapter;
154
152
  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
- }
153
+
154
+ if (this.options.dryRun) {
155
+ MessageFormatter.info("DRY RUN MODE - No actual changes will be made", {
156
+ prefix: "Transfer",
157
+ });
158
+ }
159
+
160
+ // Show rate limiting configuration
161
+ const baseLimit = this.options.concurrencyLimit || 10;
162
+ const userLimit = Math.max(1, Math.floor(baseLimit / 2));
163
+ const fileLimit = Math.max(1, Math.floor(baseLimit / 4));
164
+
165
+ MessageFormatter.info(
166
+ `Rate limits: General=${baseLimit}, Users=${userLimit}, Files=${fileLimit}`,
167
+ { prefix: "Transfer" }
168
+ );
169
+
170
+ // Ensure temp directory exists
171
+ if (!fs.existsSync(this.tempDir)) {
172
+ fs.mkdirSync(this.tempDir, { recursive: true });
173
+ }
174
+
175
+ // Execute transfers in the correct order
176
+ if (this.options.transferUsers !== false) {
177
+ await this.transferAllUsers();
178
+ }
179
+
180
+ if (this.options.transferTeams !== false) {
181
+ await this.transferAllTeams();
182
+ }
183
+
184
+ if (this.options.transferDatabases !== false) {
185
+ await this.transferAllDatabases();
186
+ }
187
+
188
+ if (this.options.transferBuckets !== false) {
189
+ await this.transferAllBuckets();
190
+ }
191
+
192
+ if (this.options.transferFunctions !== false) {
193
+ await this.transferAllFunctions();
194
+ }
195
+
196
+ this.results.totalTime = Date.now() - this.startTime;
197
+ this.printSummary();
198
+
199
+ return this.results;
200
+ } catch (error) {
201
+ MessageFormatter.error(
202
+ "Comprehensive transfer failed",
203
+ error instanceof Error ? error : new Error(String(error)),
204
+ { prefix: "Transfer" }
205
+ );
206
+ throw error;
207
+ } finally {
208
+ // Clean up temp directory
209
+ if (fs.existsSync(this.tempDir)) {
210
+ fs.rmSync(this.tempDir, { recursive: true, force: true });
211
+ }
212
+ }
213
+ }
214
+
215
+ private async transferAllUsers(): Promise<void> {
216
+ MessageFormatter.info("Starting user transfer phase", {
217
+ prefix: "Transfer",
218
+ });
219
+
220
+ if (this.options.dryRun) {
221
+ const usersList = await this.sourceUsers.list([Query.limit(1)]);
222
+ MessageFormatter.info(
223
+ `DRY RUN: Would transfer ${usersList.total} users`,
224
+ { prefix: "Transfer" }
225
+ );
226
+ return;
227
+ }
228
+
229
+ try {
230
+ // Use the existing user transfer function
231
+ // Note: The rate limiting is handled at the API level, not per-user
232
+ // since user operations are already sequential in the existing implementation
233
+ await transferUsersLocalToRemote(
234
+ this.sourceUsers,
235
+ this.options.targetEndpoint,
236
+ this.options.targetProject,
237
+ this.options.targetKey
238
+ );
239
+
240
+ // Get actual count for results
241
+ const usersList = await this.sourceUsers.list([Query.limit(1)]);
242
+ this.results.users.transferred = usersList.total;
243
+
244
+ MessageFormatter.success(`User transfer completed`, {
245
+ prefix: "Transfer",
246
+ });
247
+ } catch (error) {
248
+ MessageFormatter.error(
249
+ "User transfer failed",
250
+ error instanceof Error ? error : new Error(String(error)),
251
+ { prefix: "Transfer" }
252
+ );
253
+ this.results.users.failed = 1;
254
+ }
255
+ }
256
+
257
+ private async transferAllTeams(): Promise<void> {
258
+ MessageFormatter.info("Starting team transfer phase", {
259
+ prefix: "Transfer",
260
+ });
261
+
262
+ try {
263
+ // Fetch all teams from source with pagination
264
+ const allSourceTeams = await this.fetchAllTeams(this.sourceTeams);
265
+ const allTargetTeams = await this.fetchAllTeams(this.targetTeams);
266
+
267
+ if (this.options.dryRun) {
268
+ let totalMemberships = 0;
269
+ for (const team of allSourceTeams) {
270
+ const memberships = await this.sourceTeams.listMemberships(team.$id, [
271
+ Query.limit(1),
272
+ ]);
273
+ totalMemberships += memberships.total;
274
+ }
275
+ MessageFormatter.info(
276
+ `DRY RUN: Would transfer ${allSourceTeams.length} teams with ${totalMemberships} memberships`,
277
+ { prefix: "Transfer" }
278
+ );
279
+ return;
280
+ }
281
+
282
+ const transferTasks = allSourceTeams.map((team) =>
283
+ this.limit(async () => {
284
+ try {
285
+ // Check if team exists in target
286
+ const existingTeam = allTargetTeams.find(
287
+ (tt) => tt.$id === team.$id
288
+ );
289
+
290
+ if (!existingTeam) {
291
+ // Fetch all memberships to extract unique roles before creating team
292
+ MessageFormatter.info(
293
+ `Fetching memberships for team ${team.name} to extract roles`,
294
+ { prefix: "Transfer" }
295
+ );
296
+ const memberships = await this.fetchAllMemberships(team.$id);
297
+
298
+ // Extract unique roles from all memberships
299
+ const allRoles = new Set<string>();
300
+ memberships.forEach((membership) => {
301
+ membership.roles.forEach((role) => allRoles.add(role));
302
+ });
303
+ const uniqueRoles = Array.from(allRoles);
304
+
305
+ MessageFormatter.info(
306
+ `Found ${uniqueRoles.length} unique roles for team ${
307
+ team.name
308
+ }: ${uniqueRoles.join(", ")}`,
309
+ { prefix: "Transfer" }
310
+ );
311
+
312
+ // Create team in target with the collected roles
313
+ await this.targetTeams.create(team.$id, team.name, uniqueRoles);
314
+ MessageFormatter.success(
315
+ `Created team: ${team.name} with roles: ${uniqueRoles.join(
316
+ ", "
317
+ )}`,
318
+ { prefix: "Transfer" }
319
+ );
320
+ } else {
321
+ MessageFormatter.info(
322
+ `Team ${team.name} already exists, updating if needed`,
323
+ { prefix: "Transfer" }
324
+ );
325
+
326
+ // Update team if needed
327
+ if (existingTeam.name !== team.name) {
328
+ await this.targetTeams.updateName(team.$id, team.name);
329
+ MessageFormatter.success(`Updated team name: ${team.name}`, {
330
+ prefix: "Transfer",
331
+ });
332
+ }
333
+ }
334
+
335
+ // Transfer team memberships
336
+ await this.transferTeamMemberships(team.$id);
337
+
338
+ this.results.teams.transferred++;
339
+ MessageFormatter.success(
340
+ `Team ${team.name} transferred successfully`,
341
+ { prefix: "Transfer" }
342
+ );
343
+ } catch (error) {
344
+ MessageFormatter.error(
345
+ `Team ${team.name} transfer failed`,
346
+ error instanceof Error ? error : new Error(String(error)),
347
+ { prefix: "Transfer" }
348
+ );
349
+ this.results.teams.failed++;
350
+ }
351
+ })
352
+ );
353
+
354
+ await Promise.all(transferTasks);
355
+ MessageFormatter.success("Team transfer phase completed", {
356
+ prefix: "Transfer",
357
+ });
358
+ } catch (error) {
359
+ MessageFormatter.error(
360
+ "Team transfer phase failed",
361
+ error instanceof Error ? error : new Error(String(error)),
362
+ { prefix: "Transfer" }
363
+ );
364
+ }
365
+ }
366
+
367
+ private async transferAllDatabases(): Promise<void> {
368
+ MessageFormatter.info("Starting database transfer phase", {
369
+ prefix: "Transfer",
370
+ });
371
+
372
+ try {
373
+ const sourceDatabases = await this.sourceDatabases.list();
374
+ const targetDatabases = await this.targetDatabases.list();
375
+
376
+ if (this.options.dryRun) {
377
+ MessageFormatter.info(
378
+ `DRY RUN: Would transfer ${sourceDatabases.databases.length} databases`,
379
+ { prefix: "Transfer" }
380
+ );
381
+ return;
382
+ }
383
+
384
+ // Phase 1: Create all databases and collections (structure only)
385
+ MessageFormatter.info(
386
+ "Phase 1: Creating database structures (databases, collections, attributes, indexes)",
387
+ { prefix: "Transfer" }
388
+ );
389
+
390
+ const structureCreationTasks = sourceDatabases.databases.map((db) =>
391
+ this.limit(async () => {
392
+ try {
393
+ // Check if database exists in target
394
+ const existingDb = targetDatabases.databases.find(
395
+ (tdb) => tdb.$id === db.$id
396
+ );
397
+
398
+ if (!existingDb) {
399
+ // Create database in target
400
+ await this.targetDatabases.create(db.$id, db.name, db.enabled);
401
+ MessageFormatter.success(`Created database: ${db.name}`, {
402
+ prefix: "Transfer",
403
+ });
404
+ }
405
+
406
+ // Create collections, attributes, and indexes WITHOUT transferring documents
407
+ await this.createDatabaseStructure(db.$id);
408
+
409
+ MessageFormatter.success(`Database structure created: ${db.name}`, {
410
+ prefix: "Transfer",
411
+ });
412
+ } catch (error) {
413
+ MessageFormatter.error(
414
+ `Database structure creation failed for ${db.name}`,
415
+ error instanceof Error ? error : new Error(String(error)),
416
+ { prefix: "Transfer" }
417
+ );
418
+ this.results.databases.failed++;
419
+ }
420
+ })
421
+ );
422
+
423
+ await Promise.all(structureCreationTasks);
424
+
425
+ // Phase 2: Transfer all documents after all structures are created
426
+ MessageFormatter.info(
427
+ "Phase 2: Transferring documents to all collections",
428
+ { prefix: "Transfer" }
429
+ );
430
+
431
+ const documentTransferTasks = sourceDatabases.databases.map((db) =>
432
+ this.limit(async () => {
433
+ try {
434
+ // Transfer documents for this database
435
+ await this.transferDatabaseDocuments(db.$id);
436
+
437
+ this.results.databases.transferred++;
438
+ MessageFormatter.success(
439
+ `Database documents transferred: ${db.name}`,
440
+ { prefix: "Transfer" }
441
+ );
442
+ } catch (error) {
443
+ MessageFormatter.error(
444
+ `Document transfer failed for ${db.name}`,
445
+ error instanceof Error ? error : new Error(String(error)),
446
+ { prefix: "Transfer" }
447
+ );
448
+ this.results.databases.failed++;
449
+ }
450
+ })
451
+ );
452
+
453
+ await Promise.all(documentTransferTasks);
454
+ MessageFormatter.success("Database transfer phase completed", {
455
+ prefix: "Transfer",
456
+ });
457
+ } catch (error) {
458
+ MessageFormatter.error(
459
+ "Database transfer phase failed",
460
+ error instanceof Error ? error : new Error(String(error)),
461
+ { prefix: "Transfer" }
462
+ );
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Phase 1: Create database structure (collections, attributes, indexes) without transferring documents
468
+ */
469
+ private async createDatabaseStructure(dbId: string): Promise<void> {
470
+ MessageFormatter.info(`Creating database structure for ${dbId}`, {
471
+ prefix: "Transfer",
472
+ });
473
+
474
+ try {
475
+ // Get all collections from source database
476
+ const sourceCollections = await this.fetchAllCollections(
477
+ dbId,
478
+ this.sourceDatabases
479
+ );
480
+ MessageFormatter.info(
481
+ `Found ${sourceCollections.length} collections in source database ${dbId}`,
482
+ { prefix: "Transfer" }
483
+ );
484
+
485
+ // Process each collection
486
+ for (const collection of sourceCollections) {
487
+ MessageFormatter.info(
488
+ `Processing collection: ${collection.name} (${collection.$id})`,
489
+ { prefix: "Transfer" }
490
+ );
491
+
492
+ try {
493
+ // Create or update collection in target
494
+ let targetCollection: Models.Collection;
495
+ const existingCollection = await tryAwaitWithRetry(async () =>
496
+ this.targetDatabases.listCollections(dbId, [
497
+ Query.equal("$id", collection.$id),
498
+ ])
499
+ );
500
+
501
+ if (existingCollection.collections.length > 0) {
502
+ targetCollection = existingCollection.collections[0];
503
+ MessageFormatter.info(
504
+ `Collection ${collection.name} exists in target database`,
505
+ { prefix: "Transfer" }
506
+ );
507
+
508
+ // Update collection if needed
509
+ if (
510
+ targetCollection.name !== collection.name ||
511
+ JSON.stringify(targetCollection.$permissions) !==
512
+ JSON.stringify(collection.$permissions) ||
513
+ targetCollection.documentSecurity !==
514
+ collection.documentSecurity ||
515
+ targetCollection.enabled !== collection.enabled
516
+ ) {
517
+ targetCollection = await tryAwaitWithRetry(async () =>
518
+ this.targetDatabases.updateCollection(
519
+ dbId,
520
+ collection.$id,
521
+ collection.name,
522
+ collection.$permissions,
523
+ collection.documentSecurity,
524
+ collection.enabled
525
+ )
526
+ );
527
+ MessageFormatter.success(
528
+ `Collection ${collection.name} updated`,
529
+ { prefix: "Transfer" }
530
+ );
531
+ }
532
+ } else {
533
+ MessageFormatter.info(
534
+ `Creating collection ${collection.name} in target database...`,
535
+ { prefix: "Transfer" }
536
+ );
537
+ targetCollection = await tryAwaitWithRetry(async () =>
538
+ this.targetDatabases.createCollection(
539
+ dbId,
540
+ collection.$id,
541
+ collection.name,
542
+ collection.$permissions,
543
+ collection.documentSecurity,
544
+ collection.enabled
545
+ )
546
+ );
547
+ MessageFormatter.success(`Collection ${collection.name} created`, {
548
+ prefix: "Transfer",
549
+ });
550
+ }
551
+
552
+ // Handle attributes with enhanced status checking
553
+ MessageFormatter.info(
554
+ `Creating attributes for collection ${collection.name} with enhanced monitoring...`,
555
+ { prefix: "Transfer" }
556
+ );
557
+
558
+ const attributesToCreate = collection.attributes.map((attr) =>
559
+ parseAttribute(attr as any)
560
+ );
561
+
562
+ const attributesSuccess =
563
+ await this.createCollectionAttributesWithStatusCheck(
564
+ this.targetDatabases,
565
+ dbId,
566
+ targetCollection,
567
+ attributesToCreate
568
+ );
569
+
570
+ if (!attributesSuccess) {
571
+ MessageFormatter.error(
572
+ `Failed to create some attributes for collection ${collection.name}`,
573
+ undefined,
574
+ { prefix: "Transfer" }
575
+ );
576
+ MessageFormatter.error(
577
+ `Skipping index creation and document transfer for collection ${collection.name} due to attribute failures`,
578
+ undefined,
579
+ { prefix: "Transfer" }
580
+ );
581
+ // Skip indexes and document transfer if attributes failed
582
+ continue;
583
+ } else {
584
+ MessageFormatter.success(
585
+ `All attributes created successfully for collection ${collection.name}`,
586
+ { prefix: "Transfer" }
587
+ );
588
+ }
589
+
590
+ // Handle indexes with enhanced status checking
591
+ MessageFormatter.info(
592
+ `Creating indexes for collection ${collection.name} with enhanced monitoring...`,
593
+ { prefix: "Transfer" }
594
+ );
595
+
596
+ let indexesSuccess = true;
597
+ // Check if indexes need to be created ahead of time
598
+ if (
599
+ collection.indexes.some(
600
+ (index) =>
601
+ !targetCollection.indexes.some(
602
+ (ti) =>
603
+ ti.key === index.key ||
604
+ ti.attributes.sort().join(",") ===
605
+ index.attributes.sort().join(",")
606
+ )
607
+ ) ||
608
+ collection.indexes.length !== targetCollection.indexes.length
609
+ ) {
610
+ indexesSuccess = await this.createCollectionIndexesWithStatusCheck(
611
+ dbId,
612
+ this.targetDatabases,
613
+ targetCollection.$id,
614
+ targetCollection,
615
+ collection.indexes as any
616
+ );
617
+ }
618
+
619
+ if (!indexesSuccess) {
620
+ MessageFormatter.error(
621
+ `Failed to create some indexes for collection ${collection.name}`,
622
+ undefined,
623
+ { prefix: "Transfer" }
624
+ );
625
+ MessageFormatter.warning(
626
+ `Proceeding with document transfer despite index failures for collection ${collection.name}`,
627
+ { prefix: "Transfer" }
628
+ );
629
+ } else {
630
+ MessageFormatter.success(
631
+ `All indexes created successfully for collection ${collection.name}`,
632
+ { prefix: "Transfer" }
633
+ );
634
+ }
635
+
636
+ MessageFormatter.success(
637
+ `Structure complete for collection ${collection.name}`,
638
+ { prefix: "Transfer" }
639
+ );
640
+ } catch (error) {
641
+ MessageFormatter.error(
642
+ `Error processing collection ${collection.name}`,
643
+ error instanceof Error ? error : new Error(String(error)),
644
+ { prefix: "Transfer" }
645
+ );
646
+ }
649
647
  }
650
648
  // After processing all collections' attributes and indexes, process any queued
651
649
  // relationship attributes so dependencies are resolved within this phase.
652
650
  if (queuedOperations.length > 0) {
653
651
  MessageFormatter.info(
654
- `Processing ${queuedOperations.length} queued relationship operations`,
652
+ `Processing ${queuedOperations.length} queued relationship operations`,
653
+ { prefix: "Transfer" }
654
+ );
655
+ await processQueue(this.targetDatabases, dbId);
656
+ } else {
657
+ MessageFormatter.info("No queued relationship operations to process", {
658
+ prefix: "Transfer",
659
+ });
660
+ }
661
+ } catch (error) {
662
+ MessageFormatter.error(
663
+ `Failed to create database structure for ${dbId}`,
664
+ error instanceof Error ? error : new Error(String(error)),
665
+ { prefix: "Transfer" }
666
+ );
667
+ throw error;
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Phase 2: Transfer documents to all collections in the database
673
+ */
674
+ private async transferDatabaseDocuments(dbId: string): Promise<void> {
675
+ MessageFormatter.info(`Transferring documents for database ${dbId}`, {
676
+ prefix: "Transfer",
677
+ });
678
+
679
+ try {
680
+ // Get all collections from source database
681
+ const sourceCollections = await this.fetchAllCollections(
682
+ dbId,
683
+ this.sourceDatabases
684
+ );
685
+ MessageFormatter.info(
686
+ `Transferring documents for ${sourceCollections.length} collections in database ${dbId}`,
687
+ { prefix: "Transfer" }
688
+ );
689
+
690
+ // Process each collection
691
+ for (const collection of sourceCollections) {
692
+ MessageFormatter.info(
693
+ `Transferring documents for collection: ${collection.name} (${collection.$id})`,
694
+ { prefix: "Transfer" }
695
+ );
696
+
697
+ try {
698
+ // Transfer documents
699
+ await this.transferDocumentsBetweenDatabases(
700
+ this.sourceDatabases,
701
+ this.targetDatabases,
702
+ dbId,
703
+ dbId,
704
+ collection.$id,
705
+ collection.$id
706
+ );
707
+
708
+ MessageFormatter.success(
709
+ `Documents transferred for collection ${collection.name}`,
710
+ { prefix: "Transfer" }
711
+ );
712
+ } catch (error) {
713
+ MessageFormatter.error(
714
+ `Error transferring documents for collection ${collection.name}`,
715
+ error instanceof Error ? error : new Error(String(error)),
716
+ { prefix: "Transfer" }
717
+ );
718
+ }
719
+ }
720
+ } catch (error) {
721
+ MessageFormatter.error(
722
+ `Failed to transfer documents for database ${dbId}`,
723
+ error instanceof Error ? error : new Error(String(error)),
724
+ { prefix: "Transfer" }
725
+ );
726
+ throw error;
727
+ }
728
+ }
729
+
730
+ private async transferAllBuckets(): Promise<void> {
731
+ MessageFormatter.info("Starting bucket transfer phase", {
732
+ prefix: "Transfer",
733
+ });
734
+
735
+ try {
736
+ // Get all buckets from source with pagination
737
+ const allSourceBuckets = await this.fetchAllBuckets(this.sourceStorage);
738
+ const allTargetBuckets = await this.fetchAllBuckets(this.targetStorage);
739
+
740
+ if (this.options.dryRun) {
741
+ let totalFiles = 0;
742
+ for (const bucket of allSourceBuckets) {
743
+ const files = await this.sourceStorage.listFiles(bucket.$id, [
744
+ Query.limit(1),
745
+ ]);
746
+ totalFiles += files.total;
747
+ }
748
+ MessageFormatter.info(
749
+ `DRY RUN: Would transfer ${allSourceBuckets.length} buckets with ${totalFiles} files`,
750
+ { prefix: "Transfer" }
751
+ );
752
+ return;
753
+ }
754
+
755
+ const transferTasks = allSourceBuckets.map((bucket) =>
756
+ this.limit(async () => {
757
+ try {
758
+ // Check if bucket exists in target
759
+ const existingBucket = allTargetBuckets.find(
760
+ (tb) => tb.$id === bucket.$id
761
+ );
762
+
763
+ if (!existingBucket) {
764
+ // Create bucket with fallback strategy for maximumFileSize
765
+ await this.createBucketWithFallback(bucket);
766
+ MessageFormatter.success(`Created bucket: ${bucket.name}`, {
767
+ prefix: "Transfer",
768
+ });
769
+ } else {
770
+ // Compare bucket permissions and update if needed
771
+ const sourcePermissions = JSON.stringify(
772
+ bucket.$permissions?.sort() || []
773
+ );
774
+ const targetPermissions = JSON.stringify(
775
+ existingBucket.$permissions?.sort() || []
776
+ );
777
+
778
+ if (
779
+ sourcePermissions !== targetPermissions ||
780
+ existingBucket.name !== bucket.name ||
781
+ existingBucket.fileSecurity !== bucket.fileSecurity ||
782
+ existingBucket.enabled !== bucket.enabled
783
+ ) {
784
+ MessageFormatter.warning(
785
+ `Bucket ${bucket.name} exists but has different settings. Updating to match source.`,
786
+ { prefix: "Transfer" }
787
+ );
788
+
789
+ try {
790
+ await this.targetStorage.updateBucket(
791
+ bucket.$id,
792
+ bucket.name,
793
+ bucket.$permissions,
794
+ bucket.fileSecurity,
795
+ bucket.enabled,
796
+ bucket.maximumFileSize,
797
+ bucket.allowedFileExtensions,
798
+ bucket.compression as any,
799
+ bucket.encryption,
800
+ bucket.antivirus
801
+ );
802
+ MessageFormatter.success(
803
+ `Updated bucket ${bucket.name} to match source`,
804
+ { prefix: "Transfer" }
805
+ );
806
+ } catch (updateError) {
807
+ MessageFormatter.error(
808
+ `Failed to update bucket ${bucket.name}`,
809
+ updateError instanceof Error
810
+ ? updateError
811
+ : new Error(String(updateError)),
812
+ { prefix: "Transfer" }
813
+ );
814
+ }
815
+ } else {
816
+ MessageFormatter.info(
817
+ `Bucket ${bucket.name} already exists with matching settings`,
818
+ { prefix: "Transfer" }
819
+ );
820
+ }
821
+ }
822
+
823
+ // Transfer bucket files with enhanced validation
824
+ await this.transferBucketFiles(bucket.$id, bucket.$id);
825
+
826
+ this.results.buckets.transferred++;
827
+ MessageFormatter.success(
828
+ `Bucket ${bucket.name} transferred successfully`,
829
+ { prefix: "Transfer" }
830
+ );
831
+ } catch (error) {
832
+ MessageFormatter.error(
833
+ `Bucket ${bucket.name} transfer failed`,
834
+ error instanceof Error ? error : new Error(String(error)),
835
+ { prefix: "Transfer" }
836
+ );
837
+ this.results.buckets.failed++;
838
+ }
839
+ })
840
+ );
841
+
842
+ await Promise.all(transferTasks);
843
+ MessageFormatter.success("Bucket transfer phase completed", {
844
+ prefix: "Transfer",
845
+ });
846
+ } catch (error) {
847
+ MessageFormatter.error(
848
+ "Bucket transfer phase failed",
849
+ error instanceof Error ? error : new Error(String(error)),
850
+ { prefix: "Transfer" }
851
+ );
852
+ }
853
+ }
854
+
855
+ private async createBucketWithFallback(bucket: Models.Bucket): Promise<void> {
856
+ // Determine the optimal size to try first
857
+ let sizeToTry: number;
858
+
859
+ if (this.cachedMaxFileSize) {
860
+ // Use cached size if it's smaller than or equal to the bucket's original size
861
+ if (bucket.maximumFileSize >= this.cachedMaxFileSize) {
862
+ sizeToTry = this.cachedMaxFileSize;
863
+ MessageFormatter.info(
864
+ `Bucket ${bucket.name}: Using cached maximumFileSize ${sizeToTry} (${(
865
+ sizeToTry / 1_000_000_000
866
+ ).toFixed(1)}GB)`,
867
+ { prefix: "Transfer" }
868
+ );
869
+ } else {
870
+ // Original size is smaller than cached size, try original first
871
+ sizeToTry = bucket.maximumFileSize;
872
+ }
873
+ } else {
874
+ // No cached size yet, try original size first
875
+ sizeToTry = bucket.maximumFileSize;
876
+ }
877
+
878
+ // Try the optimal size first
879
+ try {
880
+ await this.targetStorage.createBucket(
881
+ bucket.$id,
882
+ bucket.name,
883
+ bucket.$permissions,
884
+ bucket.fileSecurity,
885
+ bucket.enabled,
886
+ sizeToTry,
887
+ bucket.allowedFileExtensions,
888
+ bucket.compression as any,
889
+ bucket.encryption,
890
+ bucket.antivirus
891
+ );
892
+
893
+ // Success - cache this size if it's not already cached or is smaller than cached
894
+ if (!this.cachedMaxFileSize || sizeToTry < this.cachedMaxFileSize) {
895
+ this.cachedMaxFileSize = sizeToTry;
896
+ MessageFormatter.info(
897
+ `Bucket ${
898
+ bucket.name
899
+ }: Cached successful maximumFileSize ${sizeToTry} (${(
900
+ sizeToTry / 1_000_000_000
901
+ ).toFixed(1)}GB)`,
902
+ { prefix: "Transfer" }
903
+ );
904
+ }
905
+
906
+ // Log if we used a different size than original
907
+ if (sizeToTry !== bucket.maximumFileSize) {
908
+ MessageFormatter.warning(
909
+ `Bucket ${
910
+ bucket.name
911
+ }: maximumFileSize used ${sizeToTry} instead of original ${
912
+ bucket.maximumFileSize
913
+ } (${(sizeToTry / 1_000_000_000).toFixed(1)}GB)`,
914
+ { prefix: "Transfer" }
915
+ );
916
+ }
917
+
918
+ return; // Success, exit the function
919
+ } catch (error) {
920
+ const err = error instanceof Error ? error : new Error(String(error));
921
+
922
+ // Check if the error is related to maximumFileSize validation
923
+ if (
924
+ err.message.includes("maximumFileSize") ||
925
+ err.message.includes("valid range")
926
+ ) {
927
+ MessageFormatter.warning(
928
+ `Bucket ${bucket.name}: Failed with maximumFileSize ${sizeToTry}, falling back to smaller sizes...`,
929
+ { prefix: "Transfer" }
930
+ );
931
+ // Continue to fallback logic below
932
+ } else {
933
+ // Different error, don't retry
934
+ throw err;
935
+ }
936
+ }
937
+
938
+ // Fallback to progressively smaller sizes
939
+ const fallbackSizes = [
940
+ 5_000_000_000, // 5GB
941
+ 2_500_000_000, // 2.5GB
942
+ 2_000_000_000, // 2GB
943
+ 1_000_000_000, // 1GB
944
+ 500_000_000, // 500MB
945
+ 100_000_000, // 100MB
946
+ ];
947
+
948
+ // Remove sizes that are larger than or equal to the already-tried size
949
+ const validSizes = fallbackSizes
950
+ .filter((size) => size < sizeToTry)
951
+ .sort((a, b) => b - a); // Sort descending
952
+
953
+ let lastError: Error | null = null;
954
+
955
+ for (const fileSize of validSizes) {
956
+ try {
957
+ await this.targetStorage.createBucket(
958
+ bucket.$id,
959
+ bucket.name,
960
+ bucket.$permissions,
961
+ bucket.fileSecurity,
962
+ bucket.enabled,
963
+ fileSize,
964
+ bucket.allowedFileExtensions,
965
+ bucket.compression as any,
966
+ bucket.encryption,
967
+ bucket.antivirus
968
+ );
969
+
970
+ // Success - cache this size if it's not already cached or is smaller than cached
971
+ if (!this.cachedMaxFileSize || fileSize < this.cachedMaxFileSize) {
972
+ this.cachedMaxFileSize = fileSize;
973
+ MessageFormatter.info(
974
+ `Bucket ${
975
+ bucket.name
976
+ }: Cached successful maximumFileSize ${fileSize} (${(
977
+ fileSize / 1_000_000_000
978
+ ).toFixed(1)}GB)`,
979
+ { prefix: "Transfer" }
980
+ );
981
+ }
982
+
983
+ // Log if we had to reduce the file size
984
+ if (fileSize !== bucket.maximumFileSize) {
985
+ MessageFormatter.warning(
986
+ `Bucket ${bucket.name}: maximumFileSize reduced from ${
987
+ bucket.maximumFileSize
988
+ } to ${fileSize} (${(fileSize / 1_000_000_000).toFixed(1)}GB)`,
989
+ { prefix: "Transfer" }
990
+ );
991
+ }
992
+
993
+ return; // Success, exit the function
994
+ } catch (error) {
995
+ lastError = error instanceof Error ? error : new Error(String(error));
996
+
997
+ // Check if the error is related to maximumFileSize validation
998
+ if (
999
+ lastError.message.includes("maximumFileSize") ||
1000
+ lastError.message.includes("valid range")
1001
+ ) {
1002
+ MessageFormatter.warning(
1003
+ `Bucket ${bucket.name}: Failed with maximumFileSize ${fileSize}, trying smaller size...`,
1004
+ { prefix: "Transfer" }
1005
+ );
1006
+ continue; // Try next smaller size
1007
+ } else {
1008
+ // Different error, don't retry
1009
+ throw lastError;
1010
+ }
1011
+ }
1012
+ }
1013
+
1014
+ // If we get here, all fallback sizes failed
1015
+ MessageFormatter.error(
1016
+ `Bucket ${bucket.name}: All fallback file sizes failed. Last error: ${lastError?.message}`,
1017
+ lastError || undefined,
1018
+ { prefix: "Transfer" }
1019
+ );
1020
+ throw lastError || new Error("All fallback file sizes failed");
1021
+ }
1022
+
1023
+ private async transferBucketFiles(
1024
+ sourceBucketId: string,
1025
+ targetBucketId: string
1026
+ ): Promise<void> {
1027
+ let lastFileId: string | undefined;
1028
+ let transferredFiles = 0;
1029
+
1030
+ while (true) {
1031
+ const queries = [Query.limit(50)]; // Smaller batch size for better rate limiting
1032
+ if (lastFileId) {
1033
+ queries.push(Query.cursorAfter(lastFileId));
1034
+ }
1035
+
1036
+ const files = await this.sourceStorage.listFiles(sourceBucketId, queries);
1037
+ if (files.files.length === 0) break;
1038
+
1039
+ // Process files with rate limiting
1040
+ const fileTasks = files.files.map((file) =>
1041
+ this.fileLimit(async () => {
1042
+ try {
1043
+ // Check if file already exists and compare permissions
1044
+ let existingFile: Models.File | null = null;
1045
+ try {
1046
+ existingFile = await this.targetStorage.getFile(
1047
+ targetBucketId,
1048
+ file.$id
1049
+ );
1050
+
1051
+ // Compare permissions between source and target file
1052
+ const sourcePermissions = JSON.stringify(
1053
+ file.$permissions?.sort() || []
1054
+ );
1055
+ const targetPermissions = JSON.stringify(
1056
+ existingFile.$permissions?.sort() || []
1057
+ );
1058
+
1059
+ if (sourcePermissions !== targetPermissions) {
1060
+ MessageFormatter.warning(
1061
+ `File ${file.name} (${file.$id}) exists but has different permissions. Source: ${sourcePermissions}, Target: ${targetPermissions}`,
1062
+ { prefix: "Transfer" }
1063
+ );
1064
+
1065
+ // Update file permissions to match source
1066
+ try {
1067
+ await this.targetStorage.updateFile(
1068
+ targetBucketId,
1069
+ file.$id,
1070
+ file.name,
1071
+ file.$permissions
1072
+ );
1073
+ MessageFormatter.success(
1074
+ `Updated file ${file.name} permissions to match source`,
1075
+ { prefix: "Transfer" }
1076
+ );
1077
+ } catch (updateError) {
1078
+ MessageFormatter.error(
1079
+ `Failed to update permissions for file ${file.name}`,
1080
+ updateError instanceof Error
1081
+ ? updateError
1082
+ : new Error(String(updateError)),
1083
+ { prefix: "Transfer" }
1084
+ );
1085
+ }
1086
+ } else {
1087
+ MessageFormatter.info(
1088
+ `File ${file.name} already exists with matching permissions, skipping`,
1089
+ { prefix: "Transfer" }
1090
+ );
1091
+ }
1092
+ return;
1093
+ } catch (error) {
1094
+ // File doesn't exist, proceed with transfer
1095
+ }
1096
+
1097
+ // Download file with validation
1098
+ const fileData = await this.validateAndDownloadFile(
1099
+ sourceBucketId,
1100
+ file.$id
1101
+ );
1102
+ if (!fileData) {
1103
+ MessageFormatter.warning(
1104
+ `File ${file.name} failed validation, skipping`,
1105
+ { prefix: "Transfer" }
1106
+ );
1107
+ return;
1108
+ }
1109
+
1110
+ // Upload file to target
1111
+ const fileToCreate = InputFile.fromBuffer(
1112
+ new Uint8Array(fileData),
1113
+ file.name
1114
+ );
1115
+
1116
+ await this.targetStorage.createFile(
1117
+ targetBucketId,
1118
+ file.$id,
1119
+ fileToCreate,
1120
+ file.$permissions
1121
+ );
1122
+
1123
+ transferredFiles++;
1124
+ MessageFormatter.success(`Transferred file: ${file.name}`, {
1125
+ prefix: "Transfer",
1126
+ });
1127
+ } catch (error) {
1128
+ MessageFormatter.error(
1129
+ `Failed to transfer file ${file.name}`,
1130
+ error instanceof Error ? error : new Error(String(error)),
1131
+ { prefix: "Transfer" }
1132
+ );
1133
+ }
1134
+ })
1135
+ );
1136
+
1137
+ await Promise.all(fileTasks);
1138
+
1139
+ if (files.files.length < 50) break;
1140
+ lastFileId = files.files[files.files.length - 1].$id;
1141
+ }
1142
+
1143
+ MessageFormatter.info(
1144
+ `Transferred ${transferredFiles} files from bucket ${sourceBucketId}`,
1145
+ { prefix: "Transfer" }
1146
+ );
1147
+ }
1148
+
1149
+ private async validateAndDownloadFile(
1150
+ bucketId: string,
1151
+ fileId: string
1152
+ ): Promise<ArrayBuffer | null> {
1153
+ let attempts = 3;
1154
+ while (attempts > 0) {
1155
+ try {
1156
+ const fileData = await this.sourceStorage.getFileDownload(
1157
+ bucketId,
1158
+ fileId
1159
+ );
1160
+
1161
+ // Basic validation - ensure file is not empty and not too large
1162
+ if (fileData.byteLength === 0) {
1163
+ MessageFormatter.warning(`File ${fileId} is empty`, {
1164
+ prefix: "Transfer",
1165
+ });
1166
+ return null;
1167
+ }
1168
+
1169
+ if (fileData.byteLength > 50 * 1024 * 1024) {
1170
+ // 50MB limit
1171
+ MessageFormatter.warning(
1172
+ `File ${fileId} is too large (${fileData.byteLength} bytes)`,
1173
+ { prefix: "Transfer" }
1174
+ );
1175
+ return null;
1176
+ }
1177
+
1178
+ return fileData;
1179
+ } catch (error) {
1180
+ attempts--;
1181
+ MessageFormatter.warning(
1182
+ `Error downloading file ${fileId}, attempts left: ${attempts}`,
1183
+ { prefix: "Transfer" }
1184
+ );
1185
+ if (attempts === 0) {
1186
+ MessageFormatter.error(
1187
+ `Failed to download file ${fileId} after all attempts`,
1188
+ error instanceof Error ? error : new Error(String(error)),
1189
+ { prefix: "Transfer" }
1190
+ );
1191
+ return null;
1192
+ }
1193
+ // Wait before retry
1194
+ await new Promise((resolve) =>
1195
+ setTimeout(resolve, 1000 * (4 - attempts))
1196
+ );
1197
+ }
1198
+ }
1199
+ return null;
1200
+ }
1201
+
1202
+ private async transferAllFunctions(): Promise<void> {
1203
+ MessageFormatter.info("Starting function transfer phase", {
1204
+ prefix: "Transfer",
1205
+ });
1206
+
1207
+ try {
1208
+ const sourceFunctions = await listFunctions(this.sourceClient, [
1209
+ Query.limit(1000),
1210
+ ]);
1211
+ const targetFunctions = await listFunctions(this.targetClient, [
1212
+ Query.limit(1000),
1213
+ ]);
1214
+
1215
+ if (this.options.dryRun) {
1216
+ MessageFormatter.info(
1217
+ `DRY RUN: Would transfer ${sourceFunctions.functions.length} functions`,
655
1218
  { prefix: "Transfer" }
656
1219
  );
657
- await processQueue(this.targetDatabases, dbId);
658
- } else {
659
- MessageFormatter.info("No queued relationship operations to process", {
660
- prefix: "Transfer",
661
- });
1220
+ return;
662
1221
  }
1222
+
1223
+ const transferTasks = sourceFunctions.functions.map((func) =>
1224
+ this.limit(async () => {
1225
+ try {
1226
+ // Check if function exists in target
1227
+ const existingFunc = targetFunctions.functions.find(
1228
+ (tf) => tf.$id === func.$id
1229
+ );
1230
+
1231
+ if (existingFunc) {
1232
+ MessageFormatter.info(
1233
+ `Function ${func.name} already exists, skipping creation`,
1234
+ { prefix: "Transfer" }
1235
+ );
1236
+ this.results.functions.skipped++;
1237
+ return;
1238
+ }
1239
+
1240
+ // Download function from source
1241
+ const functionPath = await this.downloadFunction(func);
1242
+ if (!functionPath) {
1243
+ MessageFormatter.error(
1244
+ `Failed to download function ${func.name}`,
1245
+ undefined,
1246
+ { prefix: "Transfer" }
1247
+ );
1248
+ this.results.functions.failed++;
1249
+ return;
1250
+ }
1251
+
1252
+ // Deploy function to target
1253
+ const functionConfig = {
1254
+ $id: func.$id,
1255
+ name: func.name,
1256
+ runtime: func.runtime as any,
1257
+ execute: func.execute,
1258
+ events: func.events,
1259
+ enabled: func.enabled,
1260
+ logging: func.logging,
1261
+ entrypoint: func.entrypoint,
1262
+ commands: func.commands,
1263
+ scopes: func.scopes as any,
1264
+ timeout: func.timeout,
1265
+ schedule: func.schedule,
1266
+ installationId: func.installationId,
1267
+ providerRepositoryId: func.providerRepositoryId,
1268
+ providerBranch: func.providerBranch,
1269
+ providerSilentMode: func.providerSilentMode,
1270
+ providerRootDirectory: func.providerRootDirectory,
1271
+ specification: func.specification as any,
1272
+ dirPath: functionPath,
1273
+ };
1274
+
1275
+ await deployLocalFunction(
1276
+ this.targetClient,
1277
+ func.name,
1278
+ functionConfig,
1279
+ undefined,
1280
+ this.tempDir
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
+ });
663
1303
  } catch (error) {
664
1304
  MessageFormatter.error(
665
- `Failed to create database structure for ${dbId}`,
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}`,
666
1325
  error instanceof Error ? error : new Error(String(error)),
667
1326
  { prefix: "Transfer" }
668
1327
  );
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
- */
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
1429
  private async createCollectionAttributesWithStatusCheck(
1430
1430
  databases: Databases,
1431
1431
  dbId: string,
@@ -1490,10 +1490,10 @@ export class ComprehensiveTransfer {
1490
1490
  return false;
1491
1491
  }
1492
1492
  }
1493
-
1494
- /**
1495
- * Helper method to create collection indexes with status checking
1496
- */
1493
+
1494
+ /**
1495
+ * Helper method to create collection indexes with status checking
1496
+ */
1497
1497
  private async createCollectionIndexesWithStatusCheck(
1498
1498
  dbId: string,
1499
1499
  databases: Databases,
@@ -1523,763 +1523,763 @@ export class ComprehensiveTransfer {
1523
1523
  return false;
1524
1524
  }
1525
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
- }
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
+ }