fix(core): race-safe matching, visibility-checked ID matches, child-preserving merges

- migration 0002: unique index on works (parent_id, position)
- matchWork retries the whole transaction on a unique violation (3 attempts)
- addMissingExternalIds uses ON CONFLICT DO NOTHING
- ID matches only return shared works or the user's own; private works cannot carry external ids
- adminMergeWorks requires shared works and repoints non-colliding children
- draft is computed for the aliased work id

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-30 19:29:48 +01:00
1 parent a46fb60eee
commit 437ca143e5
9 files changed
+270 -18

No files matched your search

+21 -2
View File
@@ -1,6 +1,6 @@
import { expect, test } from "bun:test";
import { sql, type Kysely } from "kysely";
import { Migrator } from "kysely/migration";
import { Migrator, NO_MIGRATIONS } from "kysely/migration";
import { newId, nowIso } from "../src/ids.ts";
import { toJson } from "../src/json.ts";
import { migrations } from "../src/migrations/index.ts";
@@ -79,10 +79,29 @@ forEachDialect("schema", (getDb) => {
await expect(k.insertInto("entries").values({ id: newId(), ...entry }).execute()).rejects.toThrow();
});
test("works (parent_id, position) is unique; null positions and null parents are not", async () => {
const k = getDb().k;
const now = nowIso();
const work = (id: string, parent: string | null, position: string | null) => ({
id, media_type: "show", title: "T", original_title: "", year: null, release_date: null, creators: "[]",
description: "", cover_url: "", cover_attachment_id: null, metadata: "{}", parent_id: parent, position,
owner_user_id: null, merged_into_id: null, created_at: now, updated_at: now,
});
const show = newId();
await k.insertInto("works").values(work(show, null, null)).execute();
await k.insertInto("works").values(work(newId(), null, null)).execute();
await k.insertInto("works").values(work(newId(), null, '{"season":1}')).execute();
await k.insertInto("works").values(work(newId(), null, '{"season":1}')).execute();
await k.insertInto("works").values(work(newId(), show, null)).execute();
await k.insertInto("works").values(work(newId(), show, null)).execute();
await k.insertInto("works").values(work(newId(), show, '{"season":1}')).execute();
await expect(k.insertInto("works").values(work(newId(), show, '{"season":1}')).execute()).rejects.toThrow();
});
test("migrating down removes the tables", async () => {
const k = getDb().k as Kysely<any>;
const migrator = new Migrator({ db: k, provider: { getMigrations: async () => migrations } });
const { error } = await migrator.migrateDown();
const { error } = await migrator.migrateTo(NO_MIGRATIONS);
expect(error).toBeUndefined();
const probe = sql`select 1 from users`.execute(k);
await expect(probe).rejects.toThrow();