fix(core): race-safe entry upsert and lossless per-user moves
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
512e6e9464
commit
7154ca0a61
6 files changed
+327
-83
No files matched your search
@@ -7,7 +7,7 @@ import {
|
||||
softDeleteEngagement,
|
||||
updateEngagement,
|
||||
} from "../src/engagements.ts";
|
||||
import { upsertEntry } from "../src/entries.ts";
|
||||
import { softDeleteEntry, upsertEntry } from "../src/entries.ts";
|
||||
import { NotFoundError, ValidationError } from "../src/errors.ts";
|
||||
import { getHistory, restoreVersion } from "../src/history.ts";
|
||||
import { makeUser, makeWork } from "./user-layer-fixtures.ts";
|
||||
@@ -114,6 +114,14 @@ forEachDialect("engagements", (getDb) => {
|
||||
expect((await listEngagements(db, userId, entry.id)).map((e) => e.id)).toEqual([early.id, later.id, undated.id]);
|
||||
});
|
||||
|
||||
test("engagements of a soft-deleted entry cannot be changed", async () => {
|
||||
const { db, userId, entry } = await setup();
|
||||
const eg = await addEngagement(db, userId, entry.id, {}, "user");
|
||||
await softDeleteEntry(db, userId, entry.id, "user");
|
||||
await expect(updateEngagement(db, userId, eg.id, { rating: 1 }, "user")).rejects.toThrow(NotFoundError);
|
||||
await expect(softDeleteEngagement(db, userId, eg.id, "user")).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
test("another user's engagements are not found", async () => {
|
||||
const { db, userId, entry } = await setup();
|
||||
const bob = await makeUser(db, "bob");
|
||||
|
||||
Reference in new issue
Block a user