├── .gitignore ├── .travis.yml ├── BACKGROUND.md ├── PROPOSAL.md ├── README.md ├── lib ├── core │ ├── apollo-error-converter.js │ ├── constants.js │ ├── index.js │ ├── map-items.js │ └── tests │ │ ├── apollo-error-converter.core.test.js │ │ └── extend-map-item.core.test.js └── utils │ ├── create-apollo-error.js │ ├── error-handlers.js │ ├── get-map-item.js │ ├── index.js │ ├── lodash-is-plain-object │ ├── LICENSE │ ├── _Symbol.js │ ├── _baseGetTag.js │ ├── _freeGlobal.js │ ├── _getPrototype.js │ ├── _getRawTag.js │ ├── _objectToString.js │ ├── _overArg.js │ ├── _root.js │ ├── index.js │ ├── isObjectLike.js │ └── stubArray.js │ ├── parse-config-options.js │ ├── should-error-pass-through.js │ ├── tests │ ├── __mocks__ │ │ └── index.js │ ├── create-apollo-error.util.test.js │ ├── error-handlers.util.test.js │ ├── get-map-item.util.test.js │ ├── parse-config-options.util.test.js │ ├── should-error-pass-through.util.test.js │ └── utils.util.test.js │ └── utils.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | before_install: 5 | - npm i 6 | cache: 7 | - directories: 8 | - node_modules 9 | jobs: 10 | include: 11 | - stage: test 12 | script: 13 | - npm run test:travis -------------------------------------------------------------------------------- /BACKGROUND.md: -------------------------------------------------------------------------------- 1 | # Why was this package made? 2 | 3 | When designing a public facing API it's important to hide implementation details of your codebase for security and consumer experience. By design GraphQL provides a layer of abstraction that hides internal implementations behind its public facing Types, Queries, and Mutations. However, one of the most prevalent sources of leaking implementation details comes from Errors thrown within the API server's resolver functions. 4 | 5 | Thrown Errors should be transparent for internal use (logging) while also being cleaned and shaped for the consumer. The latter is designed to provide the minimum information needed for the consumer to identify and/or resolve the source of the issue. 6 | 7 | ## Current state of Apollo Server Error handling 8 | 9 | Apollo's goals revolve around providing powerful and intuitive tooling while remaining unopinionated with how that tooling should be used. The Apollo team has done a fantastic job with designing their various libraries but Error handling in Apollo Server is still a tedious and repetitious pain point. Let's explore the current state of Error handling in Apollo Server (full documentation can be found [here](https://www.apollographql.com/docs/apollo-server/features/errors)): 10 | 11 | - `NODE_ENV=production` strips stack traces from Errors sent in responses 12 | - `debug: false` in the `ApolloServer` constructor strips stack traces on other `NODE_ENV` settings 13 | - A set of `ApolloError` base & subclass constructors is available in all `apollo-server-X` libs 14 | - designed to adhere to the GraphQL spec Error procedures and ease of usage in associated Apollo Client libraries 15 | - subclasses can be derived from `ApolloError` to further customize Error handling 16 | - can be used in the codebase for catching original Errors and re-throwing in an `ApolloError` format 17 | - a `formatError` function option in the `ApolloServer` constructor 18 | - the gateway between Errors thrown within resolvers and the response to the API consumer 19 | - a function designed to receive an Error object, whatever is returned by the function is passed in the response 20 | - Errors can be captured here for logging and shaping before exiting 21 | 22 | In order to maintain core principles within the bounds of what Apollo Server makes available the following patterns emerge 23 | 24 | - resolver level handling 25 | - `try/catch` or `.catch()` usage in resolvers 26 | - logging is performed in each resolver 27 | - Errors can be caught and rethrown in an `ApolloError` format 28 | - **benefits** 29 | - custom handling of Error types with respect to logging and shaping 30 | - **issues** 31 | - **every single resolver** must have its Errors explicitly handled 32 | - behavior must be tediously repeated throughout the codebase 33 | - custom handling logic is not portable across codebases 34 | - global level handling 35 | - Errors are handled in a single location - the `formatError` function 36 | - Error logging can be performed 37 | - Error formatting through shaping or returning `ApolloError` objects 38 | - **benefits** 39 | - centralizes Error logging and shaping logic to reduce repitition 40 | - **issues** 41 | - complexity in customizing the handling of specific Error types 42 | - custom handling logic is not portable across codebases 43 | 44 | ## Solution 45 | 46 | `apollo-error-converter` is an answer that combines the benefits and negates the issues associated with the previous patterns. The core export `ApolloErrorConverter` [AEC] constructs a utility assigned to the `ApolloServer` constructor `formatError` option and provides global Error handling. It is designed to maintain core principles while removing tedious and repetitive logic throughout the codebase. 47 | 48 | AEC is designed to only expose `ApolloError`-shaped Errors to the API consumer. It accomplishes this by shaping and converting all resolver-thrown Errors into an `ApolloError` equivalent. At the same time it provides customizable logging behavior of original Errors for internal usage. 49 | 50 | AEC uses MapItems that define a configuration for how an Error should be processed and converted. An ErrorMap is used to associate a MapItem to an Error based on the Error's `name`, `code` or `type` property. Unmapped Errors (those that do not have an entry in the ErrorMap) are handled by a fallback MapItem. 51 | 52 | Here are some highlights: 53 | 54 | - never exposes raw Error details like types, messages, data and stack traces 55 | - only exposes readable and useable `ApolloError` Errors that adhere to the GraphQL spec and Apollo Client libs 56 | - **never write Error handling logic in resolvers or underlying functions again!** 57 | - for backwards compatibility and flexibility 58 | - any `ApolloError` objects received from resolvers are passed through untouched 59 | - allows teams to implement or transition from resolver-level Error handling 60 | - custom Error handling of "mapped Errors" through the use of an [ErrorMap](#ErrorMap) 61 | - the ErrorMap maps an Error by its `name`, `code` or `type` property to a MapItem 62 | - [MapItem](#MapItem) objects provide a simple means of Error handling on a per Error basis including 63 | - custom messages 64 | - custom supplementary data 65 | - custom logging behavior 66 | - MapItems can be assigned to multiple mappings for reusability 67 | - **ErrorMaps and MapItems are portable and extendable across codebases and teams** 68 | - define them once for your team or third party libs and reuse them in any other Apollo Server projects 69 | - easily extend or reconfigure individual MapItems or ErrorMaps to fit the needs of the project 70 | - automatic logging of "unmapped Errors" 71 | - over time your team can develop new MapItems from these logs to customize the handling of recurring Errors 72 | - customizable logging of mapped Errors 73 | - use a default or task-specific logger in a MapItem to help organize your Error logs 74 | -------------------------------------------------------------------------------- /PROPOSAL.md: -------------------------------------------------------------------------------- 1 | # ApolloError Error Mapping Pattern 2 | A design pattern for ensuring User facing Errors are always exposed as instances of `ApolloError`. This pattern restricts exposure of implementation details and provides a controlled and concise interface for Error handling in a GraphQL Apollo Server. 3 | 4 | The implementation can be broken down into 3 principles: 5 | - Each Data Source should export an Object of data interfaces and an optional Error Map respective to the data source: 6 | - Objects (models, utilities) which provide interfaces to their underlying data source 7 | - An Error Map which provides a mapping between the thrown Errors specific to the Data Source and its controlled equivalent ApolloError (or subclass) 8 | - Data Source methods and their usage within resolvers do not need to be wrapped in `try/catch` or `.catch` calls 9 | - All Errors thrown from within resolvers are funnelled to the final capturing zone: the `ApolloServer` config `formatError` option 10 | - `formatError` consumes the `rethrowAsApollo` utility which translates all thrown Errors into `ApolloError`/subclass equivalents before being emitted back to the client / User. 11 | 12 | ## Data Sources 13 | Each data source should provide interfaces through Object methods (Models) and / or utility functions. These functions and methods create the bridge between a resolver which consumes them and the underlying data sources. 14 | 15 | By separating the data sources from their consumption in the resolver layer we gain the following benefits: 16 | - Separation of concerns 17 | - Data Source: responsible for controlled interaction with underlying data 18 | - the only mechanism of direct interaction with the data 19 | - Resolver: responsible for controlled communication between a client / User and resolution of behavior with a data source 20 | - the only mechanism of direct communication with the consumer (client / User) 21 | - Modularized logic: data sources can be used in any future environments (both GraphQL and otherwise) as they have no affinity to their consumer 22 | 23 | The data source may optionally export a loader function which will perform any startup / connection logic and return the Data Source Object discussed above. This approach should be used when utilizing the Apollo Server `dataSources` option. 24 | 25 | Otherwise simply passing the Data Source Object into the Apollo Server `context` is appropriate. This approach indicates that startup / connection logic should be done in the same file as the Apollo Server construction. 26 | 27 | ### Error Map 28 | Each data source used in this Apollo Server pattern should provide an Error Map which provides a translation between Errors thrown by the data source and its corresponding `ApolloError` equivalent. The Error Map should contain entries for each Data Source Error `name` along with a mapping shape as a value. The mapping shape is as follows: 29 | 30 | ```js 31 | { 32 | // required fields 33 | message: String, 34 | constructor: ApolloError, 35 | // optional fields 36 | data:? Object | Function 37 | logger:? Boolean | Function 38 | } 39 | ``` 40 | 41 | - `message`: the final message emitted to the consumer 42 | - `constructor`: An `ApolloError` or subclass constructor that the Data Source Error will be converted to when rethrown 43 | - `data`: an optional field that provides additional context for the error 44 | - an Object with hard coded data specific to the Data Source Error 45 | - a function which consumes the original Data Source Error and produces a `data` Object output 46 | - `logger`: an optional field that controls original Data Source Error logging behavior 47 | - `Boolean`: `true` to use the default logger method (provided in the `rethrowAsApollo` constructor) 48 | - `false` or `undefined` means ignore logging 49 | - `Function`: a logger method reference to use for logging this type of Error 50 | 51 | Custom utility functions can be used to provide `data` shaping. It is recommended to place all utility functions as well as the Error Map in one file so that they can be easily referenced. 52 | 53 | To determine the Data Source Error types you can consult the library reference. Here is an example using `Sequelize` Error names: 54 | 55 | ```js 56 | const errorMap = { 57 | 'SequelizeValidationError': { 58 | message: 'Invalid Felds', 59 | constructor: UserInputError, 60 | data: (error) => shapeFieldErrors(error), 61 | // log omitted, do not log this Error 62 | }, 63 | 64 | 'SequelizeUniqueConstraintError': { 65 | message: 'Unique Violation', 66 | constructor: ValidationError, 67 | data: (error) => shapeFieldErrors(error), 68 | // log omitted, do not log this Error 69 | } 70 | }; 71 | ``` 72 | 73 | You can also create custom Error subclasses for the Data Source that can be thrown. In this case they should have a unique `name` property which identifies them. That `name` can be added to the Error Map to provide translation. 74 | 75 | ## `rethrowAsApollo` 76 | This utility should be used in the `formatError` Apollo Server config option. Its purpose is to: 77 | - consume every Error thrown by an underlying resolver 78 | - translate the Error into its `ApolloError` equivalent 79 | - perform logging as dictated by each Data Source Error Map and the logger configuration of `rethrowAsApollo` 80 | - throw the translated `ApolloError` instance which will be emitted back to the API consumer 81 | 82 | The `options` shape is as follows: 83 | 84 | ```js 85 | { 86 | errorMaps:? Object | Array, 87 | fallback:? ApolloError | Object 88 | logger:? Function 89 | } 90 | ``` 91 | 92 | There are several options and defaults that can be used: 93 | 94 | - `errorMaps` [optional]: Error Map Object(s) 95 | - `Array`: an Array of individual Error Maps which will be merged to a single Object 96 | - `Object`: a single Object which has already merged the individual Error Map Objects 97 | - `undefined`: no specific Error Maps provided, always use `fallback` 98 | - `fallback` [optional]: The fallback used when no Error Map match is found for the Error that was thrown 99 | - `ApolloError`: an `ApolloError` constructor that should be used during rethrowing 100 | - `Object`: an Object of the same shape as Error Map entries which defines the `message`, `constructor` and optional `data`, `logger` behavior 101 | - `undefined`: defaults to 102 | - `message`: Internal Server Error 103 | - `constructor`: `ApolloError` (base type) 104 | - `logger`: the `options.logger` value 105 | - `logger`: a logger method used for logging errors (as dictated by individual Error Map entries and/or `fallback` configuration) 106 | - `Function`: called whenever an Error should be logged 107 | - `undefined`: uses `console.error` 108 | 109 | ### instantiation behavior 110 | - All Error Map Item(s) are verified to ensure correct shape and valid ApolloError/subclass constructors 111 | - throws `Error(Invalid Error Map Item: ${JSON.stringify(item)})` if verification fails to prevent runtime errors 112 | 113 | ## Apollo Server Configuration Example 114 | In the Apollo Server configuration the `rethrowAsApollo` instance should be configured and passed in the Apollo Server `formatError` option field. 115 | 116 | Here is an example that has the following simple structure with a single Data Source: 117 | - Server Logger: `winston` with external configuration 118 | - Data Sources: Sequelize 119 | - Error Maps: Sequelize 120 | - configuration 121 | 122 | Directory Structure 123 | ```sh 124 | server/ 125 | index.js <--- Apollo Server construction / exporting 126 | data-sources/ 127 | index.js <--- main exporter of Data Source(s) + Error Map(s) 128 | sequelize/ 129 | index.js <--- exporter of models and errorMap 130 | errorMap.js <--- utility functions and Error Map exporter 131 | models/ <--- Sequelize models 132 | index.js <--- standard Sequelize models exporter 133 | ``` 134 | 135 | `server/data-sources/sequelize/errorMap.js` 136 | 137 | ```js 138 | // constructors can be imported from any valid Apollo Error (JavaScript) source 139 | // ex: apollo-server, apollo-server-express, ... 140 | const { UserInputError, ValidationError } = require('apollo-server-express'); 141 | 142 | /** 143 | * Extracts and shapes field errors from a Sequelize Error object 144 | * @param {ValidationError} validationError Sequelize ValidationError or subclass 145 | * @return {{ fieldName: string }} field errors object in { field: message, } form 146 | */ 147 | const shapeFieldErrors = (validationError) => { 148 | const { errors } = validationError; 149 | if (!errors) return {}; 150 | 151 | const fields = errors.reduce((output, validationErrorItem) => { 152 | const { message, path } = validationErrorItem; 153 | 154 | output[path] = message; 155 | return output; 156 | }, {}); 157 | 158 | return fields; 159 | }; 160 | 161 | const errorMap = { 162 | 'SequelizeValidationError': { 163 | message: 'Invalid Felds', 164 | constructor: UserInputError, 165 | data: (error) => shapeFieldErrors(error), 166 | }, 167 | 168 | 'SequelizeUniqueConstraintError': { 169 | message: 'Unique Violation', 170 | constructor: ValidationError, 171 | data: (error) => shapeFieldErrors(error), 172 | } 173 | }; 174 | 175 | module.exports = { 176 | errorMap, 177 | shapeFieldErrors, 178 | }; 179 | ``` 180 | 181 | `server/data-sources/sequelize/index.js` 182 | 183 | ```js 184 | const models = require('./models'); 185 | const { errorMap } = require('./errorMap'); 186 | 187 | module.exports = { 188 | models, 189 | errorMap 190 | }; 191 | ``` 192 | 193 | `server/data-sources/index.js` 194 | 195 | ```js 196 | const sequelize = require('./sequelize'); 197 | 198 | module.exports = { 199 | sequelize, 200 | }; 201 | ``` 202 | 203 | `server/index.js` 204 | 205 | ```js 206 | // imports of tooling 207 | 208 | // winston logger configured and exported from logger.js 209 | const logger = require('./logger'); 210 | 211 | // importing models initializes the Sequelize connection logic 212 | const { 213 | sequelize: { errorMap, models }, 214 | } = require('./data-sources'); 215 | 216 | const formatError = new rethrowAsApollo({ 217 | errorMaps: errorMap, // only one Error Map, pass directly 218 | logger: logger.error, // error logger 219 | // default fallback 220 | }); 221 | 222 | module.exports = new ApolloServer({ 223 | typeDefs, 224 | resolvers, 225 | formatError, 226 | context: ({ req }) => ({ 227 | // other context attributes 228 | models, // Sequelize models 229 | }) 230 | }); 231 | ``` 232 | 233 | This configuration would result in the following behavior: 234 | 235 | A `ValidationError` is thrown from a Sequelize model method consumed in a resolver: 236 | - translated to `UserInputError` 237 | - rethrown as `new UserInputError('Invalid Fields', { fields }) 238 | - no logging 239 | 240 | A `UniqueConstrainError` is thrown from a Sequelize model method consumed in a resolver: 241 | - translated to `ValidationError` 242 | - rethrown as `new ValidationError('Unique Violation', { fields }) 243 | - no logging 244 | 245 | Any un-mapped Error is thrown from a resolver: 246 | - translated to `ApolloError` 247 | - rethrown as `new ApolloError('Internal Server Error') 248 | - logged through `logger.error` for internal use 249 | 250 | Over time repeated Errors whose causes are determined can be added to the Error Map to provide customized rethrow and logging behavior. 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apollo Error Converter 2 | 3 | [![Build Status](https://travis-ci.org/the-vampiire/apollo-error-converter.svg?branch=master)](https://travis-ci.org/the-vampiire/apollo-error-converter) [![Coverage Status](https://coveralls.io/repos/github/the-vampiire/apollo-error-converter/badge.svg?branch=master)](https://coveralls.io/github/the-vampiire/apollo-error-converter?branch=master) [![NPM Package](https://img.shields.io/npm/v/apollo-error-converter.svg?label=NPM:%20apollo-error-converter)](https://npmjs.org/apollo-error-converter) 4 | 5 | A utility for greatly simplifying GraphQL Apollo Server Error handling without sacrificing core principles: 6 | 7 | 1. Hide implementation details exposed from Errors thrown by resolvers and their underlying calls 8 | 2. Provide logging of thrown Errors for internal administrative use and records 9 | 3. Provide clean and useful Errors to the clients consuming the API 10 | 11 | If you want to read more about the background and motivation for this package check out the [BACKGROUND.md](./BACKGROUND.md) file. 12 | 13 | # How it works 14 | 15 | Stop writing `try/catch` or rethrowing `ApolloErrors` throughout your Apollo Server API. Let the Errors flow! Apollo Error Converter will catch, log and convert all of your resolver-thrown Errors for you. All converted Errors adhere to the core principles you expect from a well designed API. AEC can be used with any `apollo-server-x` flavor. 16 | 17 | Using the [default configuration](#Default-Configuration) will provide you with a secure API in seconds. If you choose to customize AEC your ErrorMap and MapItems are not only simple to configure but portable and reusable across all of your GraphQL API projects! 18 | 19 | AEC categorizes the Errors it processes as either `mapped` or `unmapped`. Mapped Errors use an [ErrorMap](#ErrorMap) and [MapItems](#MapItem) to define how they should be logged and converted. Unmapped Errors use a fallback MapItem for processing. Any `ApolloError` you manually throw from a resolver will be passed through. 20 | 21 | The converted Errors all respect the [GraphQL spec](https://graphql.github.io/graphql-spec/draft/#sec-Errors) and will have the following shape: 22 | 23 | ```js 24 | { 25 | "errors": [ 26 | { 27 | "path": ["failure", "path"], 28 | "locations": [{ "line": #, "column": # }], 29 | "message": "your custom message", 30 | "extensions": { 31 | "data": { 32 | // custom data to include 33 | }, 34 | "code": "YOUR_CUSTOM_CODE" 35 | } 36 | } 37 | ], 38 | } 39 | ``` 40 | 41 | # Usage 42 | 43 | Install using [npm](https://npmjs.org/apollo-error-converter): 44 | 45 | ```sh 46 | npm i apollo-error-converter 47 | ``` 48 | 49 | Create an instance of `ApolloErrorConverter` and assign it to the `formatError` option of the `ApolloServer` constructor. 50 | 51 | ```js 52 | const { 53 | ApolloErrorConverter, // required: core export 54 | mapItemBases, // optional: MapItem bases of common Errors that can be extended 55 | extendMapItem, // optional: tool for extending MapItems with new configurations 56 | } = require("apollo-error-converter"); 57 | 58 | // assign it to the formatError option in ApolloError constructor 59 | new ApolloServer({ 60 | formatError: new ApolloErrorConverter(), // default 61 | formatError: new ApolloErrorConverter({ logger, fallback, errorMap }), // customize with options 62 | }); 63 | ``` 64 | 65 | # Configuration & Behavior 66 | 67 | ## Default Configuration 68 | 69 | ```js 70 | const { ApolloErrorConverter } = require("apollo-error-converter"); 71 | 72 | // assign it to the formatError option 73 | const server = new ApolloServer({ 74 | formatError: new ApolloErrorConverter(), 75 | }); 76 | ``` 77 | 78 | Behaviors for Errors handled by AEC`: 79 | 80 | - unmapped Errors 81 | - logged by default `logger` 82 | - `console.error` 83 | - converted to an `ApolloError` using the default `fallback` MapItem 84 | - `message`: `"Internal Server Error"` 85 | - `code`: `"INTERNAL_SERVER_ERROR"` 86 | - `data`: `{}` 87 | - mapped Errors 88 | - all Errors are considered unmapped in this configuration since there is no [ErrorMap](#ErrorMap) defined 89 | - `ApolloError` (or subclass) Errors 90 | - manually thrown from a resolver 91 | - no logging 92 | - passed through directly to the API consumer 93 | 94 | ## Custom Configuration 95 | 96 | For custom configurations take a look at the [Options](#Options) section below. 97 | 98 | ```js 99 | const { ApolloErrorConverter } = require("apollo-error-converter"); 100 | 101 | // assign it to the formatError option 102 | new ApolloServer({ 103 | formatError: new ApolloErrorConverter({ logger, fallback, errorMap }), 104 | }); 105 | ``` 106 | 107 | Behaviors for Errors handled by AEC: 108 | 109 | - unmapped Errors 110 | - logged using 111 | - `options.logger` 112 | - no logger defined: the default logger (see [Options](#Options)) 113 | - processed and converted using 114 | - `options.fallback` 115 | - no fallback: the default fallback (see [Options](#Options)) 116 | - mapped Errors 117 | - behavior dependent on [MapItem](#MapItem) configuration for the mapped Error 118 | - `ApolloError` (or subclass) Errors 119 | - no logging 120 | - passed through 121 | 122 | # Customization 123 | 124 | AEC can have its behavior customized through the [`options` object](#Options) in its constructor. In addition there are two other exports [extendMapItem](#extendMapItem) and [mapItemBases](#mapItemBases) that can be used to quickly generate or extend [MapItems](#MapItem). 125 | 126 | There is a [Full Example](#Full-Sequelize-Example) at the end of this doc that shows how an API using a Sequelize database source can configure AEC. The example includes defining an ErrorMap, MapItems, and using a `winston` logger. There is also a section with tips on [How to create your ErrorMap](#How-to-create-your-ErrorMap). 127 | 128 | ## Options 129 | 130 | AEC constructor signature & defaults: 131 | 132 | ```sh 133 | ApolloErrorConverter(options = {}, debug = false) -> formatError function 134 | ``` 135 | 136 | ### `options.logger`: used for logging Errors 137 | 138 | **default if `options.logger` is `undefined`** 139 | 140 | ```js 141 | const defaultLogger = console.error; 142 | ``` 143 | 144 | **custom `options.logger`, NOTE: `winston` logger users [see winston usage](#winston-logger-usage)** 145 | 146 | ```js 147 | const options = { 148 | logger: false, // disables logging of unmapped Errors 149 | logger: true, // enables logging using the default logger 150 | logger: yourLogger, // enables logging using this function / method 151 | }; 152 | ``` 153 | 154 | ### `options.fallback`: a MapItem used for processing unmapped Errors 155 | 156 | **default if `options.fallback` is `undefined`** 157 | 158 | ```js 159 | const defaultFallback = { 160 | logger: defaultLogger, // or options.logger if defined 161 | code: "INTERNAL_SERVER_ERROR", 162 | message: "Internal Server Error", 163 | data: {}, 164 | }; 165 | ``` 166 | 167 | **custom `options.fallback`, for more details on configuring a custom `fallback` see [MapItem](#MapItem)** 168 | 169 | ```js 170 | const options = { 171 | // a fallback MapItem 172 | fallback: { 173 | code: "", // the Error code you want to use 174 | message: "", // the Error message you want to use 175 | data: { 176 | // additional pre-formatted data to include 177 | }, 178 | data: originalError => { 179 | // use the original Error to format and return extra data to include 180 | return formattedDataObject; 181 | }, 182 | }, 183 | }; 184 | ``` 185 | 186 | ### `errorMap`: the ErrorMap used for customized Error processing 187 | 188 | The ErrorMap associates an Error to a [MapItem](#MapItem) by the `name`, `code` or `type` property of the original Error object. You can reuse MapItems for more than one entry. 189 | 190 | **default if `options.errorMap` is `undefined`** 191 | 192 | ```js 193 | const defaultErrorMap = {}; 194 | ``` 195 | 196 | **custom `options.errorMap`, see [ErrorMap](#ErrorMap) for design details and [MapItem](#MapItem) for configuring an Error mapping** 197 | 198 | ```js 199 | const options = { 200 | errorMap: { 201 | ErrorName: MapItem, // map by error.name 202 | ErrorCode: MapItem, // map by error.code 203 | ErrorType: MapItem, // map by error.type 204 | }, 205 | }; 206 | ``` 207 | 208 | **multiple ErrorMaps** 209 | 210 | The most robust way to use AEC is to create ErrorMaps for each of your data sources. You can then reuse the ErrorMaps in other projects that use those data sources. You can pass an Array of ErrorMap Objects as `options.errorMap` which will be automatically merged. 211 | 212 | ```js 213 | const options = { 214 | errorMap: [errorMap, otherErrorMap], 215 | }; 216 | ``` 217 | 218 | **note: the `errorMap` is validated during construction of AEC** 219 | 220 | - an Error will be thrown by the first `MapItem` that is found to be invalid within the `errorMap` or merged `errorMap` 221 | - this validation prevents unexpected runtime Errors after server startup 222 | 223 | ## `winston` logger usage 224 | 225 | Winston logger "level methods" are bound to the objects they are assigned to. Due to the way [winston is designed](https://github.com/winstonjs/winston/issues/1591#issuecomment-459335734) passing the logger method as `options.logger` will bind `this` to the options object and cause the following Error when used: 226 | 227 | ```sh 228 | TypeError: self._addDefaultMeta is not a function 229 | ``` 230 | 231 | In order to pass a `winston` logger level method as `options.logger` use the following approach: 232 | 233 | ```js 234 | const logger = require("./logger"); // winston logger object 235 | const { ApolloErrorConverter } = require("apollo-error-converter"); 236 | 237 | new ApolloServer({ 238 | formatError: new ApolloErrorConverter({ 239 | // assign logger. as the configured logger 240 | logger: logger.error.bind(logger), // bind the original winston logger object 241 | }), 242 | }); 243 | ``` 244 | 245 | **this behavior also applies to `winston` logger methods assigned in [MapItem](#MapItem) configurations** 246 | 247 | ```js 248 | const mapItem = { 249 | // other MapItem options, 250 | logger: logger.warning.bind(logger), 251 | }; 252 | ``` 253 | 254 | ## Debug Mode 255 | 256 | Debug mode behaves as if no `formatError` function exists. 257 | 258 | - all Errors are passed through directly from the API server to the consumer 259 | - to enter debug mode pass `true` as the second argument in the constructor 260 | 261 | ```js 262 | new ApolloServer({ 263 | formatError: new ApolloErrorConverter(options, true), 264 | }); 265 | ``` 266 | 267 | # ErrorMap 268 | 269 | The ErrorMap is a registry for mapping Errors that should receive custom handling. It can be passed as a single object or an Array of individual ErrorMaps which are automatically merged (see [Options](#Options) for details). 270 | 271 | For tips on designing your ErrorMap See [How to create your ErrorMap](#How-to-create-your-ErrorMap) at the end of the docs. 272 | 273 | ErrorMaps are made up of `ErrorIdentifier: MapItem` mapping entries. Error Objects can be identified by their `name`, `code` or `type` property. 274 | 275 | Core `NodeJS` Errors use the `code` property to distinguish themselves. However, 3rd party libraries with custom Errors use a mixture of `.name`, `.code` and `type` properties. 276 | 277 | **examples** 278 | 279 | ```js 280 | const errorMap = { 281 | // error.name is "ValidationError" 282 | ValidationError: MapItem, 283 | // error.code is "ECONNREFUSED" 284 | ECONNREFUSED: MapItem, 285 | // error.type is "UniqueConstraint" 286 | UniqueConstraint: MapItem, 287 | }; 288 | ``` 289 | 290 | You can choose to create multiple ErrorMaps specific to each of your underlying data sources or create a single ErrorMap for your entire API. In the future I hope people share their MapItems and ErrorMaps to make this process even easier. 291 | 292 | # MapItem 293 | 294 | The MapItem represents a configuration for processing an Error matched in the ErrorMap. You can also set AEC `options.fallback` to a MapItem to customize how unmapped Errors should be handled. MapItems can be reused by assigning them to multiple Error identifiers in the [ErrorMap](#ErrorMap). 295 | 296 | MapItems can be created using object literals or extended from another MapItem using the additional package export [extendMapItem](#extendMapItem). 297 | 298 | A MapItem configuration is made up of 4 options: 299 | 300 | ```js 301 | const mapItem = { 302 | code, 303 | data, 304 | logger, 305 | message, // required 306 | }; 307 | ``` 308 | 309 | **REQUIRED** 310 | 311 | - `message`: the client-facing message 312 | - appears as a top level property in the Error emitted by Apollo Server 313 | 314 | **OPTIONAL** 315 | 316 | - `logger`: used for logging the original Error 317 | - default: does not log this Error 318 | - `false`: does not log this Error 319 | - `true`: logs using AEC `options.logger` 320 | - `function`: logs using this function 321 | - **`winston` logger users [see note on usage](#winston-logger-users)** 322 | - `code`: a code for this type of Error 323 | - default: `'INTERNAL_SERVER_ERROR'` 324 | - Apollo suggested format: `ALL_CAPS_AND_SNAKE_CASE` 325 | - appears in `extensions.code` property of the Error emitted by Apollo Server 326 | - `data`: used for providing supplementary data to the API consumer 327 | - default: `{}` empty Object 328 | - an object: `{}` 329 | - preformatted data to be added to the converted Error 330 | - a function: `(originalError) -> {}` 331 | - a function that receives the original Error and returns a formatted `data` object 332 | - useful for extracting / shaping Error data that you want to expose to the consumer 333 | - appears in `extensions.data` property of the Error emitted by Apollo Server 334 | - **example of a data processing function (from [Full Sequelize Example](#Full-Sequelize-Example))** 335 | 336 | ```js 337 | /** 338 | * Extracts and shapes field errors from a Sequelize Error object 339 | * @param {ValidationError} validationError Sequelize ValidationError or subclass 340 | * @return {{ fieldName: string }} field errors object in { field: message, } form 341 | */ 342 | const shapeFieldErrors = validationError => { 343 | const { errors } = validationError; 344 | if (!errors) return {}; 345 | 346 | const fields = errors.reduce((output, validationErrorItem) => { 347 | const { path, message } = validationErrorItem; 348 | return { ...output, [path]: message }; 349 | }, {}); 350 | 351 | return fields; 352 | }; 353 | 354 | const mapItem = { 355 | data: shapeFieldErrors, 356 | code: "INVALID_FIELDS", 357 | message: "these fields are no good man", 358 | }; 359 | 360 | const errorMap = { 361 | ValidationError: mapItem, 362 | }; 363 | ``` 364 | 365 | # extendMapItem 366 | 367 | The `extendMapItem()` utility creates a new MapItem from a base and extending options. The `options` argument is the same as that of the [MapItem](#MapItem). If an option already exists on the base MapItem it will be overwritten by the value provided in `options`. 368 | 369 | **If the configuration provided in the `options` results in an invalid MapItem an Error will be thrown.** 370 | 371 | ```js 372 | const mapItem = extendMapItem(mapItemToExtend, { 373 | // new configuration options to be applied 374 | code, 375 | data, 376 | logger, 377 | message, 378 | }); 379 | 380 | // add the new MapItem to your ErrorMap 381 | ``` 382 | 383 | ## mapItemBases 384 | 385 | As a convenience there are some MapItems provided that can be used for extension or as MapItems themselves. They each have the minimum `message` and `code` properties assigned. 386 | 387 | ```js 388 | const InvalidFields = { 389 | code: "INVALID_FIELDS", 390 | message: "Invalid Field Values", 391 | }; 392 | 393 | const UniqueConstraint = { 394 | code: "UNIQUE_CONSTRAINT", 395 | message: "Unique Constraint Violation", 396 | }; 397 | ``` 398 | 399 | **usage** 400 | 401 | ```js 402 | const { 403 | extendMapItem, 404 | mapItemBases: { InvalidFields }, 405 | } = require("apollo-error-converter"); 406 | 407 | const mapItem = extendMapItem(InvalidFields, { 408 | message: "these fields are no good man", 409 | data: error => { 410 | /* extract some Error data and return an object */ 411 | }, 412 | }); 413 | 414 | // mapItem has the same InvalidFields code with new message and data properties 415 | ``` 416 | 417 | ## How to create your ErrorMap 418 | 419 | When designing your ErrorMap you need to determine which `ErrorIdentifier`, the `name`, `code` or `type` property of the Error object, to use as a mapping key. Once you know the identifier you can assign a MapItem to that entry. Here are some suggestions on determining the identifiers: 420 | 421 | Using AEC logged Errors 422 | 423 | - Because unmapped Errors are automatically logged (unless you explicitly turn off logging) you can reflect on common Errors showing up in your logs and create [MapItems](#MapItem) to handle them 424 | - check the `name`, `code` or `type` property of the Error in your log files 425 | - determine which is suitable as an identifier and create an entry in your ErrorMap 426 | 427 | Determining identifiers during development 428 | 429 | - Inspect Errors during testing / development 430 | - log the Error itself or `error.[name, code, type]` properties to determine which identifier is suitable 431 | 432 | Determining from Library code 433 | 434 | - Most well-known libraries define their own custom Errors 435 | - do a GitHub repo search for `Error` which may land you in a file / module designated for custom Errors 436 | - see what `name`, `code` or `type` properties are associated with the types of Errors you want to map 437 | - links to some common library's custom Errors 438 | - [NodeJS system `code` properties](https://nodejs.org/api/errors.html#errors_common_system_errors) 439 | - many 3rd party libs wrap native Node system calls (like HTTP or file usage) which use these Error codes 440 | - for example `axios` uses the native `http` module to make its requests - it will throw Errors using the `http` related Error `codes` 441 | - [Mongoose (MongoDB ODM) `name` / `code` properties](https://github.com/Automattic/mongoose/blob/master/lib/error/mongooseError.js#L30) 442 | - **note that Schema index based Errors (like unique constraints)** will have the generic `MongooseError` `name` property. Use the `code` property of the Error to map these types 443 | - `error.code = 11000` is associated with unique index violations 444 | - `error.code = 11001` is associated with bulk unique index violations 445 | - [Sequelize (SQL ORM) `name` properties](https://doc.esdoc.org/github.com/sequelize/sequelize/identifiers.html#errors) 446 | - [ObjectionJS (SQL ORM)](https://vincit.github.io/objection.js/) 447 | - [ValidationError](https://vincit.github.io/objection.js/api/types/#class-validationerror) 448 | - [NotFoundError](https://vincit.github.io/objection.js/api/types/#class-notfounderror) 449 | - more robust Errors using the [`objection-db-errors` plugin](https://github.com/Vincit/db-errors) 450 | 451 | ## Full Sequelize Example 452 | 453 | Here is an example that maps Sequelize Errors and uses `winston` logger methods. It is all done in one file here for readability but would likely be separated in a real project. 454 | 455 | A good idea for organization is to have each data source (db or service) used in your API export their corresponding ErrorMap. You can also centralize your ErrorMaps as a team-scoped (or public!) package that you install in your APIs. You can then merge these ErrorMaps by passing them as an Array to AEC `options` (see below). 456 | 457 | ```js 458 | const { 459 | ApolloServer, 460 | ApolloError, 461 | UserInputError, 462 | } = require("apollo-server-express"); 463 | const { 464 | ApolloErrorConverter, 465 | extendMapItem, 466 | mapItemBases, 467 | } = require("apollo-error-converter"); 468 | 469 | const logger = require("./logger"); // winston logger, must be binded 470 | const { schema, typeDefs } = require("./schema"); 471 | 472 | /** 473 | * Extracts and shapes field errors from a Sequelize Error object 474 | * @param {ValidationError} validationError Sequelize ValidationError or subclass 475 | * @return {{ fieldName: string }} field errors object in { field: message, } form 476 | */ 477 | const shapeFieldErrors = validationError => { 478 | const { errors } = validationError; 479 | if (!errors) return {}; 480 | 481 | const fields = errors.reduce((output, validationErrorItem) => { 482 | const { path, message } = validationErrorItem; 483 | return { ...output, [path]: message }; 484 | }, {}); 485 | 486 | return fields; 487 | }; 488 | 489 | const fallback = { 490 | message: "Something has gone horribly wrong", 491 | code: "INTERNAL_SERVER_ERROR", 492 | data: () => ({ timestamp: Date.now() }), 493 | }; 494 | 495 | const sequelizeErrorMap = { 496 | SequelizeValidationError: extendMapItem(mapItemBases.InvalidFields, { 497 | data: shapeFieldErrors, 498 | }), 499 | 500 | SequelizeUniqueConstraintError: extendMapItem(mapItemBases.UniqueConstraint, { 501 | logger: logger.db.bind(logger), // db specific logger, winston logger must be binded 502 | }), 503 | }; 504 | 505 | const formatError = new ApolloErrorConverter({ 506 | errorMap: sequelizeErrorMap, 507 | // or for multiple data source ErrorMaps 508 | errorMap: [sequelizeErrorMap, otherDataSourceErrorMap], 509 | fallback, 510 | logger: logger.error.bind(logger), // error specific logger, winston logger must be binded 511 | }); 512 | 513 | module.exports = new ApolloServer({ 514 | typeDefs, 515 | resolvers, 516 | formatError, 517 | }); 518 | ``` 519 | 520 | Behaviors for Errors received in `formatError`: 521 | 522 | - unmapped Errors 523 | - logged by `logger.error` method from a `winston` logger 524 | - converted using custom `fallback` 525 | - sets a custom `message`, `code` and `data.timestamp` 526 | - mapped Errors 527 | - `SequelizeUniqueConstraintError` 528 | - extends `UniqueConstraint` from `mapItemBases` 529 | - (from base) uses code `'UNIQUE_CONSTRAINT'` 530 | - (from base) uses message `'Unique Constrain Violation'` 531 | - (extended) logs original Error with `logger.db` method 532 | - `SequelizeValidationError` 533 | - extends `InvalidFields` from `mapItemBases` 534 | - (from base) uses code `'INVALID_FIELDS'` 535 | - (from base) uses message `'Invalid Field Values'` 536 | - (extended) adds field error messages extracted from the original Error by `shapeFieldErrors()` 537 | - does not log 538 | - `ApolloError` (or subclass) Errors 539 | - no logging 540 | - passed through 541 | -------------------------------------------------------------------------------- /lib/core/apollo-error-converter.js: -------------------------------------------------------------------------------- 1 | const { 2 | getMapItem, 3 | parseConfigOptions, 4 | handleMappedError, 5 | handleUnmappedError, 6 | shouldErrorPassThrough, 7 | } = require("../utils"); 8 | 9 | /** 10 | * @typedef {{ string: MapItem }} ErrorMap 11 | * @typedef {{ message: string, code?: string, logger?: boolean | function, data?: {} | function }} MapItem 12 | */ 13 | 14 | /** 15 | * Contructs a function for automatically converting all resolver-thrown errors into ApolloError equivalents 16 | * The returned function should be assigned to the ApolloServer constructor options.formatError field 17 | * 18 | * Behaviors of Errors thrown from resolvers (or underlying resolver calls) 19 | * - Mapped Error thrown (error.[name, code, type] mapping found in errorMap) 20 | * - only logs if instructed in the mapped MapItem configuration 21 | * - converts to ApolloError according to MapItem configuration 22 | * - Unmapped Error thrown (Error mapping not found in errorMap) 23 | * - logs the Error using options.logger / default logger 24 | * - converts to ApolloError according to options.fallback MapItem 25 | * - ApolloError thrown (indicates Error handling was implemented in the resolver) 26 | * - does not log 27 | * - passes any ApolloError through 28 | * - debug mode enabled 29 | * - passes all Errors through (does not process any Errors) 30 | * 31 | * Options defaults 32 | * - debug: false 33 | * - errorMap: {} 34 | * - logger: console.error 35 | * - fallback: 36 | * - code: "INTERNAL_SERVER_ERROR" 37 | * - message: "Internal Server Error" 38 | * - logger: options.logger or default logger 39 | * @param options 40 | * @param {ErrorMap | [ErrorMap]} options.errorMap used for Mapped Errors, Arrays are merged into a single ErrorMap object 41 | * @param {FallbackMapItem | function} options.fallback MapItem used for Unmapped Errors 42 | * @param {boolean | function} options.logger: false to disable logging (Mapped & Unmapped), true to use default logger, or a function which accepts an Error object 43 | * @param {boolean} debug [false] if true, does nothing (passes all Errors through) 44 | * 45 | * @example 46 | * const { ApolloErrorConverter } = require('apollo-error-converter'); 47 | * 48 | * const options = { 49 | * errorMap: // (optional) { errorName: MapItem, errorCode: MapItem, ... } or [ErrorMap, ...] 50 | * logger: // (optional) function for logging Errors 51 | * fallback: // (optional) MapItem used as a fallback for Unmapped Errors 52 | * } 53 | * 54 | * ... 55 | * 56 | * new ApolloServer({ 57 | * ... 58 | * formatError: new ApolloErrorConverter(options), 59 | * // formatError: new ApolloErrorConverter(), uses all defaults with no Mapped Errors 60 | * // formatError: new ApolloErrorConverter(options, true), enabled debug mode 61 | * }); 62 | */ 63 | function ApolloErrorConverter(options = {}, debug = false) { 64 | const configuration = parseConfigOptions(options); 65 | Object.entries(configuration).forEach((entry) => { 66 | const [option, value] = entry; 67 | this[option] = value; 68 | }); 69 | 70 | this.getMapItem = getMapItem.bind(this); 71 | this.handleMappedError = handleMappedError.bind(this); 72 | this.handleUnmappedError = handleUnmappedError.bind(this); 73 | 74 | function formatError(graphQLError) { 75 | const { originalError } = graphQLError; 76 | 77 | if (shouldErrorPassThrough(debug, originalError)) { 78 | return graphQLError; 79 | } 80 | 81 | // check for a MapItem configured for this Error 82 | const mapItem = this.getMapItem(originalError); 83 | 84 | return mapItem 85 | ? this.handleMappedError(graphQLError, mapItem) 86 | : this.handleUnmappedError(graphQLError); 87 | } 88 | 89 | return formatError.bind(this); 90 | } 91 | 92 | module.exports = ApolloErrorConverter; 93 | -------------------------------------------------------------------------------- /lib/core/constants.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | const defaultLogger = console.error; 3 | 4 | const defaultFallback = { 5 | logger: defaultLogger, 6 | code: "INTERNAL_SERVER_ERROR", 7 | message: "Internal Server Error", 8 | }; 9 | 10 | const requiredKeys = [{ key: "message", types: ["string"] }]; 11 | 12 | const optionalKeys = [ 13 | { key: "code", types: ["string"] }, 14 | { key: "data", types: ["function", "object"] }, 15 | { key: "logger", types: ["function", "boolean"] }, 16 | ]; 17 | 18 | module.exports = { 19 | defaultLogger, 20 | defaultFallback, 21 | mapItemShape: { 22 | requiredKeys, 23 | optionalKeys, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/core/index.js: -------------------------------------------------------------------------------- 1 | const { extendMapItem, mapItemBases } = require("./map-items"); 2 | const ApolloErrorConverter = require("./apollo-error-converter"); 3 | 4 | module.exports = { 5 | mapItemBases, 6 | extendMapItem, 7 | ApolloErrorConverter, 8 | }; 9 | -------------------------------------------------------------------------------- /lib/core/map-items.js: -------------------------------------------------------------------------------- 1 | const { isMapItemValid } = require("../utils/utils"); 2 | const { requiredKeys, optionalKeys } = require("./constants").mapItemShape; 3 | 4 | /** 5 | * Extends a MapItem with different and / or additional properties 6 | * 7 | * @throws {Error} "Invalid MapItem configuration" if the resulting configuration is invalid 8 | * @param {MapItem} baseItem the base Mapitem to extend 9 | * @param {{ message?: string, code?: string, logger?: boolean | function, data?: {} | function }} configuration 10 | */ 11 | const extendMapItem = (baseItem, configuration) => { 12 | const mapItem = { ...baseItem, ...configuration }; 13 | 14 | if (!isMapItemValid(mapItem, requiredKeys, optionalKeys)) { 15 | throw new Error("Invalid MapItem configuration"); 16 | } 17 | 18 | return mapItem; 19 | }; 20 | 21 | const InvalidFields = { 22 | code: "INVALID_FIELDS", 23 | message: "Invalid Field Values", 24 | }; 25 | 26 | const UniqueConstraint = { 27 | code: "UNIQUE_CONSTRAINT", 28 | message: "Unique Constraint Violation", 29 | }; 30 | 31 | module.exports = { 32 | extendMapItem, 33 | mapItemBases: { 34 | InvalidFields, 35 | UniqueConstraint, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /lib/core/tests/apollo-error-converter.core.test.js: -------------------------------------------------------------------------------- 1 | const { ApolloError } = require("apollo-server-core"); 2 | const ApolloErrorConverter = require("../apollo-error-converter"); 3 | 4 | const { handleMappedError, handleUnmappedError } = require("../../utils"); 5 | 6 | jest.mock("../../utils/error-handlers.js"); 7 | 8 | const testDebugAndGraphQLSyntaxBehavior = (options = {}) => { 9 | test("debug mode enabled: passes all received Errors through", () => { 10 | const formatError = new ApolloErrorConverter(options, true); 11 | const errors = [new Error(), new ApolloError()]; 12 | 13 | const outputs = errors.map(error => formatError(error)); 14 | expect(outputs).toEqual(errors); 15 | }); 16 | 17 | test("GraphQL syntax error received: passes the Error through", () => { 18 | const graphQLError = { originalError: {} }; // original Error will be an empty object 19 | const formatError = new ApolloErrorConverter(options); 20 | expect(formatError(graphQLError)).toBe(graphQLError); 21 | }); 22 | }; 23 | 24 | describe("Apollo Error Converter: core export", () => { 25 | describe("default behavior (empty options)", () => { 26 | const formatError = new ApolloErrorConverter(); 27 | 28 | testDebugAndGraphQLSyntaxBehavior(); 29 | 30 | test("original Error is an ApolloError instance: passes the Error through", () => { 31 | const graphQLError = { originalError: new ApolloError() }; 32 | 33 | const output = formatError(graphQLError); 34 | expect(output).toBe(graphQLError); 35 | }); 36 | 37 | test("original Error is a non-Apollo Error: processes and converts as an unmapped Error", () => { 38 | const error = new Error(); 39 | const graphQLError = { originalError: error }; 40 | 41 | formatError(graphQLError); 42 | expect(handleUnmappedError).toHaveBeenCalledWith(graphQLError); 43 | }); 44 | }); 45 | 46 | describe("behavior with options.errorMap defined", () => { 47 | const mappedError = new Error("an error"); 48 | mappedError.name = "MappedError"; 49 | 50 | const errorMap = { 51 | [mappedError.name]: { 52 | message: "mapped message", 53 | logger: true, 54 | }, 55 | }; 56 | 57 | const options = { errorMap }; 58 | const formatError = new ApolloErrorConverter(options); 59 | 60 | testDebugAndGraphQLSyntaxBehavior(options); 61 | 62 | test("original Error is an ApolloError instance: passes the Error through", () => { 63 | const graphQLError = { originalError: new ApolloError() }; 64 | 65 | const output = formatError(graphQLError); 66 | expect(output).toBe(graphQLError); 67 | }); 68 | 69 | test("original Error has no mapping in the ErrorMap: processes and converts the unmapped Error", () => { 70 | const error = new Error(); 71 | const graphQLError = { originalError: error }; 72 | 73 | formatError(graphQLError); 74 | expect(handleUnmappedError).toHaveBeenCalledWith(graphQLError); 75 | }); 76 | 77 | test("original Error has a mapping in the ErrorMap: processes and converts the mapped Error", () => { 78 | const graphQLError = { originalError: mappedError }; 79 | 80 | formatError(graphQLError); 81 | expect(handleMappedError).toHaveBeenCalledWith( 82 | graphQLError, 83 | errorMap[mappedError.name], 84 | ); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /lib/core/tests/extend-map-item.core.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | extendMapItem, 3 | mapItemBases: { InvalidFields }, 4 | } = require("../map-items"); 5 | 6 | describe("extendMapItem(): core utility for extending MapItem objects", () => { 7 | test("returns a new MapItem extended from the base MapItem with new configuration", () => { 8 | const configuration = { data: () => {} }; 9 | 10 | const output = extendMapItem(InvalidFields, configuration); 11 | expect(InvalidFields.data).not.toBeDefined(); // ensure no mutation of original MapItem 12 | expect(output.data).toBeDefined(); 13 | }); 14 | 15 | test("output configuration is an invalid MapItem: throws Error", () => { 16 | expect(() => extendMapItem(InvalidFields, { message: 1234 })).toThrow(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /lib/utils/create-apollo-error.js: -------------------------------------------------------------------------------- 1 | const { toApolloError } = require("apollo-server-core"); 2 | const { defaultFallback } = require("../core/constants"); 3 | 4 | /** 5 | * Extracts and shapes the arguments for formatting the Error 6 | * 7 | * defaults: 8 | * - data = null 9 | * - code = 'INTERNAL_SERVER_ERROR' 10 | * @param {GraphQLError} error GraphQL Error from formatError 11 | * @param {MapItem} mapItem MapItem to parse 12 | * @returns {[object]} arguments list 13 | */ 14 | function createApolloError(graphQLError, mapItem) { 15 | const { originalError, path, locations } = graphQLError; 16 | const { message, data = {}, code = defaultFallback.code } = mapItem; 17 | 18 | return toApolloError( 19 | { 20 | path, 21 | message, 22 | locations, 23 | extensions: { 24 | data: typeof data === "function" ? data(originalError) : data, 25 | }, 26 | }, 27 | code, 28 | ); 29 | } 30 | 31 | module.exports = createApolloError; 32 | -------------------------------------------------------------------------------- /lib/utils/error-handlers.js: -------------------------------------------------------------------------------- 1 | const createApolloError = require("./create-apollo-error"); 2 | 3 | function handleMappedError(graphQLError, mapItem) { 4 | const { logger } = mapItem; 5 | const { originalError } = graphQLError; 6 | 7 | // logger may be [undefined, true, false, function] 8 | if (logger) { 9 | // if it is a function a specific logger has been chosen for this MapItem, otherwise use AEC configured logger 10 | const errorLogger = typeof logger === "function" ? logger : this.logger; 11 | 12 | errorLogger(originalError); 13 | } 14 | 15 | return createApolloError(graphQLError, mapItem); 16 | } 17 | 18 | function handleUnmappedError(graphQLError) { 19 | const { originalError } = graphQLError; 20 | 21 | if (this.shouldLog) { 22 | this.logger(originalError); 23 | } 24 | 25 | return createApolloError(graphQLError, this.fallback); 26 | } 27 | 28 | module.exports = { 29 | handleMappedError, 30 | handleUnmappedError, 31 | }; 32 | -------------------------------------------------------------------------------- /lib/utils/get-map-item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves a MapItem for converting the Error 3 | * - the MapItem can be associated by error.[name, code, type] 4 | * @param {Error} originalError original Error object 5 | */ 6 | function getMapItem(originalError) { 7 | const { name, code, type } = originalError; 8 | return ( 9 | this.errorMap[name] || this.errorMap[code] || this.errorMap[type] || null 10 | ); 11 | } 12 | 13 | module.exports = getMapItem; 14 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | const getMapItem = require("./get-map-item"); 2 | const parseConfigOptions = require("./parse-config-options"); 3 | const shouldErrorPassThrough = require("./should-error-pass-through"); 4 | const { handleMappedError, handleUnmappedError } = require("./error-handlers"); 5 | 6 | module.exports = { 7 | getMapItem, 8 | handleMappedError, 9 | handleUnmappedError, 10 | parseConfigOptions, 11 | shouldErrorPassThrough, 12 | }; 13 | -------------------------------------------------------------------------------- /lib/utils/lodash-is-plain-object/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright JS Foundation and other contributors 4 | 5 | Based on Underscore.js, copyright Jeremy Ashkenas, 6 | DocumentCloud and Investigative Reporters & Editors 7 | 8 | This software consists of voluntary contributions made by many 9 | individuals. For exact contribution history, see the revision history 10 | available at https://github.com/lodash/lodash 11 | 12 | The following license applies to all parts of this software except as 13 | documented below: 14 | 15 | ==== 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining 18 | a copy of this software and associated documentation files (the 19 | "Software"), to deal in the Software without restriction, including 20 | without limitation the rights to use, copy, modify, merge, publish, 21 | distribute, sublicense, and/or sell copies of the Software, and to 22 | permit persons to whom the Software is furnished to do so, subject to 23 | the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be 26 | included in all copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 29 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 30 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 31 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 32 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 33 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 34 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 35 | 36 | ==== 37 | 38 | Copyright and related rights for sample code are waived via CC0. Sample 39 | code is defined as all source code displayed within the prose of the 40 | documentation. 41 | 42 | CC0: http://creativecommons.org/publicdomain/zero/1.0/ 43 | 44 | ==== 45 | 46 | Files located in the node_modules and vendor directories are externally 47 | maintained libraries used by this software which have their own 48 | licenses; we recommend you read them, as their terms may differ from the 49 | terms above. -------------------------------------------------------------------------------- /lib/utils/lodash-is-plain-object/_Symbol.js: -------------------------------------------------------------------------------- 1 | var root = require('./_root'); 2 | 3 | /** Built-in value references. */ 4 | var Symbol = root.Symbol; 5 | 6 | module.exports = Symbol; 7 | -------------------------------------------------------------------------------- /lib/utils/lodash-is-plain-object/_baseGetTag.js: -------------------------------------------------------------------------------- 1 | var Symbol = require('./_Symbol'), 2 | getRawTag = require('./_getRawTag'), 3 | objectToString = require('./_objectToString'); 4 | 5 | /** `Object#toString` result references. */ 6 | var nullTag = '[object Null]', 7 | undefinedTag = '[object Undefined]'; 8 | 9 | /** Built-in value references. */ 10 | var symToStringTag = Symbol ? Symbol.toStringTag : undefined; 11 | 12 | /** 13 | * The base implementation of `getTag` without fallbacks for buggy environments. 14 | * 15 | * @private 16 | * @param {*} value The value to query. 17 | * @returns {string} Returns the `toStringTag`. 18 | */ 19 | function baseGetTag(value) { 20 | if (value == null) { 21 | return value === undefined ? undefinedTag : nullTag; 22 | } 23 | return (symToStringTag && symToStringTag in Object(value)) 24 | ? getRawTag(value) 25 | : objectToString(value); 26 | } 27 | 28 | module.exports = baseGetTag; 29 | -------------------------------------------------------------------------------- /lib/utils/lodash-is-plain-object/_freeGlobal.js: -------------------------------------------------------------------------------- 1 | /** Detect free variable `global` from Node.js. */ 2 | var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; 3 | 4 | module.exports = freeGlobal; 5 | -------------------------------------------------------------------------------- /lib/utils/lodash-is-plain-object/_getPrototype.js: -------------------------------------------------------------------------------- 1 | var overArg = require('./_overArg'); 2 | 3 | /** Built-in value references. */ 4 | var getPrototype = overArg(Object.getPrototypeOf, Object); 5 | 6 | module.exports = getPrototype; 7 | -------------------------------------------------------------------------------- /lib/utils/lodash-is-plain-object/_getRawTag.js: -------------------------------------------------------------------------------- 1 | var Symbol = require('./_Symbol'); 2 | 3 | /** Used for built-in method references. */ 4 | var objectProto = Object.prototype; 5 | 6 | /** Used to check objects for own properties. */ 7 | var hasOwnProperty = objectProto.hasOwnProperty; 8 | 9 | /** 10 | * Used to resolve the 11 | * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) 12 | * of values. 13 | */ 14 | var nativeObjectToString = objectProto.toString; 15 | 16 | /** Built-in value references. */ 17 | var symToStringTag = Symbol ? Symbol.toStringTag : undefined; 18 | 19 | /** 20 | * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. 21 | * 22 | * @private 23 | * @param {*} value The value to query. 24 | * @returns {string} Returns the raw `toStringTag`. 25 | */ 26 | function getRawTag(value) { 27 | var isOwn = hasOwnProperty.call(value, symToStringTag), 28 | tag = value[symToStringTag]; 29 | 30 | try { 31 | value[symToStringTag] = undefined; 32 | var unmasked = true; 33 | } catch (e) {} 34 | 35 | var result = nativeObjectToString.call(value); 36 | if (unmasked) { 37 | if (isOwn) { 38 | value[symToStringTag] = tag; 39 | } else { 40 | delete value[symToStringTag]; 41 | } 42 | } 43 | return result; 44 | } 45 | 46 | module.exports = getRawTag; 47 | -------------------------------------------------------------------------------- /lib/utils/lodash-is-plain-object/_objectToString.js: -------------------------------------------------------------------------------- 1 | /** Used for built-in method references. */ 2 | var objectProto = Object.prototype; 3 | 4 | /** 5 | * Used to resolve the 6 | * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) 7 | * of values. 8 | */ 9 | var nativeObjectToString = objectProto.toString; 10 | 11 | /** 12 | * Converts `value` to a string using `Object.prototype.toString`. 13 | * 14 | * @private 15 | * @param {*} value The value to convert. 16 | * @returns {string} Returns the converted string. 17 | */ 18 | function objectToString(value) { 19 | return nativeObjectToString.call(value); 20 | } 21 | 22 | module.exports = objectToString; 23 | -------------------------------------------------------------------------------- /lib/utils/lodash-is-plain-object/_overArg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a unary function that invokes `func` with its argument transformed. 3 | * 4 | * @private 5 | * @param {Function} func The function to wrap. 6 | * @param {Function} transform The argument transform. 7 | * @returns {Function} Returns the new function. 8 | */ 9 | function overArg(func, transform) { 10 | return function(arg) { 11 | return func(transform(arg)); 12 | }; 13 | } 14 | 15 | module.exports = overArg; 16 | -------------------------------------------------------------------------------- /lib/utils/lodash-is-plain-object/_root.js: -------------------------------------------------------------------------------- 1 | var freeGlobal = require('./_freeGlobal'); 2 | 3 | /** Detect free variable `self`. */ 4 | var freeSelf = typeof self == 'object' && self && self.Object === Object && self; 5 | 6 | /** Used as a reference to the global object. */ 7 | var root = freeGlobal || freeSelf || Function('return this')(); 8 | 9 | module.exports = root; 10 | -------------------------------------------------------------------------------- /lib/utils/lodash-is-plain-object/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lodash (Custom Build) 3 | * Build: `lodash category="lang" plus="isPlainObject" exports="node" modularize -p` 4 | * Copyright JS Foundation and other contributors 5 | * Released under MIT license 6 | * Based on Underscore.js 1.8.3 7 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 8 | */ 9 | var baseGetTag = require('./_baseGetTag'), 10 | getPrototype = require('./_getPrototype'), 11 | isObjectLike = require('./isObjectLike'); 12 | 13 | /** `Object#toString` result references. */ 14 | var objectTag = '[object Object]'; 15 | 16 | /** Used for built-in method references. */ 17 | var funcProto = Function.prototype, 18 | objectProto = Object.prototype; 19 | 20 | /** Used to resolve the decompiled source of functions. */ 21 | var funcToString = funcProto.toString; 22 | 23 | /** Used to check objects for own properties. */ 24 | var hasOwnProperty = objectProto.hasOwnProperty; 25 | 26 | /** Used to infer the `Object` constructor. */ 27 | var objectCtorString = funcToString.call(Object); 28 | 29 | /** 30 | * Checks if `value` is a plain object, that is, an object created by the 31 | * `Object` constructor or one with a `[[Prototype]]` of `null`. 32 | * 33 | * @static 34 | * @memberOf _ 35 | * @since 0.8.0 36 | * @category Lang 37 | * @param {*} value The value to check. 38 | * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. 39 | * @example 40 | * 41 | * function Foo() { 42 | * this.a = 1; 43 | * } 44 | * 45 | * _.isPlainObject(new Foo); 46 | * // => false 47 | * 48 | * _.isPlainObject([1, 2, 3]); 49 | * // => false 50 | * 51 | * _.isPlainObject({ 'x': 0, 'y': 0 }); 52 | * // => true 53 | * 54 | * _.isPlainObject(Object.create(null)); 55 | * // => true 56 | */ 57 | function isPlainObject(value) { 58 | if (!isObjectLike(value) || baseGetTag(value) != objectTag) { 59 | return false; 60 | } 61 | var proto = getPrototype(value); 62 | if (proto === null) { 63 | return true; 64 | } 65 | var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor; 66 | return typeof Ctor == 'function' && Ctor instanceof Ctor && 67 | funcToString.call(Ctor) == objectCtorString; 68 | } 69 | 70 | module.exports = isPlainObject; 71 | -------------------------------------------------------------------------------- /lib/utils/lodash-is-plain-object/isObjectLike.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lodash (Custom Build) 3 | * Build: `lodash category="lang" plus="isPlainObject" exports="node" modularize -p` 4 | * Copyright JS Foundation and other contributors 5 | * Released under MIT license 6 | * Based on Underscore.js 1.8.3 7 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 8 | */ 9 | 10 | /** 11 | * Checks if `value` is object-like. A value is object-like if it's not `null` 12 | * and has a `typeof` result of "object". 13 | * 14 | * @static 15 | * @memberOf _ 16 | * @since 4.0.0 17 | * @category Lang 18 | * @param {*} value The value to check. 19 | * @returns {boolean} Returns `true` if `value` is object-like, else `false`. 20 | * @example 21 | * 22 | * _.isObjectLike({}); 23 | * // => true 24 | * 25 | * _.isObjectLike([1, 2, 3]); 26 | * // => true 27 | * 28 | * _.isObjectLike(_.noop); 29 | * // => false 30 | * 31 | * _.isObjectLike(null); 32 | * // => false 33 | */ 34 | function isObjectLike(value) { 35 | return value != null && typeof value == 'object'; 36 | } 37 | 38 | module.exports = isObjectLike; 39 | -------------------------------------------------------------------------------- /lib/utils/lodash-is-plain-object/stubArray.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This method returns a new empty array. 3 | * 4 | * @static 5 | * @memberOf _ 6 | * @since 4.13.0 7 | * @category Util 8 | * @returns {Array} Returns the new empty array. 9 | * @example 10 | * 11 | * var arrays = _.times(2, _.stubArray); 12 | * 13 | * console.log(arrays); 14 | * // => [[], []] 15 | * 16 | * console.log(arrays[0] === arrays[1]); 17 | * // => false 18 | */ 19 | function stubArray() { 20 | return []; 21 | } 22 | 23 | module.exports = stubArray; 24 | -------------------------------------------------------------------------------- /lib/utils/parse-config-options.js: -------------------------------------------------------------------------------- 1 | const { 2 | isOneOfTypes, 3 | mergeErrorMaps, 4 | fallbackIsValid, 5 | validateErrorMap, 6 | } = require("./utils"); 7 | 8 | const { 9 | defaultLogger, 10 | defaultFallback, 11 | mapItemShape: { requiredKeys, optionalKeys }, 12 | } = require("../core/constants"); 13 | 14 | /** 15 | * Parses configuration options for ApolloErrorConverter construction 16 | * - all are optional and use defaults for simple construction 17 | * @param {ConfigOptions} options 18 | */ 19 | const parseConfigOptions = (options) => { 20 | /* eslint no-console: 0 */ 21 | const { logger, fallback, errorMap } = options; 22 | 23 | const config = { 24 | errorMap: {}, 25 | shouldLog: true, 26 | logger: defaultLogger, 27 | fallback: defaultFallback, 28 | }; 29 | 30 | if (logger === false) { 31 | config.shouldLog = false; 32 | } else if (typeof logger === "function") { 33 | config.logger = logger; 34 | } else if (!isOneOfTypes(logger, ["undefined", "boolean", "function"])) { 35 | console.warn( 36 | "[Apollo Error Converter] invalid logger option, using default logger", 37 | ); 38 | } 39 | 40 | if (fallback) { 41 | if (fallbackIsValid(fallback)) { 42 | config.fallback = fallback; 43 | } else { 44 | console.warn( 45 | "[Apollo Error Converter] invalid fallback option, using default fallback", 46 | ); 47 | } 48 | } 49 | 50 | if (errorMap) { 51 | const mergedMap = mergeErrorMaps(errorMap); 52 | config.errorMap = validateErrorMap(mergedMap, requiredKeys, optionalKeys); 53 | } 54 | 55 | return config; 56 | }; 57 | 58 | module.exports = parseConfigOptions; 59 | -------------------------------------------------------------------------------- /lib/utils/should-error-pass-through.js: -------------------------------------------------------------------------------- 1 | const isPlainObject = require("./lodash-is-plain-object"); 2 | 3 | // TODO: discuss whether GraphQL syntax / schema errors should be passed through, logged, ignored? 4 | 5 | /** 6 | * Controls whether an Error should be passed through without being processed by AEC 7 | * 8 | * conditions to pass through: 9 | * - debug mode enabled 10 | * - original Error is an ApolloError instance (custom handled in resolver) 11 | * - [DISCUSS] GraphQL syntax or schema error (empty originalError) 12 | * @param {boolean} debug debug mode flag 13 | * @param {Error} originalError original Error object 14 | */ 15 | const shouldErrorPassThrough = (debug, originalError) => debug 16 | || originalError.extensions !== undefined // ApolloErrors set this property 17 | // TODO: confirm, does this handle all GraphQL syntax / schema errors? 18 | || (isPlainObject(originalError) && Object.keys(originalError).length === 0); 19 | 20 | module.exports = shouldErrorPassThrough; 21 | -------------------------------------------------------------------------------- /lib/utils/tests/__mocks__/index.js: -------------------------------------------------------------------------------- 1 | const { mapItemBases, extendMapItem } = require("../../../core/map-items"); 2 | 3 | // dummy implementation 4 | const shapeFieldErrors = () => ({ fields: {} }); 5 | 6 | const InvalidFields = extendMapItem(mapItemBases.InvalidFields, { 7 | data: shapeFieldErrors, 8 | }); 9 | 10 | const UniqueConstraint = extendMapItem(mapItemBases.UniqueConstraint, { 11 | data: shapeFieldErrors, 12 | logger: true, 13 | }); 14 | 15 | const errorMap1 = { 16 | ValidatorError: InvalidFields, 17 | ValidationError: UniqueConstraint, 18 | }; 19 | const errorMap2 = { 20 | SequelizeValidationError: InvalidFields, 21 | SequelizeUniqueConstraintError: UniqueConstraint, 22 | }; 23 | 24 | const errorMapsArray = [errorMap1, errorMap2]; 25 | 26 | const errorMap = { 27 | ...errorMap1, 28 | ...errorMap2, 29 | }; 30 | 31 | module.exports = { 32 | errorMap, 33 | errorMapsArray, 34 | }; 35 | -------------------------------------------------------------------------------- /lib/utils/tests/create-apollo-error.util.test.js: -------------------------------------------------------------------------------- 1 | const { defaultFallback } = require("../../core/constants"); 2 | const createApolloError = require("../create-apollo-error"); 3 | 4 | const path = ["a", "path"]; 5 | const locations = [{ line: 1, column: 1 }]; 6 | const originalError = { 7 | stack: [], 8 | message: "something has goofed tremendously", 9 | }; 10 | 11 | const graphQLError = { 12 | path, 13 | locations, 14 | originalError, 15 | }; 16 | 17 | const mapItem = { 18 | code: "TREMENDOUS_GOOFERY", 19 | data: { some: "extra data" }, 20 | message: "those responsible for the sacking have been sacked", 21 | }; 22 | 23 | describe("createApolloError: converts the original error to an Apollo Error", () => { 24 | let result; 25 | beforeAll(() => { 26 | result = createApolloError(graphQLError, mapItem); 27 | }); 28 | 29 | test("converted error includes the GraphQL path and location", () => { 30 | ["path", "location"].forEach(property => 31 | expect(result[property]).toBe(graphQLError[property]), 32 | ); 33 | }); 34 | 35 | test("converted error has a top level message property from the mapItem", () => 36 | expect(result.message).toBe(mapItem.message)); 37 | 38 | test("converted error includes extensions object, { code, data }, from the mapItem", () => { 39 | expect(result.extensions).toBeDefined(); 40 | expect(result.extensions.data).toBe(mapItem.data); 41 | expect(result.extensions.code).toBe(mapItem.code); 42 | }); 43 | 44 | test(`mapItem.code is not provided: uses default fallback code [${defaultFallback.code}]`, () => { 45 | const noCodeResult = createApolloError(graphQLError, { 46 | message: "no code", 47 | }); 48 | expect(noCodeResult.extensions.code).toBe(defaultFallback.code); 49 | }); 50 | 51 | describe("variants on mapItem.data", () => { 52 | test("data is undefined: extensions.data is an empty object", () => { 53 | const { message, code } = mapItem; 54 | 55 | const noDataResult = createApolloError(graphQLError, { message, code }); 56 | expect(noDataResult.extensions.data).toBeDefined(); 57 | expect(Object.keys(noDataResult.extensions.data).length).toBe(0); 58 | }); 59 | 60 | test("data is a function: extensions.data is the result of executing the function providing it the original error", () => { 61 | const expectedData = "some function output"; 62 | const dataFunction = jest.fn(() => expectedData); 63 | 64 | const dataFunctionResult = createApolloError(graphQLError, { 65 | ...mapItem, 66 | data: dataFunction, 67 | }); 68 | expect(dataFunction).toHaveBeenCalledWith(graphQLError.originalError); 69 | expect(dataFunctionResult.extensions.data).toBe(expectedData); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /lib/utils/tests/error-handlers.util.test.js: -------------------------------------------------------------------------------- 1 | const handlers = require("../error-handlers"); 2 | const createApolloError = require("../create-apollo-error"); 3 | 4 | const { mapItemBases } = require("../../core/map-items"); 5 | const { extendMapItem } = require("../../core/map-items"); 6 | const { defaultFallback } = require("../../core/constants"); 7 | 8 | jest.mock("../create-apollo-error.js"); 9 | 10 | const optionsLogger = jest 11 | // enable spying on default logger 12 | .spyOn(defaultFallback, "logger") 13 | // suppress actual logging 14 | .mockImplementation(() => {}); 15 | 16 | const defaultConfig = { 17 | shouldLog: true, 18 | logger: optionsLogger, 19 | fallback: defaultFallback, 20 | }; 21 | 22 | describe("handleUnmappedError", () => { 23 | const loggingOffConfig = { 24 | shouldLog: false, 25 | fallback: defaultFallback, 26 | }; 27 | const error = new Error("a message"); 28 | const graphQLError = { originalError: error }; 29 | 30 | afterEach(() => jest.clearAllMocks()); 31 | 32 | test("returns ApolloError converted with fallback MapItem configuration", () => { 33 | handlers.handleUnmappedError.call(defaultConfig, graphQLError); 34 | 35 | expect(createApolloError).toHaveBeenCalledWith( 36 | graphQLError, 37 | defaultConfig.fallback, 38 | ); 39 | }); 40 | 41 | test("AEC options.logger is false: does not log", () => { 42 | handlers.handleUnmappedError.call(loggingOffConfig, graphQLError); 43 | 44 | expect(optionsLogger).not.toHaveBeenCalled(); 45 | }); 46 | 47 | test("AEC options.logger is configured: logs the original error", () => { 48 | handlers.handleUnmappedError.call(defaultConfig, graphQLError); 49 | 50 | expect(optionsLogger).toHaveBeenCalledWith(graphQLError.originalError); 51 | }); 52 | }); 53 | 54 | describe("handleMappedError", () => { 55 | const mappedError = new Error("original message"); 56 | const graphQLError = { originalError: mappedError }; 57 | const customLogger = jest.fn(); 58 | const mapItem = extendMapItem(mapItemBases.InvalidFields, { 59 | logger: customLogger, 60 | }); 61 | 62 | afterEach(() => jest.clearAllMocks()); 63 | 64 | test("returns ApolloError converted with mapped MapItem configuration", () => { 65 | handlers.handleMappedError.call(defaultConfig, graphQLError, mapItem); 66 | expect(createApolloError).toHaveBeenCalledWith(graphQLError, mapItem); 67 | }); 68 | 69 | test("mapItem.logger is false: does not log", () => { 70 | handlers.handleMappedError.call(defaultConfig, graphQLError, { 71 | ...mapItem, 72 | logger: false, 73 | }); 74 | expect(optionsLogger).not.toHaveBeenCalled(); 75 | expect(customLogger).not.toHaveBeenCalled(); 76 | }); 77 | 78 | test("mapItem.logger is undefined: does not log", () => { 79 | handlers.handleMappedError.call(defaultConfig, graphQLError, { 80 | ...mapItem, 81 | logger: false, 82 | }); 83 | expect(optionsLogger).not.toHaveBeenCalled(); 84 | expect(customLogger).not.toHaveBeenCalled(); 85 | }); 86 | 87 | test("mapItem.logger is true: uses AEC options.logger to log original Error", () => { 88 | handlers.handleMappedError.call(defaultConfig, graphQLError, { 89 | ...mapItem, 90 | logger: true, 91 | }); 92 | expect(optionsLogger).toHaveBeenCalledWith(graphQLError.originalError); 93 | }); 94 | 95 | test("mapItem.logger is a function: uses MapItem logger to log original Error", () => { 96 | handlers.handleMappedError.call(defaultConfig, graphQLError, mapItem); 97 | expect(customLogger).toHaveBeenCalledWith(graphQLError.originalError); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /lib/utils/tests/get-map-item.util.test.js: -------------------------------------------------------------------------------- 1 | const getMapItem = require("../get-map-item"); 2 | 3 | describe("getMapItem(): looks up a MapItem by the original Error name, code or type property", () => { 4 | const errorNameMapItem = "mapping by Error.name"; 5 | const errorCodeMapItem = "mapping by Error.code"; 6 | const errorTypeMapItem = "mapping by Error.type"; 7 | 8 | const errorMap = { 9 | errorMap: { 10 | ErrorName: errorNameMapItem, 11 | ErrorCode: errorCodeMapItem, 12 | ErrorType: errorTypeMapItem, 13 | }, 14 | }; 15 | 16 | test("Error.name used for MapItem configuration: returns the MapItem", () => { 17 | const errorWithName = new Error(); 18 | errorWithName.name = "ErrorName"; 19 | 20 | const output = getMapItem.call(errorMap, errorWithName); 21 | expect(output).toBe(errorNameMapItem); 22 | }); 23 | 24 | test("Error.code used for MapItem configuration: returns the MapItem", () => { 25 | const errorWithCode = new Error(); 26 | errorWithCode.code = "ErrorCode"; 27 | 28 | const output = getMapItem.call(errorMap, errorWithCode); 29 | expect(output).toBe(errorCodeMapItem); 30 | }); 31 | 32 | test("Error.type used for MapItem configuration: returns the MapItem", () => { 33 | const errorWithType = new Error(); 34 | errorWithType.type = "ErrorType"; 35 | 36 | const output = getMapItem.call(errorMap, errorWithType); 37 | expect(output).toBe(errorTypeMapItem); 38 | }); 39 | 40 | test("No mapping found by error.[name, code, type]: returns null", () => { 41 | const noMatchError = new Error(); 42 | 43 | const output = getMapItem.call(errorMap, noMatchError); 44 | expect(output).toBeNull(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /lib/utils/tests/parse-config-options.util.test.js: -------------------------------------------------------------------------------- 1 | const parseConfigOptions = require("../parse-config-options"); 2 | const { defaultFallback, defaultLogger } = require("../../core/constants"); 3 | 4 | describe("parseConfigOptions: Parses configuration options for ApolloErrorConverter construction", () => { 5 | beforeAll(() => { 6 | global.console.warn = jest.fn(); 7 | }); 8 | afterEach(() => jest.clearAllMocks()); 9 | 10 | describe("config.shouldLog: controls whether logging should occur when converting unmapped Errors", () => { 11 | test("options.logger is false: config.shouldLog is false", () => { 12 | const options = { logger: false }; 13 | const config = parseConfigOptions(options); 14 | expect(config.shouldLog).toBe(false); 15 | }); 16 | 17 | test("options.logger is undefined: config.shouldLog is true [DEFAULT]", () => { 18 | const options = {}; 19 | const config = parseConfigOptions(options); 20 | expect(config.shouldLog).toBe(true); 21 | }); 22 | }); 23 | 24 | describe("config.logger: defines the default logger function used for logging", () => { 25 | test("options.logger is a function reference: config.logger is the function reference", () => { 26 | const logger = () => {}; 27 | const options = { logger }; 28 | const config = parseConfigOptions(options); 29 | expect(config.logger).toBe(logger); 30 | }); 31 | 32 | test("options.logger is undefined: config.logger is the [DEFAULT LOGGER]", () => { 33 | const options = {}; 34 | const config = parseConfigOptions(options); 35 | expect(config.logger).toBe(defaultLogger); 36 | }); 37 | 38 | test("options.logger is invalid: config.logger is the [DEFAULT LOGGER], emits console warning", () => { 39 | const options = { logger: "a string" }; 40 | const config = parseConfigOptions(options); 41 | expect(config.logger).toBe(defaultLogger); 42 | expect(global.console.warn).toHaveBeenCalled(); 43 | }); 44 | }); 45 | 46 | describe("config.fallback: the fallback MapItem used for converting unmapped Errors", () => { 47 | test("options.fallback is undefined: config.fallback is the [DEFAULT FALLBACK]", () => { 48 | const options = {}; 49 | const config = parseConfigOptions(options); 50 | expect(config.fallback).toBe(defaultFallback); 51 | }); 52 | 53 | test("options.fallback is a valid MapItem: config.fallback is the MapItem", () => { 54 | const options = { 55 | fallback: { 56 | message: "", 57 | logger: () => {}, 58 | }, 59 | }; 60 | 61 | const config = parseConfigOptions(options); 62 | expect(config.fallback).toBe(options.fallback); 63 | }); 64 | 65 | test("options.fallback is an invalid MapItem: config.fallback is [DEFAULT FALLBACK], emits console warning", () => { 66 | const options = { fallback: { nonsense: "" } }; 67 | const config = parseConfigOptions(options); 68 | expect(config.fallback).toBe(defaultFallback); 69 | expect(global.console.warn).toHaveBeenCalled(); 70 | }); 71 | }); 72 | 73 | describe("config.errorMap: the ErrorMap used for mapping Errors to MapItems", () => { 74 | const errorMap = { 75 | AMappedError: defaultFallback, 76 | SomeMappedError: { message: "", data: { extra: "error data" } }, 77 | }; 78 | 79 | test("options.errorMap is undefined: config.errorMap is {}", () => { 80 | const options = {}; 81 | const config = parseConfigOptions(options); 82 | const isEmpyObject = Object.keys(config.errorMap).length === 0; 83 | expect(isEmpyObject).toBe(true); 84 | }); 85 | 86 | test("options.errorMap is a valid ErrorMap: config.errorMap is the ErrorMap", () => { 87 | const options = { errorMap }; 88 | const config = parseConfigOptions(options); 89 | expect(config.errorMap).toBe(errorMap); 90 | }); 91 | 92 | test("options.errorMap is an Array of ErrorMap objects: config.errorMap is the merged ErrorMap ", () => { 93 | const otherErrorMap = { OtherMappedError: { message: "another one" } }; 94 | const errorMapArray = [errorMap, otherErrorMap]; 95 | 96 | const expectedMappings = [ 97 | ...Object.keys(errorMap), 98 | ...Object.keys(otherErrorMap), 99 | ]; 100 | 101 | const options = { errorMap: errorMapArray }; 102 | const config = parseConfigOptions(options); 103 | expectedMappings.forEach(mapping => expect(config.errorMap[mapping]).toBeDefined()); 104 | }); 105 | 106 | test("options.errorMap contains invalid MapItem(s): throws Error", () => { 107 | const options = { errorMap: { anInvalidItem: { nonsense: "" } } }; 108 | expect(() => parseConfigOptions(options)).toThrow(); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /lib/utils/tests/should-error-pass-through.util.test.js: -------------------------------------------------------------------------------- 1 | const shouldErrorPassThrough = require("../should-error-pass-through"); 2 | 3 | describe("shouldErrorPassThrough: controls whether an Error should be passed through before being processed by AEC", () => { 4 | test("debug mode enabled: returns true", () => expect(shouldErrorPassThrough(true)).toBe(true)); 5 | 6 | test("original Error is an ApolloError or subclass with an 'extensions' property: returns true", () => { 7 | const originalError = { extensions: { stuff: "in here" } }; 8 | expect(shouldErrorPassThrough(false, originalError)).toBe(true); 9 | }); 10 | 11 | test("GraphQL syntax error (original Error is an empty object): returns true", () => { 12 | const originalError = {}; 13 | expect(shouldErrorPassThrough(false, originalError)).toBe(true); 14 | }); 15 | 16 | test("(debug off) originalError should be processed by AEC: returns false", () => { 17 | const originalError = new Error(); 18 | expect(shouldErrorPassThrough(false, originalError)).toBe(false); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /lib/utils/tests/utils.util.test.js: -------------------------------------------------------------------------------- 1 | const { ApolloError } = require("apollo-server-core"); 2 | 3 | const { 4 | defaultFallback, 5 | mapItemShape: { requiredKeys, optionalKeys }, 6 | } = require("../../core/constants"); 7 | 8 | const { 9 | isOneOfTypes, 10 | isMapItemValid, 11 | mergeErrorMaps, 12 | fallbackIsValid, 13 | validateErrorMap, 14 | validateRequiredMapItemFields, 15 | validateOptionalMapItemFields, 16 | } = require("../utils"); 17 | 18 | // tests will break if either of these shapes change - forces consistent tests 19 | const { errorMap, errorMapsArray } = require("./__mocks__"); 20 | 21 | describe("Error Map utilities", () => { 22 | // will break if mapItemShape changes - forces consistent tests 23 | describe("validateErrorMap: Validates every Map Item entry in a merged Error Map", () => { 24 | test("valid merged Error Map: returns valid Error Map", () => { 25 | const result = validateErrorMap(errorMap, requiredKeys, optionalKeys); 26 | expect(result).toEqual(errorMap); 27 | }); 28 | 29 | test("invalid merged Error Map: throws Invalid Map Item error with item key and value", () => { 30 | const invalidMap = { 31 | ...errorMap, 32 | BadKey: { 33 | message: "a message", 34 | logger: 123, 35 | data: () => {}, 36 | }, 37 | }; 38 | 39 | expect(() => validateErrorMap(invalidMap, requiredKeys, optionalKeys)).toThrow(); 40 | }); 41 | }); 42 | 43 | describe("isMapItemValid: Validates an individual entry in the merged Error Map", () => { 44 | const mapItemBase = { message: "a message", errorConstructor: ApolloError }; 45 | 46 | test("only required fields with correct shapes: returns true", () => { 47 | const result = isMapItemValid(mapItemBase, requiredKeys, optionalKeys); 48 | expect(result).toBe(true); 49 | }); 50 | 51 | test("required and optional fields with correct shapes: returns true", () => { 52 | const mapItem = { ...mapItemBase, logger: true, data: () => {} }; 53 | 54 | const result = isMapItemValid(mapItem, requiredKeys, optionalKeys); 55 | expect(result).toBe(true); 56 | }); 57 | 58 | test("missing required: returns false", () => { 59 | const mapItem = { logger: () => {} }; // missing message 60 | expect(isMapItemValid(mapItem, requiredKeys, optionalKeys)).toBe(false); 61 | }); 62 | 63 | test("invalid shapes: returns false", () => { 64 | const mapItem = { 65 | message: 1234, 66 | logger: true, 67 | }; 68 | expect(isMapItemValid(mapItem, requiredKeys, optionalKeys)).toBe(false); 69 | }); 70 | }); 71 | 72 | describe("mergeErrorMaps: Consumes and merges ErrorMap Object(s)", () => { 73 | test("given an Array of ErrorMap elements: returns merged ErrorMap", () => { 74 | const expectedKeys = Object.keys(errorMap); 75 | 76 | const result = mergeErrorMaps(errorMapsArray); 77 | expect(Object.keys(result)).toEqual(expectedKeys); 78 | }); 79 | 80 | test("given a single pre-merged Errormap: returns the ErrorMap", () => { 81 | const result = mergeErrorMaps(errorMap); 82 | expect(result).toBe(errorMap); 83 | }); 84 | }); 85 | 86 | describe("fallbackIsValid: Ensures the fallback is a valid MapItem configuration", () => { 87 | test("fallback is undefined: returns false", () => { 88 | expect(fallbackIsValid()).toBe(false); 89 | }); 90 | 91 | test("fallback is a valid MapItem: returns true", () => { 92 | expect(fallbackIsValid(defaultFallback)).toBe(true); 93 | }); 94 | 95 | test("fallback is an invalid MapItem: returns false", () => { 96 | // missing message 97 | expect(fallbackIsValid({ logger: () => {} })).toBe(false); 98 | }); 99 | }); 100 | 101 | describe("validateRequiredMapItemFields: Validates the required Map Item fields presence and shape", () => { 102 | // arbitrary 103 | const required = [ 104 | { key: "errorConstructor", types: ["function"] }, 105 | { key: "someKey", types: ["string", "function", "object"] }, 106 | { key: "someOther", types: ["array", "function", "object"] }, 107 | ]; 108 | 109 | test("has fields and correct shape: returns true", () => { 110 | const mapItem = { 111 | errorConstructor: ApolloError, 112 | someKey: () => {}, 113 | someOther: [], 114 | }; 115 | 116 | const result = validateRequiredMapItemFields(mapItem, required); 117 | expect(result).toBe(true); 118 | }); 119 | 120 | test("has fields with incorrect shape: returns false", () => { 121 | const mapItem = { 122 | errorConstructor: ApolloError, 123 | someKey: 5, 124 | someOther: [], 125 | }; 126 | 127 | const result = validateRequiredMapItemFields(mapItem, required); 128 | expect(result).toBe(false); 129 | }); 130 | 131 | test("missing a required field: returns false", () => { 132 | const mapItem = { errorConstructor: ApolloError }; 133 | 134 | const result = validateRequiredMapItemFields(mapItem, required); 135 | expect(result).toBe(false); 136 | }); 137 | }); 138 | 139 | describe("validateOptionalMapItemFields: Validates the optional Map Item field shapes", () => { 140 | // arbitrary 141 | const optional = [ 142 | { key: "aKey", types: ["function", "object", "boolean"] }, 143 | { key: "someStuff", types: ["number", "string", "object"] }, 144 | ]; 145 | 146 | test("no optional fields: returns true", () => { 147 | const mapItem = { requiredField: "correct" }; 148 | 149 | const result = validateOptionalMapItemFields(mapItem, optional); 150 | expect(result).toBe(true); 151 | }); 152 | 153 | test("optional field missing + others have valid shape: returns true", () => { 154 | const mapItem = { requiredField: "correct", someStuff: {} }; 155 | 156 | const result = validateOptionalMapItemFields(mapItem, optional); 157 | expect(result).toBe(true); 158 | }); 159 | 160 | test("all optional fields and shapes correct: returns true", () => { 161 | // expected shape as of v0.0.1 162 | const optionalFields = [ 163 | { key: "logger", types: ["function", "boolean"] }, 164 | { key: "data", types: ["function", "object"] }, 165 | ]; 166 | const mapItem = { 167 | requiredField: "correct", 168 | logger: () => {}, 169 | data: () => {}, 170 | }; 171 | 172 | const result = validateOptionalMapItemFields(mapItem, optionalFields); 173 | expect(result).toBe(true); 174 | }); 175 | 176 | test("optional field has invalid shape: returns false", () => { 177 | const mapItem = { requiredField: "correct", aKey: [] }; 178 | 179 | const result = validateOptionalMapItemFields(mapItem, optional); 180 | expect(result).toBe(false); 181 | }); 182 | }); 183 | 184 | describe("isOneOfTypes: Checks if the value is one of the list of type strings", () => { 185 | describe("of supported types: [string, number, function, boolean, array, object]", () => { 186 | const ofTypes = [ 187 | "string", 188 | "number", 189 | "function", 190 | "boolean", 191 | "array", 192 | "object", 193 | ]; 194 | 195 | const tests = { 196 | string: "a string", 197 | number: 2.1, 198 | function: () => {}, 199 | boolean: true, 200 | array: [], 201 | object: {}, 202 | }; 203 | 204 | Object.entries(tests).forEach(([type, value]) => test(`type: ${type}`, () => expect(isOneOfTypes(value, ofTypes)).toBe(true))); 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /lib/utils/utils.js: -------------------------------------------------------------------------------- 1 | const isPlainObject = require("./lodash-is-plain-object"); 2 | const { 3 | requiredKeys, 4 | optionalKeys, 5 | } = require("../core/constants").mapItemShape; 6 | 7 | // TODO: how to import / share type defs? 8 | /** 9 | * Shapes as of v0.0.1 10 | * @typedef {{ string: MapItem }} ErrorMap 11 | * @typedef {[{ key: string, types: [string] } ]} KeyAndShapeList 12 | * @typedef {{ logger?: boolean | function, fallback?: MapItem, errorMap?: ErrorMap || [ErrorMap] }} ConfigOptions 13 | * @typedef {{ message: string, logger?: boolean | function, data?: {} | function, code?: string }} MapItem 14 | */ 15 | 16 | /** 17 | * Checks if the value is one of the list of type strings 18 | * - handles: [undefined, string, number, boolean, function, array, object] 19 | * - object refers to a plain JSO 20 | * @param {object} value 21 | * @param {string[]} ofTypes list of type strings 22 | */ 23 | const isOneOfTypes = (value, ofTypes) => ofTypes.some((type) => { 24 | if (type === "array") return Array.isArray(value); 25 | if (type === "object") return isPlainObject(value); 26 | /* eslint valid-typeof: "error" */ 27 | return typeof value === type; 28 | }); 29 | 30 | /** 31 | * Validates the required Map Item field presence and shape 32 | * @param {MapItem} mapItem 33 | * @param {KeyAndShapeList} required 34 | */ 35 | const validateRequiredMapItemFields = (mapItem, required) => required.every((fieldShape) => { 36 | const { key, types } = fieldShape; 37 | const mapItemValue = mapItem[key]; 38 | 39 | if (mapItemValue === undefined) return false; 40 | 41 | return isOneOfTypes(mapItemValue, types); 42 | }); 43 | 44 | /** 45 | * Validates the optional Map Item field shapes 46 | * @param {MapItem} mapItem 47 | * @param {KeyAndShapeList} optional 48 | */ 49 | const validateOptionalMapItemFields = (mapItem, optional) => optional.every((fieldShape) => { 50 | const { key, types } = fieldShape; 51 | const mapItemValue = mapItem[key]; 52 | 53 | if (mapItemValue === undefined) return true; 54 | 55 | return isOneOfTypes(mapItemValue, types); 56 | }); 57 | 58 | /** 59 | * Validates an individual entry in the merged Error Map 60 | * - validates each required and optional field + shape 61 | * @param {MapItem} mapItem 62 | * @param {KeyAndShapeList} required list of required keys/shapes 63 | * @param {KeyAndShapeList} optional list of optional keys/shapes 64 | * @returns {boolean} 65 | */ 66 | const isMapItemValid = (mapItem, required, optional) => validateRequiredMapItemFields(mapItem, required) 67 | && validateOptionalMapItemFields(mapItem, optional); 68 | 69 | /** 70 | * Validates every Map Item entry in a merged Error Map 71 | * - validates shape of required and optional fields 72 | * - throws Error at first Map Item that fails validation 73 | * @param {{ string: MapItem }} errorMap merged map of { 'ErrorName': MapItem } entries 74 | * @param {KeyAndShapeList} required list of required MapItem keys/shapes 75 | * @param {KeyAndShapeList} optional list of optional MapItem keys/shapes 76 | * @throws {Error} Invalid Error Map Item - key: [${key}] value: [${value}] 77 | * @returns {ErrorMap} a valid ErrorMap 78 | */ 79 | const validateErrorMap = (errorMap, required, optional) => { 80 | Object.entries(errorMap).forEach((entry) => { 81 | const [errorMapping, mapItem] = entry; 82 | const isValid = isMapItemValid(mapItem, required, optional); 83 | 84 | if (!isValid) { 85 | throw new Error( 86 | `Invalid Error Map Item - item mapping: [${errorMapping}] item value: [${JSON.stringify( 87 | mapItem, 88 | )}]`, 89 | ); 90 | } 91 | }); 92 | return errorMap; 93 | }; 94 | 95 | /** 96 | * Consumes and merges ErrorMap Object(s) 97 | * - from an Array of individual ErrorMaps 98 | * - a single pre-merged ErrorMap 99 | * @param {[ErrorMap] | ErrorMap} errorMaps 100 | * @returns {ErrorMap} Array: merges all ErrorMap elements into one Object 101 | * @returns {ErrorMap} Object: returns the merged Object 102 | */ 103 | const mergeErrorMaps = (errorMaps) => { 104 | if (Array.isArray(errorMaps)) { 105 | return errorMaps.reduce((mergedMap, errorMap) => { 106 | validateErrorMap(errorMap, requiredKeys, optionalKeys); 107 | return Object.assign(mergedMap, errorMap); 108 | }, {}); 109 | } 110 | 111 | return errorMaps; 112 | }; 113 | 114 | /** 115 | * Ensures the fallback is a valid MapItem configuration 116 | * @param {MapItem} fallback 117 | * @returns {boolean} result of validity test 118 | */ 119 | const fallbackIsValid = fallback => Boolean(fallback) && isMapItemValid(fallback, requiredKeys, optionalKeys); 120 | 121 | module.exports = { 122 | isOneOfTypes, 123 | isMapItemValid, 124 | mergeErrorMaps, 125 | fallbackIsValid, 126 | validateErrorMap, 127 | validateRequiredMapItemFields, 128 | validateOptionalMapItemFields, 129 | }; 130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-error-converter", 3 | "version": "1.1.1", 4 | "author": "Vampiire", 5 | "license": "MIT", 6 | "description": "Global Apollo Server Error handling made easy. Remove verbose and repetitive resolver / data source Error handling. Ensures no implementation details are ever leaked while preserving internal Error logging.", 7 | "main": "lib/core/index.js", 8 | "directories": { 9 | "lib": "lib", 10 | "test": "tests" 11 | }, 12 | "scripts": { 13 | "test": "jest", 14 | "test:core": "jest */*.core.test*", 15 | "test:utils": "jest */*.util.test*", 16 | "test:travis": "jest --no-cache --coverage && coveralls < coverage/lcov.info" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/the-vampiire/apollo-error-converter.git" 21 | }, 22 | "keywords": [ 23 | "apollo", 24 | "server", 25 | "error", 26 | "handling", 27 | "simple", 28 | "mapping" 29 | ], 30 | "bugs": { 31 | "url": "https://github.com/the-vampiire/apollo-error-converter/issues" 32 | }, 33 | "jest": { 34 | "collectCoverageFrom": [ 35 | "lib/*/*.js", 36 | "!lib/*/index.js" 37 | ], 38 | "coveragePathIgnorePatterns": [ 39 | "/node_modules/", 40 | "/lib/utils/lodash-is-plain-object/" 41 | ] 42 | }, 43 | "homepage": "https://github.com/the-vampiire/apollo-error-converter#readme", 44 | "dependencies": { 45 | "apollo-server-core": "^2.19.2" 46 | }, 47 | "devDependencies": { 48 | "@types/jest": "^24.0.15", 49 | "coveralls": "^3.0.3", 50 | "eslint": "^5.16.0", 51 | "eslint-config-airbnb-base": "^13.1.0", 52 | "eslint-config-prettier": "^4.3.0", 53 | "eslint-plugin-import": "^2.17.2", 54 | "eslint-plugin-jest": "^22.6.4", 55 | "graphql": "^14.2.0", 56 | "jest": "^26.6.3" 57 | }, 58 | "peerDependencies": { 59 | "graphql": "14.x.x", 60 | "apollo-server-core": "2.x.x" 61 | }, 62 | "eslintConfig": { 63 | "env": { 64 | "es6": true, 65 | "commonjs": true, 66 | "jest/globals": true 67 | }, 68 | "extends": [ 69 | "prettier", 70 | "airbnb-base", 71 | "plugin:jest/recommended" 72 | ], 73 | "plugins": [ 74 | "jest" 75 | ], 76 | "globals": { 77 | "Atomics": "readonly", 78 | "SharedArrayBuffer": "readonly" 79 | }, 80 | "parserOptions": { 81 | "ecmaVersion": 2018 82 | }, 83 | "rules": { 84 | "quotes": [ 85 | "error", 86 | "double" 87 | ], 88 | "max-len": [ 89 | 0, 90 | 100 91 | ] 92 | } 93 | } 94 | } 95 | --------------------------------------------------------------------------------