import { mkdirSync } from "node:fs"; import { dirname } from "node:path"; import Database from "better-sqlite3"; export type CompanionDatabase = Database.Database; export function openDatabase(path: string): CompanionDatabase { if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true, mode: 0o750 }); const database = new Database(path); database.pragma("journal_mode = WAL"); database.pragma("foreign_keys = ON"); database.pragma("busy_timeout = 5000"); migrate(database); return database; } function migrate(database: CompanionDatabase): void { database.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE COLLATE NOCASE, password_hash TEXT NOT NULL, role TEXT NOT NULL CHECK (role = 'admin'), created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS sessions ( id_hash TEXT PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS sessions_expiry_idx ON sessions(expires_at); CREATE TABLE IF NOT EXISTS audit_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, action TEXT NOT NULL, result TEXT NOT NULL CHECK (result IN ('success', 'failure')), ip_address TEXT, details_json TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS job_runs ( id TEXT PRIMARY KEY, job_id TEXT NOT NULL, actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, status TEXT NOT NULL CHECK (status IN ( 'queued', 'running', 'succeeded', 'failed', 'timed_out', 'interrupted' )), input_json TEXT NOT NULL DEFAULT '{}', output_json TEXT, error_code TEXT, created_at TEXT NOT NULL, started_at TEXT, finished_at TEXT ); CREATE INDEX IF NOT EXISTS job_runs_created_idx ON job_runs(created_at DESC); `); database .prepare( `UPDATE job_runs SET status = 'interrupted', error_code = 'PROCESS_RESTARTED', finished_at = ? WHERE status IN ('queued', 'running')` ) .run(new Date().toISOString()); }