Journal

model.js 132 lines

Files · 9
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132import manifest from "./manifest.json";
import { DDL } from "./schema.js";
import {
  runtime,
  localDate,
  validDate,
  addDays,
  nowISO,
  uid,
  nonblank,
} from "./runtime.js";
export const moods = ["very-low", "low", "neutral", "good", "great"];
export function cleanTags(tags) {
  if (
    !Array.isArray(tags) ||
    tags.some((t) => typeof t !== "string" || !t.trim())
  )
    throw new Error("Tags must be nonempty strings.");
  return [...new Set(tags.map((t) => t.trim()))];
}
export function createModel(L) {
  const rt = runtime(L, manifest, DDL);
  async function save(data, append = false) {
    const date = validDate(data.date ?? localDate()),
      text = nonblank(data.text, "Entry text");
    if (data.mood !== undefined && !moods.includes(data.mood))
      throw new Error("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 () => {
        await save(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)
          throw new Error("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)
          await save({
            date: addDays(localDate(), -e.ago),
            mood: e.mood,
            tags: e.tags,
            text: e.text,
          });
        await rt.flush();
      }),
  };
}