diff --git a/lib/gui/app/models/settings.js b/lib/gui/app/models/settings.js index eabac49f..e3158ce2 100644 --- a/lib/gui/app/models/settings.js +++ b/lib/gui/app/models/settings.js @@ -24,6 +24,7 @@ const _ = require('lodash') const Bluebird = require('bluebird') // eslint-disable-next-line node/no-missing-require const localSettings = require('./local-settings') +// eslint-disable-next-line node/no-missing-require const errors = require('../../../shared/errors') const packageJSON = require('../../../../package.json') const debug = require('debug')('etcher:models:settings') diff --git a/lib/gui/app/models/store.js b/lib/gui/app/models/store.js index 755815f9..616cc13c 100644 --- a/lib/gui/app/models/store.js +++ b/lib/gui/app/models/store.js @@ -23,6 +23,7 @@ const uuidV4 = require('uuid/v4') const constraints = require('../../../shared/drive-constraints') // eslint-disable-next-line node/no-missing-require const supportedFormats = require('../../../shared/supported-formats') +// eslint-disable-next-line node/no-missing-require const errors = require('../../../shared/errors') // eslint-disable-next-line node/no-missing-require const fileExtensions = require('../../../shared/file-extensions') diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index c9ad0a42..0a92f4a9 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -25,6 +25,7 @@ const electron = require('electron') const store = require('../models/store') const settings = require('../models/settings') const flashState = require('../models/flash-state') +// eslint-disable-next-line node/no-missing-require const errors = require('../../../shared/errors') const permissions = require('../../../shared/permissions') // eslint-disable-next-line node/no-missing-require diff --git a/lib/gui/app/os/dialog.js b/lib/gui/app/os/dialog.js index 77c62043..6dc45c25 100644 --- a/lib/gui/app/os/dialog.js +++ b/lib/gui/app/os/dialog.js @@ -19,6 +19,7 @@ const _ = require('lodash') const electron = require('electron') const Bluebird = require('bluebird') +// eslint-disable-next-line node/no-missing-require const errors = require('../../../shared/errors') // eslint-disable-next-line node/no-missing-require const supportedFormats = require('../../../shared/supported-formats') diff --git a/lib/gui/modules/child-writer.js b/lib/gui/modules/child-writer.js index 76811582..7463f933 100644 --- a/lib/gui/modules/child-writer.js +++ b/lib/gui/modules/child-writer.js @@ -22,6 +22,7 @@ const ipc = require('node-ipc') const sdk = require('etcher-sdk') // eslint-disable-next-line node/no-missing-require const EXIT_CODES = require('../../shared/exit-codes') +// eslint-disable-next-line node/no-missing-require const errors = require('../../shared/errors') ipc.config.id = process.env.IPC_CLIENT_ID diff --git a/lib/shared/errors.js b/lib/shared/errors.js deleted file mode 100644 index f69b31ad..00000000 --- a/lib/shared/errors.js +++ /dev/null @@ -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) -} diff --git a/lib/shared/errors.ts b/lib/shared/errors.ts new file mode 100644 index 00000000..a932cd9c --- /dev/null +++ b/lib/shared/errors.ts @@ -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); +} diff --git a/lib/shared/permissions.js b/lib/shared/permissions.js index 3a35c2d0..eb4f18b9 100755 --- a/lib/shared/permissions.js +++ b/lib/shared/permissions.js @@ -28,6 +28,7 @@ const semver = require('semver') const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt')) const { promisify } = require('util') +// eslint-disable-next-line node/no-missing-require const errors = require('./errors') const { tmpFileDisposer } = require('./utils') diff --git a/lib/shared/utils.js b/lib/shared/utils.js index 2107be80..03ea594e 100755 --- a/lib/shared/utils.js +++ b/lib/shared/utils.js @@ -21,6 +21,7 @@ const Bluebird = require('bluebird') const request = Bluebird.promisifyAll(require('request')) const tmp = require('tmp') +// eslint-disable-next-line node/no-missing-require const errors = require('./errors') /** diff --git a/tests/shared/errors.spec.js b/tests/shared/errors.spec.js index 758c3436..a0517c5f 100644 --- a/tests/shared/errors.spec.js +++ b/tests/shared/errors.spec.js @@ -18,6 +18,7 @@ const m = require('mochainon') const _ = require('lodash') +// eslint-disable-next-line node/no-missing-require const errors = require('../../lib/shared/errors') describe('Shared: Errors', function () {