2018-05-29 15:58:52 -07:00
import {
makeExecutableSchema ,
addMockFunctionsToSchema ,
IResolvers ,
mergeSchemas ,
} 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 ,
2018-06-05 13:38:32 -07:00
print ,
2018-04-18 09:56:02 -04:00
GraphQLSchema ,
subscribe ,
ExecutionResult ,
GraphQLError ,
2018-06-01 15:16:16 -07:00
GraphQLFieldResolver ,
2018-04-23 10:37:46 -04:00
ValidationContext ,
FieldDefinitionNode ,
2018-04-18 09:56:02 -04:00
} from 'graphql' ;
2018-05-16 17:44:51 -07:00
import { GraphQLExtension } from 'graphql-extensions' ;
2018-05-30 17:59:03 -07:00
import { EngineReportingAgent } from 'apollo-engine-reporting' ;
2018-05-01 06:09:48 -07:00
2018-04-18 08:59:26 -04:00
import {
SubscriptionServer ,
ExecutionParams ,
} from 'subscriptions-transport-ws' ;
2018-04-09 01:25:37 -04:00
2018-06-11 15:44:20 -07:00
//use as default persisted query store
import Keyv = require ( 'keyv' ) ;
import QuickLru = require ( 'quick-lru' ) ;
2018-05-02 16:40:03 -07:00
import { formatApolloErrors } from './errors' ;
2018-06-11 15:44:20 -07:00
import {
GraphQLServerOptions as GraphQLOptions ,
PersistedQueryOptions ,
} from './graphqlOptions' ;
2018-06-01 15:16:16 -07:00
import { LogFunction } from './logging' ;
2018-05-01 06:09:48 -07:00
2018-04-09 01:25:37 -04:00
import {
Config ,
ListenOptions ,
2018-05-01 06:09:48 -07:00
RegistrationOptions ,
2018-04-09 01:25:37 -04:00
ServerInfo ,
Context ,
ContextFunction ,
2018-05-11 17:30:52 -07:00
SubscriptionServerOptions ,
2018-04-09 01:25:37 -04:00
} from './types' ;
2018-04-23 10:37:46 -04:00
const NoIntrospection = ( context : ValidationContext ) = > ( {
Field ( node : FieldDefinitionNode ) {
if ( node . name . value === '__schema' || node . name . value === '__type' ) {
context . reportError (
new GraphQLError (
2018-05-11 17:00:45 -07:00
'GraphQL introspection is not allowed by Apollo Server, but the query contained __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-06-12 17:46:56 -07:00
export class ApolloServerBase {
2018-05-20 02:12:43 -07:00
public disableTools : boolean ;
2018-05-11 17:30:52 -07:00
// set in the listen function if subscriptions are enabled
public subscriptionsPath : string ;
2018-05-29 15:58:52 -07:00
public requestOptions : Partial < GraphQLOptions < any > > ;
2018-05-01 06:09:48 -07:00
private schema : GraphQLSchema ;
2018-04-09 01:25:37 -04:00
private context? : Context | ContextFunction ;
2018-05-01 06:09:48 -07:00
private graphqlPath : string = '/graphql' ;
2018-05-30 17:59:03 -07:00
private engineReportingAgent? : EngineReportingAgent ;
2018-05-16 17:44:51 -07:00
private extensions : Array < ( ) = > GraphQLExtension > ;
2018-04-09 01:25:37 -04:00
2018-05-01 06:09:48 -07:00
private http? : HttpServer ;
2018-05-20 02:18:32 -07:00
private subscriptionServer? : SubscriptionServer ;
2018-05-01 06:09:48 -07:00
protected getHttp : ( ) = > HttpServer ;
2018-06-12 17:46:56 -07:00
//The constructor should be universal across all environments. All environment specific behavior should be set in an exported registerServer or in by overriding listen
2018-06-01 15:16:16 -07:00
constructor ( config : Config ) {
2018-04-09 01:25:37 -04:00
const {
2018-04-23 10:37:46 -04:00
context ,
resolvers ,
schema ,
schemaDirectives ,
typeDefs ,
2018-05-11 17:33:52 -07:00
introspection ,
2018-05-01 11:30:30 -07:00
mocks ,
2018-05-16 17:44:51 -07:00
extensions ,
2018-05-30 17:59:03 -07:00
engine ,
2018-04-23 10:37:46 -04:00
. . . requestOptions
2018-04-09 01:25:37 -04:00
} = config ;
2018-05-20 02:12:43 -07:00
//While reading process.env is slow, a server should only be constructed
//once per run, so we place the env check inside the constructor. If env
//should be used outside of the constructor context, place it as a private
//or protected field of the class instead of a global. Keeping the read in
//the contructor enables testing of different environments
const env = process . env . NODE_ENV ;
const isDev = env !== 'production' && env !== 'test' ;
2018-04-24 11:57:04 -04:00
// we use this.disableTools to track this internally for later use when
2018-06-12 22:56:09 -07:00
// constructing middleware by frameworks to disable graphql playground
this . disableTools = ! isDev ;
2018-04-24 11:57:04 -04:00
2018-06-12 22:56:09 -07:00
// if this is local dev, introspection should turned on
// in production, we can manually turn introspection on by passing {
// introspection: true } to the constructor of ApolloServer
if (
( typeof introspection === 'boolean' && ! introspection ) ||
( introspection === undefined && ! isDev )
) {
2018-04-23 10:37:46 -04:00
const noIntro = [ NoIntrospection ] ;
requestOptions . validationRules = requestOptions . validationRules
? requestOptions . validationRules . concat ( noIntro )
: noIntro ;
}
2018-06-11 15:44:20 -07:00
if ( requestOptions . persistedQueries !== false ) {
if ( ! requestOptions . persistedQueries ) {
//maxSize is the number of elements that can be stored inside of the cache
//https://github.com/withspectrum/spectrum has about 200 instances of gql`
//300 queries seems reasonable
const lru = new QuickLru ( { maxSize : 300 } ) ;
requestOptions . persistedQueries = {
cache : new Keyv ( { store : lru } ) ,
} ;
}
} else {
//the user does not want to use persisted queries, so we remove the field
delete requestOptions . persistedQueries ;
}
this . requestOptions = requestOptions as GraphQLOptions ;
2018-04-09 01:25:37 -04:00
this . context = context ;
2018-05-01 06:09:48 -07:00
2018-06-05 13:38:32 -07:00
if (
typeof typeDefs === 'string' ||
( Array . isArray ( typeDefs ) && typeDefs . find ( d = > typeof d === 'string' ) )
) {
2018-06-06 09:33:43 -07:00
const startSchema =
( typeof typeDefs === 'string' &&
( typeDefs as string ) . substring ( 0 , 200 ) ) ||
( Array . isArray ( typeDefs ) &&
( typeDefs . find ( d = > typeof d === 'string' ) as any ) . substring ( 0 , 200 ) ) ;
2018-06-05 13:38:32 -07:00
throw new Error ( ` typeDefs must be tagged with the gql exported from apollo-server:
const { gql } = require ( 'apollo-server' ) ;
2018-06-06 09:33:43 -07:00
const typeDefs = gql \ ` ${ startSchema } \`
2018-06-05 13:38:32 -07:00
` );
}
const enhancedTypeDefs = Array . isArray ( typeDefs )
? typeDefs . map ( print )
: [ print ( typeDefs ) ] ;
2018-05-29 15:58:52 -07:00
enhancedTypeDefs . push ( ` scalar Upload ` ) ;
2018-04-09 01:25:37 -04:00
this . schema = schema
? schema
: makeExecutableSchema ( {
2018-05-29 15:58:52 -07:00
typeDefs : enhancedTypeDefs.join ( '\n' ) ,
2018-04-09 01:25:37 -04:00
schemaDirectives ,
resolvers ,
} ) ;
2018-05-01 11:30:30 -07:00
if ( mocks ) {
addMockFunctionsToSchema ( {
schema : this.schema ,
preserveResolvers : true ,
mocks : typeof mocks === 'boolean' ? { } : mocks ,
} ) ;
}
2018-05-16 17:44:51 -07:00
2018-06-13 12:28:36 -07:00
// Note: doRunQuery will add its own extensions if you set tracing,
// cacheControl, or logFunction.
2018-05-30 17:59:03 -07:00
this . extensions = [ ] ;
if ( engine || ( engine !== false && process . env . ENGINE_API_KEY ) ) {
this . engineReportingAgent = new EngineReportingAgent (
engine === true ? { } : engine ,
) ;
// Let's keep this extension first so it wraps everything.
this . extensions . push ( ( ) = > this . engineReportingAgent . newExtension ( ) ) ;
}
if ( extensions ) {
this . extensions = [ . . . this . extensions , . . . extensions ] ;
}
2018-04-09 01:25:37 -04:00
}
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-05-29 15:58:52 -07:00
public enhanceSchema (
schema : GraphQLSchema | { typeDefs : string ; resolvers : IResolvers } ,
) {
this . schema = mergeSchemas ( {
schemas : [
this . schema ,
'typeDefs' in schema ? schema [ 'typeDefs' ] : schema ,
] ,
resolvers : 'resolvers' in schema ? [ , schema [ 'resolvers' ] ] : { } ,
} ) ;
}
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-05-11 10:01:28 -07:00
2018-04-09 01:25:37 -04:00
const options = {
. . . opts ,
2018-05-21 23:20:56 -07:00
http : {
port : process.env.PORT || 4000 ,
. . . opts . http ,
} ,
2018-04-09 01:25:37 -04:00
} ;
2018-05-11 10:01:28 -07:00
if ( opts . subscriptions !== false ) {
2018-05-11 17:30:52 -07:00
let config : SubscriptionServerOptions ;
if (
opts . subscriptions === true ||
typeof opts . subscriptions === 'undefined'
) {
config = {
path : this.graphqlPath ,
} ;
} else if ( typeof opts . subscriptions === 'string' ) {
config = { path : opts.subscriptions } ;
} else {
config = { path : this.graphqlPath , . . . opts . subscriptions } ;
}
this . subscriptionsPath = config . path ;
2018-05-20 02:18:32 -07:00
this . subscriptionServer = this . createSubscriptionServer (
this . http ,
config ,
) ;
2018-04-09 01:25:37 -04:00
}
2018-06-13 12:28:36 -07:00
return new Promise ( resolve = > {
2018-04-24 11:57:04 -04:00
// all options for http listeners
// https://nodejs.org/api/net.html#net_server_listen_options_callback
2018-05-11 17:00:45 -07:00
// https://github.com/apollographql/apollo-server/pull/979/files/33ea0c92a1e4e76c8915ff08806f15dae391e1f0#discussion_r184470435
// https://github.com/apollographql/apollo-server/pull/979#discussion_r184471445
2018-05-21 23:20:56 -07:00
function listenCallback() {
const listeningAddress : 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 = listeningAddress . address ;
if (
listeningAddress . address === '' ||
listeningAddress . address === '::'
)
hostForUrl = 'localhost' ;
listeningAddress . url = require ( 'url' ) . format ( {
protocol : 'http' ,
hostname : hostForUrl ,
port : listeningAddress.port ,
pathname : this.graphqlPath ,
} ) ;
resolve ( listeningAddress ) ;
}
if ( options . http . handle ) {
this . http . listen (
options . http . handle ,
options . http . backlog ,
listenCallback . bind ( this ) ,
) ;
} else {
this . http . listen ( options . http , listenCallback . bind ( this ) ) ;
}
2018-04-18 08:59:26 -04:00
} ) ;
2018-04-09 01:25:37 -04:00
}
public async stop() {
2018-05-20 02:18:32 -07:00
if ( this . subscriptionServer ) await this . subscriptionServer . close ( ) ;
2018-04-09 01:25:37 -04:00
if ( this . http ) await new Promise ( s = > this . http . close ( s ) ) ;
2018-06-11 18:44:55 -07:00
if ( this . engineReportingAgent ) {
this . engineReportingAgent . stop ( ) ;
await this . engineReportingAgent . sendReport ( ) ;
}
2018-04-09 01:25:37 -04:00
}
2018-05-11 17:30:52 -07:00
private createSubscriptionServer (
server : HttpServer ,
config : SubscriptionServerOptions ,
) {
const { onDisconnect , onConnect , keepAlive , path } = config ;
2018-05-20 02:18:32 -07:00
return SubscriptionServer . create (
2018-04-09 01:25:37 -04:00
{
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-05-02 23:28:24 -07:00
errors :
value . errors &&
2018-05-21 15:41:36 -07:00
formatApolloErrors ( [ . . . value . errors ] , {
2018-05-02 23:28:24 -07:00
formatter : this.requestOptions.formatError ,
debug : this.requestOptions.debug ,
logFunction : this.requestOptions.logFunction ,
} ) ,
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 ) {
2018-05-02 23:28:24 -07:00
throw formatApolloErrors ( [ e ] , {
formatter : this.requestOptions.formatError ,
debug : this.requestOptions.debug ,
logFunction : this.requestOptions.logFunction ,
} ) [ 0 ] ;
2018-04-09 01:25:37 -04:00
}
return { . . . connection , context } ;
} ,
keepAlive ,
} ,
{
server ,
2018-05-11 17:33:52 -07:00
path ,
2018-05-01 11:30:30 -07:00
} ,
2018-04-09 01:25:37 -04:00
) ;
}
2018-06-12 17:46:56 -07:00
//This function is used by the integrations to generate the graphQLOptions
//from an object containing the request and other integration specific
//options
protected async graphQLServerOptions (
integrationContextArgument? : Record < string , any > ,
) {
let context : Context = this . context ? this . context : { } ;
2018-04-09 01:25:37 -04:00
2018-05-30 16:02:48 -07:00
try {
context =
typeof this . context === 'function'
2018-06-12 17:46:56 -07:00
? await this . context ( integrationContextArgument || { } )
2018-05-30 16:02:48 -07:00
: context ;
} catch ( error ) {
//Defer context error resolution to inside of runQuery
context = ( ) = > {
throw error ;
} ;
}
2018-04-09 01:25:37 -04:00
return {
schema : this.schema ,
2018-05-16 17:44:51 -07:00
extensions : this.extensions ,
2018-04-09 01:25:37 -04:00
context ,
2018-06-01 15:16:16 -07:00
// Allow overrides from options. Be explicit about a couple of them to
// avoid a bad side effect of the otherwise useful noUnusedLocals option
// (https://github.com/Microsoft/TypeScript/issues/21673).
logFunction : this.requestOptions.logFunction as LogFunction ,
2018-06-11 15:44:20 -07:00
persistedQueries : this.requestOptions
. persistedQueries as PersistedQueryOptions ,
2018-06-01 15:16:16 -07:00
fieldResolver : this.requestOptions.fieldResolver as GraphQLFieldResolver <
any ,
any
> ,
2018-04-23 10:37:46 -04:00
. . . this . requestOptions ,
2018-06-12 17:46:56 -07:00
} as GraphQLOptions ;
2018-04-09 01:25:37 -04:00
}
}