Trips

model.js 203 lines

Files · 9
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203import manifest from "./manifest.json";
import { DDL } from "./schema.js";
import {
  runtime,
  localDate,
  validDate,
  addDays,
  nowISO,
  uid,
  nonblank,
} from "./runtime.js";
export function createModel(L) {
  const rt = runtime(L, manifest, DDL);
  async function trip(id) {
    const row = (await L.db.query("SELECT * FROM trips WHERE id=?", [id]))[0];
    if (!row) throw new Error("Choose an existing trip.");
    return row;
  }
  async function addTrip(data) {
    const title = nonblank(data.title, "Trip title"),
      destination = nonblank(data.destination, "Destination"),
      start_date = validDate(data.start_date),
      end_date = validDate(data.end_date);
    if (end_date < start_date)
      throw new Error("End date must be on or after the start date.");
    const row = {
      id: uid(),
      title,
      destination,
      start_date,
      end_date,
      created_at: nowISO(),
    };
    await L.db.exec(
      "INSERT INTO trips VALUES (?,?,?,?,?,?)",
      Object.values(row),
    );
    const { id, ...rest } = row;
    await rt.publish("trip-created", { trip_id: id, ...rest });
    return row;
  }
  async function addItem(data) {
    rt.validate("listeners", "add-itinerary-item", data);
    const parent = await trip(data.trip_id),
      date = validDate(data.date),
      title = nonblank(data.title, "Item title");
    if (date < parent.start_date || date > parent.end_date)
      throw new Error("Itinerary date must be within the trip dates.");
    const row = {
      id: uid(),
      trip_id: parent.id,
      kind: data.kind,
      title,
      date,
      time: data.time ?? "",
      location: data.location ?? "",
      confirmation_code: data.confirmation_code ?? "",
      created_at: nowISO(),
    };
    await L.db.exec(
      "INSERT INTO itinerary_items VALUES (?,?,?,?,?,?,?,?,?)",
      Object.values(row),
    );
    const { id, ...rest } = row;
    await rt.publish("item-added", { item_id: id, ...rest });
  }
  async function addPacking(data) {
    rt.validate("listeners", "add-packing-item", data);
    await trip(data.trip_id);
    await L.db.exec(
      "INSERT INTO packing_items (id,trip_id,label,quantity,created_at) VALUES (?,?,?,?,?)",
      [
        uid(),
        data.trip_id,
        nonblank(data.label, "Packing item"),
        data.quantity ?? 1,
        nowISO(),
      ],
    );
  }
  const ready = rt.ready.then(() => {
    rt.listen("add-itinerary-item", addItem);
    rt.listen("add-packing-item", addPacking);
    return rt.run(() => rt.flush());
  });
  return {
    ready,
    subscribe: rt.subscribe,
    load: async () => {
      await ready;
      return {
        trips: await L.db.query("SELECT * FROM trips ORDER BY start_date,id"),
        items: await L.db.query(
          "SELECT * FROM itinerary_items ORDER BY date, CASE WHEN time='' THEN 1 ELSE 0 END, time, created_at",
        ),
        packing: await L.db.query(
          "SELECT * FROM packing_items ORDER BY created_at,id",
        ),
        today: localDate(),
        pending: rt.deliveryError,
      };
    },
    addTrip: (data) =>
      rt.run(async () => {
        const row = await addTrip(data);
        await rt.flush();
        return row;
      }),
    addItem: (data) =>
      rt.run(async () => {
        await addItem(data);
        await rt.flush();
      }),
    addPacking: (data) => rt.run(() => addPacking(data)),
    toggle: (item) =>
      rt.run(async () => {
        const current = (
          await L.db.query("SELECT * FROM packing_items WHERE id=?", [item.id])
        )[0];
        if (!current) throw new Error("Packing item no longer exists.");
        const checked = !current.checked,
          stamp = checked ? nowISO() : null;
        await L.db.exec(
          "UPDATE packing_items SET checked=?,checked_at=? WHERE id=?",
          [checked ? 1 : 0, stamp, current.id],
        );
        if (checked)
          await rt.publish("packing-item-checked", {
            packing_item_id: current.id,
            trip_id: current.trip_id,
            label: current.label,
            quantity: current.quantity,
            checked: true,
            checked_at: stamp,
          });
        await rt.flush();
      }),
    removeItem: (id) =>
      rt.run(() => L.db.exec("DELETE FROM itinerary_items WHERE id=?", [id])),
    removePacking: (id) =>
      rt.run(() => L.db.exec("DELETE FROM packing_items WHERE id=?", [id])),
    tick: () => rt.run(() => rt.flush()),
    sample: () =>
      rt.run(async () => {
        if ((await L.db.query("SELECT id FROM trips")).length)
          throw new Error(
            "Sample data can only be added to an empty trip list.",
          );
        const start = addDays(localDate(), 10),
          end = addDays(start, 4);
        const t = await addTrip({
          title: "A few days in Lisbon",
          destination: "Lisbon, Portugal",
          start_date: start,
          end_date: end,
        });
        await addItem({
          trip_id: t.id,
          kind: "flight",
          title: "Flight to Lisbon",
          date: start,
          time: "09:40",
          location: "Departure terminal 2",
          confirmation_code: "SAMPLE-4K2",
        });
        await addItem({
          trip_id: t.id,
          kind: "lodging",
          title: "Check in · Alfama apartment",
          date: start,
          time: "15:00",
          location: "Alfama, Lisbon",
          confirmation_code: "SAMPLE-891",
        });
        await addItem({
          trip_id: t.id,
          kind: "activity",
          title: "Walk along the waterfront",
          date: addDays(start, 1),
          time: "10:00",
          location: "Praça do Comércio",
        });
        await addItem({
          trip_id: t.id,
          kind: "activity",
          title: "Day trip to Sintra",
          date: addDays(start, 2),
          time: "08:30",
          location: "Rossio station",
        });
        for (const [label, quantity] of [
          ["Passport", 1],
          ["Walking shoes", 1],
          ["Phone charger", 1],
          ["T-shirts", 4],
          ["Light jacket", 1],
        ])
          await addPacking({ trip_id: t.id, label, quantity });
        await rt.flush();
        return t;
      }),
  };
}