diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..1b790bf --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015"], + "sourceMaps": true +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a0c5bbb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# editorconfig.org +root = true + +[*.js] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..c48b339 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,20 @@ +{ + "ecmaFeatures": { + "modules": true + }, + "env": { + "browser": true, + "node": true + }, + "parser": "babel-eslint", + "rules": { + "quotes": [2, "single"], + "strict": [2, "never"], + "babel/new-cap": 1, + "babel/object-shorthand": 0, + "babel/arrow-parens": 0 + }, + "plugins": [ + "babel" + ] +} diff --git a/.gitignore b/.gitignore index 5148e52..9aeb862 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ jspm_packages # Optional REPL history .node_repl_history + +# Yarn +yarn.lock diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..1064ff7 --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +src +test +.babelrc +.editorconfig +.eslintrc +.gitignore +circle.yml +Makefile diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c6c437f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +1.0.1 / 2016-11-10 +================== + + * Initial release diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bf4762d --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +PATH := node_modules/.bin:$(PATH) +SHELL := /bin/bash + +UNAME_S := $(shell uname -s) + +ifeq ($(UNAME_S),Linux) + OS_TYPE := linux +endif +ifeq ($(UNAME_S),Darwin) + OS_TYPE := osx +endif + +.FORCE: + +all: clean + babel src -d dist --source-maps + +clean: .FORCE + rimraf npm-debug.log dist + +osx-syspackages: .FORCE + brew update + brew install yarn + +linux-syspackages: .FORCE + sudo apt-key adv --keyserver pgp.mit.edu --recv D101F7899D41F3C3 + echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list + sudo apt-get -y update + sudo apt-get install yarn + +environment: .FORCE + @if [ "${OS_TYPE}" = "osx" ]; then \ + make osx-syspackages; \ + else \ + make linux-syspackages; \ + fi + +dependencies: .FORCE + yarn + +test: all + mocha + +lint: .FORCE + eslint src diff --git a/README.md b/README.md index 9621bee..8c230eb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,104 @@ # apollo-errors -A sane way to create and throw custom errors with Apollo's graphql server +Machine-readable custom errors for Apollostack's GraphQL server + +[![NPM](https://nodei.co/npm/apollo-errors.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/apollo-errors/) + +[![CircleCI](https://circleci.com/gh/thebigredgeek/apollo-errors.svg?style=shield)](https://circleci.com/gh/thebigredgeek/apollo-errors/tree/master) + + +## Installation and usage + +Install the package: + +```bash +npm install apollo-errors +``` + +Create some errors: + +```javascript +import { createError } from 'apollo-errors'; + +export const FooError = createError('FooError', { + message: 'A foo error has occurred' +}); +``` + +Hook up formatting: + +```javascript +import express from 'express'; +import bodyParser from 'body-parser'; +import { formatError } from 'apollo-errors'; +import schema from './schema'; + +const app = express(); + +app.use('/graphql', + bodyParser.json(), + graphqlExpress({ + formatError, + schema + }) +); + +app.listen(8080) +``` + +Throw some errors: + +```javascript +import { FooError } from './errors'; + +const resolverThatThrowsError = (root, params, context) => { + throw new FooError({ + data: { + something: 'important' + } + }); +} +``` + +Witness glorious simplicity: + +`POST /graphql (200)` + +```json +{ + "data": {}, + "errors": [ + { + "message":"A foo error has occurred", + "name":"FooError", + "time_thrown":"2016-11-11T00:40:50.954Z", + "data":{ + "something": "important" + } + } + ] +} +``` + +## API + +### ApolloError ({ [time_thrown: String, data: Object]}) + +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) and `data` object (which will be merged with data specified through `createError`, if it exists). + + +### createError(name, {message: String, [data: Object]}): ApolloError + +Creates and returns an error class with the given `name` and `message`, optionally initialized with the given `data`. `data` passed to `createError` will later be merged with any data passed to the constructor. + +### formatError (error, strict = false): ApolloError|Error|null +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. + +## TODO + +- Add better docs diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..1357e3f --- /dev/null +++ b/circle.yml @@ -0,0 +1,14 @@ +machine: + node: + version: 5.5.0 + +dependencies: + override: + - make environment + - make dependencies + +test: + override: + - make lint + - make + - make test diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..06bcb74 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,111 @@ +'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; + }, ''); +}; +var deserializeName = function deserializeName() { + var name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + + var arr = []; + var str = name.split(DELIMITER); + arr.push(str.shift()); + arr.push(str.join(DELIMITER)); + return arr; +}; + +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; + + _classCallCheck(this, ApolloError); + + var t = arguments[2] && arguments[2].thrown_at || time_thrown; + var d = Object.assign({}, data, arguments[2] && arguments[2].data || {}); + + var _this = _possibleConstructorReturn(this, (ApolloError.__proto__ || Object.getPrototypeOf(ApolloError)).call(this, serializeName([name, t]))); + + _this._name = name; + _this._humanized_message = message || ''; + _this._time_thrown = t; + _this._data = d; + 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; + return { + message: message, + name: name, + time_thrown: time_thrown, + data: data + }; + } + }]); + + 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' }; + + var e = ApolloError.bind(null, name, data); + errorMap.set(name, e); + return e; +}; + +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, 2), + name = _deserializeName2[0], + thrown_at = _deserializeName2[1]; + + 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 + }); + return error.serialize(); +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map new file mode 100644 index 0000000..0458466 --- /dev/null +++ b/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/index.js"],"names":[],"mappings":";;;;;;;;;;;AAAA;;;;;;;;;;;;AAEA,IAAM,WAAW,IAAI,GAAJ,EAAX;;AAEN,IAAM,YAAY,GAAZ;;AAEN,IAAM,gBAAgB,SAAhB,aAAgB;MAAC,0EAAM;SAAO,IAAI,MAAJ,CAAW,UAAC,GAAD,EAAM,GAAN;iBAAiB,IAAI,MAAJ,GAAa,CAAb,GAAiB,MAAM,SAAN,GAAkB,GAAnC,IAAyC;GAA1D,EAAiE,EAA5E;CAAd;AACtB,IAAM,kBAAkB,SAAlB,eAAkB,GAAe;MAAd,2EAAO,GAAO;;AACrC,MAAM,MAAM,EAAN,CAD+B;AAErC,MAAM,MAAM,KAAK,KAAL,CAAW,SAAX,CAAN,CAF+B;AAGrC,MAAI,IAAJ,CAAS,IAAI,KAAJ,EAAT,EAHqC;AAIrC,MAAI,IAAJ,CAAS,IAAI,IAAJ,CAAS,SAAT,CAAT,EAJqC;AAKrC,SAAO,GAAP,CALqC;CAAf;;IAQlB;;;AACJ,uBAAa,IAAb,QAIG;QAHD;gCACA;uDAAc,IAAK,IAAJ,EAAD,CAAa,WAAb;yBACd;yCAAO,eACN;;;;AACD,QAAM,IAAI,SAAC,CAAU,CAAV,KAAgB,UAAU,CAAV,EAAa,SAAb,IAA2B,WAA5C,CADT;AAED,QAAM,IAAI,OAAO,MAAP,CAAc,EAAd,EAAkB,IAAlB,EAAyB,SAAC,CAAU,CAAV,KAAgB,UAAU,CAAV,EAAa,IAAb,IAAsB,EAAvC,CAA7B,CAFL;;0HAIK,cAAc,CAClB,IADkB,EAElB,CAFkB,CAAd,IAJL;;AASD,UAAK,KAAL,GAAa,IAAb,CATC;AAUD,UAAK,kBAAL,GAA0B,WAAW,EAAX,CAVzB;AAWD,UAAK,YAAL,GAAoB,CAApB,CAXC;AAYD,UAAK,KAAL,GAAa,CAAb,CAZC;;GAJH;;;;gCAkBa;AACX,UAAM,OAAO,KAAK,KAAL,CADF;AAEX,UAAM,UAAU,KAAK,kBAAL,CAFL;AAGX,UAAM,cAAc,KAAK,YAAL,CAHT;AAIX,UAAM,OAAO,KAAK,KAAL,CAJF;AAKX,aAAO;AACL,wBADK;AAEL,kBAFK;AAGL,gCAHK;AAIL,kBAJK;OAAP,CALW;;;;;;;AAcR,IAAM,oCAAc,SAAd,WAAc,CAAC,IAAD,EAAuD;MAAhD,2EAAO,EAAE,SAAS,uBAAT,GAAuC;;AAChF,MAAM,IAAI,YAAY,IAAZ,CAAiB,IAAjB,EAAuB,IAAvB,EAA6B,IAA7B,CAAJ,CAD0E;AAEhF,WAAS,GAAT,CAAa,IAAb,EAAmB,CAAnB,EAFgF;AAGhF,SAAO,CAAP,CAHgF;CAAvD;;AAMpB,IAAM,oCAAc,SAAd,WAAc,CAAC,aAAD,EAAuC;MAAvB,iFAAa,MAAU;;yBACpC,gBAAgB,cAAc,OAAd;;MAApC;MAAM,iCADkD;;AAEhE,MAAI,CAAC,IAAD,EAAO,OAAO,aAAa,IAAb,GAAoB,aAApB,CAAlB;AACA,MAAM,cAAc,SAAS,GAAT,CAAa,IAAb,CAAd,CAH0D;AAIhE,MAAI,CAAC,WAAD,EAAc,OAAO,aAAa,IAAb,GAAoB,aAApB,CAAzB;AACA,MAAM,QAAQ,IAAI,WAAJ,CAAgB;AAC5B,wBAD4B;GAAhB,CAAR,CAL0D;AAQhE,SAAO,MAAM,SAAN,EAAP,CARgE;CAAvC","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}`, '');\nconst deserializeName = (name = '') => {\n const arr = [];\n const str = name.split(DELIMITER);\n arr.push(str.shift());\n arr.push(str.join(DELIMITER));\n return arr;\n};\n\nclass ApolloError extends ExtendableError {\n constructor (name, {\n message,\n time_thrown = (new Date()).toISOString(),\n data = {}\n }) {\n const t = (arguments[2] && arguments[2].thrown_at) || time_thrown;\n const d = Object.assign({}, data, ((arguments[2] && arguments[2].data) || {}));\n\n super(serializeName([\n name,\n t\n ]));\n\n this._name = name;\n this._humanized_message = message || '';\n this._time_thrown = t;\n this._data = d;\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 return {\n message,\n name,\n time_thrown,\n data\n };\n }\n}\n\nexport const createError = (name, data = { message: 'An error has occurred' }) => {\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 ] = deserializeName(originalError.message);\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 });\n return error.serialize();\n};\n"]} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6f251ee --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "apollo-errors", + "version": "1.0.1", + "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" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..8c69338 --- /dev/null +++ b/src/index.js @@ -0,0 +1,64 @@ +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}`, ''); +const deserializeName = (name = '') => { + const arr = []; + const str = name.split(DELIMITER); + arr.push(str.shift()); + arr.push(str.join(DELIMITER)); + return arr; +}; + +class ApolloError extends ExtendableError { + constructor (name, { + message, + time_thrown = (new Date()).toISOString(), + data = {} + }) { + const t = (arguments[2] && arguments[2].thrown_at) || time_thrown; + const d = Object.assign({}, data, ((arguments[2] && arguments[2].data) || {})); + + super(serializeName([ + name, + t + ])); + + this._name = name; + this._humanized_message = message || ''; + this._time_thrown = t; + this._data = d; + } + serialize () { + const name = this._name; + const message = this._humanized_message; + const time_thrown = this._time_thrown; + const data = this._data; + return { + message, + name, + time_thrown, + data + }; + } +} + +export const createError = (name, data = { message: 'An error has occurred' }) => { + const e = ApolloError.bind(null, name, data); + errorMap.set(name, e); + return e; +}; + +export const formatError = (originalError, returnNull = false) => { + const [ name, thrown_at ] = deserializeName(originalError.message); + if (!name) return returnNull ? null : originalError; + const CustomError = errorMap.get(name); + if (!CustomError) return returnNull ? null : originalError; + const error = new CustomError({ + thrown_at + }); + return error.serialize(); +}; diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..6b233a1 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +--compilers js:babel-register diff --git a/test/spec.js b/test/spec.js new file mode 100644 index 0000000..c6a5597 --- /dev/null +++ b/test/spec.js @@ -0,0 +1,81 @@ +import { expect } from 'chai'; + +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' + } + }); + + const iso = new Date().toISOString(); + + const e = new FooError({ + data: { + foo: 'bar' + } + }); + + 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(iso); + expect(data).to.eql({ + hello: 'world', + foo: 'bar' + }); + }); +}); + +describe('formatError', () => { + context('second parameter is not truthy', () => { + context('error is not known', () => { + it('returns the original error', () => { + const e = new Error('blah'); + expect(formatError(e, false)).to.equal(e); + }); + }); + context('error is known', () => { + it('returns the serialized form of the real error', () => { + const FooError = createError('FooError', { + message: 'A foo error has occurred' + }); + + const e = new FooError(); + + const s = formatError({ + message: e.message + }, false); + + expect(s).to.eql(e.serialize()); + }); + }); + }); + context('second parameter is truthy', () => { + context('error is not known', () => { + it('returns null', () => { + const e = new Error('blah'); + expect(formatError(e, true)).to.be.null; + }); + }); + context('error is known', () => { + it('returns the real error', () => { + const FooError = createError('FooError', { + message: 'A foo error has occurred' + }); + + const e = new FooError(); + + const s = formatError({ + message: e.message + }, true); + + expect(s).to.eql(e.serialize()); + }); + }); + }); +});