Skip to content

Commit

Permalink
UISACQCOMP-219 Create common utilities for managing response errors
Browse files Browse the repository at this point in the history
  • Loading branch information
usavkov-epam committed Oct 3, 2024
1 parent d0dde18 commit daf9db9
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* ECS - Display all consortium tenants in the affiliation selection of the location lookup. Refs UISACQCOMP-202.
* ECS - Add `isLoading` prop for `ConsortiumFieldInventory` component. Refs UISACQCOMP-215.
* Add "Amount must be a positive number" validation for "Set exchange rate" field. Refs UISACQCOMP-218.
* Create common utilities for managing response errors. Refs UISACQCOMP-219.

## [5.1.2](https://github.com/folio-org/stripes-acq-components/tree/v5.1.2) (2024-09-13)
[Full Changelog](https://github.com/folio-org/stripes-acq-components/compare/v5.1.1...v5.1.2)
Expand Down
43 changes: 43 additions & 0 deletions lib/utils/errorHandling/ResponseErrorContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* @class ResponseErrorContainer
* @description A container class for handling individual errors from a response.
* @param {Object} error - The error object containing details such as message, code, type, and parameters.
*/
export class ResponseErrorContainer {
constructor(error = {}) {
this.error = error;
}

get message() {
return this.error.message;
}

get code() {
return this.error.code;
}

get type() {
return this.error.type;
}

get parameters() {
return this.error.parameters;
}

/**
* @description Convert the error parameters into a Map.
* @returns {Map<string, Object>} A Map where the keys are parameter keys and the values are the parameter objects.
*/
getParameters() {
return new Map(this.error.parameters?.map((parameter) => [parameter.key, parameter]));
}

/**
* @description Get a specific parameter value by its key.
* @param {string} key - The key of the parameter to retrieve.
* @returns {any} The value of the specified parameter, or undefined if not found.
*/
getParameter(key) {
return this.getParameters().get(key)?.value;
}
}
63 changes: 63 additions & 0 deletions lib/utils/errorHandling/ResponseErrorContainer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ResponseErrorContainer } from './ResponseErrorContainer';

describe('ResponseErrorContainer', () => {
it('should return message, code, and type from error', () => {
const errorData = {
message: 'Test error message',
code: 'testCode',
type: 'testType',
};
const errorContainer = new ResponseErrorContainer(errorData);

expect(errorContainer.message).toBe('Test error message');
expect(errorContainer.code).toBe('testCode');
expect(errorContainer.type).toBe('testType');
});

it('should return undefined if no message, code, or type is provided', () => {
const errorContainer = new ResponseErrorContainer();

expect(errorContainer.message).toBeUndefined();
expect(errorContainer.code).toBeUndefined();
expect(errorContainer.type).toBeUndefined();
});

it('should return parameters map if parameters are provided', () => {
const errorData = {
parameters: [
{ key: 'param1', value: 'value1' },
{ key: 'param2', value: 'value2' },
],
};
const errorContainer = new ResponseErrorContainer(errorData);

const parametersMap = errorContainer.getParameters();

expect(parametersMap.size).toBe(2);
expect(parametersMap.get('param1').value).toBe('value1');
expect(parametersMap.get('param2').value).toBe('value2');
});

it('should return an empty map if no parameters are provided', () => {
const errorContainer = new ResponseErrorContainer();

const parametersMap = errorContainer.getParameters();

expect(parametersMap.size).toBe(0);
});

it('should return parameter value for a given key', () => {
const errorData = {
parameters: [{ key: 'param1', value: 'value1' }],
};
const errorContainer = new ResponseErrorContainer(errorData);

expect(errorContainer.getParameter('param1')).toBe('value1');
});

it('should return undefined if parameter key is not found', () => {
const errorContainer = new ResponseErrorContainer();

expect(errorContainer.getParameter('nonexistentKey')).toBeUndefined();
});
});
149 changes: 149 additions & 0 deletions lib/utils/errorHandling/ResponseErrorsContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { ERROR_CODE_GENERIC } from '../../constants';
import { ResponseErrorContainer } from './ResponseErrorContainer';

const ERROR_MESSAGE_GENERIC = 'An unknown error occurred';

/**
* @class
* @description Container for response errors
* @param {Object} responseBody - Response body
* @param {Response} response - Response
*/
export class ResponseErrorsContainer {
/* private */ constructor(responseBody, response) {
this.originalResponseBody = responseBody;
this.originalResponse = response;

// Initialize the map of errors
this.errorsMap = new Map(
responseBody.errors?.reduce((acc, error) => {
const errorContainer = this.getStructuredError(error);

acc.push([errorContainer.code || ERROR_CODE_GENERIC, errorContainer]);

return acc;
}, []),
);

this.totalRecords = responseBody.total_records;
}

/**
* @static
* @description Create a new instance of ResponseErrorsContainer.
* @param {Response} response - The Response object from which to create the error handler.
* @returns {Promise<{handler: ResponseErrorsContainer}>} A promise that resolves to an object containing the error handler.
*/
static async create(response) {
try {
const responseBody = await response.clone().json();

return {
handler: (
responseBody?.errors
? new ResponseErrorsContainer(responseBody, response)
: new ResponseErrorsContainer({ errors: [responseBody], total_records: 1 }, response)
),
};
} catch (error) {
return {
handler: new ResponseErrorsContainer({ errors: [error], total_records: 1 }, response),
};
}
}

/**
* @description Handle the errors using a given strategy.
* @param {Object} strategy - The error handling strategy to be applied.
*/
handle(strategy) {
return strategy.handle(this);
}

get status() {
return this.originalResponse?.status;
}

/**
* @description Get an array of error messages.
* @returns {Array<string | undefined>} An array of error messages.
*/
get errorMessages() {
return this.errors.map((error) => error.message);
}

/**
* @description Get an array of error codes.
* @returns {Array<string | undefined>} An array of error codes.
*/
get errorCodes() {
return this.errors.map((error) => error.code);
}

/**
* @description Get all errors as an array.
* @returns {Array<ResponseErrorContainer>} An array of error containers.
*/
get errors() {
return Array.from(this.errorsMap.values());
}

/**
* @description Get all errors as a map.
* @returns {Map<string, ResponseErrorContainer>} A map of errors with error codes as keys.
*/
getErrors() {
return this.errorsMap;
}

/**
* @description Get a specific error by its code or the first error if no code is provided.
* @param {string} [code] - The error code to search for.
* @returns {ResponseErrorContainer} The corresponding error container or a generic error if not found.
*/
getError(code) {
return (code ? this.errorsMap.get(code) : this.errors[0]) || new ResponseErrorContainer();
}

/**
* @private
* @description Normalize an unknown error into a structured ResponseErrorContainer.
* @param {unknown} error - The unstructured error object.
* @returns {ResponseErrorContainer} A structured error container.
*/
getStructuredError(error) {
let normalizedError;

if (typeof error === 'string') {
try {
const parsed = JSON.parse(error);

normalizedError = {
message: parsed.message || error,
code: parsed.code || ERROR_CODE_GENERIC,
...parsed,
};
} catch {
normalizedError = {
message: error,
code: ERROR_CODE_GENERIC,
};
}
} else {
let message;

try {
message = error?.message || error?.error || JSON.stringify(error);
} catch {
message = ERROR_MESSAGE_GENERIC;
}
normalizedError = {
code: error?.code || ERROR_CODE_GENERIC,
message,
...error,
};
}

return new ResponseErrorContainer(normalizedError);
}
}
54 changes: 54 additions & 0 deletions lib/utils/errorHandling/ResponseErrorsContainer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ERROR_CODE_GENERIC } from '../../constants';
import { ResponseErrorsContainer } from './ResponseErrorsContainer';
import { ResponseErrorContainer } from './ResponseErrorContainer';

describe('ResponseErrorsContainer', () => {
let mockResponse; let
mockResponseBody;

beforeEach(() => {
mockResponseBody = {
errors: [{ code: '404', message: 'Not found' }],
total_records: 1,
};
mockResponse = {
clone: jest.fn().mockReturnThis(),
json: jest.fn().mockResolvedValue(mockResponseBody),
status: 404,
};
});

it('should create a new ResponseErrorsContainer instance from a response', async () => {
const { handler } = await ResponseErrorsContainer.create(mockResponse);

expect(handler).toBeInstanceOf(ResponseErrorsContainer);
expect(handler.status).toBe(404); // Check for status
expect(handler.errorMessages).toEqual(['Not found']);
});

it('should return a specific error by code', async () => {
const { handler } = await ResponseErrorsContainer.create(mockResponse);
const error = handler.getError('404');

expect(error).toBeInstanceOf(ResponseErrorContainer);
expect(error.message).toBe('Not found');
expect(error.code).toBe('404');
});

it('should handle unknown errors', async () => {
const mockUnknownError = { errors: ['Unknown error'] };

mockResponse.json.mockResolvedValueOnce(mockUnknownError);

const { handler } = await ResponseErrorsContainer.create(mockResponse);

expect(handler.errorMessages).toEqual(['Unknown error']);
});

it('should return generic error when no error code is found', async () => {
const { handler } = await ResponseErrorsContainer.create(mockResponse);
const error = handler.getError('500');

expect(error.code).toBe(ERROR_CODE_GENERIC);
});
});
1 change: 1 addition & 0 deletions lib/utils/errorHandling/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ResponseErrorsContainer } from './ResponseErrorsContainer';
1 change: 1 addition & 0 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './calculateFundAmount';
export * from './consortia';
export * from './createClearFilterHandler';
export * from './downloadBase64';
export * from './errorHandling';
export * from './EventEmitter';
export * from './fetchAllRecords';
export * from './fetchExportDataByIds';
Expand Down

0 comments on commit daf9db9

Please sign in to comment.