37 lines
1.4 KiB
TypeScript
37 lines
1.4 KiB
TypeScript
/**
|
|
* The answer of a validate endpoint: problems maps a field of the validated value to a message; message describes what
|
|
* the value would break elsewhere, or is empty.
|
|
*/
|
|
export type Validation = {
|
|
problems: Record<string, string>
|
|
message: string
|
|
}
|
|
|
|
/** A failed call: the server's message and status, with the problems and message of a refused write (422). */
|
|
export class ApiError extends Error {
|
|
status: number
|
|
problems: Record<string, string>
|
|
detail: string
|
|
|
|
constructor(message: string, status: number, problems: Record<string, string> = {}, detail = '') {
|
|
super(message)
|
|
this.status = status
|
|
this.problems = problems
|
|
this.detail = detail
|
|
}
|
|
}
|
|
|
|
/** Calls the backend and returns the decoded JSON; a failed call throws ApiError with the backend's message. */
|
|
export async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
const response = await fetch(path, {
|
|
method,
|
|
headers: body === undefined ? {} : { 'Content-Type': 'application/json' },
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
})
|
|
if (!response.ok) {
|
|
const payload = (await response.json().catch(() => null)) as { error?: string; problems?: Record<string, string>; message?: string } | null
|
|
throw new ApiError(payload?.error ?? `${method} ${path} failed (${response.status}).`, response.status, payload?.problems, payload?.message)
|
|
}
|
|
return (await response.json()) as T
|
|
}
|