Files
YANPM/apps/frontend/app/lib/api.ts
2025-12-19 21:16:04 +08:00

122 lines
3.8 KiB
TypeScript

import type { AxiosInstance, AxiosResponse } from 'axios';
import { type Fetcher, type Method, createApiClient } from '../generated/api-client/api-client';
import { TanstackQueryApiClient } from '../generated/api-client/tanstack-client';
const API_BASE_URL: string | undefined = import.meta.env.VITE_API_BASE_URL;
const get_fetch: (axios: AxiosInstance) => Fetcher['fetch'] =
(axios) =>
async ({ method, url: incomingUrl, parameters: params }) => {
// Use a plain object for Axios headers
const headers: Record<string, string> = {};
// Replace path parameters (supports both {param} and :param formats)
const actualUrl = replacePathParams(incomingUrl.toString(), (params?.path ?? {}) as Record<string, string>);
const url = new URL(actualUrl);
// Handle query parameters
if (params?.query) {
const searchParams = new URLSearchParams();
Object.entries(params.query).forEach(([key, value]) => {
if (value != null) {
// Skip null/undefined values
if (Array.isArray(value)) {
value.forEach((val) => val != null && searchParams.append(key, String(val)));
} else {
searchParams.append(key, String(value));
}
}
});
url.search = searchParams.toString();
}
// Handle request body for mutation methods (use Axios `data`)
const data = (['post', 'put', 'patch', 'delete'] satisfies Method[] as string[]).includes(method.toLowerCase()) ? params?.body : undefined;
if (data != null) {
headers['Content-Type'] = 'application/json';
}
// Add custom headers
if (params?.header) {
Object.entries(params.header).forEach(([key, value]) => {
if (value != null) {
headers[key] = String(value);
}
});
}
const response = await axios(url.toString(), {
method: method.toUpperCase(),
...(data !== undefined && { data }),
headers: headers,
});
return axiosResponseToFetchResponse(response);
};
function axiosResponseToFetchResponse(response: AxiosResponse): Response {
const headers = new Headers();
Object.entries(response.headers).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((val) => headers.append(key, val));
} else {
headers.append(key, value);
}
});
// Normalize Axios response.data to a Fetch-compatible BodyInit
let body: BodyInit | null = null;
const data = response.data;
if (data == null) {
body = null;
} else if (
typeof data === 'string' ||
data instanceof Blob ||
data instanceof ArrayBuffer ||
ArrayBuffer.isView(data) ||
data instanceof FormData ||
data instanceof URLSearchParams
) {
body = data as BodyInit;
} else {
try {
body = JSON.stringify(data);
if (!headers.has('content-type')) {
headers.set('content-type', 'application/json;charset=utf-8');
}
} catch {
console.warn('Failed to stringify response data as JSON, falling back to string conversion.');
body = String(data);
}
}
return new Response(body, {
status: response.status,
statusText: response.statusText,
headers: headers,
});
}
/**
* Replace path parameters in URL
* Supports both OpenAPI format {param} and Express format :param
*/
function replacePathParams(url: string, params: Record<string, string>): string {
return url.replace(/{(\w+)}/g, (_, key: string) => params[key] || `{${key}}`).replace(/:([a-zA-Z0-9_]+)/g, (_, key: string) => params[key] || `:${key}`);
}
export function createApi(axios: AxiosInstance) {
return createApiClient(
{
fetch: get_fetch(axios),
},
API_BASE_URL ?? window.location.origin
);
}
export function createTanstackApi(axios: AxiosInstance) {
return new TanstackQueryApiClient(createApi(axios));
}