2018-04-18 08:59:26 -04:00
import { makeExecutableSchema } from 'graphql-tools' ;
2018-04-09 01:25:37 -04:00
import { Server as HttpServer } from 'http' ;
2018-04-18 09:56:02 -04:00
import {
execute ,
GraphQLSchema ,
subscribe ,
ExecutionResult ,
GraphQLError ,
} from 'graphql' ;
import { formatError } from 'apollo-server-core' ;
2018-04-09 01:25:37 -04:00
import { ApolloEngine as Engine } from 'apollo-engine' ;
2018-04-18 08:59:26 -04:00
import {
SubscriptionServer ,
ExecutionParams ,
} from 'subscriptions-transport-ws' ;
2018-04-09 01:25:37 -04:00
import { CorsOptions } from 'cors' ;
import {
Config ,
ListenOptions ,
MiddlewareOptions ,
MiddlewareRegistrationOptions ,
ServerInfo ,
Context ,
ContextFunction ,
} from './types' ;
2018-04-11 19:29:03 -04:00
// taken from engine
export function joinHostPort ( host : string , port : number ) {
2018-04-18 08:59:26 -04:00
if ( host . includes ( ':' ) ) host = ` [ ${ host } ] ` ;
2018-04-11 19:29:03 -04:00
return ` ${ host } : ${ port } ` ;
}
2018-04-09 01:25:37 -04:00
2018-04-18 08:59:26 -04:00
export class ApolloServerBase <
Server = HttpServer ,
Request = any ,
Cors = CorsOptions
> {
2018-04-09 01:25:37 -04:00
app? : Server ;
schema : GraphQLSchema ;
private context? : Context | ContextFunction ;
private engine? : Engine ;
private appCreated : boolean = false ;
private middlewareRegistered : boolean = false ;
private http? : HttpServer ;
private subscriptions? : any ;
private graphqlEndpoint : string = '/graphql' ;
2018-04-18 08:59:26 -04:00
private cors? : Cors ;
private engineEnabled : boolean = false ;
2018-04-18 09:56:02 -04:00
private debug? : boolean ;
2018-04-09 01:25:37 -04:00
2018-04-18 08:59:26 -04:00
constructor ( config : Config < Server , Request , Cors > ) {
2018-04-09 01:25:37 -04:00
const {
typeDefs ,
resolvers ,
schemaDirectives ,
schema ,
context ,
app ,
engine ,
2018-04-18 08:59:26 -04:00
engineInRequestPath ,
2018-04-09 01:25:37 -04:00
subscriptions ,
cors ,
2018-04-18 09:56:02 -04:00
debug ,
2018-04-09 01:25:37 -04:00
} = config ;
2018-04-18 09:56:02 -04:00
this . debug = debug ;
2018-04-09 01:25:37 -04:00
this . context = context ;
2018-04-18 08:59:26 -04:00
// XXX should we move this to the `start` call? This would make hot
// reloading eaiser but may not be worth it?
2018-04-09 01:25:37 -04:00
this . schema = schema
? schema
: makeExecutableSchema ( {
typeDefs : Array.isArray ( typeDefs )
? typeDefs . reduce ( ( prev , next ) = > prev + '\n' + next )
: typeDefs ,
schemaDirectives ,
resolvers ,
} ) ;
this . subscriptions = subscriptions ;
this . cors = cors ;
if ( app ) {
this . app = app ;
} else {
this . app = this . createApp ( ) ;
this . appCreated = true ;
}
// only access this onces as its slower on node
const { ENGINE_API_KEY , ENGINE_CONFIG } = process . env ;
2018-04-18 08:59:26 -04:00
const shouldLoadEngine = ENGINE_API_KEY || ENGINE_CONFIG || engine ;
2018-04-09 01:25:37 -04:00
if ( engine === false && shouldLoadEngine ) {
console . warn (
2018-04-18 08:59:26 -04:00
'engine is set to false when creating ApolloServer but either ENGINE_CONFIG or ENGINE_API_KEY were found in the environment' ,
2018-04-09 01:25:37 -04:00
) ;
}
2018-04-11 19:29:03 -04:00
let ApolloEngine ;
2018-04-18 08:59:26 -04:00
if ( engine ) {
// detect engine if it is set to true or has a config, and possibly load it
2018-04-09 01:25:37 -04:00
try {
2018-04-11 19:29:03 -04:00
ApolloEngine = require ( 'apollo-engine' ) . ApolloEngine ;
2018-04-09 01:25:37 -04:00
} catch ( e ) {
if ( shouldLoadEngine ) {
2018-04-18 08:59:26 -04:00
console . warn ( ` ApolloServer was unable to load Apollo Engine and found either environment variables that seem like you want it to be running or engine was configured on the options when creating this ApolloServer? To fix this, run the following command:
2018-04-09 01:25:37 -04:00
npm install apollo - engine -- save
` );
}
}
2018-04-11 19:29:03 -04:00
2018-04-18 08:59:26 -04:00
if ( ! shouldLoadEngine ) {
2018-04-11 19:29:03 -04:00
throw new Error ( `
ApolloServer was unable to load the configuration for Apollo Engine . Please verify that you are either passing in an engine config to the new ApolloServer call , or have set ENGINE_CONFIG or ENGINE_API_KEY in your environment
` );
}
this . engine = new ApolloEngine ( engine ) ;
2018-04-09 01:25:37 -04:00
}
2018-04-18 08:59:26 -04:00
// XXX should this allow for header overrides from graphql-playground?
if ( this . engine || engineInRequestPath ) this . engineEnabled = true ;
2018-04-09 01:25:37 -04:00
}
public applyMiddleware ( opts : MiddlewareOptions = { } ) {
if ( this . appCreated ) {
throw new Error ( ` It looks like server.applyMiddleware was called when app was not passed into ApolloServer. To use middlware, you need to create an ApolloServer from a variant package and pass in your app:
const { ApolloServer } = require ( 'apollo-server/express' ) ;
const express = require ( 'express' ) ;
const app = express ( ) ;
const server = new ApolloServer ( { app , resolvers , typeDefs } ) ;
// then when you want to add the middleware
server . applyMiddleware ( ) ;
// then start the server
server . listen ( )
` );
}
2018-04-18 08:59:26 -04:00
const registerOptions : MiddlewareRegistrationOptions <
Server ,
Request ,
Cors
> = {
2018-04-09 01:25:37 -04:00
endpoint : this.graphqlEndpoint ,
cors : this.cors ,
2018-04-18 08:59:26 -04:00
subscriptions : true ,
2018-04-09 01:25:37 -04:00
. . . opts ,
2018-04-18 08:59:26 -04:00
graphiql :
opts . graphiql === false ? null : ` ${ opts . graphiql || '/graphiql' } ` ,
2018-04-09 01:25:37 -04:00
app : this.app ,
request : this.request.bind ( this ) ,
} ;
2018-04-18 08:59:26 -04:00
2018-04-09 01:25:37 -04:00
this . graphqlEndpoint = registerOptions . endpoint ;
// this function can either mutate the app (normal)
// or some frameworks maj need to return a new one
const possiblyNewServer = this . registerMiddleware ( registerOptions ) ;
this . middlewareRegistered = true ;
if ( possiblyNewServer ) this . app = possiblyNewServer ;
}
2018-04-18 08:59:26 -04:00
public listen ( opts : ListenOptions = { } ) : Promise < ServerInfo > {
2018-04-09 01:25:37 -04:00
if ( ! this . appCreated && ! this . middlewareRegistered ) {
throw new Error (
` It looks like you are trying to run ApolloServer without applying the middleware. This error is thrown when using a variant of ApolloServer (i.e. require('apollo-server/variant')) and passing in a custom app. To fix this, before you call server.listen, you need to call server.applyMiddleware():
const app = express ( ) ;
const server = new ApolloServer ( { app , resolvers , typeDefs } ) ;
// XXX this part is missing currently!
server . applyMiddleware ( ) ;
server . listen ( ) ;
2018-04-18 08:59:26 -04:00
` ,
2018-04-09 01:25:37 -04:00
) ;
}
const options = {
port : process.env.PORT || 4000 ,
. . . opts ,
} ;
this . http = this . getHttpServer ( this . app ) ;
if ( this . subscriptions !== false ) {
const config =
this . subscriptions === true || typeof this . subscriptions === 'undefined'
? {
path : this.graphqlEndpoint ,
}
: this . subscriptions ;
this . createSubscriptionServer ( this . http , config ) ;
}
2018-04-18 08:59:26 -04:00
return new Promise ( ( s , f ) = > {
if ( this . engine ) {
this . engine . listen (
Object . assign ( { } , options . engine , {
graphqlPaths : [ this . graphqlEndpoint ] ,
port : options.port ,
httpServer : this.http ,
} ) ,
( ) = > s ( this . engine . engineListeningAddress ) ,
) ;
this . engine . on ( 'error' , f ) ;
return ;
2018-04-11 19:29:03 -04:00
}
2018-04-18 08:59:26 -04:00
this . http . listen ( options . port , ( ) = > {
const la : any = this . http . address ( ) ;
// Convert IPs which mean "any address" (IPv4 or IPv6) into localhost
// corresponding loopback ip. Note that the url field we're setting is
// primarily for consumption by our test suite. If this heuristic is
// wrong for your use case, explicitly specify a frontend host (in the
// `frontends.host` field in your engine config, or in the `host`
// option to ApolloEngine.listen).
let hostForUrl = la . address ;
if ( la . address === '' || la . address === '::' ) hostForUrl = 'localhost' ;
la . url = ` http:// ${ joinHostPort ( hostForUrl , la . port ) } ` ;
s ( la ) ;
2018-04-11 19:29:03 -04:00
} ) ;
2018-04-18 08:59:26 -04:00
} ) ;
2018-04-09 01:25:37 -04:00
}
public async stop() {
if ( this . engine ) await this . engine . stop ( ) ;
if ( this . http ) await new Promise ( s = > this . http . close ( s ) ) ;
}
2018-04-18 08:59:26 -04:00
private createSubscriptionServer ( server : HttpServer , config : ListenOptions ) {
const { onDisconnect , onConnect , keepAlive } = config ;
2018-04-09 01:25:37 -04:00
SubscriptionServer . create (
{
schema : this.schema ,
execute ,
subscribe ,
onConnect : onConnect
? onConnect
2018-04-18 08:59:26 -04:00
: ( connectionParams : Object ) = > ( { . . . connectionParams } ) ,
2018-04-09 01:25:37 -04:00
onDisconnect : onDisconnect ,
2018-04-18 08:59:26 -04:00
onOperation : async ( _ : string , connection : ExecutionParams ) = > {
connection . formatResponse = ( value : ExecutionResult ) = > ( {
2018-04-09 01:25:37 -04:00
. . . value ,
2018-04-18 09:56:02 -04:00
errors : value.errors && value . errors . map ( err = > formatError ( err ) ) ,
2018-04-09 01:25:37 -04:00
} ) ;
let context : Context = this . context ? this . context : { connection } ;
try {
context =
typeof this . context === 'function'
? await this . context ( { connection } )
: context ;
} catch ( e ) {
console . error ( e ) ;
throw e ;
}
return { . . . connection , context } ;
} ,
keepAlive ,
} ,
{
server ,
2018-04-18 08:59:26 -04:00
path : this.graphqlEndpoint ,
} ,
2018-04-09 01:25:37 -04:00
) ;
}
async request ( request : Request ) {
if ( ! this ) {
throw new Error ( ` It looks like you tried to call this.request but didn't bind it to the parent class. To fix this,
when calling this . request , either call it using an error function , or bind it like so :
this . request . bind ( this ) ;
` );
}
let context : Context = this . context ? this . context : { request } ;
try {
context =
typeof this . context === 'function'
? await this . context ( { req : request } )
: context ;
} catch ( e ) {
console . error ( e ) ;
throw e ;
}
return {
schema : this.schema ,
2018-04-18 08:59:26 -04:00
tracing : Boolean ( this . engineEnabled ) ,
cacheControl : Boolean ( this . engineEnabled ) ,
2018-04-18 09:56:02 -04:00
formatError : ( e : GraphQLError ) = > formatError ( e , Boolean ( this . debug ) ) ,
debug : Boolean ( this . debug ) ,
2018-04-09 01:25:37 -04:00
context ,
} ;
}
/* region: vanilla ApolloServer */
createApp ( ) : Server {
throw new Error ( ` It looks like you called server.listen on an ApolloServer that is missing a server! This means that either you need to pass an external server when creating an ApolloServer, or use an ApolloServer variant that supports a default server:
const { ApolloServer } = require ( 'apollo-server' ) ;
// or
const { ApolloServer } = require ( 'apollo-server/express' ) ;
To see all supported servers , check the docs at https : //apollographql.com/docs/server
` );
}
/* end region: vanilla ApolloServer */
/* region: variant ApolloServer */
registerMiddleware (
2018-04-18 08:59:26 -04:00
config : MiddlewareRegistrationOptions < Server , Request , Cors > ,
2018-04-09 01:25:37 -04:00
) : Server | void {
throw new Error ( ` It looks like you called server.addMiddleware on an ApolloServer that is missing a server! Make sure you pass in an app when creating a server:
const { ApolloServer } = require ( 'apollo-server/express' ) ;
const express = require ( 'express' ) ;
const app = express ( ) ;
const server = new ApolloServer ( { app , typeDefs , resolvers } ) ;
` );
}
getHttpServer ( app : Server ) : HttpServer {
throw new Error (
2018-04-18 08:59:26 -04:00
` It looks like you are trying to use subscriptions with ApolloServer but we couldn't find an http server from your framework. To fix this, please open an issue for you variant at the apollographql/apollo-server repo ` ,
2018-04-09 01:25:37 -04:00
) ;
}
/* end region: variant ApolloServer */
closeApp ( app : Server ) : Promise < void > | void { }
}