120 lines
2.9 KiB
TypeScript
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;
|
|
}
|
|
}
|