diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..2622a5c --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,54 @@ +workflows: + version: 2 + node-multi-build: + jobs: + - node-v8 + - node-v10 + - node-v12 + - node-v14 +version: 2 +jobs: + node-base: &node-base + docker: + - image: node + steps: + - checkout + - restore_cache: + keys: + - npm-lock-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }} + - npm-lock-master-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }} + - npm-cache-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }} + - npm-cache-master-{{ .Environment.CIRCLE_JOB }} + - run: + name: Install dependencies + command: npm install + - run: + name: Build + command: npm run build + - run: + name: Test + command: npm run test + - save_cache: + key: npm-lock-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }} + paths: + - node_modules + - save_cache: + key: npm-cache-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }} + paths: + - ~/.npm/_cacache + node-v8: + <<: *node-base + docker: + - image: circleci/node:8 + node-v10: + <<: *node-base + docker: + - image: circleci/node:10 + node-v12: + <<: *node-base + docker: + - image: circleci/node:12 + node-v14: + <<: *node-base + docker: + - image: circleci/node:14 diff --git a/.gitignore b/.gitignore index 9aeb862..9c6fc03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,12 @@ +# intellij idea +.idea + + # Logs logs *.log npm-debug.log* +package-lock.json # Runtime data pids @@ -38,3 +43,6 @@ jspm_packages # Yarn yarn.lock + +# Direnv +.envrc diff --git a/CHANGELOG.md b/CHANGELOG.md index bfcdc58..636b0cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,65 @@ +1.9.0 / 2018-04-17 +================== + + * Feature: Added `internalData` property for non-serialized context data (@n1ru4l, #41) + +1.8.0 / 2018-04-10 +================== + + * Fix: Wrong and missing data being returned in errors (@thebigredgeek) + * Fix: Reverted breaking change regarding spec compliance (@thebigredgeek) + +1.7.1 / 2018-02-22 +================== + + * Fix: less ambiguity around data type (@thebigredgeek) + +1.7.0 / 2018-02-22 +================== + + * Misc: Refactored in TypeScript (#19, #22, @iamdanthdev, @lakhansamani) + * Fix: Fix createError interface post-typescript refactor (@thebigredgeek) + * Fix: Fix showPath option to correctly toggle error path exposure regardless of showLocation state (@thebigredgeek) + +1.5.1 / 2017-09-07 +================== + + * Package: Added new keywords (@thebigredgeek) + * Docs: Updated docs to reference [apollo-server](https://dev.apollodata.com/tools/apollo-server/setup.html) (@thebigredgeek) + +1.5.0 / 2017-08-29 +================== + + * Fix: fixed syntax error for createError config parameter (@thebigredgeek) + +1.4.0 / 2017-03-14 +================== + * Feature: (BREAKING) Rely on error.originalError and fail back to error for formatError rather than serializing body into message + * Docs: Add docs for `isInstance` + +1.3.0 / 2017-02-01 +================== + * Docs: Update README.md (@thebigredgeek) + * Docs: Add Beerpay's badge (@thebigredgeek) + * Docs: Added isInstance method + +1.2.1 / 2016-12-07 +================== + * Fix: bug with overriding message serialization (@thebigredgeek) + 1.2.0 / 2016-12-07 ================== - * Added option to override message when throwing error (@thebigredgeek) + * Feature: Added option to override message when throwing error (@thebigredgeek) 1.1.0 / 2016-12-01 ================== - * Added option to show original locations and paths in error (#1, @scf4) + * Feature: Added option to show original locations and paths in error (#1, @scf4) 1.0.2 / 2016-11-11 ================== - * Fix for data serialization (@thebigredgeek) + * Fix: data serialization (@thebigredgeek) 1.0.1 / 2016-11-10 ================== - * Initial release (@thebigredgeek) diff --git a/Makefile b/Makefile index bf4762d..8b3638a 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ endif .FORCE: all: clean - babel src -d dist --source-maps + tsc clean: .FORCE rimraf npm-debug.log dist diff --git a/README.md b/README.md index 7ef6ac1..2446d07 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ Machine-readable custom errors for Apollostack's GraphQL server [![CircleCI](https://circleci.com/gh/thebigredgeek/apollo-errors.svg?style=shield)](https://circleci.com/gh/thebigredgeek/apollo-errors/tree/master) +## Example from Apollo Day + +[![Authentication and Error Handling in GraphQL](https://img.youtube.com/vi/xaorvBjCE7A/0.jpg)](https://www.youtube.com/watch?v=xaorvBjCE7A) ## Installation and usage @@ -30,6 +33,7 @@ Hook up formatting: import express from 'express'; import bodyParser from 'body-parser'; import { formatError } from 'apollo-errors'; +import { graphqlExpress } from 'apollo-server-express'; import schema from './schema'; const app = express(); @@ -54,6 +58,9 @@ const resolverThatThrowsError = (root, params, context) => { throw new FooError({ data: { something: 'important' + }, + internalData: { + error: `The SQL server died.` } }); } @@ -79,18 +86,39 @@ Witness glorious simplicity: } ``` +The `internalData` property is meant for data you want to store on the error object (e.g. for logging), but not send out to your end users. +You can utilize this data for logging purposes. + +```js +import { isInstance as isApolloErrorInstance, formatError as formatApolloError } from 'apollo-errors'; + +function formatError(error) { + const { originalError } = error; + if (isApolloErrorInstance(originalError)) { + // log internalData to stdout but not include it in the formattedError + console.log(JSON.stringify({ + type: `error`, + data: originalError.data, + internalData: originalError.internalData + })); + } + return formatApolloError(error) +} + +``` + ## API -### ApolloError ({ [time_thrown: String, data: Object, message: String ]}) +### ApolloError ({ [time_thrown: String, data: Object, internalData: object, message: String ]}) Creates a new ApolloError object. Note that `ApolloError` in this context refers to an error class created and returned by `createError` documented below. Error can be -initialized with a custom `time_thrown` ISODate (default is current ISODate), `data` object (which will be merged with data specified through `createError`, if it exists), and `message` (which will override the message specified through `createError`). +initialized with a custom `time_thrown` ISODate (default is current ISODate), `data` object (which will be merged with data specified through `createError`, if it exists), `internalData` object (which will be merged with internalData specified trough `createError`) and `message` (which will override the message specified through `createError`). -### createError(name, {message: String, [data: Object, options: Object]}): ApolloError +### createError(name, {message: String, [data: Object, internalData: object, options: Object]}): ApolloError -Creates and returns an error class with the given `name` and `message`, optionally initialized with the given `data` and `options`. `data` passed to `createError` will later be merged with any data passed to the constructor. +Creates and returns an error class with the given `name` and `message`, optionally initialized with the given `data`, `internalData` and `options`. `data` and `internalData` passed to `createError` will later be merged with any data passed to the constructor. #### Options (default): @@ -103,3 +131,6 @@ If the error is a known ApolloError, returns the serialized form of said error. **Otherwise**, *if strict is not truthy*, returns the original error passed into formatError. **Otherwise**, *if strict is truthy*, returns null. + +### isInstance (error): Boolean +Returns true if the error is an instance of an ApolloError. Otherwise, returns false diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 1357e3f..0000000 --- a/circle.yml +++ /dev/null @@ -1,14 +0,0 @@ -machine: - node: - version: 5.5.0 - -dependencies: - override: - - make environment - - make dependencies - -test: - override: - - make lint - - make - - make test diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..eb2549e --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,35 @@ +import ExtendableError from 'extendable-error'; +export interface ErrorConfig { + message: string; + time_thrown?: string; + data?: object; + internalData?: object; + options?: { + showPath?: boolean; + showLocations?: boolean; + }; +} +export interface ErrorInfo { + message: string; + name: string; + time_thrown: string; + data?: object; + path?: string; + locations?: any; +} +export declare class ApolloError extends ExtendableError { + name: string; + message: string; + time_thrown: string; + data: object; + internalData: object; + path: any; + locations: any; + _showLocations: boolean; + _showPath: boolean; + constructor(name: string, config: ErrorConfig, ctorConfig: ErrorConfig); + serialize(): ErrorInfo; +} +export declare const isInstance: (e: any) => boolean; +export declare const createError: (name: string, config: ErrorConfig) => any; +export declare const formatError: (error: any, returnNull?: boolean) => ErrorInfo; diff --git a/dist/index.js b/dist/index.js index 66fc43e..a63302d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,129 +1,102 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.formatError = exports.createError = undefined; - -var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _es6Error = require('es6-error'); - -var _es6Error2 = _interopRequireDefault(_es6Error); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var errorMap = new Map(); - -var DELIMITER = '/::/'; - -var serializeName = function serializeName() { - var arr = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; - return arr.reduce(function (str, val) { - return '' + (str.length > 0 ? str + DELIMITER : str) + (val.toString ? val.toString() : val); - }, ''); -}; -var deserializeName = function deserializeName() { - var name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - return name.split(DELIMITER); +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __assign = (this && this.__assign) || Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; }; - -var ApolloError = function (_ExtendableError) { - _inherits(ApolloError, _ExtendableError); - - function ApolloError(name, _ref) { - var message = _ref.message, - _ref$time_thrown = _ref.time_thrown, - time_thrown = _ref$time_thrown === undefined ? new Date().toISOString() : _ref$time_thrown, - _ref$data = _ref.data, - data = _ref$data === undefined ? {} : _ref$data, - _ref$options = _ref.options, - options = _ref$options === undefined ? {} : _ref$options; - - _classCallCheck(this, ApolloError); - - var t = arguments[2] && arguments[2].thrown_at || time_thrown; - var d = Object.assign({}, data, arguments[2] && arguments[2].data || {}); - var m = arguments[2] && arguments[2].message || message; - var opts = Object.assign({}, options, arguments[2] && arguments[2].options || {}); - - var _this = _possibleConstructorReturn(this, (ApolloError.__proto__ || Object.getPrototypeOf(ApolloError)).call(this, serializeName([name, t, Object.assign({}, d, { - toString: function toString() { - return JSON.stringify(d); - } - })]))); - - _this._name = name; - _this._humanized_message = m || ''; - _this._time_thrown = t; - _this._data = d; - _this._locations = opts.showLocations && arguments[2] && arguments[2].locations; - _this._path = opts.showPath && arguments[2] && arguments[2].path; - return _this; - } - - _createClass(ApolloError, [{ - key: 'serialize', - value: function serialize() { - var name = this._name; - var message = this._humanized_message; - var time_thrown = this._time_thrown; - var data = this._data; - var locations = this._locations; - var path = this._path; - var error = { - message: message, - name: name, - time_thrown: time_thrown, - data: data - }; - if (locations) error.locations = locations; - if (path) error.path = path; - return error; +Object.defineProperty(exports, "__esModule", { value: true }); +var assert = require("assert"); +var extendable_error_1 = require("extendable-error"); +var isString = function (d) { return Object.prototype.toString.call(d) === '[object String]'; }; +var isObject = function (d) { return Object.prototype.toString.call(d) === '[object Object]'; }; +var ApolloError = /** @class */ (function (_super) { + __extends(ApolloError, _super); + // NOTE: The object passed to the Constructor is actually `ctorData`. + // We are binding the constructor to the name and config object + // for the first two parameters inside of `createError` + function ApolloError(name, config, ctorConfig) { + var _this = _super.call(this, (ctorConfig && ctorConfig.message) || (config && config.message) || '') || this; + _this._showLocations = false; + _this._showPath = false; + var t = (ctorConfig && ctorConfig.time_thrown) || (config && config.time_thrown) || (new Date()).toISOString(); + var m = (ctorConfig && ctorConfig.message) || (config && config.message) || ''; + var ctorData = (ctorConfig && ctorConfig.data) || {}; + var ctorInternalData = (ctorConfig && ctorConfig.internalData) || {}; + var configData = (config && config.data) || {}; + var configInternalData = (config && config.internalData) || {}; + var d = __assign({}, _this.data, configData, ctorData); + var id = __assign({}, _this.internalData, configInternalData, ctorInternalData); + var ctorOptions = (ctorConfig && ctorConfig.options) || {}; + var configOptions = (config && config.options) || {}; + var opts = __assign({}, configOptions, ctorOptions); + _this.name = name; + _this.message = m; + _this.time_thrown = t; + _this.data = d; + _this.internalData = id; + _this._showLocations = !!opts.showLocations; + _this._showPath = !!opts.showPath; + return _this; } - }]); - - return ApolloError; -}(_es6Error2.default); - -var createError = exports.createError = function createError(name) { - var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { message: 'An error has occurred', options: options }; - - var e = ApolloError.bind(null, name, data); - errorMap.set(name, e); - return e; + ApolloError.prototype.serialize = function () { + var _a = this, name = _a.name, message = _a.message, time_thrown = _a.time_thrown, data = _a.data, _showLocations = _a._showLocations, _showPath = _a._showPath, path = _a.path, locations = _a.locations; + var error = { + message: message, + name: name, + time_thrown: time_thrown, + data: data, + path: path, + locations: locations + }; + if (_showLocations) { + error.locations = locations; + } + if (_showPath) { + error.path = path; + } + return error; + }; + return ApolloError; +}(extendable_error_1.default)); +exports.ApolloError = ApolloError; +exports.isInstance = function (e) { return e instanceof ApolloError; }; +exports.createError = function (name, config) { + assert(isObject(config), 'createError requires a config object as the second parameter'); + assert(isString(config.message), 'createError requires a "message" property on the config object passed as the second parameter'); + // NOTE: The first two parameters give to the constructor will always be name and config + // Parameters passed to the constructor when `new` is invoked will be passed as + // subsequent parameters. + return ApolloError.bind(null, name, config); }; - -var formatError = exports.formatError = function formatError(originalError) { - var returnNull = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - var _deserializeName = deserializeName(originalError.message), - _deserializeName2 = _slicedToArray(_deserializeName, 3), - name = _deserializeName2[0], - thrown_at = _deserializeName2[1], - d = _deserializeName2[2]; - - var locations = originalError.locations, - path = originalError.path; - - var data = d !== undefined ? JSON.parse(d) : {}; - if (!name) return returnNull ? null : originalError; - var CustomError = errorMap.get(name); - if (!CustomError) return returnNull ? null : originalError; - var error = new CustomError({ - thrown_at: thrown_at, - data: data, - locations: locations, - path: path - }); - return error.serialize(); +exports.formatError = function (error, returnNull) { + if (returnNull === void 0) { returnNull = false; } + var originalError = error ? error.originalError || error : null; + if (!originalError) + return returnNull ? null : error; + var name = originalError.name; + if (!name || !exports.isInstance(originalError)) + return returnNull ? null : error; + var time_thrown = originalError.time_thrown, message = originalError.message, data = originalError.data, _showLocations = originalError._showLocations, _showPath = originalError._showPath; + var locations = error.locations, path = error.path; + if (_showLocations) { + originalError.locations = locations; + } + if (_showPath) { + originalError.path = path; + } + return originalError.serialize(); }; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map index 6903d3f..5361035 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/index.js"],"names":["errorMap","Map","DELIMITER","serializeName","arr","reduce","str","val","length","toString","deserializeName","name","split","ApolloError","message","time_thrown","Date","toISOString","data","options","t","arguments","thrown_at","d","Object","assign","m","opts","JSON","stringify","_name","_humanized_message","_time_thrown","_data","_locations","showLocations","locations","_path","showPath","path","error","createError","e","bind","set","formatError","originalError","returnNull","undefined","parse","CustomError","get","serialize"],"mappings":"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","file":"index.js","sourcesContent":["import ExtendableError from 'es6-error';\n\nconst errorMap = new Map();\n\nconst DELIMITER = '/::/';\n\nconst serializeName = (arr = []) => arr.reduce((str, val) => `${str.length > 0 ? str + DELIMITER : str}${val.toString ? val.toString() : val}`, '');\nconst deserializeName = (name = '') => name.split(DELIMITER);\n\nclass ApolloError extends ExtendableError {\n constructor (name, {\n message,\n time_thrown = (new Date()).toISOString(),\n data = {},\n options = {},\n }) {\n const t = (arguments[2] && arguments[2].thrown_at) || time_thrown;\n const d = Object.assign({}, data, ((arguments[2] && arguments[2].data) || {}));\n const m = (arguments[2] && arguments[2].message) || message;\n const opts = Object.assign({}, options, ((arguments[2] && arguments[2].options) || {}));\n\n super(serializeName([\n name,\n t,\n Object.assign({}, d, {\n toString: () => JSON.stringify(d)\n }),\n ]));\n\n this._name = name;\n this._humanized_message = m || '';\n this._time_thrown = t;\n this._data = d;\n this._locations = (opts.showLocations && arguments[2] && arguments[2].locations)\n this._path = (opts.showPath && arguments[2] && arguments[2].path);\n }\n serialize () {\n const name = this._name;\n const message = this._humanized_message;\n const time_thrown = this._time_thrown;\n const data = this._data;\n const locations = this._locations;\n const path = this._path;\n let error = {\n message,\n name,\n time_thrown,\n data,\n };\n if (locations) error.locations = locations;\n if (path) error.path = path;\n return error;\n }\n}\n\nexport const createError = (name, data = { message: 'An error has occurred', options }) => {\n const e = ApolloError.bind(null, name, data);\n errorMap.set(name, e);\n return e;\n};\n\nexport const formatError = (originalError, returnNull = false) => {\n const [ name, thrown_at, d ] = deserializeName(originalError.message);\n const { locations, path } = originalError;\n const data = d !== undefined ? JSON.parse(d) : {};\n if (!name) return returnNull ? null : originalError;\n const CustomError = errorMap.get(name);\n if (!CustomError) return returnNull ? null : originalError;\n const error = new CustomError({\n thrown_at,\n data,\n locations,\n path,\n });\n return error.serialize();\n};\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/package.json b/package.json index a6da4c1..b2a9e70 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,48 @@ { - "name": "apollo-errors", - "version": "1.2.0", - "description": "Machine-readable custom errors for Apollostack's GraphQL server", - "main": "dist/index.js", - "scripts": { - "test": "make test" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/thebigredgeek/apollo-errors.git" - }, - "keywords": [ - "apollostack", - "graphql", - "error", - "api" - ], - "author": "Andrew E. Rhyne ", - "license": "MIT", - "bugs": { - "url": "https://github.com/thebigredgeek/apollo-errors/issues" - }, - "homepage": "https://github.com/thebigredgeek/apollo-errors#readme", - "dependencies": { - "es6-error": "^4.0.0" - }, - "devDependencies": { - "babel-cli": "^6.18.0", - "babel-core": "^6.17.0", - "babel-eslint": "^7.0.0", - "babel-preset-es2015": "^6.16.0", - "babel-register": "^6.18.0", - "chai": "^3.5.0", - "eslint": "^3.8.1", - "eslint-plugin-babel": "^3.3.0", - "mocha": "^3.1.2", - "rimraf": "^2.5.4" - } + "name": "apollo-errors", + "version": "1.9.0", + "description": "Machine-readable custom errors for Apollostack's GraphQL server", + "main": "dist/index.js", + "scripts": { + "test": "mocha", + "build": "tsc", + "lint": "eslint src", + "clean": "rimraf dist" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/thebigredgeek/apollo-errors.git" + }, + "keywords": [ + "apollostack", + "graphql", + "apollo-server", + "apollo-client", + "error", + "api" + ], + "author": "Andrew E. Rhyne ", + "license": "MIT", + "bugs": { + "url": "https://github.com/thebigredgeek/apollo-errors/issues" + }, + "homepage": "https://github.com/thebigredgeek/apollo-errors#readme", + "dependencies": { + "assert": "^2.0.0", + "extendable-error": "^0.1.5" + }, + "devDependencies": { + "babel-cli": "6.26.0", + "babel-core": "6.26.3", + "babel-eslint": "7.2.3", + "babel-preset-es2015": "6.24.1", + "babel-register": "6.26.0", + "chai": "3.5.0", + "eslint": "3.19.0", + "eslint-plugin-babel": "3.3.0", + "mocha": "3.5.3", + "rimraf": "2.6.3", + "typescript": "2.9.2" + }, + "typings": "dist/index.d.ts" } diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..f45d8f1 --- /dev/null +++ b/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] +} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 92c1810..0000000 --- a/src/index.js +++ /dev/null @@ -1,76 +0,0 @@ -import ExtendableError from 'es6-error'; - -const errorMap = new Map(); - -const DELIMITER = '/::/'; - -const serializeName = (arr = []) => arr.reduce((str, val) => `${str.length > 0 ? str + DELIMITER : str}${val.toString ? val.toString() : val}`, ''); -const deserializeName = (name = '') => name.split(DELIMITER); - -class ApolloError extends ExtendableError { - constructor (name, { - message, - time_thrown = (new Date()).toISOString(), - data = {}, - options = {}, - }) { - const t = (arguments[2] && arguments[2].thrown_at) || time_thrown; - const d = Object.assign({}, data, ((arguments[2] && arguments[2].data) || {})); - const m = (arguments[2] && arguments[2].message) || message; - const opts = Object.assign({}, options, ((arguments[2] && arguments[2].options) || {})); - - super(serializeName([ - name, - t, - Object.assign({}, d, { - toString: () => JSON.stringify(d) - }), - ])); - - this._name = name; - this._humanized_message = m || ''; - this._time_thrown = t; - this._data = d; - this._locations = (opts.showLocations && arguments[2] && arguments[2].locations) - this._path = (opts.showPath && arguments[2] && arguments[2].path); - } - serialize () { - const name = this._name; - const message = this._humanized_message; - const time_thrown = this._time_thrown; - const data = this._data; - const locations = this._locations; - const path = this._path; - let error = { - message, - name, - time_thrown, - data, - }; - if (locations) error.locations = locations; - if (path) error.path = path; - return error; - } -} - -export const createError = (name, data = { message: 'An error has occurred', options }) => { - const e = ApolloError.bind(null, name, data); - errorMap.set(name, e); - return e; -}; - -export const formatError = (originalError, returnNull = false) => { - const [ name, thrown_at, d ] = deserializeName(originalError.message); - const { locations, path } = originalError; - const data = d !== undefined ? JSON.parse(d) : {}; - if (!name) return returnNull ? null : originalError; - const CustomError = errorMap.get(name); - if (!CustomError) return returnNull ? null : originalError; - const error = new CustomError({ - thrown_at, - data, - locations, - path, - }); - return error.serialize(); -}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..683c776 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,122 @@ +import * as assert from 'assert'; +import ExtendableError from 'extendable-error'; + +const isString = d => Object.prototype.toString.call(d) === '[object String]'; +const isObject = d => Object.prototype.toString.call(d) === '[object Object]'; + +export interface ErrorConfig { + message: string; + time_thrown?: string; + data?: object; + internalData?: object; + options?: { + showPath?: boolean; + showLocations?: boolean; + }; +} + +export interface ErrorInfo { + message: string; + name: string; + time_thrown: string; + data?: object; + path?: string; + locations?: any; +} + +export class ApolloError extends ExtendableError { + name: string; + message: string; + time_thrown: string; + data: object; + internalData: object; + path: any; + locations: any; + _showLocations: boolean = false; + _showPath: boolean = false; + + // NOTE: The object passed to the Constructor is actually `ctorData`. + // We are binding the constructor to the name and config object + // for the first two parameters inside of `createError` + constructor(name: string, config: ErrorConfig, ctorConfig: ErrorConfig) { + super((ctorConfig && ctorConfig.message) || (config && config.message) || ''); + + const t = (ctorConfig && ctorConfig.time_thrown) || (config && config.time_thrown) || (new Date()).toISOString(); + const m = (ctorConfig && ctorConfig.message) || (config && config.message) || ''; + const ctorData = (ctorConfig && ctorConfig.data) || {}; + const ctorInternalData = (ctorConfig && ctorConfig.internalData) || {} + const configData = (config && config.data) || {}; + const configInternalData = (config && config.internalData) || {} + const d = { ...this.data, ...configData, ...ctorData }; + const id = { ...this.internalData, ...configInternalData, ...ctorInternalData} + const ctorOptions = (ctorConfig && ctorConfig.options) || {}; + const configOptions = (config && config.options) || {}; + const opts = { ...configOptions, ...ctorOptions }; + + + this.name = name; + this.message = m; + this.time_thrown = t; + this.data = d; + this.internalData = id; + this._showLocations = !!opts.showLocations; + this._showPath = !!opts.showPath; + } + + serialize(): ErrorInfo { + const { name, message, time_thrown, data, _showLocations, _showPath, path, locations } = this; + + let error: ErrorInfo = { + message, + name, + time_thrown, + data, + path, + locations + }; + + if (_showLocations) { + error.locations = locations; + } + + if (_showPath) { + error.path = path; + } + + return error; + } +} + +export const isInstance = e => e instanceof ApolloError; + +export const createError = (name: string, config: ErrorConfig) => { + assert(isObject(config), 'createError requires a config object as the second parameter'); + assert(isString(config.message), 'createError requires a "message" property on the config object passed as the second parameter'); + // NOTE: The first two parameters give to the constructor will always be name and config + // Parameters passed to the constructor when `new` is invoked will be passed as + // subsequent parameters. + return ApolloError.bind(null, name, config); +}; + +export const formatError = (error, returnNull = false): ErrorInfo => { + const originalError = error ? error.originalError || error : null; + + if (!originalError) return returnNull ? null : error; + + const { name } = originalError; + + if (!name || !isInstance(originalError)) return returnNull ? null : error; + + const { time_thrown, message, data, _showLocations, _showPath } = originalError; + const { locations, path } = error; + + if (_showLocations) { + originalError.locations = locations; + } + + if (_showPath) { + originalError.path = path; + } + + return originalError.serialize(); +}; diff --git a/test/spec.js b/test/spec.js index 7b632e4..4b2df72 100644 --- a/test/spec.js +++ b/test/spec.js @@ -1,41 +1,87 @@ import { expect } from 'chai'; +import assert from 'assert'; import { createError, formatError } from '../dist'; -describe('createError', () => { - it('returns an error that serializes properly', () => { - const FooError = createError('FooError', { - message: 'A foo error has occurred', - data: { - hello: 'world' - }, - options: { - showLocations: false, - showPath: true, - }, - }); +const { AssertionError } = assert; - const iso = new Date().toISOString(); +describe('createError', () => { + context('when properly used', () => { + it('returns an error that serializes properly', () => { + const FooError = createError('FooError', { + message: 'A foo error has occurred', + data: { + hello: 'world' + }, + options: { + showLocations: false, + showPath: true, + }, + }); + const iso = new Date().toISOString(); + const e = new FooError({ + message: 'A foo 2.0 error has occurred', + data: { + hello: 'world', + foo: 'bar' + }, + options: { + showLocations: true, + showPath: false, + }, + }); - const e = new FooError({ - message: 'A foo 2.0 error has occurred', - data: { + const { message, name, time_thrown, data } = e.serialize(); + expect(message).to.equal('A foo 2.0 error has occurred'); + expect(name).to.equal('FooError'); + expect(time_thrown).to.equal(e.time_thrown); + expect(data).to.eql({ + hello: 'world', foo: 'bar' - }, - options: { - showLocations: true, - showPath: false, - }, + }); }); + it('uses original message and merges original data', () => { + const FooError = createError('FooError', { + message: 'A foo error has occurred', + data: { + hello: 'world' + }, + options: { + showLocations: false, + showPath: true + } + }); + const e = new FooError({data: {foo: 'bar'}}); - const { message, name, time_thrown, data } = e.serialize(); + const { message, name, time_thrown, data } = e.serialize(); + expect(message).to.equal('A foo error has occurred'); + expect(name).to.equal('FooError'); + expect(time_thrown).to.equal(e.time_thrown); + expect(data).to.eql({ hello: 'world', foo: 'bar' }); + }); + }); + context('when missing a config as the second parameter', () => { + it('throws an assertion error with a useful message', () => { + try { + createError('FooError'); + throw new Error('did not throw as expected'); + } catch (err) { + expect(err instanceof AssertionError).to.be.true; + expect(err.message).to.equal('createError requires a config object as the second parameter'); + } + }); + }); + context('when missing a message from the config object passed as the second parameter', () => { + it('throws an assertion error with a useful message', () => { + try { + createError('FooError', { - expect(message).to.equal('A foo 2.0 error has occurred'); - expect(name).to.equal('FooError'); - expect(time_thrown).to.equal(e._time_thrown); - expect(data).to.eql({ - hello: 'world', - foo: 'bar' + }); + throw new Error('did not throw as expected'); + } catch (err) { + expect(err instanceof AssertionError).to.be.true; + expect(err.message).to.equal('createError requires a "message" property on the config object passed as the second parameter') + } }); }); }); @@ -55,13 +101,13 @@ describe('formatError', () => { }); const e = new FooError({ + message: 'A foo 2.0 error has occurred', data: { oh: 'shit' } }); - const s = formatError({ - message: e.message + originalError: e }, false); expect(s).to.eql(e.serialize()); @@ -84,11 +130,28 @@ describe('formatError', () => { const e = new FooError(); const s = formatError({ - message: e.message + originalError: e }, true); expect(s).to.eql(e.serialize()); }); }); }); + context('error has internalData', () => { + it('does not include the internalData property', () => { + const FooError = createError('FooError', { + message: 'A foo error has occurred', + internalData: { + secret: 'SQL ERROR' + } + }); + + const e = new FooError(); + expect(e.internalData).to.to.eql({ + secret: 'SQL ERROR' + }) + const s = formatError(e); + expect(s.internalData).to.eq(undefined) + }) + }) }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a84597b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "removeComments": false, + "noImplicitAny": false, + "outDir": "./dist", + "declaration": true + }, + "include": [ + "./src/**/*" + ] +}