gramps-graphql/gramps-errors

View on GitHub
src/index.js

Summary

Maintainability
A
0 mins
Test Coverage
import uuid from 'uuid';
import { EOL } from 'os';
import {
initSevenBoom,
SevenBoom,
formatErrorGenerator,
} from 'graphql-apollo-errors';
 
// 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.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,
},
];
 
// Add the custom fields. Copy the array because this function mutates its args.
initSevenBoom([...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
* @param {boolean?} serializeError whether to serialize the error into JSON
* @return {Error} SevenBoom error for output, or normal Error if serialized
*/
export function GrampsError(
{
error = false,
statusCode = false,
data = null,
message = '',
description = null,
errorCode = 'GRAMPS_ERROR',
graphqlModel = null,
targetEndpoint = null,
docsLink = null,
} = {},
serializeError = false,
) {
const httpErrorCode = statusCode || error.statusCode || 500;
 
// If we’re wrapping an error, the function and first three arguments change.
const fn = error ? 'wrap' : 'create';
const baseArgs = error
? [error, httpErrorCode, message]
: [httpErrorCode, message, data];
 
// Add the custom arguments to the first three.
const args = baseArgs.concat([
description,
errorCode,
graphqlModel,
targetEndpoint,
docsLink,
]);
 
// Call the function and spread the args array into individual arguments.
const boom = SevenBoom[fn](...args);
 
// If specified, serialize the error into a JSON string so it can be parsed later.
if (serializeError) {
const serializedBoom = JSON.stringify(boom);
 
return Error(serializedBoom);
}
 
// Otherwise, just return the error.
return 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}: ${error[field.name]}`)
.filter(field => !!field);
 
/**
* Checks incoming errors to ensure the formatting is correct.
* @param {Object} error error to be formatted
* @return {GrampsError} formatted GrAMPS error
*/
export const handleQueryErrors = error => {
if (!error.isBoom) {
// Check to make sure we don’t swallow GraphQL syntax errors, etc..
return GrampsError({
error,
errorCode: 'GRAPHQL_ERROR',
description: error.message,
data: {
locations: error.locations,
path: error.path,
},
});
}
 
return error;
};
 
/**
* 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}
*/
export const printDetailedServerLog = logger => ({
data,
stack,
output: { payload },
}) => {
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(EOL), stack].join(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
*/
export 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;
delete error.message;
}
 
// To avoid escaped quotes, change them to single quotes.
error.description = error.description.replace(/"/g, "'");
 
/* eslint-enable no-param-reassign */
 
return error;
};
 
/**
* Attempts to deserialize a stringified error message.
* @param {Error} error
* @return {Error}
*/
export const deserializeError = error => {
// If the error message is valid JSON, we convert it back to a GrAMPS error.
try {
const deserialized = JSON.parse(error.message);
const payload = deserialized.output.payload;
 
return GrampsError({
...payload,
error: Error(),
});
} catch (exception) {
// If not, we just pass it through
return error;
}
};
 
/**
* 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
*/
export const formatError = (logger = console) =>
formatErrorGenerator({
hooks: {
onOriginalError: handleQueryErrors,
onProcessedError: printDetailedServerLog(logger),
onFinalError: formatClientErrorData,
},
});