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.

391 lines (373 loc) 14.8 kB
import { TableCreateSchema } from "appwrite-utils"; import { MessageFormatter } from "../shared/messageFormatter.js"; import { validateCollectionsTablesConfig } from "./configValidation.js"; import path from "path"; import fs from "fs"; import chalk from "chalk"; /** * Creates a migration plan for converting collections to tables */ export function createMigrationPlan(config, strategy = "full_migration", specificCollections) { const collections = config.collections || []; const existingTables = config.tables || []; const collectionsToMigrate = []; const collectionsToKeep = []; const tablesToCreate = []; const expectedChanges = []; const warnings = []; const recommendations = []; // Determine which collections to migrate based on strategy collections.forEach((collection, index) => { const shouldMigrate = determineShouldMigrate(collection, strategy, specificCollections); if (shouldMigrate) { const migrationItem = createCollectionMigrationItem(collection, index); collectionsToMigrate.push(migrationItem); tablesToCreate.push(migrationItem.newTable); // Check for potential issues if (migrationItem.warnings.length > 0) { warnings.push(...migrationItem.warnings); } } else { collectionsToKeep.push(collection); } }); // Create expected changes expectedChanges.push(...createExpectedChanges(strategy, collectionsToMigrate, collectionsToKeep)); // Assess complexity const estimatedComplexity = assessMigrationComplexity(collectionsToMigrate, strategy); // Generate recommendations recommendations.push(...generateMigrationRecommendations(config, strategy, collectionsToMigrate)); // Add strategy-specific warnings if (strategy === "dual_format") { warnings.push("Dual format increases configuration file size and maintenance overhead"); recommendations.push("Consider migrating to tables_only after testing dual format"); } if (strategy === "full_migration" && collections.length > 10) { warnings.push("Large migration may require careful testing and staged deployment"); recommendations.push("Consider incremental migration for large projects"); } return { strategy, collectionsToMigrate, collectionsToKeep, tablesToCreate, expectedChanges, estimatedComplexity, warnings, recommendations }; } /** * Executes a migration plan */ export function executeMigrationPlan(config, plan, options = {}) { const { validateResult = true, dryRun = false, preserveOriginal = false } = options; try { // Create new configuration based on strategy const newConfig = applyMigrationPlan(config, plan, preserveOriginal); // Validate the result if requested let validation = { isValid: true, errors: [], warnings: [], suggestions: [] }; if (validateResult) { validation = validateCollectionsTablesConfig(newConfig); } const changes = plan.expectedChanges; const warnings = [...plan.warnings]; const errors = []; // Add validation errors to the result if (validation.errors.length > 0) { errors.push(...validation.errors.map(e => e.message)); } if (validation.warnings.length > 0) { warnings.push(...validation.warnings.map(w => w.message)); } const success = errors.length === 0; if (dryRun) { MessageFormatter.info("Dry run completed - no changes were made", { prefix: "Migration" }); } return { success, newConfig, changes, validation, warnings, errors }; } catch (error) { return { success: false, newConfig: config, changes: [], validation: { isValid: false, errors: [], warnings: [], suggestions: [] }, warnings: [], errors: [error instanceof Error ? error.message : "Unknown migration error"] }; } } /** * Converts a single collection to table format */ export function convertCollectionToTable(collection) { const table = { name: collection.name, tableId: collection.$id || collection.name.toLowerCase().replace(/\s+/g, '_'), enabled: collection.enabled, documentSecurity: collection.documentSecurity, $permissions: collection.$permissions, attributes: collection.attributes, indexes: collection.indexes, importDefs: collection.importDefs, databaseId: collection.databaseId }; // Add $id for backward compatibility if it exists if (collection.$id) { table.$id = collection.$id; } return table; } /** * Creates a migration item for a single collection */ function createCollectionMigrationItem(collection, index) { const tableInput = convertCollectionToTable(collection); const newTable = TableCreateSchema.parse(tableInput); const changes = []; const warnings = []; // Document changes changes.push(`Convert collection '${collection.name}' to table format`); if (collection.$id && collection.$id !== newTable.tableId) { changes.push(`Change ID from '$id: ${collection.$id}' to 'tableId: ${newTable.tableId}'`); } // Check for potential issues if (collection.attributes.some(attr => attr.type === 'relationship')) { warnings.push(`Collection '${collection.name}' has relationship attributes that may need manual review`); } if (collection.indexes && collection.indexes.length > 5) { warnings.push(`Collection '${collection.name}' has many indexes (${collection.indexes.length}) - verify compatibility`); } return { collection, index, newTable, changes, warnings }; } /** * Determines if a collection should be migrated based on strategy */ function determineShouldMigrate(collection, strategy, specificCollections) { switch (strategy) { case "full_migration": case "tables_only": return true; case "dual_format": return true; case "incremental": if (!specificCollections || specificCollections.length === 0) { return false; } return specificCollections.includes(collection.name) || specificCollections.includes(collection.$id || ""); default: return false; } } /** * Creates expected changes list based on migration plan */ function createExpectedChanges(strategy, collectionsToMigrate, collectionsToKeep) { const changes = []; // Add table creation changes collectionsToMigrate.forEach(item => { changes.push({ type: "add", description: `Add table '${item.newTable.name}' converted from collection`, impact: "medium", location: `tables[${item.newTable.name}]` }); }); // Add collection removal changes (if not preserving) if (strategy === "full_migration" || strategy === "tables_only") { collectionsToMigrate.forEach(item => { changes.push({ type: "remove", description: `Remove collection '${item.collection.name}' (converted to table)`, impact: "high", location: `collections[${item.index}]` }); }); } // Add array structure changes if (collectionsToMigrate.length > 0) { changes.push({ type: "modify", description: `${strategy === "dual_format" ? "Add" : "Create"} tables array with ${collectionsToMigrate.length} items`, impact: "medium", location: "config.tables" }); } if (strategy === "tables_only" && collectionsToKeep.length === 0) { changes.push({ type: "remove", description: "Remove collections array (fully migrated to tables)", impact: "high", location: "config.collections" }); } return changes; } /** * Assesses migration complexity */ function assessMigrationComplexity(collectionsToMigrate, strategy) { const collectionCount = collectionsToMigrate.length; const hasRelationships = collectionsToMigrate.some(item => item.collection.attributes.some(attr => attr.type === 'relationship')); const hasComplexIndexes = collectionsToMigrate.some(item => (item.collection.indexes?.length || 0) > 5); if (collectionCount === 0) return "low"; if (collectionCount <= 3 && !hasRelationships && !hasComplexIndexes) return "low"; if (collectionCount <= 10 && !hasComplexIndexes && strategy !== "full_migration") return "medium"; return "high"; } /** * Generates migration recommendations */ function generateMigrationRecommendations(config, strategy, collectionsToMigrate) { const recommendations = []; if (collectionsToMigrate.length > 5) { recommendations.push("Consider migrating in smaller batches for easier rollback"); } if (collectionsToMigrate.some(item => item.warnings.length > 0)) { recommendations.push("Review relationship attributes and complex indexes after migration"); } if (!config.apiMode || config.apiMode === "auto") { recommendations.push("Set apiMode to 'tablesdb' after migration for explicit API selection"); } if (strategy === "dual_format") { recommendations.push("Test with dual format before removing collections array"); recommendations.push("Monitor for any breaking changes with both APIs"); } return recommendations; } /** * Applies migration plan to create new configuration */ function applyMigrationPlan(config, plan, preserveOriginal) { const newConfig = { ...config }; // Initialize tables array if it doesn't exist if (!newConfig.tables) { newConfig.tables = []; } // Add migrated tables newConfig.tables.push(...plan.tablesToCreate); // Handle collections based on strategy if (plan.strategy === "full_migration" || plan.strategy === "tables_only") { if (preserveOriginal) { // Keep original collections but mark them as migrated newConfig.collections = (newConfig.collections || []).map(collection => ({ ...collection, // Add a comment or marker to indicate it's been migrated _migrated: true })); } else { // Remove migrated collections const migratedNames = new Set(plan.collectionsToMigrate.map(item => item.collection.name)); newConfig.collections = (newConfig.collections || []).filter(collection => !migratedNames.has(collection.name)); // If no collections remain, remove the array for tables_only strategy if (newConfig.collections.length === 0 && plan.strategy === "tables_only") { delete newConfig.collections; } } } // For dual_format and incremental, keep existing collections unchanged return newConfig; } /** * Utility function to migrate collections to tables with simple interface */ export function migrateCollectionsToTables(config, options = {}) { const { strategy = "full_migration", specificCollections, validateResult = true, dryRun = false } = options; // Create migration plan const plan = createMigrationPlan(config, strategy, specificCollections); // Execute migration const result = executeMigrationPlan(config, plan, { validateResult, dryRun }); return result; } /** * Saves migration results to files */ export async function saveMigrationResult(result, outputPath, options = {}) { const { createBackup = true, originalConfigPath } = options; try { // Create backup if requested if (createBackup && originalConfigPath && fs.existsSync(originalConfigPath)) { const backupPath = `${originalConfigPath}.backup.${Date.now()}`; fs.copyFileSync(originalConfigPath, backupPath); MessageFormatter.success(`Backup created: ${backupPath}`, { prefix: "Migration" }); } // Write new configuration const configContent = `import { type AppwriteConfig } from "appwrite-utils"; const appwriteConfig: AppwriteConfig = ${JSON.stringify(result.newConfig, null, 2)}; export default appwriteConfig; `; fs.writeFileSync(outputPath, configContent, 'utf8'); MessageFormatter.success(`Migration result saved: ${outputPath}`, { prefix: "Migration" }); // Create migration report const reportPath = outputPath.replace(/\.(ts|js)$/, '.migration-report.md'); const report = generateMigrationReport(result); fs.writeFileSync(reportPath, report, 'utf8'); MessageFormatter.info(`Migration report saved: ${reportPath}`, { prefix: "Migration" }); } catch (error) { throw new Error(`Failed to save migration result: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Generates a detailed migration report */ function generateMigrationReport(result) { const timestamp = new Date().toISOString(); return `# Migration Report **Generated:** ${timestamp} **Status:** ${result.success ? "✅ Success" : "❌ Failed"} ## Changes Applied ${result.changes.map(change => `- **${change.type.toUpperCase()}**: ${change.description} (Impact: ${change.impact})`).join('\n')} ## Validation Results **Valid Configuration:** ${result.validation.isValid ? "✅ Yes" : "❌ No"} ${result.validation.errors.length > 0 ? ` ### Errors ${result.validation.errors.map(error => `- ${error.message}`).join('\n')} ` : ''} ${result.validation.warnings.length > 0 ? ` ### Warnings ${result.validation.warnings.map(warning => `- ${warning.message}`).join('\n')} ` : ''} ${result.validation.suggestions.length > 0 ? ` ### Suggestions ${result.validation.suggestions.map(suggestion => `- ${suggestion.message}`).join('\n')} ` : ''} ${result.warnings.length > 0 ? ` ## Migration Warnings ${result.warnings.map(warning => `- ${warning}`).join('\n')} ` : ''} ${result.errors.length > 0 ? ` ## Migration Errors ${result.errors.map(error => `- ${error}`).join('\n')} ` : ''} ## Next Steps 1. Review the migrated configuration 2. Test with your Appwrite instance 3. Update your deployment scripts if necessary 4. Consider setting \`apiMode\` to 'tablesdb' for explicit API selection --- *Generated by appwrite-utils-cli migration tools* `; }