feat(db): migrate from lowdb to SQLite with repos pattern
- Add modular DB layer (adapters, migrations, repos, helpers) - Replace localDb/usageDb/requestDetailsDb monoliths with repos - Add Tailscale tunnel integration & status check API - Add /api/cli-tools/all-statuses aggregated endpoint - Add settingsStore (Zustand) and mitm/dbReader - Add DB unit tests (benchmark, concurrent, migration, vs-lowdb)
This commit is contained in:
1 parent
145f588cc0
commit
bee8dad946
63 files changed
+4223
-2330
No files matched your search
@@ -0,0 +1,55 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { PRAGMA_SQL } from "../schema.js";
|
||||
|
||||
// Periodic checkpoint to keep WAL file small (avoid huge -wal/-shm growth)
|
||||
const CHECKPOINT_INTERVAL_MS = 60 * 1000;
|
||||
|
||||
export function createBetterSqliteAdapter(filePath) {
|
||||
const db = new Database(filePath);
|
||||
db.exec(PRAGMA_SQL);
|
||||
// Schema is created/synced by migrate.js after adapter init
|
||||
|
||||
const stmtCache = new Map();
|
||||
|
||||
function prepare(sql) {
|
||||
let stmt = stmtCache.get(sql);
|
||||
if (!stmt) {
|
||||
stmt = db.prepare(sql);
|
||||
stmtCache.set(sql, stmt);
|
||||
}
|
||||
return stmt;
|
||||
}
|
||||
|
||||
// Truncate WAL periodically so file stays small for backup/copy
|
||||
const checkpointTimer = setInterval(() => {
|
||||
try { db.pragma("wal_checkpoint(TRUNCATE)"); } catch {}
|
||||
}, CHECKPOINT_INTERVAL_MS);
|
||||
if (typeof checkpointTimer.unref === "function") checkpointTimer.unref();
|
||||
|
||||
function gracefulClose() {
|
||||
try { db.pragma("wal_checkpoint(TRUNCATE)"); } catch {}
|
||||
try { stmtCache.clear(); } catch {}
|
||||
try { db.close(); } catch {}
|
||||
}
|
||||
|
||||
// Ensure WAL is flushed and -wal/-shm files removed on shutdown
|
||||
const onShutdown = () => gracefulClose();
|
||||
process.once("beforeExit", onShutdown);
|
||||
process.once("SIGINT", () => { onShutdown(); process.exit(0); });
|
||||
process.once("SIGTERM", () => { onShutdown(); process.exit(0); });
|
||||
|
||||
return {
|
||||
driver: "better-sqlite3",
|
||||
run(sql, params = []) { return prepare(sql).run(params); },
|
||||
get(sql, params = []) { return prepare(sql).get(params); },
|
||||
all(sql, params = []) { return prepare(sql).all(params); },
|
||||
exec(sql) { return db.exec(sql); },
|
||||
transaction(fn) { return db.transaction(fn)(); },
|
||||
checkpoint() { try { db.pragma("wal_checkpoint(TRUNCATE)"); } catch {} },
|
||||
close() {
|
||||
clearInterval(checkpointTimer);
|
||||
gracefulClose();
|
||||
},
|
||||
raw: db,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import fs from "node:fs";
|
||||
import initSqlJs from "sql.js";
|
||||
import { PRAGMA_SQL } from "../schema.js";
|
||||
|
||||
let SQL = null;
|
||||
|
||||
async function loadSql() {
|
||||
if (SQL) return SQL;
|
||||
SQL = await initSqlJs();
|
||||
return SQL;
|
||||
}
|
||||
|
||||
export async function createSqlJsAdapter(filePath) {
|
||||
const SQLLib = await loadSql();
|
||||
const buf = fs.existsSync(filePath) ? fs.readFileSync(filePath) : null;
|
||||
const db = new SQLLib.Database(buf);
|
||||
db.exec(PRAGMA_SQL);
|
||||
// Schema is created/synced by migrate.js after adapter init
|
||||
|
||||
let dirty = false;
|
||||
let saveTimer = null;
|
||||
const SAVE_DEBOUNCE_MS = 100;
|
||||
|
||||
function persist() {
|
||||
const data = db.export();
|
||||
fs.writeFileSync(filePath, Buffer.from(data));
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
function scheduleSave() {
|
||||
dirty = true;
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(() => {
|
||||
saveTimer = null;
|
||||
if (dirty) {
|
||||
try { persist(); } catch (e) { console.error("[sqljs] save failed:", e); }
|
||||
}
|
||||
}, SAVE_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
function paramsObj(params) {
|
||||
if (!params || (Array.isArray(params) && params.length === 0)) return undefined;
|
||||
return params;
|
||||
}
|
||||
|
||||
function run(sql, params = []) {
|
||||
const stmt = db.prepare(sql);
|
||||
try {
|
||||
stmt.bind(paramsObj(params));
|
||||
stmt.step();
|
||||
const changes = db.getRowsModified();
|
||||
const lastInsertRowid = db.exec("SELECT last_insert_rowid() as id")[0]?.values?.[0]?.[0] ?? null;
|
||||
scheduleSave();
|
||||
return { changes, lastInsertRowid };
|
||||
} finally {
|
||||
stmt.free();
|
||||
}
|
||||
}
|
||||
|
||||
function get(sql, params = []) {
|
||||
const stmt = db.prepare(sql);
|
||||
try {
|
||||
stmt.bind(paramsObj(params));
|
||||
if (stmt.step()) return stmt.getAsObject();
|
||||
return undefined;
|
||||
} finally {
|
||||
stmt.free();
|
||||
}
|
||||
}
|
||||
|
||||
function all(sql, params = []) {
|
||||
const stmt = db.prepare(sql);
|
||||
try {
|
||||
stmt.bind(paramsObj(params));
|
||||
const rows = [];
|
||||
while (stmt.step()) rows.push(stmt.getAsObject());
|
||||
return rows;
|
||||
} finally {
|
||||
stmt.free();
|
||||
}
|
||||
}
|
||||
|
||||
function exec(sql) {
|
||||
db.exec(sql);
|
||||
scheduleSave();
|
||||
}
|
||||
|
||||
function transaction(fn) {
|
||||
db.exec("BEGIN");
|
||||
try {
|
||||
const result = fn();
|
||||
db.exec("COMMIT");
|
||||
scheduleSave();
|
||||
return result;
|
||||
} catch (e) {
|
||||
db.exec("ROLLBACK");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
if (dirty) persist();
|
||||
db.close();
|
||||
}
|
||||
|
||||
// Flush on shutdown
|
||||
const flush = () => { if (dirty) try { persist(); } catch {} };
|
||||
process.on("beforeExit", flush);
|
||||
process.on("SIGINT", flush);
|
||||
process.on("SIGTERM", flush);
|
||||
|
||||
return { driver: "sql.js", run, get, all, exec, transaction, close, raw: db };
|
||||
}
|
||||
Reference in new issue
Block a user