appwrite-utils-cli 1.0.9 → 1.1.1

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.
package/README.md CHANGED
@@ -327,6 +327,54 @@ This updated CLI ensures that developers have robust tools at their fingertips t
327
327
 
328
328
  ## Changelog
329
329
 
330
+ ### 1.1.0 - Enhanced Transfer System with Fault Tolerance
331
+
332
+ **🔧 Robust Transfer Operations with Status Monitoring**
333
+
334
+ #### Enhanced Attribute Creation with Fault Tolerance
335
+ - **Exponential Backoff**: Intelligent retry strategy starting at 2 seconds, doubling each retry (2s, 4s, 8s, 16s, 30s max)
336
+ - **Status Monitoring**: Real-time monitoring of attribute states ('available', 'processing', 'stuck', 'failed', 'deleting')
337
+ - **Retry Logic**: Collection deletion/recreation for stuck attributes with up to 5 retry attempts
338
+ - **Sequential Processing**: Attributes processed one-by-one to prevent overwhelming the server
339
+ - **Enhanced Logging**: Comprehensive console feedback with color-coded status messages
340
+
341
+ #### Enhanced Index Creation with Status Monitoring
342
+ - **Similar Fault Tolerance**: Index creation now includes the same robust monitoring and retry logic
343
+ - **Status Checking**: Real-time monitoring of index creation states with proper error handling
344
+ - **Collection Recreation**: Automatic collection deletion/recreation for stuck index operations
345
+ - **Sequential Processing**: Prevents rate limiting by processing indexes individually
346
+
347
+ #### Document Transfer Reliability Improvements
348
+ - **Enhanced Error Handling**: Improved document transfer with exponential backoff retry logic
349
+ - **Smaller Batch Sizes**: Reduced batch sizes (10 documents) to prevent server overload
350
+ - **Better Progress Reporting**: Enhanced progress tracking with success/failure counts
351
+ - **Fault Tolerance**: Graceful handling of duplicate documents and API errors
352
+
353
+ #### Remote Database Transfer Enhancements
354
+ - **Integrated Enhanced Methods**: Updated `transferDatabaseLocalToRemote` to use new attribute and index creation
355
+ - **Proper Wait Logic**: System now properly waits for attributes/indexes to be fully created before proceeding
356
+ - **Status Validation**: Comprehensive status checking throughout the transfer process
357
+ - **Continued Operation**: Transfer continues even if some attributes/indexes fail (with warnings)
358
+
359
+ #### AppwriteConfig Integration for Comprehensive Transfer
360
+ - **Smart Configuration Detection**: Automatically detects existing appwriteConfig for reuse
361
+ - **Source/Target Options**: Users can select their appwriteConfig for either source or target endpoints
362
+ - **Streamlined Setup**: Enhanced user experience with clear configuration prompts
363
+
364
+ #### Technical Implementation
365
+ - **Rate Limiting Respect**: Enhanced operations respect existing rate limiting while adding reliability
366
+ - **Memory Efficiency**: Optimized processing to handle large operations without overwhelming system resources
367
+ - **Error Resilience**: Comprehensive error handling with detailed user feedback and recovery options
368
+ - **Status Persistence**: Operations maintain state information for better debugging and monitoring
369
+
370
+ #### Usage Benefits
371
+ - **Reliability**: Transfer operations no longer fail due to timing issues or stuck operations
372
+ - **Visibility**: Clear progress indicators and status messages throughout all operations
373
+ - **Recovery**: Automatic retry and recovery mechanisms prevent data loss
374
+ - **Performance**: Optimized timing prevents API throttling while maintaining speed
375
+
376
+ **Breaking Change**: None - fully backward compatible with significantly enhanced reliability.
377
+
330
378
  ### 1.0.9 - Enhanced User Transfer with Password Preservation
331
379
 
332
380
  **🔐 Complete Password Hash Preservation During User Transfers**
@@ -1,4 +1,12 @@
1
1
  import { type Databases, type Models } from "node-appwrite";
2
2
  import { type Attribute } from "appwrite-utils";
3
+ /**
4
+ * Enhanced attribute creation with proper status monitoring and retry logic
5
+ */
6
+ export declare const createOrUpdateAttributeWithStatusCheck: (db: Databases, dbId: string, collection: Models.Collection, attribute: Attribute, retryCount?: number, maxRetries?: number) => Promise<boolean>;
3
7
  export declare const createOrUpdateAttribute: (db: Databases, dbId: string, collection: Models.Collection, attribute: Attribute) => Promise<void>;
8
+ /**
9
+ * Enhanced collection attribute creation with proper status monitoring
10
+ */
11
+ export declare const createUpdateCollectionAttributesWithStatusCheck: (db: Databases, dbId: string, collection: Models.Collection, attributes: Attribute[]) => Promise<boolean>;
4
12
  export declare const createUpdateCollectionAttributes: (db: Databases, dbId: string, collection: Models.Collection, attributes: Attribute[]) => Promise<void>;
@@ -3,6 +3,103 @@ import { attributeSchema, parseAttribute, } from "appwrite-utils";
3
3
  import { nameToIdMapping, enqueueOperation } from "../shared/operationQueue.js";
4
4
  import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
5
5
  import chalk from "chalk";
6
+ /**
7
+ * Wait for attribute to become available, with retry logic for stuck attributes and exponential backoff
8
+ */
9
+ const waitForAttributeAvailable = async (db, dbId, collectionId, attributeKey, maxWaitTime = 60000, // 1 minute
10
+ retryCount = 0, maxRetries = 5) => {
11
+ const startTime = Date.now();
12
+ let checkInterval = 2000; // Start with 2 seconds
13
+ // Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
14
+ if (retryCount > 0) {
15
+ const exponentialDelay = Math.min(2000 * Math.pow(2, retryCount), 30000);
16
+ console.log(chalk.blue(`Waiting for attribute '${attributeKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`));
17
+ await delay(exponentialDelay);
18
+ }
19
+ else {
20
+ console.log(chalk.blue(`Waiting for attribute '${attributeKey}' to become available...`));
21
+ }
22
+ while (Date.now() - startTime < maxWaitTime) {
23
+ try {
24
+ const collection = await db.getCollection(dbId, collectionId);
25
+ const attribute = collection.attributes.find((attr) => attr.key === attributeKey);
26
+ if (!attribute) {
27
+ console.log(chalk.red(`Attribute '${attributeKey}' not found`));
28
+ return false;
29
+ }
30
+ console.log(chalk.gray(`Attribute '${attributeKey}' status: ${attribute.status}`));
31
+ switch (attribute.status) {
32
+ case 'available':
33
+ console.log(chalk.green(`✅ Attribute '${attributeKey}' is now available`));
34
+ return true;
35
+ case 'failed':
36
+ console.log(chalk.red(`❌ Attribute '${attributeKey}' failed: ${attribute.error}`));
37
+ return false;
38
+ case 'stuck':
39
+ console.log(chalk.yellow(`⚠️ Attribute '${attributeKey}' is stuck, will retry...`));
40
+ return false;
41
+ case 'processing':
42
+ // Continue waiting
43
+ break;
44
+ case 'deleting':
45
+ console.log(chalk.yellow(`Attribute '${attributeKey}' is being deleted`));
46
+ break;
47
+ default:
48
+ console.log(chalk.yellow(`Unknown status '${attribute.status}' for attribute '${attributeKey}'`));
49
+ break;
50
+ }
51
+ await delay(checkInterval);
52
+ }
53
+ catch (error) {
54
+ console.log(chalk.red(`Error checking attribute status: ${error}`));
55
+ return false;
56
+ }
57
+ }
58
+ // Timeout reached
59
+ console.log(chalk.yellow(`⏰ Timeout waiting for attribute '${attributeKey}' (${maxWaitTime}ms)`));
60
+ // If we have retries left and this isn't the last retry, try recreating
61
+ if (retryCount < maxRetries) {
62
+ console.log(chalk.yellow(`🔄 Retrying attribute creation (attempt ${retryCount + 1}/${maxRetries})`));
63
+ return false; // Signal that we need to retry
64
+ }
65
+ return false;
66
+ };
67
+ /**
68
+ * Wait for all attributes in a collection to become available
69
+ */
70
+ const waitForAllAttributesAvailable = async (db, dbId, collectionId, attributeKeys, maxWaitTime = 60000) => {
71
+ console.log(chalk.blue(`Waiting for ${attributeKeys.length} attributes to become available...`));
72
+ const failedAttributes = [];
73
+ for (const attributeKey of attributeKeys) {
74
+ const success = await waitForAttributeAvailable(db, dbId, collectionId, attributeKey, maxWaitTime);
75
+ if (!success) {
76
+ failedAttributes.push(attributeKey);
77
+ }
78
+ }
79
+ return failedAttributes;
80
+ };
81
+ /**
82
+ * Delete collection and recreate with retry logic
83
+ */
84
+ const deleteAndRecreateCollection = async (db, dbId, collection, retryCount) => {
85
+ try {
86
+ console.log(chalk.yellow(`🗑️ Deleting collection '${collection.name}' for retry ${retryCount}`));
87
+ // Delete the collection
88
+ await db.deleteCollection(dbId, collection.$id);
89
+ console.log(chalk.yellow(`Deleted collection '${collection.name}'`));
90
+ // Wait a bit before recreating
91
+ await delay(2000);
92
+ // Recreate the collection
93
+ console.log(chalk.blue(`🔄 Recreating collection '${collection.name}'`));
94
+ const newCollection = await db.createCollection(dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled);
95
+ console.log(chalk.green(`✅ Recreated collection '${collection.name}'`));
96
+ return newCollection;
97
+ }
98
+ catch (error) {
99
+ console.log(chalk.red(`Failed to delete/recreate collection '${collection.name}': ${error}`));
100
+ return null;
101
+ }
102
+ };
6
103
  const attributesSame = (databaseAttribute, configAttribute) => {
7
104
  const attributesToCheck = [
8
105
  "key",
@@ -53,6 +150,46 @@ const attributesSame = (databaseAttribute, configAttribute) => {
53
150
  return false;
54
151
  });
55
152
  };
153
+ /**
154
+ * Enhanced attribute creation with proper status monitoring and retry logic
155
+ */
156
+ export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collection, attribute, retryCount = 0, maxRetries = 5) => {
157
+ console.log(chalk.blue(`Creating/updating attribute '${attribute.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`));
158
+ try {
159
+ // First, try to create/update the attribute using existing logic
160
+ await createOrUpdateAttribute(db, dbId, collection, attribute);
161
+ // Now wait for the attribute to become available
162
+ const success = await waitForAttributeAvailable(db, dbId, collection.$id, attribute.key, 60000, // 1 minute timeout
163
+ retryCount, maxRetries);
164
+ if (success) {
165
+ return true;
166
+ }
167
+ // If not successful and we have retries left, delete collection and try again
168
+ if (retryCount < maxRetries) {
169
+ console.log(chalk.yellow(`Attribute '${attribute.key}' failed/stuck, retrying...`));
170
+ // Get fresh collection data
171
+ const freshCollection = await db.getCollection(dbId, collection.$id);
172
+ // Delete and recreate collection
173
+ const newCollection = await deleteAndRecreateCollection(db, dbId, freshCollection, retryCount + 1);
174
+ if (newCollection) {
175
+ // Retry with the new collection
176
+ return await createOrUpdateAttributeWithStatusCheck(db, dbId, newCollection, attribute, retryCount + 1, maxRetries);
177
+ }
178
+ }
179
+ console.log(chalk.red(`❌ Failed to create attribute '${attribute.key}' after ${maxRetries + 1} attempts`));
180
+ return false;
181
+ }
182
+ catch (error) {
183
+ console.log(chalk.red(`Error creating attribute '${attribute.key}': ${error}`));
184
+ if (retryCount < maxRetries) {
185
+ console.log(chalk.yellow(`Retrying attribute '${attribute.key}' due to error...`));
186
+ // Wait a bit before retry
187
+ await delay(2000);
188
+ return await createOrUpdateAttributeWithStatusCheck(db, dbId, collection, attribute, retryCount + 1, maxRetries);
189
+ }
190
+ return false;
191
+ }
192
+ };
56
193
  export const createOrUpdateAttribute = async (db, dbId, collection, attribute) => {
57
194
  let action = "create";
58
195
  let foundAttribute;
@@ -273,6 +410,64 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
273
410
  break;
274
411
  }
275
412
  };
413
+ /**
414
+ * Enhanced collection attribute creation with proper status monitoring
415
+ */
416
+ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId, collection, attributes) => {
417
+ console.log(chalk.green(`Creating/Updating attributes for collection: ${collection.name} with status monitoring`));
418
+ const existingAttributes =
419
+ // @ts-expect-error
420
+ collection.attributes.map((attr) => parseAttribute(attr)) || [];
421
+ const attributesToRemove = existingAttributes.filter((attr) => !attributes.some((a) => a.key === attr.key));
422
+ const indexesToRemove = collection.indexes.filter((index) => attributesToRemove.some((attr) => index.attributes.includes(attr.key)));
423
+ // Handle attribute removal first
424
+ if (attributesToRemove.length > 0) {
425
+ if (indexesToRemove.length > 0) {
426
+ console.log(chalk.red(`Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove
427
+ .map((index) => index.key)
428
+ .join(", ")}`));
429
+ for (const index of indexesToRemove) {
430
+ await tryAwaitWithRetry(async () => await db.deleteIndex(dbId, collection.$id, index.key));
431
+ await delay(500); // Longer delay for deletions
432
+ }
433
+ }
434
+ for (const attr of attributesToRemove) {
435
+ console.log(chalk.red(`Removing attribute: ${attr.key} as it is no longer in the collection`));
436
+ await tryAwaitWithRetry(async () => await db.deleteAttribute(dbId, collection.$id, attr.key));
437
+ await delay(500); // Longer delay for deletions
438
+ }
439
+ }
440
+ // Create attributes ONE BY ONE with proper status checking
441
+ console.log(chalk.blue(`Creating ${attributes.length} attributes sequentially with status monitoring...`));
442
+ let currentCollection = collection;
443
+ const failedAttributes = [];
444
+ for (const attribute of attributes) {
445
+ console.log(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`));
446
+ const success = await createOrUpdateAttributeWithStatusCheck(db, dbId, currentCollection, attribute);
447
+ if (success) {
448
+ console.log(chalk.green(`✅ Successfully created attribute: ${attribute.key}`));
449
+ // Get updated collection data for next iteration
450
+ try {
451
+ currentCollection = await db.getCollection(dbId, collection.$id);
452
+ }
453
+ catch (error) {
454
+ console.log(chalk.yellow(`Warning: Could not refresh collection data: ${error}`));
455
+ }
456
+ // Add delay between successful attributes
457
+ await delay(1000);
458
+ }
459
+ else {
460
+ console.log(chalk.red(`❌ Failed to create attribute: ${attribute.key}`));
461
+ failedAttributes.push(attribute.key);
462
+ }
463
+ }
464
+ if (failedAttributes.length > 0) {
465
+ console.log(chalk.red(`\n❌ Failed to create ${failedAttributes.length} attributes: ${failedAttributes.join(', ')}`));
466
+ return false;
467
+ }
468
+ console.log(chalk.green(`\n✅ Successfully created all ${attributes.length} attributes for collection: ${collection.name}`));
469
+ return true;
470
+ };
276
471
  export const createUpdateCollectionAttributes = async (db, dbId, collection, attributes) => {
277
472
  console.log(chalk.green(`Creating/Updating attributes for collection: ${collection.name}`));
278
473
  const existingAttributes =
@@ -1,4 +1,12 @@
1
1
  import { type Index } from "appwrite-utils";
2
2
  import { Databases, type Models } from "node-appwrite";
3
+ /**
4
+ * Enhanced index creation with proper status monitoring and retry logic
5
+ */
6
+ export declare const createOrUpdateIndexWithStatusCheck: (dbId: string, db: Databases, collectionId: string, collection: Models.Collection, index: Index, retryCount?: number, maxRetries?: number) => Promise<boolean>;
7
+ /**
8
+ * Enhanced index creation with status monitoring for all indexes
9
+ */
10
+ export declare const createOrUpdateIndexesWithStatusCheck: (dbId: string, db: Databases, collectionId: string, collection: Models.Collection, indexes: Index[]) => Promise<boolean>;
3
11
  export declare const createOrUpdateIndex: (dbId: string, db: Databases, collectionId: string, index: Index) => Promise<Models.Index | null>;
4
12
  export declare const createOrUpdateIndexes: (dbId: string, db: Databases, collectionId: string, indexes: Index[]) => Promise<void>;
@@ -1,6 +1,156 @@
1
1
  import { indexSchema } from "appwrite-utils";
2
2
  import { Databases, IndexType, Query } from "node-appwrite";
3
3
  import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
4
+ import chalk from "chalk";
5
+ /**
6
+ * Wait for index to become available, with retry logic for stuck indexes and exponential backoff
7
+ */
8
+ const waitForIndexAvailable = async (db, dbId, collectionId, indexKey, maxWaitTime = 60000, // 1 minute
9
+ retryCount = 0, maxRetries = 5) => {
10
+ const startTime = Date.now();
11
+ let checkInterval = 2000; // Start with 2 seconds
12
+ // Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
13
+ if (retryCount > 0) {
14
+ const exponentialDelay = Math.min(2000 * Math.pow(2, retryCount), 30000);
15
+ console.log(chalk.blue(`Waiting for index '${indexKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`));
16
+ await delay(exponentialDelay);
17
+ }
18
+ else {
19
+ console.log(chalk.blue(`Waiting for index '${indexKey}' to become available...`));
20
+ }
21
+ while (Date.now() - startTime < maxWaitTime) {
22
+ try {
23
+ const indexList = await db.listIndexes(dbId, collectionId);
24
+ const index = indexList.indexes.find((idx) => idx.key === indexKey);
25
+ if (!index) {
26
+ console.log(chalk.red(`Index '${indexKey}' not found`));
27
+ return false;
28
+ }
29
+ console.log(chalk.gray(`Index '${indexKey}' status: ${index.status}`));
30
+ switch (index.status) {
31
+ case 'available':
32
+ console.log(chalk.green(`✅ Index '${indexKey}' is now available`));
33
+ return true;
34
+ case 'failed':
35
+ console.log(chalk.red(`❌ Index '${indexKey}' failed: ${index.error}`));
36
+ return false;
37
+ case 'stuck':
38
+ console.log(chalk.yellow(`⚠️ Index '${indexKey}' is stuck, will retry...`));
39
+ return false;
40
+ case 'processing':
41
+ // Continue waiting
42
+ break;
43
+ case 'deleting':
44
+ console.log(chalk.yellow(`Index '${indexKey}' is being deleted`));
45
+ break;
46
+ default:
47
+ console.log(chalk.yellow(`Unknown status '${index.status}' for index '${indexKey}'`));
48
+ break;
49
+ }
50
+ await delay(checkInterval);
51
+ }
52
+ catch (error) {
53
+ console.log(chalk.red(`Error checking index status: ${error}`));
54
+ return false;
55
+ }
56
+ }
57
+ // Timeout reached
58
+ console.log(chalk.yellow(`⏰ Timeout waiting for index '${indexKey}' (${maxWaitTime}ms)`));
59
+ // If we have retries left and this isn't the last retry, try recreating
60
+ if (retryCount < maxRetries) {
61
+ console.log(chalk.yellow(`🔄 Retrying index creation (attempt ${retryCount + 1}/${maxRetries})`));
62
+ return false; // Signal that we need to retry
63
+ }
64
+ return false;
65
+ };
66
+ /**
67
+ * Delete collection and recreate for index retry (reused from attributes.ts)
68
+ */
69
+ const deleteAndRecreateCollectionForIndex = async (db, dbId, collection, retryCount) => {
70
+ try {
71
+ console.log(chalk.yellow(`🗑️ Deleting collection '${collection.name}' for index retry ${retryCount}`));
72
+ // Delete the collection
73
+ await db.deleteCollection(dbId, collection.$id);
74
+ console.log(chalk.yellow(`Deleted collection '${collection.name}'`));
75
+ // Wait a bit before recreating
76
+ await delay(2000);
77
+ // Recreate the collection
78
+ console.log(chalk.blue(`🔄 Recreating collection '${collection.name}'`));
79
+ const newCollection = await db.createCollection(dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled);
80
+ console.log(chalk.green(`✅ Recreated collection '${collection.name}'`));
81
+ return newCollection;
82
+ }
83
+ catch (error) {
84
+ console.log(chalk.red(`Failed to delete/recreate collection '${collection.name}': ${error}`));
85
+ return null;
86
+ }
87
+ };
88
+ /**
89
+ * Enhanced index creation with proper status monitoring and retry logic
90
+ */
91
+ export const createOrUpdateIndexWithStatusCheck = async (dbId, db, collectionId, collection, index, retryCount = 0, maxRetries = 5) => {
92
+ console.log(chalk.blue(`Creating/updating index '${index.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`));
93
+ try {
94
+ // First, try to create/update the index using existing logic
95
+ await createOrUpdateIndex(dbId, db, collectionId, index);
96
+ // Now wait for the index to become available
97
+ const success = await waitForIndexAvailable(db, dbId, collectionId, index.key, 60000, // 1 minute timeout
98
+ retryCount, maxRetries);
99
+ if (success) {
100
+ return true;
101
+ }
102
+ // If not successful and we have retries left, delete collection and try again
103
+ if (retryCount < maxRetries) {
104
+ console.log(chalk.yellow(`Index '${index.key}' failed/stuck, retrying...`));
105
+ // Get fresh collection data
106
+ const freshCollection = await db.getCollection(dbId, collectionId);
107
+ // Delete and recreate collection
108
+ const newCollection = await deleteAndRecreateCollectionForIndex(db, dbId, freshCollection, retryCount + 1);
109
+ if (newCollection) {
110
+ // Retry with the new collection
111
+ return await createOrUpdateIndexWithStatusCheck(dbId, db, newCollection.$id, newCollection, index, retryCount + 1, maxRetries);
112
+ }
113
+ }
114
+ console.log(chalk.red(`❌ Failed to create index '${index.key}' after ${maxRetries + 1} attempts`));
115
+ return false;
116
+ }
117
+ catch (error) {
118
+ console.log(chalk.red(`Error creating index '${index.key}': ${error}`));
119
+ if (retryCount < maxRetries) {
120
+ console.log(chalk.yellow(`Retrying index '${index.key}' due to error...`));
121
+ // Wait a bit before retry
122
+ await delay(2000);
123
+ return await createOrUpdateIndexWithStatusCheck(dbId, db, collectionId, collection, index, retryCount + 1, maxRetries);
124
+ }
125
+ return false;
126
+ }
127
+ };
128
+ /**
129
+ * Enhanced index creation with status monitoring for all indexes
130
+ */
131
+ export const createOrUpdateIndexesWithStatusCheck = async (dbId, db, collectionId, collection, indexes) => {
132
+ console.log(chalk.blue(`Creating/updating ${indexes.length} indexes with status monitoring...`));
133
+ const failedIndexes = [];
134
+ for (const index of indexes) {
135
+ console.log(chalk.blue(`\n--- Processing index: ${index.key} ---`));
136
+ const success = await createOrUpdateIndexWithStatusCheck(dbId, db, collectionId, collection, index);
137
+ if (success) {
138
+ console.log(chalk.green(`✅ Successfully created index: ${index.key}`));
139
+ // Add delay between successful indexes
140
+ await delay(1000);
141
+ }
142
+ else {
143
+ console.log(chalk.red(`❌ Failed to create index: ${index.key}`));
144
+ failedIndexes.push(index.key);
145
+ }
146
+ }
147
+ if (failedIndexes.length > 0) {
148
+ console.log(chalk.red(`\n❌ Failed to create ${failedIndexes.length} indexes: ${failedIndexes.join(', ')}`));
149
+ return false;
150
+ }
151
+ console.log(chalk.green(`\n✅ Successfully created all ${indexes.length} indexes`));
152
+ return true;
153
+ };
4
154
  export const createOrUpdateIndex = async (dbId, db, collectionId, index) => {
5
155
  const existingIndex = await db.listIndexes(dbId, collectionId, [
6
156
  Query.equal("key", index.key),
@@ -7,6 +7,7 @@ import { isNull, isUndefined, isNil, isPlainObject, isString, isJSONValue, chunk
7
7
  import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
8
8
  import { MessageFormatter } from "../shared/messageFormatter.js";
9
9
  import { ProgressManager } from "../shared/progressManager.js";
10
+ import chalk from "chalk";
10
11
  export const documentExists = async (db, dbId, targetCollectionId, toCreateObject) => {
11
12
  const collection = await db.getCollection(dbId, targetCollectionId);
12
13
  const attributes = collection.attributes;
@@ -380,69 +381,120 @@ export const transferDocumentsBetweenDbsLocalToLocal = async (db, fromDbId, toDb
380
381
  }
381
382
  MessageFormatter.success(`Transferred ${totalDocumentsTransferred} documents from database ${fromDbId} to database ${toDbId} -- collection ${fromCollId} to collection ${toCollId}`, { prefix: "Transfer" });
382
383
  };
384
+ /**
385
+ * Enhanced document transfer with fault tolerance and exponential backoff
386
+ */
387
+ const transferDocumentWithRetry = async (db, dbId, collectionId, documentId, documentData, permissions, maxRetries = 3, retryCount = 0) => {
388
+ try {
389
+ await db.createDocument(dbId, collectionId, documentId, documentData, permissions);
390
+ return true;
391
+ }
392
+ catch (error) {
393
+ // Check if document already exists
394
+ if (error.code === 409 || error.message?.includes('already exists')) {
395
+ console.log(chalk.yellow(`Document ${documentId} already exists, skipping...`));
396
+ return true;
397
+ }
398
+ if (retryCount < maxRetries) {
399
+ // Calculate exponential backoff: 1s, 2s, 4s
400
+ const exponentialDelay = Math.min(1000 * Math.pow(2, retryCount), 8000);
401
+ console.log(chalk.yellow(`Retrying document ${documentId} (attempt ${retryCount + 1}/${maxRetries}, backoff: ${exponentialDelay}ms)`));
402
+ await delay(exponentialDelay);
403
+ return await transferDocumentWithRetry(db, dbId, collectionId, documentId, documentData, permissions, maxRetries, retryCount + 1);
404
+ }
405
+ console.log(chalk.red(`Failed to transfer document ${documentId} after ${maxRetries} retries: ${error.message}`));
406
+ return false;
407
+ }
408
+ };
409
+ /**
410
+ * Enhanced batch document transfer with fault tolerance
411
+ */
412
+ const transferDocumentBatchWithRetry = async (db, dbId, collectionId, documents, batchSize = 10) => {
413
+ let successful = 0;
414
+ let failed = 0;
415
+ // Process documents in smaller batches to avoid overwhelming the server
416
+ const documentBatches = chunk(documents, batchSize);
417
+ for (const batch of documentBatches) {
418
+ console.log(chalk.blue(`Processing batch of ${batch.length} documents...`));
419
+ const batchPromises = batch.map(async (doc) => {
420
+ const toCreateObject = { ...doc };
421
+ delete toCreateObject.$databaseId;
422
+ delete toCreateObject.$collectionId;
423
+ delete toCreateObject.$createdAt;
424
+ delete toCreateObject.$updatedAt;
425
+ delete toCreateObject.$id;
426
+ delete toCreateObject.$permissions;
427
+ const result = await transferDocumentWithRetry(db, dbId, collectionId, doc.$id, toCreateObject, doc.$permissions || []);
428
+ return { docId: doc.$id, success: result };
429
+ });
430
+ const results = await Promise.allSettled(batchPromises);
431
+ results.forEach((result, index) => {
432
+ if (result.status === 'fulfilled') {
433
+ if (result.value.success) {
434
+ successful++;
435
+ }
436
+ else {
437
+ failed++;
438
+ }
439
+ }
440
+ else {
441
+ console.log(chalk.red(`Batch promise rejected for document ${batch[index].$id}: ${result.reason}`));
442
+ failed++;
443
+ }
444
+ });
445
+ // Add delay between batches to avoid rate limiting
446
+ if (documentBatches.indexOf(batch) < documentBatches.length - 1) {
447
+ await delay(500);
448
+ }
449
+ }
450
+ return { successful, failed };
451
+ };
383
452
  export const transferDocumentsBetweenDbsLocalToRemote = async (localDb, endpoint, projectId, apiKey, fromDbId, toDbId, fromCollId, toCollId) => {
453
+ console.log(chalk.blue(`Starting enhanced document transfer from ${fromCollId} to ${toCollId}...`));
384
454
  const client = new Client()
385
455
  .setEndpoint(endpoint)
386
456
  .setProject(projectId)
387
457
  .setKey(apiKey);
388
- let totalDocumentsTransferred = 0;
389
458
  const remoteDb = new Databases(client);
390
- let fromCollDocs = await tryAwaitWithRetry(async () => localDb.listDocuments(fromDbId, fromCollId, [Query.limit(50)]));
391
- if (fromCollDocs.documents.length === 0) {
459
+ let totalDocumentsProcessed = 0;
460
+ let totalSuccessful = 0;
461
+ let totalFailed = 0;
462
+ // Fetch documents in batches
463
+ let hasMoreDocuments = true;
464
+ let lastDocumentId;
465
+ while (hasMoreDocuments) {
466
+ const queries = [Query.limit(50)];
467
+ if (lastDocumentId) {
468
+ queries.push(Query.cursorAfter(lastDocumentId));
469
+ }
470
+ const fromCollDocs = await tryAwaitWithRetry(async () => localDb.listDocuments(fromDbId, fromCollId, queries));
471
+ if (fromCollDocs.documents.length === 0) {
472
+ hasMoreDocuments = false;
473
+ break;
474
+ }
475
+ console.log(chalk.blue(`Processing ${fromCollDocs.documents.length} documents...`));
476
+ const { successful, failed } = await transferDocumentBatchWithRetry(remoteDb, toDbId, toCollId, fromCollDocs.documents);
477
+ totalDocumentsProcessed += fromCollDocs.documents.length;
478
+ totalSuccessful += successful;
479
+ totalFailed += failed;
480
+ // Check if we have more documents to process
481
+ if (fromCollDocs.documents.length < 50) {
482
+ hasMoreDocuments = false;
483
+ }
484
+ else {
485
+ lastDocumentId = fromCollDocs.documents[fromCollDocs.documents.length - 1].$id;
486
+ }
487
+ console.log(chalk.gray(`Batch complete: ${successful} successful, ${failed} failed`));
488
+ }
489
+ if (totalDocumentsProcessed === 0) {
392
490
  MessageFormatter.info(`No documents found in collection ${fromCollId}`, { prefix: "Transfer" });
393
491
  return;
394
492
  }
395
- else if (fromCollDocs.documents.length < 50) {
396
- const batchedPromises = fromCollDocs.documents.map((doc) => {
397
- const toCreateObject = {
398
- ...doc,
399
- };
400
- delete toCreateObject.$databaseId;
401
- delete toCreateObject.$collectionId;
402
- delete toCreateObject.$createdAt;
403
- delete toCreateObject.$updatedAt;
404
- delete toCreateObject.$id;
405
- delete toCreateObject.$permissions;
406
- return tryAwaitWithRetry(async () => remoteDb.createDocument(toDbId, toCollId, doc.$id, toCreateObject, doc.$permissions));
407
- });
408
- await Promise.all(batchedPromises);
409
- totalDocumentsTransferred += fromCollDocs.documents.length;
493
+ const message = `Total documents processed: ${totalDocumentsProcessed}, successful: ${totalSuccessful}, failed: ${totalFailed}`;
494
+ if (totalFailed > 0) {
495
+ MessageFormatter.warning(message, { prefix: "Transfer" });
410
496
  }
411
497
  else {
412
- const batchedPromises = fromCollDocs.documents.map((doc) => {
413
- const toCreateObject = {
414
- ...doc,
415
- };
416
- delete toCreateObject.$databaseId;
417
- delete toCreateObject.$collectionId;
418
- delete toCreateObject.$createdAt;
419
- delete toCreateObject.$updatedAt;
420
- delete toCreateObject.$id;
421
- delete toCreateObject.$permissions;
422
- return tryAwaitWithRetry(async () => remoteDb.createDocument(toDbId, toCollId, doc.$id, toCreateObject, doc.$permissions));
423
- });
424
- await Promise.all(batchedPromises);
425
- totalDocumentsTransferred += fromCollDocs.documents.length;
426
- while (fromCollDocs.documents.length === 50) {
427
- fromCollDocs = await tryAwaitWithRetry(async () => localDb.listDocuments(fromDbId, fromCollId, [
428
- Query.limit(50),
429
- Query.cursorAfter(fromCollDocs.documents[fromCollDocs.documents.length - 1].$id),
430
- ]));
431
- const batchedPromises = fromCollDocs.documents.map((doc) => {
432
- const toCreateObject = {
433
- ...doc,
434
- };
435
- delete toCreateObject.$databaseId;
436
- delete toCreateObject.$collectionId;
437
- delete toCreateObject.$createdAt;
438
- delete toCreateObject.$updatedAt;
439
- delete toCreateObject.$id;
440
- delete toCreateObject.$permissions;
441
- return tryAwaitWithRetry(async () => remoteDb.createDocument(toDbId, toCollId, doc.$id, toCreateObject, doc.$permissions));
442
- });
443
- await Promise.all(batchedPromises);
444
- totalDocumentsTransferred += fromCollDocs.documents.length;
445
- }
498
+ MessageFormatter.success(message, { prefix: "Transfer" });
446
499
  }
447
- MessageFormatter.success(`Total documents transferred from database ${fromDbId} to database ${toDbId} -- collection ${fromCollId} to collection ${toCollId}: ${totalDocumentsTransferred}`, { prefix: "Transfer" });
448
500
  };