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.

151 lines (150 loc) 6.82 kB
import {} from "appwrite-utils"; import { Databases, IndexType, Query } from "node-appwrite"; import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js"; import chalk from "chalk"; import pLimit from "p-limit"; // Concurrency limits for different operations const indexLimit = pLimit(3); // Low limit for index operations const queryLimit = pLimit(25); // Higher limit for read operations export const indexesSame = (databaseIndex, configIndex) => { return (databaseIndex.key === configIndex.key && databaseIndex.type === configIndex.type && JSON.stringify(databaseIndex.attributes) === JSON.stringify(configIndex.attributes) && JSON.stringify(databaseIndex.orders) === JSON.stringify(configIndex.orders)); }; export const createOrUpdateIndex = async (dbId, db, collectionId, index, options = {}) => { const { verbose = false, forceRecreate = false } = options; return await indexLimit(async () => { // Check for existing index const existingIndexes = await queryLimit(() => tryAwaitWithRetry(async () => await db.listIndexes(dbId, collectionId, [Query.equal("key", index.key)]))); let shouldCreate = false; let existingIndex; if (existingIndexes.total > 0) { existingIndex = existingIndexes.indexes[0]; if (forceRecreate || !indexesSame(existingIndex, index)) { if (verbose) { console.log(chalk.yellow(`⚠ Updating index ${index.key} in collection ${collectionId}`)); } // Delete existing index await tryAwaitWithRetry(async () => { await db.deleteIndex(dbId, collectionId, existingIndex.key); }); await delay(500); // Wait for deletion to complete shouldCreate = true; } else { if (verbose) { console.log(chalk.green(`✓ Index ${index.key} is up to date`)); } return existingIndex; } } else { shouldCreate = true; if (verbose) { console.log(chalk.blue(`+ Creating index ${index.key} in collection ${collectionId}`)); } } if (shouldCreate) { const newIndex = await tryAwaitWithRetry(async () => { return await db.createIndex(dbId, collectionId, index.key, index.type, index.attributes, index.orders); }); if (verbose) { console.log(chalk.green(`✓ Created index ${index.key}`)); } return newIndex; } return null; }); }; export const createOrUpdateIndexes = async (dbId, db, collectionId, indexes, options = {}) => { const { verbose = false } = options; if (!indexes || indexes.length === 0) { return; } if (verbose) { console.log(chalk.blue(`Processing ${indexes.length} indexes for collection ${collectionId}`)); } // Process indexes sequentially to avoid conflicts for (const index of indexes) { try { await createOrUpdateIndex(dbId, db, collectionId, index, options); // Add delay between index operations to prevent rate limiting await delay(250); } catch (error) { console.error(chalk.red(`❌ Failed to process index ${index.key}:`), error); throw error; } } if (verbose) { console.log(chalk.green(`✓ Completed processing indexes for collection ${collectionId}`)); } }; export const createUpdateCollectionIndexes = async (db, dbId, collection, collectionConfig, options = {}) => { if (!collectionConfig.indexes) return; await createOrUpdateIndexes(dbId, db, collection.$id, collectionConfig.indexes, options); }; export const deleteObsoleteIndexes = async (db, dbId, collection, collectionConfig, options = {}) => { const { verbose = false } = options; const configIndexes = collectionConfig.indexes || []; const configIndexKeys = new Set(configIndexes.map(index => index.key)); // Get all existing indexes const existingIndexes = await queryLimit(() => tryAwaitWithRetry(async () => await db.listIndexes(dbId, collection.$id))); // Find indexes that exist in the database but not in the config const obsoleteIndexes = existingIndexes.indexes.filter((index) => !configIndexKeys.has(index.key)); if (obsoleteIndexes.length === 0) { return; } if (verbose) { console.log(chalk.yellow(`🗑️ Removing ${obsoleteIndexes.length} obsolete indexes from collection ${collection.name}`)); } // Process deletions with rate limiting for (const index of obsoleteIndexes) { await indexLimit(async () => { await tryAwaitWithRetry(async () => { await db.deleteIndex(dbId, collection.$id, index.key); }); }); if (verbose) { console.log(chalk.gray(`🗑️ Deleted obsolete index ${index.key}`)); } await delay(250); } }; export const validateIndexConfiguration = (indexes, options = {}) => { const { verbose = false } = options; const errors = []; for (const index of indexes) { // Validate required fields if (!index.key) { errors.push(`Index missing required 'key' field`); } if (!index.type) { errors.push(`Index '${index.key}' missing required 'type' field`); } if (!index.attributes || index.attributes.length === 0) { errors.push(`Index '${index.key}' missing required 'attributes' field`); } // Validate index type const validTypes = Object.values(IndexType); if (index.type && !validTypes.includes(index.type)) { errors.push(`Index '${index.key}' has invalid type '${index.type}'. Valid types: ${validTypes.join(', ')}`); } // Validate orders array matches attributes length (if provided) if (index.orders && index.attributes && index.orders.length !== index.attributes.length) { errors.push(`Index '${index.key}' orders array length (${index.orders.length}) does not match attributes array length (${index.attributes.length})`); } // Check for duplicate keys within the same collection const duplicateKeys = indexes.filter(i => i.key === index.key); if (duplicateKeys.length > 1) { errors.push(`Duplicate index key '${index.key}' found`); } } if (verbose && errors.length > 0) { console.log(chalk.red(`❌ Index validation errors:`)); errors.forEach(error => console.log(chalk.red(` - ${error}`))); } return { valid: errors.length === 0, errors }; };