appwrite-utils-cli 1.1.2 → 1.1.4

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.
@@ -459,17 +459,53 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
459
459
  await delay(500); // Longer delay for deletions
460
460
  }
461
461
  }
462
- // Create attributes ONE BY ONE with proper status checking and persistent retry logic
463
- console.log(chalk.blue(`Creating ${attributes.length} attributes sequentially with status monitoring...`));
462
+ // First, get fresh collection data and determine which attributes actually need processing
463
+ console.log(chalk.blue(`Analyzing ${attributes.length} attributes to determine which need processing...`));
464
464
  let currentCollection = collection;
465
- let attributesToProcess = [...attributes];
465
+ try {
466
+ currentCollection = await db.getCollection(dbId, collection.$id);
467
+ }
468
+ catch (error) {
469
+ console.log(chalk.yellow(`Warning: Could not refresh collection data: ${error}`));
470
+ }
471
+ const existingAttributesMap = new Map();
472
+ try {
473
+ // @ts-expect-error
474
+ const parsedAttributes = currentCollection.attributes.map((attr) => parseAttribute(attr));
475
+ parsedAttributes.forEach(attr => existingAttributesMap.set(attr.key, attr));
476
+ }
477
+ catch (error) {
478
+ console.log(chalk.yellow(`Warning: Could not parse existing attributes: ${error}`));
479
+ }
480
+ // Filter to only attributes that need processing (new or changed)
481
+ const attributesToProcess = attributes.filter(attribute => {
482
+ const existing = existingAttributesMap.get(attribute.key);
483
+ if (!existing) {
484
+ console.log(chalk.blue(`āž• New attribute: ${attribute.key}`));
485
+ return true;
486
+ }
487
+ const needsUpdate = !attributesSame(existing, attribute);
488
+ if (needsUpdate) {
489
+ console.log(chalk.blue(`šŸ”„ Changed attribute: ${attribute.key}`));
490
+ }
491
+ else {
492
+ console.log(chalk.gray(`āœ… Unchanged attribute: ${attribute.key} (skipping)`));
493
+ }
494
+ return needsUpdate;
495
+ });
496
+ if (attributesToProcess.length === 0) {
497
+ console.log(chalk.green(`āœ… All ${attributes.length} attributes are already up to date for collection: ${collection.name}`));
498
+ return true;
499
+ }
500
+ console.log(chalk.blue(`Creating ${attributesToProcess.length} attributes sequentially with status monitoring...`));
501
+ let remainingAttributes = [...attributesToProcess];
466
502
  let overallRetryCount = 0;
467
503
  const maxOverallRetries = 3;
468
- while (attributesToProcess.length > 0 && overallRetryCount < maxOverallRetries) {
469
- const remainingAttributes = [...attributesToProcess];
470
- attributesToProcess = []; // Reset for next iteration
471
- console.log(chalk.blue(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${remainingAttributes.length} attributes ===`));
472
- for (const attribute of remainingAttributes) {
504
+ while (remainingAttributes.length > 0 && overallRetryCount < maxOverallRetries) {
505
+ const attributesToProcessThisRound = [...remainingAttributes];
506
+ remainingAttributes = []; // Reset for next iteration
507
+ console.log(chalk.blue(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${attributesToProcessThisRound.length} attributes ===`));
508
+ for (const attribute of attributesToProcessThisRound) {
473
509
  console.log(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`));
474
510
  const success = await createOrUpdateAttributeWithStatusCheck(db, dbId, currentCollection, attribute);
475
511
  if (success) {
@@ -486,11 +522,11 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
486
522
  }
487
523
  else {
488
524
  console.log(chalk.red(`āŒ Failed to create attribute: ${attribute.key}, will retry in next round`));
489
- attributesToProcess.push(attribute); // Add back to retry list
525
+ remainingAttributes.push(attribute); // Add back to retry list
490
526
  }
491
527
  }
492
- if (attributesToProcess.length === 0) {
493
- console.log(chalk.green(`\nāœ… Successfully created all ${attributes.length} attributes for collection: ${collection.name}`));
528
+ if (remainingAttributes.length === 0) {
529
+ console.log(chalk.green(`\nāœ… Successfully created all ${attributesToProcess.length} attributes for collection: ${collection.name}`));
494
530
  return true;
495
531
  }
496
532
  overallRetryCount++;
@@ -63,35 +63,22 @@ retryCount = 0, maxRetries = 5) => {
63
63
  }
64
64
  return false;
65
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
66
  /**
89
67
  * Enhanced index creation with proper status monitoring and retry logic
90
68
  */
91
69
  export const createOrUpdateIndexWithStatusCheck = async (dbId, db, collectionId, collection, index, retryCount = 0, maxRetries = 5) => {
92
70
  console.log(chalk.blue(`Creating/updating index '${index.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`));
93
71
  try {
94
- // First, try to create/update the index using existing logic
72
+ // First, validate that all required attributes exist
73
+ const freshCollection = await db.getCollection(dbId, collectionId);
74
+ const existingAttributeKeys = freshCollection.attributes.map((attr) => attr.key);
75
+ const missingAttributes = index.attributes.filter(attr => !existingAttributeKeys.includes(attr));
76
+ if (missingAttributes.length > 0) {
77
+ console.log(chalk.red(`āŒ Index '${index.key}' cannot be created: missing attributes [${missingAttributes.join(', ')}]`));
78
+ console.log(chalk.red(`Available attributes: [${existingAttributeKeys.join(', ')}]`));
79
+ return false; // Don't retry if attributes are missing
80
+ }
81
+ // Try to create/update the index using existing logic
95
82
  await createOrUpdateIndex(dbId, db, collectionId, index);
96
83
  // Now wait for the index to become available
97
84
  const success = await waitForIndexAvailable(db, dbId, collectionId, index.key, 60000, // 1 minute timeout
@@ -99,23 +86,28 @@ export const createOrUpdateIndexWithStatusCheck = async (dbId, db, collectionId,
99
86
  if (success) {
100
87
  return true;
101
88
  }
102
- // If not successful and we have retries left, delete collection and try again
89
+ // If not successful and we have retries left, just retry the index creation
103
90
  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
- }
91
+ console.log(chalk.yellow(`Index '${index.key}' failed/stuck, retrying (${retryCount + 1}/${maxRetries})...`));
92
+ // Wait a bit before retry
93
+ await new Promise(resolve => setTimeout(resolve, 2000 * (retryCount + 1)));
94
+ // Retry the index creation
95
+ return await createOrUpdateIndexWithStatusCheck(dbId, db, collectionId, collection, index, retryCount + 1, maxRetries);
113
96
  }
114
97
  console.log(chalk.red(`āŒ Failed to create index '${index.key}' after ${maxRetries + 1} attempts`));
115
98
  return false;
116
99
  }
117
100
  catch (error) {
118
- console.log(chalk.red(`Error creating index '${index.key}': ${error}`));
101
+ const errorMessage = error instanceof Error ? error.message : String(error);
102
+ console.log(chalk.red(`Error creating index '${index.key}': ${errorMessage}`));
103
+ // Check if this is a permanent error that shouldn't be retried
104
+ if (errorMessage.includes('not found') ||
105
+ errorMessage.includes('missing') ||
106
+ errorMessage.includes('does not exist') ||
107
+ errorMessage.includes('attribute') && errorMessage.includes('not found')) {
108
+ console.log(chalk.red(`āŒ Index '${index.key}' has permanent error - not retrying`));
109
+ return false;
110
+ }
119
111
  if (retryCount < maxRetries) {
120
112
  console.log(chalk.yellow(`Retrying index '${index.key}' due to error...`));
121
113
  // Wait a bit before retry
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.1.2",
4
+ "version": "1.1.4",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -849,21 +849,60 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
849
849
  }
850
850
  }
851
851
 
852
- // Create attributes ONE BY ONE with proper status checking and persistent retry logic
853
- console.log(chalk.blue(`Creating ${attributes.length} attributes sequentially with status monitoring...`));
852
+ // First, get fresh collection data and determine which attributes actually need processing
853
+ console.log(chalk.blue(`Analyzing ${attributes.length} attributes to determine which need processing...`));
854
854
 
855
855
  let currentCollection = collection;
856
- let attributesToProcess = [...attributes];
856
+ try {
857
+ currentCollection = await db.getCollection(dbId, collection.$id);
858
+ } catch (error) {
859
+ console.log(chalk.yellow(`Warning: Could not refresh collection data: ${error}`));
860
+ }
861
+
862
+ const existingAttributesMap = new Map<string, Attribute>();
863
+ try {
864
+ // @ts-expect-error
865
+ const parsedAttributes = currentCollection.attributes.map((attr) => parseAttribute(attr));
866
+ parsedAttributes.forEach(attr => existingAttributesMap.set(attr.key, attr));
867
+ } catch (error) {
868
+ console.log(chalk.yellow(`Warning: Could not parse existing attributes: ${error}`));
869
+ }
870
+
871
+ // Filter to only attributes that need processing (new or changed)
872
+ const attributesToProcess = attributes.filter(attribute => {
873
+ const existing = existingAttributesMap.get(attribute.key);
874
+ if (!existing) {
875
+ console.log(chalk.blue(`āž• New attribute: ${attribute.key}`));
876
+ return true;
877
+ }
878
+
879
+ const needsUpdate = !attributesSame(existing, attribute);
880
+ if (needsUpdate) {
881
+ console.log(chalk.blue(`šŸ”„ Changed attribute: ${attribute.key}`));
882
+ } else {
883
+ console.log(chalk.gray(`āœ… Unchanged attribute: ${attribute.key} (skipping)`));
884
+ }
885
+ return needsUpdate;
886
+ });
887
+
888
+ if (attributesToProcess.length === 0) {
889
+ console.log(chalk.green(`āœ… All ${attributes.length} attributes are already up to date for collection: ${collection.name}`));
890
+ return true;
891
+ }
892
+
893
+ console.log(chalk.blue(`Creating ${attributesToProcess.length} attributes sequentially with status monitoring...`));
894
+
895
+ let remainingAttributes = [...attributesToProcess];
857
896
  let overallRetryCount = 0;
858
897
  const maxOverallRetries = 3;
859
898
 
860
- while (attributesToProcess.length > 0 && overallRetryCount < maxOverallRetries) {
861
- const remainingAttributes = [...attributesToProcess];
862
- attributesToProcess = []; // Reset for next iteration
899
+ while (remainingAttributes.length > 0 && overallRetryCount < maxOverallRetries) {
900
+ const attributesToProcessThisRound = [...remainingAttributes];
901
+ remainingAttributes = []; // Reset for next iteration
863
902
 
864
- console.log(chalk.blue(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${remainingAttributes.length} attributes ===`));
903
+ console.log(chalk.blue(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${attributesToProcessThisRound.length} attributes ===`));
865
904
 
866
- for (const attribute of remainingAttributes) {
905
+ for (const attribute of attributesToProcessThisRound) {
867
906
  console.log(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`));
868
907
 
869
908
  const success = await createOrUpdateAttributeWithStatusCheck(
@@ -887,12 +926,12 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
887
926
  await delay(1000);
888
927
  } else {
889
928
  console.log(chalk.red(`āŒ Failed to create attribute: ${attribute.key}, will retry in next round`));
890
- attributesToProcess.push(attribute); // Add back to retry list
929
+ remainingAttributes.push(attribute); // Add back to retry list
891
930
  }
892
931
  }
893
932
 
894
- if (attributesToProcess.length === 0) {
895
- console.log(chalk.green(`\nāœ… Successfully created all ${attributes.length} attributes for collection: ${collection.name}`));
933
+ if (remainingAttributes.length === 0) {
934
+ console.log(chalk.green(`\nāœ… Successfully created all ${attributesToProcess.length} attributes for collection: ${collection.name}`));
896
935
  return true;
897
936
  }
898
937
 
@@ -98,44 +98,6 @@ const waitForIndexAvailable = async (
98
98
  return false;
99
99
  };
100
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
101
 
140
102
  /**
141
103
  * Enhanced index creation with proper status monitoring and retry logic
@@ -152,7 +114,19 @@ export const createOrUpdateIndexWithStatusCheck = async (
152
114
  console.log(chalk.blue(`Creating/updating index '${index.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`));
153
115
 
154
116
  try {
155
- // First, try to create/update the index using existing logic
117
+ // First, validate that all required attributes exist
118
+ const freshCollection = await db.getCollection(dbId, collectionId);
119
+ const existingAttributeKeys = freshCollection.attributes.map((attr: any) => attr.key);
120
+
121
+ const missingAttributes = index.attributes.filter(attr => !existingAttributeKeys.includes(attr));
122
+
123
+ if (missingAttributes.length > 0) {
124
+ console.log(chalk.red(`āŒ Index '${index.key}' cannot be created: missing attributes [${missingAttributes.join(', ')}]`));
125
+ console.log(chalk.red(`Available attributes: [${existingAttributeKeys.join(', ')}]`));
126
+ return false; // Don't retry if attributes are missing
127
+ }
128
+
129
+ // Try to create/update the index using existing logic
156
130
  await createOrUpdateIndex(dbId, db, collectionId, index);
157
131
 
158
132
  // Now wait for the index to become available
@@ -170,35 +144,40 @@ export const createOrUpdateIndexWithStatusCheck = async (
170
144
  return true;
171
145
  }
172
146
 
173
- // If not successful and we have retries left, delete collection and try again
147
+ // If not successful and we have retries left, just retry the index creation
174
148
  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);
149
+ console.log(chalk.yellow(`Index '${index.key}' failed/stuck, retrying (${retryCount + 1}/${maxRetries})...`));
179
150
 
180
- // Delete and recreate collection
181
- const newCollection = await deleteAndRecreateCollectionForIndex(db, dbId, freshCollection, retryCount + 1);
151
+ // Wait a bit before retry
152
+ await new Promise(resolve => setTimeout(resolve, 2000 * (retryCount + 1)));
182
153
 
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
- }
154
+ // Retry the index creation
155
+ return await createOrUpdateIndexWithStatusCheck(
156
+ dbId,
157
+ db,
158
+ collectionId,
159
+ collection,
160
+ index,
161
+ retryCount + 1,
162
+ maxRetries
163
+ );
195
164
  }
196
165
 
197
166
  console.log(chalk.red(`āŒ Failed to create index '${index.key}' after ${maxRetries + 1} attempts`));
198
167
  return false;
199
168
 
200
169
  } catch (error) {
201
- console.log(chalk.red(`Error creating index '${index.key}': ${error}`));
170
+ const errorMessage = error instanceof Error ? error.message : String(error);
171
+ console.log(chalk.red(`Error creating index '${index.key}': ${errorMessage}`));
172
+
173
+ // Check if this is a permanent error that shouldn't be retried
174
+ if (errorMessage.includes('not found') ||
175
+ errorMessage.includes('missing') ||
176
+ errorMessage.includes('does not exist') ||
177
+ errorMessage.includes('attribute') && errorMessage.includes('not found')) {
178
+ console.log(chalk.red(`āŒ Index '${index.key}' has permanent error - not retrying`));
179
+ return false;
180
+ }
202
181
 
203
182
  if (retryCount < maxRetries) {
204
183
  console.log(chalk.yellow(`Retrying index '${index.key}' due to error...`));