Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

273 lines
8.1 KiB
TypeScript

import { sql } from "kysely";
import { afterEach, beforeEach, expect, it } from "vitest";
import type { Database } from "../../../src/database/types.js";
import {
describeEachDialect,
setupForDialect,
teardownForDialect,
type DialectTestContext,
} from "../../utils/test-db.js";
describeEachDialect("Content references schema", (dialect) => {
let ctx: DialectTestContext;
beforeEach(async () => {
ctx = await setupForDialect(dialect); // runs all migrations
});
afterEach(async () => {
await teardownForDialect(ctx);
});
it("creates _emdash_relations and _emdash_content_references", async () => {
for (const table of ["_emdash_relations", "_emdash_content_references"] as const) {
const rows = await ctx.db
.selectFrom(table as keyof Database)
.selectAll()
.execute();
expect(Array.isArray(rows), `table ${table} should exist`).toBe(true);
}
});
it("accepts a relation row and an edge row with the expected columns", async () => {
await ctx.db
.insertInto("_emdash_relations")
.values({
id: "rel_manages",
name: "manages",
parent_collection: "employees",
child_collection: "employees",
parent_label: "Manager",
child_label: "Direct report",
translation_group: "rel_manages",
})
.execute();
await ctx.db
.insertInto("_emdash_content_references")
.values({
id: "ref_1",
relation_group: "rel_manages",
parent_group: "grp_alice",
child_group: "grp_bob",
})
.execute();
const rel = await ctx.db
.selectFrom("_emdash_relations")
.selectAll()
.where("name", "=", "manages")
.executeTakeFirstOrThrow();
expect(rel.locale).toBe("en"); // default locale backfill
expect(rel.child_collection).toBe("employees");
const edge = await ctx.db
.selectFrom("_emdash_content_references")
.selectAll()
.where("id", "=", "ref_1")
.executeTakeFirstOrThrow();
expect(edge.relation_group).toBe("rel_manages");
expect(edge.sort_order).toBe(0); // default
});
it("rejects a duplicate edge (same relation, parent, child)", async () => {
await ctx.db
.insertInto("_emdash_content_references")
.values({ id: "e1", relation_group: "r1", parent_group: "p1", child_group: "c1" })
.execute();
await expect(
ctx.db
.insertInto("_emdash_content_references")
.values({ id: "e2", relation_group: "r1", parent_group: "p1", child_group: "c1" })
.execute(),
).rejects.toThrow();
});
it("rejects a duplicate relation name within one locale, allows across locales", async () => {
const base = {
name: "manages",
parent_collection: "employees",
child_collection: "employees",
parent_label: "Manager",
child_label: "Report",
translation_group: "tg1",
};
await ctx.db
.insertInto("_emdash_relations")
.values({ id: "r_en", locale: "en", ...base })
.execute();
// Same (name, locale) -> rejected
await expect(
ctx.db
.insertInto("_emdash_relations")
.values({ id: "r_en2", locale: "en", ...base })
.execute(),
).rejects.toThrow();
// Same name, different locale -> allowed
await ctx.db
.insertInto("_emdash_relations")
.values({
id: "r_fr",
locale: "fr",
...base,
parent_label: "Responsable",
child_label: "Subordonné",
})
.execute();
const rows = await ctx.db
.selectFrom("_emdash_relations")
.select(["id", "locale"])
.where("name", "=", "manages")
.execute();
expect(rows).toHaveLength(2);
});
it("rejects a duplicate (translation_group, locale), allows across locales", async () => {
const base = {
parent_collection: "employees",
child_collection: "employees",
parent_label: "Manager",
child_label: "Report",
translation_group: "shared_tg",
};
await ctx.db
.insertInto("_emdash_relations")
.values({ id: "g_en", name: "manages", locale: "en", ...base })
.execute();
// Same (translation_group, locale) -> rejected by the partial unique,
// even though `name` differs.
await expect(
ctx.db
.insertInto("_emdash_relations")
.values({ id: "g_en2", name: "leads", locale: "en", ...base })
.execute(),
).rejects.toThrow();
// Same translation_group, different locale -> allowed.
await ctx.db
.insertInto("_emdash_relations")
.values({ id: "g_fr", name: "gere", locale: "fr", ...base })
.execute();
const rows = await ctx.db
.selectFrom("_emdash_relations")
.select(["id", "locale"])
.where("translation_group", "=", "shared_tg")
.execute();
expect(rows).toHaveLength(2);
});
it("rejects a relation with a null translation_group", async () => {
// translation_group is NOT NULL: a relation must be addressable by edges
// (`_emdash_content_references.relation_group` is NOT NULL), so a null
// group would be an unreferenceable, dead row.
await expect(
ctx.db
.insertInto("_emdash_relations")
.values({
id: "n1",
name: "manages",
parent_collection: "employees",
child_collection: "employees",
parent_label: "Manager",
child_label: "Report",
locale: "en",
translation_group: null as unknown as string,
})
.execute(),
).rejects.toThrow();
});
it("forward and backlink traversal return the expected rows", async () => {
// Parent p1 references children c1, c2 (ordered); p2 also references c1.
await ctx.db
.insertInto("_emdash_content_references")
.values([
{ id: "e1", relation_group: "r1", parent_group: "p1", child_group: "c1", sort_order: 0 },
{ id: "e2", relation_group: "r1", parent_group: "p1", child_group: "c2", sort_order: 1 },
{ id: "e3", relation_group: "r1", parent_group: "p2", child_group: "c1", sort_order: 0 },
])
.execute();
// Forward: p1's children for relation r1, ordered.
const children = await ctx.db
.selectFrom("_emdash_content_references")
.select("child_group")
.where("parent_group", "=", "p1")
.where("relation_group", "=", "r1")
.orderBy("sort_order")
.execute();
expect(children.map((r) => r.child_group)).toEqual(["c1", "c2"]);
// Backlink: who references c1 (any parent) for relation r1.
const parents = await ctx.db
.selectFrom("_emdash_content_references")
.select("parent_group")
.where("child_group", "=", "c1")
.where("relation_group", "=", "r1")
.orderBy("parent_group")
.execute();
expect(parents.map((r) => r.parent_group)).toEqual(["p1", "p2"]);
});
it("allows same-collection and self references", async () => {
// Self reference: parent_group === child_group is permitted.
await ctx.db
.insertInto("_emdash_content_references")
.values({ id: "self1", relation_group: "r1", parent_group: "x1", child_group: "x1" })
.execute();
const row = await ctx.db
.selectFrom("_emdash_content_references")
.selectAll()
.where("id", "=", "self1")
.executeTakeFirstOrThrow();
expect(row.parent_group).toBe(row.child_group);
});
it("creates the expected indexes (sqlite)", async () => {
if (ctx.dialect !== "sqlite") return; // index introspection is dialect-specific
const result = await sql<{ name: string }>`
SELECT name FROM sqlite_master WHERE type = 'index'
`.execute(ctx.db);
const names = new Set(result.rows.map((r) => r.name));
for (const idx of [
"idx__emdash_relations_locale",
"idx__emdash_relations_translation_group",
"idx__emdash_relations_parent_collection",
"idx__emdash_relations_child_collection",
"idx__emdash_relations_group_locale_unique",
"idx__emdash_content_references_parent",
"idx__emdash_content_references_child",
"idx__emdash_content_references_relation",
]) {
expect(names.has(idx), `missing index ${idx}`).toBe(true);
}
});
it("down() drops both tables and up() can recreate them", async () => {
const { down, up } = await import("../../../src/database/migrations/043_content_references.js");
await down(ctx.db);
// Tables gone: a raw query against them should reject.
await expect(sql`SELECT 1 FROM _emdash_content_references`.execute(ctx.db)).rejects.toThrow();
await expect(sql`SELECT 1 FROM _emdash_relations`.execute(ctx.db)).rejects.toThrow();
// Re-applying up() restores them.
await up(ctx.db);
const rows = await ctx.db.selectFrom("_emdash_relations").selectAll().execute();
expect(Array.isArray(rows)).toBe(true);
});
});