46 lines
1.3 KiB
TypeScript
46 lines
1.3 KiB
TypeScript
|
|
import { ApiError } from './api'
|
||
|
|
|
||
|
|
/** Per-field validation messages: a 422 body's `{ errors: { field: [msg] } }`. */
|
||
|
|
export type FieldErrors = Record<string, string[]>
|
||
|
|
|
||
|
|
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<string, unknown>)
|
||
|
|
if (!entries.every(([, messages]) => isStringArray(messages))) {
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
|
||
|
|
return parsed.errors as FieldErrors
|
||
|
|
}
|