UNPKG

@invisiblecities/sidequest-cqo

Version:

Configuration-agnostic TypeScript and ESLint orchestrator with real-time watch mode, SQLite persistence, and intelligent terminal detection

447 lines (431 loc) 17.7 kB
/** * Database connection and initialization for Code Quality Orchestrator * Handles SQLite setup, migrations, and Kysely configuration */ import { Kysely, SqliteDialect, FileMigrationProvider, Migrator, sql, } from "kysely"; import Database from "better-sqlite3"; import * as path from "node:path"; import * as fs from "node:fs/promises"; import { fileURLToPath } from "node:url"; // ES module equivalent of __dirname const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // ============================================================================ // Database Connection Management // ============================================================================ export class DatabaseConnection { static instance = undefined; static config = undefined; static sqliteDatabase = undefined; /** * Initialize database connection with configuration */ static async initialize(config) { if (this.instance) { return this.instance; } this.config = config; // Ensure database directory exists const databaseDirectory = path.dirname(config.path); await fs.mkdir(databaseDirectory, { recursive: true }); // Create SQLite database instance const database = new Database(config.path); this.sqliteDatabase = database; // Configure SQLite pragmas for performance const defaultPragmas = { journal_mode: "WAL", // Write-Ahead Logging for better concurrency synchronous: "NORMAL", // Good balance of safety and performance cache_size: -64_000, // 64MB cache foreign_keys: "ON", // Enable foreign key constraints temp_store: "memory", // Store temp tables in memory mmap_size: 134_217_728, // 128MB memory map ...config.pragmas, }; // Apply pragmas for (const [key, value] of Object.entries(defaultPragmas)) { database.pragma(`${key} = ${value}`); } // Create Kysely instance this.instance = new Kysely({ dialect: new SqliteDialect({ database, }), }); console.log(`[Database] Connected to SQLite database: ${config.path}`); // Run migrations if enabled await (config.migrations?.enabled ? this.runMigrations() : this.initializeSchema()); return this.instance; } /** * Get existing database connection */ static getInstance() { if (!this.instance) { throw new Error("Database not initialized. Call DatabaseConnection.initialize() first."); } return this.instance; } /** * Close database connection */ static async close() { if (this.instance) { await this.instance.destroy(); this.instance = undefined; this.config = undefined; console.log("[Database] Connection closed"); } } /** * Run database migrations */ static async runMigrations() { if (!this.instance || !this.config?.migrations?.path) { throw new Error("Database or migration path not configured"); } const migrationProvider = new FileMigrationProvider({ fs: await import("node:fs/promises"), path: await import("node:path"), migrationFolder: this.config.migrations.path, }); const migrator = new Migrator({ db: this.instance, provider: migrationProvider, }); const { error, results } = await migrator.migrateToLatest(); if (error) { console.error("[Database] Migration failed:", error); throw error; } if (results) { results.forEach((result) => { if (result.status === "Success") { console.log(`[Database] Migration "${result.migrationName}" executed successfully`); } else { console.error(`[Database] Migration "${result.migrationName}" failed with status:`, result.status); } }); } console.log("[Database] All migrations completed"); } /** * Initialize schema directly (when migrations are disabled) */ static async initializeSchema() { if (!this.instance) { throw new Error("Database not initialized"); } try { if (!this.sqliteDatabase) { throw new Error("SQLite database instance not available"); } // Check if schema is already initialized const tablesExist = this.sqliteDatabase .prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name='violations'") .get(); if (tablesExist.count > 0) { console.log("[Database] Schema already exists, skipping initialization"); return; } console.log("[Database] Creating schema from inline SQL..."); // Inline schema definition - self-contained, no external files needed const schemaSQL = ` -- Code Quality Orchestrator Database Schema -- SQLite schema for efficient violation tracking, rule scheduling, and historical analysis -- Main violations table - stores current state of all violations CREATE TABLE violations ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_path TEXT NOT NULL, rule_id TEXT NOT NULL, category TEXT NOT NULL, -- e.g., 'record-type', 'code-quality', 'type-alias' severity TEXT NOT NULL, -- 'error', 'warn', 'info' source TEXT NOT NULL, -- 'typescript', 'eslint' message TEXT NOT NULL, line_number INTEGER, column_number INTEGER, code_snippet TEXT, -- Optional code context hash TEXT NOT NULL UNIQUE, -- SHA-256 hash for deduplication: hash(file_path + line + rule_id + message) first_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP, status TEXT DEFAULT 'active' CHECK (status IN ('active', 'resolved', 'ignored')) ); -- Rule execution tracking - stores each time a rule is checked CREATE TABLE rule_checks ( id INTEGER PRIMARY KEY AUTOINCREMENT, rule_id TEXT NOT NULL, engine TEXT NOT NULL, -- 'typescript', 'eslint' started_at DATETIME DEFAULT CURRENT_TIMESTAMP, completed_at DATETIME, status TEXT NOT NULL CHECK (status IN ('running', 'completed', 'failed', 'timeout')), violations_found INTEGER DEFAULT 0, execution_time_ms INTEGER, error_message TEXT, -- If status is 'failed' files_checked INTEGER DEFAULT 0, files_with_violations INTEGER DEFAULT 0 ); -- Historical tracking for violation deltas over time CREATE TABLE violation_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, check_id INTEGER NOT NULL REFERENCES rule_checks(id) ON DELETE CASCADE, violation_hash TEXT NOT NULL, action TEXT NOT NULL CHECK (action IN ('added', 'removed', 'modified', 'unchanged')), previous_line INTEGER, -- For 'modified' actions previous_message TEXT, -- For 'modified' actions recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Client TypeScript configuration cache CREATE TABLE typescript_configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_path TEXT NOT NULL, config_path TEXT NOT NULL UNIQUE, strict_mode BOOLEAN DEFAULT FALSE, exact_optional_properties BOOLEAN DEFAULT FALSE, no_unchecked_indexed_access BOOLEAN DEFAULT FALSE, no_implicit_any BOOLEAN DEFAULT FALSE, target TEXT DEFAULT 'ES5', module_system TEXT DEFAULT 'CommonJS', config_hash TEXT NOT NULL, first_scanned_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_scanned_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_modified_at DATETIME ); -- Rule scheduling and round-robin state management CREATE TABLE rule_schedules ( id INTEGER PRIMARY KEY AUTOINCREMENT, rule_id TEXT NOT NULL, engine TEXT NOT NULL, enabled BOOLEAN DEFAULT true, priority INTEGER DEFAULT 1, check_frequency_ms INTEGER DEFAULT 30000, last_run_at DATETIME, next_run_at DATETIME, consecutive_zero_count INTEGER DEFAULT 0, avg_execution_time_ms INTEGER DEFAULT 0, avg_violations_found REAL DEFAULT 0.0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(rule_id, engine) ); -- Session tracking for watch mode analytics CREATE TABLE watch_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_start DATETIME DEFAULT CURRENT_TIMESTAMP, session_end DATETIME, total_checks INTEGER DEFAULT 0, total_violations_start INTEGER DEFAULT 0, total_violations_end INTEGER DEFAULT 0, configuration JSON, user_agent TEXT ); -- Performance metrics for optimization CREATE TABLE performance_metrics ( id INTEGER PRIMARY KEY AUTOINCREMENT, metric_type TEXT NOT NULL, metric_value REAL NOT NULL, metric_unit TEXT NOT NULL, context TEXT, recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Primary lookup indexes CREATE INDEX idx_violations_file_path ON violations(file_path); CREATE INDEX idx_violations_rule_id ON violations(rule_id); CREATE INDEX idx_violations_hash ON violations(hash); CREATE INDEX idx_violations_status ON violations(status); CREATE INDEX idx_violations_last_seen ON violations(last_seen_at); -- Composite indexes for common queries CREATE INDEX idx_violations_active_by_category ON violations(category, severity) WHERE status = 'active'; CREATE INDEX idx_violations_active_by_source ON violations(source, category) WHERE status = 'active'; CREATE INDEX idx_violations_file_rule ON violations(file_path, rule_id); -- Rule check indexes CREATE INDEX idx_rule_checks_rule_id ON rule_checks(rule_id); CREATE INDEX idx_rule_checks_started_at ON rule_checks(started_at); CREATE INDEX idx_rule_checks_status ON rule_checks(status); CREATE INDEX idx_rule_checks_engine ON rule_checks(engine); -- History tracking indexes CREATE INDEX idx_violation_history_check_id ON violation_history(check_id); CREATE INDEX idx_violation_history_hash ON violation_history(violation_hash); CREATE INDEX idx_violation_history_action ON violation_history(action); CREATE INDEX idx_violation_history_recorded_at ON violation_history(recorded_at); -- Scheduling indexes CREATE INDEX idx_rule_schedules_next_run ON rule_schedules(next_run_at) WHERE enabled = true; CREATE INDEX idx_rule_schedules_engine ON rule_schedules(engine); CREATE INDEX idx_rule_schedules_priority ON rule_schedules(priority, next_run_at); -- Session tracking indexes CREATE INDEX idx_watch_sessions_start ON watch_sessions(session_start); CREATE INDEX idx_performance_metrics_type ON performance_metrics(metric_type, recorded_at); -- Index for fast config lookups CREATE INDEX idx_typescript_configs_path ON typescript_configs(project_path, config_path); -- Triggers for automatic maintenance CREATE TRIGGER update_violation_last_seen AFTER UPDATE ON violations WHEN NEW.status = 'active' AND OLD.status = 'active' BEGIN UPDATE violations SET last_seen_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; -- Views for common queries CREATE VIEW violation_summary AS SELECT category, source, severity, COUNT(*) as count, COUNT(DISTINCT file_path) as affected_files, MIN(first_seen_at) as first_occurrence, MAX(last_seen_at) as last_occurrence FROM violations WHERE status = 'active' GROUP BY category, source, severity ORDER BY count DESC; `; // Use better-sqlite3's exec method which can handle multiple statements this.sqliteDatabase.exec(schemaSQL); console.log("[Database] Schema initialized successfully"); } catch (error) { console.error("[Database] Schema initialization failed:", error); throw error; } } /** * Health check - verify database is accessible and properly initialized */ static async healthCheck() { try { if (!this.instance) { return false; } // Test basic query await this.instance .selectFrom("violations") .select("id") .limit(1) .execute(); return true; } catch (error) { console.error("[Database] Health check failed:", error); return false; } } /** * Get database statistics for monitoring */ static async getStats() { if (!this.instance || !this.config) { throw new Error("Database not initialized"); } const database = this.instance; // Get table counts const [violationsResult, ruleChecksResult] = await Promise.all([ database .selectFrom("violations") .select((eb) => eb.fn.count("id").as("count")) .executeTakeFirst(), database .selectFrom("rule_checks") .select((eb) => eb.fn.count("id").as("count")) .executeTakeFirst(), ]); // Get file sizes let databaseSizeMb = 0; let walSizeMb = 0; try { const databaseStats = await fs.stat(this.config.path); databaseSizeMb = databaseStats.size / (1024 * 1024); const walPath = `${this.config.path}-wal`; try { const walStats = await fs.stat(walPath); walSizeMb = walStats.size / (1024 * 1024); } catch { // WAL file might not exist } } catch (error) { console.warn("[Database] Could not get file size stats:", error); } return { violations_count: Number(violationsResult?.count || 0), rule_checks_count: Number(ruleChecksResult?.count || 0), database_size_mb: Math.round(databaseSizeMb * 100) / 100, wal_size_mb: Math.round(walSizeMb * 100) / 100, }; } /** * Vacuum database to reclaim space and optimize performance */ static async vacuum() { if (!this.instance) { throw new Error("Database not initialized"); } console.log("[Database] Starting VACUUM operation..."); const startTime = Date.now(); await sql `VACUUM`.execute(this.instance); const duration = Date.now() - startTime; console.log(`[Database] VACUUM completed in ${duration}ms`); } /** * Analyze database to update query planner statistics */ static async analyze() { if (!this.instance) { throw new Error("Database not initialized"); } console.log("[Database] Running ANALYZE..."); await sql `ANALYZE`.execute(this.instance); console.log("[Database] ANALYZE completed"); } } // ============================================================================ // Database Configuration Helpers // ============================================================================ /** * Create default database configuration */ function createDefaultDatabaseConfig(databasePath) { const defaultPath = databasePath || path.join(process.cwd(), "data", "code-quality.db"); return { path: defaultPath, enableWAL: true, pragmas: { journal_mode: "WAL", synchronous: "NORMAL", cache_size: -64_000, foreign_keys: "ON", temp_store: "memory", mmap_size: 134_217_728, }, migrations: { enabled: false, // Use direct schema initialization for now path: path.join(__dirname, "migrations"), }, }; } /** * Initialize database with default configuration */ export async function initializeDatabase(config) { const fullConfig = { ...createDefaultDatabaseConfig(), ...config, }; return await DatabaseConnection.initialize(fullConfig); } /** * Get database connection instance */ export function getDatabase() { return DatabaseConnection.getInstance(); } /** * Close database connection */ export async function closeDatabase() { await DatabaseConnection.close(); } //# sourceMappingURL=connection.js.map