diff --git a/.eslintrc b/.eslintrc index 9ce92c1..11d152b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,5 @@ { + "extends": "airbnb/base", "ecmaFeatures": { "blockBindings": true, "forOf": true, @@ -17,102 +18,9 @@ }, "parser": "babel-eslint", "rules": { - "accessor-pairs": 2, - "arrow-spacing": [2, { "before": true, "after": true }], - "brace-style": [2, "1tbs", { "allowSingleLine": true }], - "comma-dangle": [2, "never"], - "comma-spacing": [2, { "before": false, "after": true }], - "comma-style": [2, "last"], - "constructor-super": 2, - "curly": [2, "multi-line"], - "dot-location": [2, "property"], - "eol-last": 2, - "eqeqeq": [2, "allow-null"], - "generator-star-spacing": [2, { "before": true, "after": true }], - "handle-callback-err": [2, "^(err|error)$" ], - "indent": [2, 2, { "indentSwitchCase": true }], - "key-spacing": [2, { "beforeColon": false, "afterColon": true }], - "new-cap": [2, { "newIsCap": true, "capIsNew": false }], - "new-parens": 2, - "no-array-constructor": 2, - "no-caller": 2, - "no-class-assign": 2, - "no-cond-assign": 2, - "no-const-assign": 2, - "no-control-regex": 2, - "no-debugger": 2, - "no-delete-var": 2, - "no-dupe-args": 2, - "no-dupe-keys": 2, - "no-duplicate-case": 2, - "no-empty-character-class": 2, - "no-empty-label": 2, - "no-eval": 2, - "no-ex-assign": 2, - "no-extend-native": 2, - "no-extra-bind": 2, - "no-extra-boolean-cast": 2, - "no-extra-parens": [2, "functions"], - "no-fallthrough": 2, - "no-floating-decimal": 2, - "no-func-assign": 2, - "no-implied-eval": 2, - "no-inner-declarations": [2, "functions"], - "no-invalid-regexp": 2, - "no-irregular-whitespace": 2, - "no-iterator": 2, - "no-label-var": 2, - "no-labels": 2, - "no-lone-blocks": 2, - "no-mixed-spaces-and-tabs": 2, - "no-multi-spaces": 2, - "no-multi-str": 2, - "no-multiple-empty-lines": [2, { "max": 1 }], - "no-native-reassign": 2, - "no-negated-in-lhs": 2, - "no-new": 2, - "no-new-func": 2, - "no-new-object": 2, - "no-new-require": 2, - "no-new-wrappers": 2, - "no-obj-calls": 2, - "no-octal": 2, - "no-octal-escape": 2, - "no-proto": 2, - "no-redeclare": 2, - "no-regex-spaces": 2, - "no-return-assign": 2, - "no-self-compare": 2, - "no-sequences": 2, - "no-shadow-restricted-names": 2, - "no-spaced-func": 2, - "no-sparse-arrays": 2, - "no-this-before-super": 2, - "no-throw-literal": 2, - "no-trailing-spaces": 2, - "no-undef": 2, - "no-undef-init": 2, - "no-unexpected-multiline": 2, - "no-unneeded-ternary": 2, - "no-unreachable": 2, - "no-useless-call": 2, - "no-with": 2, - "one-var": [2, { "initialized": "never" }], - "operator-linebreak": [2, "after"], - "quotes": [2, "single", "avoid-escape"], - "radix": 2, - "semi": [2, "always"], - "space-after-keywords": [2, "always"], - "space-before-blocks": [2, "always"], - "space-before-function-paren": [2, "never"], - "space-in-parens": [2, "never"], - "space-infix-ops": 2, - "space-return-throw-case": 2, - "space-unary-ops": [2, { "words": true, "nonwords": false }], - "spaced-comment": [2, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], - "use-isnan": 2, - "valid-typeof": 2, - "wrap-iife": [2, "any"], - "yoda": [2, "never"] + "no-shadow": 0, + "no-param-reassign": 0, + "id-length": [2, {"exceptions": ["_", "i"]}], + "comma-dangle": [1, "never"] } -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index 123ae94..e167cf9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # Logs logs *.log +lib +dist # Runtime data pids @@ -13,15 +15,9 @@ lib-cov # Coverage directory used by tools like istanbul coverage -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - # node-waf configuration .lock-wscript -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..93e214d --- /dev/null +++ b/.jscsrc @@ -0,0 +1,4 @@ +{ + "preset": "airbnb", + "requireTrailingComma": false +} diff --git a/.npmignore b/.npmignore index d723e5f..e1abae3 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,5 @@ coverage -.travis.yml example .idea +src *.spec.js diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 77e93e2..0000000 --- a/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: node_js -node_js: - - "0.12" - - "iojs" diff --git a/CHANGELOG.md b/CHANGELOG.md index ee3b293..b4a7d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,16 @@ ### chore -* chore(package): bump version to 1.0.2 - ([7c6aef2](https://github.com/RisingStack/graffiti/commit/7c6aef2)) +* chore(changelog): update changelog ([bb10f78](https://github.com/RisingStack/graffiti/commit/bb10f78)) +* chore(package): bump version to 1.0.2 ([7c6aef2](https://github.com/RisingStack/graffiti/commit/7c6aef2)) ### docs -* docs(readme): add roadmap - ([1a33dc0](https://github.com/RisingStack/graffiti/commit/1a33dc0)) +* docs(readme): add roadmap ([1a33dc0](https://github.com/RisingStack/graffiti/commit/1a33dc0)) ### fix -* fix(middleware): use graphql of the adapter - ([d1af1ee](https://github.com/RisingStack/graffiti/commit/d1af1ee)) +* fix(middleware): use graphql of the adapter ([d1af1ee](https://github.com/RisingStack/graffiti/commit/d1af1ee)) @@ -25,12 +23,10 @@ ### chore -* chore(npmignore): add .npmignore file - ([b1917b4](https://github.com/RisingStack/graffiti/commit/b1917b4)) -* chore(package): add keywords to package.json, fix license - ([5478001](https://github.com/RisingStack/graffiti/commit/5478001)) -* chore(package): bump version to 1.0.1 - ([e5c0476](https://github.com/RisingStack/graffiti/commit/e5c0476)) +* chore(changelog): update CHANGELOG.md ([a928d35](https://github.com/RisingStack/graffiti/commit/a928d35)) +* chore(npmignore): add .npmignore file ([b1917b4](https://github.com/RisingStack/graffiti/commit/b1917b4)) +* chore(package): add keywords to package.json, fix license ([5478001](https://github.com/RisingStack/graffiti/commit/5478001)) +* chore(package): bump version to 1.0.1 ([e5c0476](https://github.com/RisingStack/graffiti/commit/e5c0476)) @@ -40,88 +36,51 @@ ### chore -* chore(changelog): add CHANGELOG file to project - ([1fa3fb7](https://github.com/RisingStack/graffiti/commit/1fa3fb7)) -* chore(ci): configure travis via file - ([8d930bd](https://github.com/RisingStack/graffiti/commit/8d930bd)) -* chore(contributing): add CONTRIBUTING file - ([8b41f3e](https://github.com/RisingStack/graffiti/commit/8b41f3e)) -* chore(package): add --harmony flag to mocha to support Node.js v0.12 - ([39338d6](https://github.com/RisingStack/graffiti/commit/39338d6)) -* chore(package): add package.json file - ([eec7c31](https://github.com/RisingStack/graffiti/commit/eec7c31)) -* chore(project): add ESLint - ([05b2abc](https://github.com/RisingStack/graffiti/commit/05b2abc)) -* chore(project): add LICENSE - ([ff0cc46](https://github.com/RisingStack/graffiti/commit/ff0cc46)) -* chore(project): add README and .gitignore files - ([172dc93](https://github.com/RisingStack/graffiti/commit/172dc93)) +* chore(changelog): add CHANGELOG file to project ([1fa3fb7](https://github.com/RisingStack/graffiti/commit/1fa3fb7)) +* chore(changelog): update CHANGELOG.md ([7a74fde](https://github.com/RisingStack/graffiti/commit/7a74fde)) +* chore(ci): configure travis via file ([8d930bd](https://github.com/RisingStack/graffiti/commit/8d930bd)) +* chore(contributing): add CONTRIBUTING file ([8b41f3e](https://github.com/RisingStack/graffiti/commit/8b41f3e)) +* chore(package): add --harmony flag to mocha to support Node.js v0.12 ([39338d6](https://github.com/RisingStack/graffiti/commit/39338d6)) +* chore(package): add package.json file ([eec7c31](https://github.com/RisingStack/graffiti/commit/eec7c31)) +* chore(project): add ESLint ([05b2abc](https://github.com/RisingStack/graffiti/commit/05b2abc)) +* chore(project): add LICENSE ([ff0cc46](https://github.com/RisingStack/graffiti/commit/ff0cc46)) +* chore(project): add README and .gitignore files ([172dc93](https://github.com/RisingStack/graffiti/commit/172dc93)) ### docs -* docs(readkme): add "What is GraphQL?" section to README - ([553f1f6](https://github.com/RisingStack/graffiti/commit/553f1f6)) -* docs(readme): add Express and Hapi middleware examples to README - ([c09de39](https://github.com/RisingStack/graffiti/commit/c09de39)) -* docs(readme): add TravisCI badge to README - ([59b9288](https://github.com/RisingStack/graffiti/commit/59b9288)) -* docs(readme): add graffiti overview to README - ([ff74268](https://github.com/RisingStack/graffiti/commit/ff74268)) -* docs(readme): add logo to the README - ([e8cefcd](https://github.com/RisingStack/graffiti/commit/e8cefcd)), closes [#1](https://github.com/RisingStack/graffiti/issues/1) -* docs(readme): add plugins section to README - ([b804dfa](https://github.com/RisingStack/graffiti/commit/b804dfa)) -* docs(readme): fix GraphQL tutorial anchor text - ([3c7e01a](https://github.com/RisingStack/graffiti/commit/3c7e01a)) -* docs(readme): fix Hapi related typo in README - ([6bdb276](https://github.com/RisingStack/graffiti/commit/6bdb276)) -* docs(readme): fix typo in README - ([d036436](https://github.com/RisingStack/graffiti/commit/d036436)) -* docs(readme): fix typo in REAME - ([5e47ec5](https://github.com/RisingStack/graffiti/commit/5e47ec5)) -* docs(readme): fix typo in REAME - ([e6b31a6](https://github.com/RisingStack/graffiti/commit/e6b31a6)) -* docs(readme): link plugins to usage - ([ea7eb66](https://github.com/RisingStack/graffiti/commit/ea7eb66)) -* docs(readme): rename plugins to adapters in README - ([28a1fc2](https://github.com/RisingStack/graffiti/commit/28a1fc2)) +* docs(readkme): add "What is GraphQL?" section to README ([553f1f6](https://github.com/RisingStack/graffiti/commit/553f1f6)) +* docs(readme): add Express and Hapi middleware examples to README ([c09de39](https://github.com/RisingStack/graffiti/commit/c09de39)) +* docs(readme): add graffiti overview to README ([ff74268](https://github.com/RisingStack/graffiti/commit/ff74268)) +* docs(readme): add logo to the README ([e8cefcd](https://github.com/RisingStack/graffiti/commit/e8cefcd)), closes [#1](https://github.com/RisingStack/graffiti/issues/1) +* docs(readme): add plugins section to README ([b804dfa](https://github.com/RisingStack/graffiti/commit/b804dfa)) +* docs(readme): add TravisCI badge to README ([59b9288](https://github.com/RisingStack/graffiti/commit/59b9288)) +* docs(readme): fix GraphQL tutorial anchor text ([3c7e01a](https://github.com/RisingStack/graffiti/commit/3c7e01a)) +* docs(readme): fix Hapi related typo in README ([6bdb276](https://github.com/RisingStack/graffiti/commit/6bdb276)) +* docs(readme): fix typo in README ([d036436](https://github.com/RisingStack/graffiti/commit/d036436)) +* docs(readme): fix typo in REAME ([5e47ec5](https://github.com/RisingStack/graffiti/commit/5e47ec5)) +* docs(readme): fix typo in REAME ([e6b31a6](https://github.com/RisingStack/graffiti/commit/e6b31a6)) +* docs(readme): link plugins to usage ([ea7eb66](https://github.com/RisingStack/graffiti/commit/ea7eb66)) +* docs(readme): rename plugins to adapters in README ([28a1fc2](https://github.com/RisingStack/graffiti/commit/28a1fc2)) ### feat -* feat(express): add Express middleware - ([80b5ddc](https://github.com/RisingStack/graffiti/commit/80b5ddc)) -* feat(hapi): add Hapi plugin - ([fac1791](https://github.com/RisingStack/graffiti/commit/fac1791)) -* feat(koa): add Koa middleware - ([cdecd28](https://github.com/RisingStack/graffiti/commit/cdecd28)) -* feat(middleware): add project skeleton - ([e50c0ce](https://github.com/RisingStack/graffiti/commit/e50c0ce)) -* feat(util): add object property assert with throw - ([2cfb447](https://github.com/RisingStack/graffiti/commit/2cfb447)) +* feat(express): add Express middleware ([80b5ddc](https://github.com/RisingStack/graffiti/commit/80b5ddc)) +* feat(hapi): add Hapi plugin ([fac1791](https://github.com/RisingStack/graffiti/commit/fac1791)) +* feat(koa): add Koa middleware ([cdecd28](https://github.com/RisingStack/graffiti/commit/cdecd28)) +* feat(middleware): add project skeleton ([e50c0ce](https://github.com/RisingStack/graffiti/commit/e50c0ce)) +* feat(util): add object property assert with throw ([2cfb447](https://github.com/RisingStack/graffiti/commit/2cfb447)) ### refactor -* refactor(util): move isGet and isPrefixed to util - ([72a104b](https://github.com/RisingStack/graffiti/commit/72a104b)) +* refactor(util): move isGet and isPrefixed to util ([72a104b](https://github.com/RisingStack/graffiti/commit/72a104b)) ### test -* test(express): add failing test cases for Express middleware - ([4349a48](https://github.com/RisingStack/graffiti/commit/4349a48)) -* test(express): cover Express middleware schema creation with tests - ([348aac9](https://github.com/RisingStack/graffiti/commit/348aac9)) -* test(express): cover Express middleware with tests - ([8dc7282](https://github.com/RisingStack/graffiti/commit/8dc7282)) -* test(express): finish up test cases for the Express middleware - ([b0b3dd0](https://github.com/RisingStack/graffiti/commit/b0b3dd0)) -* test(hapi): cover Hapi plugin with tests - ([e826ad3](https://github.com/RisingStack/graffiti/commit/e826ad3)) -* test(hapi): improve Hapi plugin test coverage - ([1be58ad](https://github.com/RisingStack/graffiti/commit/1be58ad)) -* test(koa): cover Koa middleware with tests - ([f0dce2e](https://github.com/RisingStack/graffiti/commit/f0dce2e)) -* test(util): add skipping test cases for isPrefixed and isGet - ([e824db5](https://github.com/RisingStack/graffiti/commit/e824db5)) - - - +* test(express): add failing test cases for Express middleware ([4349a48](https://github.com/RisingStack/graffiti/commit/4349a48)) +* test(express): cover Express middleware schema creation with tests ([348aac9](https://github.com/RisingStack/graffiti/commit/348aac9)) +* test(express): cover Express middleware with tests ([8dc7282](https://github.com/RisingStack/graffiti/commit/8dc7282)) +* test(express): finish up test cases for the Express middleware ([b0b3dd0](https://github.com/RisingStack/graffiti/commit/b0b3dd0)) +* test(hapi): cover Hapi plugin with tests ([e826ad3](https://github.com/RisingStack/graffiti/commit/e826ad3)) +* test(hapi): improve Hapi plugin test coverage ([1be58ad](https://github.com/RisingStack/graffiti/commit/1be58ad)) +* test(koa): cover Koa middleware with tests ([f0dce2e](https://github.com/RisingStack/graffiti/commit/f0dce2e)) +* test(util): add skipping test cases for isPrefixed and isGet ([e824db5](https://github.com/RisingStack/graffiti/commit/e824db5)) diff --git a/README.md b/README.md index cfa490b..b48725f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ![graffiti](https://cloud.githubusercontent.com/assets/1764512/8900273/9ed758dc-343e-11e5-95ba-e82f876cf52d.png) -[![Build Status](https://travis-ci.org/RisingStack/graffiti.svg)](https://travis-ci.org/RisingStack/graffiti) +[ ![Codeship Status for RisingStack/graffiti](https://codeship.com/projects/0c4fb010-5969-0133-8c37-4255fd5efb39/status?branch=master)](https://codeship.com/projects/110029) Currently the consumption of HTTP REST APIs dominate the client-side world, [GraphQL](https://github.com/facebook/graphql) aims to change this. @@ -35,26 +35,29 @@ For a running **example server** and **executable queries**, check out our examp ## Install -``` +```bash npm install @risingstack/graffiti --save ``` ## Usage +1. register the middleware +2. provide a schema (returned by an adapters `getSchema` method or your own `GraphQLSchema` instance) +3. the GraphQL endpoint is available on `/graphql` + ### Express ```javascript -var express = require('express'); -var graffiti = require('@risingstack/graffiti'); -var graffitiMongoose = require('@risingstack/graffiti-mongoose'); -var User = require('./models/user'); -var Cat = require('./models/cat'); +import express from 'express'; +import graffiti from '@risingstack/graffiti'; +import {getSchema} from '@risingstack/graffiti-mongoose'; + +import Cat from './models/Cat'; +import User from './models/User'; -var app = express(); +const app = express(); app.use(graffiti.express({ - prefix: '/graphql', - adapter: graffitiMongoose, - models: [User, Cat] + schema: getSchema([User, Cat]) })); app.listen(3000); @@ -63,22 +66,17 @@ app.listen(3000); ### Hapi ```javascript -var hapi = require('hapi'); -var graffiti = require('@risingstack/graffiti'); -var graffitiMongoose = require('@risingstack/graffiti-mongoose'); +import {Server} from 'hapi'; +import graffiti from '@risingstack/graffiti'; +import {getSchema} from '@risingstack/graffiti-mongoose'; -var server = new Hapi.Server(); +const server = new Server(); server.connection({ port: 3000 }); server.register({ register: graffiti.hapi, options: { - adapter: graffitiMongoose, - models: [] - } -}, { - routes: { - prefix: '/graphql' + schema: getSchema([User, Cat]) } }, function (err) { if (err) { @@ -94,29 +92,28 @@ server.register({ ### Koa ```javascript -var koa = require('koa'); -var graffiti = require('@risingstack/graffiti'); -var graffitiMongoose = require('@risingstack/graffiti-mongoose'); +import koa from 'koa'; +import graffiti from '@risingstack/graffiti'; +import {getSchema} from '@risingstack/graffiti-mongoose'; + +import Cat from './models/Cat'; +import User from './models/User'; -var app = koa(); +const app = koa(); app.use(graffiti.koa({ - prefix: '/graphql', - adapter: graffitiMongoose, - models: [] + schema: getSchema([User, Cat]) })); app.listen(3000); ``` +## Options + +- `schema`: a `GraphQLSchema` instance. You can use an adapters `getSchema` method, or provide your own schema. (required) +- `graphiql`: may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser. (default: `true`) + ## Test -``` +```bash npm test ``` -## Roadmap - -* Query support *(done)* -* Mutation support *(in progress)* -* More adapters - * RethinkDB - Thinky *(in progress)* - * SQL - Bookshelf *(in progress)* diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..9ea1a95 --- /dev/null +++ b/example/README.md @@ -0,0 +1,13 @@ +# Graffiti examples + +### Run + +```bash +# Install dependencies +cd .. && npm install && cd example && npm install +# Run all servers +node . +# Express server listening on port 3001 +# Hapi server listening on port 3002 +# Koa server listening on port 3003 +``` diff --git a/example/express.js b/example/express.js index 91f2e9e..7d5c8d8 100644 --- a/example/express.js +++ b/example/express.js @@ -1,19 +1,16 @@ -var express = require('express'); -var graffiti = require('../'); -var graffitiMongoose = require('@risingstack/graffiti-mongoose'); - -var app = express(); +import express from 'express'; +import graffiti from '../'; +import schema from './schema'; +const app = express(); app.use(graffiti.express({ - prefix: '/graphql', - adapter: graffitiMongoose, - models: [] + schema })); -app.listen(3000, function (err) { +app.listen(3001, (err) => { if (err) { - throw err - }; + throw err; + } - console.log('server is listening'); + console.log('Express server is listening on port 3001'); }); diff --git a/example/hapi.js b/example/hapi.js index 011a771..4dd7f27 100644 --- a/example/hapi.js +++ b/example/hapi.js @@ -1,26 +1,20 @@ -var Hapi = require('hapi'); -var graffiti = require('../'); -var graffitiMongoose = require('@risingstack/graffiti-mongoose'); - -var server = new Hapi.Server(); -server.connection({ port: 3000 }); +import {Server} from 'hapi'; +import graffiti from '../'; +import schema from './schema'; +const server = new Server(); +server.connection({port: 3002}); server.register({ register: graffiti.hapi, options: { - adapter: graffitiMongoose, - models: [] - } -}, { - routes: { - prefix: '/graphql' + schema } -}, function (err) { +}, (err) => { if (err) { console.error('Failed to load plugin:', err); } - server.start(function () { - console.log('Server running at:', server.info.uri); + server.start(() => { + console.log('Hapi server is listening on port 3002'); }); }); diff --git a/example/index.js b/example/index.js new file mode 100644 index 0000000..6106d8a --- /dev/null +++ b/example/index.js @@ -0,0 +1,4 @@ +require('babel/register'); +require('./express'); +require('./hapi'); +require('./koa'); diff --git a/example/koa.js b/example/koa.js index 45d5bc4..ad1a467 100644 --- a/example/koa.js +++ b/example/koa.js @@ -1,18 +1,16 @@ -var koa = require('koa'); -var graffiti = require('../'); -var graffitiMongoose = require('@risingstack/graffiti-mongoose'); +import koa from 'koa'; +import graffiti from '../'; +import schema from './schema'; -var app = koa(); +const app = koa(); app.use(graffiti.koa({ - prefix: '/graphql', - adapter: graffitiMongoose, - models: [] + schema })); -app.listen(3000, function (err) { +app.listen(3003, (err) => { if (err) { - throw err - }; + throw err; + } - console.log('server is listening'); + console.log('Koa server is listening on port 3003'); }); diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..9949dcb --- /dev/null +++ b/example/package.json @@ -0,0 +1,17 @@ +{ + "name": "graffiti-examples", + "description": "Example servers using graffiti", + "scripts": { + "start": "node .", + "postinstall": "npm uninstall graphql" + }, + "dependencies": { + "@risingstack/graffiti": "../", + "@risingstack/graffiti-mongoose": "^4.0.1", + "babel": "^5.8.21", + "express": "^4.13.3", + "koa": "^1.1.0", + "koa-static": "^1.5.1", + "mongoose": "^4.2.3" + } +} diff --git a/example/schema/index.js b/example/schema/index.js new file mode 100644 index 0000000..bf6c5eb --- /dev/null +++ b/example/schema/index.js @@ -0,0 +1,8 @@ +import mongoose from 'mongoose'; +import User from './user'; +import Pet from './pet'; +import {getSchema} from '@risingstack/graffiti-mongoose'; + +mongoose.connect(process.env.MONGO_URI || 'mongodb://localhost/graphql'); + +export default getSchema([Pet, User]); diff --git a/example/schema/pet.js b/example/schema/pet.js new file mode 100644 index 0000000..02c6919 --- /dev/null +++ b/example/schema/pet.js @@ -0,0 +1,17 @@ +import mongoose from 'mongoose'; + +const PetSchema = new mongoose.Schema({ + name: { + type: String + }, + type: { + type: String + }, + age: { + type: Number + } +}); + +const Pet = mongoose.model('Pet', PetSchema); + +export default Pet; diff --git a/example/schema/user.js b/example/schema/user.js new file mode 100644 index 0000000..af29c05 --- /dev/null +++ b/example/schema/user.js @@ -0,0 +1,27 @@ +import mongoose from 'mongoose'; + +const UserSchema = new mongoose.Schema({ + name: { + type: String + }, + age: { + type: Number, + index: true + }, + friends: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }], + pets: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'Pet' + }], + createdAt: { + type: Date, + default: Date.now + } +}); + +const User = mongoose.model('User', UserSchema); + +export default User; diff --git a/index.js b/index.js deleted file mode 100644 index 211e452..0000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./src'); \ No newline at end of file diff --git a/package.json b/package.json index c75969b..e3288a8 100644 --- a/package.json +++ b/package.json @@ -2,16 +2,18 @@ "name": "@risingstack/graffiti", "version": "1.0.2", "description": "Mongoose adapter for graffiti (Node.js GraphQL ORM)", - "main": "index.js", + "main": "lib/index.js", "scripts": { - "test": "NODE_ENV=test mocha --harmony --require co-mocha $(find src -name \"*.spec.js\")", - "eslint": "eslint src" + "test": "NODE_ENV=test mocha --compilers js:babel/register --require co-mocha 'src/**/*.spec.js'", + "eslint": "eslint src", + "prepublish": "npm test && npm run build", + "build": "rm -rf lib/* && babel src --ignore *.spec.js --out-dir lib" }, + "author": "RisingStack", "repository": { "type": "git", "url": "git@github.com:RisingStack/graffiti.git" }, - "author": "RisingStack", "license": "MIT", "keywords": [ "GraphQL", @@ -26,20 +28,27 @@ }, "homepage": "https://github.com/RisingStack/graffiti#readme", "dependencies": { - "boom": "^2.8.0" + "boom": "^3.0.0", + "co-body": "^4.0.0" + }, + "peerDependencies": { + "graphql": "^0.4.12" }, "devDependencies": { - "babel-eslint": "^4.0.0", - "chai": "^3.2.0", - "chai-subset": "^1.0.1", + "babel": "^5.8.29", + "babel-eslint": "^4.1.3", + "chai": "^3.4.0", + "chai-subset": "^1.1.0", "co-mocha": "^1.1.2", - "eslint": "1.0.0-rc-3", - "express": "^4.13.1", - "hapi": "^8.8.0", - "koa": "^0.21.0", - "mocha": "^2.2.5", - "pre-commit": "^1.0.10", - "sinon": "^1.15.4", + "eslint": "1.8.0", + "eslint-config-airbnb": "^0.1.0", + "express": "^4.13.3", + "graphql": "^0.4.12", + "hapi": "^11.0.3", + "koa": "^2.0.0-alpha.2", + "mocha": "^2.3.3", + "pre-commit": "^1.1.2", + "sinon": "^1.17.2", "sinon-chai": "^2.8.0" }, "pre-commit": [ diff --git a/src/config.js b/src/config.js deleted file mode 100644 index 7b0dfa1..0000000 --- a/src/config.js +++ /dev/null @@ -1,3 +0,0 @@ -var config = {}; - -module.exports = config; diff --git a/src/express/express.js b/src/express/express.js new file mode 100644 index 0000000..74c0be4 --- /dev/null +++ b/src/express/express.js @@ -0,0 +1,50 @@ +import {graphql} from 'graphql'; +import {json} from 'co-body'; +import { + badRequest, + methodNotAllowed +} from 'boom'; +import { + required, + isPath, + isGet, + isPost, + renderGraphiQL +} from '../util'; + +function sendError(response, boom) { + const {statusCode, payload} = boom.output; + response.status(statusCode).send(payload); +} + +export default function middleware({graphiql = true, schema = required()} = {}) { + return (request, response, next) => { + if (isPath(request) && (isPost(request) || isGet(request))) { + return json(request).then((body) => { + const {query, variables} = Object.assign({}, body, request.query); + + if (isGet(request) && request.accepts('html') && graphiql) { + return response.send(renderGraphiQL({query, variables})); + } + + if (isGet(request) && query && query.includes('mutation')) { + const boom = methodNotAllowed('GraphQL mutation only allowed in POST request.'); + return sendError(response, boom); + } + + return graphql(schema, query, request, variables) + .then((result) => { + if (result.errors) { + const message = result.errors.map((error) => error.message).join('\n'); + const boom = badRequest(message); + return sendError(response, boom); + } + + response.json(result); + }); + }); + } + + return next(); + }; +} diff --git a/src/express/express.spec.js b/src/express/express.spec.js new file mode 100644 index 0000000..ac7521a --- /dev/null +++ b/src/express/express.spec.js @@ -0,0 +1,185 @@ +import {expect} from 'chai'; +import parser from 'co-body'; +import express from './'; + +describe('graffiti express', () => { + const mwFactory = express; + + beforeEach(function stub() { + this.sandbox.stub(parser, 'json', (request) => Promise.resolve(request.body)); + }); + + describe('checks for required options', () => { + it('should throw an error if not all met', () => { + // schema is missing + expect(() => mwFactory({ + })).to.throw(Error); + }); + + it('shouldn\'t throw an error if all is passed', () => { + expect(() => mwFactory({ + schema: {} + })).not.to.throw(Error); + }); + }); + + describe('checks for allowed methods', () => { + it('should only allow GET and POST requests', function methodTest(done) { + const mw = mwFactory({ + schema: this.schema + }); + + const request = { + method: 'PUT', + path: '/graphql' + }; + + const next = () => done(); + mw(request, null, next); + }); + }); + + describe('requested path matches with /graphql', () => { + it('should handle requests', function pathTest(done) { + const mw = mwFactory({ + schema: this.schema + }); + + const request = { + path: '/notGraphql' + }; + + const next = () => done(); + mw(request, null, next); + }); + + it('should return with proper result on GET', function postTest(done) { + const result = {data: 1}; + const mw = mwFactory({ + schema: this.schema + }); + + const request = { + method: 'GET', + path: '/graphql', + query: { + query: '{data}', + variables: {} + }, + accepts: (type) => type === 'json' + }; + + const response = { + json: ({data}) => { + expect(data).to.be.eql(result); + done(); + } + }; + + mw(request, response); + }); + + it('should reject mutations on GET', function postTest(done) { + const mw = mwFactory({ + schema: this.schema + }); + + const request = { + method: 'GET', + path: '/graphql', + query: { + query: `mutation mutate { + updateData(data: "123") { + data + } + }` + }, + accepts: (type) => type === 'json' + }; + + const response = { + status: () => response, + send: ({statusCode}) => { + expect(statusCode).to.be.eql(405); + done(); + } + }; + + mw(request, response); + }); + + it('should return with proper result on POST', function postTest(done) { + const result = {data: 1}; + const mw = mwFactory({ + schema: this.schema + }); + + const request = { + method: 'POST', + path: '/graphql', + body: { + query: '{data}', + variables: {} + }, + accepts: (type) => type === 'json' + }; + + const response = { + json: ({data}) => { + expect(data).to.be.eql(result); + done(); + } + }; + + mw(request, response); + }); + + it('should return with GraphiQL on GET when it is enabled', function getTest(done) { + const mw = mwFactory({ + schema: this.schema + }); + + const request = { + method: 'GET', + path: '/graphql', + accepts: (type) => type === 'html' + }; + + const response = { + send: (data) => { + expect(data.includes('GraphiQL')).to.be.ok; // eslint-disable-line + done(); + } + }; + + mw(request, response); + }); + + it('should not provide GraphiQL when it is disabled', function getTest(done) { + const mw = mwFactory({ + schema: this.schema, + graphiql: false + }); + + const request = { + method: 'GET', + path: '/graphql', + query: { + query: '{data}', + variables: {} + }, + accepts: (type) => type === 'html' + }; + + const result = {data: 1}; + const response = { + json: ({data}) => { + expect(data).to.be.eql(result); + done(); + } + }; + + mw(request, response); + }); + }); +}); diff --git a/src/express/index.js b/src/express/index.js index 9d582c8..61da234 100644 --- a/src/express/index.js +++ b/src/express/index.js @@ -1,33 +1 @@ -var checkDep = require('../util').checkDep; -var isPrefixed = require('../util').isPrefixed; -var isGet = require('../util').isGet; - -function create() { - return function(options) { - - var models = checkDep(options, 'models'); - var adapter = checkDep(options, 'adapter'); - var prefix = checkDep(options, 'prefix'); - - var schema = adapter.getSchema(models); - - return function(request, respone, next) { - - var query = request.query.q; - - if (isGet(request) && isPrefixed(request, prefix)) { - return adapter.graphql(schema, query) - .then(function(result) { - respone.json(result); - }) - .catch(function(err) { - next(err); - }); - } - - return next(); - }; - }; -} - -module.exports.create = create; +export {default} from './express'; diff --git a/src/express/index.spec.js b/src/express/index.spec.js deleted file mode 100644 index 89d7b5d..0000000 --- a/src/express/index.spec.js +++ /dev/null @@ -1,136 +0,0 @@ -var expect = require('chai').expect; - -describe('graffiti express', function() { - - var express = require('./'); - - describe('checks for required options', function() { - - it('throws an error if not all met', function() { - - var mwFactory = express.create(); - - try { - mwFactory({ - prefix: '/graphql' - }); - } catch (ex) { - expect(ex).to.be.ok; - return; - } - - throw new Error('Error should have been thrown'); - - }); - - it('doesn\'t throw if all is passed', function() { - - var mwFactory = express.create(); - - var mw = mwFactory({ - prefix: '/graphql', - models: [], - adapter: { - getSchema: function() {} - } - }); - - expect(mw).to.be.ok; - }); - }); - - it('creates the schema', function() { - var mwFactory = express.create(); - var getSchemaSpy = this.sandbox.spy(); - var models = [{ - name: 'User' - }]; - - mwFactory({ - prefix: '/graphql', - models: models, - adapter: { - getSchema: getSchemaSpy - } - }); - - expect(getSchemaSpy).to.be.calledWith(models); - }); - - describe('requested url starts with prefix', function() { - - it('returns with proper results', function() { - - var result = { - data: 1 - }; - - var mwFactory = express.create(); - - var mw = mwFactory({ - prefix: '/graphql', - models: [], - adapter: { - getSchema: function() {}, - graphql: function() { - return Promise.resolve(); - } - } - }); - - var request = { - method: 'GET', - path: '/graphql', - query: { - q: '{__type}' - } - }; - - var response = { - json: function(d) { - expect(d).to.eql(result); - } - }; - - mw(request, response); - }); - - }); - - describe('requested url does not start with prefix', function() { - - it('calls the next middleware', function() { - - var mwFactory = express.create(); - - var mw = mwFactory({ - prefix: '/graphql', - models: [], - adapter: { - getSchema: function() {}, - graphql: function() { - return Promise.resolve(); - } - } - }); - - var request = { - method: 'GET', - path: '/not-good', - query: { - q: '{__type}' - } - }; - - var response = {}; - - var nextSpy = this.sandbox.spy(); - - mw(request, response, nextSpy); - - expect(nextSpy.called).to.be.ok; - }); - - }); - -}); diff --git a/src/hapi/hapi.js b/src/hapi/hapi.js new file mode 100644 index 0000000..5d4965d --- /dev/null +++ b/src/hapi/hapi.js @@ -0,0 +1,66 @@ +import {graphql} from 'graphql'; +import { + badRequest, + methodNotAllowed +} from 'boom'; +import { + isGet, + required, + renderGraphiQL +} from '../util'; + +import pkg from '../../package.json'; + +function accepts({headers}, type) { + return headers.accept && headers.accept.includes(type); +} + +const plugin = { + register: (server, {graphiql = true, schema = required()} = {}, next) => { + const handler = (request, reply) => { + const data = request.payload || request.query || {}; + const {query, variables} = data; + + if (accepts(request, 'html') && graphiql) { + return reply(renderGraphiQL({query, variables})); + } + + if (query && query.includes('mutation') && isGet(request)) { + return reply(methodNotAllowed('GraphQL mutation only allowed in POST request.')); + } + + return graphql(schema, query, request, variables) + .then((result) => { + if (result.errors) { + const message = result.errors.map((error) => error.message).join('\n'); + return reply(badRequest(message)); + } + + reply(result); + }) + .catch((err) => { + reply(badRequest(err)); + }); + }; + + server.route({ + method: 'POST', + path: '/graphql', + handler + }); + + if (graphiql) { + server.route({ + method: 'GET', + path: '/graphql', + handler + }); + } + + next(); + } +}; + +plugin.register.attributes = {pkg}; + +export default plugin; diff --git a/src/hapi/hapi.spec.js b/src/hapi/hapi.spec.js new file mode 100644 index 0000000..0ef403b --- /dev/null +++ b/src/hapi/hapi.spec.js @@ -0,0 +1,152 @@ +import {expect} from 'chai'; +import {Server} from 'hapi'; +import hapi from './'; + +describe('graffiti hapi', () => { + describe('checks for required options', () => { + it('should throw an error if not all met', () => { + const mwFactory = hapi; + + // schema is missing + expect(() => mwFactory({ + })).to.throw(Error); + }); + }); + + describe('requested path matches with /graphql', () => { + it('should return with proper result on GET', function getTest(done) { + const server = new Server(); + server.connection({ port: 3000 }); + + const result = {data: 1}; + + server.register({ + register: hapi, + options: { + schema: this.schema + } + }, (err) => { + if (err) { + return done(err); + } + + server.inject({ + method: 'GET', + url: '/graphql?query={data}' + }, ({payload}) => { + expect(JSON.parse(payload).data).to.eql(result); + done(); + }); + }); + }); + + it('should reject mutations on GET', function getTest(done) { + const server = new Server(); + server.connection({ port: 3000 }); + + server.register({ + register: hapi, + options: { + schema: this.schema + } + }, (err) => { + if (err) { + return done(err); + } + + server.inject({ + method: 'GET', + url: '/graphql?query=mutation%20mutate%20{updateData(data:"123"){data}}' + }, ({payload}) => { + expect(JSON.parse(payload).error).to.be.ok; // eslint-disable-line + done(); + }); + }); + }); + + it('should return with proper result on POST', function postTest(done) { + const server = new Server(); + server.connection({ port: 3000 }); + + const result = {data: 1}; + + server.register({ + register: hapi, + options: { + schema: this.schema + } + }, (err) => { + if (err) { + return done(err); + } + + server.inject({ + method: 'POST', + url: '/graphql', + payload: { + query: '{data}', + variables: {} + } + }, ({payload}) => { + expect(JSON.parse(payload).data).to.eql(result); + done(); + }); + }); + }); + + it('should return with GraphiQL on GET when it is enabled', function getTest(done) { + const server = new Server(); + server.connection({ port: 3000 }); + + server.register({ + register: hapi, + options: { + schema: this.schema + } + }, (err) => { + if (err) { + return done(err); + } + + server.inject({ + method: 'GET', + url: '/graphql', + headers: { + Accept: 'text/html' + } + }, ({payload}) => { + expect(payload.includes('GraphiQL')).to.be.ok; // eslint-disable-line + done(); + }); + }); + }); + + it('should not provide GraphiQL when it is disabled', function getTest(done) { + const server = new Server(); + server.connection({ port: 3000 }); + + server.register({ + register: hapi, + options: { + schema: this.schema, + graphiql: false + } + }, (err) => { + if (err) { + return done(err); + } + + server.inject({ + method: 'GET', + url: '/graphql', + headers: { + Accept: 'text/html' + } + }, ({statusCode}) => { + expect(statusCode).to.be.eql(404); + done(); + }); + }); + }); + }); +}); diff --git a/src/hapi/index.js b/src/hapi/index.js index 5ef9730..e5200c7 100644 --- a/src/hapi/index.js +++ b/src/hapi/index.js @@ -1,42 +1 @@ -var checkDep = require('../util').checkDep; -var boom = require('boom'); - -function create() { - var plugin = { - register: function(server, options, next) { - - var models = checkDep(options, 'models'); - var adapter = checkDep(options, 'adapter'); - - var schema = adapter.getSchema(models); - - server.route({ - method: 'GET', - path: '/', - handler: function(request, reply) { - - var query = request.query.q; - - return adapter.graphql(schema, query) - .then(function(result) { - reply(result); - }) - .catch(function(err) { - reply(boom.badImplementation(err)); - }); - } - }); - - next(); - } - }; - - plugin.register.attributes = { - name: 'graffiti-hapi', - version: '1.0.0' - }; - - return plugin; -} - -module.exports.create = create; +export {default} from './hapi'; diff --git a/src/hapi/index.spec.js b/src/hapi/index.spec.js deleted file mode 100644 index 2f0b7c1..0000000 --- a/src/hapi/index.spec.js +++ /dev/null @@ -1,69 +0,0 @@ -var expect = require('chai').expect; -var Hapi = require('hapi'); - -describe('graffiti hapi', function() { - - var hapi = require('./'); - - describe('checks for required options', function() { - - it('throws an error if not all met', function() { - - var mwFactory = hapi.create(); - - try { - mwFactory({ - prefix: '/graphql' - }); - } catch (ex) { - expect(ex).to.be.ok; - return; - } - - throw new Error('Error should have been thrown'); - - }); - - }); - - describe('returns with proper results', function(done) { - - var server = new Hapi.Server(); - server.connection({ port: 3000 }); - - var result = { - users: [1, 2, 3] - }; - - server.register({ - register: hapi.create(), - options: { - models: [], - adapter: { - getSchema: function() {}, - graphql: function() { - return Promise.resolve(); - } - } - } - }, { - routes: { - prefix: '/graphql' - } - }, function(err) { - if (err) { - return done(err); - } - - server.inject({ - method: 'GET', - url: '/graphql?q={__type}' - }, function(res) { - expect(JSON.parse(res.payload)).to.eql(result); - done(); - }); - }); - - }); - -}); diff --git a/src/index.js b/src/index.js index 6398c96..52514ba 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,9 @@ -var koa = require('./koa'); -var hapi = require('./hapi'); -var express = require('./express'); +import koa from './koa'; +import hapi from './hapi'; +import express from './express'; -module.exports.koa = koa.create(); -module.exports.hapi = hapi.create(); -module.exports.express = express.create(); +export default { + koa, + hapi, + express +}; diff --git a/src/index.spec.js b/src/index.spec.js index 290c209..a8045de 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -1,19 +1,16 @@ -var expect = require('chai').expect; +import {expect} from 'chai'; +import graffiti from './'; -describe('graffiti', function() { - - var graffiti = require('./'); - - it('exposes an express function', function() { - expect(graffiti.express).to.be.ok; +describe('graffiti', () => { + it('should expose an express function', () => { + expect(graffiti.express).to.be.ok; // eslint-disable-line }); - it('exposes an hapi function', function() { - expect(graffiti.hapi).to.be.ok; + it('should expose an hapi function', () => { + expect(graffiti.hapi).to.be.ok; // eslint-disable-line }); - it('exposes an koa function', function() { - expect(graffiti.koa).to.be.ok; + it('should expose an koa function', () => { + expect(graffiti.koa).to.be.ok; // eslint-disable-line }); - }); diff --git a/src/koa/index.js b/src/koa/index.js index 36263d4..dd4633d 100644 --- a/src/koa/index.js +++ b/src/koa/index.js @@ -1,28 +1 @@ -var checkDep = require('../util').checkDep; -var isPrefixed = require('../util').isPrefixed; -var isGet = require('../util').isGet; - -function create() { - return function koa(options) { - - var models = checkDep(options, 'models'); - var adapter = checkDep(options, 'adapter'); - var prefix = checkDep(options, 'prefix'); - - var schema = adapter.getSchema(models); - - return function *(next) { - - var query = this.query.q; - - if (isGet(this) && isPrefixed(this, prefix)) { - this.body = yield adapter.graphql(schema, query); - return this.body; - } - - yield next; - }; - }; -} - -module.exports.create = create; +export {default} from './koa'; diff --git a/src/koa/index.spec.js b/src/koa/index.spec.js deleted file mode 100644 index 61b95c2..0000000 --- a/src/koa/index.spec.js +++ /dev/null @@ -1,115 +0,0 @@ -var expect = require('chai').expect; - -describe('graffiti koa', function() { - - var koa = require('./'); - - describe('checks for required options', function() { - - it('throws an error if not all met', function() { - - var mwFactory = koa.create(); - - try { - mwFactory({ - prefix: '/graphql' - }); - } catch (ex) { - expect(ex).to.be.ok; - return; - } - - throw new Error('Error should have been thrown'); - - }); - - }); - - it('creates the schema', function() { - var mwFactory = koa.create(); - var getSchemaSpy = this.sandbox.spy(); - var models = [{ - name: 'User' - }]; - - mwFactory({ - prefix: '/graphql', - models: models, - adapter: { - getSchema: getSchemaSpy - } - }); - - expect(getSchemaSpy).to.be.calledWith(models); - }); - - describe('requested url starts with prefix', function() { - - it('returns with proper results', function *() { - - var result = { - data: 1 - }; - - var mwFactory = koa.create(); - - var mw = mwFactory({ - prefix: '/graphql', - models: [], - adapter: { - getSchema: function() {}, - graphql: function() { - return Promise.resolve({ - data: 1 - }); - } - } - }); - - var request = { - method: 'GET', - path: '/graphql', - query: { - q: '{__type}' - } - }; - - var res = yield mw.call(request); - - expect(res).to.eql(result); - }); - - }); - - describe('requested url does not start with prefix', function() { - - it('calls the next middleware', function *(done) { - - var mwFactory = koa.create(); - - var mw = mwFactory({ - prefix: '/graphql', - models: [], - adapter: { - getSchema: function() {} - } - }); - - var request = { - method: 'GET', - path: '/not-good', - query: { - q: '{__type}' - } - }; - - yield mw.call(request, function *() { - yield done(); - }); - - throw new Error('next should have been called'); - }); - - }); - -}); diff --git a/src/koa/koa.js b/src/koa/koa.js new file mode 100644 index 0000000..a164a9b --- /dev/null +++ b/src/koa/koa.js @@ -0,0 +1,38 @@ +import {graphql} from 'graphql'; +import {json} from 'co-body'; +import { + required, + isPath, + isGet, + isPost, + renderGraphiQL +} from '../util'; + +function accepts(type) { + return this.headers && this.headers.accept && this.headers.accept.includes(type); +} + +export default function middleware({graphiql = true, schema = required()} = {}) { + return function *middleware(next) { + if (isPath(this) && (isPost(this) || isGet(this))) { + const body = yield json(this); + const {query, variables} = Object.assign({}, body, this.query); + + if (isGet(this) && accepts.call(this, 'html') && graphiql) { + this.body = renderGraphiQL({query, variables}); + return this.body; + } + + if (isGet(this) && query && query.includes('mutation')) { + this.status = 406; + this.body = 'GraphQL mutation only allowed in POST request.'; + return this.body; + } + + this.body = yield graphql(schema, query, this, variables); + return this.body; + } + + yield next; + }; +} diff --git a/src/koa/koa.spec.js b/src/koa/koa.spec.js new file mode 100644 index 0000000..0943d12 --- /dev/null +++ b/src/koa/koa.spec.js @@ -0,0 +1,106 @@ +import {expect} from 'chai'; +import parser from 'co-body'; +import koa from './'; + +describe('graffiti koa', () => { + beforeEach(function stub() { + this.sandbox.stub(parser, 'json', (request) => Promise.resolve(request.payload || request.body)); + }); + + describe('checks for required options', () => { + it('should throw an error if not all met', () => { + const mwFactory = koa; + + // schema is missing + expect(() => mwFactory({ + })).to.throw(Error); + }); + }); + + describe('requested path matches with /graphql', () => { + it('should return with proper result on GET', function *getTest() { + const mwFactory = koa; + + const mw = mwFactory({ + schema: this.schema + }); + + const request = { + method: 'GET', + path: '/graphql', + query: { + query: '{data}', + variables: {} + } + }; + + const result = {data: 1}; + const res = yield mw.call(request); + + expect(res.data).to.eql(result); + }); + + it('should return with proper result on POST', function *postTest() { + const mwFactory = koa; + + const mw = mwFactory({ + schema: this.schema + }); + + const request = { + method: 'POST', + path: '/graphql', + payload: { + query: '{data}', + variables: {} + } + }; + + const result = {data: 1}; + const res = yield mw.call(request); + + expect(res.data).to.eql(result); + }); + + it('should return with GraphiQL on GET when it is enabled', function *getTest() { + const mwFactory = koa; + + const mw = mwFactory({ + schema: this.schema + }); + + const request = { + method: 'GET', + path: '/graphql', + headers: { + accept: 'text/html' + } + }; + + const res = yield mw.call(request); + + expect(res.includes('GraphiQL')).to.be.ok; // eslint-disable-line + }); + + it('should not provide GraphiQL when it is disabled', function *getTest() { + const mwFactory = koa; + + const mw = mwFactory({ + schema: this.schema, + graphiql: false + }); + + const request = { + method: 'GET', + path: '/graphql', + query: { + query: '{data}' + } + }; + + const result = {data: 1}; + const res = yield mw.call(request); + expect(res.data).to.eql(result); + }); + }); +}); diff --git a/src/test-setup.spec.js b/src/test-setup.spec.js index 19165ca..4110006 100644 --- a/src/test-setup.spec.js +++ b/src/test-setup.spec.js @@ -1,17 +1,34 @@ -var sinon = require('sinon'); -var chai = require('chai'); -var sinonChai = require('sinon-chai'); -var chaiSubset = require('chai-subset'); +import sinon from 'sinon'; +import chai from 'chai'; +import sinonChai from 'sinon-chai'; +import chaiSubset from 'chai-subset'; +import { + GraphQLSchema, + GraphQLObjectType, + GraphQLInt +} from 'graphql'; -before(function() { +before(() => { chai.use(chaiSubset); chai.use(sinonChai); }); -beforeEach(function() { +beforeEach(function beforeEachTest() { this.sandbox = sinon.sandbox.create(); + this.schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'RootQuery', + fields: { + data: { + name: 'data', + type: GraphQLInt, + resolve: () => 1 + } + } + }) + }); }); -afterEach(function() { +afterEach(function afterEachTest() { this.sandbox.restore(); }); diff --git a/src/util/index.js b/src/util/index.js index 4cd5b94..bbebd88 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -1,23 +1 @@ -function isGet(request) { - return request.method === 'GET'; -} - -function isPrefixed(request, prefix) { - return request.path.indexOf(prefix) === 0; -} - -function checkDep(obj, propName) { - if (!obj) { - throw new Error('Object cannot be undefined'); - } - - if (!obj[propName]) { - throw new Error(propName + ' cannot be undefined'); - } - - return obj[propName]; -}; - -module.exports.checkDep = checkDep; -module.exports.isGet = isGet; -module.exports.isPrefixed = isPrefixed; +export * from './util'; diff --git a/src/util/index.spec.js b/src/util/index.spec.js deleted file mode 100644 index 2824f55..0000000 --- a/src/util/index.spec.js +++ /dev/null @@ -1,61 +0,0 @@ -var expect = require('chai').expect; - -describe('graffiti util', function() { - - var util = require('./'); - - describe('#isPrefixed', function() { - - it('returns true for request paths starting with prefix'); - - it('returns false for request paths not starting with prefix'); - - }); - - describe('#isGet', function() { - - it('returns true for GET'); - - it('returns false for not GETs'); - - }); - - describe('#checkDep', function() { - - it('throws if first argument is undefined', function() { - - try { - util.checkDep(); - } catch (ex) { - expect(ex.message).to.eql('Object cannot be undefined'); - return; - } - - throw new Error('Error should have been thrown'); - }); - - it('throws property does not exists', function() { - - try { - util.checkDep({}, 'colour'); - } catch (ex) { - expect(ex.message).to.eql('colour cannot be undefined'); - return; - } - - throw new Error('Error should have been thrown'); - }); - - it('returns the property', function() { - - var prop = util.checkDep({ - colour: 'red' - }, 'colour'); - - expect(prop).to.eql('red'); - - }); - - }); - -}); diff --git a/src/util/util.js b/src/util/util.js new file mode 100644 index 0000000..517fba4 --- /dev/null +++ b/src/util/util.js @@ -0,0 +1,103 @@ +export function isGet(request) { + return request.method === 'GET'; +} + +export function isPost(request) { + return request.method === 'POST'; +} + +export function isPath(request) { + return request.path === '/graphql'; +} + +export function required() { + throw new Error('Required option is missing'); +} + +const GRAPHIQL_VERSION = '0.3.1'; + +// TODO default query +// const defaultQuery = ``; +export function renderGraphiQL({query, variables, version = GRAPHIQL_VERSION} = {}) { + return ` + + + + + + + + + + Loading... + + + `; +} diff --git a/src/util/utils.spec.js b/src/util/utils.spec.js new file mode 100644 index 0000000..a54e4c4 --- /dev/null +++ b/src/util/utils.spec.js @@ -0,0 +1,26 @@ +import {expect} from 'chai'; +import { + isPost +} from './'; + +describe('graffiti util', () => { + describe('isPost()', () => { + it('should return true for POST', () => { + const request = { + method: 'POST' + }; + + const post = isPost(request); + expect(post).to.be.ok; // eslint-disable-line + }); + + it('returns false for not GETs', () => { + const request = { + method: 'GET' + }; + + const get = isPost(request); + expect(get).not.to.be.ok; // eslint-disable-line + }); + }); +});