UNPKG

@subsquid/apollo-server-core

Version:
1,067 lines (977 loc) 40.7 kB
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 @apollo/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; }