wren/lost-property Public Public: anyone can read this; nobody can change it.

workbenchthis machine
Files fe26fca1242e118d0a6a72d88af55fc09071adcf
78 lines · 2.8 KB · ~820 tok Raw History
1
/**
2
 * The collection: items that were lost and the notes visitors leave on
3
 * them. One SQLite file under FORGE_APP_DATA; production and each preview
4
 * get their own, so a preview starts from production's data and never
5
 * writes back to it.
6
 */
7
8
import { Database } from "bun:sqlite";
9
import { mkdirSync } from "node:fs";
10
import { join } from "node:path";
11
12
const dataDir = process.env.FORGE_APP_DATA ?? "./data";
13
mkdirSync(dataDir, { recursive: true });
14
export const db = new Database(join(dataDir, "app.db"));
15
db.exec("PRAGMA journal_mode = WAL");
16
17
// An earlier version of this app kept a flat note table with no item to
18
// belong to. `CREATE TABLE IF NOT EXISTS` below would leave that shape in
19
// place forever, so a table missing item_id is moved aside once, keeping
20
// its rows rather than losing them.
21
const existingNoteColumns = db.query<{ name: string }, []>("PRAGMA table_info(note)").all();
22
if (existingNoteColumns.length > 0 && !existingNoteColumns.some((c) => c.name === "item_id")) {
23
  db.exec("ALTER TABLE note RENAME TO note_legacy");
24
}
25
26
db.exec(
27
  `CREATE TABLE IF NOT EXISTS item (
28
    id INTEGER PRIMARY KEY,
29
    description TEXT NOT NULL,
30
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
31
  )`,
32
);
33
db.exec(
34
  `CREATE TABLE IF NOT EXISTS note (
35
    id INTEGER PRIMARY KEY,
36
    item_id INTEGER NOT NULL REFERENCES item(id),
37
    kind TEXT NOT NULL CHECK (kind IN ('sighting', 'match')),
38
    body TEXT NOT NULL,
39
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
40
  )`,
41
);
42
43
export type Item = { id: number; description: string; created_at: string };
44
export type ItemWithCount = Item & { note_count: number };
45
export type NoteKind = "sighting" | "match";
46
export type Note = { id: number; item_id: number; kind: NoteKind; body: string; created_at: string };
47
48
export function createItem(description: string): number {
49
  const row = db
50
    .query<{ id: number }, [string]>("INSERT INTO item (description) VALUES (?1) RETURNING id")
51
    .get(description);
52
  return row!.id;
53
}
54
55
export function getItem(id: number): Item | undefined {
56
  return db.query<Item, [number]>("SELECT id, description, created_at FROM item WHERE id = ?1").get(id) ?? undefined;
57
}
58
59
export function listItems(): ItemWithCount[] {
60
  return db
61
    .query<
62
      ItemWithCount,
63
      []
64
    >(`SELECT item.id, item.description, item.created_at,
65
              (SELECT count(*) FROM note WHERE note.item_id = item.id) AS note_count
66
       FROM item ORDER BY item.id DESC LIMIT 100`)
67
    .all();
68
}
69
70
export function listNotes(itemId: number): Note[] {
71
  return db
72
    .query<Note, [number]>("SELECT id, item_id, kind, body, created_at FROM note WHERE item_id = ?1 ORDER BY id ASC")
73
    .all(itemId);
74
}
75
76
export function addNote(itemId: number, kind: NoteKind, body: string): void {
77
  db.query("INSERT INTO note (item_id, kind, body) VALUES (?1, ?2, ?3)").run(itemId, kind, body);
78
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts