-
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
Apollo koa integration #59
Changes from 1 commit
978270f
5fa12db
bc587b7
925a307
d40ce2a
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,2 +1,3 @@ | ||
export { apolloExpress, graphiqlExpress } from './integrations/expressApollo'; | ||
export { ApolloHAPI, GraphiQLHAPI } from './integrations/hapiApollo'; | ||
export { apolloKoa } from './integrations/koaApollo'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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', () => { | ||
|
@@ -80,6 +84,7 @@ export default (createApp: CreateAppFunc) => { | |
query: 'query test{ testString }', | ||
}); | ||
return req.then((res) => { | ||
destroyApp(app); | ||
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. Did |
||
expect(res.status).to.equal(200); | ||
return expect(res.body.data).to.deep.equal(expected); | ||
}); | ||
|
@@ -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); | ||
}); | ||
|
@@ -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); | ||
}); | ||
|
@@ -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.'); | ||
}); | ||
|
@@ -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); | ||
}); | ||
|
@@ -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); | ||
}); | ||
|
@@ -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); | ||
}); | ||
|
@@ -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); | ||
}); | ||
|
@@ -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); | ||
}); | ||
|
@@ -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); | ||
}); | ||
|
@@ -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); | ||
}); | ||
|
@@ -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); | ||
}); | ||
|
@@ -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}'); | ||
|
@@ -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); | ||
}); | ||
|
@@ -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); | ||
}); | ||
|
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'; | ||
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. 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', () => { | ||
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. 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', () => { | ||
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. 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); | ||
}); |
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'; | ||
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. 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; | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,10 +6,13 @@ | |
}, | ||
"globalDependencies": { | ||
"body-parser": "registry:dt/body-parser#0.0.0+20160619023215", | ||
"cookies": "registry:dt/cookies#0.5.1+20160316171810", | ||
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. Where do we use this? I can't find any reference in the code. Is this something that Koa requires? 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. I guess this is needed by koa typings. If I remove it, I get this error
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", | ||
|
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.
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.