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
JavaScript
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*
`;
}