mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
280 lines
8.0 KiB
TypeScript
280 lines
8.0 KiB
TypeScript
import type { PrimaryFieldType } from "core";
|
|
import { isDebug } from "core/env";
|
|
import { encodeSearch } from "core/utils/reqres";
|
|
|
|
export type { PrimaryFieldType };
|
|
export type BaseModuleApiOptions = {
|
|
host: string;
|
|
basepath?: string;
|
|
token?: string;
|
|
headers?: Headers;
|
|
token_transport?: "header" | "cookie" | "none";
|
|
};
|
|
|
|
/** @deprecated */
|
|
export type ApiResponse<Data = any> = {
|
|
success: boolean;
|
|
status: number;
|
|
body: Data;
|
|
data?: Data extends { data: infer R } ? R : any;
|
|
res: Response;
|
|
};
|
|
|
|
export type TInput = string | (string | number | PrimaryFieldType)[];
|
|
|
|
export abstract class ModuleApi<Options extends BaseModuleApiOptions = BaseModuleApiOptions> {
|
|
protected fetcher?: typeof fetch;
|
|
|
|
constructor(protected readonly _options: Partial<Options> = {}) {}
|
|
|
|
protected getDefaultOptions(): Partial<Options> {
|
|
return {};
|
|
}
|
|
|
|
get options(): Options {
|
|
return {
|
|
host: "http://localhost",
|
|
token: undefined,
|
|
...this.getDefaultOptions(),
|
|
...this._options
|
|
} as Options;
|
|
}
|
|
|
|
/**
|
|
* used for SWR invalidation of basepath
|
|
*/
|
|
key(): string {
|
|
return this.options.basepath ?? "";
|
|
}
|
|
|
|
protected getUrl(path: string) {
|
|
const basepath = this.options.basepath ?? "";
|
|
return this.options.host + (basepath + "/" + path).replace(/\/{2,}/g, "/").replace(/\/$/, "");
|
|
}
|
|
|
|
protected request<Data = any>(
|
|
_input: TInput,
|
|
_query?: Record<string, any> | URLSearchParams,
|
|
_init?: RequestInit
|
|
): FetchPromise<ResponseObject<Data>> {
|
|
const method = _init?.method ?? "GET";
|
|
const input = Array.isArray(_input) ? _input.join("/") : _input;
|
|
let url = this.getUrl(input);
|
|
|
|
if (_query instanceof URLSearchParams) {
|
|
url += "?" + _query.toString();
|
|
} else if (typeof _query === "object") {
|
|
if (Object.keys(_query).length > 0) {
|
|
url += "?" + encodeSearch(_query);
|
|
}
|
|
}
|
|
|
|
const headers = new Headers(this.options.headers ?? {});
|
|
// add init headers
|
|
for (const [key, value] of Object.entries(_init?.headers ?? {})) {
|
|
headers.set(key, value as string);
|
|
}
|
|
|
|
headers.set("Accept", "application/json");
|
|
|
|
// only add token if initial headers not provided
|
|
if (this.options.token && this.options.token_transport === "header") {
|
|
//console.log("setting token", this.options.token);
|
|
headers.set("Authorization", `Bearer ${this.options.token}`);
|
|
}
|
|
|
|
let body: any = _init?.body;
|
|
if (_init && "body" in _init && ["POST", "PATCH", "PUT"].includes(method)) {
|
|
const requestContentType = (headers.get("Content-Type") as string) ?? undefined;
|
|
if (!requestContentType || requestContentType.startsWith("application/json")) {
|
|
body = JSON.stringify(_init.body);
|
|
headers.set("Content-Type", "application/json");
|
|
}
|
|
}
|
|
|
|
const request = new Request(url, {
|
|
..._init,
|
|
method,
|
|
body,
|
|
headers
|
|
});
|
|
|
|
return new FetchPromise(request, {
|
|
fetcher: this.fetcher
|
|
});
|
|
}
|
|
|
|
get<Data = any>(
|
|
_input: TInput,
|
|
_query?: Record<string, any> | URLSearchParams,
|
|
_init?: RequestInit
|
|
) {
|
|
return this.request<Data>(_input, _query, {
|
|
..._init,
|
|
method: "GET"
|
|
});
|
|
}
|
|
|
|
post<Data = any>(_input: TInput, body?: any, _init?: RequestInit) {
|
|
return this.request<Data>(_input, undefined, {
|
|
..._init,
|
|
body,
|
|
method: "POST"
|
|
});
|
|
}
|
|
|
|
patch<Data = any>(_input: TInput, body?: any, _init?: RequestInit) {
|
|
return this.request<Data>(_input, undefined, {
|
|
..._init,
|
|
body,
|
|
method: "PATCH"
|
|
});
|
|
}
|
|
|
|
put<Data = any>(_input: TInput, body?: any, _init?: RequestInit) {
|
|
return this.request<Data>(_input, undefined, {
|
|
..._init,
|
|
body,
|
|
method: "PUT"
|
|
});
|
|
}
|
|
|
|
delete<Data = any>(_input: TInput, _init?: RequestInit) {
|
|
return this.request<Data>(_input, undefined, {
|
|
..._init,
|
|
method: "DELETE"
|
|
});
|
|
}
|
|
}
|
|
|
|
export type ResponseObject<Body = any, Data = Body extends { data: infer R } ? R : Body> = Data & {
|
|
raw: Response;
|
|
res: Response;
|
|
data: Data;
|
|
body: Body;
|
|
ok: boolean;
|
|
status: number;
|
|
toJSON(): Data;
|
|
};
|
|
|
|
export function createResponseProxy<Body = any, Data = any>(
|
|
raw: Response,
|
|
body: Body,
|
|
data?: Data
|
|
): ResponseObject<Body, Data> {
|
|
const actualData = data ?? (body as unknown as Data);
|
|
const _props = ["raw", "body", "ok", "status", "res", "data", "toJSON"];
|
|
|
|
return new Proxy(actualData as any, {
|
|
get(target, prop, receiver) {
|
|
if (prop === "raw" || prop === "res") return raw;
|
|
if (prop === "body") return body;
|
|
if (prop === "data") return data;
|
|
if (prop === "ok") return raw.ok;
|
|
if (prop === "status") return raw.status;
|
|
if (prop === "toJSON") {
|
|
return () => target;
|
|
}
|
|
return Reflect.get(target, prop, receiver);
|
|
},
|
|
has(target, prop) {
|
|
if (_props.includes(prop as string)) {
|
|
return true;
|
|
}
|
|
return Reflect.has(target, prop);
|
|
},
|
|
ownKeys(target) {
|
|
return Array.from(new Set([...Reflect.ownKeys(target), ..._props]));
|
|
},
|
|
getOwnPropertyDescriptor(target, prop) {
|
|
if (_props.includes(prop as string)) {
|
|
return {
|
|
configurable: true,
|
|
enumerable: true,
|
|
value: Reflect.get({ raw, body, ok: raw.ok, status: raw.status }, prop)
|
|
};
|
|
}
|
|
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
}
|
|
}) as ResponseObject<Body, Data>;
|
|
}
|
|
|
|
export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
|
|
// @ts-ignore
|
|
[Symbol.toStringTag]: "FetchPromise";
|
|
|
|
constructor(
|
|
public request: Request,
|
|
protected options?: {
|
|
fetcher?: typeof fetch;
|
|
}
|
|
) {}
|
|
|
|
async execute(): Promise<ResponseObject<T>> {
|
|
// delay in dev environment
|
|
isDebug() && (await new Promise((resolve) => setTimeout(resolve, 200)));
|
|
|
|
const fetcher = this.options?.fetcher ?? fetch;
|
|
const res = await fetcher(this.request);
|
|
let resBody: any;
|
|
let resData: any;
|
|
|
|
const contentType = res.headers.get("Content-Type") ?? "";
|
|
if (contentType.startsWith("application/json")) {
|
|
resBody = await res.json();
|
|
if (typeof resBody === "object") {
|
|
resData = "data" in resBody ? resBody.data : resBody;
|
|
}
|
|
} else if (contentType.startsWith("text")) {
|
|
resBody = await res.text();
|
|
}
|
|
|
|
return createResponseProxy<T>(res, resBody, resData);
|
|
}
|
|
|
|
// biome-ignore lint/suspicious/noThenProperty: it's a promise :)
|
|
then<TResult1 = T, TResult2 = never>(
|
|
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
|
|
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
|
|
): Promise<TResult1 | TResult2> {
|
|
return this.execute().then(onfulfilled as any, onrejected);
|
|
}
|
|
|
|
catch<TResult = never>(
|
|
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined
|
|
): Promise<T | TResult> {
|
|
return this.then(undefined, onrejected);
|
|
}
|
|
|
|
finally(onfinally?: (() => void) | null | undefined): Promise<T> {
|
|
return this.then(
|
|
(value) => {
|
|
onfinally?.();
|
|
return value;
|
|
},
|
|
(reason) => {
|
|
onfinally?.();
|
|
throw reason;
|
|
}
|
|
);
|
|
}
|
|
|
|
path(): string {
|
|
const url = new URL(this.request.url);
|
|
return url.pathname;
|
|
}
|
|
|
|
key(options?: { search: boolean }): string {
|
|
const url = new URL(this.request.url);
|
|
return options?.search !== false ? this.path() + url.search : this.path();
|
|
}
|
|
|
|
keyArray(options?: { search: boolean }): string[] {
|
|
const url = new URL(this.request.url);
|
|
const path = this.path().split("/");
|
|
return (options?.search !== false ? [...path, url.searchParams.toString()] : path).filter(
|
|
Boolean
|
|
);
|
|
}
|
|
}
|