Files
004.erp.generic/frontend/src/lib/api.ts
mariomitte 5d39e048ba
Some checks failed
ERP CI Pipeline / test (push) Has been cancelled
prvi kod za live uporabu
2026-07-09 21:21:17 +02:00

113 lines
3.0 KiB
TypeScript

export class ApiError extends Error {
status: number;
details?: unknown;
constructor(message: string, status: number, details?: unknown) {
super(message);
this.name = 'ApiError';
this.status = status;
this.details = details;
}
}
export interface FetchJsonOptions extends Omit<RequestInit, 'body'> {
body?: unknown;
timeoutMs?: number;
}
const API_BASE = import.meta.env.PUBLIC_API_URL || 'http://localhost:8001/api/';
function buildUrl(endpoint: string): string {
return new URL(endpoint.replace(/^\/+/, ''), API_BASE).toString();
}
function normalizeHeaders(headers: HeadersInit | undefined): Record<string, string> {
if (!headers) return {};
if (headers instanceof Headers) {
return Object.fromEntries(headers.entries());
}
if (Array.isArray(headers)) {
return Object.fromEntries(headers);
}
return { ...headers };
}
function createTimeoutSignal(signal: AbortSignal | null | undefined, timeoutMs: number): {
signal: AbortSignal;
clear: () => void;
} {
const timeoutController = new AbortController();
const timer = setTimeout(() => timeoutController.abort(), timeoutMs);
const clear = () => {
clearTimeout(timer);
};
if (signal) {
signal.addEventListener(
'abort',
() => {
clear();
timeoutController.abort();
},
{ once: true }
);
}
return { signal: timeoutController.signal, clear };
}
export async function fetchJson<T>(endpoint: string, options: FetchJsonOptions = {}): Promise<T> {
const {
body,
headers,
timeoutMs = 10000,
signal,
...rest
} = options;
const requestHeaders: Record<string, string> = {
Accept: 'application/json',
...normalizeHeaders(headers),
};
let requestBody: BodyInit | undefined;
if (body !== undefined && body !== null) {
requestHeaders['Content-Type'] = 'application/json';
requestBody = JSON.stringify(body);
}
const timeout = createTimeoutSignal(signal, timeoutMs);
let response: Response;
try {
response = await fetch(buildUrl(endpoint), {
...rest,
headers: requestHeaders,
body: requestBody,
signal: timeout.signal,
});
} finally {
timeout.clear();
}
if (!response.ok) {
let details: unknown = null;
try {
details = await response.json();
} catch {
details = await response.text().catch(() => null);
}
const message =
typeof details === 'object' && details && 'detail' in details
? String((details as { detail: unknown }).detail)
: `API request failed (${response.status})`;
throw new ApiError(message, response.status, details);
}
if (response.status === 204) {
return null as T;
}
return (await response.json()) as T;
}