@subsquid/apollo-server-core
Version:
Core engine for Apollo GraphQL server
1,067 lines (977 loc) • 40.7 kB
text/typescript
import { addMocksToSchema } from '@graphql-tools/mock';
import { makeExecutableSchema } from '@graphql-tools/schema';
import loglevel from 'loglevel';
import {
GraphQLSchema,
GraphQLError,
ValidationContext,
FieldDefinitionNode,
DocumentNode,
ParseOptions,
print,
} from 'graphql';
import resolvable, { Resolvable } from '@josephg/resolvable';
import {
InMemoryLRUCache,
PrefixingKeyValueCache,
} from '@apollo/utils.keyvaluecache';
import type {
ApolloServerPlugin,
GraphQLServiceContext,
GraphQLServerListener,
LandingPage,
} from 'apollo-server-plugin-base';
import type { GraphQLServerOptions } from './graphqlOptions';
import type {
Config,
Context,
ContextFunction,
DocumentStore,
PluginDefinition,
} from './types';
import { generateSchemaHash } from './utils/schemaHash';
import {
processGraphQLRequest,
GraphQLRequestContext,
GraphQLRequest,
APQ_CACHE_PREFIX,
} from './requestPipeline';
import { Headers } from 'apollo-server-env';
import { buildServiceDefinition } from '@apollographql/apollo-tools';
import type { SchemaHash, ApolloConfig } from 'apollo-server-types';
import type { Logger } from '@apollo/utils.logger';
import { cloneObject } from './runHttpQuery';
import isNodeLike from './utils/isNodeLike';
import { determineApolloConfig } from './determineApolloConfig';
import {
ApolloServerPluginSchemaReporting,
ApolloServerPluginSchemaReportingOptions,
ApolloServerPluginInlineTrace,
ApolloServerPluginUsageReporting,
ApolloServerPluginCacheControl,
ApolloServerPluginLandingPageLocalDefault,
ApolloServerPluginLandingPageProductionDefault,
} from './plugin';
import { InternalPluginId, pluginIsInternal } from './internalPlugin';
import { newCachePolicy } from './cachePolicy';
import { GatewayIsTooOldError, SchemaManager } from './utils/schemaManager';
import * as uuid from 'uuid';
import { UnboundedCache } from './utils/UnboundedCache';
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',
[node],
),
);
}
},
});
export type SchemaDerivedData = {
schema: GraphQLSchema;
// Not a very useful schema hash (not the same one schema and usage reporting
// use!) but kept around for backwards compatibility.
schemaHash: SchemaHash;
// A store that, when enabled (default), will store the parsed and validated
// versions of operations in-memory, allowing subsequent parses/validates
// on the same operation to be executed immediately.
documentStore: DocumentStore | null;
};
type ServerState =
| {
phase: 'initialized';
schemaManager: SchemaManager;
}
| {
phase: 'starting';
barrier: Resolvable<void>;
schemaManager: SchemaManager;
}
| {
phase: 'failed to start';
error: Error;
}
| {
phase: 'started';
schemaManager: SchemaManager;
}
| {
phase: 'draining';
schemaManager: SchemaManager;
barrier: Resolvable<void>;
}
| {
phase: 'stopping';
barrier: Resolvable<void>;
}
| {
phase: 'stopped';
stopError: Error | null;
};
// Throw this in places that should be unreachable (because all other cases have
// been handled, reducing the type of the argument to `never`). TypeScript will
// complain if in fact there is a valid type for the argument.
class UnreachableCaseError extends Error {
constructor(val: never) {
super(`Unreachable case: ${val}`);
}
}
// Our recommended set of CSRF prevention headers. Operations that do not
// provide a content-type such as `application/json` (in practice, this
// means GET operations) must include at least one of these headers.
// Apollo Client Web's default behavior is to always sends a
// `content-type` even for `GET`, and Apollo iOS and Apollo Kotlin always
// send `x-apollo-operation-name`. So if you set
// `csrfPreventionRequestHeaders: true` then any `GET` operation from these
// three client projects and any `POST` operation at all should work
// successfully; if you need `GET`s from another kind of client to work,
// just add `apollo-require-preflight: true` to their requests.
const recommendedCsrfPreventionRequestHeaders = [
'x-apollo-operation-name',
'apollo-require-preflight',
];
export class ApolloServerBase<
// The type of the argument to the `context` function for this integration.
ContextFunctionParams = any,
> {
private logger: Logger;
public graphqlPath: string = '/graphql';
public requestOptions: Partial<GraphQLServerOptions<any>> =
Object.create(null);
private context?: Context | ContextFunction<ContextFunctionParams>;
private apolloConfig: ApolloConfig;
protected plugins: ApolloServerPlugin[] = [];
protected csrfPreventionRequestHeaders: string[] | null;
private parseOptions: ParseOptions;
private config: Config<ContextFunctionParams>;
private state: ServerState;
private toDispose = new Set<() => Promise<void>>();
private toDisposeLast = new Set<() => Promise<void>>();
private drainServers: (() => Promise<void>) | null = null;
private stopOnTerminationSignals: boolean;
private landingPage: LandingPage | null = null;
// The constructor should be universal across all environments. All environment specific behavior should be set by adding or overriding methods
constructor(config: Config<ContextFunctionParams>) {
if (!config) throw new Error('ApolloServer requires options.');
this.config = {
...config,
nodeEnv: config.nodeEnv ?? process.env.NODE_ENV,
};
const {
context,
resolvers,
schema,
modules,
typeDefs,
parseOptions = {},
introspection,
plugins,
gateway,
apollo,
stopOnTerminationSignals,
// These next options aren't used in this function but they don't belong in
// requestOptions.
mocks,
mockEntireSchema,
documentStore,
csrfPrevention,
...requestOptions
} = this.config;
// Setup logging facilities
if (config.logger) {
this.logger = config.logger;
} else {
// If the user didn't provide their own logger, we'll initialize one.
const loglevelLogger = loglevel.getLogger('apollo-server');
// We don't do much logging in Apollo Server right now. There's a notion
// of a `debug` flag, which changes stack traces in some error messages,
// and adds a bit of debug logging to some plugins. `info` is primarily
// used for startup logging in plugins. We'll default to `info` so you
// get to see that startup logging.
if (this.config.debug === true) {
loglevelLogger.setLevel(loglevel.levels.DEBUG);
} else {
loglevelLogger.setLevel(loglevel.levels.INFO);
}
this.logger = loglevelLogger;
}
this.apolloConfig = determineApolloConfig(apollo, this.logger);
if (gateway && (modules || schema || typeDefs || resolvers)) {
throw new Error(
'Cannot define both `gateway` and any of: `modules`, `schema`, `typeDefs`, or `resolvers`',
);
}
this.parseOptions = parseOptions;
this.context = context;
this.csrfPreventionRequestHeaders =
csrfPrevention === true
? recommendedCsrfPreventionRequestHeaders
: csrfPrevention === false
? null
: csrfPrevention === undefined
? null // In AS4, change this to be equivalent to 'true'.
: csrfPrevention.requestHeaders ??
recommendedCsrfPreventionRequestHeaders;
const isDev = this.config.nodeEnv !== 'production';
// We handle signals if it was explicitly requested, or if we're in Node,
// not in a test, not in a serverless framework, and it wasn't explicitly
// turned off. (We only actually register the signal handlers once we've
// successfully started up, because there's nothing to stop otherwise.)
this.stopOnTerminationSignals =
typeof stopOnTerminationSignals === 'boolean'
? stopOnTerminationSignals
: isNodeLike &&
this.config.nodeEnv !== 'test' &&
!this.serverlessFramework();
// 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.cache === 'bounded') {
requestOptions.cache = new InMemoryLRUCache();
}
if (!requestOptions.cache) {
requestOptions.cache = new UnboundedCache();
if (
!isDev &&
(requestOptions.persistedQueries === undefined ||
(requestOptions.persistedQueries &&
!requestOptions.persistedQueries.cache))
) {
this.logger.warn(
'Persisted queries are enabled and are using an unbounded cache. Your server' +
' is vulnerable to denial of service attacks via memory exhaustion. ' +
'Set `cache: "bounded"` or `persistedQueries: false` in your ApolloServer ' +
'constructor, or see https://go.apollo.dev/s/cache-backends for other alternatives.',
);
}
}
if (requestOptions.persistedQueries !== false) {
const { cache: apqCache = requestOptions.cache!, ...apqOtherOptions } =
requestOptions.persistedQueries || Object.create(null);
requestOptions.persistedQueries = {
cache: new PrefixingKeyValueCache(apqCache, APQ_CACHE_PREFIX),
...apqOtherOptions,
};
} else {
// the user does not want to use persisted queries, so we remove the field
delete requestOptions.persistedQueries;
}
this.requestOptions = requestOptions as GraphQLServerOptions;
// Plugins will be instantiated if they aren't already, and this.plugins
// is populated accordingly.
this.ensurePluginInstantiation(plugins, isDev);
if (gateway) {
// ApolloServer has been initialized but we have not yet tried to load the
// schema from the gateway. That will wait until the user calls
// `server.start()` or `server.listen()`, or (in serverless frameworks)
// until the `this._start()` call at the end of this constructor.
this.state = {
phase: 'initialized',
schemaManager: new SchemaManager({
gateway,
apolloConfig: this.apolloConfig,
schemaDerivedDataProvider: (schema) =>
this.generateSchemaDerivedData(schema),
logger: this.logger,
}),
};
} else {
// We construct the schema synchronously so that we can fail fast if the
// schema can't be constructed. (This used to be more important because we
// used to have a 'schema' field that was publicly accessible immediately
// after construction, though that field never actually worked with
// gateways.)
this.state = {
phase: 'initialized',
schemaManager: new SchemaManager({
apiSchema: this.maybeAddMocksToConstructedSchema(
this.constructSchema(),
),
schemaDerivedDataProvider: (schema) =>
this.generateSchemaDerivedData(schema),
logger: this.logger,
}),
};
}
// The main entry point (createHandler) to serverless frameworks generally
// needs to be called synchronously from the top level of your entry point,
// unlike (eg) applyMiddleware, so we can't expect you to `await
// server.start()` before calling it. So we kick off the start
// asynchronously from the constructor, and failures are logged and cause
// later requests to fail (in `_ensureStarted`, called by
// `graphQLServerOptions` and from the serverless framework handlers).
// There's no way to make "the whole server fail" separately from making
// individual requests fail, but that's not entirely unreasonable for a
// "serverless" model.
if (this.serverlessFramework()) {
this._start().catch((e) => this.logStartupError(e));
}
}
// Awaiting a call to `start` ensures that a schema has been loaded and that
// all plugin `serverWillStart` hooks have been called. If either of these
// processes throw, `start` will (async) throw as well.
//
// If you're using the batteries-included `apollo-server` package, you don't
// need to call `start` yourself (in fact, it will throw if you do so); its
// `listen` method takes care of that for you (this is why the actual logic is
// in the `_start` helper).
//
// If instead you're using an integration package for a non-serverless
// framework (like Express), you must await a call to `start` immediately
// after creating your `ApolloServer`, before attaching it to your web
// framework and starting to accept requests. `start` should only be called
// once; if it throws and you'd like to retry, just create another
// `ApolloServer`. (Calling `start` was optional in Apollo Server 2, but in
// Apollo Server 3 the methods like `server.applyMiddleware` use
// `assertStarted` to throw if `start` hasn't successfully completed.)
//
// Serverless integrations like Lambda (which override `serverlessFramework()`
// to return true) do not support calling `start()`, because their lifecycle
// doesn't allow you to wait before assigning a handler or allowing the
// handler to be called. So they call `_start()` at the end of the
// constructor, and don't really differentiate between startup failures and
// request failures. This is hopefully appropriate for a "serverless"
// framework. Serverless startup failures result in returning a redacted error
// to the end user and logging the more detailed error.
public async start(): Promise<void> {
if (this.serverlessFramework()) {
throw new Error(
'When using an ApolloServer subclass from a serverless framework ' +
"package, you don't need to call start(); just call createHandler().",
);
}
return await this._start();
}
// This is protected so that it can be called from `apollo-server`. It is
// otherwise an internal implementation detail.
protected async _start(): Promise<void> {
if (this.state.phase !== 'initialized') {
throw new Error(
`called start() with surprising state ${this.state.phase}`,
);
}
const schemaManager = this.state.schemaManager;
const barrier = resolvable();
this.state = {
phase: 'starting',
barrier,
schemaManager,
};
try {
const executor = await schemaManager.start();
this.toDispose.add(async () => {
await schemaManager.stop();
});
if (executor) {
// If we loaded an executor from a gateway, use it to execute
// operations.
this.requestOptions.executor = executor;
}
const schemaDerivedData = schemaManager.getSchemaDerivedData();
const service: GraphQLServiceContext = {
logger: this.logger,
schema: schemaDerivedData.schema,
schemaHash: schemaDerivedData.schemaHash,
apollo: this.apolloConfig,
serverlessFramework: this.serverlessFramework(),
};
// The `persistedQueries` attribute on the GraphQLServiceContext was
// originally used by the operation registry, which shared the cache with
// it. This is no longer the case. However, while we are continuing to
// expand the support of the interface for `persistedQueries`, e.g. with
// additions like https://github.com/apollographql/apollo-server/pull/3623,
// we don't want to continually expand the API surface of what we expose
// to the plugin API. In this particular case, it certainly doesn't need
// to get the `ttl` default value which are intended for APQ only.
if (this.requestOptions.persistedQueries?.cache) {
service.persistedQueries = {
cache: this.requestOptions.persistedQueries.cache,
};
}
const taggedServerListeners = (
await Promise.all(
this.plugins.map(async (plugin) => ({
serverListener:
plugin.serverWillStart && (await plugin.serverWillStart(service)),
installedImplicitly:
isImplicitlyInstallablePlugin(plugin) &&
plugin.__internal_installed_implicitly__,
})),
)
).filter(
(
maybeTaggedServerListener,
): maybeTaggedServerListener is {
serverListener: GraphQLServerListener;
installedImplicitly: boolean;
} => typeof maybeTaggedServerListener.serverListener === 'object',
);
taggedServerListeners.forEach(
({ serverListener: { schemaDidLoadOrUpdate } }) => {
if (schemaDidLoadOrUpdate) {
try {
schemaManager.onSchemaLoadOrUpdate(schemaDidLoadOrUpdate);
} catch (e) {
if (e instanceof GatewayIsTooOldError) {
throw new Error(
[
`One of your plugins uses the 'schemaDidLoadOrUpdate' hook,`,
`but your gateway version is too old to support this hook.`,
`Please update your version of /gateway to at least 0.35.0.`,
].join(' '),
);
}
throw e;
}
}
},
);
const serverWillStops = taggedServerListeners.flatMap((l) =>
l.serverListener.serverWillStop
? [l.serverListener.serverWillStop]
: [],
);
if (serverWillStops.length) {
this.toDispose.add(async () => {
await Promise.all(
serverWillStops.map((serverWillStop) => serverWillStop()),
);
});
}
const drainServerCallbacks = taggedServerListeners.flatMap((l) =>
l.serverListener.drainServer ? [l.serverListener.drainServer] : [],
);
if (drainServerCallbacks.length) {
this.drainServers = async () => {
await Promise.all(
drainServerCallbacks.map((drainServer) => drainServer()),
);
};
}
// Find the renderLandingPage callback, if one is provided. If the user
// installed ApolloServerPluginLandingPageDisabled then there may be none
// found. On the other hand, if the user installed a landingPage plugin,
// then both the implicit installation of
// ApolloServerPluginLandingPage*Default and the other plugin will be
// found; we skip the implicit plugin.
let taggedServerListenersWithRenderLandingPage =
taggedServerListeners.filter((l) => l.serverListener.renderLandingPage);
if (taggedServerListenersWithRenderLandingPage.length > 1) {
taggedServerListenersWithRenderLandingPage =
taggedServerListenersWithRenderLandingPage.filter(
(l) => !l.installedImplicitly,
);
}
if (taggedServerListenersWithRenderLandingPage.length > 1) {
throw Error('Only one plugin can implement renderLandingPage.');
} else if (taggedServerListenersWithRenderLandingPage.length) {
this.landingPage = await taggedServerListenersWithRenderLandingPage[0]
.serverListener.renderLandingPage!();
} else {
this.landingPage = null;
}
this.state = {
phase: 'started',
schemaManager,
};
this.maybeRegisterTerminationSignalHandlers(['SIGINT', 'SIGTERM']);
} catch (error) {
this.state = { phase: 'failed to start', error: error as Error };
throw error;
} finally {
barrier.resolve();
}
}
private maybeRegisterTerminationSignalHandlers(signals: NodeJS.Signals[]) {
if (!this.stopOnTerminationSignals) {
return;
}
let receivedSignal = false;
const signalHandler: NodeJS.SignalsListener = async (signal) => {
if (receivedSignal) {
// If we receive another SIGINT or SIGTERM while we're waiting
// for the server to stop, just ignore it.
return;
}
receivedSignal = true;
try {
await this.stop();
} catch (e) {
this.logger.error(`stop() threw during ${signal} shutdown`);
this.logger.error(e);
// Can't rely on the signal handlers being removed.
process.exit(1);
}
// Note: this.stop will call the toDisposeLast handlers below, so at
// this point this handler will have been removed and we can re-kill
// ourself to die with the appropriate signal exit status. this.stop
// takes care to call toDisposeLast last, so the signal handler isn't
// removed until after the rest of shutdown happens.
process.kill(process.pid, signal);
};
signals.forEach((signal) => {
process.on(signal, signalHandler);
this.toDisposeLast.add(async () => {
process.removeListener(signal, signalHandler);
});
});
}
// This method is called at the beginning of each GraphQL request by
// `graphQLServerOptions`. Most of its logic is only helpful for serverless
// frameworks: unless you're in a serverless framework, you should have called
// `await server.start()` before the server got to the point of running
// GraphQL requests (`assertStarted` calls in the framework integrations
// verify that) and so the only cases for non-serverless frameworks that this
// should hit are 'started', 'stopping', and 'stopped'. For serverless
// frameworks, this lets the server wait until fully started before serving
// operations.
//
// It's also called via `ensureStarted` by serverless frameworks so that they
// can call `renderLandingPage` (or do other things like call a method on a base
// class that expects it to be started).
private async _ensureStarted(): Promise<SchemaDerivedData> {
while (true) {
switch (this.state.phase) {
case 'initialized':
// This error probably won't happen: serverless frameworks
// automatically call `_start` at the end of the constructor, and
// other frameworks call `assertStarted` before setting things up
// enough to make calling this function possible.
throw new Error(
'You need to call `server.start()` before using your Apollo Server.',
);
case 'starting':
await this.state.barrier;
// continue the while loop
break;
case 'failed to start':
// First we log the error that prevented startup (which means it will
// get logged once for every GraphQL operation).
this.logStartupError(this.state.error);
// Now make the operation itself fail.
// We intentionally do not re-throw actual startup error as it may contain
// implementation details and this error will propagate to the client.
throw new Error(
'This data graph is missing a valid configuration. More details may be available in the server logs.',
);
case 'started':
case 'draining': // We continue to run operations while draining.
return this.state.schemaManager.getSchemaDerivedData();
case 'stopping':
throw new Error(
'Cannot execute GraphQL operations while the server is stopping.',
);
case 'stopped':
throw new Error(
'Cannot execute GraphQL operations after the server has stopped.',
);
default:
throw new UnreachableCaseError(this.state);
}
}
}
// For serverless frameworks only. Just like `_ensureStarted` but hides its
// return value.
protected async ensureStarted() {
await this._ensureStarted();
}
protected assertStarted(methodName: string) {
if (this.state.phase !== 'started' && this.state.phase !== 'draining') {
throw new Error(
'You must `await server.start()` before calling `server.' +
methodName +
'()`',
);
}
// XXX do we need to do anything special for stopping/stopped?
}
// Given an error that occurred during Apollo Server startup, log it with a
// helpful message. This should only happen with serverless frameworks; with
// other frameworks, you must `await server.start()` which will throw the
// startup error directly instead of logging (or `await server.listen()` for
// the batteries-included `apollo-server`).
private logStartupError(err: Error) {
this.logger.error(
'An error occurred during Apollo Server startup. All GraphQL requests ' +
'will now fail. The startup error was: ' +
(err?.message || err),
);
}
private constructSchema(): GraphQLSchema {
const { schema, modules, typeDefs, resolvers, parseOptions } = this.config;
if (schema) {
return schema;
}
if (modules) {
const { schema, errors } = buildServiceDefinition(modules);
if (errors && errors.length > 0) {
throw new Error(errors.map((error) => error.message).join('\n\n'));
}
return schema!;
}
if (!typeDefs) {
throw Error(
'Apollo Server requires either an existing schema, modules or typeDefs',
);
}
const augmentedTypeDefs = Array.isArray(typeDefs) ? typeDefs : [typeDefs];
// For convenience, we allow you to pass a few options that we pass through
// to a particular version of `@graphql-tools/schema`'s
// `makeExecutableSchema`. If you want to use more of this function's
// features or have more control over the version of the packages used, just
// call it yourself like `new ApolloServer({schema:
// makeExecutableSchema(...)})`.
return makeExecutableSchema({
typeDefs: augmentedTypeDefs,
resolvers,
parseOptions,
});
}
private maybeAddMocksToConstructedSchema(
schema: GraphQLSchema,
): GraphQLSchema {
const { mocks, mockEntireSchema } = this.config;
if (mocks === false) {
return schema;
}
if (!mocks && typeof mockEntireSchema === 'undefined') {
return schema;
}
return addMocksToSchema({
schema,
mocks: mocks === true || typeof mocks === 'undefined' ? {} : mocks,
preserveResolvers:
typeof mockEntireSchema === 'undefined' ? false : !mockEntireSchema,
});
}
private generateSchemaDerivedData(schema: GraphQLSchema): SchemaDerivedData {
const schemaHash = generateSchemaHash(schema!);
return {
schema,
schemaHash,
// The DocumentStore is schema-derived because we put documents in it
// after checking that they pass GraphQL validation against the schema and
// use this to skip validation as well as parsing. So we can't reuse the
// same DocumentStore for different schemas because that might make us
// treat invalid operations as valid. If we're using the default
// DocumentStore, then we just create it from scratch each time we get a
// new schema. If we're using a user-provided DocumentStore, then we use a
// random prefix each time we get a new schema.
documentStore:
this.config.documentStore === undefined
? new InMemoryLRUCache()
: this.config.documentStore === null
? null
: new PrefixingKeyValueCache(
this.config.documentStore,
`${uuid.v4()}:`,
),
};
}
public async stop() {
switch (this.state.phase) {
case 'initialized':
case 'starting':
case 'failed to start':
throw Error(
'apolloServer.stop() should only be called after `await apolloServer.start()` has succeeded',
);
// Calling stop more than once should have the same result as the first time.
case 'stopped':
if (this.state.stopError) {
throw this.state.stopError;
}
return;
// Two parallel calls to stop; just wait for the other one to finish and
// do whatever it did.
case 'stopping':
case 'draining': {
await this.state.barrier;
// The cast here is because TS doesn't understand that this.state can
// change during the await
// (https://github.com/microsoft/TypeScript/issues/9998).
const state = this.state as ServerState;
if (state.phase !== 'stopped') {
throw Error(`Surprising post-stopping state ${state.phase}`);
}
if (state.stopError) {
throw state.stopError;
}
return;
}
case 'started':
// This is handled by the rest of the function.
break;
default:
throw new UnreachableCaseError(this.state);
}
const barrier = resolvable();
// Commit to stopping and start draining servers.
this.state = {
phase: 'draining',
schemaManager: this.state.schemaManager,
barrier,
};
try {
await this.drainServers?.();
// Servers are drained. Prevent further operations from starting and call
// stop handlers.
this.state = { phase: 'stopping', barrier };
// We run shutdown handlers in two phases because we don't want to turn
// off our signal listeners (ie, allow signals to kill the process) until
// we've done the important parts of shutdown like running serverWillStop
// handlers. (We can make this more generic later if it's helpful.)
await Promise.all([...this.toDispose].map((dispose) => dispose()));
await Promise.all([...this.toDisposeLast].map((dispose) => dispose()));
} catch (stopError) {
this.state = { phase: 'stopped', stopError: stopError as Error };
barrier.resolve();
throw stopError;
}
this.state = { phase: 'stopped', stopError: null };
}
protected serverlessFramework(): boolean {
return false;
}
private ensurePluginInstantiation(
userPlugins: PluginDefinition[] = [],
isDev: boolean,
): void {
this.plugins = userPlugins.map((plugin) => {
if (typeof plugin === 'function') {
return plugin();
}
return plugin;
});
const alreadyHavePluginWithInternalId = (id: InternalPluginId) =>
this.plugins.some(
(p) => pluginIsInternal(p) && p.__internal_plugin_id__() === id,
);
// Special case: cache control is on unless you explicitly disable it.
{
if (!alreadyHavePluginWithInternalId('CacheControl')) {
this.plugins.push(ApolloServerPluginCacheControl());
}
}
// Special case: usage reporting is on by default (and first!) if you
// configure an API key.
{
const alreadyHavePlugin =
alreadyHavePluginWithInternalId('UsageReporting');
if (!alreadyHavePlugin && this.apolloConfig.key) {
if (this.apolloConfig.graphRef) {
// Keep this plugin first so it wraps everything. (Unfortunately despite
// the fact that the person who wrote this line also was the original
// author of the comment above in #1105, they don't quite understand why this was important.)
this.plugins.unshift(ApolloServerPluginUsageReporting());
} else {
this.logger.warn(
'You have specified an Apollo key but have not specified a graph ref; usage ' +
'reporting is disabled. To enable usage reporting, set the `APOLLO_GRAPH_REF` ' +
'environment variable to `your-graph-id@your-graph-variant`. To disable this ' +
'warning, install `ApolloServerPluginUsageReportingDisabled`.',
);
}
}
}
// Special case: schema reporting can be turned on via environment variable.
{
const alreadyHavePlugin =
alreadyHavePluginWithInternalId('SchemaReporting');
const enabledViaEnvVar = process.env.APOLLO_SCHEMA_REPORTING === 'true';
if (!alreadyHavePlugin && enabledViaEnvVar) {
if (this.apolloConfig.key) {
const options: ApolloServerPluginSchemaReportingOptions = {};
this.plugins.push(ApolloServerPluginSchemaReporting(options));
} else {
throw new Error(
"You've enabled schema reporting by setting the APOLLO_SCHEMA_REPORTING " +
'environment variable to true, but you also need to provide your ' +
'Apollo API key, via the APOLLO_KEY environment ' +
'variable or via `new ApolloServer({apollo: {key})',
);
}
}
}
// Special case: inline tracing is on by default for federated schemas.
{
const alreadyHavePlugin = alreadyHavePluginWithInternalId('InlineTrace');
if (!alreadyHavePlugin) {
// If we haven't explicitly disabled inline tracing via
// ApolloServerPluginInlineTraceDisabled or explicitly installed our own
// ApolloServerPluginInlineTrace, we set up inline tracing in "only if
// federated" mode. (This is slightly different than the
// pre-ApolloServerPluginInlineTrace where we would also avoid doing
// this if an API key was configured and log a warning.)
this.plugins.push(
ApolloServerPluginInlineTrace({ __onlyIfSchemaIsFederated: true }),
);
}
}
// Special case: If we're not in production, show our default landing page.
//
// This works a bit differently from the other implicitly installed plugins,
// which rely entirely on the __internal_plugin_id__ to decide whether the
// plugin takes effect. That's because we want third-party plugins to be
// able to provide a landing page that overrides the default landing page,
// without them having to know about __internal_plugin_id__. So unless we
// actively disable the default landing page with
// ApolloServerPluginLandingPageDisabled, we install the default landing
// page, but with a special flag that _start() uses to ignore it if some
// other plugin defines a renderLandingPage callback. (We can't just look
// now to see if the plugin defines renderLandingPage because we haven't run
// serverWillStart yet.)
const alreadyHavePlugin = alreadyHavePluginWithInternalId(
'LandingPageDisabled',
);
if (!alreadyHavePlugin) {
const plugin = isDev
? ApolloServerPluginLandingPageLocalDefault()
: ApolloServerPluginLandingPageProductionDefault();
if (!isImplicitlyInstallablePlugin(plugin)) {
throw Error(
'default landing page plugin should be implicitly installable?',
);
}
plugin.__internal_installed_implicitly__ = true;
this.plugins.push(plugin);
}
}
// 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(
// We ought to be able to declare this as taking ContextFunctionParams, but
// that gets us into weird business around inheritance, since a subclass (eg
// Lambda subclassing Express) may have a different ContextFunctionParams.
// So it's the job of the subclass's function that calls this function to
// make sure that its argument properly matches the particular subclass's
// context params type.
integrationContextArgument?: any,
): Promise<GraphQLServerOptions> {
const { schema, schemaHash, documentStore } = await this._ensureStarted();
let context: Context = this.context ? this.context : {};
try {
context =
typeof this.context === 'function'
? await this.context(integrationContextArgument || {})
: context;
} catch (error) {
// Defer context error resolution to inside of runQuery
context = () => {
throw error;
};
}
return {
schema,
schemaHash,
logger: this.logger,
plugins: this.plugins,
documentStore,
dangerouslyDisableValidation: this.config.dangerouslyDisableValidation,
context,
parseOptions: this.parseOptions,
...this.requestOptions,
};
}
/**
* This method is primarily meant for testing: it allows you to execute a
* GraphQL operation via the request pipeline without going through the HTTP layer.
* Note that this means that any handling you do
* in your server at the HTTP level will not affect this call!
*
* For convenience, you can provide `request.query` either as a string or a
* DocumentNode, in case you choose to use the gql tag in your tests. This is
* just a convenience, not an optimization (we convert provided ASTs back into
* string).
*
* If you pass a second argument to this method and your ApolloServer's
* `context` is a function, that argument will be passed directly to your
* `context` function. It is your responsibility to make it as close as needed
* by your `context` function to the integration-specific argument that your
* integration passes to `context` (eg, for `apollo-server-express`, the
* `{req: express.Request, res: express.Response }` object) and to keep it
* updated as you upgrade Apollo Server.
*/
public async executeOperation(
request: Omit<GraphQLRequest, 'query'> & {
query?: string | DocumentNode;
},
integrationContextArgument?: ContextFunctionParams,
) {
// Since this function is mostly for testing, you don't need to explicitly
// start your server before calling it. (That also means you can use it with
// `apollo-server` which doesn't support `start()`.)
if (this.state.phase === 'initialized') {
await this._start();
}
const options = await this.graphQLServerOptions(integrationContextArgument);
if (typeof options.context === 'function') {
options.context = (options.context as () => never)();
} else if (typeof options.context === 'object') {
// TODO: We currently shallow clone the context for every request,
// but that's unlikely to be what people want.
// We allow passing in a function for `context` to ApolloServer,
// but this only runs once for a batched request (because this is resolved
// in ApolloServer#graphQLServerOptions, before runHttpQuery is invoked).
// NOTE: THIS IS DUPLICATED IN runHttpQuery.ts' buildRequestContext.
options.context = cloneObject(options.context);
}
const requestCtx: GraphQLRequestContext = {
logger: this.logger,
schema: options.schema,
schemaHash: options.schemaHash,
request: {
...request,
query:
request.query && typeof request.query !== 'string'
? print(request.query)
: request.query,
},
context: options.context || Object.create(null),
cache: options.cache!,
metrics: {},
response: {
http: {
headers: new Headers(),
},
},
debug: options.debug,
overallCachePolicy: newCachePolicy(),
requestIsBatched: false,
};
return processGraphQLRequest(options, requestCtx);
}
// This method is called by integrations after start() (because we want
// renderLandingPage callbacks to be able to take advantage of the context
// passed to serverWillStart); it returns the LandingPage from the (single)
// plugin `renderLandingPage` callback if it exists and returns what it
// returns to the integration. The integration should serve the HTML page when
// requested with `accept: text/html`. If no landing page is defined by any
// plugin, returns null. (Specifically null and not undefined; some serverless
// integrations rely on this to tell the difference between "haven't called
// renderLandingPage yet" and "there is no landing page").
protected getLandingPage(): LandingPage | null {
this.assertStarted('getLandingPage');
return this.landingPage;
}
}
export type ImplicitlyInstallablePlugin = ApolloServerPlugin & {
__internal_installed_implicitly__: boolean;
};
export function isImplicitlyInstallablePlugin(
p: ApolloServerPlugin,
): p is ImplicitlyInstallablePlugin {
return '__internal_installed_implicitly__' in p;
}