feat(core): custom field definitions and value validation
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d392a65aa7
commit
56b86e27da
@@ -0,0 +1,103 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { forEachDialect } from "@sttracker/db/testing";
|
||||
import { ValidationError } from "../src/errors.ts";
|
||||
import { defineCustomField, listCustomFields, validateCustomValues } from "../src/custom-fields.ts";
|
||||
import { makeUser } from "./user-layer-fixtures.ts";
|
||||
|
||||
forEachDialect("custom fields", (getDb) => {
|
||||
test("custom select field rejects unknown option", async () => {
|
||||
const db = getDb();
|
||||
const userId = await makeUser(db);
|
||||
const mood = await defineCustomField(db, userId, {
|
||||
name: "Mood",
|
||||
type: "single_select",
|
||||
options: ["cosy", "grim"],
|
||||
});
|
||||
expect(() => validateCustomValues([mood], { [mood.id]: "cosy" })).not.toThrow();
|
||||
let err: unknown;
|
||||
try {
|
||||
validateCustomValues([mood], { [mood.id]: "chaotic" });
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
expect(err).toBeInstanceOf(ValidationError);
|
||||
expect((err as ValidationError).field).toBe("customFields.Mood");
|
||||
expect((err as ValidationError).reason).toBe("unknown_option");
|
||||
});
|
||||
|
||||
test("listCustomFields filters by media type and stays per user", async () => {
|
||||
const db = getDb();
|
||||
const userId = await makeUser(db);
|
||||
const other = await makeUser(db, "bob");
|
||||
await defineCustomField(db, userId, { name: "Translator", type: "text", appliesTo: ["book", "manga"] });
|
||||
await defineCustomField(db, userId, { name: "Recommended by", type: "text" });
|
||||
await defineCustomField(db, other, { name: "Bob field", type: "text" });
|
||||
|
||||
expect((await listCustomFields(db, userId)).map((d) => d.name).sort()).toEqual(["Recommended by", "Translator"]);
|
||||
expect((await listCustomFields(db, userId, "game")).map((d) => d.name)).toEqual(["Recommended by"]);
|
||||
expect((await listCustomFields(db, userId, "manga")).map((d) => d.name).sort()).toEqual([
|
||||
"Recommended by",
|
||||
"Translator",
|
||||
]);
|
||||
});
|
||||
|
||||
test("defineCustomField rejects bad input", async () => {
|
||||
const db = getDb();
|
||||
const userId = await makeUser(db);
|
||||
await expect(defineCustomField(db, userId, { name: " ", type: "text" })).rejects.toThrow(ValidationError);
|
||||
await expect(defineCustomField(db, userId, { name: "X", type: "colour" as never })).rejects.toThrow(
|
||||
ValidationError,
|
||||
);
|
||||
await expect(defineCustomField(db, userId, { name: "S", type: "single_select" })).rejects.toThrow(
|
||||
ValidationError,
|
||||
);
|
||||
await defineCustomField(db, userId, { name: "Mood", type: "text" });
|
||||
await expect(defineCustomField(db, userId, { name: "mood", type: "text" })).rejects.toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
test("validateCustomValues checks every type", () => {
|
||||
const def = (id: string, type: string, options: string[] = []) => ({
|
||||
id,
|
||||
userId: "u",
|
||||
name: id,
|
||||
type: type as never,
|
||||
options,
|
||||
appliesTo: "all" as const,
|
||||
level: "entry" as const,
|
||||
createdAt: "",
|
||||
});
|
||||
const defs = [
|
||||
def("t", "text"),
|
||||
def("n", "number"),
|
||||
def("d", "date"),
|
||||
def("b", "boolean"),
|
||||
def("m", "multi_select", ["a", "b"]),
|
||||
def("u", "url"),
|
||||
def("r", "rating"),
|
||||
];
|
||||
expect(() =>
|
||||
validateCustomValues(defs, { t: "x", n: 1.5, d: "2019", b: true, m: ["a"], u: "https://x.org", r: 80 }),
|
||||
).not.toThrow();
|
||||
expect(() => validateCustomValues(defs, { t: null })).not.toThrow();
|
||||
const bad: Array<[Record<string, unknown>, string]> = [
|
||||
[{ nope: 1 }, "unknown_field"],
|
||||
[{ t: 3 }, "not_text"],
|
||||
[{ n: "3" }, "not_number"],
|
||||
[{ d: "2019-13" }, "not_date"],
|
||||
[{ b: 1 }, "not_boolean"],
|
||||
[{ m: ["c"] }, "unknown_option"],
|
||||
[{ m: "a" }, "not_list"],
|
||||
[{ u: "ftp:x" }, "not_url"],
|
||||
[{ r: 101 }, "out_of_range"],
|
||||
];
|
||||
for (const [values, reason] of bad) {
|
||||
let err: unknown;
|
||||
try {
|
||||
validateCustomValues(defs, values);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
expect((err as ValidationError).reason).toBe(reason);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user