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:
decolua committed 2026-05-09 17:48:20 +07:00
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,
};
}
+114
View File
@@ -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 };
}