magnetar/fe_calckey/frontend/magnetar-common/src/be-api.ts

188 lines
5.2 KiB
TypeScript
Raw Normal View History

2024-01-09 22:34:14 +00:00
import { PaginationShape } from "./types/PaginationShape";
export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
2023-11-03 12:17:53 +00:00
export interface BackendApiEndpoint<
M extends Method,
PP extends string[],
T,
2024-01-09 22:34:14 +00:00
R,
PG extends boolean
> {
2023-11-03 12:17:53 +00:00
method: M;
endpoint: string;
pathParams: PP;
2023-11-03 12:17:53 +00:00
request?: T;
response?: R;
2024-01-09 22:34:14 +00:00
paginated: PG;
2023-11-03 12:17:53 +00:00
}
function nestedUrlSearchParams(data: any, topLevel: boolean = true): string {
switch (typeof data) {
case "string":
case "bigint":
case "boolean":
case "number":
case "symbol":
2023-11-07 20:15:35 +00:00
if (topLevel) return encodeURIComponent(data.toString());
2023-11-03 12:17:53 +00:00
return data.toString();
case "object":
if (data === null) return "null";
2023-11-03 12:17:53 +00:00
if (Array.isArray(data))
return data
.map((d) => nestedUrlSearchParams(d, true))
2023-11-03 12:17:53 +00:00
.map(encodeURIComponent)
.join("&");
const inner = Object.entries(data).map(([k, v]) => [
k,
nestedUrlSearchParams(v, false),
]);
2023-11-03 12:17:53 +00:00
return new URLSearchParams(inner).toString();
default:
return "";
}
}
export type MagApiErrorCode = "Client:GenericApiError" | string;
2023-11-03 12:17:53 +00:00
export interface MagApiError {
status: number;
code: MagApiErrorCode;
message: string;
2023-11-03 12:17:53 +00:00
}
2024-01-09 22:34:14 +00:00
export interface PaginatedResult<T> {
prev?: URL;
data: T;
next?: URL;
}
function extractHeaderRel(
headers: Headers,
rel: "prev" | "next"
): URL | undefined {
for (const [k, v] of headers) {
if (k.toLowerCase() !== "link") {
continue;
}
for (const links of v.split(",").map(String.prototype.trim)) {
const [url, relPar] = links.split(";").map(String.prototype.trim);
if (!url || !relPar) {
continue;
}
const urlMatch = url.match(/^<(.+?)>$/)?.[1];
const relMatch = relPar.match(/rel="(.+?)"/)?.[1];
if (relMatch == rel && urlMatch) {
return new URL(urlMatch);
}
}
}
return undefined;
}
2023-11-03 12:17:53 +00:00
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"],
2024-01-09 22:34:14 +00:00
T["response"],
T["paginated"] & boolean
>
>(
2024-01-09 22:34:14 +00:00
{ endpoint, method, paginated }: T,
data: T["request"] &
(T["paginated"] extends true ? Partial<PaginationShape> : any),
pathParams: {
[K in keyof T["pathParams"] as T["pathParams"][K] & string]:
| string
| number;
},
2023-11-03 12:17:53 +00:00
token?: string | null | undefined
2024-01-09 22:34:14 +00:00
): Promise<
T["paginated"] extends true
? PaginatedResult<T["response"]>
: T["response"]
> {
2023-11-03 12:17:53 +00:00
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]}`);
}
2023-11-05 19:31:50 +00:00
const baseUrl = this.baseUrl.replace(/\/+$/, "");
let url = `${baseUrl}${endpoint}`;
2023-11-03 12:17:53 +00:00
if (method === "GET") {
const query = nestedUrlSearchParams(data as any);
if (query) {
url += `?${query}`;
}
}
2024-01-09 22:34:14 +00:00
return (await fetch(url, {
2023-11-03 12:17:53 +00:00
method,
body: method !== "GET" ? JSON.stringify(data) : undefined,
credentials: "omit",
cache: "no-cache",
headers: authorization ? { authorization } : {},
2023-11-03 12:17:53 +00:00
})
.then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
2024-01-09 22:34:14 +00:00
if (paginated) {
return {
prev: extractHeaderRel(res.headers, "prev"),
data: body as Response,
next: extractHeaderRel(res.headers, "next"),
};
} else {
return body as Response;
}
2023-11-03 12:17:53 +00:00
} else if (res.status === 204) {
2024-01-09 22:34:14 +00:00
if (paginated) {
return {
prev: extractHeaderRel(res.headers, "prev"),
data: null as any as Response,
next: extractHeaderRel(res.headers, "next"),
};
} else {
return null as any as Response;
}
2023-11-03 12:17:53 +00:00
} else {
throw body as MagApiError;
}
})
.catch((e) => {
throw {
2023-11-03 12:17:53 +00:00
status: -1,
code: "Client:GenericApiError",
message: e,
} as MagApiError;
2024-01-09 22:34:14 +00:00
})) as T["paginated"] extends true
? PaginatedResult<Response>
: Response;
2023-11-03 12:17:53 +00:00
}
}