appwrite-utils-cli 1.0.9 → 1.1.0

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,6 +1,270 @@
1
1
  import { indexSchema, type Index } from "appwrite-utils";
2
2
  import { Databases, IndexType, Query, type Models } from "node-appwrite";
3
3
  import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
4
+ import chalk from "chalk";
5
+
6
+ // Interface for index with status
7
+ interface IndexWithStatus {
8
+ key: string;
9
+ type: string;
10
+ status: 'available' | 'processing' | 'deleting' | 'stuck' | 'failed';
11
+ error: string;
12
+ attributes: string[];
13
+ orders?: string[];
14
+ $createdAt: string;
15
+ $updatedAt: string;
16
+ }
17
+
18
+ /**
19
+ * Wait for index to become available, with retry logic for stuck indexes and exponential backoff
20
+ */
21
+ const waitForIndexAvailable = async (
22
+ db: Databases,
23
+ dbId: string,
24
+ collectionId: string,
25
+ indexKey: string,
26
+ maxWaitTime: number = 60000, // 1 minute
27
+ retryCount: number = 0,
28
+ maxRetries: number = 5
29
+ ): Promise<boolean> => {
30
+ const startTime = Date.now();
31
+ let checkInterval = 2000; // Start with 2 seconds
32
+
33
+ // Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
34
+ if (retryCount > 0) {
35
+ const exponentialDelay = Math.min(2000 * Math.pow(2, retryCount), 30000);
36
+ console.log(chalk.blue(`Waiting for index '${indexKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`));
37
+ await delay(exponentialDelay);
38
+ } else {
39
+ console.log(chalk.blue(`Waiting for index '${indexKey}' to become available...`));
40
+ }
41
+
42
+ while (Date.now() - startTime < maxWaitTime) {
43
+ try {
44
+ const indexList = await db.listIndexes(dbId, collectionId);
45
+ const index = indexList.indexes.find(
46
+ (idx: any) => idx.key === indexKey
47
+ ) as IndexWithStatus | undefined;
48
+
49
+ if (!index) {
50
+ console.log(chalk.red(`Index '${indexKey}' not found`));
51
+ return false;
52
+ }
53
+
54
+ console.log(chalk.gray(`Index '${indexKey}' status: ${index.status}`));
55
+
56
+ switch (index.status) {
57
+ case 'available':
58
+ console.log(chalk.green(`✅ Index '${indexKey}' is now available`));
59
+ return true;
60
+
61
+ case 'failed':
62
+ console.log(chalk.red(`❌ Index '${indexKey}' failed: ${index.error}`));
63
+ return false;
64
+
65
+ case 'stuck':
66
+ console.log(chalk.yellow(`⚠️ Index '${indexKey}' is stuck, will retry...`));
67
+ return false;
68
+
69
+ case 'processing':
70
+ // Continue waiting
71
+ break;
72
+
73
+ case 'deleting':
74
+ console.log(chalk.yellow(`Index '${indexKey}' is being deleted`));
75
+ break;
76
+
77
+ default:
78
+ console.log(chalk.yellow(`Unknown status '${index.status}' for index '${indexKey}'`));
79
+ break;
80
+ }
81
+
82
+ await delay(checkInterval);
83
+ } catch (error) {
84
+ console.log(chalk.red(`Error checking index status: ${error}`));
85
+ return false;
86
+ }
87
+ }
88
+
89
+ // Timeout reached
90
+ console.log(chalk.yellow(`⏰ Timeout waiting for index '${indexKey}' (${maxWaitTime}ms)`));
91
+
92
+ // If we have retries left and this isn't the last retry, try recreating
93
+ if (retryCount < maxRetries) {
94
+ console.log(chalk.yellow(`🔄 Retrying index creation (attempt ${retryCount + 1}/${maxRetries})`));
95
+ return false; // Signal that we need to retry
96
+ }
97
+
98
+ return false;
99
+ };
100
+
101
+ /**
102
+ * Delete collection and recreate for index retry (reused from attributes.ts)
103
+ */
104
+ const deleteAndRecreateCollectionForIndex = async (
105
+ db: Databases,
106
+ dbId: string,
107
+ collection: Models.Collection,
108
+ retryCount: number
109
+ ): Promise<Models.Collection | null> => {
110
+ try {
111
+ console.log(chalk.yellow(`🗑️ Deleting collection '${collection.name}' for index retry ${retryCount}`));
112
+
113
+ // Delete the collection
114
+ await db.deleteCollection(dbId, collection.$id);
115
+ console.log(chalk.yellow(`Deleted collection '${collection.name}'`));
116
+
117
+ // Wait a bit before recreating
118
+ await delay(2000);
119
+
120
+ // Recreate the collection
121
+ console.log(chalk.blue(`🔄 Recreating collection '${collection.name}'`));
122
+ const newCollection = await db.createCollection(
123
+ dbId,
124
+ collection.$id,
125
+ collection.name,
126
+ collection.$permissions,
127
+ collection.documentSecurity,
128
+ collection.enabled
129
+ );
130
+
131
+ console.log(chalk.green(`✅ Recreated collection '${collection.name}'`));
132
+ return newCollection;
133
+
134
+ } catch (error) {
135
+ console.log(chalk.red(`Failed to delete/recreate collection '${collection.name}': ${error}`));
136
+ return null;
137
+ }
138
+ };
139
+
140
+ /**
141
+ * Enhanced index creation with proper status monitoring and retry logic
142
+ */
143
+ export const createOrUpdateIndexWithStatusCheck = async (
144
+ dbId: string,
145
+ db: Databases,
146
+ collectionId: string,
147
+ collection: Models.Collection,
148
+ index: Index,
149
+ retryCount: number = 0,
150
+ maxRetries: number = 5
151
+ ): Promise<boolean> => {
152
+ console.log(chalk.blue(`Creating/updating index '${index.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`));
153
+
154
+ try {
155
+ // First, try to create/update the index using existing logic
156
+ await createOrUpdateIndex(dbId, db, collectionId, index);
157
+
158
+ // Now wait for the index to become available
159
+ const success = await waitForIndexAvailable(
160
+ db,
161
+ dbId,
162
+ collectionId,
163
+ index.key,
164
+ 60000, // 1 minute timeout
165
+ retryCount,
166
+ maxRetries
167
+ );
168
+
169
+ if (success) {
170
+ return true;
171
+ }
172
+
173
+ // If not successful and we have retries left, delete collection and try again
174
+ if (retryCount < maxRetries) {
175
+ console.log(chalk.yellow(`Index '${index.key}' failed/stuck, retrying...`));
176
+
177
+ // Get fresh collection data
178
+ const freshCollection = await db.getCollection(dbId, collectionId);
179
+
180
+ // Delete and recreate collection
181
+ const newCollection = await deleteAndRecreateCollectionForIndex(db, dbId, freshCollection, retryCount + 1);
182
+
183
+ if (newCollection) {
184
+ // Retry with the new collection
185
+ return await createOrUpdateIndexWithStatusCheck(
186
+ dbId,
187
+ db,
188
+ newCollection.$id,
189
+ newCollection,
190
+ index,
191
+ retryCount + 1,
192
+ maxRetries
193
+ );
194
+ }
195
+ }
196
+
197
+ console.log(chalk.red(`❌ Failed to create index '${index.key}' after ${maxRetries + 1} attempts`));
198
+ return false;
199
+
200
+ } catch (error) {
201
+ console.log(chalk.red(`Error creating index '${index.key}': ${error}`));
202
+
203
+ if (retryCount < maxRetries) {
204
+ console.log(chalk.yellow(`Retrying index '${index.key}' due to error...`));
205
+
206
+ // Wait a bit before retry
207
+ await delay(2000);
208
+
209
+ return await createOrUpdateIndexWithStatusCheck(
210
+ dbId,
211
+ db,
212
+ collectionId,
213
+ collection,
214
+ index,
215
+ retryCount + 1,
216
+ maxRetries
217
+ );
218
+ }
219
+
220
+ return false;
221
+ }
222
+ };
223
+
224
+ /**
225
+ * Enhanced index creation with status monitoring for all indexes
226
+ */
227
+ export const createOrUpdateIndexesWithStatusCheck = async (
228
+ dbId: string,
229
+ db: Databases,
230
+ collectionId: string,
231
+ collection: Models.Collection,
232
+ indexes: Index[]
233
+ ): Promise<boolean> => {
234
+ console.log(chalk.blue(`Creating/updating ${indexes.length} indexes with status monitoring...`));
235
+
236
+ const failedIndexes: string[] = [];
237
+
238
+ for (const index of indexes) {
239
+ console.log(chalk.blue(`\n--- Processing index: ${index.key} ---`));
240
+
241
+ const success = await createOrUpdateIndexWithStatusCheck(
242
+ dbId,
243
+ db,
244
+ collectionId,
245
+ collection,
246
+ index
247
+ );
248
+
249
+ if (success) {
250
+ console.log(chalk.green(`✅ Successfully created index: ${index.key}`));
251
+
252
+ // Add delay between successful indexes
253
+ await delay(1000);
254
+ } else {
255
+ console.log(chalk.red(`❌ Failed to create index: ${index.key}`));
256
+ failedIndexes.push(index.key);
257
+ }
258
+ }
259
+
260
+ if (failedIndexes.length > 0) {
261
+ console.log(chalk.red(`\n❌ Failed to create ${failedIndexes.length} indexes: ${failedIndexes.join(', ')}`));
262
+ return false;
263
+ }
264
+
265
+ console.log(chalk.green(`\n✅ Successfully created all ${indexes.length} indexes`));
266
+ return true;
267
+ };
4
268
 
5
269
  export const createOrUpdateIndex = async (
6
270
  dbId: string,
@@ -23,6 +23,7 @@ import {
23
23
  import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
24
24
  import { MessageFormatter } from "../shared/messageFormatter.js";
25
25
  import { ProgressManager } from "../shared/progressManager.js";
26
+ import chalk from "chalk";
26
27
 
27
28
  export const documentExists = async (
28
29
  db: Databases,
@@ -615,6 +616,123 @@ export const transferDocumentsBetweenDbsLocalToLocal = async (
615
616
  );
616
617
  };
617
618
 
619
+ /**
620
+ * Enhanced document transfer with fault tolerance and exponential backoff
621
+ */
622
+ const transferDocumentWithRetry = async (
623
+ db: Databases,
624
+ dbId: string,
625
+ collectionId: string,
626
+ documentId: string,
627
+ documentData: any,
628
+ permissions: string[],
629
+ maxRetries: number = 3,
630
+ retryCount: number = 0
631
+ ): Promise<boolean> => {
632
+ try {
633
+ await db.createDocument(
634
+ dbId,
635
+ collectionId,
636
+ documentId,
637
+ documentData,
638
+ permissions
639
+ );
640
+ return true;
641
+ } catch (error: any) {
642
+ // Check if document already exists
643
+ if (error.code === 409 || error.message?.includes('already exists')) {
644
+ console.log(chalk.yellow(`Document ${documentId} already exists, skipping...`));
645
+ return true;
646
+ }
647
+
648
+ if (retryCount < maxRetries) {
649
+ // Calculate exponential backoff: 1s, 2s, 4s
650
+ const exponentialDelay = Math.min(1000 * Math.pow(2, retryCount), 8000);
651
+ console.log(chalk.yellow(`Retrying document ${documentId} (attempt ${retryCount + 1}/${maxRetries}, backoff: ${exponentialDelay}ms)`));
652
+
653
+ await delay(exponentialDelay);
654
+
655
+ return await transferDocumentWithRetry(
656
+ db,
657
+ dbId,
658
+ collectionId,
659
+ documentId,
660
+ documentData,
661
+ permissions,
662
+ maxRetries,
663
+ retryCount + 1
664
+ );
665
+ }
666
+
667
+ console.log(chalk.red(`Failed to transfer document ${documentId} after ${maxRetries} retries: ${error.message}`));
668
+ return false;
669
+ }
670
+ };
671
+
672
+ /**
673
+ * Enhanced batch document transfer with fault tolerance
674
+ */
675
+ const transferDocumentBatchWithRetry = async (
676
+ db: Databases,
677
+ dbId: string,
678
+ collectionId: string,
679
+ documents: any[],
680
+ batchSize: number = 10
681
+ ): Promise<{ successful: number; failed: number }> => {
682
+ let successful = 0;
683
+ let failed = 0;
684
+
685
+ // Process documents in smaller batches to avoid overwhelming the server
686
+ const documentBatches = chunk(documents, batchSize);
687
+
688
+ for (const batch of documentBatches) {
689
+ console.log(chalk.blue(`Processing batch of ${batch.length} documents...`));
690
+
691
+ const batchPromises = batch.map(async (doc) => {
692
+ const toCreateObject: Partial<typeof doc> = { ...doc };
693
+ delete toCreateObject.$databaseId;
694
+ delete toCreateObject.$collectionId;
695
+ delete toCreateObject.$createdAt;
696
+ delete toCreateObject.$updatedAt;
697
+ delete toCreateObject.$id;
698
+ delete toCreateObject.$permissions;
699
+
700
+ const result = await transferDocumentWithRetry(
701
+ db,
702
+ dbId,
703
+ collectionId,
704
+ doc.$id,
705
+ toCreateObject,
706
+ doc.$permissions || []
707
+ );
708
+
709
+ return { docId: doc.$id, success: result };
710
+ });
711
+
712
+ const results = await Promise.allSettled(batchPromises);
713
+
714
+ results.forEach((result, index) => {
715
+ if (result.status === 'fulfilled') {
716
+ if (result.value.success) {
717
+ successful++;
718
+ } else {
719
+ failed++;
720
+ }
721
+ } else {
722
+ console.log(chalk.red(`Batch promise rejected for document ${batch[index].$id}: ${result.reason}`));
723
+ failed++;
724
+ }
725
+ });
726
+
727
+ // Add delay between batches to avoid rate limiting
728
+ if (documentBatches.indexOf(batch) < documentBatches.length - 1) {
729
+ await delay(500);
730
+ }
731
+ }
732
+
733
+ return { successful, failed };
734
+ };
735
+
618
736
  export const transferDocumentsBetweenDbsLocalToRemote = async (
619
737
  localDb: Databases,
620
738
  endpoint: string,
@@ -625,100 +743,70 @@ export const transferDocumentsBetweenDbsLocalToRemote = async (
625
743
  fromCollId: string,
626
744
  toCollId: string
627
745
  ) => {
746
+ console.log(chalk.blue(`Starting enhanced document transfer from ${fromCollId} to ${toCollId}...`));
747
+
628
748
  const client = new Client()
629
749
  .setEndpoint(endpoint)
630
750
  .setProject(projectId)
631
751
  .setKey(apiKey);
632
- let totalDocumentsTransferred = 0;
752
+
633
753
  const remoteDb = new Databases(client);
634
- let fromCollDocs = await tryAwaitWithRetry(async () =>
635
- localDb.listDocuments(fromDbId, fromCollId, [Query.limit(50)])
636
- );
637
-
638
- if (fromCollDocs.documents.length === 0) {
754
+ let totalDocumentsProcessed = 0;
755
+ let totalSuccessful = 0;
756
+ let totalFailed = 0;
757
+
758
+ // Fetch documents in batches
759
+ let hasMoreDocuments = true;
760
+ let lastDocumentId: string | undefined;
761
+
762
+ while (hasMoreDocuments) {
763
+ const queries = [Query.limit(50)];
764
+ if (lastDocumentId) {
765
+ queries.push(Query.cursorAfter(lastDocumentId));
766
+ }
767
+
768
+ const fromCollDocs = await tryAwaitWithRetry(async () =>
769
+ localDb.listDocuments(fromDbId, fromCollId, queries)
770
+ );
771
+
772
+ if (fromCollDocs.documents.length === 0) {
773
+ hasMoreDocuments = false;
774
+ break;
775
+ }
776
+
777
+ console.log(chalk.blue(`Processing ${fromCollDocs.documents.length} documents...`));
778
+
779
+ const { successful, failed } = await transferDocumentBatchWithRetry(
780
+ remoteDb,
781
+ toDbId,
782
+ toCollId,
783
+ fromCollDocs.documents
784
+ );
785
+
786
+ totalDocumentsProcessed += fromCollDocs.documents.length;
787
+ totalSuccessful += successful;
788
+ totalFailed += failed;
789
+
790
+ // Check if we have more documents to process
791
+ if (fromCollDocs.documents.length < 50) {
792
+ hasMoreDocuments = false;
793
+ } else {
794
+ lastDocumentId = fromCollDocs.documents[fromCollDocs.documents.length - 1].$id;
795
+ }
796
+
797
+ console.log(chalk.gray(`Batch complete: ${successful} successful, ${failed} failed`));
798
+ }
799
+
800
+ if (totalDocumentsProcessed === 0) {
639
801
  MessageFormatter.info(`No documents found in collection ${fromCollId}`, { prefix: "Transfer" });
640
802
  return;
641
- } else if (fromCollDocs.documents.length < 50) {
642
- const batchedPromises = fromCollDocs.documents.map((doc) => {
643
- const toCreateObject: Partial<typeof doc> = {
644
- ...doc,
645
- };
646
- delete toCreateObject.$databaseId;
647
- delete toCreateObject.$collectionId;
648
- delete toCreateObject.$createdAt;
649
- delete toCreateObject.$updatedAt;
650
- delete toCreateObject.$id;
651
- delete toCreateObject.$permissions;
652
- return tryAwaitWithRetry(async () =>
653
- remoteDb.createDocument(
654
- toDbId,
655
- toCollId,
656
- doc.$id,
657
- toCreateObject,
658
- doc.$permissions
659
- )
660
- );
661
- });
662
- await Promise.all(batchedPromises);
663
- totalDocumentsTransferred += fromCollDocs.documents.length;
803
+ }
804
+
805
+ const message = `Total documents processed: ${totalDocumentsProcessed}, successful: ${totalSuccessful}, failed: ${totalFailed}`;
806
+
807
+ if (totalFailed > 0) {
808
+ MessageFormatter.warning(message, { prefix: "Transfer" });
664
809
  } else {
665
- const batchedPromises = fromCollDocs.documents.map((doc) => {
666
- const toCreateObject: Partial<typeof doc> = {
667
- ...doc,
668
- };
669
- delete toCreateObject.$databaseId;
670
- delete toCreateObject.$collectionId;
671
- delete toCreateObject.$createdAt;
672
- delete toCreateObject.$updatedAt;
673
- delete toCreateObject.$id;
674
- delete toCreateObject.$permissions;
675
- return tryAwaitWithRetry(async () =>
676
- remoteDb.createDocument(
677
- toDbId,
678
- toCollId,
679
- doc.$id,
680
- toCreateObject,
681
- doc.$permissions
682
- )
683
- );
684
- });
685
- await Promise.all(batchedPromises);
686
- totalDocumentsTransferred += fromCollDocs.documents.length;
687
- while (fromCollDocs.documents.length === 50) {
688
- fromCollDocs = await tryAwaitWithRetry(async () =>
689
- localDb.listDocuments(fromDbId, fromCollId, [
690
- Query.limit(50),
691
- Query.cursorAfter(
692
- fromCollDocs.documents[fromCollDocs.documents.length - 1].$id
693
- ),
694
- ])
695
- );
696
- const batchedPromises = fromCollDocs.documents.map((doc) => {
697
- const toCreateObject: Partial<typeof doc> = {
698
- ...doc,
699
- };
700
- delete toCreateObject.$databaseId;
701
- delete toCreateObject.$collectionId;
702
- delete toCreateObject.$createdAt;
703
- delete toCreateObject.$updatedAt;
704
- delete toCreateObject.$id;
705
- delete toCreateObject.$permissions;
706
- return tryAwaitWithRetry(async () =>
707
- remoteDb.createDocument(
708
- toDbId,
709
- toCollId,
710
- doc.$id,
711
- toCreateObject,
712
- doc.$permissions
713
- )
714
- );
715
- });
716
- await Promise.all(batchedPromises);
717
- totalDocumentsTransferred += fromCollDocs.documents.length;
718
- }
810
+ MessageFormatter.success(message, { prefix: "Transfer" });
719
811
  }
720
- MessageFormatter.success(
721
- `Total documents transferred from database ${fromDbId} to database ${toDbId} -- collection ${fromCollId} to collection ${toCollId}: ${totalDocumentsTransferred}`,
722
- { prefix: "Transfer" }
723
- );
724
812
  };