apollo-server/packages/apollo-server-core/src/ApolloServer.ts

324 lines
10 KiB
TypeScript
Raw Normal View History

import {
makeExecutableSchema,
addMockFunctionsToSchema,
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,
GraphQLSchema,
subscribe,
ExecutionResult,
GraphQLError,
GraphQLFieldResolver,
ValidationContext,
FieldDefinitionNode,
2018-04-18 09:56:02 -04:00
} from 'graphql';
import { GraphQLExtension } from 'graphql-extensions';
import { EngineReportingAgent } from 'apollo-engine-reporting';
import { InMemoryKeyValueCache } from 'apollo-datasource-rest';
2018-05-01 06:09:48 -07:00
import {
SubscriptionServer,
ExecutionParams,
} from 'subscriptions-transport-ws';
2018-04-09 01:25:37 -04:00
//use as default persisted query store
import Keyv = require('keyv');
import QuickLru = require('quick-lru');
import { formatApolloErrors } from './errors';
import {
GraphQLServerOptions as GraphQLOptions,
PersistedQueryOptions,
} from './graphqlOptions';
import { LogFunction } from './logging';
2018-05-01 06:09:48 -07:00
2018-04-09 01:25:37 -04:00
import {
Config,
Context,
ContextFunction,
SubscriptionServerOptions,
2018-04-09 01:25:37 -04:00
} from './types';
import { gql } from './index';
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 contained __schema or __type. To enable introspection, pass introspection: true to ApolloServer in production',
2018-05-01 11:30:30 -07:00
[node],
),
);
}
},
});
export class ApolloServerBase {
public subscriptionsPath: string;
public graphqlPath: string = '/graphql';
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;
private engineReportingAgent?: EngineReportingAgent;
private extensions: Array<() => GraphQLExtension>;
2018-06-13 16:59:27 -07:00
protected subscriptionServerOptions?: SubscriptionServerOptions;
2018-04-09 01:25:37 -04:00
// set by installSubscriptionHandlers.
private subscriptionServer?: SubscriptionServer;
2018-05-01 06:09:48 -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
constructor(config: Config) {
if (!config) throw new Error('ApolloServer requires options.');
2018-04-09 01:25:37 -04:00
const {
context,
resolvers,
schema,
schemaDirectives,
typeDefs,
introspection,
2018-05-01 11:30:30 -07:00
mocks,
extensions,
engine,
2018-06-13 16:59:27 -07:00
subscriptions,
...requestOptions
2018-04-09 01:25:37 -04:00
} = config;
//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 isDev = process.env.NODE_ENV !== 'production';
// 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)
) {
const noIntro = [NoIntrospection];
requestOptions.validationRules = requestOptions.validationRules
? requestOptions.validationRules.concat(noIntro)
: noIntro;
}
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;
}
if (!requestOptions.cache) {
requestOptions.cache = new InMemoryKeyValueCache();
}
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-04-09 01:25:37 -04:00
this.schema = schema
? schema
: makeExecutableSchema({
//we add in the upload scalar, so that schemas that don't include it
//won't error when we makeExecutableSchema
typeDefs: [
gql`
scalar Upload
`,
].concat(typeDefs),
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,
});
}
// Note: doRunQuery will add its own extensions if you set tracing,
// cacheControl, or logFunction.
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-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;
} else if (subscriptions) {
throw new Error(
'This implementation of ApolloServer does not support GraphQL subscriptions.',
);
}
}
2018-04-09 01:25:37 -04:00
}
//used by integrations to synchronize the path with subscriptions, some
//integrations do not have paths, such as lambda
public setGraphQLPath(path: string) {
this.graphqlPath = path;
}
// If this is more generally useful to things other than Upload, we can make
// it public.
protected enhanceSchema(schema: GraphQLSchema) {
this.schema = mergeSchemas({
schemas: [this.schema, schema],
});
}
2018-04-09 01:25:37 -04:00
public async stop() {
if (this.subscriptionServer) await this.subscriptionServer.close();
if (this.engineReportingAgent) {
this.engineReportingAgent.stop();
await this.engineReportingAgent.sendReport();
}
2018-04-09 01:25:37 -04:00
}
public installSubscriptionHandlers(server: HttpServer) {
if (!this.subscriptionServerOptions) {
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 16:59:27 -07:00
const {
onDisconnect,
onConnect,
keepAlive,
path,
} = this.subscriptionServerOptions;
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
: (connectionParams: Object) => ({ ...connectionParams }),
2018-04-09 01:25:37 -04:00
onDisconnect: onDisconnect,
onOperation: async (_: string, connection: ExecutionParams) => {
connection.formatResponse = (value: ExecutionResult) => ({
2018-04-09 01:25:37 -04:00
...value,
errors:
value.errors &&
formatApolloErrors([...value.errors], {
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) {
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,
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;
}
//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
try {
context =
typeof this.context === 'function'
? await this.context(integrationContextArgument || {})
: 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,
extensions: this.extensions,
2018-04-09 01:25:37 -04:00
context,
// 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,
persistedQueries: this.requestOptions
.persistedQueries as PersistedQueryOptions,
fieldResolver: this.requestOptions.fieldResolver as GraphQLFieldResolver<
any,
any
>,
...this.requestOptions,
} as GraphQLOptions;
2018-04-09 01:25:37 -04:00
}
}