2018-05-01 11:30:30 -07:00
import { makeExecutableSchema , addMockFunctionsToSchema } 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 ,
2018-04-23 10:37:46 -04:00
GraphQLResolveInfo ,
ValidationContext ,
FieldDefinitionNode ,
2018-04-18 09:56:02 -04:00
} from 'graphql' ;
2018-05-01 06:09:48 -07:00
import { ApolloEngine } 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
2018-05-01 06:09:48 -07:00
import { internalFormatError } from './errors' ;
import { GraphQLServerOptions as GraphQLOptions } from './graphqlOptions' ;
import { LogFunction } from './runQuery' ;
2018-04-09 01:25:37 -04:00
import {
Config ,
ListenOptions ,
MiddlewareOptions ,
2018-05-01 06:09:48 -07:00
RegistrationOptions ,
2018-04-09 01:25:37 -04:00
ServerInfo ,
Context ,
ContextFunction ,
} from './types' ;
2018-04-23 10:37:46 -04:00
const env = process . env . NODE_ENV ;
const isDev = env !== 'production' && env !== 'test' ;
const NoIntrospection = ( context : ValidationContext ) = > ( {
Field ( node : FieldDefinitionNode ) {
if ( node . name . value === '__schema' || node . name . value === '__type' ) {
context . reportError (
new GraphQLError (
'GraphQL introspection is not allowed by Apollo Server, but the query containted __schema or __type. To enable introspection, pass introspection: true to ApolloServer in production' ,
2018-05-01 11:30:30 -07:00
[ node ] ,
) ,
2018-04-23 10:37:46 -04:00
) ;
}
} ,
} ) ;
2018-05-01 06:09:48 -07:00
export class ApolloServerBase < Request = RequestInit > {
public subscriptions : Config < Request > [ 'subscriptions' ] ;
public disableTools : boolean = ! isDev ;
private schema : GraphQLSchema ;
2018-04-09 01:25:37 -04:00
private context? : Context | ContextFunction ;
2018-04-23 10:37:46 -04:00
private requestOptions : Partial < GraphQLOptions < any > > ;
2018-05-01 06:09:48 -07:00
private graphqlPath : string = '/graphql' ;
private engine : ApolloEngine ;
private engineEnabled : boolean = false ;
2018-04-09 01:25:37 -04:00
2018-05-01 06:09:48 -07:00
private http? : HttpServer ;
protected getHttp : ( ) = > HttpServer ;
constructor ( config : Config < Request > ) {
2018-04-09 01:25:37 -04:00
const {
2018-04-23 10:37:46 -04:00
context ,
resolvers ,
schema ,
schemaDirectives ,
2018-04-09 01:25:37 -04:00
subscriptions ,
2018-04-23 10:37:46 -04:00
typeDefs ,
2018-04-24 11:57:04 -04:00
enableIntrospection ,
2018-05-01 11:30:30 -07:00
mocks ,
2018-04-23 10:37:46 -04:00
. . . requestOptions
2018-04-09 01:25:37 -04:00
} = config ;
2018-04-25 13:40:20 -07:00
// if this is local dev, we want graphql gui and introspection to be turned on
2018-04-24 11:57:04 -04:00
// in production, you can manually turn these on by passing { enableIntrospection: true }
// to the constructor of ApolloServer
// we use this.disableTools to track this internally for later use when
// constructing middleware by frameworks
if ( enableIntrospection || isDev ) this . disableTools = false ;
if ( this . disableTools ) {
2018-04-23 10:37:46 -04:00
const noIntro = [ NoIntrospection ] ;
requestOptions . validationRules = requestOptions . validationRules
? requestOptions . validationRules . concat ( noIntro )
: noIntro ;
}
this . requestOptions = requestOptions ;
2018-04-09 01:25:37 -04:00
this . context = context ;
2018-05-01 06:09:48 -07:00
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 ,
} ) ;
2018-05-01 11:30:30 -07:00
if ( mocks ) {
addMockFunctionsToSchema ( {
schema : this.schema ,
preserveResolvers : true ,
mocks : typeof mocks === 'boolean' ? { } : mocks ,
} ) ;
}
2018-04-09 01:25:37 -04:00
this . subscriptions = subscriptions ;
}
2018-05-01 06:09:48 -07:00
public use ( { getHttp , path } : RegistrationOptions ) {
// we need to delay when we actually get the http server
// until we move into the listen function
this . getHttp = getHttp ;
this . graphqlPath = path ;
2018-04-09 01:25:37 -04:00
}
2018-04-18 08:59:26 -04:00
public listen ( opts : ListenOptions = { } ) : Promise < ServerInfo > {
2018-05-01 06:09:48 -07:00
this . http = this . getHttp ( ) ;
2018-04-09 01:25:37 -04:00
const options = {
port : process.env.PORT || 4000 ,
. . . opts ,
} ;
if ( this . subscriptions !== false ) {
2018-05-01 06:09:48 -07:00
const config : any =
2018-04-09 01:25:37 -04:00
this . subscriptions === true || typeof this . subscriptions === 'undefined'
? {
2018-04-24 11:57:04 -04:00
path : this.graphqlPath ,
2018-04-09 01:25:37 -04:00
}
: this . subscriptions ;
this . createSubscriptionServer ( this . http , config ) ;
}
2018-05-01 06:09:48 -07:00
if ( opts . engine || opts . engineInRequestPath ) this . createEngine ( opts ) ;
2018-04-24 11:57:04 -04:00
return new Promise ( ( success , fail ) = > {
2018-04-18 08:59:26 -04:00
if ( this . engine ) {
this . engine . listen (
2018-04-24 11:57:04 -04:00
Object . assign ( { } , options . engineLauncherOptions , {
graphqlPaths : [ this . graphqlPath ] ,
2018-04-18 08:59:26 -04:00
port : options.port ,
httpServer : this.http ,
} ) ,
2018-04-25 00:37:49 -07:00
( ) = > {
2018-04-26 23:21:58 -07:00
this . engine . engineListeningAddress . url = require ( 'url' ) . resolve (
this . engine . engineListeningAddress . url ,
2018-05-01 11:30:30 -07:00
this . graphqlPath ,
2018-04-26 23:21:58 -07:00
) ;
2018-04-25 00:37:49 -07:00
success ( this . engine . engineListeningAddress ) ;
2018-05-01 11:30:30 -07:00
} ,
2018-04-18 08:59:26 -04:00
) ;
2018-04-24 11:57:04 -04:00
this . engine . on ( 'error' , fail ) ;
2018-04-18 08:59:26 -04:00
return ;
2018-04-11 19:29:03 -04:00
}
2018-04-24 11:57:04 -04:00
// all options for http listeners
// https://nodejs.org/api/net.html#net_server_listen_options_callback
this . http . listen (
{
port : options.port ,
host : options.host ,
path : options.path ,
backlog : options.backlog ,
exclusive : options.exclusive ,
} ,
( ) = > {
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 ApolloServer.listen).
let hostForUrl = la . address ;
if ( la . address === '' || la . address === '::' )
hostForUrl = 'localhost' ;
2018-04-26 23:21:58 -07:00
la . url = require ( 'url' ) . format ( {
protocol : 'http' ,
hostname : hostForUrl ,
port : la.port ,
pathname : this.graphqlPath ,
} ) ;
2018-04-24 11:57:04 -04:00
success ( la ) ;
2018-05-01 11:30:30 -07:00
} ,
2018-04-24 11:57:04 -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-23 16:24:08 -07:00
errors :
value . errors && value . errors . map ( err = > internalFormatError ( 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-24 11:57:04 -04:00
path : this.graphqlPath ,
2018-05-01 11:30:30 -07:00
} ,
2018-04-09 01:25:37 -04:00
) ;
}
2018-05-01 06:09:48 -07:00
private createEngine ( { engineInRequestPath , engine } : ListenOptions ) {
// only access this onces as its slower on node
const { ENGINE_API_KEY , ENGINE_CONFIG } = process . env ;
if ( engine === false && ( ENGINE_API_KEY || ENGINE_CONFIG ) ) {
console . warn (
2018-05-01 11:30:30 -07:00
'engine is set to false when creating ApolloServer but either ENGINE_CONFIG or ENGINE_API_KEY was found in the environment' ,
2018-05-01 06:09:48 -07:00
) ;
}
let ApolloEngine ;
if ( engine ) {
// detect engine if it is set to true or has a config, and possibly load it
try {
ApolloEngine = require ( 'apollo-engine' ) . ApolloEngine ;
} catch ( e ) {
console . warn ( ` ApolloServer was unable to load Apollo Engine yet engine was configured in the options when creating this ApolloServer? To fix this, run the following command:
2018-04-09 01:25:37 -04:00
2018-05-01 06:09:48 -07:00
npm install apollo - engine -- save
2018-04-09 01:25:37 -04:00
` );
2018-05-01 06:09:48 -07:00
}
this . engine = new ApolloEngine (
2018-05-01 11:30:30 -07:00
typeof engine === 'boolean' ? undefined : engine ,
2018-05-01 06:09:48 -07:00
) ;
2018-04-09 01:25:37 -04:00
}
2018-05-01 06:09:48 -07:00
// XXX should this allow for header overrides from graphql-playground?
if ( this . engine || engineInRequestPath ) this . engineEnabled = true ;
}
async request ( request : Request ) {
2018-04-09 01:25:37 -04:00
let context : Context = this . context ? this . context : { request } ;
2018-04-24 11:57:04 -04:00
context =
typeof this . context === 'function'
? await this . context ( { req : request } )
: context ;
2018-04-09 01:25:37 -04:00
return {
schema : this.schema ,
2018-04-18 08:59:26 -04:00
tracing : Boolean ( this . engineEnabled ) ,
cacheControl : Boolean ( this . engineEnabled ) ,
2018-04-23 10:37:46 -04:00
formatError : ( e : GraphQLError ) = >
2018-04-23 16:24:08 -07:00
internalFormatError ( e , this . requestOptions . debug ) ,
2018-04-09 01:25:37 -04:00
context ,
2018-04-23 10:37:46 -04:00
// allow overrides from options
. . . this . requestOptions ,
2018-04-09 01:25:37 -04:00
} ;
}
}