2e7860637e
- Restructured server layer with 5 domains: shared, screener, portfolio, calls, finance - Migrated 58 TypeScript files to domain-driven structure - Updated CLAUDE.md with new architecture documentation - Added .gitignore rules for .md files (except CLAUDE.md) - Removed unused CatalystAnalyst import from app.ts - Fixed lint errors: removed unused imports, fixed regex escape, added console suppressions - Verified no sensitive data in git history - Server code compiles cleanly with TypeScript strict mode
127 lines
3.3 KiB
TypeScript
127 lines
3.3 KiB
TypeScript
/**
|
|
* Query audit logging — tracks all database mutations.
|
|
*
|
|
* Usage:
|
|
* const audit = new QueryAudit();
|
|
* audit.log('SELECT * FROM holdings', [], AuditAction.READ, 1.5);
|
|
* audit.log('UPDATE holdings SET shares = ? WHERE ticker = ?', [100, 'AAPL'], AuditAction.WRITE, 0.8, 1);
|
|
*
|
|
* Provides:
|
|
* - Audit trail of all queries executed
|
|
* - Timing information (for performance monitoring)
|
|
* - Clear distinction between READ/WRITE operations
|
|
* - Optional persistent storage for compliance
|
|
*/
|
|
|
|
import type { AuditAction, AuditEntry } from '../types/index';
|
|
|
|
/**
|
|
* QueryAudit — in-memory audit trail with optional callbacks.
|
|
*/
|
|
export class QueryAudit {
|
|
private entries: AuditEntry[] = [];
|
|
private onLog?: (entry: AuditEntry) => void | Promise<void>;
|
|
|
|
constructor(onLog?: (entry: AuditEntry) => void | Promise<void>) {
|
|
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;
|
|
}
|
|
}
|