gitlab-ce/app/assets/javascripts/lib/utils/error_utils.js

166 lines
5.0 KiB
JavaScript

import { isEmpty, isString, isObject } from 'lodash';
import { sprintf, __ } from '~/locale';
export class ActiveModelError extends Error {
constructor(errorAttributeMap = {}, ...params) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(...params);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ActiveModelError);
}
this.name = 'ActiveModelError';
// Custom debugging information
this.errorAttributeMap = errorAttributeMap;
}
}
const DEFAULT_ERROR = {
message: __('Something went wrong. Please try again.'),
links: {},
};
/**
* @typedef {Object<ErrorAttribute,ErrorType[]>} ErrorAttributeMap - Map of attributes to error details
* @typedef {string} ErrorAttribute - the error attribute https://api.rubyonrails.org/v7.0.4.2/classes/ActiveModel/Error.html
* @typedef {string} ErrorType - the error type https://api.rubyonrails.org/v7.0.4.2/classes/ActiveModel/Error.html
*
* @example { "email": ["taken", ...] }
* // returns `${UNLINKED_ACCOUNT_ERROR}`, i.e. the `EMAIL_TAKEN_ERROR_TYPE` error message
*
* @param {ErrorAttributeMap} errorAttributeMap
* @param {Object} errorDictionary
* @returns {(null|string)} null or error message if found
*/
function getMessageFromType(errorAttributeMap = {}, errorDictionary = {}) {
if (!isObject(errorAttributeMap)) {
return null;
}
return Object.keys(errorAttributeMap).reduce((_, attribute) => {
const errorType = errorAttributeMap[attribute].find(
(type) => errorDictionary[`${attribute}:${type}`.toLowerCase()],
);
if (errorType) {
return errorDictionary[`${attribute}:${errorType}`.toLowerCase()];
}
return null;
}, null);
}
/**
* @example "Email has already been taken, Email is invalid"
* // returns `${UNLINKED_ACCOUNT_ERROR}`, i.e. the `EMAIL_TAKEN_ERROR_TYPE` error message
*
* @param {string} errorString
* @param {Object} errorDictionary
* @returns {(null|string)} null or error message if found
*/
function getMessageFromErrorString(errorString, errorDictionary = {}) {
if (isEmpty(errorString) || !isString(errorString)) {
return null;
}
const messages = errorString.split(', ');
const errorMessage = messages.find((message) => errorDictionary[message.toLowerCase()]);
if (errorMessage) {
return errorDictionary[errorMessage.toLowerCase()];
}
return {
message: errorString,
links: {},
};
}
/**
* Receives an Error and attempts to extract the `errorAttributeMap` in
* case it is an `ActiveModelError` and returns the message if it exists.
* If a match is not found it will attempt to map a message from the
* Error.message to be returned.
* Otherwise, it will return a general error message.
*
* @param {Error|String} systemError
* @param {Object} errorDictionary
* @param {Object} defaultError
* @returns error message
*/
export function mapSystemToFriendlyError(
systemError,
errorDictionary = {},
defaultError = DEFAULT_ERROR,
) {
if (systemError instanceof String || typeof systemError === 'string') {
const messageFromErrorString = getMessageFromErrorString(systemError, errorDictionary);
if (messageFromErrorString) {
return messageFromErrorString;
}
return defaultError;
}
if (!(systemError instanceof Error)) {
return defaultError;
}
const { errorAttributeMap, message } = systemError;
const messageFromType = getMessageFromType(errorAttributeMap, errorDictionary);
if (messageFromType) {
return messageFromType;
}
const messageFromErrorString = getMessageFromErrorString(message, errorDictionary);
if (messageFromErrorString) {
return messageFromErrorString;
}
return defaultError;
}
function generateLinks(links) {
return Object.keys(links).reduce((allLinks, link) => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
const linkStart = `${link}Start`;
/* eslint-disable-next-line @gitlab/require-i18n-strings */
const linkEnd = `${link}End`;
return {
...allLinks,
[linkStart]: `<a href="${links[link]}" target="_blank" rel="noopener noreferrer">`,
[linkEnd]: '</a>',
};
}, {});
}
export const generateHelpTextWithLinks = (error) => {
if (isString(error)) {
return error;
}
if (isEmpty(error)) {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
throw new Error('The error cannot be empty.');
}
const links = generateLinks(error.links);
return sprintf(error.message, links, false);
};
/**
* Receives an error code and an error dictionary and returns true
* if the error code is found in the dictionary and false otherwise.
*
* @param {String} errorCode
* @param {Object} errorDictionary
* @returns {Boolean}
*/
export const isKnownErrorCode = (errorCode, errorDictionary) => {
if (errorCode instanceof String || typeof errorCode === 'string') {
return Object.keys(errorDictionary).includes(errorCode.toLowerCase());
}
return false;
};