import { ApiError } from './api' /** Per-field validation messages: a 422 body's `{ errors: { field: [msg] } }`. */ export type FieldErrors = Record function isStringArray(value: unknown): value is string[] { return Array.isArray(value) && value.every((item) => typeof item === 'string') } /** * Extract the `{ errors: { field: [...] } }` map from a 422 ApiError body. * Returns null for anything that isn't a well-formed 422 field-error response * (wrong status, non-ApiError, non-JSON body, or a malformed errors shape) so * the caller can fall back to a generic error message. */ export function parseFieldErrors(error: unknown): FieldErrors | null { if (!(error instanceof ApiError) || error.status !== 422) { return null } let parsed: unknown try { parsed = JSON.parse(error.body) } catch { return null } if ( typeof parsed !== 'object' || parsed === null || !('errors' in parsed) || typeof parsed.errors !== 'object' || parsed.errors === null ) { return null } // Only accept the contract shape: every field maps to an array of strings. const entries = Object.entries(parsed.errors as Record) if (!entries.every(([, messages]) => isStringArray(messages))) { return null } return parsed.errors as FieldErrors } function isViolation( value: unknown, ): value is { propertyPath: string; message: string } { return ( typeof value === 'object' && value !== null && 'propertyPath' in value && typeof value.propertyPath === 'string' && 'message' in value && typeof value.message === 'string' ) } /** * Extract the API Platform `{ violations: [{ propertyPath, message }] }` list * from a 422 ApiError body, grouped into a per-field map keyed by propertyPath * (multiple messages for the same path are collected in order). * * This is the sibling of {@link parseFieldErrors}: API Platform resources emit * this violations shape, while the hand-rolled auth endpoints emit the * `{ errors: { field: [...] } }` shape. Returns null for anything that isn't a * well-formed 422 violations response — including the auth shape — so the * caller can fall back to a generic error message. */ export function parseViolations(error: unknown): FieldErrors | null { if (!(error instanceof ApiError) || error.status !== 422) { return null } let parsed: unknown try { parsed = JSON.parse(error.body) } catch { return null } if ( typeof parsed !== 'object' || parsed === null || !('violations' in parsed) || !Array.isArray(parsed.violations) ) { return null } // Only accept the contract shape: every entry has a string propertyPath and // message. A malformed entry rejects the whole body rather than dropping it. if (!parsed.violations.every(isViolation)) { return null } const fieldErrors: FieldErrors = {} for (const { propertyPath, message } of parsed.violations) { if (!fieldErrors[propertyPath]) { fieldErrors[propertyPath] = [] } fieldErrors[propertyPath].push(message) } return fieldErrors }