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,12 +1,12 @@
1
- import { converterFunctions, tryAwaitWithRetry, parseAttribute, objectNeedsUpdate } from "appwrite-utils";
2
- import { Client, Databases, Storage, Users, Functions, Teams, Query, } from "node-appwrite";
1
+ import { converterFunctions, tryAwaitWithRetry, parseAttribute, objectNeedsUpdate, } from "appwrite-utils";
2
+ import { Client, Databases, Storage, Users, Functions, Teams, Query, AppwriteException, } from "node-appwrite";
3
3
  import { InputFile } from "node-appwrite/file";
4
4
  import { MessageFormatter } from "../shared/messageFormatter.js";
5
5
  import { ProgressManager } from "../shared/progressManager.js";
6
6
  import { getClient } from "../utils/getClientFromConfig.js";
7
- import { transferDatabaseLocalToLocal, transferDatabaseLocalToRemote, transferStorageLocalToLocal, transferStorageLocalToRemote, transferUsersLocalToRemote } from "./transfer.js";
7
+ import { transferDatabaseLocalToLocal, transferDatabaseLocalToRemote, transferStorageLocalToLocal, transferStorageLocalToRemote, transferUsersLocalToRemote, } from "./transfer.js";
8
8
  import { deployLocalFunction } from "../functions/deployments.js";
9
- import { listFunctions, downloadLatestFunctionDeployment } from "../functions/methods.js";
9
+ import { listFunctions, downloadLatestFunctionDeployment, } from "../functions/methods.js";
10
10
  import pLimit from "p-limit";
11
11
  import chalk from "chalk";
12
12
  import { join } from "node:path";
@@ -66,9 +66,13 @@ export class ComprehensiveTransfer {
66
66
  }
67
67
  async execute() {
68
68
  try {
69
- MessageFormatter.info("Starting comprehensive transfer", { prefix: "Transfer" });
69
+ MessageFormatter.info("Starting comprehensive transfer", {
70
+ prefix: "Transfer",
71
+ });
70
72
  if (this.options.dryRun) {
71
- MessageFormatter.info("DRY RUN MODE - No actual changes will be made", { prefix: "Transfer" });
73
+ MessageFormatter.info("DRY RUN MODE - No actual changes will be made", {
74
+ prefix: "Transfer",
75
+ });
72
76
  }
73
77
  // Show rate limiting configuration
74
78
  const baseLimit = this.options.concurrencyLimit || 10;
@@ -111,7 +115,9 @@ export class ComprehensiveTransfer {
111
115
  }
112
116
  }
113
117
  async transferAllUsers() {
114
- MessageFormatter.info("Starting user transfer phase", { prefix: "Transfer" });
118
+ MessageFormatter.info("Starting user transfer phase", {
119
+ prefix: "Transfer",
120
+ });
115
121
  if (this.options.dryRun) {
116
122
  const usersList = await this.sourceUsers.list([Query.limit(1)]);
117
123
  MessageFormatter.info(`DRY RUN: Would transfer ${usersList.total} users`, { prefix: "Transfer" });
@@ -125,7 +131,9 @@ export class ComprehensiveTransfer {
125
131
  // Get actual count for results
126
132
  const usersList = await this.sourceUsers.list([Query.limit(1)]);
127
133
  this.results.users.transferred = usersList.total;
128
- MessageFormatter.success(`User transfer completed`, { prefix: "Transfer" });
134
+ MessageFormatter.success(`User transfer completed`, {
135
+ prefix: "Transfer",
136
+ });
129
137
  }
130
138
  catch (error) {
131
139
  MessageFormatter.error("User transfer failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
@@ -133,7 +141,9 @@ export class ComprehensiveTransfer {
133
141
  }
134
142
  }
135
143
  async transferAllTeams() {
136
- MessageFormatter.info("Starting team transfer phase", { prefix: "Transfer" });
144
+ MessageFormatter.info("Starting team transfer phase", {
145
+ prefix: "Transfer",
146
+ });
137
147
  try {
138
148
  // Fetch all teams from source with pagination
139
149
  const allSourceTeams = await this.fetchAllTeams(this.sourceTeams);
@@ -141,37 +151,41 @@ export class ComprehensiveTransfer {
141
151
  if (this.options.dryRun) {
142
152
  let totalMemberships = 0;
143
153
  for (const team of allSourceTeams) {
144
- const memberships = await this.sourceTeams.listMemberships(team.$id, [Query.limit(1)]);
154
+ const memberships = await this.sourceTeams.listMemberships(team.$id, [
155
+ Query.limit(1),
156
+ ]);
145
157
  totalMemberships += memberships.total;
146
158
  }
147
159
  MessageFormatter.info(`DRY RUN: Would transfer ${allSourceTeams.length} teams with ${totalMemberships} memberships`, { prefix: "Transfer" });
148
160
  return;
149
161
  }
150
- const transferTasks = allSourceTeams.map(team => this.limit(async () => {
162
+ const transferTasks = allSourceTeams.map((team) => this.limit(async () => {
151
163
  try {
152
164
  // Check if team exists in target
153
- const existingTeam = allTargetTeams.find(tt => tt.$id === team.$id);
165
+ const existingTeam = allTargetTeams.find((tt) => tt.$id === team.$id);
154
166
  if (!existingTeam) {
155
167
  // Fetch all memberships to extract unique roles before creating team
156
168
  MessageFormatter.info(`Fetching memberships for team ${team.name} to extract roles`, { prefix: "Transfer" });
157
169
  const memberships = await this.fetchAllMemberships(team.$id);
158
170
  // Extract unique roles from all memberships
159
171
  const allRoles = new Set();
160
- memberships.forEach(membership => {
161
- membership.roles.forEach(role => allRoles.add(role));
172
+ memberships.forEach((membership) => {
173
+ membership.roles.forEach((role) => allRoles.add(role));
162
174
  });
163
175
  const uniqueRoles = Array.from(allRoles);
164
- MessageFormatter.info(`Found ${uniqueRoles.length} unique roles for team ${team.name}: ${uniqueRoles.join(', ')}`, { prefix: "Transfer" });
176
+ MessageFormatter.info(`Found ${uniqueRoles.length} unique roles for team ${team.name}: ${uniqueRoles.join(", ")}`, { prefix: "Transfer" });
165
177
  // Create team in target with the collected roles
166
178
  await this.targetTeams.create(team.$id, team.name, uniqueRoles);
167
- MessageFormatter.success(`Created team: ${team.name} with roles: ${uniqueRoles.join(', ')}`, { prefix: "Transfer" });
179
+ MessageFormatter.success(`Created team: ${team.name} with roles: ${uniqueRoles.join(", ")}`, { prefix: "Transfer" });
168
180
  }
169
181
  else {
170
182
  MessageFormatter.info(`Team ${team.name} already exists, updating if needed`, { prefix: "Transfer" });
171
183
  // Update team if needed
172
184
  if (existingTeam.name !== team.name) {
173
185
  await this.targetTeams.updateName(team.$id, team.name);
174
- MessageFormatter.success(`Updated team name: ${team.name}`, { prefix: "Transfer" });
186
+ MessageFormatter.success(`Updated team name: ${team.name}`, {
187
+ prefix: "Transfer",
188
+ });
175
189
  }
176
190
  }
177
191
  // Transfer team memberships
@@ -185,14 +199,18 @@ export class ComprehensiveTransfer {
185
199
  }
186
200
  }));
187
201
  await Promise.all(transferTasks);
188
- MessageFormatter.success("Team transfer phase completed", { prefix: "Transfer" });
202
+ MessageFormatter.success("Team transfer phase completed", {
203
+ prefix: "Transfer",
204
+ });
189
205
  }
190
206
  catch (error) {
191
207
  MessageFormatter.error("Team transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
192
208
  }
193
209
  }
194
210
  async transferAllDatabases() {
195
- MessageFormatter.info("Starting database transfer phase", { prefix: "Transfer" });
211
+ MessageFormatter.info("Starting database transfer phase", {
212
+ prefix: "Transfer",
213
+ });
196
214
  try {
197
215
  const sourceDatabases = await this.sourceDatabases.list();
198
216
  const targetDatabases = await this.targetDatabases.list();
@@ -202,18 +220,22 @@ export class ComprehensiveTransfer {
202
220
  }
203
221
  // Phase 1: Create all databases and collections (structure only)
204
222
  MessageFormatter.info("Phase 1: Creating database structures (databases, collections, attributes, indexes)", { prefix: "Transfer" });
205
- const structureCreationTasks = sourceDatabases.databases.map(db => this.limit(async () => {
223
+ const structureCreationTasks = sourceDatabases.databases.map((db) => this.limit(async () => {
206
224
  try {
207
225
  // Check if database exists in target
208
- const existingDb = targetDatabases.databases.find(tdb => tdb.$id === db.$id);
226
+ const existingDb = targetDatabases.databases.find((tdb) => tdb.$id === db.$id);
209
227
  if (!existingDb) {
210
228
  // Create database in target
211
229
  await this.targetDatabases.create(db.$id, db.name, db.enabled);
212
- MessageFormatter.success(`Created database: ${db.name}`, { prefix: "Transfer" });
230
+ MessageFormatter.success(`Created database: ${db.name}`, {
231
+ prefix: "Transfer",
232
+ });
213
233
  }
214
234
  // Create collections, attributes, and indexes WITHOUT transferring documents
215
235
  await this.createDatabaseStructure(db.$id);
216
- MessageFormatter.success(`Database structure created: ${db.name}`, { prefix: "Transfer" });
236
+ MessageFormatter.success(`Database structure created: ${db.name}`, {
237
+ prefix: "Transfer",
238
+ });
217
239
  }
218
240
  catch (error) {
219
241
  MessageFormatter.error(`Database structure creation failed for ${db.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
@@ -223,7 +245,7 @@ export class ComprehensiveTransfer {
223
245
  await Promise.all(structureCreationTasks);
224
246
  // Phase 2: Transfer all documents after all structures are created
225
247
  MessageFormatter.info("Phase 2: Transferring documents to all collections", { prefix: "Transfer" });
226
- const documentTransferTasks = sourceDatabases.databases.map(db => this.limit(async () => {
248
+ const documentTransferTasks = sourceDatabases.databases.map((db) => this.limit(async () => {
227
249
  try {
228
250
  // Transfer documents for this database
229
251
  await this.transferDatabaseDocuments(db.$id);
@@ -236,7 +258,9 @@ export class ComprehensiveTransfer {
236
258
  }
237
259
  }));
238
260
  await Promise.all(documentTransferTasks);
239
- MessageFormatter.success("Database transfer phase completed", { prefix: "Transfer" });
261
+ MessageFormatter.success("Database transfer phase completed", {
262
+ prefix: "Transfer",
263
+ });
240
264
  }
241
265
  catch (error) {
242
266
  MessageFormatter.error("Database transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
@@ -246,7 +270,9 @@ export class ComprehensiveTransfer {
246
270
  * Phase 1: Create database structure (collections, attributes, indexes) without transferring documents
247
271
  */
248
272
  async createDatabaseStructure(dbId) {
249
- MessageFormatter.info(`Creating database structure for ${dbId}`, { prefix: "Transfer" });
273
+ MessageFormatter.info(`Creating database structure for ${dbId}`, {
274
+ prefix: "Transfer",
275
+ });
250
276
  try {
251
277
  // Get all collections from source database
252
278
  const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
@@ -257,14 +283,18 @@ export class ComprehensiveTransfer {
257
283
  try {
258
284
  // Create or update collection in target
259
285
  let targetCollection;
260
- const existingCollection = await tryAwaitWithRetry(async () => this.targetDatabases.listCollections(dbId, [Query.equal("$id", collection.$id)]));
286
+ const existingCollection = await tryAwaitWithRetry(async () => this.targetDatabases.listCollections(dbId, [
287
+ Query.equal("$id", collection.$id),
288
+ ]));
261
289
  if (existingCollection.collections.length > 0) {
262
290
  targetCollection = existingCollection.collections[0];
263
291
  MessageFormatter.info(`Collection ${collection.name} exists in target database`, { prefix: "Transfer" });
264
292
  // Update collection if needed
265
293
  if (targetCollection.name !== collection.name ||
266
- JSON.stringify(targetCollection.$permissions) !== JSON.stringify(collection.$permissions) ||
267
- targetCollection.documentSecurity !== collection.documentSecurity ||
294
+ JSON.stringify(targetCollection.$permissions) !==
295
+ JSON.stringify(collection.$permissions) ||
296
+ targetCollection.documentSecurity !==
297
+ collection.documentSecurity ||
268
298
  targetCollection.enabled !== collection.enabled) {
269
299
  targetCollection = await tryAwaitWithRetry(async () => this.targetDatabases.updateCollection(dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled));
270
300
  MessageFormatter.success(`Collection ${collection.name} updated`, { prefix: "Transfer" });
@@ -273,11 +303,13 @@ export class ComprehensiveTransfer {
273
303
  else {
274
304
  MessageFormatter.info(`Creating collection ${collection.name} in target database...`, { prefix: "Transfer" });
275
305
  targetCollection = await tryAwaitWithRetry(async () => this.targetDatabases.createCollection(dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled));
276
- MessageFormatter.success(`Collection ${collection.name} created`, { prefix: "Transfer" });
306
+ MessageFormatter.success(`Collection ${collection.name} created`, {
307
+ prefix: "Transfer",
308
+ });
277
309
  }
278
310
  // Handle attributes with enhanced status checking
279
311
  MessageFormatter.info(`Creating attributes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
280
- const attributesToCreate = collection.attributes.map(attr => parseAttribute(attr));
312
+ const attributesToCreate = collection.attributes.map((attr) => parseAttribute(attr));
281
313
  const attributesSuccess = await this.createCollectionAttributesWithStatusCheck(this.targetDatabases, dbId, targetCollection, attributesToCreate);
282
314
  if (!attributesSuccess) {
283
315
  MessageFormatter.error(`Failed to create some attributes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
@@ -314,7 +346,9 @@ export class ComprehensiveTransfer {
314
346
  * Phase 2: Transfer documents to all collections in the database
315
347
  */
316
348
  async transferDatabaseDocuments(dbId) {
317
- MessageFormatter.info(`Transferring documents for database ${dbId}`, { prefix: "Transfer" });
349
+ MessageFormatter.info(`Transferring documents for database ${dbId}`, {
350
+ prefix: "Transfer",
351
+ });
318
352
  try {
319
353
  // Get all collections from source database
320
354
  const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
@@ -338,7 +372,9 @@ export class ComprehensiveTransfer {
338
372
  }
339
373
  }
340
374
  async transferAllBuckets() {
341
- MessageFormatter.info("Starting bucket transfer phase", { prefix: "Transfer" });
375
+ MessageFormatter.info("Starting bucket transfer phase", {
376
+ prefix: "Transfer",
377
+ });
342
378
  try {
343
379
  // Get all buckets from source with pagination
344
380
  const allSourceBuckets = await this.fetchAllBuckets(this.sourceStorage);
@@ -346,20 +382,24 @@ export class ComprehensiveTransfer {
346
382
  if (this.options.dryRun) {
347
383
  let totalFiles = 0;
348
384
  for (const bucket of allSourceBuckets) {
349
- const files = await this.sourceStorage.listFiles(bucket.$id, [Query.limit(1)]);
385
+ const files = await this.sourceStorage.listFiles(bucket.$id, [
386
+ Query.limit(1),
387
+ ]);
350
388
  totalFiles += files.total;
351
389
  }
352
390
  MessageFormatter.info(`DRY RUN: Would transfer ${allSourceBuckets.length} buckets with ${totalFiles} files`, { prefix: "Transfer" });
353
391
  return;
354
392
  }
355
- const transferTasks = allSourceBuckets.map(bucket => this.limit(async () => {
393
+ const transferTasks = allSourceBuckets.map((bucket) => this.limit(async () => {
356
394
  try {
357
395
  // Check if bucket exists in target
358
- const existingBucket = allTargetBuckets.find(tb => tb.$id === bucket.$id);
396
+ const existingBucket = allTargetBuckets.find((tb) => tb.$id === bucket.$id);
359
397
  if (!existingBucket) {
360
398
  // Create bucket with fallback strategy for maximumFileSize
361
399
  await this.createBucketWithFallback(bucket);
362
- MessageFormatter.success(`Created bucket: ${bucket.name}`, { prefix: "Transfer" });
400
+ MessageFormatter.success(`Created bucket: ${bucket.name}`, {
401
+ prefix: "Transfer",
402
+ });
363
403
  }
364
404
  else {
365
405
  // Compare bucket permissions and update if needed
@@ -375,7 +415,9 @@ export class ComprehensiveTransfer {
375
415
  MessageFormatter.success(`Updated bucket ${bucket.name} to match source`, { prefix: "Transfer" });
376
416
  }
377
417
  catch (updateError) {
378
- MessageFormatter.error(`Failed to update bucket ${bucket.name}`, updateError instanceof Error ? updateError : new Error(String(updateError)), { prefix: "Transfer" });
418
+ MessageFormatter.error(`Failed to update bucket ${bucket.name}`, updateError instanceof Error
419
+ ? updateError
420
+ : new Error(String(updateError)), { prefix: "Transfer" });
379
421
  }
380
422
  }
381
423
  else {
@@ -393,7 +435,9 @@ export class ComprehensiveTransfer {
393
435
  }
394
436
  }));
395
437
  await Promise.all(transferTasks);
396
- MessageFormatter.success("Bucket transfer phase completed", { prefix: "Transfer" });
438
+ MessageFormatter.success("Bucket transfer phase completed", {
439
+ prefix: "Transfer",
440
+ });
397
441
  }
398
442
  catch (error) {
399
443
  MessageFormatter.error("Bucket transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
@@ -434,7 +478,8 @@ export class ComprehensiveTransfer {
434
478
  catch (error) {
435
479
  const err = error instanceof Error ? error : new Error(String(error));
436
480
  // Check if the error is related to maximumFileSize validation
437
- if (err.message.includes('maximumFileSize') || err.message.includes('valid range')) {
481
+ if (err.message.includes("maximumFileSize") ||
482
+ err.message.includes("valid range")) {
438
483
  MessageFormatter.warning(`Bucket ${bucket.name}: Failed with maximumFileSize ${sizeToTry}, falling back to smaller sizes...`, { prefix: "Transfer" });
439
484
  // Continue to fallback logic below
440
485
  }
@@ -450,11 +495,11 @@ export class ComprehensiveTransfer {
450
495
  2_000_000_000, // 2GB
451
496
  1_000_000_000, // 1GB
452
497
  500_000_000, // 500MB
453
- 100_000_000 // 100MB
498
+ 100_000_000, // 100MB
454
499
  ];
455
500
  // Remove sizes that are larger than or equal to the already-tried size
456
501
  const validSizes = fallbackSizes
457
- .filter(size => size < sizeToTry)
502
+ .filter((size) => size < sizeToTry)
458
503
  .sort((a, b) => b - a); // Sort descending
459
504
  let lastError = null;
460
505
  for (const fileSize of validSizes) {
@@ -474,7 +519,8 @@ export class ComprehensiveTransfer {
474
519
  catch (error) {
475
520
  lastError = error instanceof Error ? error : new Error(String(error));
476
521
  // Check if the error is related to maximumFileSize validation
477
- if (lastError.message.includes('maximumFileSize') || lastError.message.includes('valid range')) {
522
+ if (lastError.message.includes("maximumFileSize") ||
523
+ lastError.message.includes("valid range")) {
478
524
  MessageFormatter.warning(`Bucket ${bucket.name}: Failed with maximumFileSize ${fileSize}, trying smaller size...`, { prefix: "Transfer" });
479
525
  continue; // Try next smaller size
480
526
  }
@@ -486,7 +532,7 @@ export class ComprehensiveTransfer {
486
532
  }
487
533
  // If we get here, all fallback sizes failed
488
534
  MessageFormatter.error(`Bucket ${bucket.name}: All fallback file sizes failed. Last error: ${lastError?.message}`, lastError || undefined, { prefix: "Transfer" });
489
- throw lastError || new Error('All fallback file sizes failed');
535
+ throw lastError || new Error("All fallback file sizes failed");
490
536
  }
491
537
  async transferBucketFiles(sourceBucketId, targetBucketId) {
492
538
  let lastFileId;
@@ -500,7 +546,7 @@ export class ComprehensiveTransfer {
500
546
  if (files.files.length === 0)
501
547
  break;
502
548
  // Process files with rate limiting
503
- const fileTasks = files.files.map(file => this.fileLimit(async () => {
549
+ const fileTasks = files.files.map((file) => this.fileLimit(async () => {
504
550
  try {
505
551
  // Check if file already exists and compare permissions
506
552
  let existingFile = null;
@@ -517,7 +563,9 @@ export class ComprehensiveTransfer {
517
563
  MessageFormatter.success(`Updated file ${file.name} permissions to match source`, { prefix: "Transfer" });
518
564
  }
519
565
  catch (updateError) {
520
- MessageFormatter.error(`Failed to update permissions for file ${file.name}`, updateError instanceof Error ? updateError : new Error(String(updateError)), { prefix: "Transfer" });
566
+ MessageFormatter.error(`Failed to update permissions for file ${file.name}`, updateError instanceof Error
567
+ ? updateError
568
+ : new Error(String(updateError)), { prefix: "Transfer" });
521
569
  }
522
570
  }
523
571
  else {
@@ -538,7 +586,9 @@ export class ComprehensiveTransfer {
538
586
  const fileToCreate = InputFile.fromBuffer(new Uint8Array(fileData), file.name);
539
587
  await this.targetStorage.createFile(targetBucketId, file.$id, fileToCreate, file.$permissions);
540
588
  transferredFiles++;
541
- MessageFormatter.success(`Transferred file: ${file.name}`, { prefix: "Transfer" });
589
+ MessageFormatter.success(`Transferred file: ${file.name}`, {
590
+ prefix: "Transfer",
591
+ });
542
592
  }
543
593
  catch (error) {
544
594
  MessageFormatter.error(`Failed to transfer file ${file.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
@@ -558,10 +608,13 @@ export class ComprehensiveTransfer {
558
608
  const fileData = await this.sourceStorage.getFileDownload(bucketId, fileId);
559
609
  // Basic validation - ensure file is not empty and not too large
560
610
  if (fileData.byteLength === 0) {
561
- MessageFormatter.warning(`File ${fileId} is empty`, { prefix: "Transfer" });
611
+ MessageFormatter.warning(`File ${fileId} is empty`, {
612
+ prefix: "Transfer",
613
+ });
562
614
  return null;
563
615
  }
564
- if (fileData.byteLength > 50 * 1024 * 1024) { // 50MB limit
616
+ if (fileData.byteLength > 50 * 1024 * 1024) {
617
+ // 50MB limit
565
618
  MessageFormatter.warning(`File ${fileId} is too large (${fileData.byteLength} bytes)`, { prefix: "Transfer" });
566
619
  return null;
567
620
  }
@@ -575,24 +628,30 @@ export class ComprehensiveTransfer {
575
628
  return null;
576
629
  }
577
630
  // Wait before retry
578
- await new Promise(resolve => setTimeout(resolve, 1000 * (4 - attempts)));
631
+ await new Promise((resolve) => setTimeout(resolve, 1000 * (4 - attempts)));
579
632
  }
580
633
  }
581
634
  return null;
582
635
  }
583
636
  async transferAllFunctions() {
584
- MessageFormatter.info("Starting function transfer phase", { prefix: "Transfer" });
637
+ MessageFormatter.info("Starting function transfer phase", {
638
+ prefix: "Transfer",
639
+ });
585
640
  try {
586
- const sourceFunctions = await listFunctions(this.sourceClient, [Query.limit(1000)]);
587
- const targetFunctions = await listFunctions(this.targetClient, [Query.limit(1000)]);
641
+ const sourceFunctions = await listFunctions(this.sourceClient, [
642
+ Query.limit(1000),
643
+ ]);
644
+ const targetFunctions = await listFunctions(this.targetClient, [
645
+ Query.limit(1000),
646
+ ]);
588
647
  if (this.options.dryRun) {
589
648
  MessageFormatter.info(`DRY RUN: Would transfer ${sourceFunctions.functions.length} functions`, { prefix: "Transfer" });
590
649
  return;
591
650
  }
592
- const transferTasks = sourceFunctions.functions.map(func => this.limit(async () => {
651
+ const transferTasks = sourceFunctions.functions.map((func) => this.limit(async () => {
593
652
  try {
594
653
  // Check if function exists in target
595
- const existingFunc = targetFunctions.functions.find(tf => tf.$id === func.$id);
654
+ const existingFunc = targetFunctions.functions.find((tf) => tf.$id === func.$id);
596
655
  if (existingFunc) {
597
656
  MessageFormatter.info(`Function ${func.name} already exists, skipping creation`, { prefix: "Transfer" });
598
657
  this.results.functions.skipped++;
@@ -637,7 +696,9 @@ export class ComprehensiveTransfer {
637
696
  }
638
697
  }));
639
698
  await Promise.all(transferTasks);
640
- MessageFormatter.success("Function transfer phase completed", { prefix: "Transfer" });
699
+ MessageFormatter.success("Function transfer phase completed", {
700
+ prefix: "Transfer",
701
+ });
641
702
  }
642
703
  catch (error) {
643
704
  MessageFormatter.error("Function transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
@@ -720,7 +781,7 @@ export class ComprehensiveTransfer {
720
781
  twoWay: attr.twoWay,
721
782
  twoWayKey: attr.twoWayKey,
722
783
  onDelete: attr.onDelete,
723
- side: attr.side
784
+ side: attr.side,
724
785
  };
725
786
  }
726
787
  /**
@@ -749,10 +810,12 @@ export class ComprehensiveTransfer {
749
810
  let totalSkipped = 0;
750
811
  let totalUpdated = 0;
751
812
  // Check if bulk operations are supported
752
- const supportsBulk = this.options.sourceEndpoint.includes('cloud.appwrite.io') ||
753
- this.options.targetEndpoint.includes('cloud.appwrite.io');
813
+ const supportsBulk = this.options.sourceEndpoint.includes("cloud.appwrite.io") ||
814
+ this.options.targetEndpoint.includes("cloud.appwrite.io");
754
815
  if (supportsBulk) {
755
- MessageFormatter.info(`Using bulk operations for enhanced performance`, { prefix: "Transfer" });
816
+ MessageFormatter.info(`Using bulk operations for enhanced performance`, {
817
+ prefix: "Transfer",
818
+ });
756
819
  }
757
820
  while (true) {
758
821
  // Fetch source documents in larger batches (1000 instead of 50)
@@ -766,12 +829,12 @@ export class ComprehensiveTransfer {
766
829
  }
767
830
  MessageFormatter.info(`Processing batch of ${sourceDocuments.documents.length} source documents`, { prefix: "Transfer" });
768
831
  // Extract document IDs from the current batch
769
- const sourceDocIds = sourceDocuments.documents.map(doc => doc.$id);
832
+ const sourceDocIds = sourceDocuments.documents.map((doc) => doc.$id);
770
833
  // Fetch existing documents from target in a single query
771
834
  const existingTargetDocs = await this.fetchTargetDocumentsBatch(targetDb, targetDbId, targetCollectionId, sourceDocIds);
772
835
  // Create a map for quick lookup of existing documents
773
836
  const existingDocsMap = new Map();
774
- existingTargetDocs.forEach(doc => {
837
+ existingTargetDocs.forEach((doc) => {
775
838
  existingDocsMap.set(doc.$id, doc);
776
839
  });
777
840
  // Filter documents based on existence, content comparison, and permission comparison
@@ -785,9 +848,10 @@ export class ComprehensiveTransfer {
785
848
  }
786
849
  else {
787
850
  // Document exists, compare both content and permissions
788
- const sourcePermissions = JSON.stringify((sourceDoc.$permissions || []).sort());
789
- const targetPermissions = JSON.stringify((existingTargetDoc.$permissions || []).sort());
790
- const permissionsDiffer = sourcePermissions !== targetPermissions;
851
+ const sourcePermissions = Array.from(new Set(sourceDoc.$permissions || [])).sort();
852
+ const targetPermissions = Array.from(new Set(existingTargetDoc.$permissions || [])).sort();
853
+ const permissionsDiffer = sourcePermissions.join(",") !== targetPermissions.join(",") ||
854
+ sourcePermissions.length !== targetPermissions.length;
791
855
  // Use objectNeedsUpdate to compare document content (excluding system fields)
792
856
  const contentDiffers = objectNeedsUpdate(existingTargetDoc, sourceDoc);
793
857
  if (contentDiffers && permissionsDiffer) {
@@ -795,32 +859,28 @@ export class ComprehensiveTransfer {
795
859
  documentsToUpdate.push({
796
860
  doc: sourceDoc,
797
861
  targetDoc: existingTargetDoc,
798
- reason: "content and permissions differ"
862
+ reason: "content and permissions differ",
799
863
  });
800
- MessageFormatter.info(`Document ${sourceDoc.$id} exists but content and permissions differ - will update`, { prefix: "Transfer" });
801
864
  }
802
865
  else if (contentDiffers) {
803
866
  // Only content differs
804
867
  documentsToUpdate.push({
805
868
  doc: sourceDoc,
806
869
  targetDoc: existingTargetDoc,
807
- reason: "content differs"
870
+ reason: "content differs",
808
871
  });
809
- MessageFormatter.info(`Document ${sourceDoc.$id} exists but content differs - will update`, { prefix: "Transfer" });
810
872
  }
811
873
  else if (permissionsDiffer) {
812
874
  // Only permissions differ
813
875
  documentsToUpdate.push({
814
876
  doc: sourceDoc,
815
877
  targetDoc: existingTargetDoc,
816
- reason: "permissions differ"
878
+ reason: "permissions differ",
817
879
  });
818
- MessageFormatter.info(`Document ${sourceDoc.$id} exists but permissions differ - will update`, { prefix: "Transfer" });
819
880
  }
820
881
  else {
821
882
  // Document exists with identical content AND permissions, skip
822
883
  totalSkipped++;
823
- MessageFormatter.info(`Document ${sourceDoc.$id} exists with matching content and permissions - skipping`, { prefix: "Transfer" });
824
884
  }
825
885
  }
826
886
  }
@@ -831,7 +891,6 @@ export class ComprehensiveTransfer {
831
891
  // Use bulk operations for large batches
832
892
  await this.transferDocumentsBulk(targetDb, targetDbId, targetCollectionId, documentsToTransfer);
833
893
  totalTransferred += documentsToTransfer.length;
834
- MessageFormatter.success(`Bulk transferred ${documentsToTransfer.length} new documents`, { prefix: "Transfer" });
835
894
  }
836
895
  else {
837
896
  // Use individual transfers for smaller batches or non-bulk endpoints
@@ -847,7 +906,8 @@ export class ComprehensiveTransfer {
847
906
  if (sourceDocuments.documents.length < 1000) {
848
907
  break;
849
908
  }
850
- lastId = sourceDocuments.documents[sourceDocuments.documents.length - 1].$id;
909
+ lastId =
910
+ sourceDocuments.documents[sourceDocuments.documents.length - 1].$id;
851
911
  }
852
912
  MessageFormatter.info(`Transfer complete: ${totalTransferred} new, ${totalUpdated} updated, ${totalSkipped} skipped from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
853
913
  }
@@ -861,8 +921,8 @@ export class ComprehensiveTransfer {
861
921
  for (const chunk of idChunks) {
862
922
  try {
863
923
  const result = await tryAwaitWithRetry(async () => targetDb.listDocuments(targetDbId, targetCollectionId, [
864
- Query.equal('$id', chunk),
865
- Query.limit(100)
924
+ Query.equal("$id", chunk),
925
+ Query.limit(100),
866
926
  ]));
867
927
  documents.push(...result.documents);
868
928
  }
@@ -887,12 +947,12 @@ export class ComprehensiveTransfer {
887
947
  */
888
948
  async transferDocumentsBulk(targetDb, targetDbId, targetCollectionId, documents) {
889
949
  // Prepare documents for bulk upsert
890
- const preparedDocs = documents.map(doc => {
950
+ const preparedDocs = documents.map((doc) => {
891
951
  const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
892
952
  return {
893
953
  $id,
894
954
  $permissions,
895
- ...docData
955
+ ...docData,
896
956
  };
897
957
  });
898
958
  // Process in smaller chunks for bulk operations (1000 for Pro, 100 for Free tier)
@@ -902,13 +962,8 @@ export class ComprehensiveTransfer {
902
962
  const documentBatches = this.chunkArray(preparedDocs, maxBatchSize);
903
963
  try {
904
964
  for (const batch of documentBatches) {
905
- MessageFormatter.info(`Bulk upserting ${batch.length} documents...`, { prefix: "Transfer" });
906
965
  await this.bulkUpsertDocuments(this.targetClient, targetDbId, targetCollectionId, batch);
907
966
  MessageFormatter.success(`✅ Bulk upserted ${batch.length} documents`, { prefix: "Transfer" });
908
- // Add delay between batches to respect rate limits
909
- if (documentBatches.indexOf(batch) < documentBatches.length - 1) {
910
- await new Promise(resolve => setTimeout(resolve, 200));
911
- }
912
967
  }
913
968
  processed = true;
914
969
  break; // Success, exit batch size loop
@@ -931,18 +986,20 @@ export class ComprehensiveTransfer {
931
986
  const apiPath = `/databases/${dbId}/collections/${collectionId}/documents`;
932
987
  const url = new URL(client.config.endpoint + apiPath);
933
988
  const headers = {
934
- 'Content-Type': 'application/json',
935
- 'X-Appwrite-Project': client.config.project,
936
- 'X-Appwrite-Key': client.config.key
989
+ "Content-Type": "application/json",
990
+ "X-Appwrite-Project": client.config.project,
991
+ "X-Appwrite-Key": client.config.key,
937
992
  };
938
993
  const response = await fetch(url.toString(), {
939
- method: 'PUT',
994
+ method: "PUT",
940
995
  headers,
941
- body: JSON.stringify({ documents })
996
+ body: JSON.stringify({ documents }),
942
997
  });
943
998
  if (!response.ok) {
944
- const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
945
- throw new Error(`Bulk upsert failed: ${response.status} - ${errorData.message || 'Unknown error'}`);
999
+ const errorData = await response
1000
+ .json()
1001
+ .catch(() => ({ message: "Unknown error" }));
1002
+ throw new Error(`Bulk upsert failed: ${response.status} - ${errorData.message || "Unknown error"}`);
946
1003
  }
947
1004
  return await response.json();
948
1005
  }
@@ -951,14 +1008,28 @@ export class ComprehensiveTransfer {
951
1008
  */
952
1009
  async transferDocumentsIndividual(targetDb, targetDbId, targetCollectionId, documents) {
953
1010
  let successCount = 0;
954
- const transferTasks = documents.map(doc => this.limit(async () => {
1011
+ const transferTasks = documents.map((doc) => this.limit(async () => {
955
1012
  try {
956
1013
  const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
957
1014
  await tryAwaitWithRetry(async () => targetDb.createDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
958
1015
  successCount++;
959
- MessageFormatter.success(`Transferred document ${doc.$id}`, { prefix: "Transfer" });
960
1016
  }
961
1017
  catch (error) {
1018
+ if (error instanceof AppwriteException &&
1019
+ error.message.includes("already exists")) {
1020
+ try {
1021
+ // Update it! It's here because it needs an update or a create
1022
+ const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
1023
+ await tryAwaitWithRetry(async () => targetDb.updateDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
1024
+ successCount++;
1025
+ }
1026
+ catch (updateError) {
1027
+ // just send the error to the formatter
1028
+ MessageFormatter.error(`Failed to transfer document ${doc.$id}`, updateError instanceof Error
1029
+ ? updateError
1030
+ : new Error(String(updateError)), { prefix: "Transfer" });
1031
+ }
1032
+ }
962
1033
  MessageFormatter.error(`Failed to transfer document ${doc.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
963
1034
  }
964
1035
  }));
@@ -975,7 +1046,6 @@ export class ComprehensiveTransfer {
975
1046
  const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
976
1047
  await tryAwaitWithRetry(async () => targetDb.updateDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
977
1048
  successCount++;
978
- MessageFormatter.success(`Updated document ${doc.$id} (${reason}) - permissions: [${targetDoc.$permissions?.join(', ')}] → [${doc.$permissions?.join(', ')}]`, { prefix: "Transfer" });
979
1049
  }
980
1050
  catch (error) {
981
1051
  MessageFormatter.error(`Failed to update document ${doc.$id} (${reason})`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
@@ -1044,18 +1114,23 @@ export class ComprehensiveTransfer {
1044
1114
  * Helper method to transfer team memberships
1045
1115
  */
1046
1116
  async transferTeamMemberships(teamId) {
1047
- MessageFormatter.info(`Transferring memberships for team ${teamId}`, { prefix: "Transfer" });
1117
+ MessageFormatter.info(`Transferring memberships for team ${teamId}`, {
1118
+ prefix: "Transfer",
1119
+ });
1048
1120
  try {
1049
1121
  // Fetch all memberships for this team
1050
1122
  const memberships = await this.fetchAllMemberships(teamId);
1051
1123
  if (memberships.length === 0) {
1052
- MessageFormatter.info(`No memberships found for team ${teamId}`, { prefix: "Transfer" });
1124
+ MessageFormatter.info(`No memberships found for team ${teamId}`, {
1125
+ prefix: "Transfer",
1126
+ });
1053
1127
  return;
1054
1128
  }
1055
1129
  MessageFormatter.info(`Found ${memberships.length} memberships for team ${teamId}`, { prefix: "Transfer" });
1056
1130
  let totalTransferred = 0;
1057
1131
  // Transfer memberships with rate limiting
1058
- const transferTasks = memberships.map(membership => this.userLimit(async () => {
1132
+ const transferTasks = memberships.map((membership) => this.userLimit(async () => {
1133
+ // Use userLimit for team operations (more sensitive)
1059
1134
  try {
1060
1135
  // Check if membership already exists and compare roles
1061
1136
  let existingMembership = null;
@@ -1072,7 +1147,9 @@ export class ComprehensiveTransfer {
1072
1147
  MessageFormatter.success(`Updated membership ${membership.$id} roles to match source`, { prefix: "Transfer" });
1073
1148
  }
1074
1149
  catch (updateError) {
1075
- MessageFormatter.error(`Failed to update roles for membership ${membership.$id}`, updateError instanceof Error ? updateError : new Error(String(updateError)), { prefix: "Transfer" });
1150
+ MessageFormatter.error(`Failed to update roles for membership ${membership.$id}`, updateError instanceof Error
1151
+ ? updateError
1152
+ : new Error(String(updateError)), { prefix: "Transfer" });
1076
1153
  }
1077
1154
  }
1078
1155
  else {
@@ -1114,15 +1191,25 @@ export class ComprehensiveTransfer {
1114
1191
  }
1115
1192
  printSummary() {
1116
1193
  const duration = Math.round((Date.now() - this.startTime) / 1000);
1117
- MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", { prefix: "Transfer" });
1194
+ MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", {
1195
+ prefix: "Transfer",
1196
+ });
1118
1197
  MessageFormatter.info(`Total Time: ${duration}s`, { prefix: "Transfer" });
1119
1198
  MessageFormatter.info(`Users: ${this.results.users.transferred} transferred, ${this.results.users.skipped} skipped, ${this.results.users.failed} failed`, { prefix: "Transfer" });
1120
1199
  MessageFormatter.info(`Teams: ${this.results.teams.transferred} transferred, ${this.results.teams.skipped} skipped, ${this.results.teams.failed} failed`, { prefix: "Transfer" });
1121
1200
  MessageFormatter.info(`Databases: ${this.results.databases.transferred} transferred, ${this.results.databases.skipped} skipped, ${this.results.databases.failed} failed`, { prefix: "Transfer" });
1122
1201
  MessageFormatter.info(`Buckets: ${this.results.buckets.transferred} transferred, ${this.results.buckets.skipped} skipped, ${this.results.buckets.failed} failed`, { prefix: "Transfer" });
1123
1202
  MessageFormatter.info(`Functions: ${this.results.functions.transferred} transferred, ${this.results.functions.skipped} skipped, ${this.results.functions.failed} failed`, { prefix: "Transfer" });
1124
- const totalTransferred = this.results.users.transferred + this.results.teams.transferred + this.results.databases.transferred + this.results.buckets.transferred + this.results.functions.transferred;
1125
- const totalFailed = this.results.users.failed + this.results.teams.failed + this.results.databases.failed + this.results.buckets.failed + this.results.functions.failed;
1203
+ const totalTransferred = this.results.users.transferred +
1204
+ this.results.teams.transferred +
1205
+ this.results.databases.transferred +
1206
+ this.results.buckets.transferred +
1207
+ this.results.functions.transferred;
1208
+ const totalFailed = this.results.users.failed +
1209
+ this.results.teams.failed +
1210
+ this.results.databases.failed +
1211
+ this.results.buckets.failed +
1212
+ this.results.functions.failed;
1126
1213
  if (totalFailed === 0) {
1127
1214
  MessageFormatter.success(`All ${totalTransferred} items transferred successfully!`, { prefix: "Transfer" });
1128
1215
  }