benjivers/notes Public Public: anyone can read this; nobody can change it.

Files 2b2f00dc243e6583e6d8719a8b99fe2335f1e03a
96 lines · 3.1 KB · ~918 tok Raw History
1
/**
2
 * The database: one SQLite file under FORGE_APP_DATA, so production and
3
 * every preview each have their own. WAL so readers never wait on the
4
 * writer (the terminal is a second process on the same file), foreign keys
5
 * on, and a forward-only list of migrations applied once, in a
6
 * transaction, when the file is opened.
7
 *
8
 *   bun run src/db.ts --check    the schema version and an integrity check
9
 */
10
11
import { Database } from "bun:sqlite";
12
import { mkdirSync } from "node:fs";
13
import { join } from "node:path";
14
import { dataDir } from "./env.ts";
15
16
/** Append, never edit: a migration that has run is history. */
17
export const migrations: string[] = [
18
  `CREATE TABLE note (
19
    id INTEGER PRIMARY KEY,
20
    body TEXT NOT NULL CHECK (length(body) BETWEEN 1 AND 500),
21
    author_id TEXT NOT NULL,
22
    author_name TEXT NOT NULL,
23
    created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
24
    updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
25
  )`,
26
  `CREATE INDEX note_author ON note (author_id)`,
27
  // Who tried to change what, and whether they were let. Appended by
28
  // src/audit.ts, never edited; read by admins at GET /api/audit.
29
  `CREATE TABLE audit (
30
    id INTEGER PRIMARY KEY,
31
    at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
32
    who_id TEXT,
33
    who_name TEXT,
34
    role TEXT NOT NULL,
35
    resource TEXT NOT NULL,
36
    verb TEXT NOT NULL,
37
    target TEXT,
38
    allowed INTEGER NOT NULL CHECK (allowed IN (0, 1)),
39
    outcome TEXT NOT NULL
40
  )`,
41
];
42
43
export function openDb(dir = dataDir): Database {
44
  mkdirSync(dir, { recursive: true });
45
  const db = new Database(join(dir, "app.db"));
46
  db.exec("PRAGMA journal_mode = WAL");
47
  db.exec("PRAGMA foreign_keys = ON");
48
  db.exec("PRAGMA busy_timeout = 5000");
49
  migrate(db);
50
  return db;
51
}
52
53
/** Apply the migrations past the recorded version, all or none. Returns the version. */
54
export function migrate(db: Database): number {
55
  db.exec(
56
    "CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')))",
57
  );
58
  return db.transaction(() => {
59
    const current = db
60
      .query<
61
        { version: number },
62
        []
63
      >("SELECT coalesce(max(version), 0) AS version FROM schema_version")
64
      .get()!.version;
65
    migrations.slice(current).forEach((sql, i) => {
66
      db.exec(sql);
67
      db.query("INSERT INTO schema_version (version) VALUES (?1)").run(
68
        current + i + 1,
69
      );
70
    });
71
    return migrations.length;
72
  })();
73
}
74
75
export function check(db: Database): { version: number; integrity: string } {
76
  const version = db
77
    .query<
78
      { version: number },
79
      []
80
    >("SELECT max(version) AS version FROM schema_version")
81
    .get()!.version;
82
  const integrity = db
83
    .query<{ integrity_check: string }, []>("PRAGMA integrity_check")
84
    .get()!.integrity_check;
85
  return { version, integrity };
86
}
87
88
if (import.meta.main) {
89
  const db = openDb();
90
  const { version, integrity } = check(db);
91
  console.log(`schema version ${version}`);
92
  if (process.argv.includes("--check")) {
93
    console.log(`integrity ${integrity}`);
94
    process.exit(integrity === "ok" ? 0 : 1);
95
  }
96
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts