2025-03-31 15:18:09 +08:00

236 lines
7.3 KiB
TypeScript

/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { APIClient as APIClientSDK } from '@nocobase/sdk';
import { Result } from 'ahooks/es/useRequest/src/types';
import { notification } from 'antd';
import React from 'react';
import { Application } from '../application';
function notify(type, messages, instance) {
if (!messages?.length) {
return;
}
instance[type]({
message: messages.map?.((item: any, index) => {
return React.createElement(
'div',
{ key: `${index}_${item.message}` },
typeof item === 'string' ? item : item.message,
);
}),
});
}
const handleErrorMessage = (error, notification) => {
const reader = new FileReader();
reader.readAsText(error?.response?.data, 'utf-8');
reader.onload = function () {
const messages = JSON.parse(reader.result as string).errors;
notify('error', messages, notification);
};
};
function offsetToTimeZone(offset) {
const hours = Math.floor(Math.abs(offset));
const minutes = Math.abs((offset % 1) * 60);
const formattedHours = (hours < 10 ? '0' : '') + hours;
const formattedMinutes = (minutes < 10 ? '0' : '') + minutes;
const sign = offset >= 0 ? '+' : '-';
return sign + formattedHours + ':' + formattedMinutes;
}
const getCurrentTimezone = () => {
const timezoneOffset = new Date().getTimezoneOffset() / -60;
return offsetToTimeZone(timezoneOffset);
};
const errorCache = new Map();
export class APIClient extends APIClientSDK {
services: Record<string, Result<any, any>> = {};
silence = false;
app: Application;
/** 该值会在 AntdAppProvider 中被重新赋值 */
notification: any = notification;
cloneInstance() {
const api = new APIClient(this.options);
api.options = this.options;
api.services = this.services;
api.storage = this.storage;
api.app = this.app;
api.auth = this.auth;
api.storagePrefix = this.storagePrefix;
api.notification = this.notification;
const handlers = [];
for (const handler of this.axios.interceptors.response['handlers']) {
if (handler.rejected['_name'] === 'handleNotificationError') {
handlers.push({
...handler,
rejected: api.handleNotificationError.bind(api),
});
} else {
handlers.push(handler);
}
}
api.axios.interceptors.response['handlers'] = handlers;
return api;
}
getHeaders() {
const headers = super.getHeaders();
const appName = this.app?.getName();
if (appName) {
headers['X-App'] = appName;
}
headers['X-Timezone'] = getCurrentTimezone();
headers['X-Hostname'] = window?.location?.hostname;
return headers;
}
service(uid: string) {
return this.services[uid];
}
interceptors() {
this.axios.interceptors.request.use((config) => {
config.headers['X-With-ACL-Meta'] = true;
const headers = this.getHeaders();
Object.keys(headers).forEach((key) => {
config.headers[key] = config.headers[key] || headers[key];
});
return config;
});
super.interceptors();
this.useNotificationMiddleware();
this.axios.interceptors.response.use(
(response) => {
return response;
},
(error) => {
const errs = this.toErrMessages(error);
// Hard code here temporarily
// TODO(yangqia): improve error code and message
if (errs.find((error: { code?: string }) => error.code === 'ROLE_NOT_FOUND_ERR')) {
this.auth.setRole(null);
window.location.reload();
}
if (errs.find((error: { code?: string }) => error.code === 'TOKEN_INVALID' || error.code === 'USER_LOCKED')) {
this.auth.setToken(null);
}
if (errs.find((error: { code?: string }) => error.code === 'ROLE_NOT_FOUND_FOR_USER')) {
this.auth.setRole(null);
window.location.reload();
}
throw error;
},
);
}
toErrMessages(error) {
if (typeof error?.response?.data === 'string') {
const tempElement = document.createElement('div');
tempElement.innerHTML = error?.response?.data;
let message = tempElement.textContent || tempElement.innerText;
if (message.includes('Error occurred while trying')) {
message = 'The application may be starting up. Please try again later.';
return [{ code: 'APP_WARNING', message }];
}
if (message.includes('502 Bad Gateway')) {
message = 'The application may be starting up. Please try again later.';
return [{ code: 'APP_WARNING', message }];
}
return [{ message }];
}
if (error?.response?.data?.error) {
return [error?.response?.data?.error];
}
return (
error?.response?.data?.errors ||
error?.response?.data?.messages ||
error?.response?.error || [{ message: error.message || 'Server error' }]
);
}
async handleNotificationError(error) {
if (this.silence) {
// console.error(error);
// return;
throw error;
}
const skipNotify: boolean | ((error: any) => boolean) = error.config?.skipNotify;
if (skipNotify && ((typeof skipNotify === 'function' && skipNotify(error)) || skipNotify === true)) {
throw error;
}
const redirectTo = error?.response?.data?.redirectTo;
if (redirectTo) {
return (window.location.href = redirectTo);
}
if (error?.response?.data?.type === 'application/json') {
handleErrorMessage(error, this.notification);
} else {
if (errorCache.size > 10) {
errorCache.clear();
}
const maintaining = !!error?.response?.data?.error?.maintaining;
if (this.app.maintaining !== maintaining) {
this.app.maintaining = maintaining;
}
if (this.app.maintaining) {
this.app.error = error?.response?.data?.error;
throw error;
} else if (this.app.error) {
this.app.error = null;
}
let errs = this.toErrMessages(error);
errs = errs.filter((error) => {
const lastTime = errorCache.get(error.message);
if (lastTime && new Date().getTime() - lastTime < 500) {
return false;
}
errorCache.set(error.message, new Date().getTime());
return true;
});
if (errs.length === 0) {
throw error;
}
notify('error', errs, this.notification);
}
throw error;
}
useNotificationMiddleware() {
const errorHandler = this.handleNotificationError.bind(this);
errorHandler['_name'] = 'handleNotificationError';
this.axios.interceptors.response.use((response) => {
if (response.data?.messages?.length) {
const messages = response.data.messages.filter((item) => {
const lastTime = errorCache.get(typeof item === 'string' ? item : item.message);
if (lastTime && new Date().getTime() - lastTime < 500) {
return false;
}
errorCache.set(item.message, new Date().getTime());
return true;
});
notify('success', messages, this.notification);
}
return response;
}, errorHandler);
}
silent() {
const api = this.cloneInstance();
api.silence = true;
return api;
}
}