From 08c1113256dffeaa75f2baf075d9e1fbc41887db Mon Sep 17 00:00:00 2001 From: Evans Hauser Date: Thu, 10 May 2018 17:28:06 -0700 Subject: [PATCH 1/4] apollo-server-hapi: initial implementation of apollo-server base class --- packages/apollo-server-hapi/package.json | 8 +- .../apollo-server-hapi/src/ApolloServer.ts | 86 +++++++++++++++++++ packages/apollo-server-hapi/src/hapiApollo.ts | 12 ++- packages/apollo-server-hapi/src/index.ts | 14 +++ 4 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 packages/apollo-server-hapi/src/ApolloServer.ts diff --git a/packages/apollo-server-hapi/package.json b/packages/apollo-server-hapi/package.json index 813009217bd..647b17e33b9 100644 --- a/packages/apollo-server-hapi/package.json +++ b/packages/apollo-server-hapi/package.json @@ -25,12 +25,16 @@ }, "homepage": "https://github.com/apollographql/apollo-server#readme", "dependencies": { - "apollo-server-core": "^1.3.6", + "accept": "^3.0.2", + "apollo-server-core": "2.0.0-beta.0", "apollo-server-module-graphiql": "^1.3.4", - "boom": "^7.1.0" + "boom": "^7.1.0", + "graphql-playground-html": "^1.5.6" }, "devDependencies": { "@types/graphql": "0.12.7", + "@types/hapi": "^17.0.12", + "@types/node": "^10.0.6", "apollo-server-integration-testsuite": "^1.3.6", "hapi": "17.4.0" }, diff --git a/packages/apollo-server-hapi/src/ApolloServer.ts b/packages/apollo-server-hapi/src/ApolloServer.ts new file mode 100644 index 00000000000..f17356e7def --- /dev/null +++ b/packages/apollo-server-hapi/src/ApolloServer.ts @@ -0,0 +1,86 @@ +import * as hapi from 'hapi'; +import { createServer, Server as HttpServer } from 'http'; +import { ApolloServerBase, EngineLauncherOptions } from 'apollo-server-core'; +import { parseAll } from 'accept'; +import { renderPlaygroundPage } from 'graphql-playground-html'; + +import { graphqlHapi } from './hapiApollo'; + +export interface ServerRegistration { + app: hapi.Server; + server: ApolloServerBase; + path?: string; + subscriptions?: boolean; +} + +export interface HapiListenOptions { + port?: number | string; + host?: string; // default: ''. This is where engineproxy listens. + pipePath?: string; + graphqlPaths?: string[]; // default: ['/graphql'] + innerHost?: string; // default: '127.0.0.1'. This is where Node listens. + launcherOptions?: EngineLauncherOptions; +} + +export const registerServer = async ({ + app, + server, + path, +}: ServerRegistration) => { + if (!path) path = '/graphql'; + + await app.ext({ + type: 'onRequest', + method: function(request, h) { + if (request.path !== path) { + return h.continue; + } + if (!server.disableTools && request.method === 'get') { + //perform more expensive content-type check only if necessary + const accept = parseAll(request.app); + const types = accept.mediaTypes as string[]; + const prefersHTML = + types.find( + (x: string) => x === 'text/html' || x === 'application/json', + ) === 'text/html'; + + if (prefersHTML) { + return h + .response( + renderPlaygroundPage({ + subscriptionsEndpoint: server.subscriptions && path, + endpoint: path, + version: '', + }), + ) + .type('text/html'); + } + } + return h.continue; + }, + }); + + await app.register({ + plugin: graphqlHapi, + options: { + path: path, + graphqlOptions: server.request.bind(server), + route: { + cors: true, + }, + }, + }); + + server.use({ path, getHttp: () => app.listener }); + + const listen = server.listen; + server.listen = async options => { + //requires that autoListen is false, so that + //hapi sets up app.listener without start + await app.start(); + + //starts the hapi listener at a random port when engine proxy used, + //otherwise will start the server at the provided port + return listen({ ...options }); + }; +}; diff --git a/packages/apollo-server-hapi/src/hapiApollo.ts b/packages/apollo-server-hapi/src/hapiApollo.ts index 22e0e6bcff3..592c66d1c5e 100644 --- a/packages/apollo-server-hapi/src/hapiApollo.ts +++ b/packages/apollo-server-hapi/src/hapiApollo.ts @@ -1,5 +1,5 @@ import * as Boom from 'boom'; -import { Server, Response, Request, ReplyNoContinue } from 'hapi'; +import { Server, Request } from 'hapi'; import * as GraphiQL from 'apollo-server-module-graphiql'; import { GraphQLOptions, @@ -39,13 +39,17 @@ const graphqlHapi: IPlugin = { method: ['GET', 'POST'], path: options.path || '/graphql', vhost: options.vhost || undefined, - config: options.route || {}, + options: options.route || {}, handler: async (request, h) => { try { const gqlResponse = await runHttpQuery([request], { method: request.method.toUpperCase(), options: options.graphqlOptions, - query: request.method === 'post' ? request.payload : request.query, + query: + request.method === 'post' + ? //TODO type payload as string or Record + (request.payload as any) + : request.query, }); const response = h.response(gqlResponse); @@ -98,7 +102,7 @@ const graphiqlHapi: IPlugin = { server.route({ method: 'GET', path: options.path || '/graphiql', - config: options.route || {}, + options: options.route || {}, handler: async (request, h) => { const graphiqlString = await GraphiQL.resolveGraphiQLString( request.query, diff --git a/packages/apollo-server-hapi/src/index.ts b/packages/apollo-server-hapi/src/index.ts index 20a147a0afa..3ea899d15f6 100644 --- a/packages/apollo-server-hapi/src/index.ts +++ b/packages/apollo-server-hapi/src/index.ts @@ -1,3 +1,14 @@ +// Expose types which can be used by both middleware flavors. +export { GraphQLOptions } from 'apollo-server-core'; +export { + ApolloError, + toApolloError, + SyntaxError, + ValidationError, + AuthenticationError, + ForbiddenError, +} from 'apollo-server-core'; + export { IRegister, HapiOptionsFunction, @@ -7,3 +18,6 @@ export { graphqlHapi, graphiqlHapi, } from './hapiApollo'; + +// ApolloServer integration +export { registerServer } from './ApolloServer'; From c55996d40bd87e591102b030431b7fce1ca5cd3f Mon Sep 17 00:00:00 2001 From: Evans Hauser Date: Thu, 10 May 2018 22:12:16 -0700 Subject: [PATCH 2/4] apollo-server-hapi: fix graphql gui request, bind server listen, and check for autoListen: false --- .../apollo-server-hapi/src/ApolloServer.ts | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/apollo-server-hapi/src/ApolloServer.ts b/packages/apollo-server-hapi/src/ApolloServer.ts index f17356e7def..08e8b622bc7 100644 --- a/packages/apollo-server-hapi/src/ApolloServer.ts +++ b/packages/apollo-server-hapi/src/ApolloServer.ts @@ -35,9 +35,10 @@ export const registerServer = async ({ if (request.path !== path) { return h.continue; } + if (!server.disableTools && request.method === 'get') { //perform more expensive content-type check only if necessary - const accept = parseAll(request.app); + const accept = parseAll(request.headers); const types = accept.mediaTypes as string[]; const prefersHTML = types.find( @@ -50,10 +51,11 @@ export const registerServer = async ({ renderPlaygroundPage({ subscriptionsEndpoint: server.subscriptions && path, endpoint: path, - version: '', + version: '1.4.0', }), ) - .type('text/html'); + .type('text/html') + .takeover(); } } return h.continue; @@ -73,12 +75,28 @@ export const registerServer = async ({ server.use({ path, getHttp: () => app.listener }); - const listen = server.listen; + const listen = server.listen.bind(server); server.listen = async options => { //requires that autoListen is false, so that //hapi sets up app.listener without start await app.start(); + //While this is not strictly necessary, it ensures that apollo server calls + //listen first, setting the port. Otherwise the hapi server constructor + //sets the port + if (app.listener.listening) { + throw Error( + ` +Ensure that constructor of Hapi server sets autoListen to false, as follows: + +const app = Hapi.server({ + autoListen: false, + //other parameters +}); + `, + ); + } + //starts the hapi listener at a random port when engine proxy used, //otherwise will start the server at the provided port return listen({ ...options }); From a60b81eb9043c589c751f9aecb8ad8cf3e8f6cd2 Mon Sep 17 00:00:00 2001 From: Evans Hauser Date: Thu, 10 May 2018 22:12:40 -0700 Subject: [PATCH 3/4] apollo-server-hapi: update README to reflect Apollo-server 2 changes --- packages/apollo-server-hapi/README.md | 41 +++++++++++---------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/packages/apollo-server-hapi/README.md b/packages/apollo-server-hapi/README.md index 2df55c5787f..d3a4011a19f 100644 --- a/packages/apollo-server-hapi/README.md +++ b/packages/apollo-server-hapi/README.md @@ -13,46 +13,37 @@ npm install apollo-server-hapi ## Usage -With the Hapi plugins `graphqlHapi` 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. +After constructing Apollo server, a hapi server can be enabled with a call to `registerServer`. Ensure that `autoListen` is set to false in the `Hapi.server` constructor. The code below requires Hapi 17 or higher. ```js -import Hapi from 'hapi'; -import { graphqlHapi } from 'apollo-server-hapi'; +const Hapi = require('hapi'); +const { ApolloServer } = require('apollo-server'); +const { registerServer } = require('apollo-server-hapi'); const HOST = 'localhost'; -const PORT = 3000; async function StartServer() { - const server = new Hapi.server({ + const server = new ApolloServer({ typeDefs, resolvers }); + + //Note: autoListen is required, since Apollo Server will start the listener + const app = new Hapi.server({ + autoListen: false, host: HOST, - port: PORT, }); - await server.register({ - plugin: graphqlHapi, - options: { - path: '/graphql', - graphqlOptions: { - schema: myGraphQLSchema, - }, - route: { - cors: true, - }, - }, - }); + //apply other plugins - try { - await server.start(); - } catch (err) { - console.log(`Error while starting server: ${err.message}`); - } + await registerServer({ server, app }); - console.log(`Server running at: ${server.info.uri}`); + //port is optional and defaults to 4000 + server.listen({ port: 4000 }).then(({ url }) => { + console.log(`🚀 Server ready at ${url}`); + }); } -StartServer(); +StartServer().catch(error => console.log(e)); ``` ## Principles From 32a4fc82efca73009ed3c25dbef13438e6f3d976 Mon Sep 17 00:00:00 2001 From: Evans Hauser Date: Fri, 11 May 2018 11:22:32 -0700 Subject: [PATCH 4/4] apollo-server-hapi: remove subscriptions boolean from register options and check subscriptionsEnabled --- packages/apollo-server-hapi/src/ApolloServer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/apollo-server-hapi/src/ApolloServer.ts b/packages/apollo-server-hapi/src/ApolloServer.ts index 08e8b622bc7..4389c2d31c0 100644 --- a/packages/apollo-server-hapi/src/ApolloServer.ts +++ b/packages/apollo-server-hapi/src/ApolloServer.ts @@ -10,7 +10,6 @@ export interface ServerRegistration { app: hapi.Server; server: ApolloServerBase; path?: string; - subscriptions?: boolean; } export interface HapiListenOptions { @@ -49,7 +48,7 @@ export const registerServer = async ({ return h .response( renderPlaygroundPage({ - subscriptionsEndpoint: server.subscriptions && path, + subscriptionsEndpoint: server.subscriptionsEnabled && path, endpoint: path, version: '1.4.0', }),