Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UISACQCOMP-219 Create common utilities for managing response errors #817

Merged
merged 3 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
45 changes: 45 additions & 0 deletions lib/utils/errorHandling/ResponseErrorContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ERROR_CODE_GENERIC } from '../../constants';

/**
* @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 || ERROR_CODE_GENERIC;
}

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;
}
}
66 changes: 66 additions & 0 deletions lib/utils/errorHandling/ResponseErrorContainer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* Developed collaboratively using AI (GitHub Copilot) */
SerhiiNosko marked this conversation as resolved.
Show resolved Hide resolved

import { ERROR_CODE_GENERIC } from '../../constants';
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 or type is provided, and the generic code', () => {
const errorContainer = new ResponseErrorContainer();

expect(errorContainer.message).toBeUndefined();
expect(errorContainer.code).toBe(ERROR_CODE_GENERIC);
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);
}
}
Loading
Loading