/** * Query audit logging — tracks all database mutations. * * Usage: * const audit = new QueryAudit(); * audit.logQuery('SELECT * FROM holdings', [], 'READ'); * audit.logQuery('UPDATE holdings SET shares = ? WHERE ticker = ?', [100, 'AAPL'], 'WRITE'); * * Provides: * - Audit trail of all queries executed * - Timing information (for performance monitoring) * - Clear distinction between READ/WRITE operations * - Optional persistent storage for compliance */ export enum AuditAction { READ = 'READ', WRITE = 'WRITE', DELETE = 'DELETE', } export interface AuditEntry { timestamp: string; // ISO 8601 action: AuditAction; sql: string; params: unknown[]; durationMs: number; rowsAffected?: number; error?: string; } /** * QueryAudit — in-memory audit trail with optional callbacks. */ export class QueryAudit { private entries: AuditEntry[] = []; private onLog?: (entry: AuditEntry) => void | Promise; constructor(onLog?: (entry: AuditEntry) => void | Promise) { this.onLog = onLog; } /** * Log a query execution. * @param sql The SQL string (with ? placeholders intact) * @param params The parameter array (safe to log; no raw values in SQL) * @param action The operation type (READ, WRITE, DELETE) * @param durationMs Execution time in milliseconds * @param rowsAffected Number of rows affected (for INSERT/UPDATE/DELETE) * @param error If execution failed, the error message */ log( sql: string, params: unknown[], action: AuditAction, durationMs: number, rowsAffected?: number, error?: string, ): void { const entry: AuditEntry = { timestamp: new Date().toISOString(), action, sql, params, durationMs, rowsAffected, error, }; this.entries.push(entry); // Call the optional callback (could write to file, logger, or remote service) if (this.onLog) { const result = this.onLog(entry); if (result instanceof Promise) { result.catch((err) => { console.error('QueryAudit callback failed:', err); }); } } } /** * Get all audit entries. */ all(): AuditEntry[] { return [...this.entries]; } /** * Filter audit entries by action type. */ byAction(action: AuditAction): AuditEntry[] { return this.entries.filter((e) => e.action === action); } /** * Get the most recent N entries. */ recent(count: number = 100): AuditEntry[] { return this.entries.slice(-count); } /** * Clear the audit trail. * (Typically not needed unless for testing or cleanup.) */ clear(): void { this.entries = []; } /** * Generate a human-readable audit report. */ report(limitEntries: number = 100): string { const recent = this.recent(limitEntries); let report = `\n=== Query Audit Report ===\n`; report += `Total entries: ${this.entries.length}\n`; report += `Showing last ${recent.length} entries:\n\n`; for (const entry of recent) { report += `[${entry.timestamp}] ${entry.action}`; if (entry.error) { report += ` ❌ (${entry.error})`; } else { report += ` ✓ (${entry.durationMs}ms)`; if (entry.rowsAffected !== undefined) { report += ` — ${entry.rowsAffected} rows`; } } report += `\n SQL: ${entry.sql}\n`; if (entry.params.length > 0) { report += ` Params: [${entry.params.map((p) => JSON.stringify(p)).join(', ')}]\n`; } report += '\n'; } return report; } }