Skip to content
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

Merged
merged 8 commits into from
Jan 24, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### VNEXT

* Restify integration and documents/tests.

### v0.4.2

* **Restructure Apollo Server into 6 new packages, and rename to GraphQL Server** ([@DxCx](https://github.com/DxCx)) and ([@stubailo](https://github.com/stubailo)) in [#183](https://github.com/apollostack/graphql-server/pull/183) and [#164](https://github.com/apollostack/graphql-server/pull/183).
Expand Down
1 change: 1 addition & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ GraphQL Server should come with a set of integrations for different Node.js serv
- Hapi
- Connect
- Koa
- Restify
- ...

Framework integrations take care of parsing requests, submitting them to GraphQL Server’s core runQuery function, and sending the response back to the client. These integrations should accept requests over HTTP, websockets or other means, then invoke `runQuery` as appropriate, and return the result. They should be written in such a way that makes it easy to add features, such as batched queries, subscriptions etc.
Expand Down
26 changes: 23 additions & 3 deletions README.md
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

Expand Down Expand Up @@ -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 }));
Copy link
Contributor

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. :)


sever.get('/graphiql', graphiqlRestify({ endpointURL: '/graphql' }));
Copy link
Contributor

Choose a reason for hiding this comment

The 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:
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions packages/graphql-server-restify/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*
!dist
!dist/**/*
dist/**/*.test.*
!package.json
3 changes: 3 additions & 0 deletions packages/graphql-server-restify/README.md
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)
47 changes: 47 additions & 0 deletions packages/graphql-server-restify/package.json
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"
Copy link
Contributor

Choose a reason for hiding this comment

The 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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add || ^0.8.0

},
"optionalDependencies": {
"@types/restify": "^2.0.33"
Copy link
Contributor

Choose a reason for hiding this comment

The 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"
}
}
69 changes: 69 additions & 0 deletions packages/graphql-server-restify/src/index.test.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);
});
178 changes: 178 additions & 0 deletions packages/graphql-server-restify/src/index.ts
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();
};
}
Loading