diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 5105bfbcd93..7d8683d0a97 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -22,6 +22,8 @@ import { GraphQLRequestContextDidEncounterErrors, GraphQLRequestContextDidResolveOperation, Logger, + ApolloConfig, + WithRequired, } from 'apollo-server-types'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { defaultEngineReportingSignature } from 'apollo-graphql'; @@ -30,8 +32,6 @@ import { reportingLoop, SchemaReporter } from './schemaReporter'; import { v4 as uuidv4 } from 'uuid'; import { createHash } from 'crypto'; -let warnedOnDeprecatedApiKey = false; - export interface ClientInfo { clientName?: string; clientVersion?: string; @@ -69,75 +69,6 @@ export type GenerateClientInfo = ( requestContext: GraphQLRequestContext, ) => ClientInfo; -// AS3: Drop support for deprecated `ENGINE_API_KEY`. -export function getEngineApiKey({ - engine, - skipWarn = false, - logger = console, -}: { - engine: EngineReportingOptions | boolean | undefined; - skipWarn?: boolean; - logger?: Logger; -}) { - if (typeof engine === 'object') { - if (engine.apiKey) { - return engine.apiKey; - } - } - const legacyApiKeyFromEnv = process.env.ENGINE_API_KEY; - const apiKeyFromEnv = process.env.APOLLO_KEY; - - if (legacyApiKeyFromEnv && apiKeyFromEnv && !skipWarn) { - logger.warn( - 'Using `APOLLO_KEY` since `ENGINE_API_KEY` (deprecated) is also set in the environment.', - ); - } - if (legacyApiKeyFromEnv && !warnedOnDeprecatedApiKey && !skipWarn) { - logger.warn( - '[deprecated] The `ENGINE_API_KEY` environment variable has been renamed to `APOLLO_KEY`.', - ); - warnedOnDeprecatedApiKey = true; - } - return apiKeyFromEnv || legacyApiKeyFromEnv || ''; -} - -// AS3: Drop support for deprecated `ENGINE_SCHEMA_TAG`. -export function getEngineGraphVariant( - engine: EngineReportingOptions | boolean | undefined, - logger: Logger = console, -): string | undefined { - if (engine === false) { - return; - } else if ( - typeof engine === 'object' && - (engine.graphVariant || engine.schemaTag) - ) { - if (engine.graphVariant && engine.schemaTag) { - throw new Error( - 'Cannot set both engine.graphVariant and engine.schemaTag. Please use engine.graphVariant.', - ); - } - if (engine.schemaTag) { - logger.warn( - '[deprecated] The `schemaTag` property within `engine` configuration has been renamed to `graphVariant`.', - ); - } - return engine.graphVariant || engine.schemaTag; - } else { - if (process.env.ENGINE_SCHEMA_TAG) { - logger.warn( - '[deprecated] The `ENGINE_SCHEMA_TAG` environment variable has been renamed to `APOLLO_GRAPH_VARIANT`.', - ); - } - if (process.env.ENGINE_SCHEMA_TAG && process.env.APOLLO_GRAPH_VARIANT) { - throw new Error( - '`APOLLO_GRAPH_VARIANT` and `ENGINE_SCHEMA_TAG` (deprecated) environment variables must not both be set.', - ); - } - return process.env.APOLLO_GRAPH_VARIANT || process.env.ENGINE_SCHEMA_TAG; - } -} - export interface EngineReportingOptions { /** * API key for the service. Get this from @@ -429,9 +360,8 @@ class ReportData { // to the Engine server. export class EngineReportingAgent { private readonly options: EngineReportingOptions; - private readonly apiKey: string; + private readonly apolloConfig: WithRequired; private readonly logger: Logger = console; - private readonly graphVariant: string; private readonly reportDataByExecutableSchemaId: { [executableSchemaId: string]: ReportData | undefined; @@ -452,29 +382,25 @@ export class EngineReportingAgent { private readonly tracesEndpointUrl: string; readonly schemaReport: boolean; - public constructor(options: EngineReportingOptions = {}) { + public constructor( + options: EngineReportingOptions = {}, + apolloConfig: WithRequired, + ) { this.options = options; - this.apiKey = getEngineApiKey({ - engine: this.options, - skipWarn: false, - logger: this.logger, - }); if (options.logger) this.logger = options.logger; this.bootId = uuidv4(); - this.graphVariant = getEngineGraphVariant(options, this.logger) || ''; - if (!this.apiKey) { - throw new Error( - `To use EngineReportingAgent, you must specify an API key via the apiKey option or the APOLLO_KEY environment variable.`, - ); + if (!apolloConfig.key) { + throw new Error('Missing API key.'); } + this.apolloConfig = apolloConfig; if (options.experimental_schemaReporting !== undefined) { this.logger.warn( [ '[deprecated] The "experimental_schemaReporting" option has been', - 'renamed to "reportSchema"' - ].join(' ') + 'renamed to "reportSchema"', + ].join(' '), ); if (options.reportSchema === undefined) { options.reportSchema = options.experimental_schemaReporting; @@ -485,11 +411,12 @@ export class EngineReportingAgent { this.logger.warn( [ '[deprecated] The "experimental_overrideReportedSchema" option has', - 'been renamed to "overrideReportedSchema"' - ].join(' ') + 'been renamed to "overrideReportedSchema"', + ].join(' '), ); if (options.overrideReportedSchema === undefined) { - options.overrideReportedSchema = options.experimental_overrideReportedSchema; + options.overrideReportedSchema = + options.experimental_overrideReportedSchema; } } @@ -497,18 +424,19 @@ export class EngineReportingAgent { this.logger.warn( [ '[deprecated] The "experimental_schemaReportingInitialDelayMaxMs"', - 'option has been renamed to "schemaReportingInitialDelayMaxMs"' - ].join(' ') + 'option has been renamed to "schemaReportingInitialDelayMaxMs"', + ].join(' '), ); if (options.schemaReportingInitialDelayMaxMs === undefined) { - options.schemaReportingInitialDelayMaxMs = options.experimental_schemaReportingInitialDelayMaxMs; + options.schemaReportingInitialDelayMaxMs = + options.experimental_schemaReportingInitialDelayMaxMs; } } if (options.reportSchema !== undefined) { this.schemaReport = options.reportSchema; } else { - this.schemaReport = process.env.APOLLO_SCHEMA_REPORTING === "true" + this.schemaReport = process.env.APOLLO_SCHEMA_REPORTING === 'true'; } // Since calculating the signature for Engine reporting is potentially an @@ -567,7 +495,7 @@ export class EngineReportingAgent { if (existing) { return existing; } - const reportData = new ReportData(executableSchemaId, this.graphVariant); + const reportData = new ReportData(executableSchemaId, this.apolloConfig.graphVariant); this.reportDataByExecutableSchemaId[executableSchemaId] = reportData; return reportData; } @@ -636,7 +564,7 @@ export class EngineReportingAgent { public async sendAllReports(): Promise { await Promise.all( - Object.keys(this.reportDataByExecutableSchemaId).map(id => + Object.keys(this.reportDataByExecutableSchemaId).map((id) => this.sendReport(id), ), ); @@ -702,7 +630,7 @@ export class EngineReportingAgent { method: 'POST', headers: { 'user-agent': 'apollo-engine-reporting', - 'x-api-key': this.apiKey, + 'x-api-key': this.apolloConfig.key!, 'content-encoding': 'gzip', }, body: compressed, @@ -711,8 +639,9 @@ export class EngineReportingAgent { if (curResponse.status >= 500 && curResponse.status < 600) { throw new Error( - `HTTP status ${curResponse.status}, ${(await curResponse.text()) || - '(no body)'}`, + `HTTP status ${curResponse.status}, ${ + (await curResponse.text()) || '(no body)' + }`, ); } else { return curResponse; @@ -763,14 +692,14 @@ export class EngineReportingAgent { this.logger.info('Schema to report has been overridden'); } if (this.options.schemaReportingInitialDelayMaxMs !== undefined) { - this.logger.info(`Schema reporting max initial delay override: ${ - this.options.schemaReportingInitialDelayMaxMs - } ms`); + this.logger.info( + `Schema reporting max initial delay override: ${this.options.schemaReportingInitialDelayMaxMs} ms`, + ); } if (this.options.schemaReportingUrl !== undefined) { - this.logger.info(`Schema reporting URL override: ${ - this.options.schemaReportingUrl - }`); + this.logger.info( + `Schema reporting URL override: ${this.options.schemaReportingUrl}`, + ); } if (this.currentSchemaReporter) { this.currentSchemaReporter.stop(); @@ -778,7 +707,7 @@ export class EngineReportingAgent { const serverInfo = { bootId: this.bootId, - graphVariant: this.graphVariant, + graphVariant: this.apolloConfig.graphVariant, // The infra environment in which this edge server is running, e.g. localhost, Kubernetes // Length must be <= 256 characters. platform: process.env.APOLLO_SERVER_PLATFORM || 'local', @@ -796,21 +725,20 @@ export class EngineReportingAgent { }; this.logger.info( - `Schema reporting EdgeServerInfo: ${JSON.stringify(serverInfo)}` - ) + `Schema reporting EdgeServerInfo: ${JSON.stringify(serverInfo)}`, + ); // Jitter the startup between 0 and 10 seconds const delay = Math.floor( - Math.random() * - (this.options.schemaReportingInitialDelayMaxMs || 10_000), + Math.random() * (this.options.schemaReportingInitialDelayMaxMs || 10_000), ); const schemaReporter = new SchemaReporter( serverInfo, executableSchema, - this.apiKey, + this.apolloConfig.key!, this.options.schemaReportingUrl, - this.logger + this.logger, ); const fallbackReportingDelayInMs = 20_000; @@ -818,7 +746,7 @@ export class EngineReportingAgent { this.currentSchemaReporter = schemaReporter; const logger = this.logger; - setTimeout(function() { + setTimeout(function () { reportingLoop(schemaReporter, logger, false, fallbackReportingDelayInMs); }, delay); } @@ -896,7 +824,7 @@ export class EngineReportingAgent { // either the request-specific logger on the request context (if available) // or to the `logger` that was passed into `EngineReportingOptions` which // is provided in the `EngineReportingAgent` constructor options. - this.signatureCache.set(cacheKey, generatedSignature).catch(err => { + this.signatureCache.set(cacheKey, generatedSignature).catch((err) => { logger.warn( 'Could not store signature cache. ' + (err && err.message) || err, ); @@ -907,14 +835,16 @@ export class EngineReportingAgent { public async sendAllReportsAndReportErrors(): Promise { await Promise.all( - Object.keys(this.reportDataByExecutableSchemaId).map(executableSchemaId => + Object.keys( + this.reportDataByExecutableSchemaId, + ).map((executableSchemaId) => this.sendReportAndReportErrors(executableSchemaId), ), ); } private sendReportAndReportErrors(executableSchemaId: string): Promise { - return this.sendReport(executableSchemaId).catch(err => { + return this.sendReport(executableSchemaId).catch((err) => { // This catch block is primarily intended to catch network errors from // the retried request itself, which include network errors and non-2xx // HTTP errors. diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index eda7a85adce..0d22ef72941 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -66,7 +66,6 @@ import { import { generateSchemaHash } from './utils/schemaHash'; import { isDirectiveDefined } from './utils/isDirectiveDefined'; -import createSHA from './utils/createSHA'; import { processGraphQLRequest, GraphQLRequestContext, @@ -77,14 +76,15 @@ import { import { Headers } from 'apollo-server-env'; import { buildServiceDefinition } from '@apollographql/apollo-tools'; import { plugin as pluginTracing } from "apollo-tracing"; -import { Logger, SchemaHash, ValueOrPromise } from "apollo-server-types"; +import { Logger, SchemaHash, ValueOrPromise, ApolloConfig, WithRequired } from "apollo-server-types"; import { plugin as pluginCacheControl, CacheControlExtensionOptions, } from 'apollo-cache-control'; -import { getEngineApiKey, getEngineGraphVariant } from "apollo-engine-reporting/dist/agent"; import { cloneObject } from "./runHttpQuery"; import isNodeLike from './utils/isNodeLike'; +import { determineApolloConfig } from './determineApolloConfig'; +import { federatedPlugin, EngineReportingAgent } from 'apollo-engine-reporting'; const NoIntrospection = (context: ValidationContext) => ({ Field(node: FieldDefinitionNode) { @@ -99,15 +99,6 @@ const NoIntrospection = (context: ValidationContext) => ({ }, }); -function getEngineServiceId(engine: Config['engine'], logger: Logger): string | undefined { - const engineApiKey = getEngineApiKey({engine, skipWarn: true, logger} ); - if (engineApiKey) { - return engineApiKey.split(':', 2)[1]; - } - - return; -} - const forbidUploadsForTesting = process && process.env.NODE_ENV === 'test' && !runtimeSupportsUploads; @@ -132,9 +123,8 @@ export class ApolloServerBase { public requestOptions: Partial> = Object.create(null); private context?: Context | ContextFunction; - private engineReportingAgent?: import('apollo-engine-reporting').EngineReportingAgent; - private engineServiceId?: string; - private engineApiKeyHash?: string; + private engineReportingAgent?: EngineReportingAgent; + private apolloConfig: ApolloConfig; protected plugins: ApolloServerPlugin[] = []; protected subscriptionServerOptions?: SubscriptionServerOptions; @@ -171,7 +161,6 @@ export class ApolloServerBase { mocks, mockEntireSchema, extensions, - engine, subscriptions, uploads, playground, @@ -180,9 +169,20 @@ export class ApolloServerBase { cacheControl, experimental_approximateDocumentStoreMiB, stopOnTerminationSignals, + apollo, + engine, ...requestOptions } = config; + if (engine !== undefined) { + // FIXME(no-engine): finish implementing backwards-compatibility `engine` + // mode and warn about deprecation + if (apollo) { + throw new Error("You cannot provide both `engine` and `apollo` to `new ApolloServer()`. " + + "For details on how to migrate all of your options out of `engine`, see FIXME(no-engine) URL MISSING"); + } + } + // Setup logging facilities if (config.logger) { this.logger = config.logger; @@ -205,6 +205,8 @@ export class ApolloServerBase { this.logger = loglevelLogger; } + this.apolloConfig = determineApolloConfig(apollo, engine, this.logger); + if (gateway && (modules || schema || typeDefs || resolvers)) { throw new Error( 'Cannot define both `gateway` and any of: `modules`, `schema`, `typeDefs`, or `resolvers`', @@ -299,24 +301,17 @@ export class ApolloServerBase { } } - // In an effort to avoid over-exposing the API key itself, extract the - // service ID from the API key for plugins which only needs service ID. - // The truthiness of this value can also be used in other forks of logic - // related to Engine, as is the case with EngineReportingAgent just below. - this.engineServiceId = getEngineServiceId(engine, this.logger); - const apiKey = getEngineApiKey({engine, skipWarn: true, logger: this.logger}); - if (apiKey) { - this.engineApiKeyHash = createSHA('sha512') - .update(apiKey) - .digest('hex'); - } - - if (this.engineServiceId) { - const { EngineReportingAgent } = require('apollo-engine-reporting'); + if (this.apolloConfig.key) { + // FIXME(no-engine) Eliminate EngineReportingAgent entirely. this.engineReportingAgent = new EngineReportingAgent( typeof engine === 'object' ? engine : Object.create({ logger: this.logger, }), + // This isn't part of EngineReportingOptions because it's generated + // internally by ApolloServer, not part of the end-user options... and + // hopefully EngineReportingAgent will be eliminated before this code + // is shipped anyway. + this.apolloConfig as WithRequired, ); // Don't add the extension here (we want to add it later in generateSchemaDerivedData). } @@ -395,9 +390,9 @@ export class ApolloServerBase { if ( typeof stopOnTerminationSignals === 'boolean' ? stopOnTerminationSignals - : typeof this.config.engine === 'object' && - typeof this.config.engine.handleSignals === 'boolean' - ? this.config.engine.handleSignals + : typeof engine === 'object' && + typeof engine.handleSignals === 'boolean' + ? engine.handleSignals : isNodeLike && process.env.NODE_ENV !== 'test' ) { const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM']; @@ -425,7 +420,6 @@ export class ApolloServerBase { private initSchema(): GraphQLSchema | Promise { const { gateway, - engine, schema, modules, typeDefs, @@ -445,13 +439,13 @@ export class ApolloServerBase { ), ); - const graphVariant = getEngineGraphVariant(engine, this.logger); + // For backwards compatibility with old versions of @apollo/gateway. const engineConfig = - this.engineApiKeyHash && this.engineServiceId + this.apolloConfig.keyHash && this.apolloConfig.graphId ? { - apiKeyHash: this.engineApiKeyHash, - graphId: this.engineServiceId, - ...(graphVariant && { graphVariant }), + apiKeyHash: this.apolloConfig.keyHash, + graphId: this.apolloConfig.graphId, + graphVariant: this.apolloConfig.graphVariant, } : undefined; @@ -461,7 +455,7 @@ export class ApolloServerBase { // a federated schema! this.requestOptions.executor = gateway.executor; - return gateway.load({ engine: engineConfig }) + return gateway.load({ apollo: this.apolloConfig, engine: engineConfig }) .then(config => config.schema) .catch(err => { // We intentionally do not re-throw the exact error from the gateway @@ -597,9 +591,10 @@ export class ApolloServerBase { logger: this.logger, schema: schema, schemaHash: schemaHash, + apollo: this.apolloConfig, engine: { - serviceID: this.engineServiceId, - apiKeyHash: this.engineApiKeyHash, + serviceID: this.apolloConfig.graphId, + apiKeyHash: this.apolloConfig.keyHash, }, }; @@ -838,7 +833,6 @@ export class ApolloServerBase { // We haven't configured this app to use Engine directly. But it looks like // we are a federated service backend, so we should be capable of including // our trace in a response extension if we are asked to by the gateway. - const { federatedPlugin } = require('apollo-engine-reporting'); const rewriteError = engine && typeof engine === 'object' ? engine.rewriteError : undefined; pluginsToInit.push(federatedPlugin({ rewriteError })); diff --git a/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts b/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts index 52013e02fe6..f7df01739e3 100644 --- a/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts +++ b/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts @@ -1,6 +1,7 @@ import { ApolloServerBase } from '../ApolloServer'; import { buildServiceDefinition } from '@apollographql/apollo-tools'; import gql from 'graphql-tag'; +import { Logger } from 'apollo-server-types'; const typeDefs = gql` type Query { @@ -45,7 +46,10 @@ describe('ApolloServerBase construction', () => { ).not.toThrow(); }); - it('spits out a deprecation warning when passed a schemaTag in construction', () => { + // FIXME(no-engine): This should be changed to check for a deprecation + // warning for any use of `engine` (which we can't really do until splitting + // out the plugins). + it.skip('spits out a deprecation warning when passed a schemaTag in construction', () => { const spyConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(); expect( () => @@ -112,32 +116,47 @@ describe('environment variables', () => { it('constructs a reporting agent with the ENGINE_API_KEY (deprecated) environment variable and warns', async () => { // set the variables process.env.ENGINE_API_KEY = 'just:fake:stuff'; - const spyConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + const warn = jest.fn(); + const mockLogger: Logger = { + debug: jest.fn(), + info: jest.fn(), + warn, + error: jest.fn(), + }; const server = new ApolloServerBase({ typeDefs, - resolvers + resolvers, + apollo: { graphVariant: 'xxx' }, + logger: mockLogger, }); await server.stop(); - expect(spyConsoleWarn).toHaveBeenCalledTimes(1); - spyConsoleWarn.mockReset(); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls[0][0]).toMatch(/deprecated.*ENGINE_API_KEY/); }); it('warns with both the legacy env var and new env var set', async () => { // set the variables process.env.ENGINE_API_KEY = 'just:fake:stuff'; process.env.APOLLO_KEY = 'also:fake:stuff'; - const spyConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + const warn = jest.fn(); + const mockLogger: Logger = { + debug: jest.fn(), + info: jest.fn(), + warn, + error: jest.fn(), + }; const server = new ApolloServerBase({ typeDefs, - resolvers + resolvers, + apollo: { graphVariant: 'xxx' }, + logger: mockLogger, }); await server.stop(); - // Once for deprecation, once for double-set - expect(spyConsoleWarn).toHaveBeenCalledTimes(2); - spyConsoleWarn.mockReset(); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls[0][0]).toMatch(/Using.*APOLLO_KEY.*ENGINE_API_KEY/); }); }); diff --git a/packages/apollo-server-core/src/determineApolloConfig.ts b/packages/apollo-server-core/src/determineApolloConfig.ts new file mode 100644 index 00000000000..1148057eb7f --- /dev/null +++ b/packages/apollo-server-core/src/determineApolloConfig.ts @@ -0,0 +1,109 @@ +import { ApolloConfig, ApolloConfigInput, Logger } from 'apollo-server-types'; +import { EngineReportingOptions } from 'apollo-engine-reporting'; +import createSHA from './utils/createSHA'; + +// This function combines the newer `apollo` constructor argument, the older +// `engine` constructor argument, and some environment variables to come up +// with a full ApolloConfig. +// +// The caller ensures that only one of the two constructor arguments is actually +// provided and warns if `engine` was provided, but it is this function's job +// to warn if old environment variables are used. +export function determineApolloConfig( + input: ApolloConfigInput | undefined, + // For backwards compatibility. + // AS3: Drop support for deprecated 'engine'. + engine: EngineReportingOptions | boolean | undefined, + logger: Logger, +): ApolloConfig { + if (input && engine !== undefined) { + // There's a more helpful error in the actual ApolloServer constructor. + throw Error('Cannot pass both `apollo` and `engine`'); + } + const apolloConfig: ApolloConfig = { graphVariant: 'current' }; + + const { + APOLLO_KEY, + APOLLO_GRAPH_ID, + APOLLO_GRAPH_VARIANT, + // AS3: Drop support for deprecated `ENGINE_API_KEY` and `ENGINE_SCHEMA_TAG`. + ENGINE_API_KEY, + ENGINE_SCHEMA_TAG, + } = process.env; + + // Determine key. + if (input?.key) { + apolloConfig.key = input.key; + } else if (typeof engine === 'object' && engine.apiKey) { + apolloConfig.key = engine.apiKey; + } else if (APOLLO_KEY) { + if (ENGINE_API_KEY) { + logger.warn( + 'Using `APOLLO_KEY` since `ENGINE_API_KEY` (deprecated) is also set in the environment.', + ); + } + apolloConfig.key = APOLLO_KEY; + } else if (ENGINE_API_KEY) { + logger.warn( + '[deprecated] The `ENGINE_API_KEY` environment variable has been renamed to `APOLLO_KEY`.', + ); + apolloConfig.key = ENGINE_API_KEY; + } + + // Determine key hash. + if (apolloConfig.key) { + apolloConfig.keyHash = createSHA('sha512') + .update(apolloConfig.key) + .digest('hex'); + } + + // Determine graph id. + if (input?.graphId) { + apolloConfig.graphId = input.graphId; + } else if (APOLLO_GRAPH_ID) { + apolloConfig.graphId = APOLLO_GRAPH_ID; + } else if (apolloConfig.key) { + // This is the common case: if the given key is a graph token (starts with 'service:'), + // then use the service name written in the key. + const parts = apolloConfig.key.split(':', 2); + if (parts[0] === 'service') { + apolloConfig.graphId = parts[1]; + } + } + + // Determine variant. + if (input?.graphVariant) { + apolloConfig.graphVariant = input.graphVariant; + } else if (typeof engine === 'object' && engine.graphVariant) { + if (engine.schemaTag) { + throw new Error( + 'Cannot set more than one of apollo.graphVariant, ' + + 'engine.graphVariant, and engine.schemaTag. Please use apollo.graphVariant.', + ); + } + apolloConfig.graphVariant = engine.graphVariant; + } else if (typeof engine === 'object' && engine.schemaTag) { + // No need to warn here, because ApolloServer's constructor should warn about + // the existence of `engine` at all. + apolloConfig.graphVariant = engine.schemaTag; + } else if (APOLLO_GRAPH_VARIANT) { + if (ENGINE_SCHEMA_TAG) { + throw new Error( + '`APOLLO_GRAPH_VARIANT` and `ENGINE_SCHEMA_TAG` (deprecated) environment variables must not both be set.', + ); + } + apolloConfig.graphVariant = APOLLO_GRAPH_VARIANT; + } else if (ENGINE_SCHEMA_TAG) { + logger.warn( + '[deprecated] The `ENGINE_SCHEMA_TAG` environment variable has been renamed to `APOLLO_GRAPH_VARIANT`.', + ); + apolloConfig.graphVariant = ENGINE_SCHEMA_TAG; + } else if (apolloConfig.key) { + // Leave the value 'current' in apolloConfig.graphVariant. + // We warn if it looks like they're trying to use Apollo registry features, but there's + // no reason to warn if there's no key. + logger.warn('No graph variant provided. Defaulting to `current`.'); + } + + return apolloConfig; +} diff --git a/packages/apollo-server-core/src/types.ts b/packages/apollo-server-core/src/types.ts index 491be13340a..96f1b6a80dd 100644 --- a/packages/apollo-server-core/src/types.ts +++ b/packages/apollo-server-core/src/types.ts @@ -6,10 +6,12 @@ import { GraphQLParseOptions, } from 'graphql-tools'; import { + ApolloConfig, ValueOrPromise, GraphQLExecutor, GraphQLExecutionResult, GraphQLRequestContextExecutionDidStart, + ApolloConfigInput, } from 'apollo-server-types'; import { ConnectionContext } from 'subscriptions-transport-ws'; // The types for `ws` use `export = WebSocket`, so we'll use the @@ -90,6 +92,7 @@ export type GraphQLServiceEngineConfig = { export interface GraphQLService { load(options: { + apollo?: ApolloConfig, engine?: GraphQLServiceEngineConfig; }): Promise; onSchemaChange(callback: SchemaChangeCallback): Unsubscriber; @@ -113,7 +116,6 @@ export interface Config extends BaseConfig { introspection?: boolean; mocks?: boolean | IMocks; mockEntireSchema?: boolean; - engine?: boolean | EngineReportingOptions; extensions?: Array<() => GraphQLExtension>; cacheControl?: CacheControlExtensionOptions | boolean; plugins?: PluginDefinition[]; @@ -125,8 +127,11 @@ export interface Config extends BaseConfig { gateway?: GraphQLService; experimental_approximateDocumentStoreMiB?: number; stopOnTerminationSignals?: boolean; + apollo?: ApolloConfigInput; + engine?: boolean | EngineReportingOptions; } +// Configuration for how Apollo Server talks to the Apollo registry. export interface FileUploadOptions { //Max allowed non-file multipart form field size in bytes; enough for your queries (default: 1 MB). maxFieldSize?: number; diff --git a/packages/apollo-server-core/src/utils/pluginTestHarness.ts b/packages/apollo-server-core/src/utils/pluginTestHarness.ts index af034f07e5e..30ce5d35554 100644 --- a/packages/apollo-server-core/src/utils/pluginTestHarness.ts +++ b/packages/apollo-server-core/src/utils/pluginTestHarness.ts @@ -105,6 +105,9 @@ export default async function pluginTestHarness({ logger: logger || console, schema, schemaHash, + apollo: { + graphVariant: 'current', + }, engine: {}, }); if (maybeServerListener && maybeServerListener.serverWillStop) { diff --git a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts index a1efd8ce159..e8dbace7cfa 100644 --- a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts +++ b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts @@ -1001,6 +1001,7 @@ export function testApolloServer( validationRules: [validationRule], extensions: [() => new Extension()], engine: { + graphVariant: 'current', ...engineServer.engineOptions(), apiKey: 'service:my-app:secret', maxUncompressedReportSize: 1, @@ -1076,6 +1077,7 @@ export function testApolloServer( }, }, engine: { + graphVariant: 'current', ...engineServer.engineOptions(), apiKey: 'service:my-app:secret', maxUncompressedReportSize: 1, @@ -2460,6 +2462,7 @@ export function testApolloServer( resolvers: { Query: { something: () => 'hello' } }, engine: { apiKey: 'service:my-app:secret', + graphVariant: 'current', tracesEndpointUrl: fakeEngineUrl, reportIntervalMs: 1, maxAttempts: 3, @@ -3213,6 +3216,13 @@ export function testApolloServer( }); expect(optionsSpy).toHaveBeenLastCalledWith({ + apollo: { + key: 'service:tester:1234abc', + keyHash: + '0ca858e7fe8cffc01c5f1db917d2463b348b50d267427e54c1c8c99e557b242f4145930b949905ec430642467613610e471c40bb7a251b1e2248c399bb0498c4', + graphId: 'tester', + graphVariant: 'staging', + }, engine: { apiKeyHash: '0ca858e7fe8cffc01c5f1db917d2463b348b50d267427e54c1c8c99e557b242f4145930b949905ec430642467613610e471c40bb7a251b1e2248c399bb0498c4', diff --git a/packages/apollo-server-plugin-operation-registry/src/ApolloServerPluginOperationRegistry.ts b/packages/apollo-server-plugin-operation-registry/src/ApolloServerPluginOperationRegistry.ts index 0200d75e3bf..42ca4bc57c3 100644 --- a/packages/apollo-server-plugin-operation-registry/src/ApolloServerPluginOperationRegistry.ts +++ b/packages/apollo-server-plugin-operation-registry/src/ApolloServerPluginOperationRegistry.ts @@ -49,6 +49,8 @@ export interface Options { | boolean | ForbidUnregisteredOperationsPredicate; dryRun?: boolean; + // Deprecated; configure via `new ApolloServer({apollo: {graphVariant}})` + // or $APOLLO_GRAPH_VARIANT instead. graphVariant?: string; onUnregisteredOperation?: ( requestContext: GraphQLRequestContext, @@ -63,8 +65,6 @@ export interface Options { export default function plugin(options: Options = Object.create(null)) { let agent: Agent; let store: InMemoryLRUCache; - const graphVariant = - options.graphVariant || process.env.APOLLO_GRAPH_VARIANT || 'current'; // Setup logging facilities, scoped under the appropriate name. const logger = loglevel.getLogger(`apollo-server:${pluginName}`); @@ -95,20 +95,46 @@ for observability purposes, but all operations will be permitted.`, // time depending on the usecase. Object.freeze(options); + let graphVariant: string; + return (): ApolloServerPlugin => ({ async serverWillStart({ + apollo, engine, }: GraphQLServiceContext): Promise { + if (!apollo) { + // Older version of apollo-server-core that isn't passing 'apollo' yet. + apollo = { + graphId: engine.serviceID, + // no 'key' because it's not on this part of the API, but + // this plugin doesn't need it. + keyHash: engine.apiKeyHash, + graphVariant: process.env.APOLLO_GRAPH_VARIANT || 'current', + }; + } + // Deprecated way of passing variant directly to the plugin instead of + // just getting it from serverWillStart. + if (options.graphVariant) { + apollo = { + ...apollo, + graphVariant: options.graphVariant, + }; + } + + // Make available to requestDidStart. + graphVariant = apollo.graphVariant; logger.debug('Initializing operation registry plugin.'); - if (!engine || !engine.serviceID) { - const messageEngineConfigurationRequired = + const {graphId, keyHash} = apollo; + + if (!(graphId && keyHash)) { + const messageApolloConfigurationRequired = 'The Apollo API key must be set to use the operation registry.'; - throw new Error(`${pluginName}: ${messageEngineConfigurationRequired}`); + throw new Error(`${pluginName}: ${messageApolloConfigurationRequired}`); } logger.debug( - `Operation registry is configured for '${engine.serviceID}'.`); + `Operation registry is configured for '${apollo.graphId}'.`); // An LRU store with no `maxSize` is effectively an InMemoryStore and // exactly what we want for this purpose. @@ -117,8 +143,12 @@ for observability purposes, but all operations will be permitted.`, logger.debug('Initializing operation registry agent...'); agent = new Agent({ - graphVariant, - engine, + apollo: { + ...apollo, + // Convince TypeScript that these fields are not undefined. + graphId, + keyHash + }, store, logger, fetcher: options.fetcher, @@ -298,7 +328,7 @@ for observability purposes, but all operations will be permitted.`, Object.assign(error.extensions, { operationSignature: signature, exception: { - message: `Please register your operation with \`npx apollo client:push --tag="${graphVariant}"\`. See https://www.apollographql.com/docs/platform/operation-registry/ for more details.`, + message: `Please register your operation with \`npx apollo client:push --variant="${graphVariant}"\`. See https://www.apollographql.com/docs/platform/operation-registry/ for more details.`, }, }); throw error; diff --git a/packages/apollo-server-plugin-operation-registry/src/__tests__/ApolloServerPluginOperationRegistry.test.ts b/packages/apollo-server-plugin-operation-registry/src/__tests__/ApolloServerPluginOperationRegistry.test.ts index 79b02a2fd7e..f44fd73a77b 100644 --- a/packages/apollo-server-plugin-operation-registry/src/__tests__/ApolloServerPluginOperationRegistry.test.ts +++ b/packages/apollo-server-plugin-operation-registry/src/__tests__/ApolloServerPluginOperationRegistry.test.ts @@ -55,6 +55,7 @@ describe('Operation registry plugin', () => { const engineOptions: EngineReportingOptions = { apiKey, reportTiming: false, + graphVariant: 'current', }; const typeDefs = gql` type Query { diff --git a/packages/apollo-server-plugin-operation-registry/src/__tests__/agent.test.ts b/packages/apollo-server-plugin-operation-registry/src/__tests__/agent.test.ts index 8d73a3fdbb5..51a1f40aef0 100644 --- a/packages/apollo-server-plugin-operation-registry/src/__tests__/agent.test.ts +++ b/packages/apollo-server-plugin-operation-registry/src/__tests__/agent.test.ts @@ -157,7 +157,7 @@ describe('Agent', () => { expect(relevantLogs[0][0]).toBe( `Checking for manifest changes at ${urlResolve( fakeTestBaseUrl, - getOperationManifestUrl(genericServiceID, genericStorageSecret), + getOperationManifestUrl(genericServiceID, genericStorageSecret, 'current'), )}`, ); @@ -312,7 +312,7 @@ describe('Agent', () => { }); describe('When given a graphVariant', () => { - const graphVariant = 'main'; + const graphVariant = 'different'; const getOperationManifestRelativeUrl = ( ...args: Parameters ) => @@ -321,9 +321,11 @@ describe('Agent', () => { '', ); - it('fetches manifests for the corresponding schema tag', async () => { + it('fetches manifests for the corresponding variant', async () => { nockStorageSecret(genericServiceID, genericApiKeyHash); - const agent = createAgent({ graphVariant: graphVariant }); + const agent = createAgent({ + apollo: { ...defaultAgentOptions.apollo, graphVariant }, + }); const nockedManifest = nockBase() .get( getOperationManifestRelativeUrl( diff --git a/packages/apollo-server-plugin-operation-registry/src/__tests__/helpers.test-helpers.ts b/packages/apollo-server-plugin-operation-registry/src/__tests__/helpers.test-helpers.ts index 6158e9869bb..ec3907ec65e 100644 --- a/packages/apollo-server-plugin-operation-registry/src/__tests__/helpers.test-helpers.ts +++ b/packages/apollo-server-plugin-operation-registry/src/__tests__/helpers.test-helpers.ts @@ -20,10 +20,13 @@ export const genericApiKeyHash = 'someapikeyhash123'; export const defaultTestAgentPollSeconds = 60; export const defaultAgentOptions: AgentOptions = { - engine: { serviceID: genericServiceID, apiKeyHash: genericApiKeyHash }, + apollo: { + graphId: genericServiceID, + keyHash: genericApiKeyHash, + graphVariant: 'current', + }, store: defaultStore(), pollSeconds: defaultTestAgentPollSeconds, - graphVariant: 'current', }; // Each nock is good for exactly one request! @@ -69,6 +72,7 @@ export function getOperationManifestPath( return getOperationManifestUrl( graphId, storageSecret, + 'current', ).replace(new RegExp(`^${urlOperationManifestBase}`), ''); } diff --git a/packages/apollo-server-plugin-operation-registry/src/agent.ts b/packages/apollo-server-plugin-operation-registry/src/agent.ts index 199a7a10424..98c70df4045 100644 --- a/packages/apollo-server-plugin-operation-registry/src/agent.ts +++ b/packages/apollo-server-plugin-operation-registry/src/agent.ts @@ -11,7 +11,7 @@ import { HttpRequestCache } from './cache'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { OperationManifest } from "./ApolloServerPluginOperationRegistry"; -import { Logger } from "apollo-server-types"; +import { Logger, ApolloConfig, WithRequired } from "apollo-server-types"; import { Response, RequestInit, fetch } from "apollo-server-env"; const DEFAULT_POLL_SECONDS: number = 30; @@ -21,9 +21,8 @@ export interface AgentOptions { logger?: Logger; fetcher?: typeof fetch; pollSeconds?: number; - engine: any; + apollo: WithRequired; store: InMemoryLRUCache; - graphVariant: string; } type SignatureStore = Set; @@ -49,20 +48,6 @@ export default class Agent { this.logger = this.options.logger || loglevel.getLogger(pluginName); this.fetcher = this.options.fetcher || getDefaultGcsFetcher(); - - if ( - typeof this.options.engine !== 'object' || - typeof this.options.engine.serviceID !== 'string' - ) { - throw new Error('`engine.serviceID` must be passed to the Agent.'); - } - - if ( - typeof this.options.engine !== 'object' || - typeof this.options.engine.apiKeyHash !== 'string' - ) { - throw new Error('`engine.apiKeyHash` must be passed to the Agent.'); - } } async requestPending() { @@ -132,8 +117,8 @@ export default class Agent { private async fetchAndUpdateStorageSecret(): Promise { const storageSecretUrl = getStorageSecretUrl( - this.options.engine.serviceID, - this.options.engine.apiKeyHash, + this.options.apollo.graphId, + this.options.apollo.keyHash, ); const response = await this.fetcher(storageSecretUrl, this.fetchOptions); @@ -170,9 +155,9 @@ export default class Agent { } const storageSecretManifestUrl = getOperationManifestUrl( - this.options.engine.serviceID, + this.options.apollo.graphId, storageSecret, - this.options.graphVariant, + this.options.apollo.graphVariant, ); this.logger.debug( @@ -183,7 +168,7 @@ export default class Agent { if (response.status === 404 || response.status === 403) { throw new Error( - `No manifest found for tag "${this.options.graphVariant}" at ` + + `No manifest found for tag "${this.options.apollo.graphVariant}" at ` + `${storageSecretManifestUrl}. ${callToAction}`); } return response; @@ -218,7 +203,7 @@ export default class Agent { throw new Error(`Unexpected 'Content-Type' header: ${contentType}`); } } catch (err) { - const ourErrorPrefix = `Unable to fetch operation manifest for graph ID '${this.options.engine.serviceID}': ${err}`; + const ourErrorPrefix = `Unable to fetch operation manifest for graph ID '${this.options.apollo.graphId}': ${err}`; err.message = `${ourErrorPrefix}: ${err}`; diff --git a/packages/apollo-server-plugin-operation-registry/src/common.ts b/packages/apollo-server-plugin-operation-registry/src/common.ts index 16294d409e1..8aba17f2715 100644 --- a/packages/apollo-server-plugin-operation-registry/src/common.ts +++ b/packages/apollo-server-plugin-operation-registry/src/common.ts @@ -37,7 +37,7 @@ export function getStorageSecretUrl( export function getOperationManifestUrl( graphId: string, storageSecret: string, - graphVariant: string = 'current', + graphVariant: string, ): string { return `${urlOperationManifestBase}/${graphId}/${storageSecret}/${graphVariant}/manifest.v2.json`; } diff --git a/packages/apollo-server-types/src/index.ts b/packages/apollo-server-types/src/index.ts index 3d02d315fd1..3b277b519a1 100644 --- a/packages/apollo-server-types/src/index.ts +++ b/packages/apollo-server-types/src/index.ts @@ -51,17 +51,36 @@ type Mutable = { -readonly [P in keyof T]: T[P] }; export type SchemaHash = Fauxpaque; -export interface GraphQLServiceContext { +// Configuration for how Apollo Server talks to the Apollo registry, as +// passed to the ApolloServer constructor. +export interface ApolloConfigInput { + key?: string; + graphId?: string; + graphVariant?: string; +} + +// Configuration for how Apollo Server talks to the Apollo registry, with +// some defaults filled in from the ApolloConfigInput passed to the constructor. +export interface ApolloConfig { + key?: string; + keyHash?: string; + graphId?: string; + graphVariant: string; +} + + export interface GraphQLServiceContext { logger: Logger; schema: GraphQLSchema; schemaHash: SchemaHash; + apollo: ApolloConfig; + persistedQueries?: { + cache: KeyValueCache; + }; + // For backwards compatibility only; prefer to use `apollo`. engine: { serviceID?: string; apiKeyHash?: string; }; - persistedQueries?: { - cache: KeyValueCache; - }; } export interface GraphQLRequest {