6931dc9ff7
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
/**
|
|
* Core fetch wrapper - reusable across modules
|
|
*/
|
|
|
|
export interface FetchWrapperOptions {
|
|
/** Additional headers merged into every request */
|
|
headers?: Record<string, string>;
|
|
/** Override default Content-Type (default: application/json) */
|
|
contentType?: string;
|
|
/** Override default Accept type (default: application/json) */
|
|
accept?: string;
|
|
/** Called before every request */
|
|
beforeRequest?: () => Promise<void>;
|
|
/** Called after every successful non-stream response */
|
|
afterResponse?: (data: any) => void;
|
|
}
|
|
|
|
export interface RequestCallOptions {
|
|
/** When set, the raw Response is forwarded to this handler instead of being JSON-parsed */
|
|
onStream?: (response: Response) => Promise<void>;
|
|
/** Per-call header overrides — highest priority, wins over factory-level headers */
|
|
headers?: Record<string, string>;
|
|
}
|
|
|
|
/** Error thrown for non-2xx responses, carrying the HTTP status code */
|
|
export class FetchError extends Error {
|
|
constructor(message: string, public readonly status: number) {
|
|
super(message);
|
|
this.name = 'FetchError';
|
|
}
|
|
}
|
|
|
|
function getCsrfToken(): string | null {
|
|
if (typeof document === 'undefined') return null;
|
|
const cookie = document.cookie.split('; ').find(r => r.startsWith('X-CSRF-TOKEN='));
|
|
return cookie ? (cookie.split('=')[1] ?? null) : null;
|
|
}
|
|
|
|
export function createFetchWrapper(options: FetchWrapperOptions = {}) {
|
|
async function request(
|
|
method: string,
|
|
url: string,
|
|
body?: object,
|
|
callOptions?: RequestCallOptions
|
|
): Promise<any> {
|
|
if (options.beforeRequest) {
|
|
await options.beforeRequest();
|
|
}
|
|
|
|
// Header priority: defaults < factory options.headers < callOptions.headers
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': options.contentType ?? 'application/json',
|
|
'Accept': options.accept ?? 'application/json',
|
|
...options.headers,
|
|
...callOptions?.headers,
|
|
};
|
|
|
|
const csrf = getCsrfToken();
|
|
if (csrf) headers['X-CSRF-TOKEN'] = csrf;
|
|
|
|
const requestOptions: RequestInit = {
|
|
method,
|
|
headers,
|
|
credentials: 'include',
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
};
|
|
|
|
const response = await fetch(url, requestOptions);
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
let data = null;
|
|
try {
|
|
data = text ? JSON.parse(text) : null;
|
|
} catch { /* non-JSON error body */ }
|
|
throw new FetchError(data?.message || response.statusText, response.status);
|
|
}
|
|
|
|
if (callOptions?.onStream) {
|
|
return callOptions.onStream(response);
|
|
}
|
|
|
|
const text = await response.text();
|
|
const data = text ? JSON.parse(text) : null;
|
|
|
|
options.afterResponse?.(data);
|
|
|
|
return data;
|
|
}
|
|
|
|
return {
|
|
get: (url: string, callOptions?: RequestCallOptions) =>
|
|
request('GET', url, undefined, callOptions),
|
|
post: (url: string, body?: object, callOptions?: RequestCallOptions) =>
|
|
request('POST', url, body, callOptions),
|
|
put: (url: string, body?: object, callOptions?: RequestCallOptions) =>
|
|
request('PUT', url, body, callOptions),
|
|
patch: (url: string, body?: object, callOptions?: RequestCallOptions) =>
|
|
request('PATCH', url, body, callOptions),
|
|
delete: (url: string, callOptions?: RequestCallOptions) =>
|
|
request('DELETE', url, undefined, callOptions),
|
|
};
|
|
}
|