feat: map 422 field errors for playground forms

This commit is contained in:
CREDO23 2026-07-23 18:52:50 +02:00
parent 18f5d30e61
commit d7f89bb3fd
2 changed files with 52 additions and 0 deletions

View file

@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { AppError } from "@/lib/error";
import { fieldErrorsFromError } from "./field-errors";
// Run with: pnpm exec tsx --test lib/playground/field-errors.test.ts
function validationError(fields: { loc: string[]; msg: string }[]): AppError {
const error = new AppError("Validation failed", 422, "Unprocessable Entity", "VALIDATION_ERROR");
error.fields = fields;
return error;
}
test("maps a body field failure to its top-level field name", () => {
const error = validationError([
{ loc: ["body", "startUrls", "0"], msg: "must be a valid http(s) URL" },
]);
assert.deepEqual(fieldErrorsFromError(error), {
startUrls: "must be a valid http(s) URL",
});
});
test("keeps the first failure per field", () => {
const error = validationError([
{ loc: ["body", "urls", "0"], msg: "first" },
{ loc: ["body", "urls", "3"], msg: "second" },
]);
assert.deepEqual(fieldErrorsFromError(error), { urls: "first" });
});
test("returns nothing for non-AppError or errors without fields", () => {
assert.deepEqual(fieldErrorsFromError(new Error("boom")), {});
assert.deepEqual(fieldErrorsFromError(new AppError("no fields", 500)), {});
});

View file

@ -0,0 +1,18 @@
import { AppError } from "@/lib/error";
/** Map a 422 response's field failures to ``{ fieldName: message }`` for the form. */
export function fieldErrorsFromError(error: unknown): Record<string, string> {
if (!(error instanceof AppError) || !error.fields) return {};
const errors: Record<string, string> = {};
for (const { loc, msg } of error.fields) {
const name = topLevelField(loc);
if (name && !errors[name]) errors[name] = msg;
}
return errors;
}
/** The input field a location path points at, dropping the ``body`` request root. */
function topLevelField(loc: string[]): string | undefined {
const path = loc[0] === "body" ? loc.slice(1) : loc;
return path[0];
}