2018-06-21 12:16:52 -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-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-07-04 09:26:39 +02:00
import { InMemoryLRUCache } from 'apollo-server-caching' ;
2018-05-01 06:09:48 -07:00
2018-06-21 12:16:52 -07:00
import { GraphQLUpload } from 'apollo-upload-server' ;
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-20 14:04:22 +02:00
import { formatApolloErrors } from 'apollo-server-errors' ;
2018-06-11 15:44:20 -07:00
import {
GraphQLServerOptions as GraphQLOptions ,
PersistedQueryOptions ,
} from './graphqlOptions' ;
2018-05-01 06:09:48 -07:00
2018-04-09 01:25:37 -04:00
import {
Config ,
Context ,
ContextFunction ,
2018-05-11 17:30:52 -07:00
SubscriptionServerOptions ,
2018-06-21 12:16:52 -07:00
FileUploadOptions ,
2018-04-09 01:25:37 -04:00
} from './types' ;
2018-06-29 10:36:52 -07:00
import { FormatErrorExtension } from './formatters' ;
2018-06-14 15:58:23 -07:00
import { gql } from './index' ;
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-07-04 21:18:25 +02:00
public subscriptionsPath? : string ;
2018-06-13 23:20:46 -07:00
public graphqlPath : string = '/graphql' ;
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-30 17:59:03 -07:00
private engineReportingAgent? : EngineReportingAgent ;
2018-05-16 17:44:51 -07:00
private extensions : Array < ( ) = > GraphQLExtension > ;
2018-06-13 16:59:27 -07:00
protected subscriptionServerOptions? : SubscriptionServerOptions ;
2018-06-21 12:16:52 -07:00
protected uploadsConfig? : FileUploadOptions ;
2018-04-09 01:25:37 -04:00
2018-06-29 13:33:55 +02:00
// This specifies the version of GraphQL Playground that will be served
// from graphql-playground-html, and is passed to renderPlaygroundPage
// by the integration subclasses
2018-07-09 16:35:30 -07:00
protected playgroundVersion = '1.7.2' ;
2018-06-29 13:33:55 +02:00
2018-06-14 11:48:59 -07:00
// set by installSubscriptionHandlers.
2018-05-20 02:18:32 -07:00
private subscriptionServer? : SubscriptionServer ;
2018-05-01 06:09:48 -07:00
2018-06-22 17:51:56 -07:00
// The constructor should be universal across all environments. All environment specific behavior should be set by adding or overriding methods
2018-06-01 15:16:16 -07:00
constructor ( config : Config ) {
2018-06-13 15:19:08 -07:00
if ( ! config ) throw new Error ( 'ApolloServer requires options.' ) ;
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-06-13 16:59:27 -07:00
subscriptions ,
2018-06-21 12:16:52 -07:00
uploads ,
2018-04-23 10:37:46 -04:00
. . . requestOptions
2018-04-09 01:25:37 -04:00
} = config ;
2018-06-21 13:29:14 -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
2018-06-13 15:19:08 -07:00
const isDev = process . env . NODE_ENV !== 'production' ;
2018-05-20 02:12:43 -07: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-07-09 19:24:37 -07:00
if ( ! requestOptions . cache ) {
requestOptions . cache = new InMemoryLRUCache ( ) ;
}
2018-06-11 15:44:20 -07:00
if ( requestOptions . persistedQueries !== false ) {
if ( ! requestOptions . persistedQueries ) {
requestOptions . persistedQueries = {
2018-07-09 19:24:37 -07:00
cache : requestOptions.cache ! ,
2018-06-11 15:44:20 -07:00
} ;
}
} else {
2018-06-21 13:29:14 -07:00
// the user does not want to use persisted queries, so we remove the field
2018-06-11 15:44:20 -07:00
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-21 12:16:52 -07:00
if ( uploads !== false ) {
if ( this . supportsUploads ( ) ) {
if ( uploads === true || typeof uploads === 'undefined' ) {
this . uploadsConfig = { } ;
} else {
this . uploadsConfig = uploads ;
}
//This is here to check if uploads is requested without support. By
//default we enable them if supported by the integration
} else if ( uploads ) {
throw new Error (
'This implementation of ApolloServer does not support file uploads because the environmnet cannot accept multi-part forms' ,
) ;
}
}
//Add upload resolver
if ( this . uploadsConfig ) {
if ( resolvers && ! resolvers . Upload ) {
resolvers . Upload = GraphQLUpload ;
}
}
2018-07-04 21:18:25 +02:00
if ( schema ) {
this . schema = schema ;
} else {
if ( ! typeDefs ) {
throw Error (
'Apollo Server requires either an existing schema or typeDefs' ,
) ;
}
this . schema = makeExecutableSchema ( {
// we add in the upload scalar, so that schemas that don't include it
// won't error when we makeExecutableSchema
typeDefs : this.uploadsConfig
? [
gql `
scalar Upload
` ,
] . concat ( typeDefs )
: typeDefs ,
schemaDirectives ,
resolvers ,
} ) ;
}
2018-04-09 01:25:37 -04:00
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,
2018-06-21 16:05:23 +02:00
// or cacheControl.
2018-05-30 17:59:03 -07:00
this . extensions = [ ] ;
2018-06-29 10:36:52 -07:00
// Error formatting should happen after the engine reporting agent, so that
// engine gets the unmasked errors if necessary
if ( this . requestOptions . formatError ) {
this . extensions . push (
( ) = >
new FormatErrorExtension (
2018-07-04 21:18:25 +02:00
this . requestOptions . formatError ! ,
2018-06-29 10:36:52 -07:00
this . requestOptions . debug ,
) ,
) ;
}
2018-05-30 17:59:03 -07:00
if ( engine || ( engine !== false && process . env . ENGINE_API_KEY ) ) {
this . engineReportingAgent = new EngineReportingAgent (
engine === true ? { } : engine ,
) ;
2018-06-29 10:36:52 -07:00
// Let's keep this extension second so it wraps everything, except error formatting
2018-07-04 21:18:25 +02:00
this . extensions . push ( ( ) = > this . engineReportingAgent ! . newExtension ( ) ) ;
2018-05-30 17:59:03 -07:00
}
if ( extensions ) {
this . extensions = [ . . . this . extensions , . . . extensions ] ;
}
2018-04-09 01:25:37 -04:00
2018-06-13 16:59:27 -07:00
if ( subscriptions !== false ) {
if ( this . supportsSubscriptions ( ) ) {
if ( subscriptions === true || typeof subscriptions === 'undefined' ) {
this . subscriptionServerOptions = {
path : this.graphqlPath ,
} ;
} else if ( typeof subscriptions === 'string' ) {
this . subscriptionServerOptions = { path : subscriptions } ;
} else {
this . subscriptionServerOptions = {
path : this.graphqlPath ,
. . . subscriptions ,
} ;
}
// This is part of the public API.
this . subscriptionsPath = this . subscriptionServerOptions . path ;
2018-06-21 12:16:52 -07:00
//This is here to check if subscriptions are requested without support. By
//default we enable them if supported by the integration
2018-06-13 16:59:27 -07:00
} else if ( subscriptions ) {
throw new Error (
'This implementation of ApolloServer does not support GraphQL subscriptions.' ,
) ;
}
}
2018-04-09 01:25:37 -04:00
}
2018-06-21 13:29:14 -07:00
// used by integrations to synchronize the path with subscriptions, some
// integrations do not have paths, such as lambda
2018-06-13 23:20:46 -07:00
public setGraphQLPath ( path : string ) {
this . graphqlPath = path ;
}
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-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-06-14 11:48:59 -07:00
public installSubscriptionHandlers ( server : HttpServer ) {
2018-06-13 23:20:46 -07:00
if ( ! this . subscriptionServerOptions ) {
2018-06-14 01:12:06 -07:00
if ( this . supportsSubscriptions ( ) ) {
throw Error (
'Subscriptions are disabled, due to subscriptions set to false in the ApolloServer constructor' ,
) ;
} else {
throw Error (
'Subscriptions are not supported, choose an integration, such as apollo-server-express that allows persistent connections' ,
) ;
}
2018-06-13 23:20:46 -07:00
}
2018-06-13 16:59:27 -07:00
const {
onDisconnect ,
onConnect ,
keepAlive ,
path ,
} = this . subscriptionServerOptions ;
2018-05-20 02:18:32 -07:00
2018-06-13 16:59:27 -07:00
this . subscriptionServer = 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 ,
} ) ,
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 ,
} ) [ 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-13 16:59:27 -07:00
protected supportsSubscriptions ( ) : boolean {
return false ;
}
2018-06-21 12:16:52 -07:00
protected supportsUploads ( ) : boolean {
return false ;
}
2018-06-21 13:29:14 -07:00
// This function is used by the integrations to generate the graphQLOptions
// from an object containing the request and other integration specific
// options
2018-06-12 17:46:56 -07:00
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 ) {
2018-06-21 13:29:14 -07:00
// Defer context error resolution to inside of runQuery
2018-05-30 16:02:48 -07:00
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).
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
}
}