File size: 2,340 Bytes
fc04d53 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | // @polsia:framework-owned — DO NOT EDIT. Bridges a route handler's 400 body
// onto react-hook-form so server-side field validation surfaces on the form.
//
// ROUTE 400 CONTRACT this consumes (emit exactly one of these from a handler
// when validation fails):
// A) flat map — { errors: { <field>: '<message>' } } (preferred shape;
// this is what the reference route at src/app/api/example/route.ts emits)
// B) zod shape — { errors: { fieldErrors: { <field>: ['<message>', ...] } } }
// i.e. the result of `zodError.flatten()`. Useful if a handler forwards a
// flatten() verbatim. The first message per field is used.
// Anything else is ignored (the caller should fall back to a generic toast).
import type { FieldValues, Path, UseFormSetError } from 'react-hook-form';
import { z } from 'zod';
// Shape A: flat field -> message.
const flatErrors = z.record(z.string(), z.string());
// Shape B: zod flatten() — formErrors[] + fieldErrors{ field: string[] }.
const flattenErrors = z.object({
formErrors: z.array(z.string()).optional(),
fieldErrors: z.record(z.string(), z.array(z.string())),
});
// The 400 envelope: { errors: A | B }.
const errorBody = z.object({
errors: z.union([flattenErrors, flatErrors]),
});
/**
* Map a route handler's 400 body onto react-hook-form field errors.
*
* @returns true if at least one field error was applied (so the caller can skip
* a generic error toast), false if the body did not match the 400 contract.
*/
export function applyServerErrors<TFieldValues extends FieldValues>(
body: unknown,
setError: UseFormSetError<TFieldValues>,
): boolean {
const parsed = errorBody.safeParse(body);
if (!parsed.success) {
return false;
}
const { errors } = parsed.data;
// Normalise both shapes to a flat field -> first-message map.
const flat: Record<string, string> =
'fieldErrors' in errors
? Object.fromEntries(
Object.entries(errors.fieldErrors)
.map(([field, messages]) => [field, messages[0]])
.filter((entry): entry is [string, string] => entry[1] !== undefined),
)
: errors;
let applied = false;
for (const [field, message] of Object.entries(flat)) {
setError(field as Path<TFieldValues>, { type: 'server', message });
applied = true;
}
return applied;
}
|