-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding in restify integration, docs, tests more... #189
Changes from 2 commits
d456ff5
77e1f41
d27b4af
30a98ab
3d26656
5b57865
ffe3b98
eeb9f1d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,11 @@ | ||
# GraphQL Server for Express, Connect, Hapi and Koa | ||
# GraphQL Server for Express, Connect, Hapi, Koa, and Restify | ||
|
||
[![npm version](https://badge.fury.io/js/graphql-server.svg)](https://badge.fury.io/js/graphql-server) | ||
[![Build Status](https://travis-ci.org/apollostack/graphql-server.svg?branch=master)](https://travis-ci.org/apollostack/graphql-server) | ||
[![Coverage Status](https://coveralls.io/repos/github/apollostack/graphql-server/badge.svg?branch=master)](https://coveralls.io/github/apollostack/graphql-server?branch=master) | ||
[![Get on Slack](https://img.shields.io/badge/slack-join-orange.svg)](http://www.apollostack.com/#slack) | ||
|
||
GraphQL Server is a community-maintained open-source GraphQL server. It works with all Node.js HTTP server frameworks: Express, Connect, Hapi and Koa. | ||
GraphQL Server is a community-maintained open-source GraphQL server. It works with all Node.js HTTP server frameworks: Express, Connect, Hapi, Koa and Restify. | ||
|
||
## Principles | ||
|
||
|
@@ -122,6 +122,26 @@ app.use(router.allowedMethods()); | |
app.listen(PORT); | ||
``` | ||
|
||
### Restify | ||
```js | ||
import restify from 'restify'; | ||
import { graphqlRestify, graphiqlRestify } from 'graphql-server-restify'; | ||
|
||
const PORT = 3000; | ||
|
||
const server = restify.createServer({ | ||
title: 'GraphQL Server' | ||
}); | ||
|
||
server.use(restify.bodyParser()); | ||
|
||
server.post('/graphql', graphqlRestify({ schema: myGraphQLSchema })); | ||
|
||
sever.get('/graphiql', graphiqlRestify({ endpointURL: '/graphql' })); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fix typo |
||
|
||
server.listen(PORT, () => console.log(`Listening on ${PORT}`)); | ||
``` | ||
|
||
## Options | ||
|
||
GraphQL Server can be configured with an options object with the the following fields: | ||
|
@@ -156,7 +176,7 @@ graphqlOptions = { | |
|
||
GraphQL Server and express-graphql are more or less the same thing (GraphQL middleware for Node.js), but there are a few key differences: | ||
|
||
* express-graphql works with Express and Connect, GraphQL Server supports Express, Connect, Hapi and Koa. | ||
* express-graphql works with Express and Connect, GraphQL Server supports Express, Connect, Hapi, Koa and Restify. | ||
* express-graphql's main goal is to be a minimal reference implementation, whereas GraphQL Server's goal is to be a complete production-ready GraphQL server. | ||
* Compared to express-graphql, GraphQL Server has a simpler interface and supports exactly one way of passing queries. | ||
* GraphQL Server separates serving GraphiQL (GraphQL UI) from responding to GraphQL requests. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
* | ||
!dist | ||
!dist/**/* | ||
dist/**/*.test.* | ||
!package.json |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# graphql-server-restify | ||
|
||
This is the Restify integration for the Apollo community GraphQL Server. [Read the docs.](http://dev.apollodata.com/tools/apollo-server/index.html) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
{ | ||
"name": "graphql-server-restify", | ||
"version": "0.4.3", | ||
"description": "Production-ready Node.js GraphQL server for Restify", | ||
"main": "dist/index.js", | ||
"scripts": { | ||
"compile": "tsc", | ||
"prepublish": "npm run compile" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/apollostack/graphql-server/tree/master/packages/graphql-server-restify" | ||
}, | ||
"keywords": [ | ||
"GraphQL", | ||
"Apollo", | ||
"Server", | ||
"Restify", | ||
"Javascript" | ||
], | ||
"author": "Jonas Helfer <jonas@helfer.email>", | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "https://github.com/apollostack/graphql-server/issues" | ||
}, | ||
"homepage": "https://github.com/apollostack/graphql-server#readme", | ||
"dependencies": { | ||
"graphql-server-core": "^0.4.3", | ||
"graphql-server-module-graphiql": "^0.4.3" | ||
}, | ||
"devDependencies": { | ||
"@types/restify": "^2.0.33", | ||
"graphql-server-integration-testsuite": "^0.4.3", | ||
"restify": "^4.1.1", | ||
"typed-graphql": "^1.0.2" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be replaced with @types/graphql |
||
}, | ||
"peerDependencies": { | ||
"graphql": "^0.6.1 || ^0.7.0" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add || ^0.8.0 |
||
}, | ||
"optionalDependencies": { | ||
"@types/restify": "^2.0.33" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should add @types/graphql |
||
}, | ||
"typings": "dist/index.d.ts", | ||
"typescript": { | ||
"definition": "dist/index.d.ts" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import 'mocha'; | ||
import * as restify from 'restify'; | ||
import { expect } from 'chai'; | ||
import testSuite, { Schema, CreateAppOptions } from 'graphql-server-integration-testsuite'; | ||
import { GraphQLOptions } from 'graphql-server-core'; | ||
|
||
import { graphqlRestify, graphiqlRestify } from './'; | ||
|
||
// tslint:disable-next-line | ||
const request = require('supertest-as-promised'); | ||
|
||
function createApp(options: CreateAppOptions = {}) { | ||
const server = restify.createServer({ | ||
name: 'Restify Test Server', | ||
}); | ||
|
||
options.graphqlOptions = options.graphqlOptions || { schema: Schema }; | ||
if (!options.excludeParser) { | ||
server.use(restify.bodyParser()); | ||
} | ||
if (options.graphiqlOptions ) { | ||
server.get('/graphiql', graphiqlRestify( options.graphiqlOptions )); | ||
} | ||
server.post('/graphql', graphqlRestify( options.graphqlOptions )); | ||
server.put('/graphql', graphqlRestify( options.graphqlOptions )); | ||
|
||
return server; | ||
} | ||
|
||
describe('graphqlRestify', () => { | ||
it('throws error if called without schema', () => { | ||
expect(() => graphqlRestify(undefined as GraphQLOptions)).to.throw('Apollo Server requires options.'); | ||
}); | ||
|
||
it('throws an error if called with more than one argument', () => { | ||
expect(() => (<any>graphqlRestify)({}, 'x')).to.throw( | ||
'Apollo Server expects exactly one argument, got 2'); | ||
}); | ||
|
||
it('generates a function if the options are ok', () => { | ||
expect(() => graphqlRestify({ schema: Schema })).to.be.a('function'); | ||
}); | ||
|
||
it('throws an error if POST body is not an object or array', () => { | ||
const app = createApp(); | ||
const req = request(app) | ||
.post('/graphql') | ||
.send('123'); | ||
return req.then((res) => { | ||
expect(res.status).to.equal(500); | ||
return expect(res.error.text).to.contain('Invalid POST body sent'); | ||
}); | ||
}); | ||
|
||
it('throws an error on PUT calls', () => { | ||
const app = createApp(); | ||
const req = request(app) | ||
.put('/graphql') | ||
.send(); | ||
return req.then((res) => { | ||
expect(res.status).to.equal(405); | ||
return expect(res.error.text).to.contain('supports only POST requests'); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('integration:Restify', () => { | ||
testSuite(createApp); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import * as restify from 'restify'; | ||
import * as graphql from 'graphql'; | ||
import * as url from 'url'; | ||
import { GraphQLOptions, runQuery } from 'graphql-server-core'; | ||
import * as GraphiQL from 'graphql-server-module-graphiql'; | ||
|
||
export interface RestifyGraphQLOptionsFunction { | ||
(req?: restify.Request, res?: restify.Response): GraphQLOptions | Promise<GraphQLOptions>; | ||
} | ||
|
||
// Design principles: | ||
// - there is just one way allowed: POST request with JSON body. Nothing else. | ||
// - simple, fast and secure | ||
// | ||
|
||
export interface RestifyHandler { | ||
(req: restify.Request, res: restify.Response, next): void; | ||
} | ||
|
||
export function graphqlRestify(options: GraphQLOptions | RestifyGraphQLOptionsFunction): RestifyHandler { | ||
if (!options) { | ||
throw new Error('Apollo Server requires options.'); | ||
} | ||
|
||
if (arguments.length > 1) { | ||
// TODO: test this | ||
throw new Error(`Apollo Server expects exactly one argument, got ${arguments.length}`); | ||
} | ||
|
||
return async (req: restify.Request, res: restify.Response, next) => { | ||
let optionsObject: GraphQLOptions; | ||
if (isOptionsFunction(options)) { | ||
try { | ||
optionsObject = await options(req, res); | ||
} catch (e) { | ||
res.statusCode = 500; | ||
res.write(`Invalid options provided to ApolloServer: ${e.message}`); | ||
res.end(); | ||
} | ||
} else { | ||
optionsObject = options; | ||
} | ||
|
||
const formatErrorFn = optionsObject.formatError || graphql.formatError; | ||
|
||
if (req.method !== 'POST') { | ||
res.setHeader('Allow', 'POST'); | ||
res.statusCode = 405; | ||
res.write('graphql-server-restify supports only POST requests.'); | ||
res.end(); | ||
return; | ||
} | ||
|
||
if (!req.body) { | ||
res.statusCode = 500; | ||
res.write('POST body missing. Did you forget "server.use(restify.bodyParser());"?'); | ||
res.end(); | ||
return; | ||
} | ||
|
||
let b = req.body; | ||
let isBatch = true; | ||
let payloadType = typeof b; | ||
|
||
// Only arrays and object types are allowed | ||
if (payloadType !== 'object') { | ||
res.statusCode = 500; | ||
res.write('Invalid POST body sent, expected Array or Object, but saw: ' + payloadType); | ||
res.end(); | ||
return; | ||
} | ||
|
||
if (!Array.isArray(b)) { | ||
isBatch = false; | ||
b = [b]; | ||
} | ||
|
||
let responses: Array<graphql.GraphQLResult> = []; | ||
|
||
for (let requestParams of b) { | ||
try { | ||
const query = requestParams.query; | ||
const operationName = requestParams.operationName; | ||
let variables = requestParams.variables; | ||
|
||
if (typeof variables === 'string') { | ||
try { | ||
variables = JSON.parse(variables); | ||
} catch (error) { | ||
res.statusCode = 400; | ||
res.write('Variables are invalid JSON.'); | ||
res.end(); | ||
return; | ||
} | ||
} | ||
|
||
// Shallow clone context for queries in batches. This allows | ||
// users to distinguish multiple queries in the batch and to | ||
// modify the context object without interfering with each other. | ||
let context = optionsObject.context; | ||
if (isBatch) { | ||
context = Object.assign({}, context || {}); | ||
} | ||
|
||
let params = { | ||
schema: optionsObject.schema, | ||
query: query, | ||
variables: variables, | ||
context: context, | ||
rootValue: optionsObject.rootValue, | ||
operationName: operationName, | ||
logFunction: optionsObject.logFunction, | ||
validationRules: optionsObject.validationRules, | ||
formatError: formatErrorFn, | ||
formatResponse: optionsObject.formatResponse, | ||
debug: optionsObject.debug, | ||
}; | ||
|
||
if (optionsObject.formatParams) { | ||
params = optionsObject.formatParams(params); | ||
} | ||
|
||
responses.push(await runQuery(params)); | ||
} catch (e) { | ||
responses.push({ errors: [formatErrorFn(e)] }); | ||
} | ||
} | ||
|
||
res.setHeader('Content-Type', 'application/json'); | ||
|
||
if (isBatch) { | ||
res.write(JSON.stringify(responses)); | ||
res.end(); | ||
} else { | ||
const gqlResponse = responses[0]; | ||
if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') { | ||
res.statusCode = 400; | ||
} | ||
res.write(JSON.stringify(gqlResponse)); | ||
res.end(); | ||
} | ||
}; | ||
} | ||
|
||
function isOptionsFunction(arg: GraphQLOptions | RestifyGraphQLOptionsFunction): arg is RestifyGraphQLOptionsFunction { | ||
return typeof arg === 'function'; | ||
} | ||
|
||
/* This middleware returns the html for the GraphiQL interactive query UI | ||
* | ||
* GraphiQLData arguments | ||
* | ||
* - endpointURL: the relative or absolute URL for the endpoint which GraphiQL will make queries to | ||
* - (optional) query: the GraphQL query to pre-fill in the GraphiQL UI | ||
* - (optional) variables: a JS object of variables to pre-fill in the GraphiQL UI | ||
* - (optional) operationName: the operationName to pre-fill in the GraphiQL UI | ||
* - (optional) result: the result of the query to pre-fill in the GraphiQL UI | ||
*/ | ||
|
||
export function graphiqlRestify(options: GraphiQL.GraphiQLData) { | ||
return (req: restify.Request, res: restify.Response, next) => { | ||
const q = req.url && url.parse(req.url, true).query || {}; | ||
const query = q.query || ''; | ||
const variables = q.variables || '{}'; | ||
const operationName = q.operationName || ''; | ||
|
||
const graphiQLString = GraphiQL.renderGraphiQL({ | ||
endpointURL: options.endpointURL, | ||
query: query || options.query, | ||
variables: JSON.parse(variables) || options.variables, | ||
operationName: operationName || options.operationName, | ||
passHeader: options.passHeader, | ||
}); | ||
res.setHeader('Content-Type', 'text/html'); | ||
res.write(graphiQLString); | ||
res.end(); | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be
use
as now apollo supports GET requests as well. :)