Merge pull request #14759 from strapi/enhancement/axios-refactoring

useFetchClient hook replacing axiosInstance
This commit is contained in:
Gustav Hansen 2022-12-07 17:26:51 +01:00 committed by GitHub
commit 978630d655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 285 additions and 37 deletions

View File

@ -15,6 +15,7 @@ import Informations from '../index';
jest.mock('@strapi/helper-plugin', () => ({
useCMEditViewDataManager: jest.fn(),
wrapAxiosInstance: jest.fn(() => {}),
}));
const makeApp = () => {

View File

@ -1,5 +1,5 @@
import axios from 'axios';
import { auth } from '@strapi/helper-plugin';
import { auth, wrapAxiosInstance } from '@strapi/helper-plugin';
const instance = axios.create({
baseURL: process.env.STRAPI_ADMIN_BACKEND_URL,
@ -33,4 +33,6 @@ instance.interceptors.response.use(
}
);
export default instance;
const wrapper = wrapAxiosInstance(instance);
export default wrapper;

View File

@ -10,3 +10,4 @@ export { default as usePermissionsDataManager } from './usePermissionsDataManage
export { default as useReleaseNotification } from './useReleaseNotification';
export { default as useThemeToggle } from './useThemeToggle';
export { default as useRegenerate } from './useRegenerate';
export { default as useFetchClient } from './useFetchClient';

View File

@ -0,0 +1,23 @@
import { useEffect, useRef } from 'react';
import { getFetchClient } from '../../utils/getFetchClient';
const useFetchClient = () => {
const controller = useRef(null);
if (controller.current === null) {
controller.current = new AbortController();
}
useEffect(() => {
return () => {
controller.current.abort();
};
}, []);
const defaultOptions = {
signal: controller.current.signal,
};
return getFetchClient(defaultOptions);
};
export default useFetchClient;

View File

@ -11,6 +11,7 @@ jest.mock('react-redux', () => ({
jest.mock('@strapi/helper-plugin', () => ({
useTracking: jest.fn(() => ({ trackUsage: trackUsageMock })),
wrapAxiosInstance: jest.fn(() => {}),
}));
describe('Admin | pages | Admin | useTrackUsage', () => {

View File

@ -34,27 +34,32 @@ const ModalForm = ({ queryName, onToggle }) => {
const { formatMessage } = useIntl();
const toggleNotification = useNotification();
const { lockApp, unlockApp } = useOverlayBlocker();
const postMutation = useMutation((body) => axiosInstance.post('/admin/users', body), {
async onSuccess({ data }) {
setRegistrationToken(data.data.registrationToken);
await queryClient.invalidateQueries(queryName);
goNext();
setIsSubmitting(false);
const postMutation = useMutation(
(body) => {
return axiosInstance.post('/admin/users', body);
},
onError(err) {
setIsSubmitting(false);
{
async onSuccess({ data }) {
setRegistrationToken(data.data.registrationToken);
await queryClient.invalidateQueries(queryName);
goNext();
setIsSubmitting(false);
},
onError(err) {
setIsSubmitting(false);
toggleNotification({
type: 'warning',
message: { id: 'notification.error', defaultMessage: 'An error occured' },
});
toggleNotification({
type: 'warning',
message: { id: 'notification.error', defaultMessage: 'An error occured' },
});
throw err;
},
onSettled() {
unlockApp();
},
});
throw err;
},
onSettled() {
unlockApp();
},
}
);
const headerTitle = formatMessage({
id: 'Settings.permissions.users.create',

View File

@ -0,0 +1,45 @@
import axios from 'axios';
import { auth } from '@strapi/helper-plugin';
export const reqInterceptor = async (config) => {
config.headers = {
Authorization: `Bearer ${auth.getToken()}`,
Accept: 'application/json',
'Content-Type': 'application/json',
};
return config;
};
export const reqErrorInterceptor = (error) => {
return Promise.reject(error);
};
export const resInterceptor = (response) => response;
export const resErrorInterceptor = (error) => {
// whatever you want to do with the error
if (error?.response?.status === 401) {
auth.clearAppStorage();
window.location.reload();
}
throw error;
};
export const addInterceptors = (instance) => {
instance.interceptors.request.use(reqInterceptor, reqErrorInterceptor);
instance.interceptors.response.use(resInterceptor, resErrorInterceptor);
};
export const fetchClient = ({ baseURL }) => {
const instance = axios.create({
baseURL,
});
addInterceptors(instance);
return instance;
};
export default fetchClient({ baseURL: process.env.STRAPI_ADMIN_BACKEND_URL });

View File

@ -0,0 +1,10 @@
import instance from './fetchClient';
export const getFetchClient = (defaultOptions = {}) => {
return {
get: (url, config) => instance.get(url, { ...defaultOptions, ...config }),
put: (url, data, config) => instance.put(url, data, { ...defaultOptions, ...config }),
post: (url, data, config) => instance.post(url, data, { ...defaultOptions, ...config }),
del: (url, config) => instance.delete(url, { ...defaultOptions, ...config }),
};
};

View File

@ -0,0 +1,100 @@
import { AxiosError } from 'axios';
import { auth } from '@strapi/helper-plugin';
import {
reqInterceptor,
reqErrorInterceptor,
resInterceptor,
resErrorInterceptor,
fetchClient,
addInterceptors,
} from '../fetchClient';
const token = 'coolToken';
auth.getToken = jest.fn().mockReturnValue(token);
auth.clearAppStorage = jest.fn().mockReturnValue(token);
describe('ADMIN | utils | fetchClient', () => {
describe('Test the interceptors', () => {
it('API request should add authorization token to header', async () => {
const apiInstance = fetchClient({
baseUrl: 'http://strapi',
});
const result = await apiInstance.interceptors.request.handlers[0].fulfilled({ headers: {} });
expect(result.headers.Authorization).toContain(`Bearer ${token}`);
expect(result.headers.Accept).toBe('application/json');
expect(apiInstance.interceptors.response.handlers[0].fulfilled('foo')).toBe('foo');
});
describe('Test the addInterceptor function', () => {
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
it('should add a response interceptor to the fetchClient instance', () => {
const apiInstance = fetchClient({
baseUrl: 'http://strapi-test',
});
const spyReq = jest.spyOn(apiInstance.interceptors.request, 'use');
const spyRes = jest.spyOn(apiInstance.interceptors.response, 'use');
addInterceptors(apiInstance);
expect(spyReq).toHaveBeenCalled();
expect(spyRes).toHaveBeenCalled();
});
});
});
describe('Test the interceptors callbacks', () => {
beforeAll(() => {
Object.defineProperty(window, 'location', {
configurable: true,
value: { reload: jest.fn() },
});
});
afterAll(() => {
Object.defineProperty(window, 'location', { configurable: true, value: window.location });
});
it('should return the config object passed with the correct headers the request interceptor callback on success', async () => {
const configMock = {
headers: {
common: { Accept: 'application/json, text/plain, */*' },
delete: {},
get: {},
head: {},
post: { 'Content-Type': 'application/x-www-form-urlencoded' },
put: { 'Content-Type': 'application/x-www-form-urlencoded' },
patch: { 'Content-Type': 'application/x-www-form-urlencoded' },
},
method: 'get',
url: '/test',
};
const configResponse = await reqInterceptor(configMock);
expect(configResponse.headers.Authorization).toBe(`Bearer ${token}`);
});
});
it('should throw an error when the request interceptor error callback is called', () => {
expect(reqErrorInterceptor('test')).rejects.toBe('test');
expect(reqErrorInterceptor(new Error('test error'))).rejects.toThrow(new Error('test error'));
});
it('should return the response when the result interceptor callback is called', () => {
const response = {
msg: 'I am a response',
};
expect(resInterceptor(response).msg).toBe('I am a response');
});
it('should trigger the auth clearAppStorage and the window.location.reload when the result interceptor error callback is called', () => {
const error = new AxiosError('Unauthorized');
error.config = {};
error.request = {};
error.response = {
data: { data: null, error: [Object] },
status: 401,
statusText: 'Unauthorized',
};
jest.spyOn(window.location, 'reload');
try {
resErrorInterceptor(error);
} catch (error) {
expect(error.response.status).toBe(401);
expect(auth.clearAppStorage).toHaveBeenCalledTimes(1);
expect(window.location.reload).toHaveBeenCalledTimes(1);
}
});
});

View File

@ -0,0 +1,25 @@
import { auth } from '@strapi/helper-plugin';
import { getFetchClient } from '../getFetchClient';
const token = 'coolToken';
auth.getToken = jest.fn().mockReturnValue(token);
describe('ADMIN | utils | getFetchClient', () => {
it('should return the 4 HTTP methods to call GET, POST, PUT and DELETE apis', () => {
const response = getFetchClient();
expect(response).toHaveProperty('get');
expect(response).toHaveProperty('post');
expect(response).toHaveProperty('put');
expect(response).toHaveProperty('del');
});
it('should contain the headers config values and the data when we try to reach an unknown API', async () => {
const response = getFetchClient();
try {
await response.get('/test');
} catch (err) {
const { headers } = err.config;
expect(headers.Authorization).toContain(`Bearer ${token}`);
expect(headers.Accept).toBe('application/json');
}
});
});

View File

@ -108,6 +108,7 @@ const DataManagerProvider = ({
{ data: reservedNames },
] = await Promise.all(
['components', 'content-types', 'reserved-names'].map((endPoint) => {
// TODO: remember to pass also the pluginId when you use the new get, post, put, delete methods from getFetchClient
return axiosInstance.get(endPoint);
})
);
@ -265,7 +266,7 @@ const DataManagerProvider = ({
if (userConfirm) {
lockAppWithAutoreload();
// TODO: remember to pass also the pluginId when you use the new get, post, put, delete methods from getFetchClient
await axiosInstance.delete(requestURL);
// Make sure the server has restarted
@ -315,7 +316,7 @@ const DataManagerProvider = ({
}
lockAppWithAutoreload();
// TODO: remember to pass also the pluginId when you use the new get, post, put, delete methods from getFetchClient
await axiosInstance.delete(requestURL);
// Make sure the server has restarted
@ -349,6 +350,7 @@ const DataManagerProvider = ({
lockAppWithAutoreload();
// Update the category
// TODO: remember to pass also the pluginId when you use the new get, post, put, delete methods from getFetchClient
await axiosInstance({ url: requestURL, method: 'PUT', data: body });
// Make sure the server has restarted
@ -506,7 +508,7 @@ const DataManagerProvider = ({
// Lock the app
lockAppWithAutoreload();
// TODO: remember to pass also the pluginId when you use the new get, post, put, delete methods from getFetchClient
await axiosInstance({
url: requestURL,
method,

View File

@ -1,5 +1,6 @@
import axios from 'axios';
import { auth } from '@strapi/helper-plugin';
import { auth, wrapAxiosInstance } from '@strapi/helper-plugin';
// TODO: remember to pass also the pluginId when you use the new get, post, put, delete methods from getFetchClient
import pluginId from '../pluginId';
const instance = axios.create({
@ -34,4 +35,6 @@ instance.interceptors.response.use(
}
);
export default instance;
const wrapper = wrapAxiosInstance(instance);
export default wrapper;

View File

@ -1,5 +1,5 @@
import axios from 'axios';
import { auth } from '@strapi/helper-plugin';
import { auth, wrapAxiosInstance } from '@strapi/helper-plugin';
const instance = axios.create({
baseURL: process.env.STRAPI_ADMIN_BACKEND_URL,
@ -33,4 +33,6 @@ instance.interceptors.response.use(
}
);
export default instance;
const wrapper = wrapAxiosInstance(instance);
export default wrapper;

View File

@ -95,6 +95,7 @@ export { default as contentManagementUtilRemoveFieldsFromData } from './content-
export { default as getFileExtension } from './utils/getFileExtension/getFileExtension';
export * from './utils/stopPropagation';
export { default as difference } from './utils/difference';
export { default as wrapAxiosInstance } from './utils/wrapAxiosInstance';
export { default as request } from './utils/request';
export { default as getAPIInnerErrors } from './utils/getAPIInnerErrors';

View File

@ -0,0 +1,19 @@
function wrapAxiosInstance(instance) {
if (process.env.NODE_ENV !== 'development') return instance;
const wrapper = {};
['request', 'get', 'head', 'delete', 'options', 'post', 'put', 'patch', 'getUri'].forEach(
(methodName) => {
wrapper[methodName] = (...args) => {
console.log(
'Deprecation warning: Usage of "axiosInstance" utility is deprecated and will be removed in the next major release. Instead, use the useFetchClient() hook, which is exported from the admin: { useFetchClient } from "@strapi/helper-plugin"'
);
return instance[methodName](...args);
};
}
);
return wrapper;
}
export default wrapAxiosInstance;

View File

@ -3,7 +3,7 @@
*/
import axios from 'axios';
import { auth } from '@strapi/helper-plugin';
import { auth, wrapAxiosInstance } from '@strapi/helper-plugin';
const instance = axios.create({
baseURL: process.env.STRAPI_ADMIN_BACKEND_URL,
@ -37,4 +37,6 @@ instance.interceptors.response.use(
}
);
export default instance;
const wrapper = wrapAxiosInstance(instance);
export default wrapper;

View File

@ -3,7 +3,7 @@
*/
import axios from 'axios';
import { auth } from '@strapi/helper-plugin';
import { auth, wrapAxiosInstance } from '@strapi/helper-plugin';
const instance = axios.create({
baseURL: process.env.STRAPI_ADMIN_BACKEND_URL,
@ -37,4 +37,6 @@ instance.interceptors.response.use(
}
);
export default instance;
const wrapper = wrapAxiosInstance(instance);
export default wrapper;

View File

@ -1,5 +1,5 @@
import axios from 'axios';
import { auth } from '@strapi/helper-plugin';
import { auth, wrapAxiosInstance } from '@strapi/helper-plugin';
const instance = axios.create({
baseURL: process.env.STRAPI_ADMIN_BACKEND_URL,
@ -33,4 +33,6 @@ instance.interceptors.response.use(
}
);
export default instance;
const wrapper = wrapAxiosInstance(instance);
export default wrapper;

View File

@ -1,7 +1,7 @@
import { useCallback, useReducer, useEffect, useRef } from 'react';
import { useNotification } from '@strapi/helper-plugin';
import reducer, { initialState } from './reducer';
import axiosIntance from '../../utils/axiosInstance';
import axiosInstance from '../../utils/axiosInstance';
import pluginId from '../../pluginId';
const useFetchRole = (id) => {
@ -29,7 +29,7 @@ const useFetchRole = (id) => {
try {
const {
data: { role },
} = await axiosIntance.get(`/${pluginId}/roles/${roleId}`);
} = await axiosInstance.get(`/${pluginId}/roles/${roleId}`);
// Prevent updating state on an unmounted component
if (isMounted.current) {

View File

@ -1,5 +1,5 @@
import axios from 'axios';
import { auth } from '@strapi/helper-plugin';
import { auth, wrapAxiosInstance } from '@strapi/helper-plugin';
const instance = axios.create({
baseURL: process.env.STRAPI_ADMIN_BACKEND_URL,
@ -33,4 +33,6 @@ instance.interceptors.response.use(
}
);
export default instance;
const wrapper = wrapAxiosInstance(instance);
export default wrapper;