appwrite-utils-cli 1.0.8 → 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.
@@ -8,6 +8,165 @@ import { nameToIdMapping, enqueueOperation } from "../shared/operationQueue.js";
8
8
  import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
9
9
  import chalk from "chalk";
10
10
 
11
+ // Interface for attribute with status (fixing the type issue)
12
+ interface AttributeWithStatus {
13
+ key: string;
14
+ type: string;
15
+ status: 'available' | 'processing' | 'deleting' | 'stuck' | 'failed';
16
+ error: string;
17
+ required: boolean;
18
+ array?: boolean;
19
+ $createdAt: string;
20
+ $updatedAt: string;
21
+ [key: string]: any; // For type-specific fields
22
+ }
23
+
24
+ /**
25
+ * Wait for attribute to become available, with retry logic for stuck attributes and exponential backoff
26
+ */
27
+ const waitForAttributeAvailable = async (
28
+ db: Databases,
29
+ dbId: string,
30
+ collectionId: string,
31
+ attributeKey: string,
32
+ maxWaitTime: number = 60000, // 1 minute
33
+ retryCount: number = 0,
34
+ maxRetries: number = 5
35
+ ): Promise<boolean> => {
36
+ const startTime = Date.now();
37
+ let checkInterval = 2000; // Start with 2 seconds
38
+
39
+ // Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
40
+ if (retryCount > 0) {
41
+ const exponentialDelay = Math.min(2000 * Math.pow(2, retryCount), 30000);
42
+ console.log(chalk.blue(`Waiting for attribute '${attributeKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`));
43
+ await delay(exponentialDelay);
44
+ } else {
45
+ console.log(chalk.blue(`Waiting for attribute '${attributeKey}' to become available...`));
46
+ }
47
+
48
+ while (Date.now() - startTime < maxWaitTime) {
49
+ try {
50
+ const collection = await db.getCollection(dbId, collectionId);
51
+ const attribute = (collection.attributes as any[]).find(
52
+ (attr: AttributeWithStatus) => attr.key === attributeKey
53
+ ) as AttributeWithStatus | undefined;
54
+
55
+ if (!attribute) {
56
+ console.log(chalk.red(`Attribute '${attributeKey}' not found`));
57
+ return false;
58
+ }
59
+
60
+ console.log(chalk.gray(`Attribute '${attributeKey}' status: ${attribute.status}`));
61
+
62
+ switch (attribute.status) {
63
+ case 'available':
64
+ console.log(chalk.green(`✅ Attribute '${attributeKey}' is now available`));
65
+ return true;
66
+
67
+ case 'failed':
68
+ console.log(chalk.red(`❌ Attribute '${attributeKey}' failed: ${attribute.error}`));
69
+ return false;
70
+
71
+ case 'stuck':
72
+ console.log(chalk.yellow(`⚠️ Attribute '${attributeKey}' is stuck, will retry...`));
73
+ return false;
74
+
75
+ case 'processing':
76
+ // Continue waiting
77
+ break;
78
+
79
+ case 'deleting':
80
+ console.log(chalk.yellow(`Attribute '${attributeKey}' is being deleted`));
81
+ break;
82
+
83
+ default:
84
+ console.log(chalk.yellow(`Unknown status '${attribute.status}' for attribute '${attributeKey}'`));
85
+ break;
86
+ }
87
+
88
+ await delay(checkInterval);
89
+ } catch (error) {
90
+ console.log(chalk.red(`Error checking attribute status: ${error}`));
91
+ return false;
92
+ }
93
+ }
94
+
95
+ // Timeout reached
96
+ console.log(chalk.yellow(`⏰ Timeout waiting for attribute '${attributeKey}' (${maxWaitTime}ms)`));
97
+
98
+ // If we have retries left and this isn't the last retry, try recreating
99
+ if (retryCount < maxRetries) {
100
+ console.log(chalk.yellow(`🔄 Retrying attribute creation (attempt ${retryCount + 1}/${maxRetries})`));
101
+ return false; // Signal that we need to retry
102
+ }
103
+
104
+ return false;
105
+ };
106
+
107
+ /**
108
+ * Wait for all attributes in a collection to become available
109
+ */
110
+ const waitForAllAttributesAvailable = async (
111
+ db: Databases,
112
+ dbId: string,
113
+ collectionId: string,
114
+ attributeKeys: string[],
115
+ maxWaitTime: number = 60000
116
+ ): Promise<string[]> => {
117
+ console.log(chalk.blue(`Waiting for ${attributeKeys.length} attributes to become available...`));
118
+
119
+ const failedAttributes: string[] = [];
120
+
121
+ for (const attributeKey of attributeKeys) {
122
+ const success = await waitForAttributeAvailable(db, dbId, collectionId, attributeKey, maxWaitTime);
123
+ if (!success) {
124
+ failedAttributes.push(attributeKey);
125
+ }
126
+ }
127
+
128
+ return failedAttributes;
129
+ };
130
+
131
+ /**
132
+ * Delete collection and recreate with retry logic
133
+ */
134
+ const deleteAndRecreateCollection = async (
135
+ db: Databases,
136
+ dbId: string,
137
+ collection: Models.Collection,
138
+ retryCount: number
139
+ ): Promise<Models.Collection | null> => {
140
+ try {
141
+ console.log(chalk.yellow(`🗑️ Deleting collection '${collection.name}' for retry ${retryCount}`));
142
+
143
+ // Delete the collection
144
+ await db.deleteCollection(dbId, collection.$id);
145
+ console.log(chalk.yellow(`Deleted collection '${collection.name}'`));
146
+
147
+ // Wait a bit before recreating
148
+ await delay(2000);
149
+
150
+ // Recreate the collection
151
+ console.log(chalk.blue(`🔄 Recreating collection '${collection.name}'`));
152
+ const newCollection = await db.createCollection(
153
+ dbId,
154
+ collection.$id,
155
+ collection.name,
156
+ collection.$permissions,
157
+ collection.documentSecurity,
158
+ collection.enabled
159
+ );
160
+
161
+ console.log(chalk.green(`✅ Recreated collection '${collection.name}'`));
162
+ return newCollection;
163
+
164
+ } catch (error) {
165
+ console.log(chalk.red(`Failed to delete/recreate collection '${collection.name}': ${error}`));
166
+ return null;
167
+ }
168
+ };
169
+
11
170
  const attributesSame = (
12
171
  databaseAttribute: Attribute,
13
172
  configAttribute: Attribute
@@ -72,6 +231,87 @@ const attributesSame = (
72
231
  });
73
232
  };
74
233
 
234
+ /**
235
+ * Enhanced attribute creation with proper status monitoring and retry logic
236
+ */
237
+ export const createOrUpdateAttributeWithStatusCheck = async (
238
+ db: Databases,
239
+ dbId: string,
240
+ collection: Models.Collection,
241
+ attribute: Attribute,
242
+ retryCount: number = 0,
243
+ maxRetries: number = 5
244
+ ): Promise<boolean> => {
245
+ console.log(chalk.blue(`Creating/updating attribute '${attribute.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`));
246
+
247
+ try {
248
+ // First, try to create/update the attribute using existing logic
249
+ await createOrUpdateAttribute(db, dbId, collection, attribute);
250
+
251
+ // Now wait for the attribute to become available
252
+ const success = await waitForAttributeAvailable(
253
+ db,
254
+ dbId,
255
+ collection.$id,
256
+ attribute.key,
257
+ 60000, // 1 minute timeout
258
+ retryCount,
259
+ maxRetries
260
+ );
261
+
262
+ if (success) {
263
+ return true;
264
+ }
265
+
266
+ // If not successful and we have retries left, delete collection and try again
267
+ if (retryCount < maxRetries) {
268
+ console.log(chalk.yellow(`Attribute '${attribute.key}' failed/stuck, retrying...`));
269
+
270
+ // Get fresh collection data
271
+ const freshCollection = await db.getCollection(dbId, collection.$id);
272
+
273
+ // Delete and recreate collection
274
+ const newCollection = await deleteAndRecreateCollection(db, dbId, freshCollection, retryCount + 1);
275
+
276
+ if (newCollection) {
277
+ // Retry with the new collection
278
+ return await createOrUpdateAttributeWithStatusCheck(
279
+ db,
280
+ dbId,
281
+ newCollection,
282
+ attribute,
283
+ retryCount + 1,
284
+ maxRetries
285
+ );
286
+ }
287
+ }
288
+
289
+ console.log(chalk.red(`❌ Failed to create attribute '${attribute.key}' after ${maxRetries + 1} attempts`));
290
+ return false;
291
+
292
+ } catch (error) {
293
+ console.log(chalk.red(`Error creating attribute '${attribute.key}': ${error}`));
294
+
295
+ if (retryCount < maxRetries) {
296
+ console.log(chalk.yellow(`Retrying attribute '${attribute.key}' due to error...`));
297
+
298
+ // Wait a bit before retry
299
+ await delay(2000);
300
+
301
+ return await createOrUpdateAttributeWithStatusCheck(
302
+ db,
303
+ dbId,
304
+ collection,
305
+ attribute,
306
+ retryCount + 1,
307
+ maxRetries
308
+ );
309
+ }
310
+
311
+ return false;
312
+ }
313
+ };
314
+
75
315
  export const createOrUpdateAttribute = async (
76
316
  db: Databases,
77
317
  dbId: string,
@@ -514,6 +754,105 @@ export const createOrUpdateAttribute = async (
514
754
  }
515
755
  };
516
756
 
757
+ /**
758
+ * Enhanced collection attribute creation with proper status monitoring
759
+ */
760
+ export const createUpdateCollectionAttributesWithStatusCheck = async (
761
+ db: Databases,
762
+ dbId: string,
763
+ collection: Models.Collection,
764
+ attributes: Attribute[]
765
+ ): Promise<boolean> => {
766
+ console.log(
767
+ chalk.green(
768
+ `Creating/Updating attributes for collection: ${collection.name} with status monitoring`
769
+ )
770
+ );
771
+
772
+ const existingAttributes: Attribute[] =
773
+ // @ts-expect-error
774
+ collection.attributes.map((attr) => parseAttribute(attr)) || [];
775
+
776
+ const attributesToRemove = existingAttributes.filter(
777
+ (attr) => !attributes.some((a) => a.key === attr.key)
778
+ );
779
+ const indexesToRemove = collection.indexes.filter((index) =>
780
+ attributesToRemove.some((attr) => index.attributes.includes(attr.key))
781
+ );
782
+
783
+ // Handle attribute removal first
784
+ if (attributesToRemove.length > 0) {
785
+ if (indexesToRemove.length > 0) {
786
+ console.log(
787
+ chalk.red(
788
+ `Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove
789
+ .map((index) => index.key)
790
+ .join(", ")}`
791
+ )
792
+ );
793
+ for (const index of indexesToRemove) {
794
+ await tryAwaitWithRetry(
795
+ async () => await db.deleteIndex(dbId, collection.$id, index.key)
796
+ );
797
+ await delay(500); // Longer delay for deletions
798
+ }
799
+ }
800
+ for (const attr of attributesToRemove) {
801
+ console.log(
802
+ chalk.red(
803
+ `Removing attribute: ${attr.key} as it is no longer in the collection`
804
+ )
805
+ );
806
+ await tryAwaitWithRetry(
807
+ async () => await db.deleteAttribute(dbId, collection.$id, attr.key)
808
+ );
809
+ await delay(500); // Longer delay for deletions
810
+ }
811
+ }
812
+
813
+ // Create attributes ONE BY ONE with proper status checking
814
+ console.log(chalk.blue(`Creating ${attributes.length} attributes sequentially with status monitoring...`));
815
+
816
+ let currentCollection = collection;
817
+ const failedAttributes: string[] = [];
818
+
819
+ for (const attribute of attributes) {
820
+ console.log(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`));
821
+
822
+ const success = await createOrUpdateAttributeWithStatusCheck(
823
+ db,
824
+ dbId,
825
+ currentCollection,
826
+ attribute
827
+ );
828
+
829
+ if (success) {
830
+ console.log(chalk.green(`✅ Successfully created attribute: ${attribute.key}`));
831
+
832
+ // Get updated collection data for next iteration
833
+ try {
834
+ currentCollection = await db.getCollection(dbId, collection.$id);
835
+ } catch (error) {
836
+ console.log(chalk.yellow(`Warning: Could not refresh collection data: ${error}`));
837
+ }
838
+
839
+ // Add delay between successful attributes
840
+ await delay(1000);
841
+ } else {
842
+ console.log(chalk.red(`❌ Failed to create attribute: ${attribute.key}`));
843
+ failedAttributes.push(attribute.key);
844
+ }
845
+ }
846
+
847
+ if (failedAttributes.length > 0) {
848
+ console.log(chalk.red(`\n❌ Failed to create ${failedAttributes.length} attributes: ${failedAttributes.join(', ')}`));
849
+ return false;
850
+ }
851
+
852
+ console.log(chalk.green(`\n✅ Successfully created all ${attributes.length} attributes for collection: ${collection.name}`));
853
+ return true;
854
+ };
855
+
517
856
  export const createUpdateCollectionAttributes = async (
518
857
  db: Databases,
519
858
  dbId: string,
@@ -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,