From 668819d26d0e1be98086b7b82662051d53ca13a7 Mon Sep 17 00:00:00 2001 From: "Andrew E. Rhyne" Date: Thu, 10 Nov 2016 16:25:22 -0800 Subject: [PATCH] initial --- .babelrc | 4 ++ .editorconfig | 10 +++++ .eslintrc | 20 +++++++++ .gitignore | 3 ++ .npmignore | 8 ++++ CHANGELOG.md | 4 ++ Makefile | 45 +++++++++++++++++++ README.md | 104 ++++++++++++++++++++++++++++++++++++++++++- circle.yml | 14 ++++++ dist/index.js | 111 ++++++++++++++++++++++++++++++++++++++++++++++ dist/index.js.map | 1 + package.json | 40 +++++++++++++++++ src/index.js | 64 ++++++++++++++++++++++++++ test/mocha.opts | 1 + test/spec.js | 81 +++++++++++++++++++++++++++++++++ 15 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 .babelrc create mode 100644 .editorconfig create mode 100644 .eslintrc create mode 100644 .npmignore create mode 100644 CHANGELOG.md create mode 100644 Makefile create mode 100644 circle.yml create mode 100644 dist/index.js create mode 100644 dist/index.js.map create mode 100644 package.json create mode 100644 src/index.js create mode 100644 test/mocha.opts create mode 100644 test/spec.js 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()); + }); + }); + }); +});