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,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" });
@@ -290,7 +322,14 @@ export class ComprehensiveTransfer {
290
322
  }
291
323
  // Handle indexes with enhanced status checking
292
324
  MessageFormatter.info(`Creating indexes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
293
- const indexesSuccess = await this.createCollectionIndexesWithStatusCheck(dbId, this.targetDatabases, targetCollection.$id, targetCollection, collection.indexes);
325
+ let indexesSuccess = true;
326
+ // Check if indexes need to be created ahead of time
327
+ if (collection.indexes.some((index) => !targetCollection.indexes.some((ti) => ti.key === index.key ||
328
+ ti.attributes.sort().join(",") ===
329
+ index.attributes.sort().join(","))) ||
330
+ collection.indexes.length !== targetCollection.indexes.length) {
331
+ indexesSuccess = await this.createCollectionIndexesWithStatusCheck(dbId, this.targetDatabases, targetCollection.$id, targetCollection, collection.indexes);
332
+ }
294
333
  if (!indexesSuccess) {
295
334
  MessageFormatter.error(`Failed to create some indexes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
296
335
  MessageFormatter.warning(`Proceeding with document transfer despite index failures for collection ${collection.name}`, { prefix: "Transfer" });
@@ -314,7 +353,9 @@ export class ComprehensiveTransfer {
314
353
  * Phase 2: Transfer documents to all collections in the database
315
354
  */
316
355
  async transferDatabaseDocuments(dbId) {
317
- MessageFormatter.info(`Transferring documents for database ${dbId}`, { prefix: "Transfer" });
356
+ MessageFormatter.info(`Transferring documents for database ${dbId}`, {
357
+ prefix: "Transfer",
358
+ });
318
359
  try {
319
360
  // Get all collections from source database
320
361
  const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
@@ -338,7 +379,9 @@ export class ComprehensiveTransfer {
338
379
  }
339
380
  }
340
381
  async transferAllBuckets() {
341
- MessageFormatter.info("Starting bucket transfer phase", { prefix: "Transfer" });
382
+ MessageFormatter.info("Starting bucket transfer phase", {
383
+ prefix: "Transfer",
384
+ });
342
385
  try {
343
386
  // Get all buckets from source with pagination
344
387
  const allSourceBuckets = await this.fetchAllBuckets(this.sourceStorage);
@@ -346,20 +389,24 @@ export class ComprehensiveTransfer {
346
389
  if (this.options.dryRun) {
347
390
  let totalFiles = 0;
348
391
  for (const bucket of allSourceBuckets) {
349
- const files = await this.sourceStorage.listFiles(bucket.$id, [Query.limit(1)]);
392
+ const files = await this.sourceStorage.listFiles(bucket.$id, [
393
+ Query.limit(1),
394
+ ]);
350
395
  totalFiles += files.total;
351
396
  }
352
397
  MessageFormatter.info(`DRY RUN: Would transfer ${allSourceBuckets.length} buckets with ${totalFiles} files`, { prefix: "Transfer" });
353
398
  return;
354
399
  }
355
- const transferTasks = allSourceBuckets.map(bucket => this.limit(async () => {
400
+ const transferTasks = allSourceBuckets.map((bucket) => this.limit(async () => {
356
401
  try {
357
402
  // Check if bucket exists in target
358
- const existingBucket = allTargetBuckets.find(tb => tb.$id === bucket.$id);
403
+ const existingBucket = allTargetBuckets.find((tb) => tb.$id === bucket.$id);
359
404
  if (!existingBucket) {
360
405
  // Create bucket with fallback strategy for maximumFileSize
361
406
  await this.createBucketWithFallback(bucket);
362
- MessageFormatter.success(`Created bucket: ${bucket.name}`, { prefix: "Transfer" });
407
+ MessageFormatter.success(`Created bucket: ${bucket.name}`, {
408
+ prefix: "Transfer",
409
+ });
363
410
  }
364
411
  else {
365
412
  // Compare bucket permissions and update if needed
@@ -375,7 +422,9 @@ export class ComprehensiveTransfer {
375
422
  MessageFormatter.success(`Updated bucket ${bucket.name} to match source`, { prefix: "Transfer" });
376
423
  }
377
424
  catch (updateError) {
378
- MessageFormatter.error(`Failed to update bucket ${bucket.name}`, updateError instanceof Error ? updateError : new Error(String(updateError)), { prefix: "Transfer" });
425
+ MessageFormatter.error(`Failed to update bucket ${bucket.name}`, updateError instanceof Error
426
+ ? updateError
427
+ : new Error(String(updateError)), { prefix: "Transfer" });
379
428
  }
380
429
  }
381
430
  else {
@@ -393,7 +442,9 @@ export class ComprehensiveTransfer {
393
442
  }
394
443
  }));
395
444
  await Promise.all(transferTasks);
396
- MessageFormatter.success("Bucket transfer phase completed", { prefix: "Transfer" });
445
+ MessageFormatter.success("Bucket transfer phase completed", {
446
+ prefix: "Transfer",
447
+ });
397
448
  }
398
449
  catch (error) {
399
450
  MessageFormatter.error("Bucket transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
@@ -434,7 +485,8 @@ export class ComprehensiveTransfer {
434
485
  catch (error) {
435
486
  const err = error instanceof Error ? error : new Error(String(error));
436
487
  // Check if the error is related to maximumFileSize validation
437
- if (err.message.includes('maximumFileSize') || err.message.includes('valid range')) {
488
+ if (err.message.includes("maximumFileSize") ||
489
+ err.message.includes("valid range")) {
438
490
  MessageFormatter.warning(`Bucket ${bucket.name}: Failed with maximumFileSize ${sizeToTry}, falling back to smaller sizes...`, { prefix: "Transfer" });
439
491
  // Continue to fallback logic below
440
492
  }
@@ -450,11 +502,11 @@ export class ComprehensiveTransfer {
450
502
  2_000_000_000, // 2GB
451
503
  1_000_000_000, // 1GB
452
504
  500_000_000, // 500MB
453
- 100_000_000 // 100MB
505
+ 100_000_000, // 100MB
454
506
  ];
455
507
  // Remove sizes that are larger than or equal to the already-tried size
456
508
  const validSizes = fallbackSizes
457
- .filter(size => size < sizeToTry)
509
+ .filter((size) => size < sizeToTry)
458
510
  .sort((a, b) => b - a); // Sort descending
459
511
  let lastError = null;
460
512
  for (const fileSize of validSizes) {
@@ -474,7 +526,8 @@ export class ComprehensiveTransfer {
474
526
  catch (error) {
475
527
  lastError = error instanceof Error ? error : new Error(String(error));
476
528
  // Check if the error is related to maximumFileSize validation
477
- if (lastError.message.includes('maximumFileSize') || lastError.message.includes('valid range')) {
529
+ if (lastError.message.includes("maximumFileSize") ||
530
+ lastError.message.includes("valid range")) {
478
531
  MessageFormatter.warning(`Bucket ${bucket.name}: Failed with maximumFileSize ${fileSize}, trying smaller size...`, { prefix: "Transfer" });
479
532
  continue; // Try next smaller size
480
533
  }
@@ -486,7 +539,7 @@ export class ComprehensiveTransfer {
486
539
  }
487
540
  // If we get here, all fallback sizes failed
488
541
  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');
542
+ throw lastError || new Error("All fallback file sizes failed");
490
543
  }
491
544
  async transferBucketFiles(sourceBucketId, targetBucketId) {
492
545
  let lastFileId;
@@ -500,7 +553,7 @@ export class ComprehensiveTransfer {
500
553
  if (files.files.length === 0)
501
554
  break;
502
555
  // Process files with rate limiting
503
- const fileTasks = files.files.map(file => this.fileLimit(async () => {
556
+ const fileTasks = files.files.map((file) => this.fileLimit(async () => {
504
557
  try {
505
558
  // Check if file already exists and compare permissions
506
559
  let existingFile = null;
@@ -517,7 +570,9 @@ export class ComprehensiveTransfer {
517
570
  MessageFormatter.success(`Updated file ${file.name} permissions to match source`, { prefix: "Transfer" });
518
571
  }
519
572
  catch (updateError) {
520
- MessageFormatter.error(`Failed to update permissions for file ${file.name}`, updateError instanceof Error ? updateError : new Error(String(updateError)), { prefix: "Transfer" });
573
+ MessageFormatter.error(`Failed to update permissions for file ${file.name}`, updateError instanceof Error
574
+ ? updateError
575
+ : new Error(String(updateError)), { prefix: "Transfer" });
521
576
  }
522
577
  }
523
578
  else {
@@ -538,7 +593,9 @@ export class ComprehensiveTransfer {
538
593
  const fileToCreate = InputFile.fromBuffer(new Uint8Array(fileData), file.name);
539
594
  await this.targetStorage.createFile(targetBucketId, file.$id, fileToCreate, file.$permissions);
540
595
  transferredFiles++;
541
- MessageFormatter.success(`Transferred file: ${file.name}`, { prefix: "Transfer" });
596
+ MessageFormatter.success(`Transferred file: ${file.name}`, {
597
+ prefix: "Transfer",
598
+ });
542
599
  }
543
600
  catch (error) {
544
601
  MessageFormatter.error(`Failed to transfer file ${file.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
@@ -558,10 +615,13 @@ export class ComprehensiveTransfer {
558
615
  const fileData = await this.sourceStorage.getFileDownload(bucketId, fileId);
559
616
  // Basic validation - ensure file is not empty and not too large
560
617
  if (fileData.byteLength === 0) {
561
- MessageFormatter.warning(`File ${fileId} is empty`, { prefix: "Transfer" });
618
+ MessageFormatter.warning(`File ${fileId} is empty`, {
619
+ prefix: "Transfer",
620
+ });
562
621
  return null;
563
622
  }
564
- if (fileData.byteLength > 50 * 1024 * 1024) { // 50MB limit
623
+ if (fileData.byteLength > 50 * 1024 * 1024) {
624
+ // 50MB limit
565
625
  MessageFormatter.warning(`File ${fileId} is too large (${fileData.byteLength} bytes)`, { prefix: "Transfer" });
566
626
  return null;
567
627
  }
@@ -575,24 +635,30 @@ export class ComprehensiveTransfer {
575
635
  return null;
576
636
  }
577
637
  // Wait before retry
578
- await new Promise(resolve => setTimeout(resolve, 1000 * (4 - attempts)));
638
+ await new Promise((resolve) => setTimeout(resolve, 1000 * (4 - attempts)));
579
639
  }
580
640
  }
581
641
  return null;
582
642
  }
583
643
  async transferAllFunctions() {
584
- MessageFormatter.info("Starting function transfer phase", { prefix: "Transfer" });
644
+ MessageFormatter.info("Starting function transfer phase", {
645
+ prefix: "Transfer",
646
+ });
585
647
  try {
586
- const sourceFunctions = await listFunctions(this.sourceClient, [Query.limit(1000)]);
587
- const targetFunctions = await listFunctions(this.targetClient, [Query.limit(1000)]);
648
+ const sourceFunctions = await listFunctions(this.sourceClient, [
649
+ Query.limit(1000),
650
+ ]);
651
+ const targetFunctions = await listFunctions(this.targetClient, [
652
+ Query.limit(1000),
653
+ ]);
588
654
  if (this.options.dryRun) {
589
655
  MessageFormatter.info(`DRY RUN: Would transfer ${sourceFunctions.functions.length} functions`, { prefix: "Transfer" });
590
656
  return;
591
657
  }
592
- const transferTasks = sourceFunctions.functions.map(func => this.limit(async () => {
658
+ const transferTasks = sourceFunctions.functions.map((func) => this.limit(async () => {
593
659
  try {
594
660
  // Check if function exists in target
595
- const existingFunc = targetFunctions.functions.find(tf => tf.$id === func.$id);
661
+ const existingFunc = targetFunctions.functions.find((tf) => tf.$id === func.$id);
596
662
  if (existingFunc) {
597
663
  MessageFormatter.info(`Function ${func.name} already exists, skipping creation`, { prefix: "Transfer" });
598
664
  this.results.functions.skipped++;
@@ -637,7 +703,9 @@ export class ComprehensiveTransfer {
637
703
  }
638
704
  }));
639
705
  await Promise.all(transferTasks);
640
- MessageFormatter.success("Function transfer phase completed", { prefix: "Transfer" });
706
+ MessageFormatter.success("Function transfer phase completed", {
707
+ prefix: "Transfer",
708
+ });
641
709
  }
642
710
  catch (error) {
643
711
  MessageFormatter.error("Function transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
@@ -720,7 +788,7 @@ export class ComprehensiveTransfer {
720
788
  twoWay: attr.twoWay,
721
789
  twoWayKey: attr.twoWayKey,
722
790
  onDelete: attr.onDelete,
723
- side: attr.side
791
+ side: attr.side,
724
792
  };
725
793
  }
726
794
  /**
@@ -749,10 +817,13 @@ export class ComprehensiveTransfer {
749
817
  let totalSkipped = 0;
750
818
  let totalUpdated = 0;
751
819
  // Check if bulk operations are supported
752
- const supportsBulk = this.options.sourceEndpoint.includes('cloud.appwrite.io') ||
753
- this.options.targetEndpoint.includes('cloud.appwrite.io');
820
+ const bulkEnabled = false;
821
+ // Temporarily disable to see if it fixes my permissions issues
822
+ const supportsBulk = bulkEnabled ? this.options.targetEndpoint.includes("cloud.appwrite.io") : false;
754
823
  if (supportsBulk) {
755
- MessageFormatter.info(`Using bulk operations for enhanced performance`, { prefix: "Transfer" });
824
+ MessageFormatter.info(`Using bulk operations for enhanced performance`, {
825
+ prefix: "Transfer",
826
+ });
756
827
  }
757
828
  while (true) {
758
829
  // Fetch source documents in larger batches (1000 instead of 50)
@@ -766,12 +837,12 @@ export class ComprehensiveTransfer {
766
837
  }
767
838
  MessageFormatter.info(`Processing batch of ${sourceDocuments.documents.length} source documents`, { prefix: "Transfer" });
768
839
  // Extract document IDs from the current batch
769
- const sourceDocIds = sourceDocuments.documents.map(doc => doc.$id);
840
+ const sourceDocIds = sourceDocuments.documents.map((doc) => doc.$id);
770
841
  // Fetch existing documents from target in a single query
771
842
  const existingTargetDocs = await this.fetchTargetDocumentsBatch(targetDb, targetDbId, targetCollectionId, sourceDocIds);
772
843
  // Create a map for quick lookup of existing documents
773
844
  const existingDocsMap = new Map();
774
- existingTargetDocs.forEach(doc => {
845
+ existingTargetDocs.forEach((doc) => {
775
846
  existingDocsMap.set(doc.$id, doc);
776
847
  });
777
848
  // Filter documents based on existence, content comparison, and permission comparison
@@ -785,9 +856,10 @@ export class ComprehensiveTransfer {
785
856
  }
786
857
  else {
787
858
  // 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;
859
+ const sourcePermissions = Array.from(new Set(sourceDoc.$permissions || [])).sort();
860
+ const targetPermissions = Array.from(new Set(existingTargetDoc.$permissions || [])).sort();
861
+ const permissionsDiffer = sourcePermissions.join(",") !== targetPermissions.join(",") ||
862
+ sourcePermissions.length !== targetPermissions.length;
791
863
  // Use objectNeedsUpdate to compare document content (excluding system fields)
792
864
  const contentDiffers = objectNeedsUpdate(existingTargetDoc, sourceDoc);
793
865
  if (contentDiffers && permissionsDiffer) {
@@ -795,32 +867,28 @@ export class ComprehensiveTransfer {
795
867
  documentsToUpdate.push({
796
868
  doc: sourceDoc,
797
869
  targetDoc: existingTargetDoc,
798
- reason: "content and permissions differ"
870
+ reason: "content and permissions differ",
799
871
  });
800
- MessageFormatter.info(`Document ${sourceDoc.$id} exists but content and permissions differ - will update`, { prefix: "Transfer" });
801
872
  }
802
873
  else if (contentDiffers) {
803
874
  // Only content differs
804
875
  documentsToUpdate.push({
805
876
  doc: sourceDoc,
806
877
  targetDoc: existingTargetDoc,
807
- reason: "content differs"
878
+ reason: "content differs",
808
879
  });
809
- MessageFormatter.info(`Document ${sourceDoc.$id} exists but content differs - will update`, { prefix: "Transfer" });
810
880
  }
811
881
  else if (permissionsDiffer) {
812
882
  // Only permissions differ
813
883
  documentsToUpdate.push({
814
884
  doc: sourceDoc,
815
885
  targetDoc: existingTargetDoc,
816
- reason: "permissions differ"
886
+ reason: "permissions differ",
817
887
  });
818
- MessageFormatter.info(`Document ${sourceDoc.$id} exists but permissions differ - will update`, { prefix: "Transfer" });
819
888
  }
820
889
  else {
821
890
  // Document exists with identical content AND permissions, skip
822
891
  totalSkipped++;
823
- MessageFormatter.info(`Document ${sourceDoc.$id} exists with matching content and permissions - skipping`, { prefix: "Transfer" });
824
892
  }
825
893
  }
826
894
  }
@@ -831,7 +899,6 @@ export class ComprehensiveTransfer {
831
899
  // Use bulk operations for large batches
832
900
  await this.transferDocumentsBulk(targetDb, targetDbId, targetCollectionId, documentsToTransfer);
833
901
  totalTransferred += documentsToTransfer.length;
834
- MessageFormatter.success(`Bulk transferred ${documentsToTransfer.length} new documents`, { prefix: "Transfer" });
835
902
  }
836
903
  else {
837
904
  // Use individual transfers for smaller batches or non-bulk endpoints
@@ -847,7 +914,8 @@ export class ComprehensiveTransfer {
847
914
  if (sourceDocuments.documents.length < 1000) {
848
915
  break;
849
916
  }
850
- lastId = sourceDocuments.documents[sourceDocuments.documents.length - 1].$id;
917
+ lastId =
918
+ sourceDocuments.documents[sourceDocuments.documents.length - 1].$id;
851
919
  }
852
920
  MessageFormatter.info(`Transfer complete: ${totalTransferred} new, ${totalUpdated} updated, ${totalSkipped} skipped from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
853
921
  }
@@ -861,8 +929,8 @@ export class ComprehensiveTransfer {
861
929
  for (const chunk of idChunks) {
862
930
  try {
863
931
  const result = await tryAwaitWithRetry(async () => targetDb.listDocuments(targetDbId, targetCollectionId, [
864
- Query.equal('$id', chunk),
865
- Query.limit(100)
932
+ Query.equal("$id", chunk),
933
+ Query.limit(100),
866
934
  ]));
867
935
  documents.push(...result.documents);
868
936
  }
@@ -887,12 +955,12 @@ export class ComprehensiveTransfer {
887
955
  */
888
956
  async transferDocumentsBulk(targetDb, targetDbId, targetCollectionId, documents) {
889
957
  // Prepare documents for bulk upsert
890
- const preparedDocs = documents.map(doc => {
958
+ const preparedDocs = documents.map((doc) => {
891
959
  const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
892
960
  return {
893
961
  $id,
894
962
  $permissions,
895
- ...docData
963
+ ...docData,
896
964
  };
897
965
  });
898
966
  // Process in smaller chunks for bulk operations (1000 for Pro, 100 for Free tier)
@@ -902,13 +970,8 @@ export class ComprehensiveTransfer {
902
970
  const documentBatches = this.chunkArray(preparedDocs, maxBatchSize);
903
971
  try {
904
972
  for (const batch of documentBatches) {
905
- MessageFormatter.info(`Bulk upserting ${batch.length} documents...`, { prefix: "Transfer" });
906
973
  await this.bulkUpsertDocuments(this.targetClient, targetDbId, targetCollectionId, batch);
907
974
  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
975
  }
913
976
  processed = true;
914
977
  break; // Success, exit batch size loop
@@ -931,18 +994,20 @@ export class ComprehensiveTransfer {
931
994
  const apiPath = `/databases/${dbId}/collections/${collectionId}/documents`;
932
995
  const url = new URL(client.config.endpoint + apiPath);
933
996
  const headers = {
934
- 'Content-Type': 'application/json',
935
- 'X-Appwrite-Project': client.config.project,
936
- 'X-Appwrite-Key': client.config.key
997
+ "Content-Type": "application/json",
998
+ "X-Appwrite-Project": client.config.project,
999
+ "X-Appwrite-Key": client.config.key,
937
1000
  };
938
1001
  const response = await fetch(url.toString(), {
939
- method: 'PUT',
1002
+ method: "PUT",
940
1003
  headers,
941
- body: JSON.stringify({ documents })
1004
+ body: JSON.stringify({ documents }),
942
1005
  });
943
1006
  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'}`);
1007
+ const errorData = await response
1008
+ .json()
1009
+ .catch(() => ({ message: "Unknown error" }));
1010
+ throw new Error(`Bulk upsert failed: ${response.status} - ${errorData.message || "Unknown error"}`);
946
1011
  }
947
1012
  return await response.json();
948
1013
  }
@@ -951,14 +1016,28 @@ export class ComprehensiveTransfer {
951
1016
  */
952
1017
  async transferDocumentsIndividual(targetDb, targetDbId, targetCollectionId, documents) {
953
1018
  let successCount = 0;
954
- const transferTasks = documents.map(doc => this.limit(async () => {
1019
+ const transferTasks = documents.map((doc) => this.limit(async () => {
955
1020
  try {
956
1021
  const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
957
1022
  await tryAwaitWithRetry(async () => targetDb.createDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
958
1023
  successCount++;
959
- MessageFormatter.success(`Transferred document ${doc.$id}`, { prefix: "Transfer" });
960
1024
  }
961
1025
  catch (error) {
1026
+ if (error instanceof AppwriteException &&
1027
+ error.message.includes("already exists")) {
1028
+ try {
1029
+ // Update it! It's here because it needs an update or a create
1030
+ const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
1031
+ await tryAwaitWithRetry(async () => targetDb.updateDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
1032
+ successCount++;
1033
+ }
1034
+ catch (updateError) {
1035
+ // just send the error to the formatter
1036
+ MessageFormatter.error(`Failed to transfer document ${doc.$id}`, updateError instanceof Error
1037
+ ? updateError
1038
+ : new Error(String(updateError)), { prefix: "Transfer" });
1039
+ }
1040
+ }
962
1041
  MessageFormatter.error(`Failed to transfer document ${doc.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
963
1042
  }
964
1043
  }));
@@ -975,7 +1054,6 @@ export class ComprehensiveTransfer {
975
1054
  const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
976
1055
  await tryAwaitWithRetry(async () => targetDb.updateDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
977
1056
  successCount++;
978
- MessageFormatter.success(`Updated document ${doc.$id} (${reason}) - permissions: [${targetDoc.$permissions?.join(', ')}] → [${doc.$permissions?.join(', ')}]`, { prefix: "Transfer" });
979
1057
  }
980
1058
  catch (error) {
981
1059
  MessageFormatter.error(`Failed to update document ${doc.$id} (${reason})`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
@@ -1044,18 +1122,23 @@ export class ComprehensiveTransfer {
1044
1122
  * Helper method to transfer team memberships
1045
1123
  */
1046
1124
  async transferTeamMemberships(teamId) {
1047
- MessageFormatter.info(`Transferring memberships for team ${teamId}`, { prefix: "Transfer" });
1125
+ MessageFormatter.info(`Transferring memberships for team ${teamId}`, {
1126
+ prefix: "Transfer",
1127
+ });
1048
1128
  try {
1049
1129
  // Fetch all memberships for this team
1050
1130
  const memberships = await this.fetchAllMemberships(teamId);
1051
1131
  if (memberships.length === 0) {
1052
- MessageFormatter.info(`No memberships found for team ${teamId}`, { prefix: "Transfer" });
1132
+ MessageFormatter.info(`No memberships found for team ${teamId}`, {
1133
+ prefix: "Transfer",
1134
+ });
1053
1135
  return;
1054
1136
  }
1055
1137
  MessageFormatter.info(`Found ${memberships.length} memberships for team ${teamId}`, { prefix: "Transfer" });
1056
1138
  let totalTransferred = 0;
1057
1139
  // Transfer memberships with rate limiting
1058
- const transferTasks = memberships.map(membership => this.userLimit(async () => {
1140
+ const transferTasks = memberships.map((membership) => this.userLimit(async () => {
1141
+ // Use userLimit for team operations (more sensitive)
1059
1142
  try {
1060
1143
  // Check if membership already exists and compare roles
1061
1144
  let existingMembership = null;
@@ -1072,7 +1155,9 @@ export class ComprehensiveTransfer {
1072
1155
  MessageFormatter.success(`Updated membership ${membership.$id} roles to match source`, { prefix: "Transfer" });
1073
1156
  }
1074
1157
  catch (updateError) {
1075
- MessageFormatter.error(`Failed to update roles for membership ${membership.$id}`, updateError instanceof Error ? updateError : new Error(String(updateError)), { prefix: "Transfer" });
1158
+ MessageFormatter.error(`Failed to update roles for membership ${membership.$id}`, updateError instanceof Error
1159
+ ? updateError
1160
+ : new Error(String(updateError)), { prefix: "Transfer" });
1076
1161
  }
1077
1162
  }
1078
1163
  else {
@@ -1114,15 +1199,25 @@ export class ComprehensiveTransfer {
1114
1199
  }
1115
1200
  printSummary() {
1116
1201
  const duration = Math.round((Date.now() - this.startTime) / 1000);
1117
- MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", { prefix: "Transfer" });
1202
+ MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", {
1203
+ prefix: "Transfer",
1204
+ });
1118
1205
  MessageFormatter.info(`Total Time: ${duration}s`, { prefix: "Transfer" });
1119
1206
  MessageFormatter.info(`Users: ${this.results.users.transferred} transferred, ${this.results.users.skipped} skipped, ${this.results.users.failed} failed`, { prefix: "Transfer" });
1120
1207
  MessageFormatter.info(`Teams: ${this.results.teams.transferred} transferred, ${this.results.teams.skipped} skipped, ${this.results.teams.failed} failed`, { prefix: "Transfer" });
1121
1208
  MessageFormatter.info(`Databases: ${this.results.databases.transferred} transferred, ${this.results.databases.skipped} skipped, ${this.results.databases.failed} failed`, { prefix: "Transfer" });
1122
1209
  MessageFormatter.info(`Buckets: ${this.results.buckets.transferred} transferred, ${this.results.buckets.skipped} skipped, ${this.results.buckets.failed} failed`, { prefix: "Transfer" });
1123
1210
  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;
1211
+ const totalTransferred = this.results.users.transferred +
1212
+ this.results.teams.transferred +
1213
+ this.results.databases.transferred +
1214
+ this.results.buckets.transferred +
1215
+ this.results.functions.transferred;
1216
+ const totalFailed = this.results.users.failed +
1217
+ this.results.teams.failed +
1218
+ this.results.databases.failed +
1219
+ this.results.buckets.failed +
1220
+ this.results.functions.failed;
1126
1221
  if (totalFailed === 0) {
1127
1222
  MessageFormatter.success(`All ${totalTransferred} items transferred successfully!`, { prefix: "Transfer" });
1128
1223
  }