Files
MosaicIQ/packages/shared/src/db/database.ts

120 lines
2.9 KiB
TypeScript

/**
* 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;
}
}