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
2026-05-09 17:48:20 +07:00
parent 145f588cc0
commit bee8dad946
63 changed files with 4223 additions and 2330 deletions
+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 };
}