├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── circle.yml ├── index.d.ts ├── lib ├── index.js └── index.spec.js ├── package.json └── wallaby.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [ 6 | "syntax-async-functions", 7 | "transform-regenerator", 8 | "transform-object-rest-spread" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "parser": "babel-eslint", 4 | 5 | "env": { 6 | "es6": true, 7 | "mocha": true, 8 | "node": true, 9 | "browser": true 10 | }, 11 | 12 | "rules": { 13 | "strict": 0 14 | }, 15 | 16 | "parserOptions": { 17 | "ecmaVersion": 7, 18 | "experimentalObjectRestSpread": true, 19 | "sourceType": "module" 20 | }, 21 | 22 | "extends": [ "eslint:recommended" ] 23 | 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Gilad Shoham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-apollo-errors 2 | A small library to handle graphql and apollo errors in a better way 3 | 4 | This library is fully tested with 100% coverage 5 | 6 | [![CircleCI](https://circleci.com/gh/GiladShoham/graphql-apollo-errors/tree/master.svg?style=svg)](https://circleci.com/gh/GiladShoham/graphql-apollo-errors/tree/master) 7 | [![Coverage Status](https://coveralls.io/repos/github/GiladShoham/graphql-apollo-errors/badge.svg?branch=master)](https://coveralls.io/github/GiladShoham/graphql-apollo-errors?branch=master) 8 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=CYFBUDM226DLS&lc=IL&item_name=graphql%2dapollo%2derrors&item_number=github%2dnpm¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) 9 | 10 | ## Initiative 11 | Error handling requires few core features to be useful: 12 | * Ability to customize error in well defined structure cross app 13 | * Ability to hook the error bubbling (In order to log or store the errors somewhere) 14 | * Ability to send the error to the client while sending all the relevant information yet keeping all the sensitive information only on the server 15 | 16 | Looking around I found only 2 libraries dealing with errors in graphql and apollo - [graphql-errors](https://github.com/kadirahq/graphql-errors) , [apollo-errors](https://github.com/thebigredgeek/apollo-errors). 17 | 18 | Both libraries are great start, but they are not powerful enough for my opinion, therefore I decided to write my own error handler. 19 | Talking with some friends, I understand I'm not alone with this need, so I created this library as open source. 20 | 21 | ## Usage 22 | 23 | (Look in the spec files to understand more) 24 | 25 | Configure apollo error formatting 26 | 27 | ```js 28 | import express from 'express'; 29 | import bodyParser from 'body-parser'; 30 | import { formatErrorGenerator } from 'graphql-apollo-errors'; 31 | import schema from './schema'; 32 | // You can use what ever you want, this is just an example 33 | var logger = require('minilog')('errors-logger'); 34 | 35 | const formatErrorOptions = { 36 | logger, 37 | publicDataPath: 'public', // Only data under this path in the data object will be sent to the client (path parts should be separated by . - some.public.path) 38 | showLocations: true, // whether to add the graphql locations to the final error (default false) 39 | showPath: true, // whether to add the graphql path to the final error (default false) 40 | hideSensitiveData: false, // whether to remove the data object from internal server errors (default true) 41 | hooks: { 42 | // This run on the error you really throw from your code (not the graphql error - it means not with path and locations) 43 | onOriginalError: (originalError) => {logger.info(originalError.message)}, 44 | // This will run on the processed error, which means after we convert it to boom error if needed 45 | // and after we added the path and location (if requested) 46 | // If the error is not a boom error, this error won't include the original message but general internal server error message 47 | // This will run before we take only the payload and the public path of data 48 | onProcessedError: (processedError) => {logger.info(processedError.message)}, 49 | // This will run on the final error, it will only contains the output.payload, and if you configured the publicDataPath 50 | // it will only contain this data under the data object 51 | // If the error is internal error this error will be a wrapped internal error which not contains the sensitive details 52 | // This is the error which will be sent to the client 53 | onFinalError: (finalError) => {logger.info(finalError.message)}, 54 | }, 55 | nonBoomTransformer: (nonBoomError) => {error instanceof GraphQLError ? SevenBoom.badRequest(error.message) : SevenBoom.badImplementation(error)} 56 | // Optional function to transform non-Boom errors, such as those from Apollo & other 3rd-party libraries, into Boom errors 57 | }; 58 | const formatError = formatErrorGenerator(formatErrorOptions); 59 | const app = express(); 60 | 61 | app.use('/graphql', 62 | bodyParser.json(), 63 | graphqlExpress({ 64 | formatError, 65 | schema 66 | }) 67 | ); 68 | 69 | app.listen(8080) 70 | ``` 71 | 72 | Init SevenBoom object 73 | The defalut args for SevenBoom are 74 | ```js 75 | const defaultArgsDef = [ 76 | { 77 | name : 'errorCode', 78 | order: 1 79 | }, { 80 | name : 'timeThrown', 81 | order: 2, 82 | default: null 83 | }, { 84 | name : 'guid', 85 | order: 3, 86 | default: null 87 | } 88 | ]; 89 | ``` 90 | If you want you can change it using the initSevenBoom function: 91 | ```js 92 | import { initSevenBoom } from 'graphql-apollo-errors'; 93 | const customArgsDefs = [ 94 | { 95 | name : 'errorCode', 96 | order: 1 97 | } 98 | ]; 99 | initSevenBoom(customArgsDefs); 100 | ``` 101 | 102 | Use SevenBoom to create your custom error and throw it. 103 | 104 | ```js 105 | import { SevenBoom } from 'graphql-apollo-errors'; 106 | 107 | // A resolver which throws error 108 | const getUserByIdResolver = (root, { userId }, context) => { 109 | UserService.getUserById(userId) 110 | .then((user) => { 111 | if (user) return user; 112 | const errorMessage = `User with id: ${userId} not found`; 113 | const errorData = { userId }; 114 | const errorName = 'USER_NOT_FOUND'; 115 | const err = SevenBoom.notFound(errorMessage, errorData, errorName); 116 | throw(err); 117 | } 118 | } 119 | ``` 120 | 121 | Enjoy your shiny error on the client 122 | ```js 123 | { 124 | "data": {}, 125 | "errors": [ 126 | { 127 | statusCode: 404, 128 | error: 'Not Found', 129 | message: 'User with id: 123 not found.', 130 | code: 'USER_NOT_FOUND', 131 | timeThrown: "2017-01-16T21:25:58.536Z", 132 | guid: 'b6c44655-0aae-486a-8d28-533db6c6c343', 133 | data: { 134 | userId: '123' 135 | } 136 | } 137 | ] 138 | } 139 | ``` 140 | ## Upgrade from v1.*.* 141 | There is a lot of changes from v1. (In the implementation, which leads to API changes) 142 | * onStoredError hook is no longer exist (actually the onOriginalError result is the same as the onStoredError before) 143 | * You should not use the throwError any more (it was deleted), you can use the native throw now. 144 | 145 | ## How does it work 146 | In general this library contain 2 parts: 147 | 148 | 1. [SevenBoom](https://github.com/GiladShoham/seven-boom) - A small library i wrote to create customize errors 149 | 2. format error function - which knows to fetch the real error, hide sensitive server data, add some hooks points and configuration, and pass it to the client. 150 | 151 | ## License 152 | MIT - Do what ever you want 153 | 154 | ## Contribute 155 | I'm open to hear any feedback - new ideas, bugs, needs. 156 | Feel free to open issues / PR 157 | 158 | ## Support on PayPal 159 | Hey dude! Help me out for a couple of :beers:! 160 | 161 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=CYFBUDM226DLS&lc=IL&item_name=graphql%2dapollo%2derrors&item_number=github%2dnpm¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) 162 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | 3 | node: 4 | version: 4.2.6 5 | 6 | test: 7 | post: 8 | - npm run coveralls 9 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for `graphql-apollo-errors` 2 | // Project: https://github.com/GiladShoham/graphql-apollo-errors 3 | // Definitions by: Keith Gillette 4 | 5 | export function formatErrorGenerator(options: FormatErrorOptions): FormatErrorFunction; 6 | 7 | export interface ArgumentDefinition { 8 | name: string; 9 | order: number; 10 | default: string | Function | null | undefined; 11 | } 12 | 13 | export interface ErrorResult { 14 | isBoom: boolean; 15 | isServer: boolean; 16 | message: string; 17 | data: any; 18 | output: { 19 | statusCode: number; 20 | payload: ErrorPayload 21 | }; 22 | } 23 | 24 | export interface ErrorPayload { 25 | statusCode: number; 26 | error: string; 27 | message: string; 28 | errorName?: string; 29 | timeThrown?: string; 30 | guid?: string; 31 | 32 | [key: string]: any; 33 | 34 | data: any; 35 | } 36 | 37 | export interface FormatErrorOptions { 38 | logger?: Function; 39 | publicDataPath?: string; 40 | showLocations?: boolean; 41 | showPath?: boolean; 42 | hideSensitiveData?: boolean; 43 | hooks?: { 44 | onOriginalError?: (originalError: any) => void; 45 | onProcessedError?: (processedError: any) => void; 46 | onFinalError?: (finalError: any) => void; 47 | }; 48 | nonBoomTransformer?: (error: any) => ErrorResult; 49 | } 50 | 51 | export interface FormatErrorFunction { 52 | (graphQLError: any): any; 53 | } 54 | 55 | export declare module SevenBoom { 56 | 57 | export function init(argsDefs: ArgumentDefinition[]): void; 58 | 59 | export function wrap(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 60 | 61 | export function create(statusCode: number, errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 62 | 63 | export function badRequest(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 64 | 65 | export function unauthorized(errorMessage: string, scheme?: string | string[], attributes?: { [key: string]: any }, errorCode?: any): ErrorResult; 66 | 67 | export function paymentRequired(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 68 | 69 | export function forbidden(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 70 | 71 | export function notFound(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 72 | 73 | export function methodNotAllowed(errorMessage?: string, errorData?: any, allow?: string | string[], errorCode?: any): ErrorResult; 74 | 75 | export function notAcceptable(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 76 | 77 | export function proxyAuthRequired(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 78 | 79 | export function clientTimeout(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 80 | 81 | export function conflict(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 82 | 83 | export function resourceGone(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 84 | 85 | export function lengthRequired(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 86 | 87 | export function preconditionFailed(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 88 | 89 | export function entityTooLarge(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 90 | 91 | export function uriTooLong(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 92 | 93 | export function unsupportedMediaType(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 94 | 95 | export function rangeNotSatisfiable(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 96 | 97 | export function expectationFailed(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 98 | 99 | export function teapot(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 100 | 101 | export function badData(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 102 | 103 | export function locked(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 104 | 105 | export function preconditionRequired(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 106 | 107 | export function tooManyRequests(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 108 | 109 | export function illegal(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 110 | 111 | export function badImplementation(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 112 | 113 | export function notImplemented(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 114 | 115 | export function badGateway(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 116 | 117 | export function serverUnavailable(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 118 | 119 | export function gatewayTimeout(errorMessage?: string, errorData?: any, errorCode?: any): ErrorResult; 120 | 121 | } 122 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const log = require('minilog')('graphql-apollo-error'); 2 | require('minilog').enable(); 3 | import _ from 'lodash'; 4 | import SevenBoom from 'seven-boom'; 5 | 6 | const defaultArgsDef = [ 7 | { 8 | name: 'errorCode', 9 | order: 1 10 | }, { 11 | name: 'timeThrown', 12 | order: 2, 13 | default: null 14 | }, { 15 | name: 'guid', 16 | order: 3, 17 | default: null 18 | } 19 | ]; 20 | SevenBoom.init(defaultArgsDef); 21 | 22 | const defaultFormatErrorOptions = { 23 | logger: log, 24 | publicDataPath: '', 25 | hideSensitiveData: true, 26 | hooks: {} 27 | }; 28 | 29 | export {SevenBoom}; 30 | 31 | export const initSevenBoom = (argsDef) => { 32 | SevenBoom.init(argsDef); 33 | }; 34 | 35 | export const formatErrorGenerator = (formatErrorOptions) => { 36 | const actualOptions = _.defaults(formatErrorOptions, defaultFormatErrorOptions); 37 | let {logger, publicDataPath, hooks, showLocations, showPath, hideSensitiveData, nonBoomTransformer} = actualOptions; 38 | const {onOriginalError, onProcessedError, onFinalError} = hooks; 39 | publicDataPath = publicDataPath ? `data.${publicDataPath}` : 'data'; 40 | 41 | return function formatError(graphqlError) { 42 | 43 | let err = graphqlError.originalError || graphqlError; 44 | 45 | if (onOriginalError && _.isFunction(onOriginalError)) { 46 | onOriginalError(err) 47 | } 48 | 49 | if (err === undefined || !err.isBoom) { 50 | if (nonBoomTransformer && _.isFunction(nonBoomTransformer)) { 51 | err = nonBoomTransformer(err) 52 | } else { 53 | logger[logger.info ? 'info' : 'log'](`Transform error to boom error: ${err}`); 54 | err = SevenBoom.wrap(err, 500); 55 | } 56 | } 57 | 58 | if (showLocations) { 59 | err.output.payload.locations = graphqlError.locations; 60 | } 61 | 62 | if (showPath) { 63 | err.output.payload.path = graphqlError.path; 64 | } 65 | 66 | if (onProcessedError && _.isFunction(onProcessedError)) { 67 | onProcessedError(err) 68 | } 69 | 70 | err.output.payload.data = _.get(err, publicDataPath, {}); 71 | let finalError = err.output.payload; 72 | 73 | // Special implementation for internal server errors 74 | if (err.isServer && hideSensitiveData) { 75 | // logger.error(err); 76 | delete finalError.data; 77 | } else { 78 | // logger.debug(err); 79 | } 80 | 81 | if (onFinalError && _.isFunction(onFinalError)) { 82 | onFinalError(finalError) 83 | } 84 | 85 | return finalError; 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /lib/index.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from "chai-as-promised"; 3 | 4 | import 'regenerator-runtime/runtime'; 5 | // var rewire = require("rewire"); 6 | // var index = rewire("./index.js"); 7 | 8 | import { initSevenBoom, SevenBoom, formatErrorGenerator } from './index'; 9 | 10 | chai.use(chaiAsPromised); 11 | chai.use(require('sinon-chai')); 12 | import sinon from 'sinon'; 13 | 14 | const expect = chai.expect; 15 | 16 | // Most of this tests are taken from the Boom repo 17 | // In order to make sure that you can just replace all Boom with SevenBoom 18 | describe('Init seven boom', () => { 19 | it('uses the default seven boom argsDefs - guid generator and timeThrown with errorCode arg', (done) => { 20 | const opts = [ 21 | { 22 | name : 'errorCode', 23 | order: 1 24 | }, { 25 | name : 'timeThrown', 26 | order: 2, 27 | default: null 28 | }, { 29 | name : 'guid', 30 | order: 3, 31 | default: null 32 | } 33 | ]; 34 | 35 | initSevenBoom(opts); 36 | const error = SevenBoom.badRequest('my message', {'key': 'val'}, 'myErrCode'); 37 | expect(error.output.payload.guid).to.be.a('string'); 38 | // expect(error.output.payload.timeThrown).to.be.a.dateString(); 39 | expect(error.message).to.equal('my message'); 40 | expect(error.output.statusCode).to.equal(400); 41 | expect(error.output.payload).to.include({ 42 | statusCode: 400, 43 | error: 'Bad Request', 44 | message: 'my message', 45 | errorCode: 'myErrCode' 46 | }); 47 | expect(error.data).to.include({'key': 'val'}); 48 | done(); 49 | }); 50 | 51 | it('I can override the default seven-boom args', (done) => { 52 | const opts = [ 53 | { 54 | name : 'myCustomField', 55 | order: 1 56 | } 57 | ]; 58 | 59 | initSevenBoom(opts); 60 | const error = SevenBoom.badRequest('my message', {'key': 'val'}, 'myCustomFieldValue'); 61 | expect(error.message).to.equal('my message'); 62 | expect(error.output.statusCode).to.equal(400); 63 | expect(error.output.payload).to.include({ 64 | statusCode: 400, 65 | error: 'Bad Request', 66 | message: 'my message', 67 | myCustomField: 'myCustomFieldValue' 68 | }); 69 | expect(error.data).to.include({'key': 'val'}); 70 | done(); 71 | }); 72 | }); 73 | 74 | describe('Format error', () => { 75 | it('Send all data object in default', (done) => { 76 | const formatError = formatErrorGenerator(); 77 | const errData = {'key': 'val'}; 78 | const error = SevenBoom.badRequest('my message', errData, 'myErrCode'); 79 | const err = _simulateGraphqlWrapping(error); 80 | const finalError = formatError(err); 81 | expect(finalError.data).to.equal(errData); 82 | done(); 83 | }); 84 | 85 | it('Send only publicPath data', (done) => { 86 | const fromatErrorOpts = { 87 | publicDataPath: 'public' 88 | } 89 | const formatError = formatErrorGenerator(fromatErrorOpts); 90 | const publicData = {'myPublic': 'data'}; 91 | const errData = {'key': 'val', public:publicData}; 92 | const error = SevenBoom.badRequest('my message', errData, 'myErrCode'); 93 | const err = _simulateGraphqlWrapping(error); 94 | const finalError = formatError(err); 95 | expect(finalError.data).to.equal(publicData); 96 | done(); 97 | }); 98 | 99 | it('Hooks are called', (done) => { 100 | const onOriginalError = sinon.spy(); 101 | const onProcessedError = sinon.spy(); 102 | const onFinalError = sinon.spy(); 103 | 104 | const hooks = { 105 | onOriginalError, 106 | onProcessedError, 107 | onFinalError 108 | } 109 | const fromatErrorOpts = { 110 | hooks 111 | } 112 | const formatError = formatErrorGenerator(fromatErrorOpts); 113 | // const errData = {'key': 'val'}; 114 | // const originalError = new Error('my message', errData, 'myErrCode'); 115 | const originalError = new Error('my message'); 116 | const processedError = SevenBoom.wrap(originalError, 500); 117 | const err = _simulateGraphqlWrapping(originalError); 118 | const finalError = formatError(err); 119 | expect( onOriginalError.calledWith(originalError) ).to.be.true; 120 | expect( onProcessedError.calledWith(processedError) ).to.be.true; 121 | expect( onFinalError.calledWith(finalError) ).to.be.true; 122 | done(); 123 | }); 124 | 125 | it('Transform regular error to SevenBoom error', (done) => { 126 | const formatError = formatErrorGenerator(); 127 | const error = new Error('my message'); 128 | const err = _simulateGraphqlWrapping(error); 129 | const finalError = formatError(err); 130 | expect( finalError.statusCode ).to.equal(500); 131 | expect( finalError.message ).to.equal('An internal server error occurred'); 132 | done(); 133 | }); 134 | 135 | it('Add the locations and path if requested', (done) => { 136 | const fromatErrorOpts = { 137 | showLocations: true, 138 | showPath: true 139 | } 140 | const formatError = formatErrorGenerator(fromatErrorOpts); 141 | const PATH = 'My path to the future'; 142 | const LOCATIONS = 'In a great place'; 143 | const error = new Error('my message'); 144 | const err = _simulateGraphqlWrapping(error, LOCATIONS, PATH); 145 | 146 | const finalError = formatError(err); 147 | expect( finalError.path ).to.equal(PATH); 148 | expect( finalError.locations ).to.equal(LOCATIONS); 149 | done(); 150 | }); 151 | 152 | it('Default hide sensitive data from internal error', (done) => { 153 | const argsDef = [ 154 | { 155 | name : 'errorCode', 156 | order: 1 157 | },{ 158 | name : 'timeThrown', 159 | order: 3, 160 | default: null 161 | }, { 162 | name : 'guid', 163 | order: 4, 164 | default: null 165 | } 166 | ]; 167 | initSevenBoom(argsDef); 168 | const formatError = formatErrorGenerator(); 169 | const sensitiveData = {'secret': 'SevenBoom'}; 170 | const internalError = SevenBoom.internal('Technial message which client should not see', sensitiveData, 'myErrCode'); 171 | const err = _simulateGraphqlWrapping(internalError); 172 | 173 | const finalError = formatError(err); 174 | expect( finalError.data ).to.be.empty; 175 | expect( finalError.statusCode ).to.equal(500); 176 | expect( finalError.message ).to.equal('An internal server error occurred'); 177 | 178 | done(); 179 | }); 180 | 181 | it('Do not hide sensitive data from internal error when specifically asked', (done) => { 182 | const argsDef = [ 183 | { 184 | name : 'errorCode', 185 | order: 1 186 | },{ 187 | name : 'timeThrown', 188 | order: 3, 189 | default: null 190 | }, { 191 | name : 'guid', 192 | order: 4, 193 | default: null 194 | } 195 | ]; 196 | initSevenBoom(argsDef); 197 | const formatError = formatErrorGenerator({hideSensitiveData: false}); 198 | const sensitiveData = {'secret': 'SevenBoom'}; 199 | const internalError = SevenBoom.internal('Technial message which client should not see', sensitiveData, 'myErrCode'); 200 | const err = _simulateGraphqlWrapping(internalError); 201 | const finalError = formatError(err); 202 | expect( finalError.data ).to.include(sensitiveData); 203 | expect( finalError.statusCode ).to.equal(500); 204 | expect( finalError.message ).to.equal('An internal server error occurred'); 205 | done(); 206 | }); 207 | }); 208 | 209 | // Code taken from grpahql implemantation here (with some changes): 210 | // https://github.com/graphql/graphql-js/blob/44f315d1ff72ab32b794937fd11a7f8e792fd873/src/error/GraphQLError.js#L66-L69 211 | function _simulateGraphqlWrapping(originalError, locations, path, nodes, source, positions){ 212 | var resultError = new Error(); 213 | Object.defineProperties(resultError, { 214 | message: { 215 | value: originalError.message, 216 | // By being enumerable, JSON.stringify will include `message` in the 217 | // resulting output. This ensures that the simplist possible GraphQL 218 | // service adheres to the spec. 219 | enumerable: true, 220 | writable: true 221 | }, 222 | locations: { 223 | // Coercing falsey values to undefined ensures they will not be included 224 | // in JSON.stringify() when not provided. 225 | value: locations || [ 226 | { 227 | "line": 5, 228 | "column": 12, 229 | "field": "email" // HERE 230 | } 231 | ], 232 | // By being enumerable, JSON.stringify will include `locations` in the 233 | // resulting output. This ensures that the simplist possible GraphQL 234 | // service adheres to the spec. 235 | enumerable: true 236 | }, 237 | path: { 238 | // Coercing falsey values to undefined ensures they will not be included 239 | // in JSON.stringify() when not provided. 240 | value: path || "Some path", 241 | // By being enumerable, JSON.stringify will include `path` in the 242 | // resulting output. This ensures that the simplist possible GraphQL 243 | // service adheres to the spec. 244 | enumerable: true 245 | }, 246 | nodes: { 247 | value: nodes || 'Nodes' 248 | }, 249 | source: { 250 | value: source || 'Source', 251 | }, 252 | positions: { 253 | value: positions || 'Positions', 254 | }, 255 | originalError: { 256 | value: originalError 257 | } 258 | }); 259 | 260 | return resultError; 261 | } 262 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-apollo-errors", 3 | "version": "2.0.3", 4 | "description": "A small library to handle graphql and apollo errors in a better way", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "compile": "babel -d dist/ lib/", 8 | "watch": "babel --watch -d dist/ lib/", 9 | "prepublish": "npm run compile", 10 | "test": "mocha --compilers js:babel-core/register \"lib/**/*.spec.js\"", 11 | "coverage": "istanbul cover _mocha -- --compilers js:babel-core/register \"lib/**/*.spec.js\" -R spec", 12 | "coveralls": "istanbul cover _mocha --report lcovonly -- --compilers js:babel-core/register \"lib/**/*.spec.js\" -R spec && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/GiladShoham/graphql-apollo-errors" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/GiladShoham/graphql-apollo-errors/issues" 20 | }, 21 | "keywords": [ 22 | "error", 23 | "apollostack", 24 | "graphql", 25 | "error", 26 | "api", 27 | "http" 28 | ], 29 | "author": "Gilad Shoham ", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "babel-cli": "^6.7.5", 33 | "babel-core": "^6.10.4", 34 | "babel-eslint": "^6.0.4", 35 | "babel-plugin-syntax-async-functions": "^6.8.0", 36 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 37 | "babel-plugin-transform-regenerator": "^6.9.0", 38 | "babel-preset-es2015": "^6.6.0", 39 | "babel-preset-react": "^6.11.1", 40 | "chai": "^3.5.0", 41 | "chai-as-promised": "^5.3.0", 42 | "chai-date-string": "^0.1.0", 43 | "coveralls": "^2.11.11", 44 | "eslint": "^3.15.0", 45 | "istanbul": "^1.1.0-alpha.1", 46 | "mocha": "^2.5.3", 47 | "mocha-lcov-reporter": "^1.2.0", 48 | "regenerator-runtime": "^0.10.1", 49 | "rewire": "^2.5.2", 50 | "sinon": "^1.17.4", 51 | "sinon-chai": "^2.8.0", 52 | "winston": "^2.2.0" 53 | }, 54 | "dependencies": { 55 | "lodash": "^4.17.4", 56 | "minilog": "^3.1.0", 57 | "seven-boom": "^1.0.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | return { 3 | files: [ 4 | 'lib/**/*.js', 5 | '!lib/**/*.spec.js', 6 | ], 7 | 8 | tests: [ 9 | 'lib/**/*.spec.js', 10 | ], 11 | 12 | compilers: { 13 | 'lib/**/*.js': wallaby.compilers.babel(), 14 | }, 15 | 16 | env: { 17 | type: 'node', 18 | }, 19 | 20 | }; 21 | }; 22 | --------------------------------------------------------------------------------