67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
// Investor Flow — SQLite DB client (node:sqlite). DESIGN.md §1 schema seam.
|
|||
|
|
//
|
||
|
|
// Runtime adaptation (reversible): design specified Bun + bun:sqlite. Implemented on
|
||
|
|
// Node 26 native TS (type-stripping) + node:sqlite (built-in) so the backend has ZERO
|
||
|
|
// native dependencies. Deep-module architecture (CacheRepository, etc.) is unchanged.
|
||
|
|
import { DatabaseSync } from 'node:sqlite';
|
||
|
|
import { readFileSync, mkdirSync } from 'node:fs';
|
||
|
|
import { dirname, join, resolve } from 'node:path';
|
||
|
|
import { fileURLToPath } from 'node:url';
|
||
|
|
|
||
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
|
|
const SCHEMA_PATH = join(__dirname, 'schema.sql');
|
||
|
|
const DEFAULT_DB_DIR = join(__dirname, '..', '..', 'data');
|
||
|
|
const DEFAULT_DB_PATH = join(DEFAULT_DB_DIR, 'investor-flow.db');
|
||
|
|
|
||
|
|
export interface DbOptions {
|
||
|
|
/** SQLite file path. Defaults to $IFLOW_DB_PATH or <server>/data/investor-flow.db. Use ':memory:' for tests. */
|
||
|
|
path?: string;
|
||
|
|
readonly?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Open a SQLite connection with foreign keys + WAL enabled. */
|
||
|
|
export function createDb(opts: DbOptions = {}): DatabaseSync {
|
||
|
|
const path = opts.path ?? process.env.IFLOW_DB_PATH ?? DEFAULT_DB_PATH;
|
||
|
|
if (!opts.readonly && path !== ':memory:') {
|
||
|
|
mkdirSync(dirname(path), { recursive: true });
|
||
|
|
}
|
||
|
|
const database = new DatabaseSync(path, {
|
||
|
|
readOnly: opts.readonly ?? false,
|
||
|
|
enableForeignKeyConstraints: true,
|
||
|
|
});
|
||
|
|
// WAL is persistent on the file; setting per-connection is harmless and ensures it.
|
||
|
|
try {
|
||
|
|
database.exec('PRAGMA journal_mode = WAL;');
|
||
|
|
} catch {
|
||
|
|
/* readOnly or disallowed — ignore */
|
||
|
|
}
|
||
|
|
return database;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Apply the full schema (CREATE TABLE IF NOT EXISTS). Idempotent. */
|
||
|
|
export function initSchema(database: DatabaseSync): void {
|
||
|
|
const sql = readFileSync(SCHEMA_PATH, 'utf8');
|
||
|
|
database.exec(sql);
|
||
|
|
}
|
||
|
|
|
||
|
|
let _db: DatabaseSync | null = null;
|
||
|
|
/** Process-wide singleton connection (auto-creates + inits schema on first call). */
|
||
|
|
export function db(): DatabaseSync {
|
||
|
|
if (!_db) {
|
||
|
|
_db = createDb();
|
||
|
|
initSchema(_db);
|
||
|
|
}
|
||
|
|
return _db;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** CLI entry: `node src/db/client.ts` initializes the on-disk database. */
|
||
|
|
const isMain = process.argv[1] ? resolve(process.argv[1]) === fileURLToPath(import.meta.url) : false;
|
||
|
|
if (isMain) {
|
||
|
|
const d = createDb();
|
||
|
|
initSchema(d);
|
||
|
|
const path = process.env.IFLOW_DB_PATH ?? DEFAULT_DB_PATH;
|
||
|
|
const rows = d.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all() as Array<{ name: string }>;
|
||
|
|
console.log(`[investor-flow] schema initialized at ${path} (${rows.length} tables)`);
|
||
|
|
d.close();
|
||
|
|
}
|