Skip to content

Commit

Permalink
feat: added allowList config and refactored tests
Browse files Browse the repository at this point in the history
  • Loading branch information
angelxmoreno committed Sep 28, 2024
1 parent e6bc438 commit 8182ada
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 49 deletions.
116 changes: 84 additions & 32 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
99 changes: 82 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,108 @@
import axios, { AxiosInstance, AxiosResponse, CreateAxiosDefaults } from 'axios';

interface DateTransformerConfig<T = any> extends CreateAxiosDefaults<T> {
// 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<T = any> = CreateAxiosDefaults<T> & 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);
}
}
}

return data;
};

const isDateString = (value: any): boolean => {
return dateRegex.test(value);
};

const transformDates = <T = any>(response: AxiosResponse<T>): AxiosResponse<T> => {
/**
* 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 = <T = any>(response: AxiosResponse<T>, allowlist?: string[]): AxiosResponse<T> => {
if (response.data) {
response.data = recursiveDateConversion(response.data);
response.data = recursiveDateConversion(response.data, allowlist);
}

return response;
};

export const createAxiosDateTransformer = <T = any>(config: DateTransformerConfig<T> = {}): 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 = <T = any>(config: DateTransformerAxiosConfig<T> = {}): 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;
};

0 comments on commit 8182ada

Please sign in to comment.