@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
JavaScript
;
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