2024-01-09 22:34:14 +00:00
|
|
|
import { PaginationShape } from "./types/PaginationShape";
|
|
|
|
|
2023-11-05 14:28:55 +00:00
|
|
|
export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
2023-11-03 12:17:53 +00:00
|
|
|
|
2023-11-03 20:14:17 +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 20:14:17 +00:00
|
|
|
> {
|
2023-11-03 12:17:53 +00:00
|
|
|
method: M;
|
|
|
|
endpoint: string;
|
2023-11-03 20:14:17 +00:00
|
|
|
pathParams: PP;
|
2023-11-03 12:17:53 +00:00
|
|
|
request?: T;
|
2024-01-12 02:56:22 +00:00
|
|
|
response?: R & (PG extends true ? any[] : any);
|
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":
|
2023-11-03 20:14:17 +00:00
|
|
|
if (data === null) return "null";
|
2023-11-03 12:17:53 +00:00
|
|
|
|
|
|
|
if (Array.isArray(data))
|
2023-11-03 20:14:17 +00:00
|
|
|
return data
|
|
|
|
.map((d) => nestedUrlSearchParams(d, true))
|
2023-11-03 12:17:53 +00:00
|
|
|
.map(encodeURIComponent)
|
|
|
|
.join("&");
|
|
|
|
|
2024-01-16 17:09:28 +00:00
|
|
|
const inner = Object.entries(data)
|
|
|
|
.filter(([_, v]) => typeof v !== "undefined")
|
|
|
|
.map(([k, v]) => [k, nestedUrlSearchParams(v, false)]);
|
2023-11-03 12:17:53 +00:00
|
|
|
|
|
|
|
return new URLSearchParams(inner).toString();
|
|
|
|
|
|
|
|
default:
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-05 14:28:55 +00:00
|
|
|
export type MagApiErrorCode = "Client:GenericApiError" | string;
|
2023-11-03 12:17:53 +00:00
|
|
|
|
|
|
|
export interface MagApiError {
|
|
|
|
status: number;
|
2023-11-03 20:14:17 +00:00
|
|
|
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;
|
2024-01-12 02:56:22 +00:00
|
|
|
data: T & any[];
|
2024-01-09 22:34:14 +00:00
|
|
|
next?: URL;
|
|
|
|
}
|
|
|
|
|
|
|
|
function extractHeaderRel(
|
|
|
|
headers: Headers,
|
|
|
|
rel: "prev" | "next"
|
|
|
|
): URL | undefined {
|
|
|
|
for (const [k, v] of headers) {
|
|
|
|
if (k.toLowerCase() !== "link") {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-01-12 03:47:00 +00:00
|
|
|
for (const links of v.split(",").map((s) => s.trim())) {
|
|
|
|
const [url, relPar] = links.split(";").map((s) => s.trim());
|
2024-01-09 22:34:14 +00:00
|
|
|
if (!url || !relPar) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const urlMatch = url.match(/^<(.+?)>$/)?.[1];
|
|
|
|
const relMatch = relPar.match(/rel="(.+?)"/)?.[1];
|
|
|
|
|
|
|
|
if (relMatch == rel && urlMatch) {
|
2024-01-12 02:56:22 +00:00
|
|
|
try {
|
|
|
|
return new URL(urlMatch);
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
return undefined;
|
|
|
|
}
|
2024-01-09 22:34:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2023-11-03 12:17:53 +00:00
|
|
|
export class MagApiClient {
|
|
|
|
private readonly baseUrl: string;
|
|
|
|
|
|
|
|
constructor(baseUrl: string) {
|
|
|
|
this.baseUrl = baseUrl;
|
|
|
|
}
|
|
|
|
|
2023-11-03 20:14:17 +00:00
|
|
|
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
|
2023-11-03 20:14:17 +00:00
|
|
|
>
|
|
|
|
>(
|
2024-01-09 22:34:14 +00:00
|
|
|
{ endpoint, method, paginated }: T,
|
|
|
|
data: T["request"] &
|
|
|
|
(T["paginated"] extends true ? Partial<PaginationShape> : any),
|
2023-11-03 20:14:17 +00:00
|
|
|
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;
|
|
|
|
|
2023-11-03 20:14:17 +00:00
|
|
|
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",
|
2023-11-03 20:14:17 +00:00
|
|
|
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) => {
|
2024-01-12 03:47:00 +00:00
|
|
|
console.error(
|
|
|
|
"Generic API error. This should not usually happen!",
|
|
|
|
e
|
|
|
|
);
|
2023-11-03 20:14:17 +00:00
|
|
|
throw {
|
2023-11-03 12:17:53 +00:00
|
|
|
status: -1,
|
|
|
|
code: "Client:GenericApiError",
|
2023-11-03 20:14:17 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|