import { expect, test } from "bun:test"; import type { Db } from "@sttracker/db/db"; import { newId, nowIso } from "@sttracker/db/ids"; import { toJson } from "@sttracker/db/json"; import { forEachDialect } from "@sttracker/db/testing"; import { addEngagement } from "../../src/engagements.ts"; import { upsertEntry } from "../../src/entries.ts"; import { createList } from "../../src/lists.ts"; import { listChanges } from "../../src/sync/changes.ts"; import { makeConnection, makeUser, makeWork } from "./helpers.ts"; async function event( db: Db, userId: string, connectionId: string, e: { type: "changed" | "removed" | "merged"; targetEntity: string; targetId: string; payload?: Record }, ): Promise { const id = newId(); await db.k .insertInto("change_events") .values({ id, user_id: userId, connection_id: connectionId, run_id: null, record_id: null, type: e.type, target_entity: e.targetEntity, target_id: e.targetId, payload: toJson(e.payload ?? {}), status: "pending", created_at: nowIso(), }) .execute(); return id; } async function record(db: Db, connectionId: string, key: string, targetEntity: string, targetId: string): Promise { const now = nowIso(); await db.k .insertInto("source_records") .values({ id: newId(), connection_id: connectionId, key, kind: targetEntity, current_hash: "h", first_seen: now, last_seen: now, deleted_at: null, merged_into_key: null, target_entity: targetEntity, target_id: targetId, }) .execute(); } forEachDialect("change titles", (getDb) => { test("events carry their target's title, and merged events the merge target's", async () => { const db = getDb(); const userId = await makeUser(db); const conn = await makeConnection(db, userId, "hardcover"); const dune = await upsertEntry(db, userId, await makeWork(db, { title: "Dune" }), { overrides: { title: "Dune (1965)" } }, "user"); const piranesi = await upsertEntry(db, userId, await makeWork(db, { title: "Piranesi" }), {}, "user"); const deluxe = await upsertEntry(db, userId, await makeWork(db, { title: "Piranesi (Deluxe)" }), {}, "user"); const read = await addEngagement(db, userId, piranesi.id, {}, "user"); const list = await createList(db, userId, { name: "Want to read" }); await record(db, conn, "b:deluxe", "entry", deluxe.id); const onEntry = await event(db, userId, conn, { type: "changed", targetEntity: "entry", targetId: dune.id, payload: { field: "rating", from: 80, to: 60 } }); const onRead = await event(db, userId, conn, { type: "removed", targetEntity: "engagement", targetId: read.id }); const onList = await event(db, userId, conn, { type: "removed", targetEntity: "list", targetId: list.id }); const merged = await event(db, userId, conn, { type: "merged", targetEntity: "entry", targetId: piranesi.id, payload: { key: "b:p", intoKey: "b:deluxe" } }); const unknownMerge = await event(db, userId, conn, { type: "merged", targetEntity: "entry", targetId: piranesi.id, payload: { key: "b:p", intoKey: "b:nope" } }); const byId = new Map((await listChanges(db, userId)).items.map((c) => [c.id, c])); expect(byId.get(onEntry)!.title).toBe("Dune (1965)"); expect(byId.get(onRead)!.title).toBe("Piranesi"); expect(byId.get(onList)!.title).toBe("Want to read"); expect(byId.get(merged)!.title).toBe("Piranesi"); expect(byId.get(merged)!.intoTitle).toBe("Piranesi (Deluxe)"); expect(byId.get(unknownMerge)!.intoTitle).toBeNull(); expect(byId.get(onEntry)!.intoTitle).toBeUndefined(); }); test("a missing target gives a null title", async () => { const db = getDb(); const userId = await makeUser(db); const conn = await makeConnection(db, userId, "hardcover"); const id = await event(db, userId, conn, { type: "removed", targetEntity: "entry", targetId: newId() }); const [c] = (await listChanges(db, userId)).items; expect(c!.id).toBe(id); expect(c!.title).toBeNull(); }); });