123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132import manifest from"./manifest.json";
import { DDL } from"./schema.js";
import {
runtime,
localDate,
validDate,
addDays,
nowISO,
uid,
nonblank,
} from"./runtime.js";
exportconst moods = ["very-low", "low", "neutral", "good", "great"];
exportfunctioncleanTags(tags) {
if (
!Array.isArray(tags) ||
tags.some((t) =>typeof t !== "string" || !t.trim())
)
thrownewError("Tags must be nonempty strings.");
return [...newSet(tags.map((t) => t.trim()))];
}
exportfunctioncreateModel(L) {
const rt = runtime(L, manifest, DDL);
asyncfunctionsave(data, append = false) {
const date = validDate(data.date ?? localDate()),
text = nonblank(data.text, "Entry text");
if (data.mood !== undefined && !moods.includes(data.mood))
thrownewError("Choose a valid mood.");
const existing = (
await L.db.query("SELECT * FROM entries WHERE date=?", [date])
)[0],
stamp = nowISO();
const entry = {
id: existing?.id ?? uid(),
date,
text: append && existing ? `${existing.text}\n\n${text}` : text,
mood: data.mood ?? existing?.mood ?? "neutral",
tags:
data.tags === undefined
? JSON.parse(existing?.tags ?? "[]")
: append
? cleanTags([
...JSON.parse(existing?.tags ?? "[]"),
...cleanTags(data.tags),
])
: cleanTags(data.tags),
created_at: existing?.created_at ?? stamp,
updated_at: stamp,
};
await L.db.exec(
"INSERT INTO entries (id,date,text,mood,tags,created_at,updated_at) VALUES (?,?,?,?,?,?,?) ON CONFLICT(date) DO UPDATE SET text=excluded.text,mood=excluded.mood,tags=excluded.tags,updated_at=excluded.updated_at",
[
entry.id,
date,
entry.text,
entry.mood,
JSON.stringify(entry.tags),
entry.created_at,
entry.updated_at,
],
);
const { id, ...rest } = entry;
await rt.publish(existing ? "entry-updated" : "entry-created", {
entry_id: id,
...rest,
});
}
const ready = rt.ready.then(() => {
rt.listen("add-entry", (d) =>save(d, true));
return rt.run(() => rt.flush());
});
return {
ready,
subscribe: rt.subscribe,
load: async () => {
await ready;
return {
entries: (
await L.db.query("SELECT * FROM entries ORDER BY date DESC")
).map((e) => ({ ...e, tags: JSON.parse(e.tags) })),
today: localDate(),
pending: rt.deliveryError,
};
},
save: (data) =>
rt.run(async () => {
awaitsave(data);
await rt.flush();
}),
tick: () => rt.run(() => rt.flush()),
remove: (entry) =>
rt.run(() => L.db.exec("DELETE FROM entries WHERE id=?", [entry.id])),
sample: () =>
rt.run(async () => {
if ((await L.db.query("SELECT id FROM entries")).length)
thrownewError("Sample data can only be added to an empty journal.");
const samples = [
{
ago: 0,
mood: "good",
tags: ["everyday", "outside"],
text: "Took the long way home through the park. The trees are starting to turn, and the air felt cooler than yesterday.\n\nFinished the chapter I had been putting off. A quiet evening with a book was exactly what I needed.",
},
{
ago: 1,
mood: "neutral",
tags: ["work", "reflection"],
text: "A full day, with more context switching than I expected. Writing down the next step before each meeting helped me pick things back up.\n\nTomorrow: leave a little more space between things.",
},
{
ago: 3,
mood: "great",
tags: ["friends"],
text: "Dinner with Maya and Sam. We cooked together and talked until late. I want to make more room for evenings like this.",
},
{
ago: 6,
mood: "low",
tags: ["reflection"],
text: "Low energy today. Kept the plans small: a short walk, a simple meal, and an early night.",
},
];
for (const e of samples)
awaitsave({
date: addDays(localDate(), -e.ago),
mood: e.mood,
tags: e.tags,
text: e.text,
});
await rt.flush();
}),
};
}