UNPKG

appwrite-utils-cli

Version:

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.

1,040 lines (949 loc) 34.7 kB
import { Query, type Databases, type Models } from "node-appwrite"; import { attributeSchema, parseAttribute, type Attribute, } from "appwrite-utils"; import { nameToIdMapping, enqueueOperation } from "../shared/operationQueue.js"; import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js"; import chalk from "chalk"; // Interface for attribute with status (fixing the type issue) interface AttributeWithStatus { key: string; type: string; status: 'available' | 'processing' | 'deleting' | 'stuck' | 'failed'; error: string; required: boolean; array?: boolean; $createdAt: string; $updatedAt: string; [key: string]: any; // For type-specific fields } /** * Wait for attribute to become available, with retry logic for stuck attributes and exponential backoff */ const waitForAttributeAvailable = async ( db: Databases, dbId: string, collectionId: string, attributeKey: string, maxWaitTime: number = 60000, // 1 minute retryCount: number = 0, maxRetries: number = 5 ): Promise<boolean> => { const startTime = Date.now(); let checkInterval = 2000; // Start with 2 seconds // Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s) if (retryCount > 0) { const exponentialDelay = Math.min(2000 * Math.pow(2, retryCount), 30000); console.log(chalk.blue(`Waiting for attribute '${attributeKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`)); await delay(exponentialDelay); } else { console.log(chalk.blue(`Waiting for attribute '${attributeKey}' to become available...`)); } while (Date.now() - startTime < maxWaitTime) { try { const collection = await db.getCollection(dbId, collectionId); const attribute = (collection.attributes as any[]).find( (attr: AttributeWithStatus) => attr.key === attributeKey ) as AttributeWithStatus | undefined; if (!attribute) { console.log(chalk.red(`Attribute '${attributeKey}' not found`)); return false; } console.log(chalk.gray(`Attribute '${attributeKey}' status: ${attribute.status}`)); switch (attribute.status) { case 'available': console.log(chalk.green(`✅ Attribute '${attributeKey}' is now available`)); return true; case 'failed': console.log(chalk.red(`❌ Attribute '${attributeKey}' failed: ${attribute.error}`)); return false; case 'stuck': console.log(chalk.yellow(`⚠️ Attribute '${attributeKey}' is stuck, will retry...`)); return false; case 'processing': // Continue waiting break; case 'deleting': console.log(chalk.yellow(`Attribute '${attributeKey}' is being deleted`)); break; default: console.log(chalk.yellow(`Unknown status '${attribute.status}' for attribute '${attributeKey}'`)); break; } await delay(checkInterval); } catch (error) { console.log(chalk.red(`Error checking attribute status: ${error}`)); return false; } } // Timeout reached console.log(chalk.yellow(`⏰ Timeout waiting for attribute '${attributeKey}' (${maxWaitTime}ms)`)); // If we have retries left and this isn't the last retry, try recreating if (retryCount < maxRetries) { console.log(chalk.yellow(`🔄 Retrying attribute creation (attempt ${retryCount + 1}/${maxRetries})`)); return false; // Signal that we need to retry } return false; }; /** * Wait for all attributes in a collection to become available */ const waitForAllAttributesAvailable = async ( db: Databases, dbId: string, collectionId: string, attributeKeys: string[], maxWaitTime: number = 60000 ): Promise<string[]> => { console.log(chalk.blue(`Waiting for ${attributeKeys.length} attributes to become available...`)); const failedAttributes: string[] = []; for (const attributeKey of attributeKeys) { const success = await waitForAttributeAvailable(db, dbId, collectionId, attributeKey, maxWaitTime); if (!success) { failedAttributes.push(attributeKey); } } return failedAttributes; }; /** * Delete collection and recreate with retry logic */ const deleteAndRecreateCollection = async ( db: Databases, dbId: string, collection: Models.Collection, retryCount: number ): Promise<Models.Collection | null> => { try { console.log(chalk.yellow(`🗑️ Deleting collection '${collection.name}' for retry ${retryCount}`)); // Delete the collection await db.deleteCollection(dbId, collection.$id); console.log(chalk.yellow(`Deleted collection '${collection.name}'`)); // Wait a bit before recreating await delay(2000); // Recreate the collection console.log(chalk.blue(`🔄 Recreating collection '${collection.name}'`)); const newCollection = await db.createCollection( dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled ); console.log(chalk.green(`✅ Recreated collection '${collection.name}'`)); return newCollection; } catch (error) { console.log(chalk.red(`Failed to delete/recreate collection '${collection.name}': ${error}`)); return null; } }; const attributesSame = ( databaseAttribute: Attribute, configAttribute: Attribute ): boolean => { const attributesToCheck = [ "key", "type", "array", "encrypted", "required", "size", "min", "max", "xdefault", "elements", "relationType", "twoWay", "twoWayKey", "onDelete", "relatedCollection", ]; return attributesToCheck.every((attr) => { // Check if both objects have the attribute const dbHasAttr = attr in databaseAttribute; const configHasAttr = attr in configAttribute; // If both have the attribute, compare values if (dbHasAttr && configHasAttr) { const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute]; const configValue = configAttribute[attr as keyof typeof configAttribute]; // Consider undefined and null as equivalent if ( (dbValue === undefined || dbValue === null) && (configValue === undefined || configValue === null) ) { return true; } return dbValue === configValue; } // If neither has the attribute, consider it the same if (!dbHasAttr && !configHasAttr) { return true; } // If one has the attribute and the other doesn't, check if it's undefined or null if (dbHasAttr && !configHasAttr) { const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute]; return dbValue === undefined || dbValue === null; } if (!dbHasAttr && configHasAttr) { const configValue = configAttribute[attr as keyof typeof configAttribute]; return configValue === undefined || configValue === null; } // If we reach here, the attributes are different return false; }); }; /** * Enhanced attribute creation with proper status monitoring and retry logic */ export const createOrUpdateAttributeWithStatusCheck = async ( db: Databases, dbId: string, collection: Models.Collection, attribute: Attribute, retryCount: number = 0, maxRetries: number = 5 ): Promise<boolean> => { console.log(chalk.blue(`Creating/updating attribute '${attribute.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`)); try { // First, try to create/update the attribute using existing logic await createOrUpdateAttribute(db, dbId, collection, attribute); // Now wait for the attribute to become available const success = await waitForAttributeAvailable( db, dbId, collection.$id, attribute.key, 60000, // 1 minute timeout retryCount, maxRetries ); if (success) { return true; } // If not successful and we have retries left, delete specific attribute and try again if (retryCount < maxRetries) { console.log(chalk.yellow(`Attribute '${attribute.key}' failed/stuck, deleting and retrying...`)); // Try to delete the specific stuck attribute instead of the entire collection try { await db.deleteAttribute(dbId, collection.$id, attribute.key); console.log(chalk.yellow(`Deleted stuck attribute '${attribute.key}', will retry creation`)); // Wait a bit before retry await delay(3000); // Get fresh collection data const freshCollection = await db.getCollection(dbId, collection.$id); // Retry with the same collection (attribute should be gone now) return await createOrUpdateAttributeWithStatusCheck( db, dbId, freshCollection, attribute, retryCount + 1, maxRetries ); } catch (deleteError) { console.log(chalk.red(`Failed to delete stuck attribute '${attribute.key}': ${deleteError}`)); // If attribute deletion fails, only then try collection recreation as last resort if (retryCount >= maxRetries - 1) { console.log(chalk.yellow(`Last resort: Recreating collection for attribute '${attribute.key}'`)); // Get fresh collection data const freshCollection = await db.getCollection(dbId, collection.$id); // Delete and recreate collection const newCollection = await deleteAndRecreateCollection(db, dbId, freshCollection, retryCount + 1); if (newCollection) { // Retry with the new collection return await createOrUpdateAttributeWithStatusCheck( db, dbId, newCollection, attribute, retryCount + 1, maxRetries ); } } else { // Continue to next retry without collection recreation return await createOrUpdateAttributeWithStatusCheck( db, dbId, collection, attribute, retryCount + 1, maxRetries ); } } } console.log(chalk.red(`❌ Failed to create attribute '${attribute.key}' after ${maxRetries + 1} attempts`)); return false; } catch (error) { console.log(chalk.red(`Error creating attribute '${attribute.key}': ${error}`)); if (retryCount < maxRetries) { console.log(chalk.yellow(`Retrying attribute '${attribute.key}' due to error...`)); // Wait a bit before retry await delay(2000); return await createOrUpdateAttributeWithStatusCheck( db, dbId, collection, attribute, retryCount + 1, maxRetries ); } return false; } }; export const createOrUpdateAttribute = async ( db: Databases, dbId: string, collection: Models.Collection, attribute: Attribute ): Promise<void> => { let action = "create"; let foundAttribute: Attribute | undefined; const updateEnabled = true; let finalAttribute: any = attribute; try { const collectionAttr = collection.attributes.find( (attr: any) => attr.key === attribute.key ) as unknown as any; foundAttribute = parseAttribute(collectionAttr); // console.log(`Found attribute: ${JSON.stringify(foundAttribute)}`); } catch (error) { foundAttribute = undefined; } if ( foundAttribute && attributesSame(foundAttribute, attribute) && updateEnabled ) { // No need to do anything, they are the same return; } else if ( foundAttribute && !attributesSame(foundAttribute, attribute) && updateEnabled ) { // console.log( // `Updating attribute with same key ${attribute.key} but different values` // ); finalAttribute = { ...foundAttribute, ...attribute, }; action = "update"; } else if ( !updateEnabled && foundAttribute && !attributesSame(foundAttribute, attribute) ) { await db.deleteAttribute(dbId, collection.$id, attribute.key); console.log( `Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)` ); return; } // console.log(`${action}-ing attribute: ${finalAttribute.key}`); // Relationship attribute logic with adjustments let collectionFoundViaRelatedCollection: Models.Collection | undefined; let relatedCollectionId: string | undefined; if (finalAttribute.type === "relationship") { if (nameToIdMapping.has(finalAttribute.relatedCollection)) { relatedCollectionId = nameToIdMapping.get( finalAttribute.relatedCollection ); try { collectionFoundViaRelatedCollection = await db.getCollection( dbId, relatedCollectionId! ); } catch (e) { // console.log( // `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set` // ); collectionFoundViaRelatedCollection = undefined; } } else { const collectionsPulled = await db.listCollections(dbId, [ Query.equal("name", finalAttribute.relatedCollection), ]); if (collectionsPulled.total > 0) { collectionFoundViaRelatedCollection = collectionsPulled.collections[0]; relatedCollectionId = collectionFoundViaRelatedCollection.$id; nameToIdMapping.set( finalAttribute.relatedCollection, relatedCollectionId ); } } if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) { // console.log(`Enqueueing operation for attribute: ${finalAttribute.key}`); enqueueOperation({ type: "attribute", collectionId: collection.$id, collection: collection, attribute, dependencies: [finalAttribute.relatedCollection], }); return; } } finalAttribute = parseAttribute(finalAttribute); // console.log(`Final Attribute: ${JSON.stringify(finalAttribute)}`); switch (finalAttribute.type) { case "string": if (action === "create") { await tryAwaitWithRetry( async () => await db.createStringAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.size, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false, finalAttribute.encrypted ) ); } else { await tryAwaitWithRetry( async () => await db.updateStringAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null ) ); } break; case "integer": if (action === "create") { if ( finalAttribute.min && BigInt(finalAttribute.min) === BigInt(-9223372036854776000) ) { delete finalAttribute.min; } if ( finalAttribute.max && BigInt(finalAttribute.max) === BigInt(9223372036854776000) ) { delete finalAttribute.max; } await tryAwaitWithRetry( async () => await db.createIntegerAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.min !== undefined ? finalAttribute.min : -2147483647, finalAttribute.max !== undefined ? finalAttribute.max : 2147483647, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false ) ); } else { if ( finalAttribute.min && BigInt(finalAttribute.min) === BigInt(-9223372036854776000) ) { delete finalAttribute.min; } if ( finalAttribute.max && BigInt(finalAttribute.max) === BigInt(9223372036854776000) ) { delete finalAttribute.max; } await tryAwaitWithRetry( async () => await db.updateIntegerAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.min !== undefined ? finalAttribute.min : -2147483647, finalAttribute.max !== undefined ? finalAttribute.max : 2147483647, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null ) ); } break; case "double": case "float": // Backward compatibility if (action === "create") { await tryAwaitWithRetry( async () => await db.createFloatAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.min !== undefined ? finalAttribute.min : -2147483647, finalAttribute.max !== undefined ? finalAttribute.max : 2147483647, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false ) ); } else { await tryAwaitWithRetry( async () => await db.updateFloatAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.min !== undefined ? finalAttribute.min : -2147483647, finalAttribute.max !== undefined ? finalAttribute.max : 2147483647, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null ) ); } break; case "boolean": if (action === "create") { await tryAwaitWithRetry( async () => await db.createBooleanAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false ) ); } else { await tryAwaitWithRetry( async () => await db.updateBooleanAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null ) ); } break; case "datetime": if (action === "create") { await tryAwaitWithRetry( async () => await db.createDatetimeAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false ) ); } else { await tryAwaitWithRetry( async () => await db.updateDatetimeAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null ) ); } break; case "email": if (action === "create") { await tryAwaitWithRetry( async () => await db.createEmailAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false ) ); } else { await tryAwaitWithRetry( async () => await db.updateEmailAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null ) ); } break; case "ip": if (action === "create") { await tryAwaitWithRetry( async () => await db.createIpAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false ) ); } else { await tryAwaitWithRetry( async () => await db.updateIpAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null ) ); } break; case "url": if (action === "create") { await tryAwaitWithRetry( async () => await db.createUrlAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false ) ); } else { await tryAwaitWithRetry( async () => await db.updateUrlAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null ) ); } break; case "enum": if (action === "create") { await tryAwaitWithRetry( async () => await db.createEnumAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.elements, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false ) ); } else { await tryAwaitWithRetry( async () => await db.updateEnumAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.elements, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null ) ); } break; case "relationship": if (action === "create") { await tryAwaitWithRetry( async () => await db.createRelationshipAttribute( dbId, collection.$id, relatedCollectionId!, finalAttribute.relationType, finalAttribute.twoWay, finalAttribute.key, finalAttribute.twoWayKey, finalAttribute.onDelete ) ); } else { await tryAwaitWithRetry( async () => await db.updateRelationshipAttribute( dbId, collection.$id, finalAttribute.key, finalAttribute.onDelete ) ); } break; default: console.error("Invalid attribute type"); break; } }; /** * Enhanced collection attribute creation with proper status monitoring */ export const createUpdateCollectionAttributesWithStatusCheck = async ( db: Databases, dbId: string, collection: Models.Collection, attributes: Attribute[] ): Promise<boolean> => { console.log( chalk.green( `Creating/Updating attributes for collection: ${collection.name} with status monitoring` ) ); const existingAttributes: Attribute[] = // @ts-expect-error collection.attributes.map((attr) => parseAttribute(attr)) || []; const attributesToRemove = existingAttributes.filter( (attr) => !attributes.some((a) => a.key === attr.key) ); const indexesToRemove = collection.indexes.filter((index) => attributesToRemove.some((attr) => index.attributes.includes(attr.key)) ); // Handle attribute removal first if (attributesToRemove.length > 0) { if (indexesToRemove.length > 0) { console.log( chalk.red( `Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove .map((index) => index.key) .join(", ")}` ) ); for (const index of indexesToRemove) { await tryAwaitWithRetry( async () => await db.deleteIndex(dbId, collection.$id, index.key) ); await delay(500); // Longer delay for deletions } } for (const attr of attributesToRemove) { console.log( chalk.red( `Removing attribute: ${attr.key} as it is no longer in the collection` ) ); await tryAwaitWithRetry( async () => await db.deleteAttribute(dbId, collection.$id, attr.key) ); await delay(500); // Longer delay for deletions } } // First, get fresh collection data and determine which attributes actually need processing console.log(chalk.blue(`Analyzing ${attributes.length} attributes to determine which need processing...`)); let currentCollection = collection; try { currentCollection = await db.getCollection(dbId, collection.$id); } catch (error) { console.log(chalk.yellow(`Warning: Could not refresh collection data: ${error}`)); } const existingAttributesMap = new Map<string, Attribute>(); try { // @ts-expect-error const parsedAttributes = currentCollection.attributes.map((attr) => parseAttribute(attr)); parsedAttributes.forEach(attr => existingAttributesMap.set(attr.key, attr)); } catch (error) { console.log(chalk.yellow(`Warning: Could not parse existing attributes: ${error}`)); } // Filter to only attributes that need processing (new or changed) const attributesToProcess = attributes.filter(attribute => { const existing = existingAttributesMap.get(attribute.key); if (!existing) { console.log(chalk.blue(`➕ New attribute: ${attribute.key}`)); return true; } const needsUpdate = !attributesSame(existing, attribute); if (needsUpdate) { console.log(chalk.blue(`🔄 Changed attribute: ${attribute.key}`)); } else { console.log(chalk.gray(`✅ Unchanged attribute: ${attribute.key} (skipping)`)); } return needsUpdate; }); if (attributesToProcess.length === 0) { console.log(chalk.green(`✅ All ${attributes.length} attributes are already up to date for collection: ${collection.name}`)); return true; } console.log(chalk.blue(`Creating ${attributesToProcess.length} attributes sequentially with status monitoring...`)); let remainingAttributes = [...attributesToProcess]; let overallRetryCount = 0; const maxOverallRetries = 3; while (remainingAttributes.length > 0 && overallRetryCount < maxOverallRetries) { const attributesToProcessThisRound = [...remainingAttributes]; remainingAttributes = []; // Reset for next iteration console.log(chalk.blue(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${attributesToProcessThisRound.length} attributes ===`)); for (const attribute of attributesToProcessThisRound) { console.log(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`)); const success = await createOrUpdateAttributeWithStatusCheck( db, dbId, currentCollection, attribute ); if (success) { console.log(chalk.green(`✅ Successfully created attribute: ${attribute.key}`)); // Get updated collection data for next iteration try { currentCollection = await db.getCollection(dbId, collection.$id); } catch (error) { console.log(chalk.yellow(`Warning: Could not refresh collection data: ${error}`)); } // Add delay between successful attributes await delay(1000); } else { console.log(chalk.red(`❌ Failed to create attribute: ${attribute.key}, will retry in next round`)); remainingAttributes.push(attribute); // Add back to retry list } } if (remainingAttributes.length === 0) { console.log(chalk.green(`\n✅ Successfully created all ${attributesToProcess.length} attributes for collection: ${collection.name}`)); return true; } overallRetryCount++; if (overallRetryCount < maxOverallRetries) { console.log(chalk.yellow(`\n⏳ Waiting 5 seconds before retrying ${attributesToProcess.length} failed attributes...`)); await delay(5000); // Refresh collection data before retry try { currentCollection = await db.getCollection(dbId, collection.$id); console.log(chalk.blue(`Refreshed collection data for retry`)); } catch (error) { console.log(chalk.yellow(`Warning: Could not refresh collection data for retry: ${error}`)); } } } // If we get here, some attributes still failed after all retries if (attributesToProcess.length > 0) { console.log(chalk.red(`\n❌ Failed to create ${attributesToProcess.length} attributes after ${maxOverallRetries} attempts: ${attributesToProcess.map(a => a.key).join(', ')}`)); console.log(chalk.red(`This may indicate a fundamental issue with the attribute definitions or Appwrite instance`)); return false; } console.log(chalk.green(`\n✅ Successfully created all ${attributes.length} attributes for collection: ${collection.name}`)); return true; }; export const createUpdateCollectionAttributes = async ( db: Databases, dbId: string, collection: Models.Collection, attributes: Attribute[] ): Promise<void> => { console.log( chalk.green( `Creating/Updating attributes for collection: ${collection.name}` ) ); const existingAttributes: Attribute[] = // @ts-expect-error collection.attributes.map((attr) => parseAttribute(attr)) || []; const attributesToRemove = existingAttributes.filter( (attr) => !attributes.some((a) => a.key === attr.key) ); const indexesToRemove = collection.indexes.filter((index) => attributesToRemove.some((attr) => index.attributes.includes(attr.key)) ); if (attributesToRemove.length > 0) { if (indexesToRemove.length > 0) { console.log( chalk.red( `Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove .map((index) => index.key) .join(", ")}` ) ); for (const index of indexesToRemove) { await tryAwaitWithRetry( async () => await db.deleteIndex(dbId, collection.$id, index.key) ); await delay(100); } } for (const attr of attributesToRemove) { console.log( chalk.red( `Removing attribute: ${attr.key} as it is no longer in the collection` ) ); await tryAwaitWithRetry( async () => await db.deleteAttribute(dbId, collection.$id, attr.key) ); await delay(50); } } const batchSize = 3; for (let i = 0; i < attributes.length; i += batchSize) { const batch = attributes.slice(i, i + batchSize); const attributePromises = batch.map((attribute) => tryAwaitWithRetry( async () => await createOrUpdateAttribute(db, dbId, collection, attribute) ) ); const results = await Promise.allSettled(attributePromises); results.forEach((result) => { if (result.status === "rejected") { console.error("An attribute promise was rejected:", result.reason); } }); // Add delay after each batch await delay(200); } console.log( `Finished creating/updating attributes for collection: ${collection.name}` ); };