🌿 Collaborative wiki on ATProto lichen.wiki
atproto
14
fork

Configure Feed

Select the types of activity you want to include in your feed.

Move to cascade deletion for notes

juprodh 3b6a987a 2ffc2607

+61 -41
+2 -3
src/server/db/queries/note.ts
··· 139 139 140 140 export function deleteNoteByAtUri(atUri: string): void { 141 141 const db = getDb(); 142 + // Cascades handle revisions, snapshots, current_note, and blobs. 143 + // Backlinks use source_note_uri (not a FK), so delete explicitly. 142 144 db.transaction(() => { 143 - db.run("DELETE FROM current_note WHERE note_at_uri = ?", [atUri]); 144 - db.run("DELETE FROM revisions WHERE note_at_uri = ?", [atUri]); 145 - db.run("DELETE FROM snapshots WHERE note_at_uri = ?", [atUri]); 146 145 db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [atUri]); 147 146 db.run("DELETE FROM notes WHERE at_uri = ?", [atUri]); 148 147 })();
+6 -6
src/server/db/queries/wiki.ts
··· 1 1 import { escapeLikePattern } from "../../../lib/html.ts"; 2 2 import { getDb } from "../index.ts"; 3 3 import type { WikiRow } from "../types.ts"; 4 - import { deleteNoteByAtUri } from "./note.ts"; 5 4 6 5 export interface WikiWithNoteCount extends WikiRow { 7 6 note_count: number; ··· 149 148 .get(atUri) as { slug: string } | null; 150 149 if (!wiki) return; 151 150 151 + // Backlinks use source_note_uri (not a FK target), so clean up before cascade. 152 + // Deleting the wiki cascades to notes, revisions, snapshots, current_note, 153 + // blobs, memberships, requests, and backlinks (via wiki_slug FK). 152 154 db.transaction(() => { 153 - const notes = db 155 + const noteUris = db 154 156 .query("SELECT at_uri FROM notes WHERE wiki_slug = ?") 155 157 .all(wiki.slug) as { at_uri: string }[]; 156 - for (const note of notes) { 157 - deleteNoteByAtUri(note.at_uri); 158 + for (const n of noteUris) { 159 + db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [n.at_uri]); 158 160 } 159 - db.run("DELETE FROM memberships WHERE wiki_slug = ?", [wiki.slug]); 160 - db.run("DELETE FROM requests WHERE wiki_slug = ?", [wiki.slug]); 161 161 db.run("DELETE FROM wikis WHERE slug = ?", [wiki.slug]); 162 162 })(); 163 163 }
+9 -11
src/server/db/schema.ts
··· 3 3 export function initSchema(db: Database): void { 4 4 db.run("PRAGMA journal_mode = WAL"); 5 5 db.run("PRAGMA foreign_keys = ON"); 6 + db.run("PRAGMA busy_timeout = 5000"); 6 7 7 8 db.run(` 8 9 CREATE TABLE IF NOT EXISTS wikis ( ··· 21 22 db.run(` 22 23 CREATE TABLE IF NOT EXISTS notes ( 23 24 slug TEXT NOT NULL, 24 - wiki_slug TEXT NOT NULL REFERENCES wikis(slug), 25 + wiki_slug TEXT NOT NULL REFERENCES wikis(slug) ON DELETE CASCADE, 25 26 title TEXT NOT NULL, 26 27 did TEXT NOT NULL, 27 28 at_uri TEXT NOT NULL UNIQUE, ··· 33 34 db.run(` 34 35 CREATE TABLE IF NOT EXISTS revisions ( 35 36 id INTEGER PRIMARY KEY AUTOINCREMENT, 36 - note_at_uri TEXT NOT NULL, 37 + note_at_uri TEXT NOT NULL REFERENCES notes(at_uri) ON DELETE CASCADE, 37 38 did TEXT NOT NULL, 38 39 at_uri TEXT NOT NULL UNIQUE, 39 40 parent_revision_uri TEXT, ··· 47 48 db.run(` 48 49 CREATE TABLE IF NOT EXISTS snapshots ( 49 50 id INTEGER PRIMARY KEY AUTOINCREMENT, 50 - note_at_uri TEXT NOT NULL, 51 + note_at_uri TEXT NOT NULL REFERENCES notes(at_uri) ON DELETE CASCADE, 51 52 revision_at_uri TEXT NOT NULL, 52 53 content TEXT NOT NULL, 53 54 created_at TEXT NOT NULL DEFAULT (datetime('now')) ··· 56 57 57 58 db.run(` 58 59 CREATE TABLE IF NOT EXISTS current_note ( 59 - note_at_uri TEXT PRIMARY KEY, 60 + note_at_uri TEXT PRIMARY KEY REFERENCES notes(at_uri) ON DELETE CASCADE, 60 61 content TEXT NOT NULL, 61 62 latest_revision_uri TEXT NOT NULL, 62 63 updated_at TEXT NOT NULL DEFAULT (datetime('now')) ··· 66 67 db.run(` 67 68 CREATE TABLE IF NOT EXISTS memberships ( 68 69 id INTEGER PRIMARY KEY AUTOINCREMENT, 69 - wiki_slug TEXT NOT NULL REFERENCES wikis(slug), 70 + wiki_slug TEXT NOT NULL REFERENCES wikis(slug) ON DELETE CASCADE, 70 71 did TEXT NOT NULL, 71 72 role TEXT NOT NULL CHECK (role IN ('admin', 'contributor', 'viewer')), 72 73 at_uri TEXT NOT NULL UNIQUE, ··· 78 79 db.run(` 79 80 CREATE TABLE IF NOT EXISTS requests ( 80 81 id INTEGER PRIMARY KEY AUTOINCREMENT, 81 - wiki_slug TEXT NOT NULL REFERENCES wikis(slug), 82 + wiki_slug TEXT NOT NULL REFERENCES wikis(slug) ON DELETE CASCADE, 82 83 did TEXT NOT NULL, 83 84 at_uri TEXT NOT NULL UNIQUE, 84 85 created_at TEXT NOT NULL DEFAULT (datetime('now')), ··· 101 102 CREATE TABLE IF NOT EXISTS backlinks ( 102 103 source_note_uri TEXT NOT NULL, 103 104 target_note_slug TEXT NOT NULL, 104 - wiki_slug TEXT NOT NULL, 105 + wiki_slug TEXT NOT NULL REFERENCES wikis(slug) ON DELETE CASCADE, 105 106 PRIMARY KEY (source_note_uri, target_note_slug) 106 107 ) 107 108 `); ··· 109 110 db.run(` 110 111 CREATE TABLE IF NOT EXISTS blobs ( 111 112 cid TEXT PRIMARY KEY, 112 - revision_at_uri TEXT NOT NULL, 113 + revision_at_uri TEXT NOT NULL REFERENCES revisions(at_uri) ON DELETE CASCADE, 113 114 mime_type TEXT NOT NULL, 114 115 storage_key TEXT NOT NULL, 115 116 created_at TEXT NOT NULL DEFAULT (datetime('now')) ··· 164 165 db.run("CREATE INDEX IF NOT EXISTS idx_requests_wiki ON requests(wiki_slug)"); 165 166 db.run( 166 167 "CREATE INDEX IF NOT EXISTS idx_backlinks_target ON backlinks(wiki_slug, target_note_slug)", 167 - ); 168 - db.run( 169 - "CREATE INDEX IF NOT EXISTS idx_blobs_revision ON blobs(revision_at_uri)", 170 168 ); 171 169 db.run("CREATE INDEX IF NOT EXISTS idx_bookmarks_did ON bookmarks(did)"); 172 170 db.run("CREATE INDEX IF NOT EXISTS idx_wikis_did ON wikis(did)");
+10 -14
tests/helpers/cleanup.ts
··· 3 3 4 4 const db = getDb(); 5 5 6 - function deleteNoteDependents(noteAtUris: { at_uri: string }[]): void { 7 - for (const note of noteAtUris) { 8 - db.run("DELETE FROM current_note WHERE note_at_uri = ?", [note.at_uri]); 9 - db.run("DELETE FROM revisions WHERE note_at_uri = ?", [note.at_uri]); 10 - db.run("DELETE FROM snapshots WHERE note_at_uri = ?", [note.at_uri]); 11 - db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [note.at_uri]); 12 - } 13 - } 14 - 15 6 export function cleanupWikiAndDependents(slug: string): void { 7 + // Backlinks use source_note_uri which isn't a FK target, clean up first. 16 8 const notes = db 17 9 .query("SELECT at_uri FROM notes WHERE wiki_slug = ?") 18 10 .all(slug) as { at_uri: string }[]; 19 - deleteNoteDependents(notes); 20 - db.run("DELETE FROM notes WHERE wiki_slug = ?", [slug]); 21 - db.run("DELETE FROM memberships WHERE wiki_slug = ?", [slug]); 22 - db.run("DELETE FROM requests WHERE wiki_slug = ?", [slug]); 11 + for (const note of notes) { 12 + db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [note.at_uri]); 13 + } 14 + // Deleting the wiki cascades to notes, revisions, snapshots, current_note, 15 + // blobs, memberships, requests, and backlinks (via wiki_slug FK). 23 16 db.run("DELETE FROM wikis WHERE slug = ?", [slug]); 24 17 } 25 18 ··· 27 20 const notes = db 28 21 .query("SELECT at_uri FROM notes WHERE wiki_slug = ? AND slug GLOB ?") 29 22 .all(wikiSlug, slugGlob) as { at_uri: string }[]; 30 - deleteNoteDependents(notes); 23 + for (const note of notes) { 24 + db.run("DELETE FROM backlinks WHERE source_note_uri = ?", [note.at_uri]); 25 + } 26 + // Deleting notes cascades to revisions, snapshots, current_note, blobs. 31 27 db.run("DELETE FROM notes WHERE wiki_slug = ? AND slug GLOB ?", [ 32 28 wikiSlug, 33 29 slugGlob,
+34 -7
tests/server/db/queries/blob.test.ts
··· 1 - import { afterAll, describe, expect, test } from "bun:test"; 1 + import { afterAll, beforeAll, describe, expect, test } from "bun:test"; 2 2 import { getDb } from "../../../../src/server/db/index.ts"; 3 3 import { 4 4 getBlobsByCids, ··· 7 7 8 8 const db = getDb(); 9 9 10 + const TEST_DID = "did:plc:blobtest"; 11 + const WIKI_SLUG = "blob-test-wiki"; 12 + const WIKI_AT_URI = `at://${TEST_DID}/wiki.lichen.wiki/${WIKI_SLUG}`; 13 + const NOTE_AT_URI = `at://${TEST_DID}/wiki.lichen.note/note1`; 14 + const REV_URI_1 = `at://${TEST_DID}/wiki.lichen.noteRevision/rev1`; 15 + const REV_URI_2 = `at://${TEST_DID}/wiki.lichen.noteRevision/rev2`; 16 + 10 17 const TEST_CIDS = [ 11 18 "bafytest-blob-1", 12 19 "bafytest-blob-2", ··· 14 21 "bafytest-blob-dup", 15 22 ]; 16 23 24 + beforeAll(() => { 25 + db.run( 26 + "INSERT INTO wikis (slug, did, name, visibility, at_uri) VALUES (?, ?, ?, ?, ?)", 27 + [WIKI_SLUG, TEST_DID, "Blob Test Wiki", "public", WIKI_AT_URI], 28 + ); 29 + db.run( 30 + "INSERT INTO notes (slug, wiki_slug, title, did, at_uri) VALUES (?, ?, ?, ?, ?)", 31 + ["note1", WIKI_SLUG, "Test Note", TEST_DID, NOTE_AT_URI], 32 + ); 33 + db.run( 34 + "INSERT INTO revisions (note_at_uri, did, at_uri, diff) VALUES (?, ?, ?, ?)", 35 + [NOTE_AT_URI, TEST_DID, REV_URI_1, ""], 36 + ); 37 + db.run( 38 + "INSERT INTO revisions (note_at_uri, did, at_uri, diff) VALUES (?, ?, ?, ?)", 39 + [NOTE_AT_URI, TEST_DID, REV_URI_2, ""], 40 + ); 41 + }); 42 + 17 43 afterAll(() => { 18 44 for (const cid of TEST_CIDS) { 19 45 db.run("DELETE FROM blobs WHERE cid = ?", [cid]); 20 46 } 47 + db.run("DELETE FROM wikis WHERE slug = ?", [WIKI_SLUG]); 21 48 }); 22 49 23 50 describe("insertBlob", () => { 24 51 test("inserts a blob row", () => { 25 52 insertBlob( 26 53 "bafytest-blob-1", 27 - "at://did:plc:test/wiki.lichen.noteRevision/abc", 54 + REV_URI_1, 28 55 "image/png", 29 56 "data/blobs/test1.png", 30 57 ); ··· 40 67 test("INSERT OR IGNORE on duplicate CID", () => { 41 68 insertBlob( 42 69 "bafytest-blob-dup", 43 - "at://did:plc:test/wiki.lichen.noteRevision/first", 70 + REV_URI_1, 44 71 "image/jpeg", 45 72 "data/blobs/first.jpg", 46 73 ); 47 - // Insert again with different values — should be ignored 74 + // Insert again with different values -- should be ignored 48 75 insertBlob( 49 76 "bafytest-blob-dup", 50 - "at://did:plc:test/wiki.lichen.noteRevision/second", 77 + REV_URI_2, 51 78 "image/png", 52 79 "data/blobs/second.png", 53 80 ); ··· 69 96 test("returns matching blobs", () => { 70 97 insertBlob( 71 98 "bafytest-blob-2", 72 - "at://did:plc:test/wiki.lichen.noteRevision/xyz", 99 + REV_URI_1, 73 100 "image/webp", 74 101 "data/blobs/test2.webp", 75 102 ); 76 103 insertBlob( 77 104 "bafytest-blob-3", 78 - "at://did:plc:test/wiki.lichen.noteRevision/xyz", 105 + REV_URI_1, 79 106 "image/gif", 80 107 "data/blobs/test3.gif", 81 108 );