mirror of
https://github.com/PeterMaquiran/tvone.git
synced 2026-04-23 12:35:51 +00:00
110 lines
3.0 KiB
TypeScript
110 lines
3.0 KiB
TypeScript
|
|
export type ApiMethod = "GET" | "POST" | "PATCH" | "DELETE";
|
||
|
|
|
||
|
|
export interface ApiClientConfig {
|
||
|
|
baseUrl: string;
|
||
|
|
getAccessToken?: () => string | Promise<string | undefined> | undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class ApiError extends Error {
|
||
|
|
status: number;
|
||
|
|
body: unknown;
|
||
|
|
|
||
|
|
constructor(status: number, message: string, body: unknown) {
|
||
|
|
super(message);
|
||
|
|
this.status = status;
|
||
|
|
this.body = body;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export class ApiClient {
|
||
|
|
private baseUrl: string;
|
||
|
|
private getAccessToken?: ApiClientConfig["getAccessToken"];
|
||
|
|
|
||
|
|
constructor(config: ApiClientConfig) {
|
||
|
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
||
|
|
this.getAccessToken = config.getAccessToken;
|
||
|
|
}
|
||
|
|
|
||
|
|
private async buildHeaders(extra?: HeadersInit): Promise<HeadersInit> {
|
||
|
|
const token = this.getAccessToken ? await this.getAccessToken() : undefined;
|
||
|
|
return {
|
||
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||
|
|
...extra,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private buildUrl(path: string, query?: Record<string, string | number | boolean | undefined>) {
|
||
|
|
const url = new URL(`${this.baseUrl}${path}`);
|
||
|
|
if (query) {
|
||
|
|
for (const [k, v] of Object.entries(query)) {
|
||
|
|
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return url.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
async request<TResponse, TBody = undefined>(params: {
|
||
|
|
method: ApiMethod;
|
||
|
|
path: string;
|
||
|
|
query?: Record<string, string | number | boolean | undefined>;
|
||
|
|
body?: TBody;
|
||
|
|
headers?: HeadersInit;
|
||
|
|
isFormData?: boolean;
|
||
|
|
}): Promise<TResponse> {
|
||
|
|
const { method, path, query, body, headers, isFormData } = params;
|
||
|
|
const finalHeaders = await this.buildHeaders(headers);
|
||
|
|
|
||
|
|
const response = await fetch(this.buildUrl(path, query), {
|
||
|
|
method,
|
||
|
|
headers: isFormData ? finalHeaders : { "Content-Type": "application/json", ...finalHeaders },
|
||
|
|
body: body
|
||
|
|
? isFormData
|
||
|
|
? (body as unknown as FormData)
|
||
|
|
: JSON.stringify(body)
|
||
|
|
: undefined,
|
||
|
|
});
|
||
|
|
|
||
|
|
const text = await response.text();
|
||
|
|
const data = text ? safeJsonParse(text) : null;
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new ApiError(response.status, response.statusText, data);
|
||
|
|
}
|
||
|
|
|
||
|
|
return data as TResponse;
|
||
|
|
}
|
||
|
|
|
||
|
|
get<TResponse>(path: string, query?: Record<string, string | number | boolean | undefined>) {
|
||
|
|
return this.request<TResponse>({ method: "GET", path, query });
|
||
|
|
}
|
||
|
|
|
||
|
|
post<TResponse, TBody = undefined>(
|
||
|
|
path: string,
|
||
|
|
body?: TBody,
|
||
|
|
options?: { headers?: HeadersInit; isFormData?: boolean }
|
||
|
|
) {
|
||
|
|
return this.request<TResponse, TBody>({
|
||
|
|
method: "POST",
|
||
|
|
path,
|
||
|
|
body,
|
||
|
|
headers: options?.headers,
|
||
|
|
isFormData: options?.isFormData,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
patch<TResponse, TBody = undefined>(path: string, body?: TBody) {
|
||
|
|
return this.request<TResponse, TBody>({ method: "PATCH", path, body });
|
||
|
|
}
|
||
|
|
|
||
|
|
delete<TResponse>(path: string) {
|
||
|
|
return this.request<TResponse>({ method: "DELETE", path });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function safeJsonParse(text: string) {
|
||
|
|
try {
|
||
|
|
return JSON.parse(text);
|
||
|
|
} catch {
|
||
|
|
return text;
|
||
|
|
}
|
||
|
|
}
|