v1.3 - added an sqlite db and built data structure
This commit is contained in:
parent
b0fba52c0e
commit
5efdafbb97
37 changed files with 6414 additions and 1988 deletions
120
server/src/db.js
Normal file
120
server/src/db.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/* ═══════════════════════════════════════════════════════════════
|
||||
DATABASE
|
||||
|
||||
One SQLite file, opened once at boot and held for the life of
|
||||
the process. Node's own sqlite module is used when it's there
|
||||
(Node 24+), better-sqlite3 otherwise. Their APIs overlap enough
|
||||
that everything below works against either, as long as you:
|
||||
|
||||
• use positional ? parameters, never named ones
|
||||
• pass 0/1 for booleans, never true/false
|
||||
• use tx() rather than db.transaction()
|
||||
|
||||
Those three rules are the whole compatibility story.
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const MIGRATIONS_DIR = join(HERE, "migrations");
|
||||
|
||||
/* ── Driver selection ──────────────────────────────────────── */
|
||||
|
||||
async function loadDriver() {
|
||||
try {
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
return { Driver: DatabaseSync, name: "node:sqlite" };
|
||||
} catch {
|
||||
const { default: BetterSqlite3 } = await import("better-sqlite3");
|
||||
return { Driver: BetterSqlite3, name: "better-sqlite3" };
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Open ──────────────────────────────────────────────────────
|
||||
WAL readers never block the writer, which matters the
|
||||
moment a feedback POST lands mid page-load
|
||||
NORMAL fsync on checkpoint rather than every commit; safe
|
||||
under WAL, and much faster
|
||||
busy wait rather than throw if something else holds the
|
||||
write lock (a backup, usually)
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
export async function openDatabase(path) {
|
||||
const { Driver, name } = await loadDriver();
|
||||
const db = new Driver(path);
|
||||
|
||||
db.exec("PRAGMA journal_mode = WAL");
|
||||
db.exec("PRAGMA foreign_keys = ON");
|
||||
db.exec("PRAGMA synchronous = NORMAL");
|
||||
db.exec("PRAGMA busy_timeout = 5000");
|
||||
|
||||
db.driverName = name;
|
||||
return db;
|
||||
}
|
||||
|
||||
/* ── Transactions ──────────────────────────────────────────────
|
||||
node:sqlite has no db.transaction(), so do it by hand. Runs
|
||||
fn() and commits, or rolls back and rethrows.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
export function tx(db, fn) {
|
||||
db.exec("BEGIN");
|
||||
try {
|
||||
const result = fn();
|
||||
db.exec("COMMIT");
|
||||
return result;
|
||||
} catch (err) {
|
||||
try {
|
||||
db.exec("ROLLBACK");
|
||||
} catch {
|
||||
/* already rolled back */
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Migrations ────────────────────────────────────────────────
|
||||
Files are NNN_name.sql. The leading number is the version.
|
||||
PRAGMA user_version tracks how far we've got, so there's no
|
||||
bookkeeping table and no ordering ambiguity.
|
||||
|
||||
Migrations only ever go forward. To undo something, write a
|
||||
new migration.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
export function migrate(db, { log = console.log } = {}) {
|
||||
const current = db.prepare("PRAGMA user_version").get().user_version;
|
||||
|
||||
const files = readdirSync(MIGRATIONS_DIR)
|
||||
.filter((f) => f.endsWith(".sql"))
|
||||
.sort();
|
||||
|
||||
let applied = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const version = Number.parseInt(file.slice(0, 3), 10);
|
||||
|
||||
if (!Number.isInteger(version) || version < 1) {
|
||||
throw new Error(`Migration "${file}" must start with a number, e.g. 001_`);
|
||||
}
|
||||
if (version <= current) continue;
|
||||
|
||||
const sql = readFileSync(join(MIGRATIONS_DIR, file), "utf8");
|
||||
|
||||
tx(db, () => {
|
||||
db.exec(sql);
|
||||
// Not parameterisable, but version is a validated integer.
|
||||
db.exec(`PRAGMA user_version = ${version}`);
|
||||
});
|
||||
|
||||
log(`migrated → ${file}`);
|
||||
applied += 1;
|
||||
}
|
||||
|
||||
const final = db.prepare("PRAGMA user_version").get().user_version;
|
||||
if (applied === 0) log(`schema up to date (v${final})`);
|
||||
|
||||
return final;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue