slice 1a-1d: DB + CacheRepository + YFinance adapter + AdapterQueue
Node 26 + node:sqlite backend (zero native deps; runtime glue adapted from Bun-spec design, deep-module architecture unchanged). 29 tests green across schema/CacheRepository/YFinance-parse/AdapterQueue-dedupe.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// 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();
|
||||
}
|
||||
Reference in New Issue
Block a user