From f27467aefd4a495f852a1f6458762ca224495642 Mon Sep 17 00:00:00 2001 From: Ali Amori Kadhim Date: Fri, 27 Sep 2024 17:31:19 +0200 Subject: [PATCH] feat: add `fetchData` to utils package - improve the code - add unit test --- packages/utils/src/fetchData/index.test.ts | 216 +++++++++++++++++++++ packages/utils/src/fetchData/index.ts | 98 ++++++++++ 2 files changed, 314 insertions(+) create mode 100644 packages/utils/src/fetchData/index.test.ts create mode 100644 packages/utils/src/fetchData/index.ts diff --git a/packages/utils/src/fetchData/index.test.ts b/packages/utils/src/fetchData/index.test.ts new file mode 100644 index 00000000..85b9d9eb --- /dev/null +++ b/packages/utils/src/fetchData/index.test.ts @@ -0,0 +1,216 @@ +/* eslint-disable no-undef */ +import { fetchData } from './index'; + +const url = 'https://api.example.com/graphql'; +const query = ` +query { + users { + id + name + } +} +`; + +describe('fetchData', () => { + it('should fetch data successfully', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ data: { users: [] } }), + ok: true, + status: 200, + statusText: 'OK', + }), + ) as jest.Mock; + global.fetch = mockFetch; + + const data = await fetchData({ url, query }); + expect(data).toEqual({ data: { users: [] } }); + expect(mockFetch).toHaveBeenCalledWith(url, { + method: 'POST', + cache: 'no-store', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); + }); + it('should have POST method by default', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ data: { users: [] } }), + ok: true, + status: 200, + statusText: 'OK', + }), + ) as jest.Mock; + global.fetch = mockFetch; + const data = await fetchData({ url, query }); + expect(data).toEqual({ data: { users: [] } }); + expect(mockFetch).toHaveBeenCalledWith(url, { + method: 'POST', + cache: 'no-store', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); + }); + it('should handle GET method', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ data: { users: [] } }), + ok: true, + status: 200, + statusText: 'OK', + }), + ) as jest.Mock; + global.fetch = mockFetch; + const data = await fetchData({ url: 'https://example.com/api/users', method: 'GET' }); + expect(data).toEqual({ data: { users: [] } }); + expect(mockFetch).toHaveBeenCalledWith('https://example.com/api/users', { + method: 'GET', + cache: 'no-store', + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + it('should handle cache', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ data: { users: [] } }), + ok: true, + status: 200, + statusText: 'OK', + }), + ) as jest.Mock; + global.fetch = mockFetch; + + const data = await fetchData({ url, query }); + expect(data).toEqual({ data: { users: [] } }); + expect(mockFetch).toHaveBeenCalledWith(url, { + cache: 'no-store', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); + }); + it('should handle network error', async () => { + const mockFetch = jest.fn(() => Promise.reject(new Error('Network error'))); + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Network error'); + }); + it('should handle error', async () => { + const mockFetch = jest.fn(() => Promise.reject(new Error('Fetch failed'))); + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow(); + }); + it('should handle error with status code', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ errors: [{ message: 'Not found' }] }), + ok: false, + status: 404, + }), + ) as jest.Mock; + global.fetch = mockFetch; + + expect(fetchData({ url, query })).rejects.toThrow('Not found'); + }); + it('should handle error with status code 400', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ errors: [{ message: 'Bad request' }] }), + ok: false, + status: 400, + statusText: 'Bad request', + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Bad request'); + }); + it('should handle error with status code 403', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ errors: [{ message: 'Forbidden' }] }), + ok: false, + status: 403, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Forbidden'); + }); + it('should handle error with status code 404', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 404, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Not found'); + }); + it('should handle error with status code 422', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 422, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Unprocessable entity'); + }); + it('should handle error with status code 500', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 500, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Internal server error'); + }); + it('should handle error with status code 503', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 503, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Service unavailable'); + }); + it('should handle error with status code 504', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 504, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Gateway timeout'); + }); + it('should handle error with status code 505', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 505, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('HTTP version not supported'); + }); + it('should handle error with custom status code', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 600, + statusText: 'Unknown error', + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Unknown error'); + }); +}); diff --git a/packages/utils/src/fetchData/index.ts b/packages/utils/src/fetchData/index.ts new file mode 100644 index 00000000..16959b63 --- /dev/null +++ b/packages/utils/src/fetchData/index.ts @@ -0,0 +1,98 @@ +import { ErrorHandler } from '../errorHandler'; + +interface HandleGraphqlRequestProps { + query?: string; + variables: any; +} + +const handleGraphqlRequest = ({ query, variables }: HandleGraphqlRequestProps) => + ({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, variables }), + cache: 'no-store', + }) as RequestInit; + +export interface FetchDataProps { + url: string; + query?: string; + variables?: any; + method?: string; +} + +/** + * @description This function is used to fetch data from the server + * @param {string} url - The url to fetch data from + * @param {string} query - The query to fetch data from the GraphQL API server + * @param {any} variables - The variables to pass it to the GraphQL Queries + * @param {string} method - The method to use for fetching data, default is POST + * @returns {Promise} - The data fetched from the server + * @example + * GraphQL Example: + * const data = await fetchData({ url: 'https://example.com/graphql', query: 'query { users { id name } }' }); + * API Example: + * const data = await fetchData({ url: 'https://example.com/api', method: 'GET' }); + * */ +// The POST method is set as the default option because it is commonly used to fetch data from GraphQL. +export const fetchData = async ({ url, query, variables, method = 'POST' }: FetchDataProps): Promise => { + const defaultOptions = { + method, + cache: 'no-store', + headers: { + 'Content-Type': 'application/json', + }, + } as RequestInit; + const graphqlOption = handleGraphqlRequest({ query, variables }); + try { + const response = await fetch(url, query ? graphqlOption : defaultOptions); + if (!response.ok) { + const { logger } = new ErrorHandler(); + logger(); + switch (response.status) { + case 400: + throw new ErrorHandler(response.statusText, { + statusCode: 400, + }); + case 403: + throw new ErrorHandler('Forbidden', { + statusCode: 403, + }); + case 404: + throw new ErrorHandler('Not found', { + statusCode: 404, + }); + case 422: + throw new ErrorHandler('Unprocessable entity', { + statusCode: 422, + }); + case 500: + throw new ErrorHandler('Internal server error', { + statusCode: 500, + }); + case 503: + logger(); + throw new ErrorHandler('Service unavailable', { + statusCode: 503, + }); + case 504: + throw new ErrorHandler('Gateway timeout', { + statusCode: 504, + }); + case 505: + throw new ErrorHandler('HTTP version not supported', { + statusCode: 505, + }); + default: + throw new ErrorHandler(response.statusText, { + statusCode: response.status, + }); + } + } + const data = await response.json(); + return data; + } catch (error: any) { + throw new ErrorHandler(error?.message, { + statusCode: error?.options?.statusCode, + }); + } +};