pike/glyph Public Public: anyone can read this; nobody can change it.

forked from apps/glyph

workbenchthis machine
Files e45e4989a79ef3f801c453528dd5b471f91bea4f
112 lines · 3.9 KB · ~1.1k 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
  // The template's example notes give way to drawings: one prompt, the SVGs
42
  // Claude drew for it (a JSON array), and where the drawing stands.
43
  `DROP TABLE note`,
44
  `CREATE TABLE drawing (
45
    id INTEGER PRIMARY KEY,
46
    prompt TEXT NOT NULL CHECK (length(prompt) BETWEEN 1 AND 400),
47
    kind TEXT NOT NULL CHECK (kind IN ('mark', 'icon', 'badge', 'spot')),
48
    status TEXT NOT NULL DEFAULT 'drawing' CHECK (status IN ('drawing', 'done', 'failed')),
49
    svgs TEXT NOT NULL DEFAULT '[]',
50
    error TEXT,
51
    author_id TEXT NOT NULL,
52
    author_name TEXT NOT NULL,
53
    created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
54
    updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
55
  )`,
56
  `CREATE INDEX drawing_author ON drawing (author_id, id)`,
57
];
58
59
export function openDb(dir = dataDir): Database {
60
  mkdirSync(dir, { recursive: true });
61
  const db = new Database(join(dir, "app.db"));
62
  db.exec("PRAGMA journal_mode = WAL");
63
  db.exec("PRAGMA foreign_keys = ON");
64
  db.exec("PRAGMA busy_timeout = 5000");
65
  migrate(db);
66
  return db;
67
}
68
69
/** Apply the migrations past the recorded version, all or none. Returns the version. */
70
export function migrate(db: Database): number {
71
  db.exec(
72
    "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')))",
73
  );
74
  return db.transaction(() => {
75
    const current = db
76
      .query<
77
        { version: number },
78
        []
79
      >("SELECT coalesce(max(version), 0) AS version FROM schema_version")
80
      .get()!.version;
81
    migrations.slice(current).forEach((sql, i) => {
82
      db.exec(sql);
83
      db.query("INSERT INTO schema_version (version) VALUES (?1)").run(
84
        current + i + 1,
85
      );
86
    });
87
    return migrations.length;
88
  })();
89
}
90
91
export function check(db: Database): { version: number; integrity: string } {
92
  const version = db
93
    .query<
94
      { version: number },
95
      []
96
    >("SELECT max(version) AS version FROM schema_version")
97
    .get()!.version;
98
  const integrity = db
99
    .query<{ integrity_check: string }, []>("PRAGMA integrity_check")
100
    .get()!.integrity_check;
101
  return { version, integrity };
102
}
103
104
if (import.meta.main) {
105
  const db = openDb();
106
  const { version, integrity } = check(db);
107
  console.log(`schema version ${version}`);
108
  if (process.argv.includes("--check")) {
109
    console.log(`integrity ${integrity}`);
110
    process.exit(integrity === "ok" ? 0 : 1);
111
  }
112
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts