oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1package docs
2
3import (
4 "context"
5 "embed"
6 "fmt"
7 "time"
8)
9
10// seedFS embeds this app's own sample-document source. Both halves of
11// each document live here: the plain-text .txt a person edits, and the
12// matching .update.bin a real Yjs update built from it — see
13// fixtures/generate-seed.mjs, the script that turns one into the
14// other. Only the .bin half is ever applied to a document (Seed,
15// below); the .txt half is embedded so a test can hold the two
16// honest against each other (seed_test.go's
17// TestSeedContentMatchesItsSource) rather than trusting that an
18// edited .txt and a stale, un-regenerated .bin still agree.
19//
20//go:embed seeddata/*.txt seeddata/*.update.bin
21var seedFS embed.FS
22
23// seedDoc describes one sample document: Base names both
24// seeddata/<Base>.txt and seeddata/<Base>.update.bin, Title is the
25// document's row title (Store.Create's title parameter — data, not
26// authored UI copy, the same reasoning handlers.go's docRow.Title
27// doc comment gives for why a document's own title is never a
28// catalog key), and Ago is how far before Seed runs its timestamps
29// should land.
30type seedDoc struct {
31 Base string
32 Title string
33 Ago time.Duration
34}
35
36// seedDocs is CARLOS Docs' own sample set: a project proposal, a
37// short note, meeting notes with attendees and actions, a short piece
38// of documentation, a letter, and one deliberately long document, per
39// the plan's own suggested shapes. Ago values are spread across every
40// bucket handlers.go's recencyBucket recognises (today, yesterday,
41// this week, earlier) and, within "today", across two different
42// times — so List's grouping has something to show rather than
43// collapsing six documents seeded in the same second into one
44// indistinguishable "Today" pile.
45var seedDocs = []seedDoc{
46 {Base: "packing-list", Title: "Packing List — Thursday Site Visit", Ago: 20 * time.Minute},
47 {Base: "proposal", Title: "Proposal: Refitting the Elm Street Reading Room", Ago: 3 * time.Hour},
48 {Base: "meeting-notes", Title: "Notes — Tuesday Planning Sync", Ago: 26 * time.Hour},
49 {Base: "documentation", Title: "Quick Reference: Resetting the Site Printer", Ago: 3*24*time.Hour + 4*time.Hour},
50 {Base: "letter", Title: "Letter to the Harbourview Allotment Committee", Ago: 9 * 24 * time.Hour},
51 {Base: "history", Title: "A History of the Millbrook Waterworks", Ago: 52 * 24 * time.Hour},
52}
53
54// Seed populates owner's document list with CARLOS Docs' sample
55// content: real English prose, several paragraphs each, applied as
56// genuine Yjs updates through Store.ApplyUpdate — the same route a
57// person's own typing takes, never a text column written directly.
58// There is no Go-side Yjs encoder (internal/core applies updates and
59// reads text; the ABI exposes no insert), so each update below is not
60// built here — it is fixtures/generate-seed.mjs's committed output,
61// exactly the route the wire-compatibility gate's own fixtures
62// already use (ReadFixture, testing.go).
63//
64// It is meant for a fresh, empty owner and does not check for or skip
65// existing documents: calling it twice against the same owner seeds
66// the sample set twice over. That is a feature for reasoning about,
67// not a safety net around; the safety belongs at the caller, not
68// here. There are exactly two callers, each with its own safety
69// story: cmd/docs/main.go's -seed flag, gated to Env == "development"
70// (main.go's own doc comment on why); and Store.EnsurePerson's
71// seedIfEnabled (store.go), called at most once per person, ever, on
72// the one event in a person's history that can only happen once —
73// their row not existing yet (seedIfEnabled's own doc comment).
74//
75// Takes a *Store rather than a *App: this is the same store either
76// caller already has (app.Store(), or the store method's own s), and
77// a *Store has no need for anything else the App wraps — the router,
78// the mailer, the auth plugin. Store.EnsurePerson calling this
79// directly, in-package, is what makes per-person seeding possible at
80// all without a Store depending back on the *App that holds it.
81// The actor is the new person themselves: seeding writes their sample
82// library as them, so it goes through exactly the same write
83// authorization as any other edit rather than around it.
84func Seed(ctx context.Context, store *Store, actor Actor) error {
85 owner := actor.PersonID
86 base := time.Now().UTC()
87
88 for _, sd := range seedDocs {
89 doc, err := store.Create(ctx, owner, sd.Title)
90 if err != nil {
91 return fmt.Errorf("docs: seed: create %q: %w", sd.Title, err)
92 }
93
94 update, err := seedFS.ReadFile("seeddata/" + sd.Base + ".update.bin")
95 if err != nil {
96 return fmt.Errorf("docs: seed: read update for %q: %w", sd.Title, err)
97 }
98 // The assigned seq is discarded here on purpose: seeding runs
99 // when a person's row is first created, before any editor of
100 // theirs can be open, so there is nobody subscribed to notify
101 // (design D5's accepted consequence — the broadcast lives in
102 // updatesPost, not in Store).
103 if _, err := store.ApplyUpdate(ctx, doc, actor, update); err != nil {
104 return fmt.Errorf("docs: seed: apply update for %q: %w", sd.Title, err)
105 }
106
107 // ApplyUpdate just bumped both timestamps to the real "now" —
108 // correct for a real edit, wrong for a seeded document meant
109 // to land in a specific recency bucket. Backdating both
110 // columns directly, after the fact, through the same owned()
111 // seam every other write in store.go goes through (store.go's
112 // own doc comment on owned: "the one place that can make it
113 // structurally impossible" for a write to drift off that
114 // scope) — doc.OwnerID is already owner here, so this is
115 // belt-and-braces, not a correction of anything wrong above.
116 at := base.Add(-sd.Ago)
117 if err := owned(store.db.WithContext(ctx), owner).
118 Model(&Document{}).
119 Where("id = ?", doc.ID).
120 Updates(map[string]any{"created_at": at, "updated_at": at}).Error; err != nil {
121 return fmt.Errorf("docs: seed: backdate %q: %w", sd.Title, err)
122 }
123 }
124 return nil
125}
126