appwrite-utils-cli 1.2.15 → 1.2.17

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,113 @@ 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
+ let indexesSuccess = true;
577
+ // Check if indexes need to be created ahead of time
578
+ if (
579
+ collection.indexes.some(
580
+ (index) =>
581
+ !targetCollection.indexes.some(
582
+ (ti) =>
583
+ ti.key === index.key ||
584
+ ti.attributes.sort().join(",") ===
585
+ index.attributes.sort().join(",")
586
+ )
587
+ ) ||
588
+ collection.indexes.length !== targetCollection.indexes.length
589
+ ) {
590
+ indexesSuccess = await this.createCollectionIndexesWithStatusCheck(
591
+ dbId,
592
+ this.targetDatabases,
593
+ targetCollection.$id,
594
+ targetCollection,
595
+ collection.indexes as any
596
+ );
597
+ }
598
+
439
599
  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" });
600
+ MessageFormatter.error(
601
+ `Failed to create some indexes for collection ${collection.name}`,
602
+ undefined,
603
+ { prefix: "Transfer" }
604
+ );
605
+ MessageFormatter.warning(
606
+ `Proceeding with document transfer despite index failures for collection ${collection.name}`,
607
+ { prefix: "Transfer" }
608
+ );
442
609
  } else {
443
- MessageFormatter.success(`All indexes created successfully for collection ${collection.name}`, { prefix: "Transfer" });
610
+ MessageFormatter.success(
611
+ `All indexes created successfully for collection ${collection.name}`,
612
+ { prefix: "Transfer" }
613
+ );
444
614
  }
445
615
 
446
- MessageFormatter.success(`Structure complete for collection ${collection.name}`, { prefix: "Transfer" });
616
+ MessageFormatter.success(
617
+ `Structure complete for collection ${collection.name}`,
618
+ { prefix: "Transfer" }
619
+ );
447
620
  } catch (error) {
448
- MessageFormatter.error(`Error processing collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
621
+ MessageFormatter.error(
622
+ `Error processing collection ${collection.name}`,
623
+ error instanceof Error ? error : new Error(String(error)),
624
+ { prefix: "Transfer" }
625
+ );
449
626
  }
450
627
  }
451
628
  } catch (error) {
452
- MessageFormatter.error(`Failed to create database structure for ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
629
+ MessageFormatter.error(
630
+ `Failed to create database structure for ${dbId}`,
631
+ error instanceof Error ? error : new Error(String(error)),
632
+ { prefix: "Transfer" }
633
+ );
453
634
  throw error;
454
635
  }
455
636
  }
@@ -458,16 +639,27 @@ export class ComprehensiveTransfer {
458
639
  * Phase 2: Transfer documents to all collections in the database
459
640
  */
460
641
  private async transferDatabaseDocuments(dbId: string): Promise<void> {
461
- MessageFormatter.info(`Transferring documents for database ${dbId}`, { prefix: "Transfer" });
642
+ MessageFormatter.info(`Transferring documents for database ${dbId}`, {
643
+ prefix: "Transfer",
644
+ });
462
645
 
463
646
  try {
464
647
  // 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" });
648
+ const sourceCollections = await this.fetchAllCollections(
649
+ dbId,
650
+ this.sourceDatabases
651
+ );
652
+ MessageFormatter.info(
653
+ `Transferring documents for ${sourceCollections.length} collections in database ${dbId}`,
654
+ { prefix: "Transfer" }
655
+ );
467
656
 
468
657
  // Process each collection
469
658
  for (const collection of sourceCollections) {
470
- MessageFormatter.info(`Transferring documents for collection: ${collection.name} (${collection.$id})`, { prefix: "Transfer" });
659
+ MessageFormatter.info(
660
+ `Transferring documents for collection: ${collection.name} (${collection.$id})`,
661
+ { prefix: "Transfer" }
662
+ );
471
663
 
472
664
  try {
473
665
  // Transfer documents
@@ -479,20 +671,33 @@ export class ComprehensiveTransfer {
479
671
  collection.$id,
480
672
  collection.$id
481
673
  );
482
-
483
- MessageFormatter.success(`Documents transferred for collection ${collection.name}`, { prefix: "Transfer" });
674
+
675
+ MessageFormatter.success(
676
+ `Documents transferred for collection ${collection.name}`,
677
+ { prefix: "Transfer" }
678
+ );
484
679
  } catch (error) {
485
- MessageFormatter.error(`Error transferring documents for collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
680
+ MessageFormatter.error(
681
+ `Error transferring documents for collection ${collection.name}`,
682
+ error instanceof Error ? error : new Error(String(error)),
683
+ { prefix: "Transfer" }
684
+ );
486
685
  }
487
686
  }
488
687
  } catch (error) {
489
- MessageFormatter.error(`Failed to transfer documents for database ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
688
+ MessageFormatter.error(
689
+ `Failed to transfer documents for database ${dbId}`,
690
+ error instanceof Error ? error : new Error(String(error)),
691
+ { prefix: "Transfer" }
692
+ );
490
693
  throw error;
491
694
  }
492
695
  }
493
696
 
494
697
  private async transferAllBuckets(): Promise<void> {
495
- MessageFormatter.info("Starting bucket transfer phase", { prefix: "Transfer" });
698
+ MessageFormatter.info("Starting bucket transfer phase", {
699
+ prefix: "Transfer",
700
+ });
496
701
 
497
702
  try {
498
703
  // Get all buckets from source with pagination
@@ -502,38 +707,52 @@ export class ComprehensiveTransfer {
502
707
  if (this.options.dryRun) {
503
708
  let totalFiles = 0;
504
709
  for (const bucket of allSourceBuckets) {
505
- const files = await this.sourceStorage.listFiles(bucket.$id, [Query.limit(1)]);
710
+ const files = await this.sourceStorage.listFiles(bucket.$id, [
711
+ Query.limit(1),
712
+ ]);
506
713
  totalFiles += files.total;
507
714
  }
508
- MessageFormatter.info(`DRY RUN: Would transfer ${allSourceBuckets.length} buckets with ${totalFiles} files`, { prefix: "Transfer" });
715
+ MessageFormatter.info(
716
+ `DRY RUN: Would transfer ${allSourceBuckets.length} buckets with ${totalFiles} files`,
717
+ { prefix: "Transfer" }
718
+ );
509
719
  return;
510
720
  }
511
721
 
512
- const transferTasks = allSourceBuckets.map(bucket =>
722
+ const transferTasks = allSourceBuckets.map((bucket) =>
513
723
  this.limit(async () => {
514
724
  try {
515
725
  // Check if bucket exists in target
516
- const existingBucket = allTargetBuckets.find(tb => tb.$id === bucket.$id);
517
-
726
+ const existingBucket = allTargetBuckets.find(
727
+ (tb) => tb.$id === bucket.$id
728
+ );
729
+
518
730
  if (!existingBucket) {
519
731
  // Create bucket with fallback strategy for maximumFileSize
520
732
  await this.createBucketWithFallback(bucket);
521
- MessageFormatter.success(`Created bucket: ${bucket.name}`, { prefix: "Transfer" });
733
+ MessageFormatter.success(`Created bucket: ${bucket.name}`, {
734
+ prefix: "Transfer",
735
+ });
522
736
  } else {
523
737
  // 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
-
738
+ const sourcePermissions = JSON.stringify(
739
+ bucket.$permissions?.sort() || []
740
+ );
741
+ const targetPermissions = JSON.stringify(
742
+ existingBucket.$permissions?.sort() || []
743
+ );
744
+
745
+ if (
746
+ sourcePermissions !== targetPermissions ||
747
+ existingBucket.name !== bucket.name ||
748
+ existingBucket.fileSecurity !== bucket.fileSecurity ||
749
+ existingBucket.enabled !== bucket.enabled
750
+ ) {
532
751
  MessageFormatter.warning(
533
- `Bucket ${bucket.name} exists but has different settings. Updating to match source.`,
752
+ `Bucket ${bucket.name} exists but has different settings. Updating to match source.`,
534
753
  { prefix: "Transfer" }
535
754
  );
536
-
755
+
537
756
  try {
538
757
  await this.targetStorage.updateBucket(
539
758
  bucket.$id,
@@ -547,16 +766,24 @@ export class ComprehensiveTransfer {
547
766
  bucket.encryption,
548
767
  bucket.antivirus
549
768
  );
550
- MessageFormatter.success(`Updated bucket ${bucket.name} to match source`, { prefix: "Transfer" });
769
+ MessageFormatter.success(
770
+ `Updated bucket ${bucket.name} to match source`,
771
+ { prefix: "Transfer" }
772
+ );
551
773
  } catch (updateError) {
552
774
  MessageFormatter.error(
553
- `Failed to update bucket ${bucket.name}`,
554
- updateError instanceof Error ? updateError : new Error(String(updateError)),
775
+ `Failed to update bucket ${bucket.name}`,
776
+ updateError instanceof Error
777
+ ? updateError
778
+ : new Error(String(updateError)),
555
779
  { prefix: "Transfer" }
556
780
  );
557
781
  }
558
782
  } else {
559
- MessageFormatter.info(`Bucket ${bucket.name} already exists with matching settings`, { prefix: "Transfer" });
783
+ MessageFormatter.info(
784
+ `Bucket ${bucket.name} already exists with matching settings`,
785
+ { prefix: "Transfer" }
786
+ );
560
787
  }
561
788
  }
562
789
 
@@ -564,31 +791,46 @@ export class ComprehensiveTransfer {
564
791
  await this.transferBucketFiles(bucket.$id, bucket.$id);
565
792
 
566
793
  this.results.buckets.transferred++;
567
- MessageFormatter.success(`Bucket ${bucket.name} transferred successfully`, { prefix: "Transfer" });
794
+ MessageFormatter.success(
795
+ `Bucket ${bucket.name} transferred successfully`,
796
+ { prefix: "Transfer" }
797
+ );
568
798
  } catch (error) {
569
- MessageFormatter.error(`Bucket ${bucket.name} transfer failed`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
799
+ MessageFormatter.error(
800
+ `Bucket ${bucket.name} transfer failed`,
801
+ error instanceof Error ? error : new Error(String(error)),
802
+ { prefix: "Transfer" }
803
+ );
570
804
  this.results.buckets.failed++;
571
805
  }
572
806
  })
573
807
  );
574
808
 
575
809
  await Promise.all(transferTasks);
576
- MessageFormatter.success("Bucket transfer phase completed", { prefix: "Transfer" });
810
+ MessageFormatter.success("Bucket transfer phase completed", {
811
+ prefix: "Transfer",
812
+ });
577
813
  } catch (error) {
578
- MessageFormatter.error("Bucket transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
814
+ MessageFormatter.error(
815
+ "Bucket transfer phase failed",
816
+ error instanceof Error ? error : new Error(String(error)),
817
+ { prefix: "Transfer" }
818
+ );
579
819
  }
580
820
  }
581
821
 
582
822
  private async createBucketWithFallback(bucket: Models.Bucket): Promise<void> {
583
823
  // Determine the optimal size to try first
584
824
  let sizeToTry: number;
585
-
825
+
586
826
  if (this.cachedMaxFileSize) {
587
827
  // Use cached size if it's smaller than or equal to the bucket's original size
588
828
  if (bucket.maximumFileSize >= this.cachedMaxFileSize) {
589
829
  sizeToTry = this.cachedMaxFileSize;
590
830
  MessageFormatter.info(
591
- `Bucket ${bucket.name}: Using cached maximumFileSize ${sizeToTry} (${(sizeToTry / 1_000_000_000).toFixed(1)}GB)`,
831
+ `Bucket ${bucket.name}: Using cached maximumFileSize ${sizeToTry} (${(
832
+ sizeToTry / 1_000_000_000
833
+ ).toFixed(1)}GB)`,
592
834
  { prefix: "Transfer" }
593
835
  );
594
836
  } else {
@@ -614,30 +856,41 @@ export class ComprehensiveTransfer {
614
856
  bucket.encryption,
615
857
  bucket.antivirus
616
858
  );
617
-
859
+
618
860
  // Success - cache this size if it's not already cached or is smaller than cached
619
861
  if (!this.cachedMaxFileSize || sizeToTry < this.cachedMaxFileSize) {
620
862
  this.cachedMaxFileSize = sizeToTry;
621
863
  MessageFormatter.info(
622
- `Bucket ${bucket.name}: Cached successful maximumFileSize ${sizeToTry} (${(sizeToTry / 1_000_000_000).toFixed(1)}GB)`,
864
+ `Bucket ${
865
+ bucket.name
866
+ }: Cached successful maximumFileSize ${sizeToTry} (${(
867
+ sizeToTry / 1_000_000_000
868
+ ).toFixed(1)}GB)`,
623
869
  { prefix: "Transfer" }
624
870
  );
625
871
  }
626
-
872
+
627
873
  // Log if we used a different size than original
628
874
  if (sizeToTry !== bucket.maximumFileSize) {
629
875
  MessageFormatter.warning(
630
- `Bucket ${bucket.name}: maximumFileSize used ${sizeToTry} instead of original ${bucket.maximumFileSize} (${(sizeToTry / 1_000_000_000).toFixed(1)}GB)`,
876
+ `Bucket ${
877
+ bucket.name
878
+ }: maximumFileSize used ${sizeToTry} instead of original ${
879
+ bucket.maximumFileSize
880
+ } (${(sizeToTry / 1_000_000_000).toFixed(1)}GB)`,
631
881
  { prefix: "Transfer" }
632
882
  );
633
883
  }
634
-
884
+
635
885
  return; // Success, exit the function
636
886
  } catch (error) {
637
887
  const err = error instanceof Error ? error : new Error(String(error));
638
-
888
+
639
889
  // Check if the error is related to maximumFileSize validation
640
- if (err.message.includes('maximumFileSize') || err.message.includes('valid range')) {
890
+ if (
891
+ err.message.includes("maximumFileSize") ||
892
+ err.message.includes("valid range")
893
+ ) {
641
894
  MessageFormatter.warning(
642
895
  `Bucket ${bucket.name}: Failed with maximumFileSize ${sizeToTry}, falling back to smaller sizes...`,
643
896
  { prefix: "Transfer" }
@@ -655,17 +908,17 @@ export class ComprehensiveTransfer {
655
908
  2_500_000_000, // 2.5GB
656
909
  2_000_000_000, // 2GB
657
910
  1_000_000_000, // 1GB
658
- 500_000_000, // 500MB
659
- 100_000_000 // 100MB
911
+ 500_000_000, // 500MB
912
+ 100_000_000, // 100MB
660
913
  ];
661
914
 
662
915
  // Remove sizes that are larger than or equal to the already-tried size
663
916
  const validSizes = fallbackSizes
664
- .filter(size => size < sizeToTry)
917
+ .filter((size) => size < sizeToTry)
665
918
  .sort((a, b) => b - a); // Sort descending
666
919
 
667
920
  let lastError: Error | null = null;
668
-
921
+
669
922
  for (const fileSize of validSizes) {
670
923
  try {
671
924
  await this.targetStorage.createBucket(
@@ -680,30 +933,39 @@ export class ComprehensiveTransfer {
680
933
  bucket.encryption,
681
934
  bucket.antivirus
682
935
  );
683
-
936
+
684
937
  // Success - cache this size if it's not already cached or is smaller than cached
685
938
  if (!this.cachedMaxFileSize || fileSize < this.cachedMaxFileSize) {
686
939
  this.cachedMaxFileSize = fileSize;
687
940
  MessageFormatter.info(
688
- `Bucket ${bucket.name}: Cached successful maximumFileSize ${fileSize} (${(fileSize / 1_000_000_000).toFixed(1)}GB)`,
941
+ `Bucket ${
942
+ bucket.name
943
+ }: Cached successful maximumFileSize ${fileSize} (${(
944
+ fileSize / 1_000_000_000
945
+ ).toFixed(1)}GB)`,
689
946
  { prefix: "Transfer" }
690
947
  );
691
948
  }
692
-
949
+
693
950
  // Log if we had to reduce the file size
694
951
  if (fileSize !== bucket.maximumFileSize) {
695
952
  MessageFormatter.warning(
696
- `Bucket ${bucket.name}: maximumFileSize reduced from ${bucket.maximumFileSize} to ${fileSize} (${(fileSize / 1_000_000_000).toFixed(1)}GB)`,
953
+ `Bucket ${bucket.name}: maximumFileSize reduced from ${
954
+ bucket.maximumFileSize
955
+ } to ${fileSize} (${(fileSize / 1_000_000_000).toFixed(1)}GB)`,
697
956
  { prefix: "Transfer" }
698
957
  );
699
958
  }
700
-
959
+
701
960
  return; // Success, exit the function
702
961
  } catch (error) {
703
962
  lastError = error instanceof Error ? error : new Error(String(error));
704
-
963
+
705
964
  // Check if the error is related to maximumFileSize validation
706
- if (lastError.message.includes('maximumFileSize') || lastError.message.includes('valid range')) {
965
+ if (
966
+ lastError.message.includes("maximumFileSize") ||
967
+ lastError.message.includes("valid range")
968
+ ) {
707
969
  MessageFormatter.warning(
708
970
  `Bucket ${bucket.name}: Failed with maximumFileSize ${fileSize}, trying smaller size...`,
709
971
  { prefix: "Transfer" }
@@ -715,17 +977,20 @@ export class ComprehensiveTransfer {
715
977
  }
716
978
  }
717
979
  }
718
-
980
+
719
981
  // If we get here, all fallback sizes failed
720
982
  MessageFormatter.error(
721
983
  `Bucket ${bucket.name}: All fallback file sizes failed. Last error: ${lastError?.message}`,
722
984
  lastError || undefined,
723
985
  { prefix: "Transfer" }
724
986
  );
725
- throw lastError || new Error('All fallback file sizes failed');
987
+ throw lastError || new Error("All fallback file sizes failed");
726
988
  }
727
989
 
728
- private async transferBucketFiles(sourceBucketId: string, targetBucketId: string): Promise<void> {
990
+ private async transferBucketFiles(
991
+ sourceBucketId: string,
992
+ targetBucketId: string
993
+ ): Promise<void> {
729
994
  let lastFileId: string | undefined;
730
995
  let transferredFiles = 0;
731
996
 
@@ -739,24 +1004,31 @@ export class ComprehensiveTransfer {
739
1004
  if (files.files.length === 0) break;
740
1005
 
741
1006
  // Process files with rate limiting
742
- const fileTasks = files.files.map(file =>
1007
+ const fileTasks = files.files.map((file) =>
743
1008
  this.fileLimit(async () => {
744
1009
  try {
745
1010
  // Check if file already exists and compare permissions
746
1011
  let existingFile: Models.File | null = null;
747
1012
  try {
748
- existingFile = await this.targetStorage.getFile(targetBucketId, file.$id);
749
-
1013
+ existingFile = await this.targetStorage.getFile(
1014
+ targetBucketId,
1015
+ file.$id
1016
+ );
1017
+
750
1018
  // Compare permissions between source and target file
751
- const sourcePermissions = JSON.stringify(file.$permissions?.sort() || []);
752
- const targetPermissions = JSON.stringify(existingFile.$permissions?.sort() || []);
753
-
1019
+ const sourcePermissions = JSON.stringify(
1020
+ file.$permissions?.sort() || []
1021
+ );
1022
+ const targetPermissions = JSON.stringify(
1023
+ existingFile.$permissions?.sort() || []
1024
+ );
1025
+
754
1026
  if (sourcePermissions !== targetPermissions) {
755
1027
  MessageFormatter.warning(
756
- `File ${file.name} (${file.$id}) exists but has different permissions. Source: ${sourcePermissions}, Target: ${targetPermissions}`,
1028
+ `File ${file.name} (${file.$id}) exists but has different permissions. Source: ${sourcePermissions}, Target: ${targetPermissions}`,
757
1029
  { prefix: "Transfer" }
758
1030
  );
759
-
1031
+
760
1032
  // Update file permissions to match source
761
1033
  try {
762
1034
  await this.targetStorage.updateFile(
@@ -765,16 +1037,24 @@ export class ComprehensiveTransfer {
765
1037
  file.name,
766
1038
  file.$permissions
767
1039
  );
768
- MessageFormatter.success(`Updated file ${file.name} permissions to match source`, { prefix: "Transfer" });
1040
+ MessageFormatter.success(
1041
+ `Updated file ${file.name} permissions to match source`,
1042
+ { prefix: "Transfer" }
1043
+ );
769
1044
  } catch (updateError) {
770
1045
  MessageFormatter.error(
771
- `Failed to update permissions for file ${file.name}`,
772
- updateError instanceof Error ? updateError : new Error(String(updateError)),
1046
+ `Failed to update permissions for file ${file.name}`,
1047
+ updateError instanceof Error
1048
+ ? updateError
1049
+ : new Error(String(updateError)),
773
1050
  { prefix: "Transfer" }
774
1051
  );
775
1052
  }
776
1053
  } else {
777
- MessageFormatter.info(`File ${file.name} already exists with matching permissions, skipping`, { prefix: "Transfer" });
1054
+ MessageFormatter.info(
1055
+ `File ${file.name} already exists with matching permissions, skipping`,
1056
+ { prefix: "Transfer" }
1057
+ );
778
1058
  }
779
1059
  return;
780
1060
  } catch (error) {
@@ -782,9 +1062,15 @@ export class ComprehensiveTransfer {
782
1062
  }
783
1063
 
784
1064
  // Download file with validation
785
- const fileData = await this.validateAndDownloadFile(sourceBucketId, file.$id);
1065
+ const fileData = await this.validateAndDownloadFile(
1066
+ sourceBucketId,
1067
+ file.$id
1068
+ );
786
1069
  if (!fileData) {
787
- MessageFormatter.warning(`File ${file.name} failed validation, skipping`, { prefix: "Transfer" });
1070
+ MessageFormatter.warning(
1071
+ `File ${file.name} failed validation, skipping`,
1072
+ { prefix: "Transfer" }
1073
+ );
788
1074
  return;
789
1075
  }
790
1076
 
@@ -802,9 +1088,15 @@ export class ComprehensiveTransfer {
802
1088
  );
803
1089
 
804
1090
  transferredFiles++;
805
- MessageFormatter.success(`Transferred file: ${file.name}`, { prefix: "Transfer" });
1091
+ MessageFormatter.success(`Transferred file: ${file.name}`, {
1092
+ prefix: "Transfer",
1093
+ });
806
1094
  } catch (error) {
807
- MessageFormatter.error(`Failed to transfer file ${file.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1095
+ MessageFormatter.error(
1096
+ `Failed to transfer file ${file.name}`,
1097
+ error instanceof Error ? error : new Error(String(error)),
1098
+ { prefix: "Transfer" }
1099
+ );
808
1100
  }
809
1101
  })
810
1102
  );
@@ -815,61 +1107,99 @@ export class ComprehensiveTransfer {
815
1107
  lastFileId = files.files[files.files.length - 1].$id;
816
1108
  }
817
1109
 
818
- MessageFormatter.info(`Transferred ${transferredFiles} files from bucket ${sourceBucketId}`, { prefix: "Transfer" });
1110
+ MessageFormatter.info(
1111
+ `Transferred ${transferredFiles} files from bucket ${sourceBucketId}`,
1112
+ { prefix: "Transfer" }
1113
+ );
819
1114
  }
820
1115
 
821
- private async validateAndDownloadFile(bucketId: string, fileId: string): Promise<ArrayBuffer | null> {
1116
+ private async validateAndDownloadFile(
1117
+ bucketId: string,
1118
+ fileId: string
1119
+ ): Promise<ArrayBuffer | null> {
822
1120
  let attempts = 3;
823
1121
  while (attempts > 0) {
824
1122
  try {
825
- const fileData = await this.sourceStorage.getFileDownload(bucketId, fileId);
826
-
1123
+ const fileData = await this.sourceStorage.getFileDownload(
1124
+ bucketId,
1125
+ fileId
1126
+ );
1127
+
827
1128
  // Basic validation - ensure file is not empty and not too large
828
1129
  if (fileData.byteLength === 0) {
829
- MessageFormatter.warning(`File ${fileId} is empty`, { prefix: "Transfer" });
1130
+ MessageFormatter.warning(`File ${fileId} is empty`, {
1131
+ prefix: "Transfer",
1132
+ });
830
1133
  return null;
831
1134
  }
832
1135
 
833
- if (fileData.byteLength > 50 * 1024 * 1024) { // 50MB limit
834
- MessageFormatter.warning(`File ${fileId} is too large (${fileData.byteLength} bytes)`, { prefix: "Transfer" });
1136
+ if (fileData.byteLength > 50 * 1024 * 1024) {
1137
+ // 50MB limit
1138
+ MessageFormatter.warning(
1139
+ `File ${fileId} is too large (${fileData.byteLength} bytes)`,
1140
+ { prefix: "Transfer" }
1141
+ );
835
1142
  return null;
836
1143
  }
837
1144
 
838
1145
  return fileData;
839
1146
  } catch (error) {
840
1147
  attempts--;
841
- MessageFormatter.warning(`Error downloading file ${fileId}, attempts left: ${attempts}`, { prefix: "Transfer" });
1148
+ MessageFormatter.warning(
1149
+ `Error downloading file ${fileId}, attempts left: ${attempts}`,
1150
+ { prefix: "Transfer" }
1151
+ );
842
1152
  if (attempts === 0) {
843
- MessageFormatter.error(`Failed to download file ${fileId} after all attempts`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1153
+ MessageFormatter.error(
1154
+ `Failed to download file ${fileId} after all attempts`,
1155
+ error instanceof Error ? error : new Error(String(error)),
1156
+ { prefix: "Transfer" }
1157
+ );
844
1158
  return null;
845
1159
  }
846
1160
  // Wait before retry
847
- await new Promise(resolve => setTimeout(resolve, 1000 * (4 - attempts)));
1161
+ await new Promise((resolve) =>
1162
+ setTimeout(resolve, 1000 * (4 - attempts))
1163
+ );
848
1164
  }
849
1165
  }
850
1166
  return null;
851
1167
  }
852
1168
 
853
1169
  private async transferAllFunctions(): Promise<void> {
854
- MessageFormatter.info("Starting function transfer phase", { prefix: "Transfer" });
1170
+ MessageFormatter.info("Starting function transfer phase", {
1171
+ prefix: "Transfer",
1172
+ });
855
1173
 
856
1174
  try {
857
- const sourceFunctions = await listFunctions(this.sourceClient, [Query.limit(1000)]);
858
- const targetFunctions = await listFunctions(this.targetClient, [Query.limit(1000)]);
1175
+ const sourceFunctions = await listFunctions(this.sourceClient, [
1176
+ Query.limit(1000),
1177
+ ]);
1178
+ const targetFunctions = await listFunctions(this.targetClient, [
1179
+ Query.limit(1000),
1180
+ ]);
859
1181
 
860
1182
  if (this.options.dryRun) {
861
- MessageFormatter.info(`DRY RUN: Would transfer ${sourceFunctions.functions.length} functions`, { prefix: "Transfer" });
1183
+ MessageFormatter.info(
1184
+ `DRY RUN: Would transfer ${sourceFunctions.functions.length} functions`,
1185
+ { prefix: "Transfer" }
1186
+ );
862
1187
  return;
863
1188
  }
864
1189
 
865
- const transferTasks = sourceFunctions.functions.map(func =>
1190
+ const transferTasks = sourceFunctions.functions.map((func) =>
866
1191
  this.limit(async () => {
867
1192
  try {
868
1193
  // Check if function exists in target
869
- const existingFunc = targetFunctions.functions.find(tf => tf.$id === func.$id);
870
-
1194
+ const existingFunc = targetFunctions.functions.find(
1195
+ (tf) => tf.$id === func.$id
1196
+ );
1197
+
871
1198
  if (existingFunc) {
872
- MessageFormatter.info(`Function ${func.name} already exists, skipping creation`, { prefix: "Transfer" });
1199
+ MessageFormatter.info(
1200
+ `Function ${func.name} already exists, skipping creation`,
1201
+ { prefix: "Transfer" }
1202
+ );
873
1203
  this.results.functions.skipped++;
874
1204
  return;
875
1205
  }
@@ -877,7 +1207,11 @@ export class ComprehensiveTransfer {
877
1207
  // Download function from source
878
1208
  const functionPath = await this.downloadFunction(func);
879
1209
  if (!functionPath) {
880
- MessageFormatter.error(`Failed to download function ${func.name}`, undefined, { prefix: "Transfer" });
1210
+ MessageFormatter.error(
1211
+ `Failed to download function ${func.name}`,
1212
+ undefined,
1213
+ { prefix: "Transfer" }
1214
+ );
881
1215
  this.results.functions.failed++;
882
1216
  return;
883
1217
  }
@@ -904,26 +1238,45 @@ export class ComprehensiveTransfer {
904
1238
  specification: func.specification as any,
905
1239
  dirPath: functionPath,
906
1240
  };
907
-
908
- await deployLocalFunction(this.targetClient, func.name, functionConfig);
1241
+
1242
+ await deployLocalFunction(
1243
+ this.targetClient,
1244
+ func.name,
1245
+ functionConfig
1246
+ );
909
1247
 
910
1248
  this.results.functions.transferred++;
911
- MessageFormatter.success(`Function ${func.name} transferred successfully`, { prefix: "Transfer" });
1249
+ MessageFormatter.success(
1250
+ `Function ${func.name} transferred successfully`,
1251
+ { prefix: "Transfer" }
1252
+ );
912
1253
  } catch (error) {
913
- MessageFormatter.error(`Function ${func.name} transfer failed`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1254
+ MessageFormatter.error(
1255
+ `Function ${func.name} transfer failed`,
1256
+ error instanceof Error ? error : new Error(String(error)),
1257
+ { prefix: "Transfer" }
1258
+ );
914
1259
  this.results.functions.failed++;
915
1260
  }
916
1261
  })
917
1262
  );
918
1263
 
919
1264
  await Promise.all(transferTasks);
920
- MessageFormatter.success("Function transfer phase completed", { prefix: "Transfer" });
1265
+ MessageFormatter.success("Function transfer phase completed", {
1266
+ prefix: "Transfer",
1267
+ });
921
1268
  } catch (error) {
922
- MessageFormatter.error("Function transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1269
+ MessageFormatter.error(
1270
+ "Function transfer phase failed",
1271
+ error instanceof Error ? error : new Error(String(error)),
1272
+ { prefix: "Transfer" }
1273
+ );
923
1274
  }
924
1275
  }
925
1276
 
926
- private async downloadFunction(func: Models.Function): Promise<string | null> {
1277
+ private async downloadFunction(
1278
+ func: Models.Function
1279
+ ): Promise<string | null> {
927
1280
  try {
928
1281
  const { path } = await downloadLatestFunctionDeployment(
929
1282
  this.sourceClient,
@@ -932,7 +1285,11 @@ export class ComprehensiveTransfer {
932
1285
  );
933
1286
  return path;
934
1287
  } catch (error) {
935
- MessageFormatter.error(`Failed to download function ${func.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1288
+ MessageFormatter.error(
1289
+ `Failed to download function ${func.name}`,
1290
+ error instanceof Error ? error : new Error(String(error)),
1291
+ { prefix: "Transfer" }
1292
+ );
936
1293
  return null;
937
1294
  }
938
1295
  }
@@ -940,7 +1297,10 @@ export class ComprehensiveTransfer {
940
1297
  /**
941
1298
  * Helper method to fetch all collections from a database
942
1299
  */
943
- private async fetchAllCollections(dbId: string, databases: Databases): Promise<Models.Collection[]> {
1300
+ private async fetchAllCollections(
1301
+ dbId: string,
1302
+ databases: Databases
1303
+ ): Promise<Models.Collection[]> {
944
1304
  const collections: Models.Collection[] = [];
945
1305
  let lastId: string | undefined;
946
1306
 
@@ -950,18 +1310,20 @@ export class ComprehensiveTransfer {
950
1310
  queries.push(Query.cursorAfter(lastId));
951
1311
  }
952
1312
 
953
- const result = await tryAwaitWithRetry(async () => databases.listCollections(dbId, queries));
954
-
1313
+ const result = await tryAwaitWithRetry(async () =>
1314
+ databases.listCollections(dbId, queries)
1315
+ );
1316
+
955
1317
  if (result.collections.length === 0) {
956
1318
  break;
957
1319
  }
958
1320
 
959
1321
  collections.push(...result.collections);
960
-
1322
+
961
1323
  if (result.collections.length < 100) {
962
1324
  break;
963
1325
  }
964
-
1326
+
965
1327
  lastId = result.collections[result.collections.length - 1].$id;
966
1328
  }
967
1329
 
@@ -981,18 +1343,20 @@ export class ComprehensiveTransfer {
981
1343
  queries.push(Query.cursorAfter(lastId));
982
1344
  }
983
1345
 
984
- const result = await tryAwaitWithRetry(async () => storage.listBuckets(queries));
985
-
1346
+ const result = await tryAwaitWithRetry(async () =>
1347
+ storage.listBuckets(queries)
1348
+ );
1349
+
986
1350
  if (result.buckets.length === 0) {
987
1351
  break;
988
1352
  }
989
1353
 
990
1354
  buckets.push(...result.buckets);
991
-
1355
+
992
1356
  if (result.buckets.length < 100) {
993
1357
  break;
994
1358
  }
995
-
1359
+
996
1360
  lastId = result.buckets[result.buckets.length - 1].$id;
997
1361
  }
998
1362
 
@@ -1020,7 +1384,7 @@ export class ComprehensiveTransfer {
1020
1384
  twoWay: attr.twoWay,
1021
1385
  twoWayKey: attr.twoWayKey,
1022
1386
  onDelete: attr.onDelete,
1023
- side: attr.side
1387
+ side: attr.side,
1024
1388
  };
1025
1389
  }
1026
1390
 
@@ -1034,8 +1398,10 @@ export class ComprehensiveTransfer {
1034
1398
  attributes: any[]
1035
1399
  ): Promise<boolean> {
1036
1400
  // Import the enhanced attribute creation function
1037
- const { createUpdateCollectionAttributesWithStatusCheck } = await import("../collections/attributes.js");
1038
-
1401
+ const { createUpdateCollectionAttributesWithStatusCheck } = await import(
1402
+ "../collections/attributes.js"
1403
+ );
1404
+
1039
1405
  return await createUpdateCollectionAttributesWithStatusCheck(
1040
1406
  databases,
1041
1407
  dbId,
@@ -1055,8 +1421,10 @@ export class ComprehensiveTransfer {
1055
1421
  indexes: any[]
1056
1422
  ): Promise<boolean> {
1057
1423
  // Import the enhanced index creation function
1058
- const { createOrUpdateIndexesWithStatusCheck } = await import("../collections/indexes.js");
1059
-
1424
+ const { createOrUpdateIndexesWithStatusCheck } = await import(
1425
+ "../collections/indexes.js"
1426
+ );
1427
+
1060
1428
  return await createOrUpdateIndexesWithStatusCheck(
1061
1429
  dbId,
1062
1430
  databases,
@@ -1077,7 +1445,10 @@ export class ComprehensiveTransfer {
1077
1445
  sourceCollectionId: string,
1078
1446
  targetCollectionId: string
1079
1447
  ): Promise<void> {
1080
- MessageFormatter.info(`Transferring documents from ${sourceCollectionId} to ${targetCollectionId} with bulk operations, content comparison, and permission filtering`, { prefix: "Transfer" });
1448
+ MessageFormatter.info(
1449
+ `Transferring documents from ${sourceCollectionId} to ${targetCollectionId} with bulk operations, content comparison, and permission filtering`,
1450
+ { prefix: "Transfer" }
1451
+ );
1081
1452
 
1082
1453
  let lastId: string | undefined;
1083
1454
  let totalTransferred = 0;
@@ -1085,11 +1456,14 @@ export class ComprehensiveTransfer {
1085
1456
  let totalUpdated = 0;
1086
1457
 
1087
1458
  // 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
-
1459
+ const bulkEnabled = false;
1460
+ // Temporarily disable to see if it fixes my permissions issues
1461
+ const supportsBulk = bulkEnabled ? this.options.targetEndpoint.includes("cloud.appwrite.io") : false;
1462
+
1091
1463
  if (supportsBulk) {
1092
- MessageFormatter.info(`Using bulk operations for enhanced performance`, { prefix: "Transfer" });
1464
+ MessageFormatter.info(`Using bulk operations for enhanced performance`, {
1465
+ prefix: "Transfer",
1466
+ });
1093
1467
  }
1094
1468
 
1095
1469
  while (true) {
@@ -1099,7 +1473,7 @@ export class ComprehensiveTransfer {
1099
1473
  queries.push(Query.cursorAfter(lastId));
1100
1474
  }
1101
1475
 
1102
- const sourceDocuments = await tryAwaitWithRetry(async () =>
1476
+ const sourceDocuments = await tryAwaitWithRetry(async () =>
1103
1477
  sourceDb.listDocuments(sourceDbId, sourceCollectionId, queries)
1104
1478
  );
1105
1479
 
@@ -1107,87 +1481,90 @@ export class ComprehensiveTransfer {
1107
1481
  break;
1108
1482
  }
1109
1483
 
1110
- MessageFormatter.info(`Processing batch of ${sourceDocuments.documents.length} source documents`, { prefix: "Transfer" });
1484
+ MessageFormatter.info(
1485
+ `Processing batch of ${sourceDocuments.documents.length} source documents`,
1486
+ { prefix: "Transfer" }
1487
+ );
1111
1488
 
1112
1489
  // Extract document IDs from the current batch
1113
- const sourceDocIds = sourceDocuments.documents.map(doc => doc.$id);
1114
-
1490
+ const sourceDocIds = sourceDocuments.documents.map((doc) => doc.$id);
1491
+
1115
1492
  // Fetch existing documents from target in a single query
1116
1493
  const existingTargetDocs = await this.fetchTargetDocumentsBatch(
1117
- targetDb,
1118
- targetDbId,
1119
- targetCollectionId,
1494
+ targetDb,
1495
+ targetDbId,
1496
+ targetCollectionId,
1120
1497
  sourceDocIds
1121
1498
  );
1122
1499
 
1123
1500
  // Create a map for quick lookup of existing documents
1124
1501
  const existingDocsMap = new Map<string, Models.Document>();
1125
- existingTargetDocs.forEach(doc => {
1502
+ existingTargetDocs.forEach((doc) => {
1126
1503
  existingDocsMap.set(doc.$id, doc);
1127
1504
  });
1128
1505
 
1129
1506
  // Filter documents based on existence, content comparison, and permission comparison
1130
1507
  const documentsToTransfer: Models.Document[] = [];
1131
- const documentsToUpdate: { doc: Models.Document; targetDoc: Models.Document; reason: string }[] = [];
1508
+ const documentsToUpdate: {
1509
+ doc: Models.Document;
1510
+ targetDoc: Models.Document;
1511
+ reason: string;
1512
+ }[] = [];
1132
1513
 
1133
1514
  for (const sourceDoc of sourceDocuments.documents) {
1134
1515
  const existingTargetDoc = existingDocsMap.get(sourceDoc.$id);
1135
-
1516
+
1136
1517
  if (!existingTargetDoc) {
1137
1518
  // Document doesn't exist in target, needs to be transferred
1138
1519
  documentsToTransfer.push(sourceDoc);
1139
1520
  } else {
1140
1521
  // 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
-
1522
+ const sourcePermissions = Array.from(
1523
+ new Set(sourceDoc.$permissions || [])
1524
+ ).sort();
1525
+ const targetPermissions = Array.from(
1526
+ new Set(existingTargetDoc.$permissions || [])
1527
+ ).sort();
1528
+ const permissionsDiffer =
1529
+ sourcePermissions.join(",") !== targetPermissions.join(",") ||
1530
+ sourcePermissions.length !== targetPermissions.length;
1531
+
1145
1532
  // Use objectNeedsUpdate to compare document content (excluding system fields)
1146
- const contentDiffers = objectNeedsUpdate(existingTargetDoc, sourceDoc);
1147
-
1533
+ const contentDiffers = objectNeedsUpdate(
1534
+ existingTargetDoc,
1535
+ sourceDoc
1536
+ );
1537
+
1148
1538
  if (contentDiffers && permissionsDiffer) {
1149
1539
  // Both content and permissions differ
1150
- documentsToUpdate.push({
1151
- doc: sourceDoc,
1152
- targetDoc: existingTargetDoc,
1153
- reason: "content and permissions differ"
1540
+ documentsToUpdate.push({
1541
+ doc: sourceDoc,
1542
+ targetDoc: existingTargetDoc,
1543
+ reason: "content and permissions differ",
1154
1544
  });
1155
- MessageFormatter.info(
1156
- `Document ${sourceDoc.$id} exists but content and permissions differ - will update`,
1157
- { prefix: "Transfer" }
1158
- );
1159
1545
  } else if (contentDiffers) {
1160
1546
  // Only content differs
1161
- documentsToUpdate.push({
1162
- doc: sourceDoc,
1163
- targetDoc: existingTargetDoc,
1164
- reason: "content differs"
1547
+ documentsToUpdate.push({
1548
+ doc: sourceDoc,
1549
+ targetDoc: existingTargetDoc,
1550
+ reason: "content differs",
1165
1551
  });
1166
- MessageFormatter.info(
1167
- `Document ${sourceDoc.$id} exists but content differs - will update`,
1168
- { prefix: "Transfer" }
1169
- );
1170
1552
  } else if (permissionsDiffer) {
1171
1553
  // Only permissions differ
1172
- documentsToUpdate.push({
1173
- doc: sourceDoc,
1174
- targetDoc: existingTargetDoc,
1175
- reason: "permissions differ"
1554
+ documentsToUpdate.push({
1555
+ doc: sourceDoc,
1556
+ targetDoc: existingTargetDoc,
1557
+ reason: "permissions differ",
1176
1558
  });
1177
- MessageFormatter.info(
1178
- `Document ${sourceDoc.$id} exists but permissions differ - will update`,
1179
- { prefix: "Transfer" }
1180
- );
1181
1559
  } else {
1182
1560
  // Document exists with identical content AND permissions, skip
1183
1561
  totalSkipped++;
1184
- MessageFormatter.info(`Document ${sourceDoc.$id} exists with matching content and permissions - skipping`, { prefix: "Transfer" });
1185
1562
  }
1186
1563
  }
1187
1564
  }
1188
1565
 
1189
1566
  MessageFormatter.info(
1190
- `Batch analysis: ${documentsToTransfer.length} to create, ${documentsToUpdate.length} to update, ${totalSkipped} skipped so far`,
1567
+ `Batch analysis: ${documentsToTransfer.length} to create, ${documentsToUpdate.length} to update, ${totalSkipped} skipped so far`,
1191
1568
  { prefix: "Transfer" }
1192
1569
  );
1193
1570
 
@@ -1202,7 +1579,6 @@ export class ComprehensiveTransfer {
1202
1579
  documentsToTransfer
1203
1580
  );
1204
1581
  totalTransferred += documentsToTransfer.length;
1205
- MessageFormatter.success(`Bulk transferred ${documentsToTransfer.length} new documents`, { prefix: "Transfer" });
1206
1582
  } else {
1207
1583
  // Use individual transfers for smaller batches or non-bulk endpoints
1208
1584
  const transferCount = await this.transferDocumentsIndividual(
@@ -1230,11 +1606,12 @@ export class ComprehensiveTransfer {
1230
1606
  break;
1231
1607
  }
1232
1608
 
1233
- lastId = sourceDocuments.documents[sourceDocuments.documents.length - 1].$id;
1609
+ lastId =
1610
+ sourceDocuments.documents[sourceDocuments.documents.length - 1].$id;
1234
1611
  }
1235
1612
 
1236
1613
  MessageFormatter.info(
1237
- `Transfer complete: ${totalTransferred} new, ${totalUpdated} updated, ${totalSkipped} skipped from ${sourceCollectionId} to ${targetCollectionId}`,
1614
+ `Transfer complete: ${totalTransferred} new, ${totalUpdated} updated, ${totalSkipped} skipped from ${sourceCollectionId} to ${targetCollectionId}`,
1238
1615
  { prefix: "Transfer" }
1239
1616
  );
1240
1617
  }
@@ -1249,29 +1626,33 @@ export class ComprehensiveTransfer {
1249
1626
  docIds: string[]
1250
1627
  ): Promise<Models.Document[]> {
1251
1628
  const documents: Models.Document[] = [];
1252
-
1629
+
1253
1630
  // Split IDs into chunks of 100 for Query.equal limitations
1254
1631
  const idChunks = this.chunkArray(docIds, 100);
1255
-
1632
+
1256
1633
  for (const chunk of idChunks) {
1257
1634
  try {
1258
- const result = await tryAwaitWithRetry(async () =>
1635
+ const result = await tryAwaitWithRetry(async () =>
1259
1636
  targetDb.listDocuments(targetDbId, targetCollectionId, [
1260
- Query.equal('$id', chunk),
1261
- Query.limit(100)
1637
+ Query.equal("$id", chunk),
1638
+ Query.limit(100),
1262
1639
  ])
1263
1640
  );
1264
1641
  documents.push(...result.documents);
1265
1642
  } catch (error) {
1266
1643
  // If query fails, fall back to individual gets (less efficient but more reliable)
1267
1644
  MessageFormatter.warning(
1268
- `Batch query failed for ${chunk.length} documents, falling back to individual checks`,
1645
+ `Batch query failed for ${chunk.length} documents, falling back to individual checks`,
1269
1646
  { prefix: "Transfer" }
1270
1647
  );
1271
-
1648
+
1272
1649
  for (const docId of chunk) {
1273
1650
  try {
1274
- const doc = await targetDb.getDocument(targetDbId, targetCollectionId, docId);
1651
+ const doc = await targetDb.getDocument(
1652
+ targetDbId,
1653
+ targetCollectionId,
1654
+ docId
1655
+ );
1275
1656
  documents.push(doc);
1276
1657
  } catch (getError) {
1277
1658
  // Document doesn't exist, which is fine
@@ -1279,7 +1660,7 @@ export class ComprehensiveTransfer {
1279
1660
  }
1280
1661
  }
1281
1662
  }
1282
-
1663
+
1283
1664
  return documents;
1284
1665
  }
1285
1666
 
@@ -1293,12 +1674,20 @@ export class ComprehensiveTransfer {
1293
1674
  documents: Models.Document[]
1294
1675
  ): Promise<void> {
1295
1676
  // Prepare documents for bulk upsert
1296
- const preparedDocs = documents.map(doc => {
1297
- const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
1298
- return {
1677
+ const preparedDocs = documents.map((doc) => {
1678
+ const {
1299
1679
  $id,
1680
+ $createdAt,
1681
+ $updatedAt,
1300
1682
  $permissions,
1683
+ $databaseId,
1684
+ $collectionId,
1301
1685
  ...docData
1686
+ } = doc;
1687
+ return {
1688
+ $id,
1689
+ $permissions,
1690
+ ...docData,
1302
1691
  };
1303
1692
  });
1304
1693
 
@@ -1308,31 +1697,27 @@ export class ComprehensiveTransfer {
1308
1697
 
1309
1698
  for (const maxBatchSize of batchSizes) {
1310
1699
  const documentBatches = this.chunkArray(preparedDocs, maxBatchSize);
1311
-
1700
+
1312
1701
  try {
1313
1702
  for (const batch of documentBatches) {
1314
- MessageFormatter.info(`Bulk upserting ${batch.length} documents...`, { prefix: "Transfer" });
1315
-
1316
1703
  await this.bulkUpsertDocuments(
1317
1704
  this.targetClient,
1318
1705
  targetDbId,
1319
1706
  targetCollectionId,
1320
1707
  batch
1321
1708
  );
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
- }
1709
+
1710
+ MessageFormatter.success(
1711
+ `✅ Bulk upserted ${batch.length} documents`,
1712
+ { prefix: "Transfer" }
1713
+ );
1329
1714
  }
1330
-
1715
+
1331
1716
  processed = true;
1332
1717
  break; // Success, exit batch size loop
1333
1718
  } catch (error) {
1334
1719
  MessageFormatter.warning(
1335
- `Bulk upsert with batch size ${maxBatchSize} failed, trying smaller size...`,
1720
+ `Bulk upsert with batch size ${maxBatchSize} failed, trying smaller size...`,
1336
1721
  { prefix: "Transfer" }
1337
1722
  );
1338
1723
  continue; // Try next smaller batch size
@@ -1341,12 +1726,17 @@ export class ComprehensiveTransfer {
1341
1726
 
1342
1727
  if (!processed) {
1343
1728
  MessageFormatter.warning(
1344
- `All bulk operations failed, falling back to individual transfers`,
1729
+ `All bulk operations failed, falling back to individual transfers`,
1345
1730
  { prefix: "Transfer" }
1346
1731
  );
1347
-
1732
+
1348
1733
  // Fall back to individual transfers
1349
- await this.transferDocumentsIndividual(targetDb, targetDbId, targetCollectionId, documents);
1734
+ await this.transferDocumentsIndividual(
1735
+ targetDb,
1736
+ targetDbId,
1737
+ targetCollectionId,
1738
+ documents
1739
+ );
1350
1740
  }
1351
1741
  }
1352
1742
 
@@ -1361,24 +1751,30 @@ export class ComprehensiveTransfer {
1361
1751
  ): Promise<any> {
1362
1752
  const apiPath = `/databases/${dbId}/collections/${collectionId}/documents`;
1363
1753
  const url = new URL(client.config.endpoint + apiPath);
1364
-
1754
+
1365
1755
  const headers = {
1366
- 'Content-Type': 'application/json',
1367
- 'X-Appwrite-Project': client.config.project,
1368
- 'X-Appwrite-Key': client.config.key
1756
+ "Content-Type": "application/json",
1757
+ "X-Appwrite-Project": client.config.project,
1758
+ "X-Appwrite-Key": client.config.key,
1369
1759
  };
1370
-
1760
+
1371
1761
  const response = await fetch(url.toString(), {
1372
- method: 'PUT',
1762
+ method: "PUT",
1373
1763
  headers,
1374
- body: JSON.stringify({ documents })
1764
+ body: JSON.stringify({ documents }),
1375
1765
  });
1376
-
1766
+
1377
1767
  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'}`);
1768
+ const errorData: any = await response
1769
+ .json()
1770
+ .catch(() => ({ message: "Unknown error" }));
1771
+ throw new Error(
1772
+ `Bulk upsert failed: ${response.status} - ${
1773
+ errorData.message || "Unknown error"
1774
+ }`
1775
+ );
1380
1776
  }
1381
-
1777
+
1382
1778
  return await response.json();
1383
1779
  }
1384
1780
 
@@ -1393,11 +1789,19 @@ export class ComprehensiveTransfer {
1393
1789
  ): Promise<number> {
1394
1790
  let successCount = 0;
1395
1791
 
1396
- const transferTasks = documents.map(doc =>
1792
+ const transferTasks = documents.map((doc) =>
1397
1793
  this.limit(async () => {
1398
1794
  try {
1399
- const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
1400
-
1795
+ const {
1796
+ $id,
1797
+ $createdAt,
1798
+ $updatedAt,
1799
+ $permissions,
1800
+ $databaseId,
1801
+ $collectionId,
1802
+ ...docData
1803
+ } = doc;
1804
+
1401
1805
  await tryAwaitWithRetry(async () =>
1402
1806
  targetDb.createDocument(
1403
1807
  targetDbId,
@@ -1409,11 +1813,47 @@ export class ComprehensiveTransfer {
1409
1813
  );
1410
1814
 
1411
1815
  successCount++;
1412
- MessageFormatter.success(`Transferred document ${doc.$id}`, { prefix: "Transfer" });
1413
1816
  } catch (error) {
1817
+ if (
1818
+ error instanceof AppwriteException &&
1819
+ error.message.includes("already exists")
1820
+ ) {
1821
+ try {
1822
+ // Update it! It's here because it needs an update or a create
1823
+ const {
1824
+ $id,
1825
+ $createdAt,
1826
+ $updatedAt,
1827
+ $permissions,
1828
+ $databaseId,
1829
+ $collectionId,
1830
+ ...docData
1831
+ } = doc;
1832
+ await tryAwaitWithRetry(async () =>
1833
+ targetDb.updateDocument(
1834
+ targetDbId,
1835
+ targetCollectionId,
1836
+ doc.$id,
1837
+ docData,
1838
+ doc.$permissions
1839
+ )
1840
+ );
1841
+ successCount++;
1842
+ } catch (updateError) {
1843
+ // just send the error to the formatter
1844
+ MessageFormatter.error(
1845
+ `Failed to transfer document ${doc.$id}`,
1846
+ updateError instanceof Error
1847
+ ? updateError
1848
+ : new Error(String(updateError)),
1849
+ { prefix: "Transfer" }
1850
+ );
1851
+ }
1852
+ }
1853
+
1414
1854
  MessageFormatter.error(
1415
- `Failed to transfer document ${doc.$id}`,
1416
- error instanceof Error ? error : new Error(String(error)),
1855
+ `Failed to transfer document ${doc.$id}`,
1856
+ error instanceof Error ? error : new Error(String(error)),
1417
1857
  { prefix: "Transfer" }
1418
1858
  );
1419
1859
  }
@@ -1431,15 +1871,27 @@ export class ComprehensiveTransfer {
1431
1871
  targetDb: Databases,
1432
1872
  targetDbId: string,
1433
1873
  targetCollectionId: string,
1434
- documentPairs: { doc: Models.Document; targetDoc: Models.Document; reason: string }[]
1874
+ documentPairs: {
1875
+ doc: Models.Document;
1876
+ targetDoc: Models.Document;
1877
+ reason: string;
1878
+ }[]
1435
1879
  ): Promise<number> {
1436
1880
  let successCount = 0;
1437
1881
 
1438
- const updateTasks = documentPairs.map(({ doc, targetDoc, reason }) =>
1882
+ const updateTasks = documentPairs.map(({ doc, targetDoc, reason }) =>
1439
1883
  this.limit(async () => {
1440
1884
  try {
1441
- const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
1442
-
1885
+ const {
1886
+ $id,
1887
+ $createdAt,
1888
+ $updatedAt,
1889
+ $permissions,
1890
+ $databaseId,
1891
+ $collectionId,
1892
+ ...docData
1893
+ } = doc;
1894
+
1443
1895
  await tryAwaitWithRetry(async () =>
1444
1896
  targetDb.updateDocument(
1445
1897
  targetDbId,
@@ -1451,14 +1903,10 @@ export class ComprehensiveTransfer {
1451
1903
  );
1452
1904
 
1453
1905
  successCount++;
1454
- MessageFormatter.success(
1455
- `Updated document ${doc.$id} (${reason}) - permissions: [${targetDoc.$permissions?.join(', ')}] → [${doc.$permissions?.join(', ')}]`,
1456
- { prefix: "Transfer" }
1457
- );
1458
1906
  } catch (error) {
1459
1907
  MessageFormatter.error(
1460
- `Failed to update document ${doc.$id} (${reason})`,
1461
- error instanceof Error ? error : new Error(String(error)),
1908
+ `Failed to update document ${doc.$id} (${reason})`,
1909
+ error instanceof Error ? error : new Error(String(error)),
1462
1910
  { prefix: "Transfer" }
1463
1911
  );
1464
1912
  }
@@ -1483,7 +1931,9 @@ export class ComprehensiveTransfer {
1483
1931
  /**
1484
1932
  * Helper method to fetch all teams with pagination
1485
1933
  */
1486
- private async fetchAllTeams(teams: Teams): Promise<Models.Team<Models.Preferences>[]> {
1934
+ private async fetchAllTeams(
1935
+ teams: Teams
1936
+ ): Promise<Models.Team<Models.Preferences>[]> {
1487
1937
  const teamsList: Models.Team<Models.Preferences>[] = [];
1488
1938
  let lastId: string | undefined;
1489
1939
 
@@ -1494,17 +1944,17 @@ export class ComprehensiveTransfer {
1494
1944
  }
1495
1945
 
1496
1946
  const result = await tryAwaitWithRetry(async () => teams.list(queries));
1497
-
1947
+
1498
1948
  if (result.teams.length === 0) {
1499
1949
  break;
1500
1950
  }
1501
1951
 
1502
1952
  teamsList.push(...result.teams);
1503
-
1953
+
1504
1954
  if (result.teams.length < 100) {
1505
1955
  break;
1506
1956
  }
1507
-
1957
+
1508
1958
  lastId = result.teams[result.teams.length - 1].$id;
1509
1959
  }
1510
1960
 
@@ -1514,7 +1964,9 @@ export class ComprehensiveTransfer {
1514
1964
  /**
1515
1965
  * Helper method to fetch all memberships for a team with pagination
1516
1966
  */
1517
- private async fetchAllMemberships(teamId: string): Promise<Models.Membership[]> {
1967
+ private async fetchAllMemberships(
1968
+ teamId: string
1969
+ ): Promise<Models.Membership[]> {
1518
1970
  const membershipsList: Models.Membership[] = [];
1519
1971
  let lastId: string | undefined;
1520
1972
 
@@ -1524,20 +1976,20 @@ export class ComprehensiveTransfer {
1524
1976
  queries.push(Query.cursorAfter(lastId));
1525
1977
  }
1526
1978
 
1527
- const result = await tryAwaitWithRetry(async () =>
1979
+ const result = await tryAwaitWithRetry(async () =>
1528
1980
  this.sourceTeams.listMemberships(teamId, queries)
1529
1981
  );
1530
-
1982
+
1531
1983
  if (result.memberships.length === 0) {
1532
1984
  break;
1533
1985
  }
1534
1986
 
1535
1987
  membershipsList.push(...result.memberships);
1536
-
1988
+
1537
1989
  if (result.memberships.length < 100) {
1538
1990
  break;
1539
1991
  }
1540
-
1992
+
1541
1993
  lastId = result.memberships[result.memberships.length - 1].$id;
1542
1994
  }
1543
1995
 
@@ -1548,40 +2000,55 @@ export class ComprehensiveTransfer {
1548
2000
  * Helper method to transfer team memberships
1549
2001
  */
1550
2002
  private async transferTeamMemberships(teamId: string): Promise<void> {
1551
- MessageFormatter.info(`Transferring memberships for team ${teamId}`, { prefix: "Transfer" });
2003
+ MessageFormatter.info(`Transferring memberships for team ${teamId}`, {
2004
+ prefix: "Transfer",
2005
+ });
1552
2006
 
1553
2007
  try {
1554
2008
  // Fetch all memberships for this team
1555
2009
  const memberships = await this.fetchAllMemberships(teamId);
1556
-
2010
+
1557
2011
  if (memberships.length === 0) {
1558
- MessageFormatter.info(`No memberships found for team ${teamId}`, { prefix: "Transfer" });
2012
+ MessageFormatter.info(`No memberships found for team ${teamId}`, {
2013
+ prefix: "Transfer",
2014
+ });
1559
2015
  return;
1560
2016
  }
1561
2017
 
1562
- MessageFormatter.info(`Found ${memberships.length} memberships for team ${teamId}`, { prefix: "Transfer" });
2018
+ MessageFormatter.info(
2019
+ `Found ${memberships.length} memberships for team ${teamId}`,
2020
+ { prefix: "Transfer" }
2021
+ );
1563
2022
 
1564
2023
  let totalTransferred = 0;
1565
2024
 
1566
2025
  // Transfer memberships with rate limiting
1567
- const transferTasks = memberships.map(membership =>
1568
- this.userLimit(async () => { // Use userLimit for team operations (more sensitive)
2026
+ const transferTasks = memberships.map((membership) =>
2027
+ this.userLimit(async () => {
2028
+ // Use userLimit for team operations (more sensitive)
1569
2029
  try {
1570
2030
  // Check if membership already exists and compare roles
1571
2031
  let existingMembership: Models.Membership | null = null;
1572
2032
  try {
1573
- existingMembership = await this.targetTeams.getMembership(teamId, membership.$id);
1574
-
2033
+ existingMembership = await this.targetTeams.getMembership(
2034
+ teamId,
2035
+ membership.$id
2036
+ );
2037
+
1575
2038
  // Compare roles between source and target membership
1576
- const sourceRoles = JSON.stringify(membership.roles?.sort() || []);
1577
- const targetRoles = JSON.stringify(existingMembership.roles?.sort() || []);
1578
-
2039
+ const sourceRoles = JSON.stringify(
2040
+ membership.roles?.sort() || []
2041
+ );
2042
+ const targetRoles = JSON.stringify(
2043
+ existingMembership.roles?.sort() || []
2044
+ );
2045
+
1579
2046
  if (sourceRoles !== targetRoles) {
1580
2047
  MessageFormatter.warning(
1581
- `Membership ${membership.$id} exists but has different roles. Source: ${sourceRoles}, Target: ${targetRoles}`,
2048
+ `Membership ${membership.$id} exists but has different roles. Source: ${sourceRoles}, Target: ${targetRoles}`,
1582
2049
  { prefix: "Transfer" }
1583
2050
  );
1584
-
2051
+
1585
2052
  // Update membership roles to match source
1586
2053
  try {
1587
2054
  await this.targetTeams.updateMembership(
@@ -1589,16 +2056,24 @@ export class ComprehensiveTransfer {
1589
2056
  membership.$id,
1590
2057
  membership.roles
1591
2058
  );
1592
- MessageFormatter.success(`Updated membership ${membership.$id} roles to match source`, { prefix: "Transfer" });
2059
+ MessageFormatter.success(
2060
+ `Updated membership ${membership.$id} roles to match source`,
2061
+ { prefix: "Transfer" }
2062
+ );
1593
2063
  } catch (updateError) {
1594
2064
  MessageFormatter.error(
1595
- `Failed to update roles for membership ${membership.$id}`,
1596
- updateError instanceof Error ? updateError : new Error(String(updateError)),
2065
+ `Failed to update roles for membership ${membership.$id}`,
2066
+ updateError instanceof Error
2067
+ ? updateError
2068
+ : new Error(String(updateError)),
1597
2069
  { prefix: "Transfer" }
1598
2070
  );
1599
2071
  }
1600
2072
  } else {
1601
- MessageFormatter.info(`Membership ${membership.$id} already exists with matching roles, skipping`, { prefix: "Transfer" });
2073
+ MessageFormatter.info(
2074
+ `Membership ${membership.$id} already exists with matching roles, skipping`,
2075
+ { prefix: "Transfer" }
2076
+ );
1602
2077
  }
1603
2078
  return;
1604
2079
  } catch (error) {
@@ -1610,7 +2085,10 @@ export class ComprehensiveTransfer {
1610
2085
  try {
1611
2086
  userData = await this.targetUsers.get(membership.userId);
1612
2087
  } catch (error) {
1613
- MessageFormatter.warning(`User ${membership.userId} not found in target, membership ${membership.$id} may fail`, { prefix: "Transfer" });
2088
+ MessageFormatter.warning(
2089
+ `User ${membership.userId} not found in target, membership ${membership.$id} may fail`,
2090
+ { prefix: "Transfer" }
2091
+ );
1614
2092
  }
1615
2093
 
1616
2094
  // Create membership using the comprehensive user data
@@ -1627,38 +2105,87 @@ export class ComprehensiveTransfer {
1627
2105
  );
1628
2106
 
1629
2107
  totalTransferred++;
1630
- MessageFormatter.success(`Transferred membership ${membership.$id} for user ${userData?.name || membership.userName}`, { prefix: "Transfer" });
2108
+ MessageFormatter.success(
2109
+ `Transferred membership ${membership.$id} for user ${
2110
+ userData?.name || membership.userName
2111
+ }`,
2112
+ { prefix: "Transfer" }
2113
+ );
1631
2114
  } catch (error) {
1632
- MessageFormatter.error(`Failed to transfer membership ${membership.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
2115
+ MessageFormatter.error(
2116
+ `Failed to transfer membership ${membership.$id}`,
2117
+ error instanceof Error ? error : new Error(String(error)),
2118
+ { prefix: "Transfer" }
2119
+ );
1633
2120
  }
1634
2121
  })
1635
2122
  );
1636
2123
 
1637
2124
  await Promise.all(transferTasks);
1638
- MessageFormatter.info(`Transferred ${totalTransferred} memberships for team ${teamId}`, { prefix: "Transfer" });
2125
+ MessageFormatter.info(
2126
+ `Transferred ${totalTransferred} memberships for team ${teamId}`,
2127
+ { prefix: "Transfer" }
2128
+ );
1639
2129
  } catch (error) {
1640
- MessageFormatter.error(`Failed to transfer memberships for team ${teamId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
2130
+ MessageFormatter.error(
2131
+ `Failed to transfer memberships for team ${teamId}`,
2132
+ error instanceof Error ? error : new Error(String(error)),
2133
+ { prefix: "Transfer" }
2134
+ );
1641
2135
  }
1642
2136
  }
1643
2137
 
1644
2138
  private printSummary(): void {
1645
2139
  const duration = Math.round((Date.now() - this.startTime) / 1000);
1646
-
1647
- MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", { prefix: "Transfer" });
2140
+
2141
+ MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", {
2142
+ prefix: "Transfer",
2143
+ });
1648
2144
  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
-
2145
+ MessageFormatter.info(
2146
+ `Users: ${this.results.users.transferred} transferred, ${this.results.users.skipped} skipped, ${this.results.users.failed} failed`,
2147
+ { prefix: "Transfer" }
2148
+ );
2149
+ MessageFormatter.info(
2150
+ `Teams: ${this.results.teams.transferred} transferred, ${this.results.teams.skipped} skipped, ${this.results.teams.failed} failed`,
2151
+ { prefix: "Transfer" }
2152
+ );
2153
+ MessageFormatter.info(
2154
+ `Databases: ${this.results.databases.transferred} transferred, ${this.results.databases.skipped} skipped, ${this.results.databases.failed} failed`,
2155
+ { prefix: "Transfer" }
2156
+ );
2157
+ MessageFormatter.info(
2158
+ `Buckets: ${this.results.buckets.transferred} transferred, ${this.results.buckets.skipped} skipped, ${this.results.buckets.failed} failed`,
2159
+ { prefix: "Transfer" }
2160
+ );
2161
+ MessageFormatter.info(
2162
+ `Functions: ${this.results.functions.transferred} transferred, ${this.results.functions.skipped} skipped, ${this.results.functions.failed} failed`,
2163
+ { prefix: "Transfer" }
2164
+ );
2165
+
2166
+ const totalTransferred =
2167
+ this.results.users.transferred +
2168
+ this.results.teams.transferred +
2169
+ this.results.databases.transferred +
2170
+ this.results.buckets.transferred +
2171
+ this.results.functions.transferred;
2172
+ const totalFailed =
2173
+ this.results.users.failed +
2174
+ this.results.teams.failed +
2175
+ this.results.databases.failed +
2176
+ this.results.buckets.failed +
2177
+ this.results.functions.failed;
2178
+
1658
2179
  if (totalFailed === 0) {
1659
- MessageFormatter.success(`All ${totalTransferred} items transferred successfully!`, { prefix: "Transfer" });
2180
+ MessageFormatter.success(
2181
+ `All ${totalTransferred} items transferred successfully!`,
2182
+ { prefix: "Transfer" }
2183
+ );
1660
2184
  } else {
1661
- MessageFormatter.warning(`${totalTransferred} items transferred, ${totalFailed} failed`, { prefix: "Transfer" });
2185
+ MessageFormatter.warning(
2186
+ `${totalTransferred} items transferred, ${totalFailed} failed`,
2187
+ { prefix: "Transfer" }
2188
+ );
1662
2189
  }
1663
2190
  }
1664
- }
2191
+ }