import type { Database } from "bun:sqlite"; import * as TID from "@atcute/tid"; import { getDevAccounts } from "../../atproto/env.ts"; import { createDiff } from "../../lib/diff.ts"; import { LIMITS } from "../../lib/limits.ts"; interface SeedNote { slug: string; title: string; content: string; } function seedWiki( db: Database, owner: { did: string; handle: string }, slug: string, name: string, description: string, notes: SeedNote[], ): void { const wikiAtUri = `at://${owner.did}/wiki.lichen.wiki/${slug}`; db.run( "INSERT INTO wikis (slug, did, name, visibility, description, at_uri) VALUES (?, ?, ?, ?, ?, ?)", [slug, owner.did, name, "public", description, wikiAtUri], ); db.run( "INSERT INTO memberships (wiki_at_uri, wiki_slug, did, role, at_uri) VALUES (?, ?, ?, ?, ?)", [ wikiAtUri, slug, owner.did, "admin", `at://${owner.did}/wiki.lichen.membership/${TID.now()}`, ], ); for (const note of notes) { const noteAtUri = `at://${owner.did}/wiki.lichen.note/${TID.now()}`; const revisionAtUri = `at://${owner.did}/wiki.lichen.noteRevision/${TID.now()}`; db.run( "INSERT INTO notes (slug, wiki_at_uri, wiki_slug, title, did, at_uri) VALUES (?, ?, ?, ?, ?, ?)", [note.slug, wikiAtUri, slug, note.title, owner.did, noteAtUri], ); // Initial revision + snapshot so the seeded note has a history entry. db.run( `INSERT INTO revisions (note_at_uri, did, at_uri, parent_revision_uri, diff, message) VALUES (?, ?, ?, NULL, ?, ?)`, [ noteAtUri, owner.did, revisionAtUri, createDiff("", note.content), "Initial content", ], ); db.run( "INSERT INTO snapshots (note_at_uri, revision_at_uri, content) VALUES (?, ?, ?)", [noteAtUri, revisionAtUri, note.content], ); db.run( `INSERT INTO current_note (note_at_uri, content, latest_revision_uri, updated_at) VALUES (?, ?, ?, datetime('now'))`, [noteAtUri, note.content, revisionAtUri], ); } } // A note whose revision count exceeds the snapshot cap, so the oldest entries in // its history view fall back to on-demand diff reconstruction (the snapshots have // been evicted, exactly as in production). Mirrors the appendRevision + capSnapshots // flow with raw SQL to stay consistent with the rest of the seed. function seedNoteWithHistory( db: Database, owner: { did: string }, wikiSlug: string, note: { slug: string; title: string; versions: string[] }, ): void { const wikiAtUri = `at://${owner.did}/wiki.lichen.wiki/${wikiSlug}`; const noteAtUri = `at://${owner.did}/wiki.lichen.note/${TID.now()}`; db.run( "INSERT INTO notes (slug, wiki_at_uri, wiki_slug, title, did, at_uri) VALUES (?, ?, ?, ?, ?, ?)", [note.slug, wikiAtUri, wikiSlug, note.title, owner.did, noteAtUri], ); let parent: string | null = null; let prev = ""; let latest = ""; for (const [i, content] of note.versions.entries()) { const revAtUri = `at://${owner.did}/wiki.lichen.noteRevision/${TID.now()}`; db.run( `INSERT INTO revisions (note_at_uri, did, at_uri, parent_revision_uri, diff, message) VALUES (?, ?, ?, ?, ?, ?)`, [ noteAtUri, owner.did, revAtUri, parent, createDiff(prev, content), i === 0 ? "Initial content" : `Edit ${i}`, ], ); db.run( "INSERT INTO snapshots (note_at_uri, revision_at_uri, content) VALUES (?, ?, ?)", [noteAtUri, revAtUri, content], ); parent = revAtUri; prev = content; latest = revAtUri; } db.run( `INSERT INTO current_note (note_at_uri, content, latest_revision_uri, updated_at) VALUES (?, ?, ?, datetime('now'))`, [noteAtUri, prev, latest], ); // Keep only the most recent snapshots, as capSnapshots does on every ingest. db.run( `DELETE FROM snapshots WHERE note_at_uri = ? AND id NOT IN ( SELECT id FROM snapshots WHERE note_at_uri = ? ORDER BY id DESC LIMIT ? )`, [noteAtUri, noteAtUri, LIMITS.snapshotCap], ); } // Builds a multi-version changelog: each edit appends one line, so old revisions // render as clean single-line insertions once reconstructed. function changelogVersions(): string[] { let body = `# Changelog A note with a long edit history, used to exercise diff reconstruction for revisions older than the snapshot cap. `; const versions = [body]; for (let i = 1; i <= 30; i++) { body += `\n- Entry ${i}: documented change number ${i}.`; versions.push(body); } return versions; } export function seedIfEmpty(db: Database): void { const count = db.query("SELECT COUNT(*) as n FROM wikis").get() as { n: number; }; if (count.n > 0) return; const accounts = getDevAccounts(); if (!accounts) return; const alice = accounts["alice"]; const bob = accounts["bob"]; if (!alice || !bob) return; console.log("Seeding database with sample data..."); db.run("BEGIN TRANSACTION"); try { for (const account of Object.values(accounts)) { db.run( "INSERT OR IGNORE INTO profile_cache (did, handle) VALUES (?, ?)", [account.did, account.handle], ); } seedWiki( db, alice, "alices-garden", "Alice's Garden", "A sample public wiki owned by Alice for exploring Lichen.", [ { slug: "home", title: "Home", content: `# Welcome to Alice's Garden This is the **home page**. Try editing it, then check the history. - [[hello|Hello]] — a sample note - [[@bob.test/bobs-notebook/home|Bob's Notebook]] — Alice contributes there too `, }, { slug: "hello", title: "Hello", content: `# Hello A short note. Edit me to see how diffs work. `, }, ], ); // >25 revisions: its oldest history entries render via reconstruction. seedNoteWithHistory(db, alice, "alices-garden", { slug: "changelog", title: "Changelog", versions: changelogVersions(), }); seedWiki( db, bob, "bobs-notebook", "Bob's Notebook", "A sample public wiki owned by Bob. Alice has contributor access.", [ { slug: "home", title: "Home", content: `# Bob's Notebook Bob owns this wiki. Alice can edit notes here as a contributor. `, }, ], ); // Alice is a contributor on Bob's wiki — log in as alice and edit it. db.run( "INSERT INTO memberships (wiki_at_uri, wiki_slug, did, role, at_uri) VALUES (?, ?, ?, ?, ?)", [ `at://${bob.did}/wiki.lichen.wiki/bobs-notebook`, "bobs-notebook", alice.did, "contributor", `at://${bob.did}/wiki.lichen.membership/${TID.now()}`, ], ); db.run("COMMIT"); } catch (err) { db.run("ROLLBACK"); throw err; } }