From 3b64f0e3ae407d1d713db9f8a1304ee73bb01f60 Mon Sep 17 00:00:00 2001 From: nnance Date: Thu, 1 Sep 2016 05:29:03 -0700 Subject: [PATCH 01/14] convert hapi plugin classes to functions --- README.md | 2 +- src/integrations/hapiApollo.test.ts | 4 +- src/integrations/hapiApollo.ts | 144 +++++++++++++--------------- 3 files changed, 72 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index cebacf7604e..e86e6e1c7c1 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ server.connection({ }); server.register({ - register: new ApolloHAPI(), + register: ApolloHAPI, options: { schema: myGraphQLSchema }, routes: { prefix: '/graphql' }, }); diff --git a/src/integrations/hapiApollo.test.ts b/src/integrations/hapiApollo.test.ts index ca0df5d121d..8c87a109a72 100644 --- a/src/integrations/hapiApollo.test.ts +++ b/src/integrations/hapiApollo.test.ts @@ -14,13 +14,13 @@ function createApp(options: CreateAppOptions = {}) { options.apolloOptions = options.apolloOptions || { schema: Schema }; server.register({ - register: new ApolloHAPI(), + register: ApolloHAPI, options: options.apolloOptions, routes: { prefix: '/graphql' }, }); server.register({ - register: new GraphiQLHAPI(), + register: GraphiQLHAPI, options: { endpointURL: '/graphql' }, routes: { prefix: '/graphiql' }, }); diff --git a/src/integrations/hapiApollo.ts b/src/integrations/hapiApollo.ts index cba1d143e85..0039e175f4e 100644 --- a/src/integrations/hapiApollo.ts +++ b/src/integrations/hapiApollo.ts @@ -14,84 +14,76 @@ export interface HAPIOptionsFunction { (req?: hapi.Request): ApolloOptions | Promise; } -export class ApolloHAPI { - constructor() { - this.register.attributes = { - name: 'graphql', - version: '0.0.1', - }; - } +const ApolloHAPI: IRegister = function(server: hapi.Server, options: ApolloOptions | HAPIOptionsFunction, next) { + server.route({ + method: 'POST', + path: '/', + handler: async (request, reply) => { + let optionsObject: ApolloOptions; + if (isOptionsFunction(options)) { + try { + optionsObject = await options(request); + } catch (e) { + reply(`Invalid options provided to ApolloServer: ${e.message}`).code(500); + } + } else { + optionsObject = options; + } - public register: IRegister = (server: hapi.Server, options: ApolloOptions | HAPIOptionsFunction, next) => { - server.route({ - method: 'POST', - path: '/', - handler: async (request, reply) => { - let optionsObject: ApolloOptions; - if (isOptionsFunction(options)) { - try { - optionsObject = await options(request); - } catch (e) { - reply(`Invalid options provided to ApolloServer: ${e.message}`).code(500); - } - } else { - optionsObject = options; - } - - if (!request.payload) { - reply('POST body missing.').code(500); - return; - } - - const responses = await processQuery(request.payload, optionsObject); - - if (responses.length > 1) { - reply(responses); - } else { - const gqlResponse = responses[0]; - if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') { - reply(gqlResponse).code(400); - } else { - reply(gqlResponse); - } - } - - }, - }); - next(); - } -} + if (!request.payload) { + reply('POST body missing.').code(500); + return; + } -export class GraphiQLHAPI { - constructor() { - this.register.attributes = { - name: 'graphiql', - version: '0.0.1', - }; - } + const responses = await processQuery(request.payload, optionsObject); + + if (responses.length > 1) { + reply(responses); + } else { + const gqlResponse = responses[0]; + if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') { + reply(gqlResponse).code(400); + } else { + reply(gqlResponse); + } + } - public register: IRegister = (server: hapi.Server, options: GraphiQL.GraphiQLData, next) => { - server.route({ - method: 'GET', - path: '/', - handler: (request, reply) => { - const q = 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, - }); - reply(graphiQLString).header('Content-Type', 'text/html'); - }, - }); - next(); - } -} + }, + }); + next(); +}; + +ApolloHAPI.attributes = { + name: 'graphql', + version: '0.0.1', +}; + +const GraphiQLHAPI: IRegister = function(server: hapi.Server, options: GraphiQL.GraphiQLData, next) { + server.route({ + method: 'GET', + path: '/', + handler: (request, reply) => { + const q = 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, + }); + reply(graphiQLString).header('Content-Type', 'text/html'); + }, + }); + next(); +}; + +GraphiQLHAPI.attributes = { + name: 'graphiql', + version: '0.0.1', +}; async function processQuery(body, optionsObject) { const formatErrorFn = optionsObject.formatError || graphql.formatError; @@ -143,3 +135,5 @@ async function processQuery(body, optionsObject) { function isOptionsFunction(arg: ApolloOptions | HAPIOptionsFunction): arg is HAPIOptionsFunction { return typeof arg === 'function'; } + +export { ApolloHAPI, GraphiQLHAPI }; From 2b620065c3af018dd30986ed175133fefdde684c Mon Sep 17 00:00:00 2001 From: nnance Date: Thu, 1 Sep 2016 05:32:22 -0700 Subject: [PATCH 02/14] improve handler --- src/integrations/hapiApollo.ts | 69 +++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/src/integrations/hapiApollo.ts b/src/integrations/hapiApollo.ts index 0039e175f4e..56fad977dae 100644 --- a/src/integrations/hapiApollo.ts +++ b/src/integrations/hapiApollo.ts @@ -17,37 +17,12 @@ export interface HAPIOptionsFunction { const ApolloHAPI: IRegister = function(server: hapi.Server, options: ApolloOptions | HAPIOptionsFunction, next) { server.route({ method: 'POST', - path: '/', - handler: async (request, reply) => { - let optionsObject: ApolloOptions; - if (isOptionsFunction(options)) { - try { - optionsObject = await options(request); - } catch (e) { - reply(`Invalid options provided to ApolloServer: ${e.message}`).code(500); - } - } else { - optionsObject = options; - } - - if (!request.payload) { - reply('POST body missing.').code(500); - return; - } - - const responses = await processQuery(request.payload, optionsObject); - - if (responses.length > 1) { - reply(responses); - } else { - const gqlResponse = responses[0]; - if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') { - reply(gqlResponse).code(400); - } else { - reply(gqlResponse); - } - } - + path: '/graphql', + config: { + pre: [{ + assign: 'graphQL', + method: 'processQueryRequest(payload, pre.options)', + }], }, }); next(); @@ -85,6 +60,38 @@ GraphiQLHAPI.attributes = { version: '0.0.1', }; +async function processQueryRequest(request, reply) { + let optionsObject: ApolloOptions; + if (isOptionsFunction(options)) { + try { + optionsObject = await options(request); + } catch (e) { + reply(`Invalid options provided to ApolloServer: ${e.message}`).code(500); + } + } else { + optionsObject = options; + } + + if (!request.payload) { + reply('POST body missing.').code(500); + return; + } + + const responses = await processQuery(request.payload, optionsObject); + + if (responses.length > 1) { + reply(responses); + } else { + const gqlResponse = responses[0]; + if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') { + reply(gqlResponse).code(400); + } else { + reply(gqlResponse); + } + } + +} + async function processQuery(body, optionsObject) { const formatErrorFn = optionsObject.formatError || graphql.formatError; From e456c7ca6955e6256ad7438bc95243a881567f31 Mon Sep 17 00:00:00 2001 From: Nick Nance Date: Fri, 2 Sep 2016 14:07:31 -0700 Subject: [PATCH 03/14] create Hapi plugin options --- src/integrations/hapiApollo.ts | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/integrations/hapiApollo.ts b/src/integrations/hapiApollo.ts index 56fad977dae..e228c5f931a 100644 --- a/src/integrations/hapiApollo.ts +++ b/src/integrations/hapiApollo.ts @@ -14,14 +14,39 @@ export interface HAPIOptionsFunction { (req?: hapi.Request): ApolloOptions | Promise; } -const ApolloHAPI: IRegister = function(server: hapi.Server, options: ApolloOptions | HAPIOptionsFunction, next) { +export interface HAPIPluginOptions { + path: string; + apolloOptions: ApolloOptions | HAPIOptionsFunction; +} + +const ApolloHAPI: IRegister = function(server: hapi.Server, options: HAPIPluginOptions, next) { + server.method('processQuery', processQuery); + server.route({ method: 'POST', - path: '/graphql', + path: options.path || '/graphql', config: { pre: [{ + async method(request, reply) { + let optionsObject: ApolloOptions; + if (isOptionsFunction(options.apolloOptions)) { + try { + optionsObject = await options.apolloOptions(request); + } catch (e) { + reply(`Invalid options provided to ApolloServer: ${e.message}`).code(500); + } + } else { + optionsObject = options.apolloOptions; + } + + if (!request.payload) { + reply('POST body missing.').code(500); + return; + } + }, + }, { assign: 'graphQL', - method: 'processQueryRequest(payload, pre.options)', + method: 'processQuery(payload, pre.options)', }], }, }); From 9af61066a4ed266a30b837ec2d742ad5b33855b7 Mon Sep 17 00:00:00 2001 From: nnance Date: Tue, 6 Sep 2016 08:24:57 -0500 Subject: [PATCH 04/14] basic requests working --- src/integrations/hapiApollo.test.ts | 14 +-- src/integrations/hapiApollo.ts | 162 +++++++++++++--------------- 2 files changed, 82 insertions(+), 94 deletions(-) diff --git a/src/integrations/hapiApollo.test.ts b/src/integrations/hapiApollo.test.ts index 8c87a109a72..3a2aca3a381 100644 --- a/src/integrations/hapiApollo.test.ts +++ b/src/integrations/hapiApollo.test.ts @@ -1,9 +1,9 @@ import * as hapi from 'hapi'; -import { ApolloHAPI, GraphiQLHAPI } from './hapiApollo'; +import { ApolloHAPI, GraphiQLHAPI, HAPIPluginOptions } from './hapiApollo'; -import testSuite, { Schema, CreateAppOptions } from './integrations.test'; +import testSuite, { Schema } from './integrations.test'; -function createApp(options: CreateAppOptions = {}) { +function createApp(createOptions: HAPIPluginOptions) { const server = new hapi.Server(); server.connection({ @@ -11,12 +11,12 @@ function createApp(options: CreateAppOptions = {}) { port: 8000, }); - options.apolloOptions = options.apolloOptions || { schema: Schema }; - server.register({ register: ApolloHAPI, - options: options.apolloOptions, - routes: { prefix: '/graphql' }, + options: { + apolloOptions: createOptions ? createOptions.apolloOptions : { schema: Schema }, + path: '/graphql', + }, }); server.register({ diff --git a/src/integrations/hapiApollo.ts b/src/integrations/hapiApollo.ts index e228c5f931a..c8d402c1d82 100644 --- a/src/integrations/hapiApollo.ts +++ b/src/integrations/hapiApollo.ts @@ -20,6 +20,7 @@ export interface HAPIPluginOptions { } const ApolloHAPI: IRegister = function(server: hapi.Server, options: HAPIPluginOptions, next) { + server.method('getApolloOptions', getApolloOptions); server.method('processQuery', processQuery); server.route({ @@ -27,112 +28,72 @@ const ApolloHAPI: IRegister = function(server: hapi.Server, options: HAPIPluginO path: options.path || '/graphql', config: { pre: [{ - async method(request, reply) { - let optionsObject: ApolloOptions; - if (isOptionsFunction(options.apolloOptions)) { - try { - optionsObject = await options.apolloOptions(request); - } catch (e) { - reply(`Invalid options provided to ApolloServer: ${e.message}`).code(500); - } - } else { - optionsObject = options.apolloOptions; - } - - if (!request.payload) { - reply('POST body missing.').code(500); - return; - } - }, + assign: 'apolloOptions', + method: 'getApolloOptions', }, { assign: 'graphQL', - method: 'processQuery(payload, pre.options)', + method: 'processQuery(payload, pre.apolloOptions)', }], + handler: function (request, reply) { + const responses = request.pre.graphQL; + if (responses.length > 1) { + return reply(responses); + } else { + const gqlResponse = responses[0]; + if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') { + return reply(gqlResponse).code(400); + } else { + return reply(gqlResponse); + } + } + }, }, }); - next(); -}; - -ApolloHAPI.attributes = { - name: 'graphql', - version: '0.0.1', -}; - -const GraphiQLHAPI: IRegister = function(server: hapi.Server, options: GraphiQL.GraphiQLData, next) { - server.route({ - method: 'GET', - path: '/', - handler: (request, reply) => { - const q = 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, - }); - reply(graphiQLString).header('Content-Type', 'text/html'); - }, - }); - next(); -}; - -GraphiQLHAPI.attributes = { - name: 'graphiql', - version: '0.0.1', -}; - -async function processQueryRequest(request, reply) { - let optionsObject: ApolloOptions; - if (isOptionsFunction(options)) { - try { - optionsObject = await options(request); - } catch (e) { - reply(`Invalid options provided to ApolloServer: ${e.message}`).code(500); + return next(); + + async function getApolloOptions(request: hapi.Request, reply: hapi.IReply): Promise<{}> { + let optionsObject: ApolloOptions; + if (isOptionsFunction(options.apolloOptions)) { + try { + const opsFunc: HAPIOptionsFunction = options.apolloOptions; + optionsObject = await opsFunc(request); + } catch (e) { + return reply(new Error(`Invalid options provided to ApolloServer: ${e.message}`)); + } + } else { + optionsObject = options.apolloOptions; } - } else { - optionsObject = options; - } - if (!request.payload) { - reply('POST body missing.').code(500); - return; - } - - const responses = await processQuery(request.payload, optionsObject); - - if (responses.length > 1) { - reply(responses); - } else { - const gqlResponse = responses[0]; - if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') { - reply(gqlResponse).code(400); + if (!request.payload) { + return reply(new Error('POST body missing.')); } else { - reply(gqlResponse); + return reply(optionsObject); } } -} +}; -async function processQuery(body, optionsObject) { +ApolloHAPI.attributes = { + name: 'graphql', + version: '0.0.1', +}; + +async function processQuery(payload, optionsObject: ApolloOptions, reply) { const formatErrorFn = optionsObject.formatError || graphql.formatError; let isBatch = true; // TODO: do something different here if the body is an array. // Throw an error if body isn't either array or object. - if (!Array.isArray(body)) { + if (!Array.isArray(payload)) { isBatch = false; - body = [body]; + payload = [payload]; } - let responses: Array = []; - for (let payload of body) { + let responses: graphql.GraphQLResult[] = []; + for (let query of payload) { try { - const operationName = payload.operationName; - let variables = payload.variables; + const operationName = query.operationName; + let variables = query.variables; if (typeof variables === 'string') { // TODO: catch errors @@ -141,7 +102,7 @@ async function processQuery(body, optionsObject) { let params = { schema: optionsObject.schema, - query: payload.query, + query: query.query, variables: variables, rootValue: optionsObject.rootValue, context: optionsObject.context, @@ -161,11 +122,38 @@ async function processQuery(body, optionsObject) { responses.push({ errors: [formatErrorFn(e)] }); } } - return responses; + return reply(responses); } function isOptionsFunction(arg: ApolloOptions | HAPIOptionsFunction): arg is HAPIOptionsFunction { return typeof arg === 'function'; } +const GraphiQLHAPI: IRegister = function(server: hapi.Server, options: GraphiQL.GraphiQLData, next) { + server.route({ + method: 'GET', + path: '/', + handler: (request, reply) => { + const q = 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, + }); + reply(graphiQLString).header('Content-Type', 'text/html'); + }, + }); + next(); +}; + +GraphiQLHAPI.attributes = { + name: 'graphiql', + version: '0.0.1', +}; + export { ApolloHAPI, GraphiQLHAPI }; From 3c9add169c3e58e9e30d59c0f3e2b9c8a74bbbf3 Mon Sep 17 00:00:00 2001 From: nnance Date: Wed, 7 Sep 2016 11:03:28 -0500 Subject: [PATCH 05/14] improve error handling --- package.json | 2 + src/integrations/hapiApollo.ts | 69 ++++++++++++++++++++-------------- typings.json | 2 + 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 8b9b5c88781..cda37bf1e10 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "babel-polyfill": "^6.9.1", "babel-preset-es2015": "^6.9.0", "body-parser": "^1.15.2", + "boom": "^4.0.0", "chai": "^3.5.0", "connect": "^3.4.1", "express": "^4.14.0", @@ -53,6 +54,7 @@ "graphql": "^0.7.0", "hapi": "^15.0.3", "istanbul": "1.0.0-alpha.2", + "joi": "^9.0.4", "koa": "^2.0.0-alpha.4", "koa-bodyparser": "^3.0.0", "koa-router": "^7.0.1", diff --git a/src/integrations/hapiApollo.ts b/src/integrations/hapiApollo.ts index c8d402c1d82..1c9ccb326fc 100644 --- a/src/integrations/hapiApollo.ts +++ b/src/integrations/hapiApollo.ts @@ -1,17 +1,18 @@ -import * as hapi from 'hapi'; -import * as graphql from 'graphql'; +import * as Boom from 'boom'; +import { Server, Request, IReply } from 'hapi'; +import { GraphQLResult, formatError } from 'graphql'; import * as GraphiQL from '../modules/renderGraphiQL'; import { runQuery } from '../core/runQuery'; import ApolloOptions from './apolloOptions'; export interface IRegister { - (server: hapi.Server, options: any, next: any): void; + (server: Server, options: any, next: any): void; attributes?: any; } export interface HAPIOptionsFunction { - (req?: hapi.Request): ApolloOptions | Promise; + (req?: Request): ApolloOptions | Promise; } export interface HAPIPluginOptions { @@ -19,7 +20,7 @@ export interface HAPIPluginOptions { apolloOptions: ApolloOptions | HAPIOptionsFunction; } -const ApolloHAPI: IRegister = function(server: hapi.Server, options: HAPIPluginOptions, next) { +const ApolloHAPI: IRegister = function(server: Server, options: HAPIPluginOptions, next) { server.method('getApolloOptions', getApolloOptions); server.method('processQuery', processQuery); @@ -27,6 +28,11 @@ const ApolloHAPI: IRegister = function(server: hapi.Server, options: HAPIPluginO method: 'POST', path: options.path || '/graphql', config: { + plugins: { + graphql: { + options, + }, + }, pre: [{ assign: 'apolloOptions', method: 'getApolloOptions', @@ -51,26 +57,6 @@ const ApolloHAPI: IRegister = function(server: hapi.Server, options: HAPIPluginO }); return next(); - async function getApolloOptions(request: hapi.Request, reply: hapi.IReply): Promise<{}> { - let optionsObject: ApolloOptions; - if (isOptionsFunction(options.apolloOptions)) { - try { - const opsFunc: HAPIOptionsFunction = options.apolloOptions; - optionsObject = await opsFunc(request); - } catch (e) { - return reply(new Error(`Invalid options provided to ApolloServer: ${e.message}`)); - } - } else { - optionsObject = options.apolloOptions; - } - - if (!request.payload) { - return reply(new Error('POST body missing.')); - } else { - return reply(optionsObject); - } - } - }; ApolloHAPI.attributes = { @@ -78,8 +64,29 @@ ApolloHAPI.attributes = { version: '0.0.1', }; +async function getApolloOptions(request: Request, reply: IReply): Promise<{}> { + const options = request.route.settings.plugins['graphql'].options; + let optionsObject: ApolloOptions; + if (isOptionsFunction(options.apolloOptions)) { + try { + const opsFunc: HAPIOptionsFunction = options.apolloOptions; + optionsObject = await opsFunc(request); + } catch (e) { + return reply(createErr(500, `Invalid options provided to ApolloServer: ${e.message}`)); + } + } else { + optionsObject = options.apolloOptions; + } + + if (!request.payload) { + return reply(createErr(500, 'POST body missing.')); + } else { + return reply(optionsObject); + } +} + async function processQuery(payload, optionsObject: ApolloOptions, reply) { - const formatErrorFn = optionsObject.formatError || graphql.formatError; + const formatErrorFn = optionsObject.formatError || formatError; let isBatch = true; // TODO: do something different here if the body is an array. @@ -89,7 +96,7 @@ async function processQuery(payload, optionsObject: ApolloOptions, reply) { payload = [payload]; } - let responses: graphql.GraphQLResult[] = []; + let responses: GraphQLResult[] = []; for (let query of payload) { try { const operationName = query.operationName; @@ -129,7 +136,13 @@ function isOptionsFunction(arg: ApolloOptions | HAPIOptionsFunction): arg is HAP return typeof arg === 'function'; } -const GraphiQLHAPI: IRegister = function(server: hapi.Server, options: GraphiQL.GraphiQLData, next) { +function createErr(code: number, message: string) { + const err = Boom.create(code); + err.output.payload.message = message; + return err; +} + +const GraphiQLHAPI: IRegister = function(server: Server, options: GraphiQL.GraphiQLData, next) { server.route({ method: 'GET', path: '/', diff --git a/typings.json b/typings.json index ddc134108c2..8e4dadd2776 100644 --- a/typings.json +++ b/typings.json @@ -6,12 +6,14 @@ }, "globalDependencies": { "body-parser": "registry:dt/body-parser#0.0.0+20160619023215", + "boom": "registry:dt/boom#0.0.0+20160724101333", "connect": "registry:dt/connect#3.4.0+20160317120654", "cookies": "registry:dt/cookies#0.5.1+20160316171810", "express": "registry:dt/express#4.0.0+20160708185218", "express-serve-static-core": "registry:dt/express-serve-static-core#4.0.0+20160805091045", "fibers": "registry:dt/fibers#0.0.0+20160317120654", "hapi": "registry:dt/hapi#13.0.0+20160803202811", + "joi": "registry:dt/joi#9.0.0+20160906140128", "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", From 3a73fcc9ea62f02e5b56afa10be96fd2befaf0ff Mon Sep 17 00:00:00 2001 From: nnance Date: Wed, 7 Sep 2016 11:32:02 -0500 Subject: [PATCH 06/14] working batch query --- src/integrations/hapiApollo.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/integrations/hapiApollo.ts b/src/integrations/hapiApollo.ts index 1c9ccb326fc..20bdfc01e71 100644 --- a/src/integrations/hapiApollo.ts +++ b/src/integrations/hapiApollo.ts @@ -22,6 +22,7 @@ export interface HAPIPluginOptions { const ApolloHAPI: IRegister = function(server: Server, options: HAPIPluginOptions, next) { server.method('getApolloOptions', getApolloOptions); + server.method('checkForBatch', checkForBatch); server.method('processQuery', processQuery); server.route({ @@ -36,13 +37,16 @@ const ApolloHAPI: IRegister = function(server: Server, options: HAPIPluginOption pre: [{ assign: 'apolloOptions', method: 'getApolloOptions', + }, { + assign: 'isBatch', + method: 'checkForBatch(payload)', }, { assign: 'graphQL', - method: 'processQuery(payload, pre.apolloOptions)', + method: 'processQuery(payload, pre.isBatch, pre.apolloOptions)', }], handler: function (request, reply) { const responses = request.pre.graphQL; - if (responses.length > 1) { + if (request.pre.isBatch) { return reply(responses); } else { const gqlResponse = responses[0]; @@ -85,14 +89,16 @@ async function getApolloOptions(request: Request, reply: IReply): Promise<{}> { } } -async function processQuery(payload, optionsObject: ApolloOptions, reply) { +function checkForBatch(payload, reply) { + return reply(payload && Array.isArray(payload)); +} + +async function processQuery(payload, isBatch = false, optionsObject: ApolloOptions, reply) { const formatErrorFn = optionsObject.formatError || formatError; - let isBatch = true; // TODO: do something different here if the body is an array. // Throw an error if body isn't either array or object. - if (!Array.isArray(payload)) { - isBatch = false; + if (!isBatch) { payload = [payload]; } From f820d97f76bb4002f7a5721bfd0c66f934ef480e Mon Sep 17 00:00:00 2001 From: nnance Date: Wed, 7 Sep 2016 23:19:10 -0500 Subject: [PATCH 07/14] add route config options --- package.json | 1 - src/integrations/hapiApollo.ts | 137 +++++++++++++++++++-------------- typings.json | 1 - 3 files changed, 78 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index cda37bf1e10..ffe6ae5daa7 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "graphql": "^0.7.0", "hapi": "^15.0.3", "istanbul": "1.0.0-alpha.2", - "joi": "^9.0.4", "koa": "^2.0.0-alpha.4", "koa-bodyparser": "^3.0.0", "koa-router": "^7.0.1", diff --git a/src/integrations/hapiApollo.ts b/src/integrations/hapiApollo.ts index 20bdfc01e71..56e65fa8189 100644 --- a/src/integrations/hapiApollo.ts +++ b/src/integrations/hapiApollo.ts @@ -17,50 +17,57 @@ export interface HAPIOptionsFunction { export interface HAPIPluginOptions { path: string; + route?: any; apolloOptions: ApolloOptions | HAPIOptionsFunction; } const ApolloHAPI: IRegister = function(server: Server, options: HAPIPluginOptions, next) { + const config = Object.assign(options.route || {}, { + plugins: { + graphql: { + options, + }, + }, + pre: [{ + assign: 'isBatch', + method: 'verifyPayload(payload)', + }, { + assign: 'graphqlParams', + method: 'getGraphQLParams(payload, pre.isBatch)', + }, { + assign: 'apolloOptions', + method: 'getApolloOptions', + }, { + assign: 'graphQL', + method: 'processQuery(pre.graphqlParams, pre.apolloOptions)', + }], + }); + + server.method('verifyPayload', verifyPayload); + server.method('getGraphQLParams', getGraphQLParams); server.method('getApolloOptions', getApolloOptions); - server.method('checkForBatch', checkForBatch); server.method('processQuery', processQuery); server.route({ method: 'POST', path: options.path || '/graphql', - config: { - plugins: { - graphql: { - options, - }, - }, - pre: [{ - assign: 'apolloOptions', - method: 'getApolloOptions', - }, { - assign: 'isBatch', - method: 'checkForBatch(payload)', - }, { - assign: 'graphQL', - method: 'processQuery(payload, pre.isBatch, pre.apolloOptions)', - }], - handler: function (request, reply) { - const responses = request.pre.graphQL; - if (request.pre.isBatch) { - return reply(responses); - } else { - const gqlResponse = responses[0]; - if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') { - return reply(gqlResponse).code(400); - } else { - return reply(gqlResponse); - } - } - }, + config, + handler: function(request, reply) { + const responses = request.pre.graphQL; + if (request.pre.isBatch) { + return reply(responses); + } else { + const gqlResponse = responses[0]; + if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') { + return reply(gqlResponse).code(400); + } else { + return reply(gqlResponse); + } + } }, }); - return next(); + return next(); }; ApolloHAPI.attributes = { @@ -68,6 +75,41 @@ ApolloHAPI.attributes = { version: '0.0.1', }; +function verifyPayload(payload, reply) { + if (!payload) { + return reply(createErr(500, 'POST body missing.')); + } + + // TODO: do something different here if the body is an array. + // Throw an error if body isn't either array or object. + reply(payload && Array.isArray(payload)); +} + +function getGraphQLParams(payload, isBatch, reply) { + if (!isBatch) { + payload = [payload]; + } + + const params = []; + for (let query of payload) { + let variables = query.variables; + if (variables && typeof variables === 'string') { + try { + variables = JSON.parse(variables); + } catch (error) { + return reply(createErr(400, 'Variables are invalid JSON.')); + } + } + + params.push({ + query: query.query, + variables: variables, + operationName: query.operationName, + }); + } + reply(params); +}; + async function getApolloOptions(request: Request, reply: IReply): Promise<{}> { const options = request.route.settings.plugins['graphql'].options; let optionsObject: ApolloOptions; @@ -81,45 +123,22 @@ async function getApolloOptions(request: Request, reply: IReply): Promise<{}> { } else { optionsObject = options.apolloOptions; } - - if (!request.payload) { - return reply(createErr(500, 'POST body missing.')); - } else { - return reply(optionsObject); - } -} - -function checkForBatch(payload, reply) { - return reply(payload && Array.isArray(payload)); + reply(optionsObject); } -async function processQuery(payload, isBatch = false, optionsObject: ApolloOptions, reply) { +async function processQuery(graphqlParams, optionsObject: ApolloOptions, reply) { const formatErrorFn = optionsObject.formatError || formatError; - // TODO: do something different here if the body is an array. - // Throw an error if body isn't either array or object. - if (!isBatch) { - payload = [payload]; - } - let responses: GraphQLResult[] = []; - for (let query of payload) { + for (let query of graphqlParams) { try { - const operationName = query.operationName; - let variables = query.variables; - - if (typeof variables === 'string') { - // TODO: catch errors - variables = JSON.parse(variables); - } - let params = { schema: optionsObject.schema, query: query.query, - variables: variables, + variables: query.variables, rootValue: optionsObject.rootValue, context: optionsObject.context, - operationName: operationName, + operationName: query.operationName, logFunction: optionsObject.logFunction, validationRules: optionsObject.validationRules, formatError: formatErrorFn, diff --git a/typings.json b/typings.json index 8e4dadd2776..780cd73f541 100644 --- a/typings.json +++ b/typings.json @@ -13,7 +13,6 @@ "express-serve-static-core": "registry:dt/express-serve-static-core#4.0.0+20160805091045", "fibers": "registry:dt/fibers#0.0.0+20160317120654", "hapi": "registry:dt/hapi#13.0.0+20160803202811", - "joi": "registry:dt/joi#9.0.0+20160906140128", "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", From a96f5783fe19bf16feec1dd2d70d3cc9994255dc Mon Sep 17 00:00:00 2001 From: nnance Date: Thu, 8 Sep 2016 07:45:53 -0500 Subject: [PATCH 08/14] improve HAPIGraphiQL api --- src/integrations/hapiApollo.test.ts | 8 ++- src/integrations/hapiApollo.ts | 81 ++++++++++++++++++++--------- 2 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/integrations/hapiApollo.test.ts b/src/integrations/hapiApollo.test.ts index 3a2aca3a381..0a5e548b289 100644 --- a/src/integrations/hapiApollo.test.ts +++ b/src/integrations/hapiApollo.test.ts @@ -21,8 +21,12 @@ function createApp(createOptions: HAPIPluginOptions) { server.register({ register: GraphiQLHAPI, - options: { endpointURL: '/graphql' }, - routes: { prefix: '/graphiql' }, + options: { + path: '/graphiql', + graphiqlOptions: { + endpointURL: '/graphql', + }, + }, }); return server.listener; diff --git a/src/integrations/hapiApollo.ts b/src/integrations/hapiApollo.ts index 56e65fa8189..b8e3c51e748 100644 --- a/src/integrations/hapiApollo.ts +++ b/src/integrations/hapiApollo.ts @@ -22,11 +22,14 @@ export interface HAPIPluginOptions { } const ApolloHAPI: IRegister = function(server: Server, options: HAPIPluginOptions, next) { + server.method('verifyPayload', verifyPayload); + server.method('getGraphQLParams', getGraphQLParams); + server.method('getApolloOptions', getApolloOptions); + server.method('processQuery', processQuery); + const config = Object.assign(options.route || {}, { plugins: { - graphql: { - options, - }, + graphql: options.apolloOptions, }, pre: [{ assign: 'isBatch', @@ -43,11 +46,6 @@ const ApolloHAPI: IRegister = function(server: Server, options: HAPIPluginOption }], }); - server.method('verifyPayload', verifyPayload); - server.method('getGraphQLParams', getGraphQLParams); - server.method('getApolloOptions', getApolloOptions); - server.method('processQuery', processQuery); - server.route({ method: 'POST', path: options.path || '/graphql', @@ -111,17 +109,17 @@ function getGraphQLParams(payload, isBatch, reply) { }; async function getApolloOptions(request: Request, reply: IReply): Promise<{}> { - const options = request.route.settings.plugins['graphql'].options; + const options = request.route.settings.plugins['graphql']; let optionsObject: ApolloOptions; - if (isOptionsFunction(options.apolloOptions)) { + if (isOptionsFunction(options)) { try { - const opsFunc: HAPIOptionsFunction = options.apolloOptions; + const opsFunc: HAPIOptionsFunction = options; optionsObject = await opsFunc(request); } catch (e) { return reply(createErr(500, `Invalid options provided to ApolloServer: ${e.message}`)); } } else { - optionsObject = options.apolloOptions; + optionsObject = options; } reply(optionsObject); } @@ -167,23 +165,35 @@ function createErr(code: number, message: string) { return err; } -const GraphiQLHAPI: IRegister = function(server: Server, options: GraphiQL.GraphiQLData, next) { +export interface GraphiQLPluginOptions { + path: string; + route?: any; + graphiqlOptions: GraphiQL.GraphiQLData; +} + +const GraphiQLHAPI: IRegister = function(server: Server, options: GraphiQLPluginOptions, next) { + server.method('getGraphiQLParams', getGraphiQLParams); + server.method('renderGraphiQL', renderGraphiQL); + + const config = Object.assign(options.route || {}, { + plugins: { + graphiql: options.graphiqlOptions, + }, + pre: [{ + assign: 'graphiqlParams', + method: 'getGraphiQLParams', + }, { + assign: 'graphiQLString', + method: 'renderGraphiQL(route, pre.graphiqlParams)', + }], + }); + server.route({ method: 'GET', - path: '/', + path: options.path || '/graphql', + config, handler: (request, reply) => { - const q = 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, - }); - reply(graphiQLString).header('Content-Type', 'text/html'); + reply(request.pre.graphiQLString).header('Content-Type', 'text/html'); }, }); next(); @@ -194,4 +204,23 @@ GraphiQLHAPI.attributes = { version: '0.0.1', }; +function getGraphiQLParams(request, reply) { + const q = request.query || {}; + const query = q.query || ''; + const variables = q.variables || '{}'; + const operationName = q.operationName || ''; + reply({ query, variables, operationName}); +} + +function renderGraphiQL(route, graphiqlParams: any, reply) { + const graphiqlOptions = route.settings.plugins['graphiql']; + const graphiQLString = GraphiQL.renderGraphiQL({ + endpointURL: graphiqlOptions.endpointURL, + query: graphiqlParams.query || graphiqlOptions.query, + variables: JSON.parse(graphiqlParams.variables) || graphiqlOptions.variables, + operationName: graphiqlParams.operationName || graphiqlOptions.operationName, + }); + reply(graphiQLString); +} + export { ApolloHAPI, GraphiQLHAPI }; From 0fd39f29bd9d553362d5b9b9daa28466a1d90247 Mon Sep 17 00:00:00 2001 From: nnance Date: Thu, 8 Sep 2016 07:55:31 -0500 Subject: [PATCH 09/14] update documentation --- CHANGELOG.md | 9 +++++++++ README.md | 13 +++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd5ff583edd..a90ae785493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +### v0.3.0 +* Refactor HAPI integration to improve the API and make the plugins more idiomatic. ([@nnance](https://github.com/nnance)) in +[PR #127](https://github.com/apollostack/apollo-server/pull/127) +* Fixed query batching with HAPI integration. Issue #123 ([@nnance](https://github.com/nnance)) in +[PR #127](https://github.com/apollostack/apollo-server/pull/127) +* Add support for route options in HAPI integration. Issue #97. ([@nnance](https://github.com/nnance)) in +[PR #127](https://github.com/apollostack/apollo-server/pull/127) + +### v0.2.6 * Expose the OperationStore as part of the public API. ([@nnance](https://github.com/nnance)) * Support adding parsed operations to the OperationStore. ([@nnance](https://github.com/nnance)) * Expose ApolloOptions as part of the public API. diff --git a/README.md b/README.md index f17712e8974..a75ee3e3a48 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,9 @@ app.listen(PORT); ``` ### hapi + +Now with the HAPI plugins `ApolloHAPI` and `GraphiQLHAPI` you can pass a route object that includes options to be applied to the route. The example below enables CORS on the `/graphql` route. + ```js import hapi from 'hapi'; import { ApolloHAPI } from 'apollo-server'; @@ -81,8 +84,13 @@ server.connection({ server.register({ register: ApolloHAPI, - options: { schema: myGraphQLSchema }, - routes: { prefix: '/graphql' }, + options: { + schema: myGraphQLSchema, + path: '/graphql', + route: { + cors: true + } + }, }); server.start((err) => { @@ -92,6 +100,7 @@ server.start((err) => { console.log(`Server running at: ${server.info.uri}`); }); ``` + ### Koa ```js import koa from 'koa'; From f75f5b6b13acf2eb662851bc2fe5a80c09b1de03 Mon Sep 17 00:00:00 2001 From: nnance Date: Thu, 8 Sep 2016 07:56:42 -0500 Subject: [PATCH 10/14] minor doc updates --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a75ee3e3a48..4551625ad6a 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ app.use('/graphql', bodyParser.json(), apolloConnect({ schema: myGraphQLSchema } app.listen(PORT); ``` -### hapi +### HAPI Now with the HAPI plugins `ApolloHAPI` and `GraphiQLHAPI` you can pass a route object that includes options to be applied to the route. The example below enables CORS on the `/graphql` route. From 8e983b45f3ca01edda69e48850853a801bf3aad9 Mon Sep 17 00:00:00 2001 From: nnance Date: Thu, 8 Sep 2016 10:08:30 -0500 Subject: [PATCH 11/14] fix readme example --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4551625ad6a..db622cc8381 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,10 @@ server.connection({ server.register({ register: ApolloHAPI, options: { - schema: myGraphQLSchema, path: '/graphql', + apolloOptions: { + schema: myGraphQLSchema, + }, route: { cors: true } From c2fd56739bf78645602344357b8d0f00e0c23117 Mon Sep 17 00:00:00 2001 From: nnance Date: Fri, 9 Sep 2016 07:36:51 -0500 Subject: [PATCH 12/14] minor doc update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index db622cc8381..5a54eae8acf 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ Apollo Server is super-easy to set up. Just npm-install apollo-server, write a G ### TypeScript -If you want to build your GraphQL server using TypeScript, Apollo Server is the project for you. +If you want to build your GraphQL server using TypeScript, Apollo Server is the project for you. **NOTE**: All typings mentioned below must be included in your project in order for it to compile. ```sh npm install apollo-server -typings i -SG dt~express dt~express-serve-static-core dt~serve-static dt~mime dt~hapi dt~cookies dt~koa +typings i -SG dt~express dt~express-serve-static-core dt~serve-static dt~mime dt~hapi dt~boom dt~cookies dt~koa ``` For using the project in JavaScript, just run `npm install --save apollo-server` and you're good to go! From 5b85422b8dc1d309fa5b8a61e5bc5876a3bf5d0d Mon Sep 17 00:00:00 2001 From: nnance Date: Fri, 9 Sep 2016 07:42:28 -0500 Subject: [PATCH 13/14] error handling for parsing query variables --- src/integrations/expressApollo.ts | 10 ++++++++-- src/integrations/integrations.test.ts | 14 ++++++++++++++ src/integrations/koaApollo.ts | 8 ++++++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/integrations/expressApollo.ts b/src/integrations/expressApollo.ts index b746fb96282..1cef167ce26 100644 --- a/src/integrations/expressApollo.ts +++ b/src/integrations/expressApollo.ts @@ -77,8 +77,14 @@ export function apolloExpress(options: ApolloOptions | ExpressApolloOptionsFunct let variables = requestParams.variables; if (typeof variables === 'string') { - // TODO: catch errors - variables = JSON.parse(variables); + try { + variables = JSON.parse(variables); + } catch (error) { + res.statusCode = 400; + res.write('Variables are invalid JSON.'); + res.end(); + return; + } } let params = { diff --git a/src/integrations/integrations.test.ts b/src/integrations/integrations.test.ts index 1e4edc28784..5c74d2b381c 100644 --- a/src/integrations/integrations.test.ts +++ b/src/integrations/integrations.test.ts @@ -228,6 +228,20 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); }); + it('can handle a request with variables as an invalid string', () => { + app = createApp(); + const req = request(app) + .post('/graphql') + .send({ + query: 'query test($echo: String!){ testArgument(echo: $echo) }', + variables: '{ echo: "world" }', + }); + return req.then((res) => { + expect(res.status).to.equal(400); + return expect(res.error.text).to.contain('Variables are invalid JSON.'); + }); + }); + it('can handle a request with operationName', () => { app = createApp(); const expected = { diff --git a/src/integrations/koaApollo.ts b/src/integrations/koaApollo.ts index ec074974380..d2825413868 100644 --- a/src/integrations/koaApollo.ts +++ b/src/integrations/koaApollo.ts @@ -56,8 +56,12 @@ export function apolloKoa(options: ApolloOptions | KoaApolloOptionsFunction): Ko let variables = requestParams.variables; if (typeof variables === 'string') { - // TODO: catch errors - variables = JSON.parse(variables); + try { + variables = JSON.parse(variables); + } catch (error) { + ctx.status = 400; + return ctx.body = 'Variables are invalid JSON.'; + } } let params = { From acc151cf9975031a26360d0a58f01c7b16902dbc Mon Sep 17 00:00:00 2001 From: nnance Date: Fri, 9 Sep 2016 07:56:22 -0500 Subject: [PATCH 14/14] update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a90ae785493..54d037d5152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,14 @@ # Changelog ### v0.3.0 -* Refactor HAPI integration to improve the API and make the plugins more idiomatic. ([@nnance](https://github.com/nnance)) in +* Refactor HAPI integration to improve the API and make the plugins more idiomatic. ([@nnance](https://github.com/nnance)) in [PR #127](https://github.com/apollostack/apollo-server/pull/127) * Fixed query batching with HAPI integration. Issue #123 ([@nnance](https://github.com/nnance)) in [PR #127](https://github.com/apollostack/apollo-server/pull/127) * Add support for route options in HAPI integration. Issue #97. ([@nnance](https://github.com/nnance)) in [PR #127](https://github.com/apollostack/apollo-server/pull/127) +* Fix error handling when parsing variables parameter. Issue #130. ([@nnance](https://github.com/nnance)) in +[PR #131](https://github.com/apollostack/apollo-server/pull/131) ### v0.2.6 * Expose the OperationStore as part of the public API. ([@nnance](https://github.com/nnance))