- upsertEntry inserts with ON CONFLICT DO NOTHING and patches through the update path, so a concurrent creator never aborts the caller's transaction on Postgres. - Undoing a move deletes the forward alias and writes no reverse alias. - Merging into an existing entry keeps 'user' field_sources on combined fields, carries the moved entry's field_sources as-is when reviving a deleted target, concatenates differing review/private notes, records a dropped rating in history, and writes history for re-parented engagements (restorable). - Moves re-point this user's copies (with history) and list items to the target work. - rejectSuggestion requires a pending suggestion; engagements of a deleted entry are not editable; restoring an entry does not re-check custom fields against current definitions. Co-Authored-By: Claude Opus 5.5 <[email protected]>
71 lines
3.6 KiB
TypeScript
71 lines
3.6 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import type { Db } from "@sttracker/db/db";
|
|
import { newId, nowIso } from "@sttracker/db/ids";
|
|
import { forEachDialect } from "@sttracker/db/testing";
|
|
import { acceptSuggestion, getEntry, quickAdd, rejectSuggestion } from "../src/entries.ts";
|
|
import { createCopy, listCopies } from "../src/copies.ts";
|
|
import { NotFoundError, ValidationError } from "../src/errors.ts";
|
|
import { getHistory } from "../src/history.ts";
|
|
import { addListItem, createList, getList } from "../src/lists.ts";
|
|
import { makeUser, makeWork } from "./user-layer-fixtures.ts";
|
|
|
|
async function suggest(db: Db, userId: string, workId: string, candidateWorkId: string): Promise<string> {
|
|
const id = newId();
|
|
await db.k
|
|
.insertInto("match_suggestions")
|
|
.values({ id, user_id: userId, work_id: workId, candidate_work_id: candidateWorkId, score: 0.9, status: "pending", created_at: nowIso() })
|
|
.execute();
|
|
return id;
|
|
}
|
|
|
|
async function statusOf(db: Db, id: string): Promise<string> {
|
|
return (await db.k.selectFrom("match_suggestions").select("status").where("id", "=", id).executeTakeFirstOrThrow()).status;
|
|
}
|
|
|
|
forEachDialect("suggestions", (getDb) => {
|
|
test("acceptSuggestion moves the entry and rejects the other pending suggestions", async () => {
|
|
const db = getDb();
|
|
const userId = await makeUser(db);
|
|
const draft = await quickAdd(db, userId, { title: "Dune??", mediaType: "book" });
|
|
const good = await makeWork(db, { title: "Dune", externalIds: { isbn13: "1" } });
|
|
const other = await makeWork(db, { title: "Dune Messiah" });
|
|
const s1 = await suggest(db, userId, draft.workId, good);
|
|
const s2 = await suggest(db, userId, draft.workId, other);
|
|
await acceptSuggestion(db, userId, s1);
|
|
expect(await statusOf(db, s1)).toBe("accepted");
|
|
expect(await statusOf(db, s2)).toBe("rejected");
|
|
const view = await getEntry(db, userId, draft.id);
|
|
expect(view.workId).toBe(good);
|
|
expect(view.draft).toBe(false);
|
|
});
|
|
|
|
test("rejectSuggestion; other users get NotFoundError", async () => {
|
|
const db = getDb();
|
|
const ada = await makeUser(db);
|
|
const bob = await makeUser(db, "bob");
|
|
const s = await suggest(db, ada, await makeWork(db), await makeWork(db));
|
|
await expect(acceptSuggestion(db, bob, s)).rejects.toThrow(NotFoundError);
|
|
await expect(rejectSuggestion(db, bob, s)).rejects.toThrow(NotFoundError);
|
|
expect(await statusOf(db, s)).toBe("pending");
|
|
await rejectSuggestion(db, ada, s);
|
|
expect(await statusOf(db, s)).toBe("rejected");
|
|
await expect(rejectSuggestion(db, ada, s)).rejects.toThrow(ValidationError);
|
|
await expect(acceptSuggestion(db, ada, s)).rejects.toThrow(ValidationError);
|
|
});
|
|
|
|
test("accepting moves this user's copies and list items to the target work", async () => {
|
|
const db = getDb();
|
|
const userId = await makeUser(db);
|
|
const draft = await quickAdd(db, userId, { title: "Dune??", mediaType: "book" });
|
|
const target = await makeWork(db, { title: "Dune", externalIds: { isbn13: "1" } });
|
|
const copy = await createCopy(db, userId, { workId: draft.workId, format: "paperback" });
|
|
const list = await createList(db, userId, { name: "Desert" });
|
|
await addListItem(db, userId, list.id, { workId: draft.workId });
|
|
await acceptSuggestion(db, userId, await suggest(db, userId, draft.workId, target));
|
|
expect((await listCopies(db, userId, { workId: target })).map((c) => c.id)).toEqual([copy.id]);
|
|
const [ch] = await getHistory(db, userId, "copy", copy.id);
|
|
expect(ch!.after).toEqual({ workId: target });
|
|
expect((await getList(db, userId, list.id)).items.map((i) => i.work.title)).toEqual(["Dune"]);
|
|
});
|
|
});
|