2020-04-22 14:02:23 -07:00
// Adapted from https://github.com/apollographql/apollo-server/blob/201630ad284754248fc9ab6ebedc7506fcc3d951/packages/apollo-server-lambda/src/ApolloServer.ts
const { ApolloServerBase , runHttpQuery } = require ( "apollo-server-core" ) ;
const { Headers } = require ( "apollo-server-env" ) ;
const {
renderPlaygroundPage ,
} = require ( "@apollographql/graphql-playground-html" ) ;
function graphqlVercel ( options ) {
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 } `
) ;
}
const graphqlHandler = ( req , res ) => {
if ( req . httpMethod === "POST" && ! req . body ) {
return res . status ( 500 ) . send ( "POST body missing." ) ;
}
runHttpQuery ( [ req , res ] , {
method : req . method ,
options : options ,
query : req . method === "POST" && req . body ? req . body : req . query ,
request : {
url : req . path ,
method : req . method ,
headers : new Headers ( req . headers ) ,
} ,
} ) . then (
( { graphqlResponse , responseInit } ) => {
setHeaders ( res , new Headers ( responseInit . headers ) ) . send (
graphqlResponse
) ;
} ,
( error ) => {
if ( "HttpQueryError" !== error . name ) {
console . error ( error ) ;
return ;
}
setHeaders ( res , new Headers ( error . headers ) )
. status ( error . statusCode )
. send ( error . message ) ;
}
) ;
} ;
return graphqlHandler ;
}
class ApolloServer extends ApolloServerBase {
// If you feel tempted to add an option to this constructor. Please consider
// another place, since the documentation becomes much more complicated when
// the constructor is not longer shared between all integration
constructor ( options ) {
if ( process . env . ENGINE _API _KEY || options . engine ) {
options . engine = {
sendReportsImmediately : true ,
... ( typeof options . engine !== "boolean" ? options . engine : { } ) ,
} ;
}
super ( options ) ;
}
// This translates the arguments from the middleware into graphQL options It
// provides typings for the integration specific behavior, ideally this would
// be propagated with a generic to the super class
createGraphQLServerOptions ( req , res ) {
return super . graphQLServerOptions ( { req , res } ) ;
}
createHandler ( { cors } = { cors : undefined } ) {
// We will kick off the `willStart` event once for the server, and then
// await it before processing any requests by incorporating its `await` into
// the GraphQLServerOptions function which is called before each request.
const promiseWillStart = this . willStart ( ) ;
const corsHeaders = new Headers ( ) ;
if ( cors ) {
if ( cors . methods ) {
if ( typeof cors . methods === "string" ) {
corsHeaders . set ( "access-control-allow-methods" , cors . methods ) ;
} else if ( Array . isArray ( cors . methods ) ) {
corsHeaders . set (
"access-control-allow-methods" ,
cors . methods . join ( "," )
) ;
}
}
if ( cors . allowedHeaders ) {
if ( typeof cors . allowedHeaders === "string" ) {
corsHeaders . set ( "access-control-allow-headers" , cors . allowedHeaders ) ;
} else if ( Array . isArray ( cors . allowedHeaders ) ) {
corsHeaders . set (
"access-control-allow-headers" ,
cors . allowedHeaders . join ( "," )
) ;
}
}
if ( cors . exposedHeaders ) {
if ( typeof cors . exposedHeaders === "string" ) {
corsHeaders . set ( "access-control-expose-headers" , cors . exposedHeaders ) ;
} else if ( Array . isArray ( cors . exposedHeaders ) ) {
corsHeaders . set (
"access-control-expose-headers" ,
cors . exposedHeaders . join ( "," )
) ;
}
}
if ( cors . credentials ) {
corsHeaders . set ( "access-control-allow-credentials" , "true" ) ;
}
if ( typeof cors . maxAge === "number" ) {
corsHeaders . set ( "access-control-max-age" , cors . maxAge . toString ( ) ) ;
}
}
return ( req , res ) => {
// Make a request-specific copy of the CORS headers, based on the server
// global CORS headers we've set above.
const requestCorsHeaders = new Headers ( corsHeaders ) ;
if ( cors && cors . origin ) {
const requestOrigin = req . headers [ "origin" ] ;
if ( typeof cors . origin === "string" ) {
requestCorsHeaders . set ( "access-control-allow-origin" , cors . origin ) ;
} else if (
requestOrigin &&
( typeof cors . origin === "boolean" ||
( Array . isArray ( cors . origin ) &&
requestOrigin &&
cors . origin . includes ( requestOrigin ) ) )
) {
requestCorsHeaders . set ( "access-control-allow-origin" , requestOrigin ) ;
}
const requestAccessControlRequestHeaders =
req . headers [ "access-control-request-headers" ] ;
if ( ! cors . allowedHeaders && requestAccessControlRequestHeaders ) {
requestCorsHeaders . set (
"access-control-allow-headers" ,
requestAccessControlRequestHeaders
) ;
}
}
// Convert the `Headers` into an object which can be spread into the
// various headers objects below.
// Note: while Object.fromEntries simplifies this code, it's only currently
// supported in Node 12 (we support >=6)
const requestCorsHeadersObject = Array . from ( requestCorsHeaders ) . reduce (
( headersObject , [ key , value ] ) => {
headersObject [ key ] = value ;
return headersObject ;
} ,
{ }
) ;
if ( res . method === "OPTIONS" ) {
setHeaders ( res , requestCorsHeadersObject ) . status ( 204 ) . send ( "" ) ;
}
if ( this . playgroundOptions && req . method === "GET" ) {
const acceptHeader = req . headers [ "accept" ] ;
if ( acceptHeader && acceptHeader . includes ( "text/html" ) ) {
const path = req . path || "/" ;
const playgroundRenderPageOptions = {
endpoint : path ,
... this . playgroundOptions ,
} ;
2020-04-22 14:55:12 -07:00
return setHeaders (
res ,
new Headers ( {
"Content-Type" : "text/html" ,
... requestCorsHeadersObject ,
} )
)
. status ( 200 )
. send ( renderPlaygroundPage ( playgroundRenderPageOptions ) ) ;
2020-04-22 14:02:23 -07:00
}
}
graphqlVercel ( async ( ) => {
// In a world where this `createHandler` was async, we might avoid this
// but since we don't want to introduce a breaking change to this API
// (by switching it to `async`), we'll leverage the
// `GraphQLServerOptions`, which are dynamically built on each request,
// to `await` the `promiseWillStart` which we kicked off at the top of
// this method to ensure that it runs to completion (which is part of
// its contract) prior to processing the request.
await promiseWillStart ;
return this . createGraphQLServerOptions ( req , res ) ;
} ) ( req , res ) ;
} ;
}
}
function setHeaders ( res , headers ) {
for ( const [ name , value ] of headers . entries ( ) ) {
res . setHeader ( name , value ) ;
}
return res ;
}
module . exports = { ApolloServer } ;