/** * Database connection and initialization for MosaicIQ */ import Database from "better-sqlite3"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { existsSync, mkdirSync } from "node:fs"; import { SCHEMA_VERSION, SQL_SCHEMA, DEFAULT_PORTFOLIO_ID } from "./schema.js"; export type Db = Database.Database; export interface DatabaseConfig { path?: string; inMemory?: boolean; } /** * Get the default data directory for the platform */ export function getDataDir(): string { const platform = process.platform; const base = platform === "darwin" ? `${process.env.HOME}/Documents/MosaicIQ` : platform === "win32" ? `${process.env.LOCALAPPDATA}\\MosaicIQ` : `${process.env.HOME}/.local/share/mosaiciq`; if (!existsSync(base)) { mkdirSync(base, { recursive: true }); } return base; } /** * Get the default database path */ export function getDefaultDbPath(): string { return `${getDataDir()}/mosaiciq.db`; } /** * Initialize the database with schema */ export function initDatabase(config: DatabaseConfig = {}): Db { const dbPath = config.inMemory ? ":memory:" : (config.path ?? getDefaultDbPath()); if (!config.inMemory && dbPath !== ":memory:") { const dbDir = dirname(dbPath); if (!existsSync(dbDir)) { mkdirSync(dbDir, { recursive: true }); } } const db = new Database(dbPath); // Enable WAL mode for better concurrent access db.pragma("journal_mode = WAL"); // Enable foreign keys db.pragma("foreign_keys = ON"); // Check if schema exists const hasMeta = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = '_meta'").get(); const version = hasMeta ? db.prepare("SELECT value FROM _meta WHERE key = 'schema_version'").get() as { value: string } | undefined : undefined; if (!version) { // Fresh database - create schema db.exec(SQL_SCHEMA); // Set schema version db.prepare("INSERT INTO _meta (key, value) VALUES ('schema_version', ?)").run(String(SCHEMA_VERSION)); // Create default portfolio db.prepare(` INSERT INTO portfolios (id, name) VALUES (?, ?) `).run(DEFAULT_PORTFOLIO_ID, "Core Retail Coverage"); console.log(`[DB] Initialized database at ${dbPath}`); } else { const currentVersion = Number(version.value); if (currentVersion !== SCHEMA_VERSION) { console.warn(`[DB] Schema version mismatch: expected ${SCHEMA_VERSION}, got ${currentVersion}`); // TODO: Run migrations } } return db; } /** * Close the database connection */ export function closeDatabase(db: Db): void { db.close(); } /** * Get a database connection (singleton pattern) */ let dbInstance: Db | null = null; export function getDatabase(config?: DatabaseConfig): Db { if (!dbInstance) { dbInstance = initDatabase(config); } return dbInstance; } export function resetDatabase(): void { if (dbInstance) { closeDatabase(dbInstance); dbInstance = null; } }