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

Apollo koa integration #59

Merged
merged 5 commits into from
Jul 29, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"graphql": "^0.6.1",
"hapi": "^13.5.0",
"http-errors": "^1.5.0",
"koa": "^2.0.0-alpha.4",
"source-map-support": "^0.4.2"
},
"devDependencies": {
Expand All @@ -52,6 +53,8 @@
"body-parser": "^1.15.2",
"chai": "^3.5.0",
"istanbul": "1.0.0-alpha.2",
"koa-bodyparser": "^3.0.0",
"koa-router": "^7.0.1",
"mocha": "^2.5.3",
"multer": "^1.1.0",
"remap-istanbul": "^0.6.4",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { apolloExpress, graphiqlExpress } from './integrations/expressApollo';
export { ApolloHAPI, GraphiQLHAPI } from './integrations/hapiApollo';
export { apolloKoa } from './integrations/koaApollo';
4 changes: 3 additions & 1 deletion src/integrations/expressApollo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ function createApp(options: CreateAppOptions = {}) {
return app;
}

function destroyApp(app) {}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you should get rid of this and instead make the destroyApp argument optional. That way you only have to modify one existing test file.


describe('graphqlHTTP', () => {
it('returns express middleware', () => {
const middleware = apolloExpress({
Expand All @@ -49,5 +51,5 @@ describe('renderGraphiQL', () => {
});

describe('integration:Express', () => {
testSuite(createApp);
testSuite(createApp, destroyApp);
});
4 changes: 3 additions & 1 deletion src/integrations/hapiApollo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ function createApp(options: CreateAppOptions = {}) {
return server.listener;
}

function destroyApp(app) {}

describe('integration:HAPI', () => {
testSuite(createApp);
testSuite(createApp, destroyApp);
});
21 changes: 20 additions & 1 deletion src/integrations/integrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ export interface CreateAppFunc {
(options?: CreateAppOptions): void;
}

export default (createApp: CreateAppFunc) => {
export interface DestroyAppFunc {
(app: any): void;
}

export default (createApp: CreateAppFunc, destroyApp: DestroyAppFunc) => {
describe('apolloServer', () => {
describe('graphqlHTTP', () => {
it('can be called with an options function', () => {
Expand All @@ -80,6 +84,7 @@ export default (createApp: CreateAppFunc) => {
query: 'query test{ testString }',
});
return req.then((res) => {
destroyApp(app);
Copy link
Contributor

Choose a reason for hiding this comment

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

Did afterEach not work? That would be preferable to adding the call explicitly in every test, I think.

expect(res.status).to.equal(200);
return expect(res.body.data).to.deep.equal(expected);
});
Expand All @@ -100,6 +105,7 @@ export default (createApp: CreateAppFunc) => {
query: 'query test{ testString }',
});
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(200);
return expect(res.body.data).to.deep.equal(expected);
});
Expand All @@ -116,6 +122,7 @@ export default (createApp: CreateAppFunc) => {
query: 'query test{ testString }',
});
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(500);
return expect(res.error.text).to.contain(expected);
});
Expand All @@ -127,6 +134,7 @@ export default (createApp: CreateAppFunc) => {
.post('/graphql')
.send();
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(500);
return expect(res.error.text).to.contain('POST body missing.');
});
Expand All @@ -144,6 +152,7 @@ export default (createApp: CreateAppFunc) => {
query: 'query test{ testString }',
});
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(200);
return expect(res.body.data).to.deep.equal(expected);
});
Expand All @@ -161,6 +170,7 @@ export default (createApp: CreateAppFunc) => {
variables: { echo: 'world' },
});
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(200);
return expect(res.body.data).to.deep.equal(expected);
});
Expand All @@ -178,6 +188,7 @@ export default (createApp: CreateAppFunc) => {
variables: '{ "echo": "world" }',
});
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(200);
return expect(res.body.data).to.deep.equal(expected);
});
Expand All @@ -198,6 +209,7 @@ export default (createApp: CreateAppFunc) => {
operationName: 'test2',
});
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(200);
return expect(res.body.data).to.deep.equal(expected);
});
Expand Down Expand Up @@ -233,6 +245,7 @@ export default (createApp: CreateAppFunc) => {
operationName: 'testX',
}]);
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(200);
return expect(res.body).to.deep.equal(expected);
});
Expand All @@ -250,6 +263,7 @@ export default (createApp: CreateAppFunc) => {
variables: { echo: 'world' },
});
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(200);
return expect(res.body.data).to.deep.equal(expected);
});
Expand All @@ -270,6 +284,7 @@ export default (createApp: CreateAppFunc) => {
variables: { echo: 'world' },
});
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(200);
return expect(res.body.extensions).to.deep.equal(expected);
});
Expand All @@ -288,6 +303,7 @@ export default (createApp: CreateAppFunc) => {
query: 'query test{ testString }',
});
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(200);
return expect(results).to.equal(expected);
});
Expand All @@ -306,6 +322,7 @@ export default (createApp: CreateAppFunc) => {
.get('/graphiql?query={test}')
.set('Accept', 'text/html');
return req.then((response) => {
destroyApp(app);
expect(response.status).to.equal(200);
expect(response.type).to.equal('text/html');
expect(response.text).to.include('{test}');
Expand Down Expand Up @@ -333,6 +350,7 @@ export default (createApp: CreateAppFunc) => {
operationName: 'testquery',
});
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(200);
return expect(res.body.data).to.deep.equal(expected);
});
Expand Down Expand Up @@ -369,6 +387,7 @@ export default (createApp: CreateAppFunc) => {
query: '{ testString }',
}]);
return req.then((res) => {
destroyApp(app);
expect(res.status).to.equal(200);
return expect(res.body).to.deep.equal(expected);
});
Expand Down
66 changes: 66 additions & 0 deletions src/integrations/koaApollo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
assert,
expect,
} from 'chai';

// tslint:disable-next-line
const request = require('supertest-as-promised');

// tslint:disable-next-line
import * as koa from 'koa';
import * as koaRouter from 'koa-router';
import * as koaBody from 'koa-bodyparser';
import ApolloOptions from './apolloOptions';
import { apolloKoa, graphiqlKoa } from './koaApollo';

import { OperationStore } from '../modules/operationStore';
Copy link
Contributor

Choose a reason for hiding this comment

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

This import isn't used.

import testSuite, { Schema, CreateAppOptions } from './integrations.test';

function createApp(options: CreateAppOptions = {}) {
const app = new koa();
const router = new koaRouter();

options.apolloOptions = options.apolloOptions || { schema: Schema };

if (!options.excludeParser) {
app.use(koaBody());
}
if (options.graphiqlOptions ) {
router.get('/graphiql', graphiqlKoa( options.graphiqlOptions ));
}
router.post('/graphql', apolloKoa( options.apolloOptions ));
app.use(router.routes());
app.use(router.allowedMethods());
return app.listen(3000);
}

function destroyApp(app) {
app.close();
}

describe('graphqlHTTP', () => {
it('returns express middleware', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this test isn't necessary any more. We could also remove it from the express tests.

const middleware = apolloKoa({
schema: Schema,
});
assert.typeOf(middleware, 'function');
});
it('throws error if called without schema', () => {
expect(() => apolloKoa(undefined as ApolloOptions)).to.throw('Apollo Server requires options.');
});
});

describe('renderGraphiQL', () => {
it('returns express middleware', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Like the previous one, this is also not necessary. But if we keep it, it should be renamed.

const query = `{ testString }`;
const middleware = graphiqlKoa({
endpointURL: '/graphql',
query: query,
});
assert.typeOf(middleware, 'function');
});
});

describe('integration:Koa', () => {
testSuite(createApp, destroyApp);
});
130 changes: 130 additions & 0 deletions src/integrations/koaApollo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as koa from 'koa';
import * as koaBody from 'koa-bodyparser';
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused import according to tslint.

import * as graphql from 'graphql';
import { runQuery } from '../core/runQuery';

import ApolloOptions from './apolloOptions';
import * as GraphiQL from '../modules/renderGraphiQL';

export interface KoaApolloOptionsFunction {
(req: koa.Request): ApolloOptions | Promise<ApolloOptions>;
}

export interface KoaHandler {
(req: any, next): void;
}

export function apolloKoa(options: ApolloOptions | KoaApolloOptionsFunction): KoaHandler {
if (!options) {
throw new Error('Apollo Server requires options.');
}

if (arguments.length > 1) {
throw new Error(`Apollo Server expects exactly one argument, got ${arguments.length + 1}`);
}

return async (ctx, next) => {
let optionsObject: ApolloOptions;
if (isOptionsFunction(options)) {
try {
optionsObject = await options(ctx.request);
} catch (e) {
ctx.status = 500;
return ctx.body = `Invalid options provided to ApolloServer: ${e.message}`;
}
} else {
optionsObject = options;
}

const formatErrorFn = optionsObject.formatError || graphql.formatError;

if (ctx.method !== 'POST') {
ctx.set('Allow', 'POST');
ctx.status = 405;
return ctx.body = 'Apollo Server supports only POST requests.';
}

if (!ctx.request.body) {
ctx.status = 500;
return ctx.body = 'POST body missing. Did you forget "app.use(koaBody())"?';
}

let b = ctx.request.body;
let isBatch = true;
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') {
// TODO: catch errors
variables = JSON.parse(variables);
}

let params = {
schema: optionsObject.schema,
query: query,
variables: variables,
context: optionsObject.context,
rootValue: optionsObject.rootValue,
operationName: operationName,
logFunction: optionsObject.logFunction,
validationRules: optionsObject.validationRules,
formatError: formatErrorFn,
formatResponse: optionsObject.formatResponse,
};

if (optionsObject.formatParams) {
params = optionsObject.formatParams(params);
}

responses.push(await runQuery(params));
} catch (e) {
responses.push({ errors: [formatErrorFn(e)] });
}
}

ctx.set('Content-Type', 'application/json');
if (isBatch) {
return ctx.body = JSON.stringify(responses);
} else {
const gqlResponse = responses[0];
if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') {
ctx.status = 400;
}
return ctx.body = JSON.stringify(gqlResponse);
}

};
}

function isOptionsFunction(arg: ApolloOptions | KoaApolloOptionsFunction): arg is KoaApolloOptionsFunction {
return typeof arg === 'function';
}

export function graphiqlKoa(options: GraphiQL.GraphiQLData) {
return (ctx, next) => {

const q = ctx.request.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,
});
ctx.set('Content-Type', 'text/html');
ctx.body = graphiQLString;
};
}
1 change: 1 addition & 0 deletions src/test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ import '../core/runQuery.test';
import '../modules/operationStore.test';
import '../integrations/expressApollo.test';
import '../integrations/hapiApollo.test';
import '../integrations/koaApollo.test';
import './testApolloServerHTTP';
5 changes: 4 additions & 1 deletion typings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
},
"globalDependencies": {
"body-parser": "registry:dt/body-parser#0.0.0+20160619023215",
"cookies": "registry:dt/cookies#0.5.1+20160316171810",
Copy link
Contributor

Choose a reason for hiding this comment

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

Where do we use this? I can't find any reference in the code. Is this something that Koa requires?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess this is needed by koa typings. If I remove it, I get this error

import * as cookies from "cookies";

typings/globals/koa/index.d.ts(5,30): error TS2307: Cannot find module 'cookies'.

I am a typescript beginner so I might be doing something wrong. @nnance can you please check this?

"express": "registry:dt/express#4.0.0+20160708185218",
"express-serve-static-core": "registry:dt/express-serve-static-core#4.0.0+20160715232503",
"hapi": "registry:dt/hapi#13.0.0+20160709092105",
"koa": "registry:dt/koa#2.0.0+20160619030120",
"koa": "registry:dt/koa#2.0.0+20160724024233",
"koa-bodyparser": "registry:dt/koa-bodyparser#3.0.0+20160414124440",
"koa-router": "registry:dt/koa-router#7.0.0+20160314083221",
"mime": "registry:dt/mime#0.0.0+20160316155526",
"mocha": "registry:dt/mocha#2.2.5+20160720003353",
"multer": "registry:dt/multer#0.0.0+20160317120654",
Expand Down