94 lines
2.7 KiB
TypeScript
94 lines
2.7 KiB
TypeScript
// SQLite database client (sql.js — pure WASM, no native deps)
|
|
import initSqlJs, { type Database, type BindParams } from "sql.js";
|
|
import { createTables } from "./schema.js";
|
|
|
|
let db: Database | null = null;
|
|
let initPromise: Promise<Database> | null = null;
|
|
|
|
/** Initialize the database (call once at startup). */
|
|
export async function initDb(dbPath?: string): Promise<Database> {
|
|
if (db) return db;
|
|
if (initPromise) return initPromise;
|
|
|
|
initPromise = (async () => {
|
|
const SQL = await initSqlJs();
|
|
const path = dbPath || process.env.DATABASE_URL || ":memory:";
|
|
|
|
// Try to load existing database from file
|
|
let buffer: ArrayLike<number> | undefined;
|
|
if (path !== ":memory:") {
|
|
try {
|
|
const fs = await import("node:fs/promises");
|
|
const data = await fs.readFile(path);
|
|
buffer = new Uint8Array(data);
|
|
} catch {
|
|
// File doesn't exist yet — start fresh
|
|
}
|
|
}
|
|
|
|
db = new SQL.Database(buffer);
|
|
db.run("PRAGMA foreign_keys = ON");
|
|
createTables(db);
|
|
return db;
|
|
})();
|
|
|
|
return initPromise;
|
|
}
|
|
|
|
/** Get the initialized database (must call initDb first). */
|
|
export function getDb(): Database {
|
|
if (!db) throw new Error("Database not initialized. Call initDb() first.");
|
|
return db;
|
|
}
|
|
|
|
/** Save database to disk. */
|
|
export async function saveDb(dbPath?: string): Promise<void> {
|
|
if (!db) return;
|
|
const path = dbPath || process.env.DATABASE_URL || "./data/app.db";
|
|
if (path === ":memory:") return;
|
|
const fs = await import("node:fs/promises");
|
|
const { dirname } = await import("node:path");
|
|
await fs.mkdir(dirname(path), { recursive: true });
|
|
const data = db.export();
|
|
await fs.writeFile(path, Buffer.from(data));
|
|
}
|
|
|
|
export async function closeDb(): Promise<void> {
|
|
if (db) {
|
|
await saveDb();
|
|
db.close();
|
|
db = null;
|
|
initPromise = null;
|
|
}
|
|
}
|
|
|
|
// Helper: run a query and return all rows as objects
|
|
export function queryAll<T = Record<string, unknown>>(sql: string, params: BindParams = []): T[] {
|
|
const d = getDb();
|
|
const stmt = d.prepare(sql);
|
|
if (params) stmt.bind(params);
|
|
const results: T[] = [];
|
|
while (stmt.step()) {
|
|
const row = stmt.getAsObject();
|
|
results.push(row as unknown as T);
|
|
}
|
|
stmt.free();
|
|
return results;
|
|
}
|
|
|
|
// Helper: run a query and return the first row
|
|
export function queryOne<T = Record<string, unknown>>(sql: string, params: BindParams = []): T | undefined {
|
|
const rows = queryAll<T>(sql, params);
|
|
return rows[0];
|
|
}
|
|
|
|
// Helper: run a mutation and return { changes, lastInsertRowid }
|
|
export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } {
|
|
const d = getDb();
|
|
d.run(sql, params);
|
|
return {
|
|
changes: d.getRowsModified(),
|
|
lastInsertRowid: 0,
|
|
};
|
|
}
|