/** * Opens the database at {@link DB_PATH} when first imported, creating it from * schema.sql if the file doesn't exist, then applies the pending migrations * and runs the startup tasks before any importer runs. Only the services * import it. * * @module */ import { DatabaseSync } from "node:sqlite"; import { DB_PATH } from "../config.ts"; import { runMigrations } from "./migrate.ts"; import { runStartupTasks } from "./startup.ts"; const isNew = await Deno.stat(DB_PATH).then(() => false, () => true); /** * Serves every query of the server, as its only connection. Its queries are * synchronous, so each one blocks the event loop while it runs. */ export const db: DatabaseSync = new DatabaseSync(DB_PATH); // schema.sql holds every migration's change, so on a new database each // migration below finds its work done and is only recorded. if (isNew) { db.exec( await Deno.readTextFile(new URL("./schema.sql", import.meta.url)), ); } // WAL lets readers run alongside a writer, and with synchronous = NORMAL a // commit doesn't wait on an fsync: a power loss can lose the last commits but // never corrupts the file. busy_timeout makes another connection to the file // wait for the lock instead of failing. SQLite enforces foreign keys, and so // the schema's ON DELETE cascades, only when each connection asks. db.exec(` PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON; `); runMigrations(db); await runStartupTasks(db); /** * Runs `fn` in a transaction, so its writes commit together, in one commit * rather than one each, and a throw rolls them all back and rethrows. A call * inside another joins the outer transaction. `fn` must be synchronous: an * `await` would commit before the rest of it runs. */ export function transaction(fn: () => T): T { if (db.isTransaction) return fn(); db.exec("BEGIN;"); try { const result = fn(); db.exec("COMMIT;"); return result; } catch (err) { db.exec("ROLLBACK;"); throw err; } }