Convert errors.js to typescript

Change-type: patch
This commit is contained in:
Alexis Svinartchouk 2020-01-08 17:33:37 +01:00 committed by Lorenzo Alberto Maria Ambrosi
parent 30c2ef58cd
commit 9109f0ccd5
10 changed files with 272 additions and 369 deletions

View File

@ -24,6 +24,7 @@ const _ = require('lodash')
const Bluebird = require('bluebird') const Bluebird = require('bluebird')
// eslint-disable-next-line node/no-missing-require // eslint-disable-next-line node/no-missing-require
const localSettings = require('./local-settings') const localSettings = require('./local-settings')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../../shared/errors') const errors = require('../../../shared/errors')
const packageJSON = require('../../../../package.json') const packageJSON = require('../../../../package.json')
const debug = require('debug')('etcher:models:settings') const debug = require('debug')('etcher:models:settings')

View File

@ -23,6 +23,7 @@ const uuidV4 = require('uuid/v4')
const constraints = require('../../../shared/drive-constraints') const constraints = require('../../../shared/drive-constraints')
// eslint-disable-next-line node/no-missing-require // eslint-disable-next-line node/no-missing-require
const supportedFormats = require('../../../shared/supported-formats') const supportedFormats = require('../../../shared/supported-formats')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../../shared/errors') const errors = require('../../../shared/errors')
// eslint-disable-next-line node/no-missing-require // eslint-disable-next-line node/no-missing-require
const fileExtensions = require('../../../shared/file-extensions') const fileExtensions = require('../../../shared/file-extensions')

View File

@ -25,6 +25,7 @@ const electron = require('electron')
const store = require('../models/store') const store = require('../models/store')
const settings = require('../models/settings') const settings = require('../models/settings')
const flashState = require('../models/flash-state') const flashState = require('../models/flash-state')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../../shared/errors') const errors = require('../../../shared/errors')
const permissions = require('../../../shared/permissions') const permissions = require('../../../shared/permissions')
// eslint-disable-next-line node/no-missing-require // eslint-disable-next-line node/no-missing-require

View File

@ -19,6 +19,7 @@
const _ = require('lodash') const _ = require('lodash')
const electron = require('electron') const electron = require('electron')
const Bluebird = require('bluebird') const Bluebird = require('bluebird')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../../shared/errors') const errors = require('../../../shared/errors')
// eslint-disable-next-line node/no-missing-require // eslint-disable-next-line node/no-missing-require
const supportedFormats = require('../../../shared/supported-formats') const supportedFormats = require('../../../shared/supported-formats')

View File

@ -22,6 +22,7 @@ const ipc = require('node-ipc')
const sdk = require('etcher-sdk') const sdk = require('etcher-sdk')
// eslint-disable-next-line node/no-missing-require // eslint-disable-next-line node/no-missing-require
const EXIT_CODES = require('../../shared/exit-codes') const EXIT_CODES = require('../../shared/exit-codes')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../shared/errors') const errors = require('../../shared/errors')
ipc.config.id = process.env.IPC_CLIENT_ID ipc.config.id = process.env.IPC_CLIENT_ID

View File

@ -1,369 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
const _ = require('lodash')
/**
* @summary Create an error details object
* @function
* @private
*
* @param {Object} options - options
* @param {(String|Function)} options.title - error title
* @param {(String|Function)} options.description - error description
* @returns {Object} error details object
*
* @example
* const details = createErrorDetails({
* title: (error) => {
* return `An error happened, the code is ${error.code}`;
* },
* description: 'This is the error description'
* });
*/
const createErrorDetails = (options) => {
return _.pick(_.mapValues(options, (value) => {
return _.isFunction(value) ? value : _.constant(value)
}), [ 'title', 'description' ])
}
/**
* @summary Human-friendly error messages
* @namespace HUMAN_FRIENDLY
* @public
*/
exports.HUMAN_FRIENDLY = {
/* eslint-disable new-cap */
/**
* @namespace ENOENT
* @memberof HUMAN_FRIENDLY
*/
ENOENT: createErrorDetails({
title: (error) => {
return `No such file or directory: ${error.path}`
},
description: 'The file you\'re trying to access doesn\'t exist'
}),
/**
* @namespace EPERM
* @memberof HUMAN_FRIENDLY
*/
EPERM: createErrorDetails({
title: 'You\'re not authorized to perform this operation',
description: 'Please ensure you have necessary permissions for this task'
}),
/**
* @namespace EACCES
* @memberof HUMAN_FRIENDLY
*/
EACCES: createErrorDetails({
title: 'You don\'t have access to this resource',
description: 'Please ensure you have necessary permissions to access this resource'
}),
/**
* @namespace ENOMEM
* @memberof HUMAN_FRIENDLY
*/
ENOMEM: createErrorDetails({
title: 'Your system ran out of memory',
description: 'Please make sure your system has enough available memory for this task'
})
/* eslint-enable new-cap */
}
/**
* @summary Get user friendly property from an error
* @function
* @private
*
* @param {Error} error - error
* @param {String} property - HUMAN_FRIENDLY property
* @returns {(String|Undefined)} user friendly message
*
* @example
* const error = new Error('My error');
* error.code = 'ENOMEM';
*
* const friendlyDescription = getUserFriendlyMessageProperty(error, 'description');
*
* if (friendlyDescription) {
* console.log(friendlyDescription);
* }
*/
const getUserFriendlyMessageProperty = (error, property) => {
const code = _.get(error, [ 'code' ])
if (_.isNil(code) || !_.isString(code)) {
return null
}
return _.invoke(exports.HUMAN_FRIENDLY, [ code, property ], error)
}
/**
* @summary Check if a string is blank
* @function
* @private
*
* @param {String} string - string
* @returns {Boolean} whether the string is blank
*
* @example
* if (isBlank(' ')) {
* console.log('The string is blank');
* }
*/
const isBlank = _.flow([ _.trim, _.isEmpty ])
/**
* @summary Get the title of an error
* @function
* @public
*
* @description
* Try to get as much information as possible about the error
* rather than falling back to generic messages right away.
*
* @param {Error} error - error
* @returns {String} error title
*
* @example
* const error = new Error('Foo bar');
* const title = errors.getTitle(error);
* console.log(title);
*/
exports.getTitle = (error) => {
if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) {
return _.toString(error)
}
const codeTitle = getUserFriendlyMessageProperty(error, 'title')
if (!_.isNil(codeTitle)) {
return codeTitle
}
const message = _.get(error, [ 'message' ])
if (!isBlank(message)) {
return message
}
const code = _.get(error, [ 'code' ])
if (!_.isNil(code) && !isBlank(code)) {
return `Error code: ${code}`
}
return 'An error ocurred'
}
/**
* @summary Get the description of an error
* @function
* @public
*
* @param {Error} error - error
* @param {Object} options - options
* @param {Boolean} [options.userFriendlyDescriptionsOnly=false] - only return user friendly descriptions
* @returns {String} error description
*
* @example
* const error = new Error('Foo bar');
* const description = errors.getDescription(error);
* console.log(description);
*/
exports.getDescription = (error, options = {}) => {
_.defaults(options, {
userFriendlyDescriptionsOnly: false
})
if (!_.isError(error) && !_.isPlainObject(error)) {
return ''
}
if (!isBlank(error.description)) {
return error.description
}
const codeDescription = getUserFriendlyMessageProperty(error, 'description')
if (!_.isNil(codeDescription)) {
return codeDescription
}
if (options.userFriendlyDescriptionsOnly) {
return ''
}
if (error.stack) {
return error.stack
}
if (_.isEmpty(error)) {
return ''
}
const INDENTATION_SPACES = 2
return JSON.stringify(error, null, INDENTATION_SPACES)
}
/**
* @summary Create an error
* @function
* @public
*
* @param {Object} options - options
* @param {String} options.title - error title
* @param {String} [options.description] - error description
* @param {Boolean} [options.report] - report error
* @returns {Error} error
*
* @example
* const error = errors.createError({
* title: 'Foo'
* description: 'Bar'
* });
*
* throw error;
*/
exports.createError = (options) => {
if (isBlank(options.title)) {
throw new Error(`Invalid error title: ${options.title}`)
}
const error = new Error(options.title)
error.description = options.description
if (!_.isNil(options.report) && !options.report) {
error.report = false
}
if (!_.isNil(options.code)) {
error.code = options.code
}
return error
}
/**
* @summary Create a user error
* @function
* @public
*
* @description
* User errors represent invalid states that the user
* caused, that are not errors on the application itself.
* Therefore, user errors don't get reported to analytics
* and error reporting services.
*
* @param {Object} options - options
* @param {String} options.title - error title
* @param {String} [options.description] - error description
* @returns {Error} user error
*
* @example
* const error = errors.createUserError({
* title: 'Foo',
* description: 'Bar'
* });
*
* throw error;
*/
exports.createUserError = (options) => {
return exports.createError({
title: options.title,
description: options.description,
report: false,
code: options.code
})
}
/**
* @summary Check if an error is an user error
* @function
* @public
*
* @param {Error} error - error
* @returns {Boolean} whether the error is a user error
*
* @example
* const error = errors.createUserError('Foo', 'Bar');
*
* if (errors.isUserError(error)) {
* console.log('This error is a user error');
* }
*/
exports.isUserError = (error) => {
return _.isNil(error.report) ? false : !error.report
}
/**
* @summary Convert an Error object to a JSON object
* @function
* @public
*
* @param {Error} error - error object
* @returns {Object} json error
*
* @example
* const error = errors.toJSON(new Error('foo'))
*
* console.log(error.message);
* > 'foo'
*/
exports.toJSON = (error) => {
// Handle string error objects to be on the safe side
const isErrorLike = _.isError(error) || _.isPlainObject(error)
const errorObject = isErrorLike ? error : new Error(error)
return {
name: errorObject.name,
message: errorObject.message,
description: errorObject.description,
stack: errorObject.stack,
report: errorObject.report,
code: errorObject.code,
syscall: errorObject.syscall,
errno: errorObject.errno,
stdout: errorObject.stdout,
stderr: errorObject.stderr,
device: errorObject.device
}
}
/**
* @summary Convert a JSON object to an Error object
* @function
* @public
*
* @param {Error} json - json object
* @returns {Object} error object
*
* @example
* const error = errors.fromJSON(errors.toJSON(new Error('foo')));
*
* console.log(error.message);
* > 'foo'
*/
exports.fromJSON = (json) => {
return _.assign(new Error(json.message), json)
}

264
lib/shared/errors.ts Normal file
View File

@ -0,0 +1,264 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as _ from 'lodash';
function createErrorDetails(options: {
title: string | ((error: Error) => string);
description: string | ((error: Error) => string);
}): {
title: (error: Error) => string;
description: (error: Error) => string;
} {
return _.pick(
_.mapValues(options, value => {
return _.isFunction(value) ? value : _.constant(value);
}),
['title', 'description'],
);
}
/**
* @summary Human-friendly error messages
*/
export const HUMAN_FRIENDLY = {
ENOENT: createErrorDetails({
title: (error: Error & { path: string }) => {
return `No such file or directory: ${error.path}`;
},
description: "The file you're trying to access doesn't exist",
}),
EPERM: createErrorDetails({
title: "You're not authorized to perform this operation",
description: 'Please ensure you have necessary permissions for this task',
}),
EACCES: createErrorDetails({
title: "You don't have access to this resource",
description:
'Please ensure you have necessary permissions to access this resource',
}),
ENOMEM: createErrorDetails({
title: 'Your system ran out of memory',
description:
'Please make sure your system has enough available memory for this task',
}),
};
/**
* @summary Get user friendly property from an error
*
* @example
* const error = new Error('My error');
* error.code = 'ENOMEM';
*
* const friendlyDescription = getUserFriendlyMessageProperty(error, 'description');
*
* if (friendlyDescription) {
* console.log(friendlyDescription);
* }
*/
function getUserFriendlyMessageProperty(
error: Error,
property: 'title' | 'description',
): string | null {
const code = _.get(error, ['code']);
if (_.isNil(code) || !_.isString(code)) {
return null;
}
return _.invoke(HUMAN_FRIENDLY, [code, property], error);
}
const isBlank = _.flow([_.trim, _.isEmpty]);
/**
* @summary Get the title of an error
*
* @description
* Try to get as much information as possible about the error
* rather than falling back to generic messages right away.
*/
export function getTitle(error: Error): string {
if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) {
return _.toString(error);
}
const codeTitle = getUserFriendlyMessageProperty(error, 'title');
if (!_.isNil(codeTitle)) {
return codeTitle;
}
const message = _.get(error, ['message']);
if (!isBlank(message)) {
return message;
}
const code = _.get(error, ['code']);
if (!_.isNil(code) && !isBlank(code)) {
return `Error code: ${code}`;
}
return 'An error ocurred';
}
/**
* @summary Get the description of an error
*/
export function getDescription(
error: Error & { description?: string },
options: { userFriendlyDescriptionsOnly?: boolean } = {},
): string {
_.defaults(options, {
userFriendlyDescriptionsOnly: false,
});
if (!_.isError(error) && !_.isPlainObject(error)) {
return '';
}
if (!isBlank(error.description)) {
return error.description as string;
}
const codeDescription = getUserFriendlyMessageProperty(error, 'description');
if (!_.isNil(codeDescription)) {
return codeDescription;
}
if (options.userFriendlyDescriptionsOnly) {
return '';
}
if (error.stack) {
return error.stack;
}
if (_.isEmpty(error)) {
return '';
}
const INDENTATION_SPACES = 2;
return JSON.stringify(error, null, INDENTATION_SPACES);
}
/**
* @summary Create an error
*/
export function createError(options: {
title: string;
description: string;
report: boolean;
code: string;
}): Error & { description?: string; report?: boolean; code?: string } {
if (isBlank(options.title)) {
throw new Error(`Invalid error title: ${options.title}`);
}
const error: Error & {
description?: string;
report?: boolean;
code?: string;
} = new Error(options.title);
error.description = options.description;
if (!_.isNil(options.report) && !options.report) {
error.report = false;
}
if (!_.isNil(options.code)) {
error.code = options.code;
}
return error;
}
/**
* @summary Create a user error
*
* @description
* User errors represent invalid states that the user
* caused, that are not errors on the application itself.
* Therefore, user errors don't get reported to analytics
* and error reporting services.
*/
export function createUserError(options: {
title: string;
description: string;
code: string;
}): Error {
return createError({
title: options.title,
description: options.description,
report: false,
code: options.code,
});
}
/**
* @summary Check if an error is an user error
*/
export function isUserError(error: Error & { report?: boolean }): boolean {
return _.isNil(error.report) ? false : !error.report;
}
/**
* @summary Convert an Error object to a JSON object
* @function
* @public
*
* @param {Error} error - error object
* @returns {Object} json error
*
* @example
* const error = errors.toJSON(new Error('foo'))
*
* console.log(error.message);
* > 'foo'
*/
export function toJSON(
error: Error & {
description?: string;
report?: boolean;
code?: string;
syscall?: string;
errno?: number;
stdout?: string;
stderr?: string;
device?: string;
},
): any {
return {
name: error.name,
message: error.message,
description: error.description,
stack: error.stack,
report: error.report,
code: error.code,
syscall: error.syscall,
errno: error.errno,
stdout: error.stdout,
stderr: error.stderr,
device: error.device,
};
}
/**
* @summary Convert a JSON object to an Error object
*/
export function fromJSON(json: any): Error {
return _.assign(new Error(json.message), json);
}

View File

@ -28,6 +28,7 @@ const semver = require('semver')
const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt')) const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt'))
const { promisify } = require('util') const { promisify } = require('util')
// eslint-disable-next-line node/no-missing-require
const errors = require('./errors') const errors = require('./errors')
const { tmpFileDisposer } = require('./utils') const { tmpFileDisposer } = require('./utils')

View File

@ -21,6 +21,7 @@ const Bluebird = require('bluebird')
const request = Bluebird.promisifyAll(require('request')) const request = Bluebird.promisifyAll(require('request'))
const tmp = require('tmp') const tmp = require('tmp')
// eslint-disable-next-line node/no-missing-require
const errors = require('./errors') const errors = require('./errors')
/** /**

View File

@ -18,6 +18,7 @@
const m = require('mochainon') const m = require('mochainon')
const _ = require('lodash') const _ = require('lodash')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../lib/shared/errors') const errors = require('../../lib/shared/errors')
describe('Shared: Errors', function () { describe('Shared: Errors', function () {