0dac8128bd
- 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
144 lines
4.2 KiB
TypeScript
144 lines
4.2 KiB
TypeScript
/**
|
|
* Database initialization and migration.
|
|
*
|
|
* Handles:
|
|
* - Creating/opening SQLite database
|
|
* - Running DDL schema setup
|
|
* - Migrating legacy JSON files (one-time)
|
|
*/
|
|
|
|
import BetterSqlite3 from 'better-sqlite3';
|
|
import { existsSync, readFileSync, renameSync } from 'fs';
|
|
import { randomUUID } from 'crypto';
|
|
import { DDL } from './queries.constant';
|
|
import { QueryBuilder } from '../utils/QueryBuilder';
|
|
|
|
export type Db = BetterSqlite3.Database;
|
|
|
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
interface LegacyHolding {
|
|
ticker: string;
|
|
shares: number;
|
|
costBasis: number;
|
|
type: string;
|
|
source: string;
|
|
}
|
|
|
|
interface LegacyCall {
|
|
id?: string;
|
|
title: string;
|
|
quarter: string;
|
|
date: string;
|
|
thesis: string;
|
|
tickers: string[];
|
|
snapshot: Record<string, unknown>;
|
|
createdAt: string;
|
|
}
|
|
|
|
// ── Main Export ──────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Initialize and open the SQLite database.
|
|
*
|
|
* Steps:
|
|
* 1. Create/open database file
|
|
* 2. Enable WAL mode (concurrent read safety)
|
|
* 3. Enable foreign keys
|
|
* 4. Run DDL (create tables if missing)
|
|
* 5. Migrate legacy JSON files (one-time)
|
|
*
|
|
* @param path Path to database file (default: ./market-screener.db)
|
|
* @returns Opened database instance (wrap in DatabaseConnection for safe access)
|
|
*/
|
|
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;
|
|
}
|
|
|
|
// ── Migration Helpers ────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Migrate legacy JSON files to SQLite (one-time, non-fatal).
|
|
* Called automatically during database initialization.
|
|
*/
|
|
function migrateJson(db: Db): void {
|
|
migratePortfolio(db);
|
|
migrateCalls(db);
|
|
}
|
|
|
|
/**
|
|
* Migrate portfolio.json → holdings table.
|
|
* If portfolio.json exists, import all holdings and rename to portfolio.json.migrated.
|
|
* If import fails, leave portfolio.json in place (non-fatal).
|
|
*/
|
|
function migratePortfolio(db: Db): void {
|
|
const src = './portfolio.json';
|
|
if (!existsSync(src)) return;
|
|
|
|
try {
|
|
const { holdings } = JSON.parse(readFileSync(src, 'utf8')) as {
|
|
holdings: LegacyHolding[];
|
|
};
|
|
|
|
const insertAll = db.transaction((rows: LegacyHolding[]) => {
|
|
for (const h of rows) {
|
|
const qb = new QueryBuilder('MIGRATION_QUERIES.HOLDINGS_INSERT_OR_IGNORE', [
|
|
h.ticker.toUpperCase(),
|
|
h.shares,
|
|
h.costBasis ?? 0,
|
|
h.type ?? 'stock',
|
|
h.source ?? 'Manual',
|
|
]);
|
|
db.prepare(qb.sql).run(...qb.queryParams);
|
|
}
|
|
});
|
|
|
|
insertAll(holdings);
|
|
renameSync(src, `${src}.migrated`);
|
|
} catch {
|
|
// Non-fatal: leave portfolio.json in place if migration fails
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrate market-calls.json → market_calls table.
|
|
* If market-calls.json exists, import all calls and rename to market-calls.json.migrated.
|
|
* If import fails, leave market-calls.json in place (non-fatal).
|
|
*/
|
|
function migrateCalls(db: Db): void {
|
|
const src = './market-calls.json';
|
|
if (!existsSync(src)) return;
|
|
|
|
try {
|
|
const { calls } = JSON.parse(readFileSync(src, 'utf8')) as {
|
|
calls: LegacyCall[];
|
|
};
|
|
|
|
const insertAll = db.transaction((rows: LegacyCall[]) => {
|
|
for (const c of rows) {
|
|
const qb = new QueryBuilder('MIGRATION_QUERIES.MARKET_CALLS_INSERT_OR_IGNORE', [
|
|
c.id ?? randomUUID(),
|
|
c.title,
|
|
c.quarter,
|
|
c.date,
|
|
c.thesis,
|
|
JSON.stringify(c.tickers ?? []),
|
|
JSON.stringify(c.snapshot ?? {}),
|
|
c.createdAt,
|
|
]);
|
|
db.prepare(qb.sql).run(...qb.queryParams);
|
|
}
|
|
});
|
|
|
|
insertAll(calls);
|
|
renameSync(src, `${src}.migrated`);
|
|
} catch {
|
|
// Non-fatal: leave market-calls.json in place if migration fails
|
|
}
|
|
}
|