diff --git a/src/index.test.ts b/src/index.test.ts index 142e03d..725ae14 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,48 +1,100 @@ +import axios, { AxiosInstance } from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import { createAxiosDateTransformer } from './index'; +import { addAxiosDateTransformer, createAxiosDateTransformer } from './index'; + +/** + * Helper function to create a mock adapter and mock a GET request. + * @param axiosInstance - The Axios instance to mock. + * @param url - The endpoint URL to mock. + * @param jsonResponse - The JSON response to return. + */ +const createMockAdapter = (axiosInstance: AxiosInstance, url: string, jsonResponse: string) => { + const mock = new MockAdapter(axiosInstance); + mock.onGet(url).reply(200, jsonResponse); + return mock; +}; + +/** + * Helper function to initialize a date-aware Axios instance. + * @param baseURL - The base URL for the Axios instance. + * @param allowlist - An optional allowlist for fields to convert. + * @returns A configured Axios instance with the date transformer applied. + */ +const initializeAxiosInstance = (baseURL: string, allowlist?: string[]): AxiosInstance => { + const axiosConfig = { baseURL }; + return allowlist + ? addAxiosDateTransformer(axios.create(axiosConfig), { allowlist }) + : createAxiosDateTransformer(axiosConfig); +}; + +/** + * Helper function to assert that a field is correctly converted to a Date object. + * @param field - The field to check. + * @param expectedDate - The expected Date object. + */ +const assertDateConversion = (field: any, expectedDate: Date) => { + expect(field).toBeInstanceOf(Date); + expect(field.toISOString()).toEqual(expectedDate.toISOString()); +}; + +/** + * Helper function to assert that a field remains a string and has the expected value. + * @param field - The field to check. + * @param expectedValue - The expected string value. + */ +const assertStringValue = (field: any, expectedValue: string) => { + expect(typeof field).toBe('string'); + expect(field).toEqual(expectedValue); +}; describe('axios-date-transformer', () => { + const baseURL = 'https://example.org'; + const url = '/api/data'; + const originalObject = { + name: 'John Doe', + dob: '1980-01-25', + issues: { + alpha: new Date('2022-01-25T12:30:00.000Z'), + beta: new Date('2024-01-26T09:45:00.000Z'), + }, + }; + const jsonResponse = JSON.stringify(originalObject); + test('transforms date strings to Date objects', async () => { - const originalObject = { - name: 'John Doe', - dob: '1980-01-25', - issues: { - alpha: new Date('2022-01-25T12:30:00.000Z'), - beta: new Date('2022-01-26T09:45:00.000Z'), - }, - }; - const jsonResponse = JSON.stringify(originalObject); - const axiosInstance = createAxiosDateTransformer({ - baseURL: 'https://example.org', - }); - - // Create a mock adapter for the axios instance - const mock = new MockAdapter(axiosInstance); - - // Mock the axios request with the resolved value - mock.onGet('/api/data').reply(200, jsonResponse); + const axiosInstance = initializeAxiosInstance(baseURL); + + // Create a mock adapter for the Axios instance + const mock = createMockAdapter(axiosInstance, url, jsonResponse); // Make the request - const { data } = await axiosInstance.get('/api/data'); + const { data } = await axiosInstance.get(url); - // Assert that the 'dob' property is an instance of Date - expect(data.dob).toBeInstanceOf(Date); + // Assert conversions + assertDateConversion(data.dob, new Date(originalObject.dob)); + assertDateConversion(data.issues.alpha, originalObject.issues.alpha); + assertDateConversion(data.issues.beta, originalObject.issues.beta); - // Assert that the date value of 'data.dob' is the same as 'response.dob' - expect(data.dob.toISOString()).toEqual(new Date(originalObject.dob).toISOString()); + // Restore the mock adapter + mock.restore(); + }); - // Assert that the 'issues.alpha' property is an instance of Date - expect(data.issues.alpha).toBeInstanceOf(Date); + test('transforms only date strings in the allowlist', async () => { + const allowlist = ['beta']; + const axiosInstance = initializeAxiosInstance(baseURL, allowlist); - // Assert that the date value of 'data.issues.alpha' is the same as 'response.issues.alpha' - expect(data.issues.alpha.toISOString()).toEqual(originalObject.issues.alpha.toISOString()); + // Create a mock adapter for the Axios instance + const mock = createMockAdapter(axiosInstance, url, jsonResponse); + + // Make the request + const { data } = await axiosInstance.get(url); - // Assert that the 'issues.beta' property is an instance of Date - expect(data.issues.beta).toBeInstanceOf(Date); + // Assert that non-allowlisted fields remain as strings + assertStringValue(data.dob, originalObject.dob); + assertStringValue(data.issues.alpha, originalObject.issues.alpha.toISOString()); - // Assert that the date value of 'data.issues.beta' is the same as 'response.issues.beta' - expect(data.issues.beta.toISOString()).toEqual(originalObject.issues.beta.toISOString()); + // Assert that only the allowlisted field is converted to a Date object + assertDateConversion(data.issues.beta, originalObject.issues.beta); // Restore the mock adapter mock.restore(); diff --git a/src/index.ts b/src/index.ts index ae4b8f1..c9c62f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,67 @@ import axios, { AxiosInstance, AxiosResponse, CreateAxiosDefaults } from 'axios'; -interface DateTransformerConfig extends CreateAxiosDefaults { - // placeholder interface for future configuration -} +/** + * Configuration options specifically for the Date Transformer. + */ +type DateTransformerConfig = { + /** + * An optional array of strings specifying the field names + * that should be converted to Date objects. If provided, + * only these fields will be parsed as dates. + */ + allowlist?: string[]; +}; + +/** + * Extended Axios configuration that includes the Date Transformer options. + */ +type DateTransformerAxiosConfig = CreateAxiosDefaults & DateTransformerConfig; const dateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(?:\.\d*)?(?:[-+]\d{2}:?\d{2}|Z)?)?$/; -const recursiveDateConversion = (data: any): any => { +/** + * Determines if a given string value matches the ISO date format. + * @param value - The value to be checked. + * @returns `true` if the value is a valid ISO date string, otherwise `false`. + */ +const isDateString = (value: any): boolean => { + return dateRegex.test(value); +}; + +/** + * Checks if a given key is present in the allowlist. + * @param key - The key to be checked. + * @param allowlist - An optional array of keys to be converted. + * @returns `true` if the allowlist is undefined, empty, or if the key is in the allowlist. + */ +const isAllowed = (key: string, allowlist?: string[]): boolean => { + return !allowlist || allowlist.length === 0 || allowlist.includes(key); +}; + +/** + * Determines if a given value should be converted to a Date object. + * @param key - The key of the value to be converted. + * @param value - The value to be checked. + * @param allowlist - An optional array of keys that should be converted. + * @returns `true` if the value should be converted, otherwise `false`. + */ +const shouldConvert = (key: string, value: any, allowlist?: string[]): boolean => { + return typeof value === 'string' && isDateString(value) && isAllowed(key, allowlist); +}; + +/** + * Recursively traverses an object and converts allowed fields to Date objects. + * @param data - The data to be traversed and transformed. + * @param allowlist - An optional array of keys that should be converted to Date objects. + * @returns The transformed data with Date objects where applicable. + */ +const recursiveDateConversion = (data: any, allowlist?: string[]): any => { if (typeof data === 'object') { for (const key in data) { - if (typeof data[key] === 'string' && isDateString(data[key])) { + if (shouldConvert(key, data[key], allowlist)) { data[key] = new Date(data[key]); } else if (typeof data[key] === 'object') { - data[key] = recursiveDateConversion(data[key]); + data[key] = recursiveDateConversion(data[key], allowlist); } } } @@ -20,24 +69,40 @@ const recursiveDateConversion = (data: any): any => { return data; }; -const isDateString = (value: any): boolean => { - return dateRegex.test(value); -}; - -const transformDates = (response: AxiosResponse): AxiosResponse => { +/** + * Transforms date strings in the Axios response data to Date objects based on the allowlist. + * @param response - The Axios response object. + * @param allowlist - An optional array of keys that should be converted to Date objects. + * @returns The transformed Axios response. + */ +const transformDates = (response: AxiosResponse, allowlist?: string[]): AxiosResponse => { if (response.data) { - response.data = recursiveDateConversion(response.data); + response.data = recursiveDateConversion(response.data, allowlist); } return response; }; -export const createAxiosDateTransformer = (config: DateTransformerConfig = {}): AxiosInstance => { - return addAxiosDateTransformer(axios.create(config)); +/** + * Creates a new Axios instance with the date transformer interceptor applied. + * @param config - The configuration for the Axios instance, including the date transformer options. + * @returns A new Axios instance with date transformation enabled. + */ +export const createAxiosDateTransformer = (config: DateTransformerAxiosConfig = {}): AxiosInstance => { + return addAxiosDateTransformer(axios.create(config), config); }; -export const addAxiosDateTransformer = (instance: AxiosInstance): AxiosInstance => { - instance.interceptors.response.use(transformDates); - +/** + * Adds the date transformer interceptor to an existing Axios instance. + * @param instance - The Axios instance to which the interceptor should be added. + * @param dateTransformerConfig - Configuration options for the date transformer, including an optional allowlist. + * @returns The Axios instance with the date transformer applied. + */ +export const addAxiosDateTransformer = ( + instance: AxiosInstance, + dateTransformerConfig?: DateTransformerConfig, +): AxiosInstance => { + const allowlist = dateTransformerConfig?.allowlist; + instance.interceptors.response.use(response => transformDates(response, allowlist)); return instance; };