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.
@@ -1516,48 +1516,140 @@ export class InteractiveCLI {
1516
1516
  async comprehensiveTransfer() {
1517
1517
  MessageFormatter.info("Starting comprehensive transfer configuration...", { prefix: "Transfer" });
1518
1518
  try {
1519
- // Get source configuration
1520
- const sourceConfig = await inquirer.prompt([
1521
- {
1522
- type: "input",
1523
- name: "sourceEndpoint",
1524
- message: "Enter the source Appwrite endpoint:",
1525
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1526
- },
1527
- {
1528
- type: "input",
1529
- name: "sourceProject",
1530
- message: "Enter the source project ID:",
1531
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1532
- },
1533
- {
1534
- type: "password",
1535
- name: "sourceKey",
1536
- message: "Enter the source API key:",
1537
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
1538
- },
1539
- ]);
1540
- // Get target configuration
1541
- const targetConfig = await inquirer.prompt([
1542
- {
1543
- type: "input",
1544
- name: "targetEndpoint",
1545
- message: "Enter the target Appwrite endpoint:",
1546
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1547
- },
1548
- {
1549
- type: "input",
1550
- name: "targetProject",
1551
- message: "Enter the target project ID:",
1552
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1553
- },
1554
- {
1555
- type: "password",
1556
- name: "targetKey",
1557
- message: "Enter the target API key:",
1558
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
1559
- },
1560
- ]);
1519
+ // Check if user has an appwrite config for easier setup
1520
+ const hasAppwriteConfig = this.controller?.config?.appwriteEndpoint &&
1521
+ this.controller?.config?.appwriteProject &&
1522
+ this.controller?.config?.appwriteKey;
1523
+ let sourceConfig;
1524
+ let targetConfig;
1525
+ if (hasAppwriteConfig) {
1526
+ // Offer to use existing config for source
1527
+ const { useConfigForSource } = await inquirer.prompt([
1528
+ {
1529
+ type: "confirm",
1530
+ name: "useConfigForSource",
1531
+ message: "Use your current appwriteConfig as the source?",
1532
+ default: true,
1533
+ },
1534
+ ]);
1535
+ if (useConfigForSource) {
1536
+ sourceConfig = {
1537
+ sourceEndpoint: this.controller.config.appwriteEndpoint,
1538
+ sourceProject: this.controller.config.appwriteProject,
1539
+ sourceKey: this.controller.config.appwriteKey,
1540
+ };
1541
+ MessageFormatter.info(`Using config source: ${sourceConfig.sourceEndpoint}`, { prefix: "Transfer" });
1542
+ }
1543
+ else {
1544
+ // Get source configuration manually
1545
+ sourceConfig = await inquirer.prompt([
1546
+ {
1547
+ type: "input",
1548
+ name: "sourceEndpoint",
1549
+ message: "Enter the source Appwrite endpoint:",
1550
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1551
+ },
1552
+ {
1553
+ type: "input",
1554
+ name: "sourceProject",
1555
+ message: "Enter the source project ID:",
1556
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1557
+ },
1558
+ {
1559
+ type: "password",
1560
+ name: "sourceKey",
1561
+ message: "Enter the source API key:",
1562
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
1563
+ },
1564
+ ]);
1565
+ }
1566
+ // Offer to use existing config for target
1567
+ const { useConfigForTarget } = await inquirer.prompt([
1568
+ {
1569
+ type: "confirm",
1570
+ name: "useConfigForTarget",
1571
+ message: "Use your current appwriteConfig as the target?",
1572
+ default: false,
1573
+ },
1574
+ ]);
1575
+ if (useConfigForTarget) {
1576
+ targetConfig = {
1577
+ targetEndpoint: this.controller.config.appwriteEndpoint,
1578
+ targetProject: this.controller.config.appwriteProject,
1579
+ targetKey: this.controller.config.appwriteKey,
1580
+ };
1581
+ MessageFormatter.info(`Using config target: ${targetConfig.targetEndpoint}`, { prefix: "Transfer" });
1582
+ }
1583
+ else {
1584
+ // Get target configuration manually
1585
+ targetConfig = await inquirer.prompt([
1586
+ {
1587
+ type: "input",
1588
+ name: "targetEndpoint",
1589
+ message: "Enter the target Appwrite endpoint:",
1590
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1591
+ },
1592
+ {
1593
+ type: "input",
1594
+ name: "targetProject",
1595
+ message: "Enter the target project ID:",
1596
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1597
+ },
1598
+ {
1599
+ type: "password",
1600
+ name: "targetKey",
1601
+ message: "Enter the target API key:",
1602
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
1603
+ },
1604
+ ]);
1605
+ }
1606
+ }
1607
+ else {
1608
+ // No appwrite config found, get both configurations manually
1609
+ MessageFormatter.info("No appwriteConfig found, please enter source and target configurations manually", { prefix: "Transfer" });
1610
+ // Get source configuration
1611
+ sourceConfig = await inquirer.prompt([
1612
+ {
1613
+ type: "input",
1614
+ name: "sourceEndpoint",
1615
+ message: "Enter the source Appwrite endpoint:",
1616
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1617
+ },
1618
+ {
1619
+ type: "input",
1620
+ name: "sourceProject",
1621
+ message: "Enter the source project ID:",
1622
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1623
+ },
1624
+ {
1625
+ type: "password",
1626
+ name: "sourceKey",
1627
+ message: "Enter the source API key:",
1628
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
1629
+ },
1630
+ ]);
1631
+ // Get target configuration
1632
+ targetConfig = await inquirer.prompt([
1633
+ {
1634
+ type: "input",
1635
+ name: "targetEndpoint",
1636
+ message: "Enter the target Appwrite endpoint:",
1637
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1638
+ },
1639
+ {
1640
+ type: "input",
1641
+ name: "targetProject",
1642
+ message: "Enter the target project ID:",
1643
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1644
+ },
1645
+ {
1646
+ type: "password",
1647
+ name: "targetKey",
1648
+ message: "Enter the target API key:",
1649
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
1650
+ },
1651
+ ]);
1652
+ }
1561
1653
  // Get transfer options
1562
1654
  const transferOptions = await inquirer.prompt([
1563
1655
  {
@@ -2,13 +2,13 @@ import { converterFunctions, tryAwaitWithRetry } from "appwrite-utils";
2
2
  import { Client, Databases, IndexType, Query, Storage, Users, } from "node-appwrite";
3
3
  import { InputFile } from "node-appwrite/file";
4
4
  import { getAppwriteClient } from "../utils/helperFunctions.js";
5
- import { createOrUpdateAttribute, createUpdateCollectionAttributes, } from "../collections/attributes.js";
5
+ import { createOrUpdateAttribute, createUpdateCollectionAttributes, createUpdateCollectionAttributesWithStatusCheck, } from "../collections/attributes.js";
6
6
  import { parseAttribute } from "appwrite-utils";
7
7
  import chalk from "chalk";
8
8
  import { fetchAllCollections } from "../collections/methods.js";
9
9
  import { MessageFormatter } from "../shared/messageFormatter.js";
10
10
  import { ProgressManager } from "../shared/progressManager.js";
11
- import { createOrUpdateIndex, createOrUpdateIndexes, } from "../collections/indexes.js";
11
+ import { createOrUpdateIndex, createOrUpdateIndexes, createOrUpdateIndexesWithStatusCheck, } from "../collections/indexes.js";
12
12
  import { getClient } from "../utils/getClientFromConfig.js";
13
13
  export const transferStorageLocalToLocal = async (storage, fromBucketId, toBucketId) => {
14
14
  MessageFormatter.info(`Transferring files from ${fromBucketId} to ${toBucketId}`, { prefix: "Transfer" });
@@ -161,20 +161,15 @@ export const transferDatabaseLocalToLocal = async (localDb, fromDbId, targetDbId
161
161
  console.log(chalk.yellow(`Creating collection ${collection.name} in target database...`));
162
162
  targetCollection = await tryAwaitWithRetry(async () => localDb.createCollection(targetDbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled));
163
163
  }
164
- // Handle attributes
165
- const existingAttributes = await tryAwaitWithRetry(async () => await localDb.listAttributes(targetDbId, targetCollection.$id));
166
- for (const attribute of collection.attributes) {
167
- const parsedAttribute = parseAttribute(attribute);
168
- const existingAttribute = existingAttributes.attributes.find((attr) => attr.key === parsedAttribute.key);
169
- if (!existingAttribute) {
170
- await tryAwaitWithRetry(async () => createOrUpdateAttribute(localDb, targetDbId, targetCollection, parsedAttribute));
171
- console.log(chalk.green(`Attribute ${parsedAttribute.key} created`));
172
- }
173
- else {
174
- console.log(chalk.blue(`Attribute ${parsedAttribute.key} exists, checking for updates...`));
175
- await tryAwaitWithRetry(async () => createOrUpdateAttribute(localDb, targetDbId, targetCollection, parsedAttribute));
176
- }
164
+ // Handle attributes with enhanced status checking
165
+ console.log(chalk.blue(`Creating attributes for collection ${collection.name} with enhanced monitoring...`));
166
+ const allAttributes = collection.attributes.map(attr => parseAttribute(attr));
167
+ const attributeSuccess = await createUpdateCollectionAttributesWithStatusCheck(localDb, targetDbId, targetCollection, allAttributes);
168
+ if (!attributeSuccess) {
169
+ console.log(chalk.red(`❌ Failed to create all attributes for collection ${collection.name}, skipping to next collection`));
170
+ continue;
177
171
  }
172
+ console.log(chalk.green(`✅ All attributes created successfully for collection ${collection.name}`));
178
173
  // Handle indexes
179
174
  const existingIndexes = await tryAwaitWithRetry(async () => await localDb.listIndexes(targetDbId, targetCollection.$id));
180
175
  for (const index of collection.indexes) {
@@ -226,32 +221,26 @@ export const transferDatabaseLocalToRemote = async (localDb, endpoint, projectId
226
221
  console.log(chalk.yellow(`Creating collection ${collection.name} in remote database...`));
227
222
  targetCollection = await tryAwaitWithRetry(async () => remoteDb.createCollection(toDbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled));
228
223
  }
229
- // Handle attributes
230
- const existingAttributes = await tryAwaitWithRetry(async () => await remoteDb.listAttributes(toDbId, targetCollection.$id));
231
- for (const attribute of collection.attributes) {
232
- const parsedAttribute = parseAttribute(attribute);
233
- const existingAttribute = existingAttributes.attributes.find((attr) => attr.key === parsedAttribute.key);
234
- if (!existingAttribute) {
235
- await tryAwaitWithRetry(async () => createOrUpdateAttribute(remoteDb, toDbId, targetCollection, parsedAttribute));
236
- console.log(chalk.green(`Attribute ${parsedAttribute.key} created`));
237
- }
238
- else {
239
- console.log(chalk.blue(`Attribute ${parsedAttribute.key} exists, checking for updates...`));
240
- await tryAwaitWithRetry(async () => createOrUpdateAttribute(remoteDb, toDbId, targetCollection, parsedAttribute));
241
- }
224
+ // Handle attributes with enhanced status checking
225
+ console.log(chalk.blue(`Creating attributes for collection ${collection.name} with enhanced monitoring...`));
226
+ const attributesToCreate = collection.attributes.map(attr => parseAttribute(attr));
227
+ const attributesSuccess = await createUpdateCollectionAttributesWithStatusCheck(remoteDb, toDbId, targetCollection, attributesToCreate);
228
+ if (!attributesSuccess) {
229
+ console.log(chalk.red(`Failed to create some attributes for collection ${collection.name}`));
230
+ // Continue with the transfer even if some attributes failed
242
231
  }
243
- // Handle indexes
244
- const existingIndexes = await tryAwaitWithRetry(async () => await remoteDb.listIndexes(toDbId, targetCollection.$id));
245
- for (const index of collection.indexes) {
246
- const existingIndex = existingIndexes.indexes.find((idx) => idx.key === index.key);
247
- if (!existingIndex) {
248
- await createOrUpdateIndex(toDbId, remoteDb, targetCollection.$id, index);
249
- console.log(chalk.green(`Index ${index.key} created`));
250
- }
251
- else {
252
- console.log(chalk.blue(`Index ${index.key} exists, checking for updates...`));
253
- await createOrUpdateIndex(toDbId, remoteDb, targetCollection.$id, index);
254
- }
232
+ else {
233
+ console.log(chalk.green(`All attributes created successfully for collection ${collection.name}`));
234
+ }
235
+ // Handle indexes with enhanced status checking
236
+ console.log(chalk.blue(`Creating indexes for collection ${collection.name} with enhanced monitoring...`));
237
+ const indexesSuccess = await createOrUpdateIndexesWithStatusCheck(toDbId, remoteDb, targetCollection.$id, targetCollection, collection.indexes);
238
+ if (!indexesSuccess) {
239
+ console.log(chalk.red(`Failed to create some indexes for collection ${collection.name}`));
240
+ // Continue with the transfer even if some indexes failed
241
+ }
242
+ else {
243
+ console.log(chalk.green(`All indexes created successfully for collection ${collection.name}`));
255
244
  }
256
245
  // Transfer documents
257
246
  const { transferDocumentsBetweenDbsLocalToRemote } = await import("../collections/methods.js");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appwrite-utils-cli",
3
3
  "description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
4
- "version": "1.0.9",
4
+ "version": "1.1.0",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -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,