export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; export interface BackendApiEndpoint< M extends Method, PP extends string[], T, R > { method: M; endpoint: string; pathParams: PP; request?: T; response?: R; } function nestedUrlSearchParams(data: any, topLevel: boolean = true): string { switch (typeof data) { case "string": case "bigint": case "boolean": case "number": case "symbol": if (topLevel) return encodeURIComponent(data.toString()) + "="; return data.toString(); case "object": if (data === null) return "null"; if (Array.isArray(data)) return data .map((d) => nestedUrlSearchParams(d, true)) .map(encodeURIComponent) .join("&"); const inner = Object.entries(data).map(([k, v]) => [ k, nestedUrlSearchParams(v, false), ]); return new URLSearchParams(inner).toString(); default: return ""; } } export type MagApiErrorCode = "Client:GenericApiError" | string; export interface MagApiError { status: number; code: MagApiErrorCode; message: string; } export class MagApiClient { private readonly baseUrl: string; constructor(baseUrl: string) { this.baseUrl = baseUrl; } async call< T extends BackendApiEndpoint< T["method"] & Method, T["pathParams"] & string[], T["request"], T["response"] > >( { endpoint, method }: T, data: T["request"], pathParams: { [K in keyof T["pathParams"] as T["pathParams"][K] & string]: | string | number; }, token?: string | null | undefined ): Promise { type Response = T["response"]; const authorizationToken = token ?? undefined; const authorization = authorizationToken ? `Bearer ${authorizationToken}` : undefined; for (const name in pathParams) { endpoint = endpoint.replace(`:${name}`, `${pathParams[name]}`); } const baseUrl = this.baseUrl.replace(/\/+$/, ""); let url = `${baseUrl}${endpoint}`; if (method === "GET") { const query = nestedUrlSearchParams(data as any); if (query) { url += `?${query}`; } } return await fetch(url, { method, body: method !== "GET" ? JSON.stringify(data) : undefined, credentials: "omit", cache: "no-cache", headers: authorization ? { authorization } : {}, }) .then(async (res) => { const body = res.status === 204 ? null : await res.json(); if (res.status === 200) { return body as Response; } else if (res.status === 204) { return null as any as Response; } else { throw body as MagApiError; } }) .catch((e) => { throw { status: -1, code: "Client:GenericApiError", message: e, } as MagApiError; }); } }