Files
YouduWiki/web/app/api-templates/http-client.ejs
2026-05-21 19:52:45 +08:00

304 lines
11 KiB
Plaintext

<%
const { apiConfig, generateResponses, config } = it;
%>
import { message as alert } from "@ctzhian/ui";
import { notFound, redirect } from 'next/navigation';
import { getServerHeader, getServerPathname, getServerSearch, getServerBasePath } from '@/utils/getServerHeader';
export type QueryParamsType = Record<string | number, any>;
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
export interface FullRequestParams extends Omit<RequestInit, "body"> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
type?: ContentType;
/** query params */
query?: QueryParamsType;
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseFormat;
/** request body */
body?: unknown;
/** base url */
baseUrl?: string;
/** request cancellation token */
cancelToken?: CancelToken;
}
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path"> & { isAlert?: boolean }
export interface DomainResponse {
/** @example 200 */
code?: number;
data?: any;
/** @example "OK" */
message?: string;
}
type ExtractDataProp<T> = T extends { data?: infer U } ? U : never;
export interface ApiConfig<SecurityDataType = unknown> {
baseUrl?: string;
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
customFetch?: typeof fetch;
}
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
data: D;
error: E;
}
type CancelToken = Symbol | string | number;
export enum ContentType {
Json = "application/json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}
const pathnameWhiteList = ['/auth/login'];
const redirectToLogin = () => {
const redirectAfterLogin = encodeURIComponent(
location.href.replace(location.origin, '')
);
const search = `redirect=${redirectAfterLogin}`;
const pathname = `${window._BASE_PATH_ || ''}/auth/login`;
window.location.href = [pathname, search]?.join('?');
};
export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = '';
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private abortControllers = new Map<CancelToken, AbortController>();
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
private baseApiParams: RequestParams = {
credentials: 'same-origin',
headers: {},
redirect: 'follow',
referrerPolicy: 'no-referrer',
}
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
Object.assign(this, apiConfig);
}
public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data;
}
protected encodeQueryParam(key: string, value: any) {
const encodedKey = encodeURIComponent(key);
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
}
protected addQueryParam(query: QueryParamsType, key: string) {
return this.encodeQueryParam(key, query[key]);
}
protected addArrayQueryParam(query: QueryParamsType, key: string) {
const value = query[key];
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
}
protected toQueryString(rawQuery?: QueryParamsType): string {
const query = rawQuery || {};
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
return keys
.map((key) =>
Array.isArray(query[key])
? this.addArrayQueryParam(query, key)
: this.addQueryParam(query, key),
)
.join("&");
}
protected addQueryParams(rawQuery?: QueryParamsType): string {
const queryString = this.toQueryString(rawQuery);
return queryString ? `?${queryString}` : "";
}
private contentFormatters: Record<ContentType, (input: any) => any> = {
[ContentType.Json]: (input:any) => input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
[ContentType.Text]: (input:any) => input !== null && typeof input !== "string" ? JSON.stringify(input) : input,
[ContentType.FormData]: (input: any) =>
Object.keys(input || {}).reduce((formData, key) => {
const property = input[key];
formData.append(
key,
property instanceof Blob ?
property :
typeof property === "object" && property !== null ?
JSON.stringify(property) :
`${property}`
);
return formData;
}, new FormData()),
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
}
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
return {
...this.baseApiParams,
...params1,
...(params2 || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
};
}
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
if (this.abortControllers.has(cancelToken)) {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
return abortController.signal;
}
return void 0;
}
const abortController = new AbortController();
this.abortControllers.set(cancelToken, abortController);
return abortController.signal;
}
public abortRequest = (cancelToken: CancelToken) => {
const abortController = this.abortControllers.get(cancelToken)
if (abortController) {
abortController.abort();
this.abortControllers.delete(cancelToken);
}
}
public request = async <T = any, E = any>({
isAlert = true,
body,
secure,
path,
type,
query,
format,
baseUrl,
cancelToken,
...params
<% if (config.unwrapResponseData) { %>
}: FullRequestParams & { isAlert?: boolean }): Promise<ExtractDataProp<T>> => {
<% } else { %>
}: FullRequestParams & { isAlert?: boolean }): Promise<HttpResponse<T, E>> => {
<% } %>
const secureParams = ((typeof secure === 'boolean' ? secure : this.baseApiParams.secure) && this.securityWorker && await this.securityWorker(this.securityData)) || {};
const requestParams = this.mergeRequestParams(params, secureParams);
const queryString = query && this.toQueryString(query);
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
const responseFormat = format || requestParams.format || "json";
let customHeaders = {};
if (typeof window === 'undefined') {
customHeaders = await getServerHeader();
}
return this.customFetch(
`${baseUrl || this.baseUrl || (typeof window !== 'undefined' ? window._BASE_PATH_ : '')}${path}${queryString ? `?${queryString}` : ""}`,
{
...requestParams,
headers: {
...customHeaders,
...(requestParams.headers || {}),
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
},
signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null,
body: typeof body === "undefined" || body === null ? null : payloadFormatter(body),
}
).then(async (response) => {
if (response.status === 401) {
if (typeof window === 'undefined') {
const pathname = await getServerPathname();
if (!pathnameWhiteList.includes(pathname)) {
const search = await getServerSearch();
const basePath = await getServerBasePath();
redirect(`${basePath}/auth/login?redirect=${encodeURIComponent(pathname +search)}`);
}
return
}
if (typeof window !== 'undefined') {
if (!pathnameWhiteList.includes(window.location.pathname)) {
if (response.status === 401) {
redirectToLogin();
}
}
return
}
}
// if (response.status === 403) {
// console.log("response 403:", response);
// if (typeof window === "undefined") {
// const pathname = await getServerPathname();
// if (pathname !== "/block") {
// redirect("/block");
// }
// }
// if (typeof window !== "undefined") {
// const pathname = window.location.pathname;
// if (pathname !== "/block") {
// window.location.href = "/block";
// }
// }
// return Promise.reject(403);
// }
// if (response.status === 404) {
// if (typeof window === "undefined") {
// notFound();
// }
// }
let data: any = {};
try {
data = await response[responseFormat]();
} catch (error) {}
if (cancelToken) {
this.abortControllers.delete(cancelToken);
}
<% if (!config.disableThrowOnError) { %>
if (!response.ok || (data.code !== undefined && data.code !== 0) || (data.success !== undefined && !data.success)) {
if (typeof window !== 'undefined') {
const urlObj = new URL(response.url);
if (urlObj.pathname !== '/api/v1/user/profile') {
if (isAlert) {
alert.error(
(data as DomainResponse).message! || response.statusText
);
}
}
}
const errorMessage = { data, url: response.url, response };
console.log("response error:", errorMessage);
return Promise.reject({...data, code: response.status === 200 ? data.code : response.status});
}
<% } %>
<% if (config.unwrapResponseData) { %>
return data.data;
<% } else { %>
return data;
<% } %>
});
};
}
export default new HttpClient({ baseUrl: process.env.TARGET }).request;