appwrite-utils-cli 1.2.15 → 1.2.16

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.
@@ -1,4 +1,9 @@
1
- import { converterFunctions, tryAwaitWithRetry, parseAttribute, objectNeedsUpdate } from "appwrite-utils";
1
+ import {
2
+ converterFunctions,
3
+ tryAwaitWithRetry,
4
+ parseAttribute,
5
+ objectNeedsUpdate,
6
+ } from "appwrite-utils";
2
7
  import {
3
8
  Client,
4
9
  Databases,
@@ -8,22 +13,24 @@ import {
8
13
  Teams,
9
14
  type Models,
10
15
  Query,
16
+ AppwriteException,
11
17
  } from "node-appwrite";
12
18
  import { InputFile } from "node-appwrite/file";
13
19
  import { MessageFormatter } from "../shared/messageFormatter.js";
14
20
  import { ProgressManager } from "../shared/progressManager.js";
15
21
  import { getClient } from "../utils/getClientFromConfig.js";
16
- import {
17
- transferDatabaseLocalToLocal,
22
+ import {
23
+ transferDatabaseLocalToLocal,
18
24
  transferDatabaseLocalToRemote,
19
25
  transferStorageLocalToLocal,
20
26
  transferStorageLocalToRemote,
21
- transferUsersLocalToRemote
27
+ transferUsersLocalToRemote,
22
28
  } from "./transfer.js";
23
- import {
24
- deployLocalFunction
25
- } from "../functions/deployments.js";
26
- import { listFunctions, downloadLatestFunctionDeployment } from "../functions/methods.js";
29
+ import { deployLocalFunction } from "../functions/deployments.js";
30
+ import {
31
+ listFunctions,
32
+ downloadLatestFunctionDeployment,
33
+ } from "../functions/methods.js";
27
34
  import pLimit from "p-limit";
28
35
  import chalk from "chalk";
29
36
  import { join } from "node:path";
@@ -100,7 +107,7 @@ export class ComprehensiveTransfer {
100
107
 
101
108
  const baseLimit = options.concurrencyLimit || 10;
102
109
  this.limit = pLimit(baseLimit);
103
-
110
+
104
111
  // Different rate limits for different operations to prevent API throttling
105
112
  // Users: Half speed (more sensitive operations)
106
113
  // Files: Quarter speed (most bandwidth intensive)
@@ -120,18 +127,25 @@ export class ComprehensiveTransfer {
120
127
 
121
128
  async execute(): Promise<TransferResults> {
122
129
  try {
123
- MessageFormatter.info("Starting comprehensive transfer", { prefix: "Transfer" });
124
-
130
+ MessageFormatter.info("Starting comprehensive transfer", {
131
+ prefix: "Transfer",
132
+ });
133
+
125
134
  if (this.options.dryRun) {
126
- MessageFormatter.info("DRY RUN MODE - No actual changes will be made", { prefix: "Transfer" });
135
+ MessageFormatter.info("DRY RUN MODE - No actual changes will be made", {
136
+ prefix: "Transfer",
137
+ });
127
138
  }
128
139
 
129
140
  // Show rate limiting configuration
130
141
  const baseLimit = this.options.concurrencyLimit || 10;
131
142
  const userLimit = Math.max(1, Math.floor(baseLimit / 2));
132
143
  const fileLimit = Math.max(1, Math.floor(baseLimit / 4));
133
-
134
- MessageFormatter.info(`Rate limits: General=${baseLimit}, Users=${userLimit}, Files=${fileLimit}`, { prefix: "Transfer" });
144
+
145
+ MessageFormatter.info(
146
+ `Rate limits: General=${baseLimit}, Users=${userLimit}, Files=${fileLimit}`,
147
+ { prefix: "Transfer" }
148
+ );
135
149
 
136
150
  // Ensure temp directory exists
137
151
  if (!fs.existsSync(this.tempDir)) {
@@ -164,7 +178,11 @@ export class ComprehensiveTransfer {
164
178
 
165
179
  return this.results;
166
180
  } catch (error) {
167
- MessageFormatter.error("Comprehensive transfer failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
181
+ MessageFormatter.error(
182
+ "Comprehensive transfer failed",
183
+ error instanceof Error ? error : new Error(String(error)),
184
+ { prefix: "Transfer" }
185
+ );
168
186
  throw error;
169
187
  } finally {
170
188
  // Clean up temp directory
@@ -175,11 +193,16 @@ export class ComprehensiveTransfer {
175
193
  }
176
194
 
177
195
  private async transferAllUsers(): Promise<void> {
178
- MessageFormatter.info("Starting user transfer phase", { prefix: "Transfer" });
196
+ MessageFormatter.info("Starting user transfer phase", {
197
+ prefix: "Transfer",
198
+ });
179
199
 
180
200
  if (this.options.dryRun) {
181
201
  const usersList = await this.sourceUsers.list([Query.limit(1)]);
182
- MessageFormatter.info(`DRY RUN: Would transfer ${usersList.total} users`, { prefix: "Transfer" });
202
+ MessageFormatter.info(
203
+ `DRY RUN: Would transfer ${usersList.total} users`,
204
+ { prefix: "Transfer" }
205
+ );
183
206
  return;
184
207
  }
185
208
 
@@ -193,20 +216,28 @@ export class ComprehensiveTransfer {
193
216
  this.options.targetProject,
194
217
  this.options.targetKey
195
218
  );
196
-
219
+
197
220
  // Get actual count for results
198
221
  const usersList = await this.sourceUsers.list([Query.limit(1)]);
199
222
  this.results.users.transferred = usersList.total;
200
-
201
- MessageFormatter.success(`User transfer completed`, { prefix: "Transfer" });
223
+
224
+ MessageFormatter.success(`User transfer completed`, {
225
+ prefix: "Transfer",
226
+ });
202
227
  } catch (error) {
203
- MessageFormatter.error("User transfer failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
228
+ MessageFormatter.error(
229
+ "User transfer failed",
230
+ error instanceof Error ? error : new Error(String(error)),
231
+ { prefix: "Transfer" }
232
+ );
204
233
  this.results.users.failed = 1;
205
234
  }
206
235
  }
207
236
 
208
237
  private async transferAllTeams(): Promise<void> {
209
- MessageFormatter.info("Starting team transfer phase", { prefix: "Transfer" });
238
+ MessageFormatter.info("Starting team transfer phase", {
239
+ prefix: "Transfer",
240
+ });
210
241
 
211
242
  try {
212
243
  // Fetch all teams from source with pagination
@@ -216,47 +247,68 @@ export class ComprehensiveTransfer {
216
247
  if (this.options.dryRun) {
217
248
  let totalMemberships = 0;
218
249
  for (const team of allSourceTeams) {
219
- const memberships = await this.sourceTeams.listMemberships(team.$id, [Query.limit(1)]);
250
+ const memberships = await this.sourceTeams.listMemberships(team.$id, [
251
+ Query.limit(1),
252
+ ]);
220
253
  totalMemberships += memberships.total;
221
254
  }
222
- MessageFormatter.info(`DRY RUN: Would transfer ${allSourceTeams.length} teams with ${totalMemberships} memberships`, { prefix: "Transfer" });
255
+ MessageFormatter.info(
256
+ `DRY RUN: Would transfer ${allSourceTeams.length} teams with ${totalMemberships} memberships`,
257
+ { prefix: "Transfer" }
258
+ );
223
259
  return;
224
260
  }
225
261
 
226
- const transferTasks = allSourceTeams.map(team =>
262
+ const transferTasks = allSourceTeams.map((team) =>
227
263
  this.limit(async () => {
228
264
  try {
229
265
  // Check if team exists in target
230
- const existingTeam = allTargetTeams.find(tt => tt.$id === team.$id);
231
-
266
+ const existingTeam = allTargetTeams.find(
267
+ (tt) => tt.$id === team.$id
268
+ );
269
+
232
270
  if (!existingTeam) {
233
271
  // Fetch all memberships to extract unique roles before creating team
234
- MessageFormatter.info(`Fetching memberships for team ${team.name} to extract roles`, { prefix: "Transfer" });
272
+ MessageFormatter.info(
273
+ `Fetching memberships for team ${team.name} to extract roles`,
274
+ { prefix: "Transfer" }
275
+ );
235
276
  const memberships = await this.fetchAllMemberships(team.$id);
236
-
277
+
237
278
  // Extract unique roles from all memberships
238
279
  const allRoles = new Set<string>();
239
- memberships.forEach(membership => {
240
- membership.roles.forEach(role => allRoles.add(role));
280
+ memberships.forEach((membership) => {
281
+ membership.roles.forEach((role) => allRoles.add(role));
241
282
  });
242
283
  const uniqueRoles = Array.from(allRoles);
243
-
244
- MessageFormatter.info(`Found ${uniqueRoles.length} unique roles for team ${team.name}: ${uniqueRoles.join(', ')}`, { prefix: "Transfer" });
245
-
284
+
285
+ MessageFormatter.info(
286
+ `Found ${uniqueRoles.length} unique roles for team ${
287
+ team.name
288
+ }: ${uniqueRoles.join(", ")}`,
289
+ { prefix: "Transfer" }
290
+ );
291
+
246
292
  // Create team in target with the collected roles
247
- await this.targetTeams.create(
248
- team.$id,
249
- team.name,
250
- uniqueRoles
293
+ await this.targetTeams.create(team.$id, team.name, uniqueRoles);
294
+ MessageFormatter.success(
295
+ `Created team: ${team.name} with roles: ${uniqueRoles.join(
296
+ ", "
297
+ )}`,
298
+ { prefix: "Transfer" }
251
299
  );
252
- MessageFormatter.success(`Created team: ${team.name} with roles: ${uniqueRoles.join(', ')}`, { prefix: "Transfer" });
253
300
  } else {
254
- MessageFormatter.info(`Team ${team.name} already exists, updating if needed`, { prefix: "Transfer" });
255
-
301
+ MessageFormatter.info(
302
+ `Team ${team.name} already exists, updating if needed`,
303
+ { prefix: "Transfer" }
304
+ );
305
+
256
306
  // Update team if needed
257
307
  if (existingTeam.name !== team.name) {
258
308
  await this.targetTeams.updateName(team.$id, team.name);
259
- MessageFormatter.success(`Updated team name: ${team.name}`, { prefix: "Transfer" });
309
+ MessageFormatter.success(`Updated team name: ${team.name}`, {
310
+ prefix: "Transfer",
311
+ });
260
312
  }
261
313
  }
262
314
 
@@ -264,54 +316,85 @@ export class ComprehensiveTransfer {
264
316
  await this.transferTeamMemberships(team.$id);
265
317
 
266
318
  this.results.teams.transferred++;
267
- MessageFormatter.success(`Team ${team.name} transferred successfully`, { prefix: "Transfer" });
319
+ MessageFormatter.success(
320
+ `Team ${team.name} transferred successfully`,
321
+ { prefix: "Transfer" }
322
+ );
268
323
  } catch (error) {
269
- MessageFormatter.error(`Team ${team.name} transfer failed`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
324
+ MessageFormatter.error(
325
+ `Team ${team.name} transfer failed`,
326
+ error instanceof Error ? error : new Error(String(error)),
327
+ { prefix: "Transfer" }
328
+ );
270
329
  this.results.teams.failed++;
271
330
  }
272
331
  })
273
332
  );
274
333
 
275
334
  await Promise.all(transferTasks);
276
- MessageFormatter.success("Team transfer phase completed", { prefix: "Transfer" });
335
+ MessageFormatter.success("Team transfer phase completed", {
336
+ prefix: "Transfer",
337
+ });
277
338
  } catch (error) {
278
- MessageFormatter.error("Team transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
339
+ MessageFormatter.error(
340
+ "Team transfer phase failed",
341
+ error instanceof Error ? error : new Error(String(error)),
342
+ { prefix: "Transfer" }
343
+ );
279
344
  }
280
345
  }
281
346
 
282
347
  private async transferAllDatabases(): Promise<void> {
283
- MessageFormatter.info("Starting database transfer phase", { prefix: "Transfer" });
348
+ MessageFormatter.info("Starting database transfer phase", {
349
+ prefix: "Transfer",
350
+ });
284
351
 
285
352
  try {
286
353
  const sourceDatabases = await this.sourceDatabases.list();
287
354
  const targetDatabases = await this.targetDatabases.list();
288
355
 
289
356
  if (this.options.dryRun) {
290
- MessageFormatter.info(`DRY RUN: Would transfer ${sourceDatabases.databases.length} databases`, { prefix: "Transfer" });
357
+ MessageFormatter.info(
358
+ `DRY RUN: Would transfer ${sourceDatabases.databases.length} databases`,
359
+ { prefix: "Transfer" }
360
+ );
291
361
  return;
292
362
  }
293
363
 
294
364
  // Phase 1: Create all databases and collections (structure only)
295
- MessageFormatter.info("Phase 1: Creating database structures (databases, collections, attributes, indexes)", { prefix: "Transfer" });
296
-
297
- const structureCreationTasks = sourceDatabases.databases.map(db =>
365
+ MessageFormatter.info(
366
+ "Phase 1: Creating database structures (databases, collections, attributes, indexes)",
367
+ { prefix: "Transfer" }
368
+ );
369
+
370
+ const structureCreationTasks = sourceDatabases.databases.map((db) =>
298
371
  this.limit(async () => {
299
372
  try {
300
373
  // Check if database exists in target
301
- const existingDb = targetDatabases.databases.find(tdb => tdb.$id === db.$id);
302
-
374
+ const existingDb = targetDatabases.databases.find(
375
+ (tdb) => tdb.$id === db.$id
376
+ );
377
+
303
378
  if (!existingDb) {
304
379
  // Create database in target
305
380
  await this.targetDatabases.create(db.$id, db.name, db.enabled);
306
- MessageFormatter.success(`Created database: ${db.name}`, { prefix: "Transfer" });
381
+ MessageFormatter.success(`Created database: ${db.name}`, {
382
+ prefix: "Transfer",
383
+ });
307
384
  }
308
385
 
309
386
  // Create collections, attributes, and indexes WITHOUT transferring documents
310
387
  await this.createDatabaseStructure(db.$id);
311
388
 
312
- MessageFormatter.success(`Database structure created: ${db.name}`, { prefix: "Transfer" });
389
+ MessageFormatter.success(`Database structure created: ${db.name}`, {
390
+ prefix: "Transfer",
391
+ });
313
392
  } catch (error) {
314
- MessageFormatter.error(`Database structure creation failed for ${db.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
393
+ MessageFormatter.error(
394
+ `Database structure creation failed for ${db.name}`,
395
+ error instanceof Error ? error : new Error(String(error)),
396
+ { prefix: "Transfer" }
397
+ );
315
398
  this.results.databases.failed++;
316
399
  }
317
400
  })
@@ -320,27 +403,43 @@ export class ComprehensiveTransfer {
320
403
  await Promise.all(structureCreationTasks);
321
404
 
322
405
  // Phase 2: Transfer all documents after all structures are created
323
- MessageFormatter.info("Phase 2: Transferring documents to all collections", { prefix: "Transfer" });
324
-
325
- const documentTransferTasks = sourceDatabases.databases.map(db =>
406
+ MessageFormatter.info(
407
+ "Phase 2: Transferring documents to all collections",
408
+ { prefix: "Transfer" }
409
+ );
410
+
411
+ const documentTransferTasks = sourceDatabases.databases.map((db) =>
326
412
  this.limit(async () => {
327
413
  try {
328
414
  // Transfer documents for this database
329
415
  await this.transferDatabaseDocuments(db.$id);
330
416
 
331
417
  this.results.databases.transferred++;
332
- MessageFormatter.success(`Database documents transferred: ${db.name}`, { prefix: "Transfer" });
418
+ MessageFormatter.success(
419
+ `Database documents transferred: ${db.name}`,
420
+ { prefix: "Transfer" }
421
+ );
333
422
  } catch (error) {
334
- MessageFormatter.error(`Document transfer failed for ${db.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
423
+ MessageFormatter.error(
424
+ `Document transfer failed for ${db.name}`,
425
+ error instanceof Error ? error : new Error(String(error)),
426
+ { prefix: "Transfer" }
427
+ );
335
428
  this.results.databases.failed++;
336
429
  }
337
430
  })
338
431
  );
339
432
 
340
433
  await Promise.all(documentTransferTasks);
341
- MessageFormatter.success("Database transfer phase completed", { prefix: "Transfer" });
434
+ MessageFormatter.success("Database transfer phase completed", {
435
+ prefix: "Transfer",
436
+ });
342
437
  } catch (error) {
343
- MessageFormatter.error("Database transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
438
+ MessageFormatter.error(
439
+ "Database transfer phase failed",
440
+ error instanceof Error ? error : new Error(String(error)),
441
+ { prefix: "Transfer" }
442
+ );
344
443
  }
345
444
  }
346
445
 
@@ -348,33 +447,51 @@ export class ComprehensiveTransfer {
348
447
  * Phase 1: Create database structure (collections, attributes, indexes) without transferring documents
349
448
  */
350
449
  private async createDatabaseStructure(dbId: string): Promise<void> {
351
- MessageFormatter.info(`Creating database structure for ${dbId}`, { prefix: "Transfer" });
450
+ MessageFormatter.info(`Creating database structure for ${dbId}`, {
451
+ prefix: "Transfer",
452
+ });
352
453
 
353
454
  try {
354
455
  // Get all collections from source database
355
- const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
356
- MessageFormatter.info(`Found ${sourceCollections.length} collections in source database ${dbId}`, { prefix: "Transfer" });
456
+ const sourceCollections = await this.fetchAllCollections(
457
+ dbId,
458
+ this.sourceDatabases
459
+ );
460
+ MessageFormatter.info(
461
+ `Found ${sourceCollections.length} collections in source database ${dbId}`,
462
+ { prefix: "Transfer" }
463
+ );
357
464
 
358
465
  // Process each collection
359
466
  for (const collection of sourceCollections) {
360
- MessageFormatter.info(`Processing collection: ${collection.name} (${collection.$id})`, { prefix: "Transfer" });
467
+ MessageFormatter.info(
468
+ `Processing collection: ${collection.name} (${collection.$id})`,
469
+ { prefix: "Transfer" }
470
+ );
361
471
 
362
472
  try {
363
473
  // Create or update collection in target
364
474
  let targetCollection: Models.Collection;
365
475
  const existingCollection = await tryAwaitWithRetry(async () =>
366
- this.targetDatabases.listCollections(dbId, [Query.equal("$id", collection.$id)])
476
+ this.targetDatabases.listCollections(dbId, [
477
+ Query.equal("$id", collection.$id),
478
+ ])
367
479
  );
368
480
 
369
481
  if (existingCollection.collections.length > 0) {
370
482
  targetCollection = existingCollection.collections[0];
371
- MessageFormatter.info(`Collection ${collection.name} exists in target database`, { prefix: "Transfer" });
483
+ MessageFormatter.info(
484
+ `Collection ${collection.name} exists in target database`,
485
+ { prefix: "Transfer" }
486
+ );
372
487
 
373
488
  // Update collection if needed
374
489
  if (
375
490
  targetCollection.name !== collection.name ||
376
- JSON.stringify(targetCollection.$permissions) !== JSON.stringify(collection.$permissions) ||
377
- targetCollection.documentSecurity !== collection.documentSecurity ||
491
+ JSON.stringify(targetCollection.$permissions) !==
492
+ JSON.stringify(collection.$permissions) ||
493
+ targetCollection.documentSecurity !==
494
+ collection.documentSecurity ||
378
495
  targetCollection.enabled !== collection.enabled
379
496
  ) {
380
497
  targetCollection = await tryAwaitWithRetry(async () =>
@@ -387,10 +504,16 @@ export class ComprehensiveTransfer {
387
504
  collection.enabled
388
505
  )
389
506
  );
390
- MessageFormatter.success(`Collection ${collection.name} updated`, { prefix: "Transfer" });
507
+ MessageFormatter.success(
508
+ `Collection ${collection.name} updated`,
509
+ { prefix: "Transfer" }
510
+ );
391
511
  }
392
512
  } else {
393
- MessageFormatter.info(`Creating collection ${collection.name} in target database...`, { prefix: "Transfer" });
513
+ MessageFormatter.info(
514
+ `Creating collection ${collection.name} in target database...`,
515
+ { prefix: "Transfer" }
516
+ );
394
517
  targetCollection = await tryAwaitWithRetry(async () =>
395
518
  this.targetDatabases.createCollection(
396
519
  dbId,
@@ -401,55 +524,99 @@ export class ComprehensiveTransfer {
401
524
  collection.enabled
402
525
  )
403
526
  );
404
- MessageFormatter.success(`Collection ${collection.name} created`, { prefix: "Transfer" });
527
+ MessageFormatter.success(`Collection ${collection.name} created`, {
528
+ prefix: "Transfer",
529
+ });
405
530
  }
406
531
 
407
532
  // Handle attributes with enhanced status checking
408
- MessageFormatter.info(`Creating attributes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
409
-
410
- const attributesToCreate = collection.attributes.map(attr => parseAttribute(attr as any));
411
-
412
- const attributesSuccess = await this.createCollectionAttributesWithStatusCheck(
413
- this.targetDatabases,
414
- dbId,
415
- targetCollection,
416
- attributesToCreate
533
+ MessageFormatter.info(
534
+ `Creating attributes for collection ${collection.name} with enhanced monitoring...`,
535
+ { prefix: "Transfer" }
536
+ );
537
+
538
+ const attributesToCreate = collection.attributes.map((attr) =>
539
+ parseAttribute(attr as any)
417
540
  );
418
-
541
+
542
+ const attributesSuccess =
543
+ await this.createCollectionAttributesWithStatusCheck(
544
+ this.targetDatabases,
545
+ dbId,
546
+ targetCollection,
547
+ attributesToCreate
548
+ );
549
+
419
550
  if (!attributesSuccess) {
420
- MessageFormatter.error(`Failed to create some attributes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
421
- MessageFormatter.error(`Skipping index creation and document transfer for collection ${collection.name} due to attribute failures`, undefined, { prefix: "Transfer" });
551
+ MessageFormatter.error(
552
+ `Failed to create some attributes for collection ${collection.name}`,
553
+ undefined,
554
+ { prefix: "Transfer" }
555
+ );
556
+ MessageFormatter.error(
557
+ `Skipping index creation and document transfer for collection ${collection.name} due to attribute failures`,
558
+ undefined,
559
+ { prefix: "Transfer" }
560
+ );
422
561
  // Skip indexes and document transfer if attributes failed
423
562
  continue;
424
563
  } else {
425
- MessageFormatter.success(`All attributes created successfully for collection ${collection.name}`, { prefix: "Transfer" });
564
+ MessageFormatter.success(
565
+ `All attributes created successfully for collection ${collection.name}`,
566
+ { prefix: "Transfer" }
567
+ );
426
568
  }
427
569
 
428
570
  // Handle indexes with enhanced status checking
429
- MessageFormatter.info(`Creating indexes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
430
-
431
- const indexesSuccess = await this.createCollectionIndexesWithStatusCheck(
432
- dbId,
433
- this.targetDatabases,
434
- targetCollection.$id,
435
- targetCollection,
436
- collection.indexes as any
571
+ MessageFormatter.info(
572
+ `Creating indexes for collection ${collection.name} with enhanced monitoring...`,
573
+ { prefix: "Transfer" }
437
574
  );
438
-
575
+
576
+ const indexesSuccess =
577
+ await this.createCollectionIndexesWithStatusCheck(
578
+ dbId,
579
+ this.targetDatabases,
580
+ targetCollection.$id,
581
+ targetCollection,
582
+ collection.indexes as any
583
+ );
584
+
439
585
  if (!indexesSuccess) {
440
- MessageFormatter.error(`Failed to create some indexes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
441
- MessageFormatter.warning(`Proceeding with document transfer despite index failures for collection ${collection.name}`, { prefix: "Transfer" });
586
+ MessageFormatter.error(
587
+ `Failed to create some indexes for collection ${collection.name}`,
588
+ undefined,
589
+ { prefix: "Transfer" }
590
+ );
591
+ MessageFormatter.warning(
592
+ `Proceeding with document transfer despite index failures for collection ${collection.name}`,
593
+ { prefix: "Transfer" }
594
+ );
442
595
  } else {
443
- MessageFormatter.success(`All indexes created successfully for collection ${collection.name}`, { prefix: "Transfer" });
596
+ MessageFormatter.success(
597
+ `All indexes created successfully for collection ${collection.name}`,
598
+ { prefix: "Transfer" }
599
+ );
444
600
  }
445
601
 
446
- MessageFormatter.success(`Structure complete for collection ${collection.name}`, { prefix: "Transfer" });
602
+ MessageFormatter.success(
603
+ `Structure complete for collection ${collection.name}`,
604
+ { prefix: "Transfer" }
605
+ );
447
606
  } catch (error) {
448
- MessageFormatter.error(`Error processing collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
607
+ MessageFormatter.error(
608
+ `Error processing collection ${collection.name}`,
609
+ error instanceof Error ? error : new Error(String(error)),
610
+ { prefix: "Transfer" }
611
+ );
449
612
  }
450
613
  }
451
614
  } catch (error) {
452
- MessageFormatter.error(`Failed to create database structure for ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
615
+ MessageFormatter.error(
616
+ `Failed to create database structure for ${dbId}`,
617
+ error instanceof Error ? error : new Error(String(error)),
618
+ { prefix: "Transfer" }
619
+ );
453
620
  throw error;
454
621
  }
455
622
  }
@@ -458,16 +625,27 @@ export class ComprehensiveTransfer {
458
625
  * Phase 2: Transfer documents to all collections in the database
459
626
  */
460
627
  private async transferDatabaseDocuments(dbId: string): Promise<void> {
461
- MessageFormatter.info(`Transferring documents for database ${dbId}`, { prefix: "Transfer" });
628
+ MessageFormatter.info(`Transferring documents for database ${dbId}`, {
629
+ prefix: "Transfer",
630
+ });
462
631
 
463
632
  try {
464
633
  // Get all collections from source database
465
- const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
466
- MessageFormatter.info(`Transferring documents for ${sourceCollections.length} collections in database ${dbId}`, { prefix: "Transfer" });
634
+ const sourceCollections = await this.fetchAllCollections(
635
+ dbId,
636
+ this.sourceDatabases
637
+ );
638
+ MessageFormatter.info(
639
+ `Transferring documents for ${sourceCollections.length} collections in database ${dbId}`,
640
+ { prefix: "Transfer" }
641
+ );
467
642
 
468
643
  // Process each collection
469
644
  for (const collection of sourceCollections) {
470
- MessageFormatter.info(`Transferring documents for collection: ${collection.name} (${collection.$id})`, { prefix: "Transfer" });
645
+ MessageFormatter.info(
646
+ `Transferring documents for collection: ${collection.name} (${collection.$id})`,
647
+ { prefix: "Transfer" }
648
+ );
471
649
 
472
650
  try {
473
651
  // Transfer documents
@@ -479,20 +657,33 @@ export class ComprehensiveTransfer {
479
657
  collection.$id,
480
658
  collection.$id
481
659
  );
482
-
483
- MessageFormatter.success(`Documents transferred for collection ${collection.name}`, { prefix: "Transfer" });
660
+
661
+ MessageFormatter.success(
662
+ `Documents transferred for collection ${collection.name}`,
663
+ { prefix: "Transfer" }
664
+ );
484
665
  } catch (error) {
485
- MessageFormatter.error(`Error transferring documents for collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
666
+ MessageFormatter.error(
667
+ `Error transferring documents for collection ${collection.name}`,
668
+ error instanceof Error ? error : new Error(String(error)),
669
+ { prefix: "Transfer" }
670
+ );
486
671
  }
487
672
  }
488
673
  } catch (error) {
489
- MessageFormatter.error(`Failed to transfer documents for database ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
674
+ MessageFormatter.error(
675
+ `Failed to transfer documents for database ${dbId}`,
676
+ error instanceof Error ? error : new Error(String(error)),
677
+ { prefix: "Transfer" }
678
+ );
490
679
  throw error;
491
680
  }
492
681
  }
493
682
 
494
683
  private async transferAllBuckets(): Promise<void> {
495
- MessageFormatter.info("Starting bucket transfer phase", { prefix: "Transfer" });
684
+ MessageFormatter.info("Starting bucket transfer phase", {
685
+ prefix: "Transfer",
686
+ });
496
687
 
497
688
  try {
498
689
  // Get all buckets from source with pagination
@@ -502,38 +693,52 @@ export class ComprehensiveTransfer {
502
693
  if (this.options.dryRun) {
503
694
  let totalFiles = 0;
504
695
  for (const bucket of allSourceBuckets) {
505
- const files = await this.sourceStorage.listFiles(bucket.$id, [Query.limit(1)]);
696
+ const files = await this.sourceStorage.listFiles(bucket.$id, [
697
+ Query.limit(1),
698
+ ]);
506
699
  totalFiles += files.total;
507
700
  }
508
- MessageFormatter.info(`DRY RUN: Would transfer ${allSourceBuckets.length} buckets with ${totalFiles} files`, { prefix: "Transfer" });
701
+ MessageFormatter.info(
702
+ `DRY RUN: Would transfer ${allSourceBuckets.length} buckets with ${totalFiles} files`,
703
+ { prefix: "Transfer" }
704
+ );
509
705
  return;
510
706
  }
511
707
 
512
- const transferTasks = allSourceBuckets.map(bucket =>
708
+ const transferTasks = allSourceBuckets.map((bucket) =>
513
709
  this.limit(async () => {
514
710
  try {
515
711
  // Check if bucket exists in target
516
- const existingBucket = allTargetBuckets.find(tb => tb.$id === bucket.$id);
517
-
712
+ const existingBucket = allTargetBuckets.find(
713
+ (tb) => tb.$id === bucket.$id
714
+ );
715
+
518
716
  if (!existingBucket) {
519
717
  // Create bucket with fallback strategy for maximumFileSize
520
718
  await this.createBucketWithFallback(bucket);
521
- MessageFormatter.success(`Created bucket: ${bucket.name}`, { prefix: "Transfer" });
719
+ MessageFormatter.success(`Created bucket: ${bucket.name}`, {
720
+ prefix: "Transfer",
721
+ });
522
722
  } else {
523
723
  // Compare bucket permissions and update if needed
524
- const sourcePermissions = JSON.stringify(bucket.$permissions?.sort() || []);
525
- const targetPermissions = JSON.stringify(existingBucket.$permissions?.sort() || []);
526
-
527
- if (sourcePermissions !== targetPermissions ||
528
- existingBucket.name !== bucket.name ||
529
- existingBucket.fileSecurity !== bucket.fileSecurity ||
530
- existingBucket.enabled !== bucket.enabled) {
531
-
724
+ const sourcePermissions = JSON.stringify(
725
+ bucket.$permissions?.sort() || []
726
+ );
727
+ const targetPermissions = JSON.stringify(
728
+ existingBucket.$permissions?.sort() || []
729
+ );
730
+
731
+ if (
732
+ sourcePermissions !== targetPermissions ||
733
+ existingBucket.name !== bucket.name ||
734
+ existingBucket.fileSecurity !== bucket.fileSecurity ||
735
+ existingBucket.enabled !== bucket.enabled
736
+ ) {
532
737
  MessageFormatter.warning(
533
- `Bucket ${bucket.name} exists but has different settings. Updating to match source.`,
738
+ `Bucket ${bucket.name} exists but has different settings. Updating to match source.`,
534
739
  { prefix: "Transfer" }
535
740
  );
536
-
741
+
537
742
  try {
538
743
  await this.targetStorage.updateBucket(
539
744
  bucket.$id,
@@ -547,16 +752,24 @@ export class ComprehensiveTransfer {
547
752
  bucket.encryption,
548
753
  bucket.antivirus
549
754
  );
550
- MessageFormatter.success(`Updated bucket ${bucket.name} to match source`, { prefix: "Transfer" });
755
+ MessageFormatter.success(
756
+ `Updated bucket ${bucket.name} to match source`,
757
+ { prefix: "Transfer" }
758
+ );
551
759
  } catch (updateError) {
552
760
  MessageFormatter.error(
553
- `Failed to update bucket ${bucket.name}`,
554
- updateError instanceof Error ? updateError : new Error(String(updateError)),
761
+ `Failed to update bucket ${bucket.name}`,
762
+ updateError instanceof Error
763
+ ? updateError
764
+ : new Error(String(updateError)),
555
765
  { prefix: "Transfer" }
556
766
  );
557
767
  }
558
768
  } else {
559
- MessageFormatter.info(`Bucket ${bucket.name} already exists with matching settings`, { prefix: "Transfer" });
769
+ MessageFormatter.info(
770
+ `Bucket ${bucket.name} already exists with matching settings`,
771
+ { prefix: "Transfer" }
772
+ );
560
773
  }
561
774
  }
562
775
 
@@ -564,31 +777,46 @@ export class ComprehensiveTransfer {
564
777
  await this.transferBucketFiles(bucket.$id, bucket.$id);
565
778
 
566
779
  this.results.buckets.transferred++;
567
- MessageFormatter.success(`Bucket ${bucket.name} transferred successfully`, { prefix: "Transfer" });
780
+ MessageFormatter.success(
781
+ `Bucket ${bucket.name} transferred successfully`,
782
+ { prefix: "Transfer" }
783
+ );
568
784
  } catch (error) {
569
- MessageFormatter.error(`Bucket ${bucket.name} transfer failed`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
785
+ MessageFormatter.error(
786
+ `Bucket ${bucket.name} transfer failed`,
787
+ error instanceof Error ? error : new Error(String(error)),
788
+ { prefix: "Transfer" }
789
+ );
570
790
  this.results.buckets.failed++;
571
791
  }
572
792
  })
573
793
  );
574
794
 
575
795
  await Promise.all(transferTasks);
576
- MessageFormatter.success("Bucket transfer phase completed", { prefix: "Transfer" });
796
+ MessageFormatter.success("Bucket transfer phase completed", {
797
+ prefix: "Transfer",
798
+ });
577
799
  } catch (error) {
578
- MessageFormatter.error("Bucket transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
800
+ MessageFormatter.error(
801
+ "Bucket transfer phase failed",
802
+ error instanceof Error ? error : new Error(String(error)),
803
+ { prefix: "Transfer" }
804
+ );
579
805
  }
580
806
  }
581
807
 
582
808
  private async createBucketWithFallback(bucket: Models.Bucket): Promise<void> {
583
809
  // Determine the optimal size to try first
584
810
  let sizeToTry: number;
585
-
811
+
586
812
  if (this.cachedMaxFileSize) {
587
813
  // Use cached size if it's smaller than or equal to the bucket's original size
588
814
  if (bucket.maximumFileSize >= this.cachedMaxFileSize) {
589
815
  sizeToTry = this.cachedMaxFileSize;
590
816
  MessageFormatter.info(
591
- `Bucket ${bucket.name}: Using cached maximumFileSize ${sizeToTry} (${(sizeToTry / 1_000_000_000).toFixed(1)}GB)`,
817
+ `Bucket ${bucket.name}: Using cached maximumFileSize ${sizeToTry} (${(
818
+ sizeToTry / 1_000_000_000
819
+ ).toFixed(1)}GB)`,
592
820
  { prefix: "Transfer" }
593
821
  );
594
822
  } else {
@@ -614,30 +842,41 @@ export class ComprehensiveTransfer {
614
842
  bucket.encryption,
615
843
  bucket.antivirus
616
844
  );
617
-
845
+
618
846
  // Success - cache this size if it's not already cached or is smaller than cached
619
847
  if (!this.cachedMaxFileSize || sizeToTry < this.cachedMaxFileSize) {
620
848
  this.cachedMaxFileSize = sizeToTry;
621
849
  MessageFormatter.info(
622
- `Bucket ${bucket.name}: Cached successful maximumFileSize ${sizeToTry} (${(sizeToTry / 1_000_000_000).toFixed(1)}GB)`,
850
+ `Bucket ${
851
+ bucket.name
852
+ }: Cached successful maximumFileSize ${sizeToTry} (${(
853
+ sizeToTry / 1_000_000_000
854
+ ).toFixed(1)}GB)`,
623
855
  { prefix: "Transfer" }
624
856
  );
625
857
  }
626
-
858
+
627
859
  // Log if we used a different size than original
628
860
  if (sizeToTry !== bucket.maximumFileSize) {
629
861
  MessageFormatter.warning(
630
- `Bucket ${bucket.name}: maximumFileSize used ${sizeToTry} instead of original ${bucket.maximumFileSize} (${(sizeToTry / 1_000_000_000).toFixed(1)}GB)`,
862
+ `Bucket ${
863
+ bucket.name
864
+ }: maximumFileSize used ${sizeToTry} instead of original ${
865
+ bucket.maximumFileSize
866
+ } (${(sizeToTry / 1_000_000_000).toFixed(1)}GB)`,
631
867
  { prefix: "Transfer" }
632
868
  );
633
869
  }
634
-
870
+
635
871
  return; // Success, exit the function
636
872
  } catch (error) {
637
873
  const err = error instanceof Error ? error : new Error(String(error));
638
-
874
+
639
875
  // Check if the error is related to maximumFileSize validation
640
- if (err.message.includes('maximumFileSize') || err.message.includes('valid range')) {
876
+ if (
877
+ err.message.includes("maximumFileSize") ||
878
+ err.message.includes("valid range")
879
+ ) {
641
880
  MessageFormatter.warning(
642
881
  `Bucket ${bucket.name}: Failed with maximumFileSize ${sizeToTry}, falling back to smaller sizes...`,
643
882
  { prefix: "Transfer" }
@@ -655,17 +894,17 @@ export class ComprehensiveTransfer {
655
894
  2_500_000_000, // 2.5GB
656
895
  2_000_000_000, // 2GB
657
896
  1_000_000_000, // 1GB
658
- 500_000_000, // 500MB
659
- 100_000_000 // 100MB
897
+ 500_000_000, // 500MB
898
+ 100_000_000, // 100MB
660
899
  ];
661
900
 
662
901
  // Remove sizes that are larger than or equal to the already-tried size
663
902
  const validSizes = fallbackSizes
664
- .filter(size => size < sizeToTry)
903
+ .filter((size) => size < sizeToTry)
665
904
  .sort((a, b) => b - a); // Sort descending
666
905
 
667
906
  let lastError: Error | null = null;
668
-
907
+
669
908
  for (const fileSize of validSizes) {
670
909
  try {
671
910
  await this.targetStorage.createBucket(
@@ -680,30 +919,39 @@ export class ComprehensiveTransfer {
680
919
  bucket.encryption,
681
920
  bucket.antivirus
682
921
  );
683
-
922
+
684
923
  // Success - cache this size if it's not already cached or is smaller than cached
685
924
  if (!this.cachedMaxFileSize || fileSize < this.cachedMaxFileSize) {
686
925
  this.cachedMaxFileSize = fileSize;
687
926
  MessageFormatter.info(
688
- `Bucket ${bucket.name}: Cached successful maximumFileSize ${fileSize} (${(fileSize / 1_000_000_000).toFixed(1)}GB)`,
927
+ `Bucket ${
928
+ bucket.name
929
+ }: Cached successful maximumFileSize ${fileSize} (${(
930
+ fileSize / 1_000_000_000
931
+ ).toFixed(1)}GB)`,
689
932
  { prefix: "Transfer" }
690
933
  );
691
934
  }
692
-
935
+
693
936
  // Log if we had to reduce the file size
694
937
  if (fileSize !== bucket.maximumFileSize) {
695
938
  MessageFormatter.warning(
696
- `Bucket ${bucket.name}: maximumFileSize reduced from ${bucket.maximumFileSize} to ${fileSize} (${(fileSize / 1_000_000_000).toFixed(1)}GB)`,
939
+ `Bucket ${bucket.name}: maximumFileSize reduced from ${
940
+ bucket.maximumFileSize
941
+ } to ${fileSize} (${(fileSize / 1_000_000_000).toFixed(1)}GB)`,
697
942
  { prefix: "Transfer" }
698
943
  );
699
944
  }
700
-
945
+
701
946
  return; // Success, exit the function
702
947
  } catch (error) {
703
948
  lastError = error instanceof Error ? error : new Error(String(error));
704
-
949
+
705
950
  // Check if the error is related to maximumFileSize validation
706
- if (lastError.message.includes('maximumFileSize') || lastError.message.includes('valid range')) {
951
+ if (
952
+ lastError.message.includes("maximumFileSize") ||
953
+ lastError.message.includes("valid range")
954
+ ) {
707
955
  MessageFormatter.warning(
708
956
  `Bucket ${bucket.name}: Failed with maximumFileSize ${fileSize}, trying smaller size...`,
709
957
  { prefix: "Transfer" }
@@ -715,17 +963,20 @@ export class ComprehensiveTransfer {
715
963
  }
716
964
  }
717
965
  }
718
-
966
+
719
967
  // If we get here, all fallback sizes failed
720
968
  MessageFormatter.error(
721
969
  `Bucket ${bucket.name}: All fallback file sizes failed. Last error: ${lastError?.message}`,
722
970
  lastError || undefined,
723
971
  { prefix: "Transfer" }
724
972
  );
725
- throw lastError || new Error('All fallback file sizes failed');
973
+ throw lastError || new Error("All fallback file sizes failed");
726
974
  }
727
975
 
728
- private async transferBucketFiles(sourceBucketId: string, targetBucketId: string): Promise<void> {
976
+ private async transferBucketFiles(
977
+ sourceBucketId: string,
978
+ targetBucketId: string
979
+ ): Promise<void> {
729
980
  let lastFileId: string | undefined;
730
981
  let transferredFiles = 0;
731
982
 
@@ -739,24 +990,31 @@ export class ComprehensiveTransfer {
739
990
  if (files.files.length === 0) break;
740
991
 
741
992
  // Process files with rate limiting
742
- const fileTasks = files.files.map(file =>
993
+ const fileTasks = files.files.map((file) =>
743
994
  this.fileLimit(async () => {
744
995
  try {
745
996
  // Check if file already exists and compare permissions
746
997
  let existingFile: Models.File | null = null;
747
998
  try {
748
- existingFile = await this.targetStorage.getFile(targetBucketId, file.$id);
749
-
999
+ existingFile = await this.targetStorage.getFile(
1000
+ targetBucketId,
1001
+ file.$id
1002
+ );
1003
+
750
1004
  // Compare permissions between source and target file
751
- const sourcePermissions = JSON.stringify(file.$permissions?.sort() || []);
752
- const targetPermissions = JSON.stringify(existingFile.$permissions?.sort() || []);
753
-
1005
+ const sourcePermissions = JSON.stringify(
1006
+ file.$permissions?.sort() || []
1007
+ );
1008
+ const targetPermissions = JSON.stringify(
1009
+ existingFile.$permissions?.sort() || []
1010
+ );
1011
+
754
1012
  if (sourcePermissions !== targetPermissions) {
755
1013
  MessageFormatter.warning(
756
- `File ${file.name} (${file.$id}) exists but has different permissions. Source: ${sourcePermissions}, Target: ${targetPermissions}`,
1014
+ `File ${file.name} (${file.$id}) exists but has different permissions. Source: ${sourcePermissions}, Target: ${targetPermissions}`,
757
1015
  { prefix: "Transfer" }
758
1016
  );
759
-
1017
+
760
1018
  // Update file permissions to match source
761
1019
  try {
762
1020
  await this.targetStorage.updateFile(
@@ -765,16 +1023,24 @@ export class ComprehensiveTransfer {
765
1023
  file.name,
766
1024
  file.$permissions
767
1025
  );
768
- MessageFormatter.success(`Updated file ${file.name} permissions to match source`, { prefix: "Transfer" });
1026
+ MessageFormatter.success(
1027
+ `Updated file ${file.name} permissions to match source`,
1028
+ { prefix: "Transfer" }
1029
+ );
769
1030
  } catch (updateError) {
770
1031
  MessageFormatter.error(
771
- `Failed to update permissions for file ${file.name}`,
772
- updateError instanceof Error ? updateError : new Error(String(updateError)),
1032
+ `Failed to update permissions for file ${file.name}`,
1033
+ updateError instanceof Error
1034
+ ? updateError
1035
+ : new Error(String(updateError)),
773
1036
  { prefix: "Transfer" }
774
1037
  );
775
1038
  }
776
1039
  } else {
777
- MessageFormatter.info(`File ${file.name} already exists with matching permissions, skipping`, { prefix: "Transfer" });
1040
+ MessageFormatter.info(
1041
+ `File ${file.name} already exists with matching permissions, skipping`,
1042
+ { prefix: "Transfer" }
1043
+ );
778
1044
  }
779
1045
  return;
780
1046
  } catch (error) {
@@ -782,9 +1048,15 @@ export class ComprehensiveTransfer {
782
1048
  }
783
1049
 
784
1050
  // Download file with validation
785
- const fileData = await this.validateAndDownloadFile(sourceBucketId, file.$id);
1051
+ const fileData = await this.validateAndDownloadFile(
1052
+ sourceBucketId,
1053
+ file.$id
1054
+ );
786
1055
  if (!fileData) {
787
- MessageFormatter.warning(`File ${file.name} failed validation, skipping`, { prefix: "Transfer" });
1056
+ MessageFormatter.warning(
1057
+ `File ${file.name} failed validation, skipping`,
1058
+ { prefix: "Transfer" }
1059
+ );
788
1060
  return;
789
1061
  }
790
1062
 
@@ -802,9 +1074,15 @@ export class ComprehensiveTransfer {
802
1074
  );
803
1075
 
804
1076
  transferredFiles++;
805
- MessageFormatter.success(`Transferred file: ${file.name}`, { prefix: "Transfer" });
1077
+ MessageFormatter.success(`Transferred file: ${file.name}`, {
1078
+ prefix: "Transfer",
1079
+ });
806
1080
  } catch (error) {
807
- MessageFormatter.error(`Failed to transfer file ${file.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1081
+ MessageFormatter.error(
1082
+ `Failed to transfer file ${file.name}`,
1083
+ error instanceof Error ? error : new Error(String(error)),
1084
+ { prefix: "Transfer" }
1085
+ );
808
1086
  }
809
1087
  })
810
1088
  );
@@ -815,61 +1093,99 @@ export class ComprehensiveTransfer {
815
1093
  lastFileId = files.files[files.files.length - 1].$id;
816
1094
  }
817
1095
 
818
- MessageFormatter.info(`Transferred ${transferredFiles} files from bucket ${sourceBucketId}`, { prefix: "Transfer" });
1096
+ MessageFormatter.info(
1097
+ `Transferred ${transferredFiles} files from bucket ${sourceBucketId}`,
1098
+ { prefix: "Transfer" }
1099
+ );
819
1100
  }
820
1101
 
821
- private async validateAndDownloadFile(bucketId: string, fileId: string): Promise<ArrayBuffer | null> {
1102
+ private async validateAndDownloadFile(
1103
+ bucketId: string,
1104
+ fileId: string
1105
+ ): Promise<ArrayBuffer | null> {
822
1106
  let attempts = 3;
823
1107
  while (attempts > 0) {
824
1108
  try {
825
- const fileData = await this.sourceStorage.getFileDownload(bucketId, fileId);
826
-
1109
+ const fileData = await this.sourceStorage.getFileDownload(
1110
+ bucketId,
1111
+ fileId
1112
+ );
1113
+
827
1114
  // Basic validation - ensure file is not empty and not too large
828
1115
  if (fileData.byteLength === 0) {
829
- MessageFormatter.warning(`File ${fileId} is empty`, { prefix: "Transfer" });
1116
+ MessageFormatter.warning(`File ${fileId} is empty`, {
1117
+ prefix: "Transfer",
1118
+ });
830
1119
  return null;
831
1120
  }
832
1121
 
833
- if (fileData.byteLength > 50 * 1024 * 1024) { // 50MB limit
834
- MessageFormatter.warning(`File ${fileId} is too large (${fileData.byteLength} bytes)`, { prefix: "Transfer" });
1122
+ if (fileData.byteLength > 50 * 1024 * 1024) {
1123
+ // 50MB limit
1124
+ MessageFormatter.warning(
1125
+ `File ${fileId} is too large (${fileData.byteLength} bytes)`,
1126
+ { prefix: "Transfer" }
1127
+ );
835
1128
  return null;
836
1129
  }
837
1130
 
838
1131
  return fileData;
839
1132
  } catch (error) {
840
1133
  attempts--;
841
- MessageFormatter.warning(`Error downloading file ${fileId}, attempts left: ${attempts}`, { prefix: "Transfer" });
1134
+ MessageFormatter.warning(
1135
+ `Error downloading file ${fileId}, attempts left: ${attempts}`,
1136
+ { prefix: "Transfer" }
1137
+ );
842
1138
  if (attempts === 0) {
843
- MessageFormatter.error(`Failed to download file ${fileId} after all attempts`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1139
+ MessageFormatter.error(
1140
+ `Failed to download file ${fileId} after all attempts`,
1141
+ error instanceof Error ? error : new Error(String(error)),
1142
+ { prefix: "Transfer" }
1143
+ );
844
1144
  return null;
845
1145
  }
846
1146
  // Wait before retry
847
- await new Promise(resolve => setTimeout(resolve, 1000 * (4 - attempts)));
1147
+ await new Promise((resolve) =>
1148
+ setTimeout(resolve, 1000 * (4 - attempts))
1149
+ );
848
1150
  }
849
1151
  }
850
1152
  return null;
851
1153
  }
852
1154
 
853
1155
  private async transferAllFunctions(): Promise<void> {
854
- MessageFormatter.info("Starting function transfer phase", { prefix: "Transfer" });
1156
+ MessageFormatter.info("Starting function transfer phase", {
1157
+ prefix: "Transfer",
1158
+ });
855
1159
 
856
1160
  try {
857
- const sourceFunctions = await listFunctions(this.sourceClient, [Query.limit(1000)]);
858
- const targetFunctions = await listFunctions(this.targetClient, [Query.limit(1000)]);
1161
+ const sourceFunctions = await listFunctions(this.sourceClient, [
1162
+ Query.limit(1000),
1163
+ ]);
1164
+ const targetFunctions = await listFunctions(this.targetClient, [
1165
+ Query.limit(1000),
1166
+ ]);
859
1167
 
860
1168
  if (this.options.dryRun) {
861
- MessageFormatter.info(`DRY RUN: Would transfer ${sourceFunctions.functions.length} functions`, { prefix: "Transfer" });
1169
+ MessageFormatter.info(
1170
+ `DRY RUN: Would transfer ${sourceFunctions.functions.length} functions`,
1171
+ { prefix: "Transfer" }
1172
+ );
862
1173
  return;
863
1174
  }
864
1175
 
865
- const transferTasks = sourceFunctions.functions.map(func =>
1176
+ const transferTasks = sourceFunctions.functions.map((func) =>
866
1177
  this.limit(async () => {
867
1178
  try {
868
1179
  // Check if function exists in target
869
- const existingFunc = targetFunctions.functions.find(tf => tf.$id === func.$id);
870
-
1180
+ const existingFunc = targetFunctions.functions.find(
1181
+ (tf) => tf.$id === func.$id
1182
+ );
1183
+
871
1184
  if (existingFunc) {
872
- MessageFormatter.info(`Function ${func.name} already exists, skipping creation`, { prefix: "Transfer" });
1185
+ MessageFormatter.info(
1186
+ `Function ${func.name} already exists, skipping creation`,
1187
+ { prefix: "Transfer" }
1188
+ );
873
1189
  this.results.functions.skipped++;
874
1190
  return;
875
1191
  }
@@ -877,7 +1193,11 @@ export class ComprehensiveTransfer {
877
1193
  // Download function from source
878
1194
  const functionPath = await this.downloadFunction(func);
879
1195
  if (!functionPath) {
880
- MessageFormatter.error(`Failed to download function ${func.name}`, undefined, { prefix: "Transfer" });
1196
+ MessageFormatter.error(
1197
+ `Failed to download function ${func.name}`,
1198
+ undefined,
1199
+ { prefix: "Transfer" }
1200
+ );
881
1201
  this.results.functions.failed++;
882
1202
  return;
883
1203
  }
@@ -904,26 +1224,45 @@ export class ComprehensiveTransfer {
904
1224
  specification: func.specification as any,
905
1225
  dirPath: functionPath,
906
1226
  };
907
-
908
- await deployLocalFunction(this.targetClient, func.name, functionConfig);
1227
+
1228
+ await deployLocalFunction(
1229
+ this.targetClient,
1230
+ func.name,
1231
+ functionConfig
1232
+ );
909
1233
 
910
1234
  this.results.functions.transferred++;
911
- MessageFormatter.success(`Function ${func.name} transferred successfully`, { prefix: "Transfer" });
1235
+ MessageFormatter.success(
1236
+ `Function ${func.name} transferred successfully`,
1237
+ { prefix: "Transfer" }
1238
+ );
912
1239
  } catch (error) {
913
- MessageFormatter.error(`Function ${func.name} transfer failed`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1240
+ MessageFormatter.error(
1241
+ `Function ${func.name} transfer failed`,
1242
+ error instanceof Error ? error : new Error(String(error)),
1243
+ { prefix: "Transfer" }
1244
+ );
914
1245
  this.results.functions.failed++;
915
1246
  }
916
1247
  })
917
1248
  );
918
1249
 
919
1250
  await Promise.all(transferTasks);
920
- MessageFormatter.success("Function transfer phase completed", { prefix: "Transfer" });
1251
+ MessageFormatter.success("Function transfer phase completed", {
1252
+ prefix: "Transfer",
1253
+ });
921
1254
  } catch (error) {
922
- MessageFormatter.error("Function transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1255
+ MessageFormatter.error(
1256
+ "Function transfer phase failed",
1257
+ error instanceof Error ? error : new Error(String(error)),
1258
+ { prefix: "Transfer" }
1259
+ );
923
1260
  }
924
1261
  }
925
1262
 
926
- private async downloadFunction(func: Models.Function): Promise<string | null> {
1263
+ private async downloadFunction(
1264
+ func: Models.Function
1265
+ ): Promise<string | null> {
927
1266
  try {
928
1267
  const { path } = await downloadLatestFunctionDeployment(
929
1268
  this.sourceClient,
@@ -932,7 +1271,11 @@ export class ComprehensiveTransfer {
932
1271
  );
933
1272
  return path;
934
1273
  } catch (error) {
935
- MessageFormatter.error(`Failed to download function ${func.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1274
+ MessageFormatter.error(
1275
+ `Failed to download function ${func.name}`,
1276
+ error instanceof Error ? error : new Error(String(error)),
1277
+ { prefix: "Transfer" }
1278
+ );
936
1279
  return null;
937
1280
  }
938
1281
  }
@@ -940,7 +1283,10 @@ export class ComprehensiveTransfer {
940
1283
  /**
941
1284
  * Helper method to fetch all collections from a database
942
1285
  */
943
- private async fetchAllCollections(dbId: string, databases: Databases): Promise<Models.Collection[]> {
1286
+ private async fetchAllCollections(
1287
+ dbId: string,
1288
+ databases: Databases
1289
+ ): Promise<Models.Collection[]> {
944
1290
  const collections: Models.Collection[] = [];
945
1291
  let lastId: string | undefined;
946
1292
 
@@ -950,18 +1296,20 @@ export class ComprehensiveTransfer {
950
1296
  queries.push(Query.cursorAfter(lastId));
951
1297
  }
952
1298
 
953
- const result = await tryAwaitWithRetry(async () => databases.listCollections(dbId, queries));
954
-
1299
+ const result = await tryAwaitWithRetry(async () =>
1300
+ databases.listCollections(dbId, queries)
1301
+ );
1302
+
955
1303
  if (result.collections.length === 0) {
956
1304
  break;
957
1305
  }
958
1306
 
959
1307
  collections.push(...result.collections);
960
-
1308
+
961
1309
  if (result.collections.length < 100) {
962
1310
  break;
963
1311
  }
964
-
1312
+
965
1313
  lastId = result.collections[result.collections.length - 1].$id;
966
1314
  }
967
1315
 
@@ -981,18 +1329,20 @@ export class ComprehensiveTransfer {
981
1329
  queries.push(Query.cursorAfter(lastId));
982
1330
  }
983
1331
 
984
- const result = await tryAwaitWithRetry(async () => storage.listBuckets(queries));
985
-
1332
+ const result = await tryAwaitWithRetry(async () =>
1333
+ storage.listBuckets(queries)
1334
+ );
1335
+
986
1336
  if (result.buckets.length === 0) {
987
1337
  break;
988
1338
  }
989
1339
 
990
1340
  buckets.push(...result.buckets);
991
-
1341
+
992
1342
  if (result.buckets.length < 100) {
993
1343
  break;
994
1344
  }
995
-
1345
+
996
1346
  lastId = result.buckets[result.buckets.length - 1].$id;
997
1347
  }
998
1348
 
@@ -1020,7 +1370,7 @@ export class ComprehensiveTransfer {
1020
1370
  twoWay: attr.twoWay,
1021
1371
  twoWayKey: attr.twoWayKey,
1022
1372
  onDelete: attr.onDelete,
1023
- side: attr.side
1373
+ side: attr.side,
1024
1374
  };
1025
1375
  }
1026
1376
 
@@ -1034,8 +1384,10 @@ export class ComprehensiveTransfer {
1034
1384
  attributes: any[]
1035
1385
  ): Promise<boolean> {
1036
1386
  // Import the enhanced attribute creation function
1037
- const { createUpdateCollectionAttributesWithStatusCheck } = await import("../collections/attributes.js");
1038
-
1387
+ const { createUpdateCollectionAttributesWithStatusCheck } = await import(
1388
+ "../collections/attributes.js"
1389
+ );
1390
+
1039
1391
  return await createUpdateCollectionAttributesWithStatusCheck(
1040
1392
  databases,
1041
1393
  dbId,
@@ -1055,8 +1407,10 @@ export class ComprehensiveTransfer {
1055
1407
  indexes: any[]
1056
1408
  ): Promise<boolean> {
1057
1409
  // Import the enhanced index creation function
1058
- const { createOrUpdateIndexesWithStatusCheck } = await import("../collections/indexes.js");
1059
-
1410
+ const { createOrUpdateIndexesWithStatusCheck } = await import(
1411
+ "../collections/indexes.js"
1412
+ );
1413
+
1060
1414
  return await createOrUpdateIndexesWithStatusCheck(
1061
1415
  dbId,
1062
1416
  databases,
@@ -1077,7 +1431,10 @@ export class ComprehensiveTransfer {
1077
1431
  sourceCollectionId: string,
1078
1432
  targetCollectionId: string
1079
1433
  ): Promise<void> {
1080
- MessageFormatter.info(`Transferring documents from ${sourceCollectionId} to ${targetCollectionId} with bulk operations, content comparison, and permission filtering`, { prefix: "Transfer" });
1434
+ MessageFormatter.info(
1435
+ `Transferring documents from ${sourceCollectionId} to ${targetCollectionId} with bulk operations, content comparison, and permission filtering`,
1436
+ { prefix: "Transfer" }
1437
+ );
1081
1438
 
1082
1439
  let lastId: string | undefined;
1083
1440
  let totalTransferred = 0;
@@ -1085,11 +1442,14 @@ export class ComprehensiveTransfer {
1085
1442
  let totalUpdated = 0;
1086
1443
 
1087
1444
  // Check if bulk operations are supported
1088
- const supportsBulk = this.options.sourceEndpoint.includes('cloud.appwrite.io') ||
1089
- this.options.targetEndpoint.includes('cloud.appwrite.io');
1090
-
1445
+ const supportsBulk =
1446
+ this.options.sourceEndpoint.includes("cloud.appwrite.io") ||
1447
+ this.options.targetEndpoint.includes("cloud.appwrite.io");
1448
+
1091
1449
  if (supportsBulk) {
1092
- MessageFormatter.info(`Using bulk operations for enhanced performance`, { prefix: "Transfer" });
1450
+ MessageFormatter.info(`Using bulk operations for enhanced performance`, {
1451
+ prefix: "Transfer",
1452
+ });
1093
1453
  }
1094
1454
 
1095
1455
  while (true) {
@@ -1099,7 +1459,7 @@ export class ComprehensiveTransfer {
1099
1459
  queries.push(Query.cursorAfter(lastId));
1100
1460
  }
1101
1461
 
1102
- const sourceDocuments = await tryAwaitWithRetry(async () =>
1462
+ const sourceDocuments = await tryAwaitWithRetry(async () =>
1103
1463
  sourceDb.listDocuments(sourceDbId, sourceCollectionId, queries)
1104
1464
  );
1105
1465
 
@@ -1107,87 +1467,90 @@ export class ComprehensiveTransfer {
1107
1467
  break;
1108
1468
  }
1109
1469
 
1110
- MessageFormatter.info(`Processing batch of ${sourceDocuments.documents.length} source documents`, { prefix: "Transfer" });
1470
+ MessageFormatter.info(
1471
+ `Processing batch of ${sourceDocuments.documents.length} source documents`,
1472
+ { prefix: "Transfer" }
1473
+ );
1111
1474
 
1112
1475
  // Extract document IDs from the current batch
1113
- const sourceDocIds = sourceDocuments.documents.map(doc => doc.$id);
1114
-
1476
+ const sourceDocIds = sourceDocuments.documents.map((doc) => doc.$id);
1477
+
1115
1478
  // Fetch existing documents from target in a single query
1116
1479
  const existingTargetDocs = await this.fetchTargetDocumentsBatch(
1117
- targetDb,
1118
- targetDbId,
1119
- targetCollectionId,
1480
+ targetDb,
1481
+ targetDbId,
1482
+ targetCollectionId,
1120
1483
  sourceDocIds
1121
1484
  );
1122
1485
 
1123
1486
  // Create a map for quick lookup of existing documents
1124
1487
  const existingDocsMap = new Map<string, Models.Document>();
1125
- existingTargetDocs.forEach(doc => {
1488
+ existingTargetDocs.forEach((doc) => {
1126
1489
  existingDocsMap.set(doc.$id, doc);
1127
1490
  });
1128
1491
 
1129
1492
  // Filter documents based on existence, content comparison, and permission comparison
1130
1493
  const documentsToTransfer: Models.Document[] = [];
1131
- const documentsToUpdate: { doc: Models.Document; targetDoc: Models.Document; reason: string }[] = [];
1494
+ const documentsToUpdate: {
1495
+ doc: Models.Document;
1496
+ targetDoc: Models.Document;
1497
+ reason: string;
1498
+ }[] = [];
1132
1499
 
1133
1500
  for (const sourceDoc of sourceDocuments.documents) {
1134
1501
  const existingTargetDoc = existingDocsMap.get(sourceDoc.$id);
1135
-
1502
+
1136
1503
  if (!existingTargetDoc) {
1137
1504
  // Document doesn't exist in target, needs to be transferred
1138
1505
  documentsToTransfer.push(sourceDoc);
1139
1506
  } else {
1140
1507
  // Document exists, compare both content and permissions
1141
- const sourcePermissions = JSON.stringify((sourceDoc.$permissions || []).sort());
1142
- const targetPermissions = JSON.stringify((existingTargetDoc.$permissions || []).sort());
1143
- const permissionsDiffer = sourcePermissions !== targetPermissions;
1144
-
1508
+ const sourcePermissions = Array.from(
1509
+ new Set(sourceDoc.$permissions || [])
1510
+ ).sort();
1511
+ const targetPermissions = Array.from(
1512
+ new Set(existingTargetDoc.$permissions || [])
1513
+ ).sort();
1514
+ const permissionsDiffer =
1515
+ sourcePermissions.join(",") !== targetPermissions.join(",") ||
1516
+ sourcePermissions.length !== targetPermissions.length;
1517
+
1145
1518
  // Use objectNeedsUpdate to compare document content (excluding system fields)
1146
- const contentDiffers = objectNeedsUpdate(existingTargetDoc, sourceDoc);
1147
-
1519
+ const contentDiffers = objectNeedsUpdate(
1520
+ existingTargetDoc,
1521
+ sourceDoc
1522
+ );
1523
+
1148
1524
  if (contentDiffers && permissionsDiffer) {
1149
1525
  // Both content and permissions differ
1150
- documentsToUpdate.push({
1151
- doc: sourceDoc,
1152
- targetDoc: existingTargetDoc,
1153
- reason: "content and permissions differ"
1526
+ documentsToUpdate.push({
1527
+ doc: sourceDoc,
1528
+ targetDoc: existingTargetDoc,
1529
+ reason: "content and permissions differ",
1154
1530
  });
1155
- MessageFormatter.info(
1156
- `Document ${sourceDoc.$id} exists but content and permissions differ - will update`,
1157
- { prefix: "Transfer" }
1158
- );
1159
1531
  } else if (contentDiffers) {
1160
1532
  // Only content differs
1161
- documentsToUpdate.push({
1162
- doc: sourceDoc,
1163
- targetDoc: existingTargetDoc,
1164
- reason: "content differs"
1533
+ documentsToUpdate.push({
1534
+ doc: sourceDoc,
1535
+ targetDoc: existingTargetDoc,
1536
+ reason: "content differs",
1165
1537
  });
1166
- MessageFormatter.info(
1167
- `Document ${sourceDoc.$id} exists but content differs - will update`,
1168
- { prefix: "Transfer" }
1169
- );
1170
1538
  } else if (permissionsDiffer) {
1171
1539
  // Only permissions differ
1172
- documentsToUpdate.push({
1173
- doc: sourceDoc,
1174
- targetDoc: existingTargetDoc,
1175
- reason: "permissions differ"
1540
+ documentsToUpdate.push({
1541
+ doc: sourceDoc,
1542
+ targetDoc: existingTargetDoc,
1543
+ reason: "permissions differ",
1176
1544
  });
1177
- MessageFormatter.info(
1178
- `Document ${sourceDoc.$id} exists but permissions differ - will update`,
1179
- { prefix: "Transfer" }
1180
- );
1181
1545
  } else {
1182
1546
  // Document exists with identical content AND permissions, skip
1183
1547
  totalSkipped++;
1184
- MessageFormatter.info(`Document ${sourceDoc.$id} exists with matching content and permissions - skipping`, { prefix: "Transfer" });
1185
1548
  }
1186
1549
  }
1187
1550
  }
1188
1551
 
1189
1552
  MessageFormatter.info(
1190
- `Batch analysis: ${documentsToTransfer.length} to create, ${documentsToUpdate.length} to update, ${totalSkipped} skipped so far`,
1553
+ `Batch analysis: ${documentsToTransfer.length} to create, ${documentsToUpdate.length} to update, ${totalSkipped} skipped so far`,
1191
1554
  { prefix: "Transfer" }
1192
1555
  );
1193
1556
 
@@ -1202,7 +1565,6 @@ export class ComprehensiveTransfer {
1202
1565
  documentsToTransfer
1203
1566
  );
1204
1567
  totalTransferred += documentsToTransfer.length;
1205
- MessageFormatter.success(`Bulk transferred ${documentsToTransfer.length} new documents`, { prefix: "Transfer" });
1206
1568
  } else {
1207
1569
  // Use individual transfers for smaller batches or non-bulk endpoints
1208
1570
  const transferCount = await this.transferDocumentsIndividual(
@@ -1230,11 +1592,12 @@ export class ComprehensiveTransfer {
1230
1592
  break;
1231
1593
  }
1232
1594
 
1233
- lastId = sourceDocuments.documents[sourceDocuments.documents.length - 1].$id;
1595
+ lastId =
1596
+ sourceDocuments.documents[sourceDocuments.documents.length - 1].$id;
1234
1597
  }
1235
1598
 
1236
1599
  MessageFormatter.info(
1237
- `Transfer complete: ${totalTransferred} new, ${totalUpdated} updated, ${totalSkipped} skipped from ${sourceCollectionId} to ${targetCollectionId}`,
1600
+ `Transfer complete: ${totalTransferred} new, ${totalUpdated} updated, ${totalSkipped} skipped from ${sourceCollectionId} to ${targetCollectionId}`,
1238
1601
  { prefix: "Transfer" }
1239
1602
  );
1240
1603
  }
@@ -1249,29 +1612,33 @@ export class ComprehensiveTransfer {
1249
1612
  docIds: string[]
1250
1613
  ): Promise<Models.Document[]> {
1251
1614
  const documents: Models.Document[] = [];
1252
-
1615
+
1253
1616
  // Split IDs into chunks of 100 for Query.equal limitations
1254
1617
  const idChunks = this.chunkArray(docIds, 100);
1255
-
1618
+
1256
1619
  for (const chunk of idChunks) {
1257
1620
  try {
1258
- const result = await tryAwaitWithRetry(async () =>
1621
+ const result = await tryAwaitWithRetry(async () =>
1259
1622
  targetDb.listDocuments(targetDbId, targetCollectionId, [
1260
- Query.equal('$id', chunk),
1261
- Query.limit(100)
1623
+ Query.equal("$id", chunk),
1624
+ Query.limit(100),
1262
1625
  ])
1263
1626
  );
1264
1627
  documents.push(...result.documents);
1265
1628
  } catch (error) {
1266
1629
  // If query fails, fall back to individual gets (less efficient but more reliable)
1267
1630
  MessageFormatter.warning(
1268
- `Batch query failed for ${chunk.length} documents, falling back to individual checks`,
1631
+ `Batch query failed for ${chunk.length} documents, falling back to individual checks`,
1269
1632
  { prefix: "Transfer" }
1270
1633
  );
1271
-
1634
+
1272
1635
  for (const docId of chunk) {
1273
1636
  try {
1274
- const doc = await targetDb.getDocument(targetDbId, targetCollectionId, docId);
1637
+ const doc = await targetDb.getDocument(
1638
+ targetDbId,
1639
+ targetCollectionId,
1640
+ docId
1641
+ );
1275
1642
  documents.push(doc);
1276
1643
  } catch (getError) {
1277
1644
  // Document doesn't exist, which is fine
@@ -1279,7 +1646,7 @@ export class ComprehensiveTransfer {
1279
1646
  }
1280
1647
  }
1281
1648
  }
1282
-
1649
+
1283
1650
  return documents;
1284
1651
  }
1285
1652
 
@@ -1293,12 +1660,20 @@ export class ComprehensiveTransfer {
1293
1660
  documents: Models.Document[]
1294
1661
  ): Promise<void> {
1295
1662
  // Prepare documents for bulk upsert
1296
- const preparedDocs = documents.map(doc => {
1297
- const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
1298
- return {
1663
+ const preparedDocs = documents.map((doc) => {
1664
+ const {
1299
1665
  $id,
1666
+ $createdAt,
1667
+ $updatedAt,
1300
1668
  $permissions,
1669
+ $databaseId,
1670
+ $collectionId,
1301
1671
  ...docData
1672
+ } = doc;
1673
+ return {
1674
+ $id,
1675
+ $permissions,
1676
+ ...docData,
1302
1677
  };
1303
1678
  });
1304
1679
 
@@ -1308,31 +1683,27 @@ export class ComprehensiveTransfer {
1308
1683
 
1309
1684
  for (const maxBatchSize of batchSizes) {
1310
1685
  const documentBatches = this.chunkArray(preparedDocs, maxBatchSize);
1311
-
1686
+
1312
1687
  try {
1313
1688
  for (const batch of documentBatches) {
1314
- MessageFormatter.info(`Bulk upserting ${batch.length} documents...`, { prefix: "Transfer" });
1315
-
1316
1689
  await this.bulkUpsertDocuments(
1317
1690
  this.targetClient,
1318
1691
  targetDbId,
1319
1692
  targetCollectionId,
1320
1693
  batch
1321
1694
  );
1322
-
1323
- MessageFormatter.success(`✅ Bulk upserted ${batch.length} documents`, { prefix: "Transfer" });
1324
-
1325
- // Add delay between batches to respect rate limits
1326
- if (documentBatches.indexOf(batch) < documentBatches.length - 1) {
1327
- await new Promise(resolve => setTimeout(resolve, 200));
1328
- }
1695
+
1696
+ MessageFormatter.success(
1697
+ `✅ Bulk upserted ${batch.length} documents`,
1698
+ { prefix: "Transfer" }
1699
+ );
1329
1700
  }
1330
-
1701
+
1331
1702
  processed = true;
1332
1703
  break; // Success, exit batch size loop
1333
1704
  } catch (error) {
1334
1705
  MessageFormatter.warning(
1335
- `Bulk upsert with batch size ${maxBatchSize} failed, trying smaller size...`,
1706
+ `Bulk upsert with batch size ${maxBatchSize} failed, trying smaller size...`,
1336
1707
  { prefix: "Transfer" }
1337
1708
  );
1338
1709
  continue; // Try next smaller batch size
@@ -1341,12 +1712,17 @@ export class ComprehensiveTransfer {
1341
1712
 
1342
1713
  if (!processed) {
1343
1714
  MessageFormatter.warning(
1344
- `All bulk operations failed, falling back to individual transfers`,
1715
+ `All bulk operations failed, falling back to individual transfers`,
1345
1716
  { prefix: "Transfer" }
1346
1717
  );
1347
-
1718
+
1348
1719
  // Fall back to individual transfers
1349
- await this.transferDocumentsIndividual(targetDb, targetDbId, targetCollectionId, documents);
1720
+ await this.transferDocumentsIndividual(
1721
+ targetDb,
1722
+ targetDbId,
1723
+ targetCollectionId,
1724
+ documents
1725
+ );
1350
1726
  }
1351
1727
  }
1352
1728
 
@@ -1361,24 +1737,30 @@ export class ComprehensiveTransfer {
1361
1737
  ): Promise<any> {
1362
1738
  const apiPath = `/databases/${dbId}/collections/${collectionId}/documents`;
1363
1739
  const url = new URL(client.config.endpoint + apiPath);
1364
-
1740
+
1365
1741
  const headers = {
1366
- 'Content-Type': 'application/json',
1367
- 'X-Appwrite-Project': client.config.project,
1368
- 'X-Appwrite-Key': client.config.key
1742
+ "Content-Type": "application/json",
1743
+ "X-Appwrite-Project": client.config.project,
1744
+ "X-Appwrite-Key": client.config.key,
1369
1745
  };
1370
-
1746
+
1371
1747
  const response = await fetch(url.toString(), {
1372
- method: 'PUT',
1748
+ method: "PUT",
1373
1749
  headers,
1374
- body: JSON.stringify({ documents })
1750
+ body: JSON.stringify({ documents }),
1375
1751
  });
1376
-
1752
+
1377
1753
  if (!response.ok) {
1378
- const errorData: any = await response.json().catch(() => ({ message: 'Unknown error' }));
1379
- throw new Error(`Bulk upsert failed: ${response.status} - ${errorData.message || 'Unknown error'}`);
1754
+ const errorData: any = await response
1755
+ .json()
1756
+ .catch(() => ({ message: "Unknown error" }));
1757
+ throw new Error(
1758
+ `Bulk upsert failed: ${response.status} - ${
1759
+ errorData.message || "Unknown error"
1760
+ }`
1761
+ );
1380
1762
  }
1381
-
1763
+
1382
1764
  return await response.json();
1383
1765
  }
1384
1766
 
@@ -1393,11 +1775,19 @@ export class ComprehensiveTransfer {
1393
1775
  ): Promise<number> {
1394
1776
  let successCount = 0;
1395
1777
 
1396
- const transferTasks = documents.map(doc =>
1778
+ const transferTasks = documents.map((doc) =>
1397
1779
  this.limit(async () => {
1398
1780
  try {
1399
- const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
1400
-
1781
+ const {
1782
+ $id,
1783
+ $createdAt,
1784
+ $updatedAt,
1785
+ $permissions,
1786
+ $databaseId,
1787
+ $collectionId,
1788
+ ...docData
1789
+ } = doc;
1790
+
1401
1791
  await tryAwaitWithRetry(async () =>
1402
1792
  targetDb.createDocument(
1403
1793
  targetDbId,
@@ -1409,11 +1799,47 @@ export class ComprehensiveTransfer {
1409
1799
  );
1410
1800
 
1411
1801
  successCount++;
1412
- MessageFormatter.success(`Transferred document ${doc.$id}`, { prefix: "Transfer" });
1413
1802
  } catch (error) {
1803
+ if (
1804
+ error instanceof AppwriteException &&
1805
+ error.message.includes("already exists")
1806
+ ) {
1807
+ try {
1808
+ // Update it! It's here because it needs an update or a create
1809
+ const {
1810
+ $id,
1811
+ $createdAt,
1812
+ $updatedAt,
1813
+ $permissions,
1814
+ $databaseId,
1815
+ $collectionId,
1816
+ ...docData
1817
+ } = doc;
1818
+ await tryAwaitWithRetry(async () =>
1819
+ targetDb.updateDocument(
1820
+ targetDbId,
1821
+ targetCollectionId,
1822
+ doc.$id,
1823
+ docData,
1824
+ doc.$permissions
1825
+ )
1826
+ );
1827
+ successCount++;
1828
+ } catch (updateError) {
1829
+ // just send the error to the formatter
1830
+ MessageFormatter.error(
1831
+ `Failed to transfer document ${doc.$id}`,
1832
+ updateError instanceof Error
1833
+ ? updateError
1834
+ : new Error(String(updateError)),
1835
+ { prefix: "Transfer" }
1836
+ );
1837
+ }
1838
+ }
1839
+
1414
1840
  MessageFormatter.error(
1415
- `Failed to transfer document ${doc.$id}`,
1416
- error instanceof Error ? error : new Error(String(error)),
1841
+ `Failed to transfer document ${doc.$id}`,
1842
+ error instanceof Error ? error : new Error(String(error)),
1417
1843
  { prefix: "Transfer" }
1418
1844
  );
1419
1845
  }
@@ -1431,15 +1857,27 @@ export class ComprehensiveTransfer {
1431
1857
  targetDb: Databases,
1432
1858
  targetDbId: string,
1433
1859
  targetCollectionId: string,
1434
- documentPairs: { doc: Models.Document; targetDoc: Models.Document; reason: string }[]
1860
+ documentPairs: {
1861
+ doc: Models.Document;
1862
+ targetDoc: Models.Document;
1863
+ reason: string;
1864
+ }[]
1435
1865
  ): Promise<number> {
1436
1866
  let successCount = 0;
1437
1867
 
1438
- const updateTasks = documentPairs.map(({ doc, targetDoc, reason }) =>
1868
+ const updateTasks = documentPairs.map(({ doc, targetDoc, reason }) =>
1439
1869
  this.limit(async () => {
1440
1870
  try {
1441
- const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
1442
-
1871
+ const {
1872
+ $id,
1873
+ $createdAt,
1874
+ $updatedAt,
1875
+ $permissions,
1876
+ $databaseId,
1877
+ $collectionId,
1878
+ ...docData
1879
+ } = doc;
1880
+
1443
1881
  await tryAwaitWithRetry(async () =>
1444
1882
  targetDb.updateDocument(
1445
1883
  targetDbId,
@@ -1451,14 +1889,10 @@ export class ComprehensiveTransfer {
1451
1889
  );
1452
1890
 
1453
1891
  successCount++;
1454
- MessageFormatter.success(
1455
- `Updated document ${doc.$id} (${reason}) - permissions: [${targetDoc.$permissions?.join(', ')}] → [${doc.$permissions?.join(', ')}]`,
1456
- { prefix: "Transfer" }
1457
- );
1458
1892
  } catch (error) {
1459
1893
  MessageFormatter.error(
1460
- `Failed to update document ${doc.$id} (${reason})`,
1461
- error instanceof Error ? error : new Error(String(error)),
1894
+ `Failed to update document ${doc.$id} (${reason})`,
1895
+ error instanceof Error ? error : new Error(String(error)),
1462
1896
  { prefix: "Transfer" }
1463
1897
  );
1464
1898
  }
@@ -1483,7 +1917,9 @@ export class ComprehensiveTransfer {
1483
1917
  /**
1484
1918
  * Helper method to fetch all teams with pagination
1485
1919
  */
1486
- private async fetchAllTeams(teams: Teams): Promise<Models.Team<Models.Preferences>[]> {
1920
+ private async fetchAllTeams(
1921
+ teams: Teams
1922
+ ): Promise<Models.Team<Models.Preferences>[]> {
1487
1923
  const teamsList: Models.Team<Models.Preferences>[] = [];
1488
1924
  let lastId: string | undefined;
1489
1925
 
@@ -1494,17 +1930,17 @@ export class ComprehensiveTransfer {
1494
1930
  }
1495
1931
 
1496
1932
  const result = await tryAwaitWithRetry(async () => teams.list(queries));
1497
-
1933
+
1498
1934
  if (result.teams.length === 0) {
1499
1935
  break;
1500
1936
  }
1501
1937
 
1502
1938
  teamsList.push(...result.teams);
1503
-
1939
+
1504
1940
  if (result.teams.length < 100) {
1505
1941
  break;
1506
1942
  }
1507
-
1943
+
1508
1944
  lastId = result.teams[result.teams.length - 1].$id;
1509
1945
  }
1510
1946
 
@@ -1514,7 +1950,9 @@ export class ComprehensiveTransfer {
1514
1950
  /**
1515
1951
  * Helper method to fetch all memberships for a team with pagination
1516
1952
  */
1517
- private async fetchAllMemberships(teamId: string): Promise<Models.Membership[]> {
1953
+ private async fetchAllMemberships(
1954
+ teamId: string
1955
+ ): Promise<Models.Membership[]> {
1518
1956
  const membershipsList: Models.Membership[] = [];
1519
1957
  let lastId: string | undefined;
1520
1958
 
@@ -1524,20 +1962,20 @@ export class ComprehensiveTransfer {
1524
1962
  queries.push(Query.cursorAfter(lastId));
1525
1963
  }
1526
1964
 
1527
- const result = await tryAwaitWithRetry(async () =>
1965
+ const result = await tryAwaitWithRetry(async () =>
1528
1966
  this.sourceTeams.listMemberships(teamId, queries)
1529
1967
  );
1530
-
1968
+
1531
1969
  if (result.memberships.length === 0) {
1532
1970
  break;
1533
1971
  }
1534
1972
 
1535
1973
  membershipsList.push(...result.memberships);
1536
-
1974
+
1537
1975
  if (result.memberships.length < 100) {
1538
1976
  break;
1539
1977
  }
1540
-
1978
+
1541
1979
  lastId = result.memberships[result.memberships.length - 1].$id;
1542
1980
  }
1543
1981
 
@@ -1548,40 +1986,55 @@ export class ComprehensiveTransfer {
1548
1986
  * Helper method to transfer team memberships
1549
1987
  */
1550
1988
  private async transferTeamMemberships(teamId: string): Promise<void> {
1551
- MessageFormatter.info(`Transferring memberships for team ${teamId}`, { prefix: "Transfer" });
1989
+ MessageFormatter.info(`Transferring memberships for team ${teamId}`, {
1990
+ prefix: "Transfer",
1991
+ });
1552
1992
 
1553
1993
  try {
1554
1994
  // Fetch all memberships for this team
1555
1995
  const memberships = await this.fetchAllMemberships(teamId);
1556
-
1996
+
1557
1997
  if (memberships.length === 0) {
1558
- MessageFormatter.info(`No memberships found for team ${teamId}`, { prefix: "Transfer" });
1998
+ MessageFormatter.info(`No memberships found for team ${teamId}`, {
1999
+ prefix: "Transfer",
2000
+ });
1559
2001
  return;
1560
2002
  }
1561
2003
 
1562
- MessageFormatter.info(`Found ${memberships.length} memberships for team ${teamId}`, { prefix: "Transfer" });
2004
+ MessageFormatter.info(
2005
+ `Found ${memberships.length} memberships for team ${teamId}`,
2006
+ { prefix: "Transfer" }
2007
+ );
1563
2008
 
1564
2009
  let totalTransferred = 0;
1565
2010
 
1566
2011
  // Transfer memberships with rate limiting
1567
- const transferTasks = memberships.map(membership =>
1568
- this.userLimit(async () => { // Use userLimit for team operations (more sensitive)
2012
+ const transferTasks = memberships.map((membership) =>
2013
+ this.userLimit(async () => {
2014
+ // Use userLimit for team operations (more sensitive)
1569
2015
  try {
1570
2016
  // Check if membership already exists and compare roles
1571
2017
  let existingMembership: Models.Membership | null = null;
1572
2018
  try {
1573
- existingMembership = await this.targetTeams.getMembership(teamId, membership.$id);
1574
-
2019
+ existingMembership = await this.targetTeams.getMembership(
2020
+ teamId,
2021
+ membership.$id
2022
+ );
2023
+
1575
2024
  // Compare roles between source and target membership
1576
- const sourceRoles = JSON.stringify(membership.roles?.sort() || []);
1577
- const targetRoles = JSON.stringify(existingMembership.roles?.sort() || []);
1578
-
2025
+ const sourceRoles = JSON.stringify(
2026
+ membership.roles?.sort() || []
2027
+ );
2028
+ const targetRoles = JSON.stringify(
2029
+ existingMembership.roles?.sort() || []
2030
+ );
2031
+
1579
2032
  if (sourceRoles !== targetRoles) {
1580
2033
  MessageFormatter.warning(
1581
- `Membership ${membership.$id} exists but has different roles. Source: ${sourceRoles}, Target: ${targetRoles}`,
2034
+ `Membership ${membership.$id} exists but has different roles. Source: ${sourceRoles}, Target: ${targetRoles}`,
1582
2035
  { prefix: "Transfer" }
1583
2036
  );
1584
-
2037
+
1585
2038
  // Update membership roles to match source
1586
2039
  try {
1587
2040
  await this.targetTeams.updateMembership(
@@ -1589,16 +2042,24 @@ export class ComprehensiveTransfer {
1589
2042
  membership.$id,
1590
2043
  membership.roles
1591
2044
  );
1592
- MessageFormatter.success(`Updated membership ${membership.$id} roles to match source`, { prefix: "Transfer" });
2045
+ MessageFormatter.success(
2046
+ `Updated membership ${membership.$id} roles to match source`,
2047
+ { prefix: "Transfer" }
2048
+ );
1593
2049
  } catch (updateError) {
1594
2050
  MessageFormatter.error(
1595
- `Failed to update roles for membership ${membership.$id}`,
1596
- updateError instanceof Error ? updateError : new Error(String(updateError)),
2051
+ `Failed to update roles for membership ${membership.$id}`,
2052
+ updateError instanceof Error
2053
+ ? updateError
2054
+ : new Error(String(updateError)),
1597
2055
  { prefix: "Transfer" }
1598
2056
  );
1599
2057
  }
1600
2058
  } else {
1601
- MessageFormatter.info(`Membership ${membership.$id} already exists with matching roles, skipping`, { prefix: "Transfer" });
2059
+ MessageFormatter.info(
2060
+ `Membership ${membership.$id} already exists with matching roles, skipping`,
2061
+ { prefix: "Transfer" }
2062
+ );
1602
2063
  }
1603
2064
  return;
1604
2065
  } catch (error) {
@@ -1610,7 +2071,10 @@ export class ComprehensiveTransfer {
1610
2071
  try {
1611
2072
  userData = await this.targetUsers.get(membership.userId);
1612
2073
  } catch (error) {
1613
- MessageFormatter.warning(`User ${membership.userId} not found in target, membership ${membership.$id} may fail`, { prefix: "Transfer" });
2074
+ MessageFormatter.warning(
2075
+ `User ${membership.userId} not found in target, membership ${membership.$id} may fail`,
2076
+ { prefix: "Transfer" }
2077
+ );
1614
2078
  }
1615
2079
 
1616
2080
  // Create membership using the comprehensive user data
@@ -1627,38 +2091,87 @@ export class ComprehensiveTransfer {
1627
2091
  );
1628
2092
 
1629
2093
  totalTransferred++;
1630
- MessageFormatter.success(`Transferred membership ${membership.$id} for user ${userData?.name || membership.userName}`, { prefix: "Transfer" });
2094
+ MessageFormatter.success(
2095
+ `Transferred membership ${membership.$id} for user ${
2096
+ userData?.name || membership.userName
2097
+ }`,
2098
+ { prefix: "Transfer" }
2099
+ );
1631
2100
  } catch (error) {
1632
- MessageFormatter.error(`Failed to transfer membership ${membership.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
2101
+ MessageFormatter.error(
2102
+ `Failed to transfer membership ${membership.$id}`,
2103
+ error instanceof Error ? error : new Error(String(error)),
2104
+ { prefix: "Transfer" }
2105
+ );
1633
2106
  }
1634
2107
  })
1635
2108
  );
1636
2109
 
1637
2110
  await Promise.all(transferTasks);
1638
- MessageFormatter.info(`Transferred ${totalTransferred} memberships for team ${teamId}`, { prefix: "Transfer" });
2111
+ MessageFormatter.info(
2112
+ `Transferred ${totalTransferred} memberships for team ${teamId}`,
2113
+ { prefix: "Transfer" }
2114
+ );
1639
2115
  } catch (error) {
1640
- MessageFormatter.error(`Failed to transfer memberships for team ${teamId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
2116
+ MessageFormatter.error(
2117
+ `Failed to transfer memberships for team ${teamId}`,
2118
+ error instanceof Error ? error : new Error(String(error)),
2119
+ { prefix: "Transfer" }
2120
+ );
1641
2121
  }
1642
2122
  }
1643
2123
 
1644
2124
  private printSummary(): void {
1645
2125
  const duration = Math.round((Date.now() - this.startTime) / 1000);
1646
-
1647
- MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", { prefix: "Transfer" });
2126
+
2127
+ MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", {
2128
+ prefix: "Transfer",
2129
+ });
1648
2130
  MessageFormatter.info(`Total Time: ${duration}s`, { prefix: "Transfer" });
1649
- MessageFormatter.info(`Users: ${this.results.users.transferred} transferred, ${this.results.users.skipped} skipped, ${this.results.users.failed} failed`, { prefix: "Transfer" });
1650
- MessageFormatter.info(`Teams: ${this.results.teams.transferred} transferred, ${this.results.teams.skipped} skipped, ${this.results.teams.failed} failed`, { prefix: "Transfer" });
1651
- MessageFormatter.info(`Databases: ${this.results.databases.transferred} transferred, ${this.results.databases.skipped} skipped, ${this.results.databases.failed} failed`, { prefix: "Transfer" });
1652
- MessageFormatter.info(`Buckets: ${this.results.buckets.transferred} transferred, ${this.results.buckets.skipped} skipped, ${this.results.buckets.failed} failed`, { prefix: "Transfer" });
1653
- MessageFormatter.info(`Functions: ${this.results.functions.transferred} transferred, ${this.results.functions.skipped} skipped, ${this.results.functions.failed} failed`, { prefix: "Transfer" });
1654
-
1655
- const totalTransferred = this.results.users.transferred + this.results.teams.transferred + this.results.databases.transferred + this.results.buckets.transferred + this.results.functions.transferred;
1656
- const totalFailed = this.results.users.failed + this.results.teams.failed + this.results.databases.failed + this.results.buckets.failed + this.results.functions.failed;
1657
-
2131
+ MessageFormatter.info(
2132
+ `Users: ${this.results.users.transferred} transferred, ${this.results.users.skipped} skipped, ${this.results.users.failed} failed`,
2133
+ { prefix: "Transfer" }
2134
+ );
2135
+ MessageFormatter.info(
2136
+ `Teams: ${this.results.teams.transferred} transferred, ${this.results.teams.skipped} skipped, ${this.results.teams.failed} failed`,
2137
+ { prefix: "Transfer" }
2138
+ );
2139
+ MessageFormatter.info(
2140
+ `Databases: ${this.results.databases.transferred} transferred, ${this.results.databases.skipped} skipped, ${this.results.databases.failed} failed`,
2141
+ { prefix: "Transfer" }
2142
+ );
2143
+ MessageFormatter.info(
2144
+ `Buckets: ${this.results.buckets.transferred} transferred, ${this.results.buckets.skipped} skipped, ${this.results.buckets.failed} failed`,
2145
+ { prefix: "Transfer" }
2146
+ );
2147
+ MessageFormatter.info(
2148
+ `Functions: ${this.results.functions.transferred} transferred, ${this.results.functions.skipped} skipped, ${this.results.functions.failed} failed`,
2149
+ { prefix: "Transfer" }
2150
+ );
2151
+
2152
+ const totalTransferred =
2153
+ this.results.users.transferred +
2154
+ this.results.teams.transferred +
2155
+ this.results.databases.transferred +
2156
+ this.results.buckets.transferred +
2157
+ this.results.functions.transferred;
2158
+ const totalFailed =
2159
+ this.results.users.failed +
2160
+ this.results.teams.failed +
2161
+ this.results.databases.failed +
2162
+ this.results.buckets.failed +
2163
+ this.results.functions.failed;
2164
+
1658
2165
  if (totalFailed === 0) {
1659
- MessageFormatter.success(`All ${totalTransferred} items transferred successfully!`, { prefix: "Transfer" });
2166
+ MessageFormatter.success(
2167
+ `All ${totalTransferred} items transferred successfully!`,
2168
+ { prefix: "Transfer" }
2169
+ );
1660
2170
  } else {
1661
- MessageFormatter.warning(`${totalTransferred} items transferred, ${totalFailed} failed`, { prefix: "Transfer" });
2171
+ MessageFormatter.warning(
2172
+ `${totalTransferred} items transferred, ${totalFailed} failed`,
2173
+ { prefix: "Transfer" }
2174
+ );
1662
2175
  }
1663
2176
  }
1664
- }
2177
+ }