api.ts had grown to 35 exported symbols in one 432-line file covering
every backend endpoint. Split into webapp/src/lib/api/{shared,logs,
params,metrics,dashboards,alerts,settings}.ts, re-exported from
lib/api/index.ts as a barrel so none of the ~10 importing files needed
their import paths changed (TS/bundler module resolution falls back to
a directory's index.ts when the sibling .ts file is gone).
Also added buildSearchParams to replace fetchLogs/fetchTimeline's
repeated hand-written `if (x !== undefined) search.set(...)` blocks.
Left fetchMetricBucketValues/fetchPromqlSeries's query-building
untouched (byte-for-byte) since the ticket scoped the helper to
fetchLogs/fetchTimeline only.
Reimplemented directly against current main (ticket 089) after the
worktree agent's branch was based on a stale pre-theme-removal state
and reintroduced CSS-upload endpoints main had deliberately deleted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
// Error shape returned by API handlers on a 4xx/5xx response (see `ErrorDto` on the backend).
|
|
export interface ApiErrorBody {
|
|
error: string
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
constructor(message: string) {
|
|
super(message)
|
|
this.name = 'ApiError'
|
|
}
|
|
}
|
|
|
|
// Builds a `URLSearchParams` from an object of optional query fields, skipping any that are
|
|
// `undefined` (falsy strings like `''` are also skipped, matching the hand-written `if` checks
|
|
// this replaces).
|
|
export function buildSearchParams(fields: Record<string, string | number | boolean | undefined>): URLSearchParams {
|
|
const search = new URLSearchParams()
|
|
for (const [key, value] of Object.entries(fields)) {
|
|
if (value !== undefined && value !== '') search.set(key, String(value))
|
|
}
|
|
return search
|
|
}
|
|
|
|
export async function getJson<T>(path: string): Promise<T> {
|
|
const res = await fetch(path)
|
|
if (!res.ok) {
|
|
let message = `${path} failed: ${res.status}`
|
|
try {
|
|
const body: unknown = await res.json()
|
|
if (body && typeof body === 'object' && 'error' in body && typeof (body as { error: unknown }).error === 'string') {
|
|
message = (body as { error: string }).error
|
|
}
|
|
} catch {
|
|
// body wasn't JSON (or had no `error` field) — fall back to the generic message
|
|
}
|
|
throw new Error(message)
|
|
}
|
|
return res.json() as Promise<T>
|
|
}
|
|
|
|
// Throws `ApiError` (not `Error`) with the parsed `{ "error": "..." }` message so callers can surface it inline.
|
|
export async function postJson<T>(path: string, body: unknown): Promise<T> {
|
|
const res = await fetch(path, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
if (!res.ok) {
|
|
let message = `${path} failed: ${res.status}`
|
|
try {
|
|
const errorBody = (await res.json()) as Partial<ApiErrorBody>
|
|
if (errorBody && typeof errorBody.error === 'string') {
|
|
message = errorBody.error
|
|
}
|
|
} catch {
|
|
// response body wasn't JSON; fall back to the generic message
|
|
}
|
|
throw new ApiError(message)
|
|
}
|
|
return res.json() as Promise<T>
|
|
}
|
|
|
|
// Same error handling as `postJson` (throws `ApiError` with the parsed message) but for a PUT — used where updating in place, rather than creating, is the operation.
|
|
export async function putJson<T>(path: string, body: unknown): Promise<T> {
|
|
const res = await fetch(path, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
if (!res.ok) {
|
|
let message = `${path} failed: ${res.status}`
|
|
try {
|
|
const errorBody = (await res.json()) as Partial<ApiErrorBody>
|
|
if (errorBody && typeof errorBody.error === 'string') {
|
|
message = errorBody.error
|
|
}
|
|
} catch {
|
|
// response body wasn't JSON; fall back to the generic message
|
|
}
|
|
throw new ApiError(message)
|
|
}
|
|
return res.json() as Promise<T>
|
|
}
|
|
|
|
export async function sendJson<T>(path: string, method: string, body?: unknown): Promise<T> {
|
|
const res = await fetch(path, {
|
|
method,
|
|
headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
})
|
|
if (!res.ok) {
|
|
let message = `${path} failed: ${res.status}`
|
|
try {
|
|
const data = await res.json()
|
|
if (data?.error) message = data.error
|
|
} catch {
|
|
// response body wasn't JSON (or was empty) — fall back to the generic message
|
|
}
|
|
throw new Error(message)
|
|
}
|
|
if (res.status === 204) return undefined as T
|
|
return res.json() as Promise<T>
|
|
}
|