UNPKG

@vasoyaprince14/sql-analyzer

Version:

šŸš€ Enhanced SQL database analyzer with AI-powered insights, comprehensive security analysis, RLS policy auditing, and beautiful HTML reports

539 lines • 26.9 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const commander_1 = require("commander"); const chalk_1 = __importDefault(require("chalk")); const figlet_1 = __importDefault(require("figlet")); const ora_1 = __importDefault(require("ora")); const inquirer_1 = __importDefault(require("inquirer")); const fs = __importStar(require("fs-extra")); const enhanced_sql_analyzer_1 = require("../src/enhanced-sql-analyzer"); const openai_1 = __importDefault(require("openai")); const program = new commander_1.Command(); // Enhanced ASCII Art Banner console.log(chalk_1.default.blue(figlet_1.default.textSync('SQL Analyzer', { horizontalLayout: 'fitted' }))); console.log(chalk_1.default.magenta('šŸš€ Enhanced Database Health Analyzer with AI Insights\n')); program .name('sql-analyzer') .description('Enhanced SQL Database Analyzer - Comprehensive health audits with AI-powered insights') .version('1.1.0'); // Global options program .option('-c, --connection <url>', 'Database connection URL') .option('-f, --format <format>', 'Output format (cli, html, json)', 'html') .option('-o, --output <path>', 'Output directory path', './reports') .option('-v, --verbose', 'Verbose output') .option('--no-colors', 'Disable colored output') .option('--config <path>', 'Configuration file path') .option('--preset <preset>', 'Configuration preset (development, production, ci, comprehensive)') .option('--ai', 'Enable AI-powered insights') .option('--openai-key <key>', 'OpenAI API key for AI insights') .option('--openai-model <model>', 'OpenAI model (e.g., gpt-4o, gpt-4o-mini, gpt-4)'); // Health audit command (main feature) program .command('health') .description('Perform comprehensive database health audit') .option('-c, --connection <url>', 'Database connection URL') .option('-f, --format <format>', 'Report format (cli, html, json, md)', 'html') .option('-o, --output <path>', 'Output directory', './reports') .option('--ai', 'Enable AI insights') .option('--openai-key <key>', 'OpenAI API key') .option('--openai-model <model>', 'OpenAI model (e.g., gpt-4o, gpt-4o-mini, gpt-4)') .option('--preset <preset>', 'Configuration preset') .option('--security-level <level>', 'Security analysis level (basic, standard, strict)', 'standard') .option('--fail-on-critical', 'Exit with error code if critical issues found') .option('--min-score <score>', 'Minimum health score required (0-10)', '0') .option('--progress', 'Show progress indicator') .option('--export-sql', 'Export aggregated SQL fix scripts (safe and destructive)') .option('--trend', 'Show deltas vs last run in the CLI summary') .option('--baseline <path>', 'Path to previous JSON report to compare against') .option('--fail-on-regression', 'Exit with error if health score drops or critical issues increase vs baseline') .action(async (options) => { const globalOptions = program.opts(); const connectionUrl = options.connection || globalOptions.connection || process.env.DATABASE_URL; if (!connectionUrl) { console.error(chalk_1.default.red('āŒ Database connection URL is required')); console.log(chalk_1.default.yellow('šŸ’” Use -c option or set DATABASE_URL environment variable')); process.exit(1); } const format = options.format || globalOptions.format || 'html'; const outputPath = options.output || globalOptions.output || './reports'; const enableAI = options.ai || globalOptions.ai || false; const openaiKey = options.openaiKey || globalOptions.openaiKey || process.env.OPENAI_API_KEY; const openaiModel = options.openaiModel || globalOptions.openaiModel || process.env.OPENAI_MODEL; let spinner; if (!options.progress) { spinner = (0, ora_1.default)('šŸ” Starting comprehensive database health audit...').start(); } try { // Create analyzer with configuration const analyzer = new enhanced_sql_analyzer_1.EnhancedSQLAnalyzer({ connectionString: connectionUrl }, { format: format, outputPath, includeAI: enableAI, preset: options.preset ? options.preset : 'production', customConfig: { ai: { enabled: enableAI, apiKey: openaiKey, model: openaiModel }, analysis: { securityLevel: options.securityLevel || 'standard' }, advanced: { verbose: globalOptions.verbose || false } } }); // Perform analysis with optional progress monitoring const result = await analyzer.analyzeAndReport({ returnReport: Boolean(options.trend), skipSave: false, exportSql: Boolean(options.exportSql), onProgress: options.progress ? (step, progress) => { const bar = 'ā–ˆ'.repeat(Math.floor(progress / 5)) + 'ā–‘'.repeat(20 - Math.floor(progress / 5)); process.stdout.write(`\r${step.padEnd(40)} [${bar}] ${progress}%`); if (progress === 100) console.log(''); // New line when complete } : undefined }); if (spinner) spinner.succeed('āœ… Analysis completed successfully!'); // Display summary const summary = result.summary; console.log('\n' + chalk_1.default.bold.blue('šŸ“Š ANALYSIS SUMMARY')); console.log(chalk_1.default.gray('═'.repeat(60))); // Health score with color coding const scoreColor = summary.overallScore >= 8 ? 'green' : summary.overallScore >= 6 ? 'yellow' : 'red'; console.log(`Overall Health Score: ${chalk_1.default[scoreColor].bold(summary.overallScore.toFixed(1))}/10`); // Issue counts with color coding const criticalColor = summary.criticalIssues === 0 ? 'green' : 'red'; const totalColor = summary.totalIssues === 0 ? 'green' : summary.totalIssues <= 5 ? 'yellow' : 'red'; console.log(`Total Issues: ${chalk_1.default[totalColor](summary.totalIssues)}`); console.log(`Critical Issues: ${chalk_1.default[criticalColor].bold(summary.criticalIssues)}`); // Risk levels with color coding const getRiskColor = (risk) => { switch (risk) { case 'low': return 'green'; case 'medium': return 'yellow'; case 'high': return 'red'; case 'critical': return 'red'; default: return 'white'; } }; console.log(`Security Risk: ${chalk_1.default[getRiskColor(summary.securityRisk)](summary.securityRisk.toUpperCase())}`); console.log(`Performance Risk: ${chalk_1.default[getRiskColor(summary.performanceRisk)](summary.performanceRisk.toUpperCase())}`); console.log(`Overall Risk: ${chalk_1.default[getRiskColor(summary.riskLevel)].bold(summary.riskLevel.toUpperCase())}`); // Cost and time estimates console.log(`Monthly Savings Potential: ${chalk_1.default.green('$' + summary.costSavingsPotential.toFixed(0))}`); console.log(`Implementation Time: ${chalk_1.default.cyan(summary.estimatedImplementationTime)}`); // Optional trend display if (options.trend) { try { const path = await Promise.resolve().then(() => __importStar(require('path'))); const fsnode = await Promise.resolve().then(() => __importStar(require('fs'))); const outputPath = options.output || globalOptions.output || './reports'; const lastSummaryPath = path.join(outputPath, 'last-summary.json'); if (fsnode.existsSync(lastSummaryPath)) { const current = JSON.parse(fsnode.readFileSync(lastSummaryPath, 'utf-8')); // Attempt to read previous-prev to compute deltas of deltas is heavy; instead attach trend from report file if available const trend = result?.report?.__trend; if (trend) { console.log('\n' + chalk_1.default.bold.magenta('šŸ“ˆ TRENDS SINCE LAST RUN')); console.log(chalk_1.default.gray('─'.repeat(60))); const fmtDelta = (n) => (n > 0 ? chalk_1.default.red(`+${n}`) : n < 0 ? chalk_1.default.green(`${n}`) : chalk_1.default.gray('0')); if (typeof trend.overallDelta === 'number') console.log(`Health Score Ī”: ${fmtDelta(trend.overallDelta)}`); if (typeof trend.totalIssuesDelta === 'number') console.log(`Total Issues Ī”: ${fmtDelta(trend.totalIssuesDelta)}`); if (typeof trend.criticalIssuesDelta === 'number') console.log(`Critical Issues Ī”: ${fmtDelta(trend.criticalIssuesDelta)}`); if (typeof trend.securityDelta === 'number') console.log(`Security Issues Ī”: ${fmtDelta(trend.securityDelta)}`); if (typeof trend.missingIdxDelta === 'number') console.log(`Missing Indexes Ī”: ${fmtDelta(trend.missingIdxDelta)}`); if (typeof trend.bloatDelta === 'number') console.log(`Bloated Tables Ī”: ${fmtDelta(trend.bloatDelta)}`); } else { console.log(chalk_1.default.gray('\n(no previous run to compare)')); } } else { console.log(chalk_1.default.gray('\nNo previous summary found to compute trends.')); } } catch { // best-effort; ignore } } // Baseline comparison if (options.baseline) { try { const fsnode = await Promise.resolve().then(() => __importStar(require('fs'))); const path = await Promise.resolve().then(() => __importStar(require('path'))); const baselinePath = path.isAbsolute(options.baseline) ? options.baseline : path.join(process.cwd(), options.baseline); const raw = fsnode.readFileSync(baselinePath, 'utf-8'); const baseline = JSON.parse(raw); // Compute baseline summary (best-effort) const getSafe = (obj, path, def) => path.reduce((a, k) => (a && a[k] != null ? a[k] : undefined), obj) ?? def; const baseOverall = getSafe(baseline, ['schemaHealth', 'overall'], 0); const baseVulns = Array.isArray(getSafe(baseline, ['securityAnalysis', 'vulnerabilities'], [])) ? baseline.securityAnalysis.vulnerabilities : []; const basePerf = Array.isArray(getSafe(baseline, ['performanceIssues'], [])) ? baseline.performanceIssues : []; const baseTablesNoPk = Array.isArray(getSafe(baseline, ['tableAnalysis', 'tablesWithoutPK'], [])) ? baseline.tableAnalysis.tablesWithoutPK : []; const baseTablesBloat = Array.isArray(getSafe(baseline, ['tableAnalysis', 'tablesWithBloat'], [])) ? baseline.tableAnalysis.tablesWithBloat : []; const baseCritical = baseVulns.filter((v) => v.severity === 'critical').length + basePerf.filter((p) => p.severity === 'critical').length; const baseTotal = baseVulns.length + basePerf.length + baseTablesNoPk.length + baseTablesBloat.length; const healthDelta = Number((summary.overallScore - baseOverall).toFixed(1)); const totalIssuesDelta = summary.totalIssues - baseTotal; const criticalIssuesDelta = summary.criticalIssues - baseCritical; console.log('\n' + chalk_1.default.bold.magenta('šŸ†š BASELINE COMPARISON')); console.log(chalk_1.default.gray('─'.repeat(60))); const fmtDelta = (n) => (n > 0 ? chalk_1.default.red(`+${n}`) : n < 0 ? chalk_1.default.green(`${n}`) : chalk_1.default.gray('0')); console.log(`Health Score Ī”: ${fmtDelta(healthDelta)}`); console.log(`Total Issues Ī”: ${fmtDelta(totalIssuesDelta)}`); console.log(`Critical Issues Ī”: ${fmtDelta(criticalIssuesDelta)}`); if (options.failOnRegression) { if (healthDelta < 0 || criticalIssuesDelta > 0) { console.log(chalk_1.default.red('\nāŒ Regression detected vs baseline')); process.exit(1); } } } catch (e) { console.log(chalk_1.default.yellow(`\nāš ļø Baseline comparison failed: ${e?.message || e}`)); } } // Top recommendations if (summary.topRecommendations.length > 0) { console.log('\n' + chalk_1.default.bold.yellow('šŸŽÆ TOP RECOMMENDATIONS')); console.log(chalk_1.default.gray('─'.repeat(60))); summary.topRecommendations.slice(0, 5).forEach((rec, index) => { console.log(`${chalk_1.default.cyan(index + 1)}. ${rec}`); }); } console.log(`\nšŸ“„ Report saved to: ${chalk_1.default.cyan(result.reportPath)}`); // Check quality gates if (options.failOnCritical && summary.criticalIssues > 0) { console.log(chalk_1.default.red(`\nāŒ Failing due to ${summary.criticalIssues} critical issues`)); process.exit(1); } const minScore = parseFloat(options.minScore || '0'); if (minScore > 0 && summary.overallScore < minScore) { console.log(chalk_1.default.red(`\nāŒ Health score ${summary.overallScore} below minimum ${minScore}`)); process.exit(1); } console.log(chalk_1.default.green('\nšŸŽ‰ Database health audit completed successfully!')); } catch (error) { if (spinner) spinner.fail('āŒ Analysis failed'); console.error(chalk_1.default.red('\nšŸ’„ Error:'), error.message); // Provide helpful error messages if (error.message.includes('ECONNREFUSED')) { console.log(chalk_1.default.yellow('\nšŸ’” Database connection failed. Please check:')); console.log(' • Database server is running'); console.log(' • Connection URL is correct'); console.log(' • Network connectivity'); } if (error.message.includes('authentication failed')) { console.log(chalk_1.default.yellow('\nšŸ’” Authentication failed. Please check:')); console.log(' • Username and password are correct'); console.log(' • User has necessary permissions'); } if (error.message.includes('OpenAI API key')) { console.log(chalk_1.default.yellow('\nšŸ’” AI features require OpenAI API key:')); console.log(' • Set OPENAI_API_KEY environment variable'); console.log(' • Or use --openai-key option'); console.log(' • Or disable AI with --no-ai'); } process.exit(1); } }); // Schema analysis command program .command('schema') .description('Analyze database schema health') .option('-c, --connection <url>', 'Database connection URL') .option('-f, --format <format>', 'Output format (cli, json)', 'cli') .option('-t, --tables <tables>', 'Comma-separated list of tables to analyze') .action(async (options) => { // Implementation for schema-specific analysis console.log(chalk_1.default.blue('šŸ” Schema analysis feature coming soon...')); console.log('For now, use the "health" command for comprehensive analysis including schema.'); }); // Performance analysis command program .command('performance') .description('Analyze database performance issues') .option('-c, --connection <url>', 'Database connection URL') .option('-f, --format <format>', 'Output format (cli, json)', 'cli') .option('--slow-queries', 'Analyze slow queries') .option('--bloat', 'Check for table bloat') .action(async (options) => { console.log(chalk_1.default.blue('šŸ” Performance analysis feature coming soon...')); console.log('For now, use the "health" command for comprehensive analysis including performance.'); }); // Interactive setup command program .command('setup') .description('Interactive setup wizard') .action(async () => { console.log(chalk_1.default.blue('šŸ› ļø Interactive Setup Wizard\n')); const answers = await inquirer_1.default.prompt([ { type: 'list', name: 'analysisType', message: 'What would you like to analyze?', choices: [ { name: 'Full Health Audit (recommended)', value: 'health' }, { name: 'Schema Health (coming soon)', value: 'schema' }, { name: 'Performance (coming soon)', value: 'performance' } ], default: 'health' }, { type: 'input', name: 'connectionUrl', message: 'Database connection URL:', default: 'postgresql://postgres:password@localhost:5432/mydb' }, { type: 'list', name: 'format', message: 'Preferred report format:', choices: ['html', 'cli', 'json'], default: 'html' }, { type: 'input', name: 'outputPath', message: 'Output directory:', default: './reports' }, { type: 'confirm', name: 'enableAI', message: 'Enable AI-powered insights?', default: false }, { type: 'input', name: 'openaiKey', message: 'OpenAI API key (optional):', when: (answers) => answers.enableAI }, { type: 'confirm', name: 'validateKey', message: 'Validate key and fetch available models?', default: true, when: (answers) => answers.enableAI && !!answers.openaiKey }, { type: 'list', name: 'openaiModel', message: 'OpenAI model:', choices: async (answers) => { const fallback = ['gpt-4o', 'gpt-4o-mini', 'gpt-4']; if (!answers.enableAI || !answers.openaiKey) return fallback; if (answers.validateKey === false) return fallback; try { const client = new openai_1.default({ apiKey: answers.openaiKey }); const models = await client.models.list(); const names = models.data.map(m => m.id).filter(id => /gpt-4|gpt-4o|mini|gpt-3\.5/i.test(id)); return Array.from(new Set([...names, ...fallback])); } catch (e) { console.log(chalk_1.default.yellow(`āš ļø Could not fetch models (${e?.message || 'unknown error'}). Falling back to defaults.`)); return fallback; } }, default: 'gpt-4o', when: (answers) => answers.enableAI }, { type: 'number', name: 'temperature', message: 'Creativity (temperature 0.0 - 1.0):', default: 0.2, when: (answers) => answers.enableAI }, { type: 'list', name: 'securityLevel', message: 'Security analysis level:', choices: ['basic', 'standard', 'strict'], default: 'standard' } ]); // Create configuration file const config = { database: { connectionString: answers.connectionUrl }, ai: { enabled: answers.enableAI, apiKey: answers.openaiKey || '', model: answers.openaiModel || undefined, temperature: typeof answers.temperature === 'number' ? answers.temperature : undefined }, analysis: { securityLevel: answers.securityLevel }, reporting: { format: answers.format, outputPath: answers.outputPath } }; const configPath = './sql-analyzer.config.json'; await fs.writeFile(configPath, JSON.stringify(config, null, 2)); console.log(chalk_1.default.green(`\nāœ… Configuration saved to ${configPath}`)); const { runNow } = await inquirer_1.default.prompt([{ type: 'confirm', name: 'runNow', message: 'Run analysis now?', default: true }]); if (runNow) { const spinner = (0, ora_1.default)('šŸ” Running analysis...').start(); try { const analyzer = new enhanced_sql_analyzer_1.EnhancedSQLAnalyzer({ connectionString: answers.connectionUrl }, { format: answers.format, outputPath: answers.outputPath, includeAI: answers.enableAI, preset: 'production', customConfig: { ai: { enabled: answers.enableAI, apiKey: answers.openaiKey, model: answers.openaiModel, temperature: answers.temperature }, analysis: { securityLevel: answers.securityLevel } } }); const result = await analyzer.analyzeAndReport({}); spinner.succeed('āœ… Analysis completed'); console.log(chalk_1.default.cyan(`šŸ“„ Report saved to: ${result.reportPath}`)); } catch (err) { spinner.fail('āŒ Analysis failed'); console.error(chalk_1.default.red(err?.message || err)); } } else { console.log(chalk_1.default.cyan('\nšŸš€ You can now run: sql-analyzer health')); } }); // Config command program .command('config') .description('Manage configuration') .option('--init', 'Initialize default configuration') .option('--validate', 'Validate current configuration') .option('--show', 'Show current configuration') .action(async (options) => { if (options.init) { const configPath = './sql-analyzer.config.json'; const defaultConfig = { database: { host: 'localhost', port: 5432, database: 'your_database', user: 'postgres', password: 'your_password' }, analysis: { securityLevel: 'standard' }, reporting: { format: 'html', outputPath: './reports' } }; await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2)); console.log(chalk_1.default.green(`āœ… Default configuration created: ${configPath}`)); } if (options.validate) { try { const configManager = enhanced_sql_analyzer_1.ConfigManager.fromEnvironment(); const validation = configManager.validateConfig(); if (validation.valid) { console.log(chalk_1.default.green('āœ… Configuration is valid')); } else { console.log(chalk_1.default.red('āŒ Configuration errors:')); validation.errors.forEach(error => { console.log(chalk_1.default.red(` • ${error}`)); }); } } catch (error) { console.error(chalk_1.default.red('āŒ Configuration validation failed:'), error.message); } } if (options.show) { try { const configManager = enhanced_sql_analyzer_1.ConfigManager.fromEnvironment(); const config = configManager.getConfig(); console.log(chalk_1.default.blue('šŸ“‹ Current Configuration:')); console.log(JSON.stringify(config, null, 2)); } catch (error) { console.error(chalk_1.default.red('āŒ Failed to load configuration:'), error.message); } } }); // Examples command program .command('examples') .description('Show usage examples') .action(() => { console.log(chalk_1.default.bold.blue('šŸ’” Usage Examples\n')); console.log(chalk_1.default.yellow('Basic health audit:')); console.log(' sql-analyzer health -c "postgresql://user:pass@localhost/db"\n'); console.log(chalk_1.default.yellow('HTML report with AI insights:')); console.log(' sql-analyzer health -c "postgresql://user:pass@localhost/db" --ai --format html\n'); console.log(chalk_1.default.yellow('CI/CD integration:')); console.log(' sql-analyzer health -c "$DATABASE_URL" --format json --fail-on-critical\n'); console.log(chalk_1.default.yellow('Interactive setup:')); console.log(' sql-analyzer setup\n'); console.log(chalk_1.default.yellow('Configuration management:')); console.log(' sql-analyzer config --init'); console.log(' sql-analyzer config --validate\n'); console.log(chalk_1.default.gray('For more information, visit: https://github.com/vasoyaprince14/sql-optimizer')); }); // Error handling program.on('command:*', () => { console.error(chalk_1.default.red('āŒ Invalid command. Use --help for available commands.')); process.exit(1); }); // Parse CLI arguments or run setup when no args if (process.argv.slice(2).length === 0) { // Redirect to setup command for a guided experience program.parse(['node', 'sql-analyzer', 'setup']); } else { program.parse(); } //# sourceMappingURL=enhanced-cli.js.map