- 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 <[email protected]>
110 lines
3.9 KiB
TypeScript
110 lines
3.9 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { sql, type Kysely } from "kysely";
|
|
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";
|
|
import { forEachDialect } from "../src/testing.ts";
|
|
|
|
forEachDialect("schema", (getDb) => {
|
|
test("all tables exist and unique constraints hold", async () => {
|
|
const k = getDb().k;
|
|
const now = nowIso();
|
|
const userId = newId();
|
|
await k
|
|
.insertInto("users")
|
|
.values({
|
|
id: userId,
|
|
username: "ada",
|
|
display_name: "Ada",
|
|
password_hash: "x",
|
|
role: "admin",
|
|
preferences: toJson({}),
|
|
created_at: now,
|
|
updated_at: now,
|
|
})
|
|
.execute();
|
|
|
|
const workId = newId();
|
|
await k
|
|
.insertInto("works")
|
|
.values({
|
|
id: workId,
|
|
media_type: "book",
|
|
title: "Dune",
|
|
original_title: "Dune",
|
|
year: 1965,
|
|
release_date: null,
|
|
creators: toJson([]),
|
|
description: "",
|
|
cover_url: "",
|
|
cover_attachment_id: null,
|
|
metadata: toJson({}),
|
|
parent_id: null,
|
|
position: null,
|
|
owner_user_id: null,
|
|
merged_into_id: null,
|
|
created_at: now,
|
|
updated_at: now,
|
|
})
|
|
.execute();
|
|
|
|
await k
|
|
.insertInto("external_ids")
|
|
.values({ id: newId(), work_id: workId, namespace: "isbn", value: "123" })
|
|
.execute();
|
|
await expect(
|
|
k.insertInto("external_ids").values({ id: newId(), work_id: workId, namespace: "isbn", value: "123" }).execute(),
|
|
).rejects.toThrow();
|
|
|
|
const entry = {
|
|
user_id: userId,
|
|
work_id: workId,
|
|
status: "planned" as const,
|
|
rating: null,
|
|
review: null,
|
|
private_notes: null,
|
|
favorite: 0,
|
|
tags: toJson([]),
|
|
custom_fields: toJson({}),
|
|
visibility: "private" as const,
|
|
overrides: toJson({}),
|
|
media_type_override: null,
|
|
field_sources: toJson({}),
|
|
created_at: now,
|
|
updated_at: now,
|
|
deleted_at: null,
|
|
};
|
|
await k.insertInto("entries").values({ id: newId(), ...entry }).execute();
|
|
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.migrateTo(NO_MIGRATIONS);
|
|
expect(error).toBeUndefined();
|
|
const probe = sql`select 1 from users`.execute(k);
|
|
await expect(probe).rejects.toThrow();
|
|
});
|
|
});
|