phase-9: domain-driven architecture complete
- 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
This commit is contained in:
committed by
saikiranvella
parent
83116baa3c
commit
96a752ecf7
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* DatabaseConnection — High-level database abstraction.
|
||||
*
|
||||
* Wraps better-sqlite3 with:
|
||||
* - QueryBuilder for type-safe, injection-proof queries
|
||||
* - QueryAudit for logging and compliance
|
||||
* - Statement caching for performance
|
||||
* - Transaction support
|
||||
*
|
||||
* Usage:
|
||||
* const db = new DatabaseConnection(betterSqlite3Db, options);
|
||||
* const qb = new QueryBuilder('holdings').select(['ticker', 'shares']).where('type = ?', ['stock']);
|
||||
* const rows = db.all(qb);
|
||||
* const row = db.get(qb);
|
||||
* db.run(qb);
|
||||
*/
|
||||
|
||||
import type BetterSqlite3 from 'better-sqlite3';
|
||||
import type { DatabaseOptions } from '../types/index';
|
||||
import { AuditAction } from '../types/index';
|
||||
import { QueryBuilder } from '../utils/QueryBuilder';
|
||||
import { QueryAudit } from './QueryAudit';
|
||||
|
||||
/**
|
||||
* DatabaseConnection — Safe, auditable, performant SQLite wrapper.
|
||||
*/
|
||||
export class DatabaseConnection {
|
||||
private db: BetterSqlite3.Database;
|
||||
private audit: QueryAudit;
|
||||
private logSlowQueries: number;
|
||||
private statementCache = new Map<string, BetterSqlite3.Statement>();
|
||||
|
||||
constructor(db: BetterSqlite3.Database, options: DatabaseOptions = {}) {
|
||||
this.db = db;
|
||||
this.audit = options.audit ?? new QueryAudit();
|
||||
this.logSlowQueries = options.logSlowQueries ?? 100; // 100ms default
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a SELECT query and return all rows.
|
||||
* Logs the query to the audit trail.
|
||||
*/
|
||||
all<T = Record<string, unknown>>(qb: QueryBuilder): T[] {
|
||||
const sql = qb.sql;
|
||||
const params = qb.queryParams;
|
||||
const startMs = performance.now();
|
||||
|
||||
try {
|
||||
const stmt = this.getOrCacheStatement(sql);
|
||||
const rows = stmt.all(...params) as T[];
|
||||
|
||||
const durationMs = performance.now() - startMs;
|
||||
this.audit.log(sql, params, AuditAction.READ, durationMs, rows.length);
|
||||
this.logIfSlow(sql, durationMs);
|
||||
|
||||
return rows;
|
||||
} catch (err) {
|
||||
const durationMs = performance.now() - startMs;
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
this.audit.log(sql, params, AuditAction.READ, durationMs, undefined, errorMsg);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a SELECT query and return the first row only.
|
||||
* Returns null if no rows match.
|
||||
* Logs the query to the audit trail.
|
||||
*/
|
||||
get<T = Record<string, unknown>>(qb: QueryBuilder): T | null {
|
||||
const sql = qb.sql;
|
||||
const params = qb.queryParams;
|
||||
const startMs = performance.now();
|
||||
|
||||
try {
|
||||
const stmt = this.getOrCacheStatement(sql);
|
||||
const row = stmt.get(...params) as T | undefined;
|
||||
|
||||
const durationMs = performance.now() - startMs;
|
||||
this.audit.log(sql, params, AuditAction.READ, durationMs, row ? 1 : 0);
|
||||
this.logIfSlow(sql, durationMs);
|
||||
|
||||
return row ?? null;
|
||||
} catch (err) {
|
||||
const durationMs = performance.now() - startMs;
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
this.audit.log(sql, params, AuditAction.READ, durationMs, undefined, errorMsg);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an INSERT, UPDATE, or DELETE query.
|
||||
* Returns the number of rows affected.
|
||||
* Logs the query to the audit trail.
|
||||
*/
|
||||
run(qb: QueryBuilder): number {
|
||||
const sql = qb.sql;
|
||||
const params = qb.queryParams;
|
||||
const startMs = performance.now();
|
||||
|
||||
// Determine audit action from SQL
|
||||
const sqlUpper = sql.toUpperCase().trim();
|
||||
const action = sqlUpper.startsWith('DELETE')
|
||||
? AuditAction.DELETE
|
||||
: sqlUpper.startsWith('INSERT')
|
||||
? AuditAction.WRITE
|
||||
: AuditAction.WRITE;
|
||||
|
||||
try {
|
||||
const stmt = this.getOrCacheStatement(sql);
|
||||
const result = stmt.run(...params);
|
||||
|
||||
const durationMs = performance.now() - startMs;
|
||||
this.audit.log(sql, params, action, durationMs, result.changes);
|
||||
this.logIfSlow(sql, durationMs);
|
||||
|
||||
return result.changes;
|
||||
} catch (err) {
|
||||
const durationMs = performance.now() - startMs;
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
this.audit.log(sql, params, action, durationMs, 0, errorMsg);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a transaction — multiple queries as an atomic unit.
|
||||
* All queries must succeed, or all are rolled back.
|
||||
*
|
||||
* Usage:
|
||||
* db.transaction(() => {
|
||||
* db.run(qb1);
|
||||
* db.run(qb2);
|
||||
* });
|
||||
*/
|
||||
transaction<T>(fn: () => T): T {
|
||||
const txn = this.db.transaction(fn);
|
||||
return txn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw better-sqlite3 Db instance (for advanced use only).
|
||||
* Prefer the DatabaseConnection methods.
|
||||
*/
|
||||
raw(): BetterSqlite3.Database {
|
||||
return this.db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the audit trail instance.
|
||||
*/
|
||||
getAudit(): QueryAudit {
|
||||
return this.audit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the statement cache (for testing or extreme memory pressure).
|
||||
*/
|
||||
clearStatementCache(): void {
|
||||
this.statementCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the audit trail instance.
|
||||
* Call db.printAudit() to see the most recent 100 queries.
|
||||
*/
|
||||
printAudit(): void {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(this.audit.report());
|
||||
}
|
||||
|
||||
// ── Private helpers ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get or create a cached prepared statement.
|
||||
* Reduces compilation overhead for frequently-run queries.
|
||||
*/
|
||||
private getOrCacheStatement(sql: string): BetterSqlite3.Statement {
|
||||
let stmt = this.statementCache.get(sql);
|
||||
if (!stmt) {
|
||||
stmt = this.db.prepare(sql);
|
||||
this.statementCache.set(sql, stmt);
|
||||
}
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log slow queries to console.
|
||||
*/
|
||||
private logIfSlow(sql: string, durationMs: number): void {
|
||||
if (durationMs > this.logSlowQueries) {
|
||||
console.warn(`[SLOW QUERY] ${durationMs.toFixed(2)}ms\n ${sql}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user