@gramps/errors
Version:
GraphQL error handling. Part of the GrAMPS family.
260 lines (215 loc) • 7.69 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.GrampsError = GrampsError;
exports.formatError = exports.formatErrorGenerator = exports.normalizeError = exports.formatClientErrorData = exports.printDetailedServerLog = void 0;
var _uuid = _interopRequireDefault(require("uuid"));
var _os = require("os");
var _sevenBoom = _interopRequireDefault(require("seven-boom"));
var _apolloServerExpress = require("apollo-server-express");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// Define the available error data that can be returned in every error.
const customErrorFields = [// Putting this first (out of order) to control display order in server logs.
{
label: 'Error GUID',
name: 'guid',
order: 10,
default: () => _uuid.default.v4()
}, {
label: 'Description',
name: 'description',
order: 1
}, {
label: 'Error Code',
name: 'errorCode',
order: 2,
default: 'GRAMPS_ERROR'
}, {
label: 'GraphQL Model',
name: 'graphqlModel',
order: 3
}, {
label: 'Target Endpoint',
name: 'targetEndpoint',
order: 4
}, {
label: 'Documentation',
name: 'docsLink',
order: 5
}, {
label: 'Message',
name: 'message',
order: 6
}, {
label: 'Locations',
name: 'locations',
order: 7
}, {
label: 'Path',
name: 'path',
order: 8
}]; // Add the custom fields. Copy the array because this function mutates its args.
_sevenBoom.default.init([...customErrorFields]);
/**
* Creates a custom error or wraps an existing error.
*
* @see https://github.com/GiladShoham/seven-boom
* @see https://github.com/hapijs/boom
*
* @param {Object?} config error configuration
* @param {Error|boolean?} config.error Error object to modify
* @param {number?} config.statusCode HTTP status code (e.g. 404)
* @param {Object?} config.data supplied data (e.g. vars)
* @param {string?} config.message human-readable error message
* @param {string?} config.errorCode custom error code
* @param {string?} config.graphqlModel which GraphQL model errored
* @param {string?} config.targetEndpoint where data was loaded from
* @param {string?} config.docsLink link to help docs
* @return {Error} SevenBoom error for output, or normal Error if serialized
*/
function GrampsError({
statusCode = false,
data = null,
description = null,
message = null,
locations = null,
path = null,
errorCode = 'GRAMPS_ERROR',
graphqlModel = null,
targetEndpoint = null,
docsLink = null
} = {}) {
const httpErrorCode = statusCode || 500;
const args = [httpErrorCode, message, data, description, errorCode, graphqlModel, targetEndpoint, docsLink, message, locations, path]; // Call the function and spread the args array into individual arguments.
const boom = _sevenBoom.default.create(...args); // Return the error.
return new _apolloServerExpress.ApolloError(message, httpErrorCode, boom);
}
/**
* Builds a list of error details based on what details are available.
*
* For each available field, check if the error contains info that matches it.
* We also add a label for human readability and removes empty array elements.
*
* @param {array} fields detail fields to format
* @param {Error} error the error data for display
* @return {array} list of formatted error details
*/
const formatDetailsArray = (fields, error) => fields.filter(field => field.name !== 'guid').map(field => error[field.name] && `${field.label}: ${JSON.stringify(error[field.name], null, 2)}`).filter(field => !!field);
/**
* Accepts a SevenBoom error object and generates a formatted server log.
* @param {object} error the SevenBoom error object
* @param {object} error.data supplied data (e.g. arguments, vars)
* @param {array} error.stack the error’s stack trace
* @param {object} error.output object containing error output
* @param {object} error.output.payload generated payload from SevenBoom
* @return {void}
*/
const printDetailedServerLog = logger => (err, stack) => {
const {
output: {
payload
},
data
} = err;
const details = formatDetailsArray(customErrorFields, payload);
const defaultMsg = 'something went wrong 💀 ';
const message = payload.description || payload.message || defaultMsg; // The first line has special formatting to make it useful + searchable.
details.unshift(`Error: ${message} (${payload.guid})`); // Data is an object, so we need to do a little extra formatting.
if (data) {
details.push(`Data: ${JSON.stringify(data, null, 2)}`);
} // Create a single string that joins each section with two line breaks.
const log = [details.join(_os.EOL), stack.join(_os.EOL)].join(_os.EOL.repeat(2));
logger.error(log);
};
/**
* Formats and sanitizes the error message for public display.
* @param {Error} error the processed SevenBoom error
* @return {Error} the display-safe error
*/
exports.printDetailedServerLog = printDetailedServerLog;
const formatClientErrorData = error => {
/* eslint-disable no-param-reassign */
if (process.env.NODE_ENV === 'production') {
// Not all API endpoints are public, so hide those in production.
delete error.targetEndpoint; // Clients can’t reach most internal docs, so hide those as well.
delete error.docsLink;
} // The message prop is overwritten by either GraphQL or SevenBoom.
if (!error.description) {
error.description = error.message;
} // To avoid escaped quotes, change them to single quotes.
error.description = error.description && error.description.replace(/"/g, "'");
/* eslint-enable no-param-reassign */
return error;
};
exports.formatClientErrorData = formatClientErrorData;
const normalizeError = err => {
const {
message,
locations,
path
} = err; // Avoid swallowing syntax errors
if (err instanceof _apolloServerExpress.ValidationError) {
return GrampsError({
errorCode: err.extensions.code,
message,
locations,
path
});
} // Errors that came in as a GrAMPSError (or SevenBoom error)
if (err.extensions.exception && err.extensions.exception.isBoom) {
return GrampsError({ ...err.extensions.exception.output.payload,
...err.extensions.exception,
message,
locations,
path
});
} // All other errors
return GrampsError({
message,
locations,
path
});
};
exports.normalizeError = normalizeError;
const formatErrorGenerator = ({
hooks
}) => {
const {
onProcessedError,
onFinalError
} = hooks;
return function formatError(err) {
const error = normalizeError(err);
const stack = err.extensions.exception && err.extensions.exception.stacktrace || [];
onProcessedError(error, stack);
const {
data,
isServer
} = error;
const {
payload
} = error.output;
let finalError = { ...payload,
data
};
if (isServer) {
delete finalError.data;
}
onFinalError(finalError);
return finalError;
};
};
/**
* Custom error formatting for the GraphQL server.
* @see http://dev.apollodata.com/tools/apollo-server/setup.html#graphqlOptions
* @see https://github.com/GiladShoham/graphql-apollo-errors
*/
exports.formatErrorGenerator = formatErrorGenerator;
const formatError = (logger = console) => formatErrorGenerator({
hooks: {
onProcessedError: printDetailedServerLog(logger),
onFinalError: formatClientErrorData
}
});
exports.formatError = formatError;