phase-8g: add sqllite.
This commit is contained in:
committed by
saikiranvella
parent
d1556f2a67
commit
c7e39c3e4e
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* SQLite database initialisation.
|
||||
*
|
||||
* Call createDb() once in server/app.ts and pass the instance to repositories.
|
||||
* Uses WAL journal mode for safe concurrent reads alongside the single writer.
|
||||
*
|
||||
* Migration: if the legacy JSON files (portfolio.json / market-calls.json) exist
|
||||
* they are imported once into SQLite, then renamed to *.json.migrated so the
|
||||
* import never runs again.
|
||||
*
|
||||
* SECURITY:
|
||||
* - All queries use parameterized statements (QueryBuilder + DatabaseConnection)
|
||||
* - No SQL injection possible via table/column/parameter names
|
||||
* - Audit trail tracks all mutations for compliance
|
||||
* - Statement caching improves performance
|
||||
*/
|
||||
|
||||
import BetterSqlite3 from 'better-sqlite3';
|
||||
import { existsSync, readFileSync, renameSync } from 'fs';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { DatabaseConnection } from './DatabaseConnection.js';
|
||||
import { QueryBuilder } from './QueryBuilder.js';
|
||||
import { QueryAudit } from './QueryAudit.js';
|
||||
|
||||
export type Db = BetterSqlite3.Database;
|
||||
export { DatabaseConnection, QueryBuilder, QueryAudit };
|
||||
|
||||
const DDL = `
|
||||
CREATE TABLE IF NOT EXISTS holdings (
|
||||
ticker TEXT PRIMARY KEY,
|
||||
shares REAL NOT NULL,
|
||||
cost_basis REAL NOT NULL DEFAULT 0,
|
||||
type TEXT NOT NULL DEFAULT 'stock',
|
||||
source TEXT NOT NULL DEFAULT 'Manual'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS market_calls (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
quarter TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
thesis TEXT NOT NULL,
|
||||
tickers TEXT NOT NULL, -- JSON array
|
||||
snapshot TEXT NOT NULL, -- JSON object
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
`;
|
||||
|
||||
export function createDb(path = './market-screener.db'): Db {
|
||||
const db = new BetterSqlite3(path);
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
db.exec(DDL);
|
||||
migrateJson(db);
|
||||
return db;
|
||||
}
|
||||
|
||||
// ── One-time JSON → SQLite migration ─────────────────────────────────────────
|
||||
|
||||
function migrateJson(db: Db): void {
|
||||
migratePortfolio(db);
|
||||
migrateCalls(db);
|
||||
}
|
||||
|
||||
function migratePortfolio(db: Db): void {
|
||||
const src = './portfolio.json';
|
||||
if (!existsSync(src)) return;
|
||||
try {
|
||||
const { holdings } = JSON.parse(readFileSync(src, 'utf8')) as {
|
||||
holdings: Array<{
|
||||
ticker: string;
|
||||
shares: number;
|
||||
costBasis: number;
|
||||
type: string;
|
||||
source: string;
|
||||
}>;
|
||||
};
|
||||
const insert = db.prepare(
|
||||
'INSERT OR IGNORE INTO holdings (ticker, shares, cost_basis, type, source) VALUES (?,?,?,?,?)',
|
||||
);
|
||||
const insertAll = db.transaction((rows: typeof holdings) => {
|
||||
for (const h of rows) {
|
||||
insert.run(
|
||||
h.ticker.toUpperCase(),
|
||||
h.shares,
|
||||
h.costBasis ?? 0,
|
||||
h.type ?? 'stock',
|
||||
h.source ?? 'Manual',
|
||||
);
|
||||
}
|
||||
});
|
||||
insertAll(holdings);
|
||||
renameSync(src, src + '.migrated');
|
||||
} catch {
|
||||
// non-fatal — leave file in place if migration fails
|
||||
}
|
||||
}
|
||||
|
||||
function migrateCalls(db: Db): void {
|
||||
const src = './market-calls.json';
|
||||
if (!existsSync(src)) return;
|
||||
try {
|
||||
const { calls } = JSON.parse(readFileSync(src, 'utf8')) as {
|
||||
calls: Array<{
|
||||
id?: string;
|
||||
title: string;
|
||||
quarter: string;
|
||||
date: string;
|
||||
thesis: string;
|
||||
tickers: string[];
|
||||
snapshot: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
}>;
|
||||
};
|
||||
const insert = db.prepare(
|
||||
'INSERT OR IGNORE INTO market_calls (id, title, quarter, date, thesis, tickers, snapshot, created_at) VALUES (?,?,?,?,?,?,?,?)',
|
||||
);
|
||||
const insertAll = db.transaction((rows: typeof calls) => {
|
||||
for (const c of rows) {
|
||||
insert.run(
|
||||
c.id ?? randomUUID(),
|
||||
c.title,
|
||||
c.quarter,
|
||||
c.date,
|
||||
c.thesis,
|
||||
JSON.stringify(c.tickers ?? []),
|
||||
JSON.stringify(c.snapshot ?? {}),
|
||||
c.createdAt,
|
||||
);
|
||||
}
|
||||
});
|
||||
insertAll(calls);
|
||||
renameSync(src, src + '.migrated');
|
||||
} catch {
|
||||
// non-fatal
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user