UNPKG

hayai-db

Version:

⚡ Instantly create and manage local databases with one command

360 lines (353 loc) 15.9 kB
import { Command } from 'commander'; import chalk from 'chalk'; import inquirer from 'inquirer'; import ora from 'ora'; import { getDockerManager } from '../../core/docker.js'; import { getTemplate } from '../../core/templates.js'; import { spawn } from 'child_process'; // Engines totalmente compatíveis com implementação nativa específica const FULLY_COMPATIBLE_ENGINES = new Set([ 'postgresql', // pg_dump + psql (nativo) 'mariadb', // mysqldump + mysql (nativo) 'redis', // BGSAVE + RDB copy (nativo) 'sqlite', // File copy (confiável) 'duckdb' // File copy (confiável) ]); function validateCloneCompatibility(sourceEngine) { if (!FULLY_COMPATIBLE_ENGINES.has(sourceEngine)) { return { compatible: false, reason: `Engine '${sourceEngine}' uses generic backup which may be unreliable` }; } return { compatible: true }; } function showManualCloneGuidance(engine) { console.log(chalk.yellow('\n💡 Manual Clone Guidance:')); switch (engine) { case 'cassandra': console.log(chalk.gray(' • Use: nodetool snapshot + sstableloader')); console.log(chalk.gray(' • Or: cqlsh COPY commands')); break; case 'influxdb2': console.log(chalk.gray(' • Use: influx backup + influx restore')); break; case 'influxdb3': console.log(chalk.gray(' • Use: influx3 export + influx3 import')); break; case 'qdrant': console.log(chalk.gray(' • Use: Qdrant snapshots API')); console.log(chalk.gray(' • Or: /collections/{collection}/snapshots')); break; case 'meilisearch': console.log(chalk.gray(' • Use: dumps API endpoint')); console.log(chalk.gray(' • POST /dumps + GET /dumps/{dumpUid}')); break; case 'milvus': console.log(chalk.gray(' • Use: Milvus backup tool')); console.log(chalk.gray(' • Or: collection export/import')); break; case 'arangodb': console.log(chalk.gray(' • Use: arangodump + arangorestore')); break; case 'timescaledb': console.log(chalk.gray(' • Use: pg_dump (TimescaleDB extensions)')); console.log(chalk.gray(' • Include: --extension timescaledb')); break; default: console.log(chalk.gray(` • Check ${engine} documentation for native backup/restore tools`)); console.log(chalk.gray(' • Use engine-specific export/import commands')); console.log(chalk.gray(' • Consider data migration tools or scripts')); } console.log(chalk.yellow('\n📚 Alternative Options:')); console.log(chalk.gray(' • Use database-specific migration tools')); console.log(chalk.gray(' • Write custom data transfer scripts')); console.log(chalk.gray(' • Use hayai studio to access admin dashboards')); console.log(chalk.cyan(' • Run: hayai studio --help')); } async function executeClone(sourceInstance, targetName) { const dockerManager = getDockerManager(); // Get source template const sourceTemplate = getTemplate(sourceInstance.engine); if (!sourceTemplate) { throw new Error(`Template not found for engine: ${sourceInstance.engine}`); } console.log(chalk.cyan(`🔄 Cloning ${sourceInstance.name}${targetName}...`)); // Create target database with same configuration await dockerManager.createDatabase(targetName, sourceTemplate, { port: undefined, // Let it auto-allocate adminDashboard: false, customEnv: { ...sourceInstance.environment } }); // Start target database await dockerManager.startDatabase(targetName); // Wait for database to be ready await new Promise(resolve => setTimeout(resolve, 3000)); // Clone data based on database type await cloneData(sourceInstance, targetName); console.log(chalk.green(`✅ Successfully cloned ${sourceInstance.name}${targetName}`)); } async function cloneData(source, targetName) { const sourceContainer = `${source.name}-db`; const targetContainer = `${targetName}-db`; switch (source.engine) { case 'postgresql': await clonePostgreSQL(sourceContainer, targetContainer); break; case 'mariadb': await cloneMariaDB(sourceContainer, targetContainer); break; case 'redis': await cloneRedis(sourceContainer, targetContainer); break; case 'sqlite': case 'duckdb': await cloneFileDB(sourceContainer, targetContainer); break; default: // This situation should never happen due to compatibility validation throw new Error(`Unsupported engine for cloning: ${source.engine}`); } } async function clonePostgreSQL(sourceContainer, targetContainer) { return new Promise((resolve, reject) => { const dumpProcess = spawn('docker', [ 'exec', sourceContainer, 'pg_dump', '-U', 'postgres', '--clean', '--create' ], { stdio: ['inherit', 'pipe', 'pipe'] }); const restoreProcess = spawn('docker', [ 'exec', '-i', targetContainer, 'psql', '-U', 'postgres' ], { stdio: ['pipe', 'inherit', 'pipe'] }); dumpProcess.stdout.pipe(restoreProcess.stdin); restoreProcess.on('close', (code) => { code === 0 ? resolve() : reject(new Error('PostgreSQL clone failed')); }); dumpProcess.on('error', reject); restoreProcess.on('error', reject); }); } async function cloneMariaDB(sourceContainer, targetContainer) { return new Promise((resolve, reject) => { const dumpProcess = spawn('docker', [ 'exec', sourceContainer, 'mysqldump', '-u', 'root', '--all-databases' ], { stdio: ['inherit', 'pipe', 'pipe'] }); const restoreProcess = spawn('docker', [ 'exec', '-i', targetContainer, 'mysql', '-u', 'root' ], { stdio: ['pipe', 'inherit', 'pipe'] }); dumpProcess.stdout.pipe(restoreProcess.stdin); restoreProcess.on('close', (code) => { code === 0 ? resolve() : reject(new Error('MariaDB clone failed')); }); dumpProcess.on('error', reject); restoreProcess.on('error', reject); }); } async function cloneRedis(sourceContainer, targetContainer) { return new Promise((resolve, reject) => { // Create RDB backup const bgsaveProcess = spawn('docker', [ 'exec', sourceContainer, 'redis-cli', 'BGSAVE' ]); bgsaveProcess.on('close', async (code) => { if (code !== 0) { reject(new Error('Redis backup failed')); return; } // Wait for backup to complete await new Promise(resolve => setTimeout(resolve, 2000)); // Copy RDB file const copyProcess = spawn('docker', [ 'cp', `${sourceContainer}:/data/dump.rdb`, '/tmp/redis-clone.rdb' ]); copyProcess.on('close', (copyCode) => { if (copyCode !== 0) { reject(new Error('Failed to copy Redis data')); return; } // Restore to target const restoreProcess = spawn('docker', [ 'cp', '/tmp/redis-clone.rdb', `${targetContainer}:/data/dump.rdb` ]); restoreProcess.on('close', async (restoreCode) => { if (restoreCode === 0) { // Restart target to load data const dockerManager = getDockerManager(); try { await dockerManager.stopDatabase(targetContainer.replace('-db', '')); await dockerManager.startDatabase(targetContainer.replace('-db', '')); resolve(); } catch (error) { reject(error); } } else { reject(new Error('Failed to restore Redis data')); } }); restoreProcess.on('error', reject); }); copyProcess.on('error', reject); }); bgsaveProcess.on('error', reject); }); } async function cloneFileDB(sourceContainer, targetContainer) { return new Promise((resolve, reject) => { // Copy database file const copyProcess = spawn('docker', [ 'cp', `${sourceContainer}:/data/database.db`, '/tmp/hayai-clone.db' ]); copyProcess.on('close', (code) => { if (code !== 0) { reject(new Error('Failed to copy database file')); return; } // Restore to target const restoreProcess = spawn('docker', [ 'cp', '/tmp/hayai-clone.db', `${targetContainer}:/data/database.db` ]); restoreProcess.on('close', (restoreCode) => { restoreCode === 0 ? resolve() : reject(new Error('Failed to restore database file')); }); restoreProcess.on('error', reject); }); copyProcess.on('error', reject); }); } async function handleClone(options) { const dockerManager = getDockerManager(); await dockerManager.initialize(); // Validate source database const sourceInstance = dockerManager.getInstance(options.from); if (!sourceInstance) { console.error(chalk.red(`❌ Source database '${options.from}' not found`)); console.log(chalk.yellow('💡 Run `hayai list` to see available databases')); process.exit(1); } // Check if source is running if (sourceInstance.status !== 'running') { console.error(chalk.red(`❌ Source database '${options.from}' must be running`)); console.log(chalk.yellow(`💡 Start it with: ${chalk.cyan(`hayai start ${options.from}`)}`)); process.exit(1); } // Validate compatibility const compatibilityResult = validateCloneCompatibility(sourceInstance.engine); if (!compatibilityResult.compatible) { console.error(chalk.red(`❌ Source engine '${sourceInstance.engine}' is not fully compatible for cloning.`)); console.error(chalk.red(`Reason: ${compatibilityResult.reason}`)); showManualCloneGuidance(sourceInstance.engine); process.exit(1); } // Determine target databases let targetNames = []; if (options.to) { targetNames = [options.to]; } else if (options.toMultiple) { targetNames = options.toMultiple.split(',').map(name => name.trim()); } else { console.error(chalk.red('❌ Must specify target database(s)')); console.log(chalk.yellow('💡 Use --to or --to-multiple')); process.exit(1); } // Validate target names for (const targetName of targetNames) { if (dockerManager.getInstance(targetName)) { if (!options.force) { console.error(chalk.red(`❌ Target database '${targetName}' already exists`)); console.log(chalk.yellow('💡 Use --force to overwrite existing databases')); process.exit(1); } } } // Show preview console.log(chalk.cyan('\n🔍 Clone Preview:')); console.log(chalk.gray(`Source: ${options.from} (${sourceInstance.engine})`)); console.log(chalk.gray(`Targets: ${targetNames.join(', ')}`)); if (options.dryRun) { console.log(chalk.yellow('\n🚧 Dry run - no actual cloning performed')); return; } // Confirmation if (!options.confirm && !options.force) { const { proceed } = await inquirer.prompt([ { type: 'confirm', name: 'proceed', message: `Clone ${options.from} to ${targetNames.length} database(s)?`, default: false, }, ]); if (!proceed) { console.log(chalk.yellow('Operation cancelled')); return; } } // Execute clones const spinner = ora('Cloning databases...').start(); try { for (let i = 0; i < targetNames.length; i++) { const targetName = targetNames[i]; spinner.text = `Cloning ${options.from}${targetName} (${i + 1}/${targetNames.length})`; // Remove existing if force if (options.force && dockerManager.getInstance(targetName)) { await dockerManager.removeDatabase(targetName); } await executeClone(sourceInstance, targetName); } spinner.succeed(`Successfully cloned ${options.from} to ${targetNames.length} database(s)`); console.log(chalk.green('\n✅ Clone operation completed!')); console.log(chalk.yellow('💡 Commands:')); console.log(` • ${chalk.cyan('hayai list')} - View all databases`); console.log(` • ${chalk.cyan('hayai studio')} - Open admin dashboards`); } catch (error) { spinner.fail('Clone operation failed'); console.error(chalk.red('\n❌ Clone failed:'), error instanceof Error ? error.message : error); process.exit(1); } } export const cloneCommand = new Command('clone') .description('Clone database instances (compatible engines only)') .option('-f, --from <name>', 'Source database name') .option('-t, --to <name>', 'Target database name (1:1 clone)') .option('-tm, --to-multiple <names>', 'Target database names (comma-separated, 1:N clone)') .option('-y, --confirm', 'Skip confirmation prompt') .option('--force', 'Overwrite existing target databases') .option('--dry-run', 'Show what would be cloned without executing') .option('--verbose', 'Enable verbose output') .addHelpText('after', ` ${chalk.bold('Supported Engines (Fully Compatible):')} ${chalk.green('✅ postgresql')} - Native pg_dump + psql ${chalk.green('✅ mariadb')} - Native mysqldump + mysql ${chalk.green('✅ redis')} - Native BGSAVE + RDB copy ${chalk.green('✅ sqlite')} - Reliable file copy ${chalk.green('✅ duckdb')} - Reliable file copy ${chalk.bold('Unsupported Engines (Manual Clone Required):')} ${chalk.red('❌ cassandra, influxdb2, influxdb3, timescaledb, questdb')} ${chalk.red('❌ qdrant, weaviate, milvus, arangodb, nebula')} ${chalk.red('❌ meilisearch, typesense, victoriametrics, horaedb')} ${chalk.red('❌ leveldb, lmdb, tikv')} ${chalk.bold('Examples:')} ${chalk.cyan('# Clone PostgreSQL database')} hayai clone --from prod-postgres --to staging-postgres hayai clone -f prod-postgres -t staging-postgres -y ${chalk.cyan('# Clone Redis to multiple instances')} hayai clone --from cache-redis --to-multiple "test1,test2,test3" hayai clone -f cache-redis -tm "dev,staging,qa" -y ${chalk.cyan('# Safe cloning with preview')} hayai clone -f prod-mariadb -t staging-mariadb --dry-run ${chalk.bold('Visual Syntax (alternative):')} ${chalk.cyan('hayai clone postgres-prod → postgres-staging')} ${chalk.gray('# Simple clone')} ${chalk.cyan('hayai clone redis-cache → redis1,redis2')} ${chalk.gray('# Multiple targets')} ${chalk.bold('For unsupported engines:')} ${chalk.yellow('Use engine-specific tools:')} cassandra (nodetool), influx (backup/restore) ${chalk.yellow('Access admin dashboards:')} hayai studio ${chalk.yellow('Manual data migration:')} Write custom scripts or use migration tools `) .action(handleClone);