From fb99d9339bf8dcfefc4e65cf6db125fcafbb51d3 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 26 Mar 2020 21:06:15 +0200 Subject: [PATCH 01/98] Use named types for the "DidEnd" hooks. Some of the "didStart" request life-cycle hooks permit returning a "didEnd" function from them which will be invoked upon completion. This wasn't always immediately clear by looking at the in-line signature, but providing named types should make it marginally easier to recognize this in the typings. --- packages/apollo-server-plugin-base/src/index.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts index 5ca040fd617..6d89ea815cd 100644 --- a/packages/apollo-server-plugin-base/src/index.ts +++ b/packages/apollo-server-plugin-base/src/index.ts @@ -45,13 +45,20 @@ export interface ApolloServerPlugin = Recor ): GraphQLRequestListener | void; } +export type GraphQLRequestListenerParsingDidEnd = + ((err?: Error) => void) | void; +export type GraphQLRequestListenerValidationDidEnd = + ((err?: ReadonlyArray) => void) | void; +export type GraphQLRequestListenerExecutionDidEnd = + ((err?: Error) => void) | void; + export interface GraphQLRequestListener> { parsingDidStart?( requestContext: GraphQLRequestContextParsingDidStart, - ): ((err?: Error) => void) | void; + ): GraphQLRequestListenerParsingDidEnd; validationDidStart?( requestContext: GraphQLRequestContextValidationDidStart, - ): ((err?: ReadonlyArray) => void) | void; + ): GraphQLRequestListenerValidationDidEnd; didResolveOperation?( requestContext: GraphQLRequestContextDidResolveOperation, ): ValueOrPromise; @@ -68,7 +75,7 @@ export interface GraphQLRequestListener> { ): ValueOrPromise; executionDidStart?( requestContext: GraphQLRequestContextExecutionDidStart, - ): ((err?: Error) => void) | void; + ): GraphQLRequestListenerExecutionDidEnd; willSendResponse?( requestContext: GraphQLRequestContextWillSendResponse, ): ValueOrPromise; From ab35c45163a4541cb54ffbdb25a8aeda65c92574 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 26 Mar 2020 21:57:30 +0200 Subject: [PATCH 02/98] Add support for `willResolveField` and `didResolveField`. This adds two of the life-cycle hooks which were available in the `graphql-extensions` API but missing from the new request pipeline plugin API. These omissions have stood in the way of our own ability to migrate our Apollo-related extensions (e.g. `apollo-cache-control`, `apollo-engine-reporting` in federated and non-federated forms, `apollo-tracing`) to the new plugin API and our intention to deprecate that API which was never intended to be public (and was certainly never documented!). That's not to say that any of the effort to do those migrations is easy (it will absolutely not be), however, this unblocks those efforts. --- .../apollo-server-core/src/requestPipeline.ts | 12 +- .../src/requestPipelineAPI.ts | 155 ++++++++++++++++++ .../apollo-server-plugin-base/src/index.ts | 6 + 3 files changed, 172 insertions(+), 1 deletion(-) diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 07a640c1924..6393d82c95a 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -17,7 +17,11 @@ import { enableGraphQLExtensions, } from 'graphql-extensions'; import { DataSource } from 'apollo-datasource'; -import { PersistedQueryOptions } from '.'; +import { + PersistedQueryOptions, + symbolRequestListenerDispatcher, + enablePluginsForSchemaResolvers, +} from '.'; import { CacheControlExtension, CacheControlExtensionOptions, @@ -127,6 +131,9 @@ export async function processGraphQLRequest( (requestContext.context as any)._extensionStack = extensionStack; const dispatcher = initializeRequestListenerDispatcher(); + Object.defineProperty(requestContext.context, symbolRequestListenerDispatcher, { + value: dispatcher, + }); await initializeDataSources(); @@ -571,6 +578,8 @@ export async function processGraphQLRequest( function initializeRequestListenerDispatcher(): Dispatcher< GraphQLRequestListener > { + enablePluginsForSchemaResolvers(config.schema); + const requestListeners: GraphQLRequestListener[] = []; if (config.plugins) { for (const plugin of config.plugins) { @@ -633,3 +642,4 @@ export async function processGraphQLRequest( } } } + diff --git a/packages/apollo-server-core/src/requestPipelineAPI.ts b/packages/apollo-server-core/src/requestPipelineAPI.ts index 47301b3f4fd..39e8857e4f3 100644 --- a/packages/apollo-server-core/src/requestPipelineAPI.ts +++ b/packages/apollo-server-core/src/requestPipelineAPI.ts @@ -1,3 +1,16 @@ +import { + GraphQLField, + getNamedType, + GraphQLObjectType, + GraphQLSchema, + ResponsePath, +} from 'graphql/type'; +import { defaultFieldResolver } from "graphql/execution"; +import { FieldNode } from "graphql/language"; +import { Dispatcher } from "./utils/dispatcher"; +import { GraphQLRequestListener } from "apollo-server-plugin-base"; +import { GraphQLObjectResolver } from "@apollographql/apollo-tools"; + export { GraphQLServiceContext, GraphQLRequest, @@ -10,3 +23,145 @@ export { GraphQLExecutor, GraphQLExecutionResult, } from 'apollo-server-types'; + +export const symbolRequestListenerDispatcher = + Symbol("apolloServerRequestListenerDispatcher"); +export const symbolPluginsEnabled = Symbol("apolloServerPluginsEnabled"); + +export function enablePluginsForSchemaResolvers( + schema: GraphQLSchema & { [symbolPluginsEnabled]?: boolean }, +) { + if (schema[symbolPluginsEnabled]) { + return schema; + } + Object.defineProperty(schema, symbolPluginsEnabled, { + value: true, + }); + + forEachField(schema, wrapField); + + return schema; +} + +function wrapField(field: GraphQLField): void { + const fieldResolver = field.resolve || defaultFieldResolver; + + field.resolve = (source, args, context, info) => { + // This is a bit of a hack, but since `ResponsePath` is a linked list, + // a new object gets created every time a path segment is added. + // So we can use that to share our `whenObjectResolved` promise across + // all field resolvers for the same object. + const parentPath = info.path.prev as ResponsePath & { + __fields?: Record>; + __whenObjectResolved?: Promise; + }; + + // The technique for implementing a "did resolve field" is accomplished by + // returning a function from the `willResolveField` handler. The + // dispatcher will return a callback which will invoke all of those handlers + // and we'll save that to call when the object resolution is complete. + const endHandler = context && context[symbolRequestListenerDispatcher] && + (context[symbolRequestListenerDispatcher] as Dispatcher) + .invokeDidStartHook('willResolveField', source, args, context, info) || + ((_err: Error | null, _result?: any) => { /* do nothing */ }); + + const resolveObject: GraphQLObjectResolver< + any, + any + > = (info.parentType as any).resolveObject; + + let whenObjectResolved: Promise | undefined; + + if (parentPath && resolveObject) { + if (!parentPath.__fields) { + parentPath.__fields = {}; + } + + parentPath.__fields[info.fieldName] = info.fieldNodes; + + whenObjectResolved = parentPath.__whenObjectResolved; + if (!whenObjectResolved) { + // Use `Promise.resolve().then()` to delay executing + // `resolveObject()` so we can collect all the fields first. + whenObjectResolved = Promise.resolve().then(() => { + return resolveObject(source, parentPath.__fields!, context, info); + }); + parentPath.__whenObjectResolved = whenObjectResolved; + } + } + + try { + let result: any; + if (whenObjectResolved) { + result = whenObjectResolved.then((resolvedObject: any) => { + return fieldResolver(resolvedObject, args, context, info); + }); + } else { + result = fieldResolver(source, args, context, info); + } + + // Call the stack's handlers either immediately (if result is not a + // Promise) or once the Promise is done. Then return that same + // maybe-Promise value. + whenResultIsFinished(result, endHandler); + return result; + } catch (error) { + // Normally it's a bad sign to see an error both handled and + // re-thrown. But it is useful to allow extensions to track errors while + // still handling them in the normal GraphQL way. + endHandler(error); + throw error; + } + };; +} + +function isPromise(x: any): boolean { + return x && typeof x.then === 'function'; +} + +// Given result (which may be a Promise or an array some of whose elements are +// promises) Promises, set up 'callback' to be invoked when result is fully +// resolved. +export function whenResultIsFinished( + result: any, + callback: (err: Error | null, result?: any) => void, +) { + if (isPromise(result)) { + result.then((r: any) => callback(null, r), (err: Error) => callback(err)); + } else if (Array.isArray(result)) { + if (result.some(isPromise)) { + Promise.all(result).then( + (r: any) => callback(null, r), + (err: Error) => callback(err), + ); + } else { + callback(null, result); + } + } else { + callback(null, result); + } +} + +function forEachField(schema: GraphQLSchema, fn: FieldIteratorFn): void { + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach(typeName => { + const type = typeMap[typeName]; + + if ( + !getNamedType(type).name.startsWith('__') && + type instanceof GraphQLObjectType + ) { + const fields = type.getFields(); + Object.keys(fields).forEach(fieldName => { + const field = fields[fieldName]; + fn(field, typeName, fieldName); + }); + } + }); +} + +type FieldIteratorFn = ( + fieldDef: GraphQLField, + typeName: string, + fieldName: string, +) => void; diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts index 6d89ea815cd..2f671030974 100644 --- a/packages/apollo-server-plugin-base/src/index.ts +++ b/packages/apollo-server-plugin-base/src/index.ts @@ -13,6 +13,7 @@ import { GraphQLRequestContextExecutionDidStart, GraphQLRequestContextWillSendResponse, } from 'apollo-server-types'; +import { GraphQLFieldResolver } from "graphql"; // We re-export all of these so plugin authors only need to depend on a single // package. The overall concept of `apollo-server-types` and this package @@ -51,6 +52,8 @@ export type GraphQLRequestListenerValidationDidEnd = ((err?: ReadonlyArray) => void) | void; export type GraphQLRequestListenerExecutionDidEnd = ((err?: Error) => void) | void; +export type GraphQLRequestListenerDidResolveField = + ((error: Error | null, result?: any) => void) | void export interface GraphQLRequestListener> { parsingDidStart?( @@ -76,6 +79,9 @@ export interface GraphQLRequestListener> { executionDidStart?( requestContext: GraphQLRequestContextExecutionDidStart, ): GraphQLRequestListenerExecutionDidEnd; + willResolveField?( + ...fieldResolverArgs: Parameters> + ): GraphQLRequestListenerDidResolveField; willSendResponse?( requestContext: GraphQLRequestContextWillSendResponse, ): ValueOrPromise; From 835a68df2d08eb902d43f4395ddba26d46b37169 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 15 Apr 2020 12:10:51 +0300 Subject: [PATCH 03/98] chore: Convert `schemaHash` from `string` to a faux-paque type. By default, TypeScript uses structural typing (as opposed to nominal typing) Put another way, if it looks like the type and walks like that type, then TypeScript lets it be a type. That's often okay, but it leaves a lot to be desired since a `string` of one type can just be passed in as `string` for that type and TypeScript won't complain. Flow offers opaque types which solve this, but TypeScript doesn't offer this (yet?). This Faux-paque type can be used to gain nominal-esque typing, which is incredibly beneficial during re-factors! For the `schemaHash`, in particular, this is very much a string representation that serves a very particular purpose. Passing it incorrectly somewhere could be problematic, but we can avoid that (particularly as I embark on some re-factoring with it momentarily), by typing it as a more-opaque type prior to refactoring. Such passing around of strings can be common, for example, in positional parameters of functions: like a function that receives five strings, but a parameter ends up being misaligned with its destination. With structural typing, it's completely possible to miss that, but `SchemaHash` will _always_ be a `SchemaHash` with this fauxpaque-typing. Happy to not land this, but I think it provides some value. Input appreciated! --- packages/apollo-engine-reporting/src/agent.ts | 6 +++--- packages/apollo-engine-reporting/src/extension.ts | 9 +++++++-- packages/apollo-server-core/src/ApolloServer.ts | 4 ++-- .../apollo-server-core/src/utils/schemaHash.ts | 5 +++-- packages/apollo-server-types/src/index.ts | 15 ++++++++++++++- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index ef5f0f4f076..dd522203b71 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -12,7 +12,7 @@ import { fetch, RequestAgent, Response } from 'apollo-server-env'; import retry from 'async-retry'; import { EngineReportingExtension } from './extension'; -import { GraphQLRequestContext, Logger } from 'apollo-server-types'; +import { GraphQLRequestContext, Logger, SchemaHash } from 'apollo-server-types'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { defaultEngineReportingSignature } from 'apollo-graphql'; @@ -202,7 +202,7 @@ export interface AddTraceArgs { trace: Trace; operationName: string; queryHash: string; - schemaHash: string; + schemaHash: SchemaHash; queryString?: string; documentAST?: DocumentNode; } @@ -278,7 +278,7 @@ export class EngineReportingAgent { handleLegacyOptions(this.options); } - public newExtension(schemaHash: string): EngineReportingExtension { + public newExtension(schemaHash: SchemaHash): EngineReportingExtension { return new EngineReportingExtension( this.options, this.addTrace.bind(this), diff --git a/packages/apollo-engine-reporting/src/extension.ts b/packages/apollo-engine-reporting/src/extension.ts index a9199e88fd4..aacc702dcb1 100644 --- a/packages/apollo-engine-reporting/src/extension.ts +++ b/packages/apollo-engine-reporting/src/extension.ts @@ -1,4 +1,9 @@ -import { GraphQLRequestContext, WithRequired, Logger } from 'apollo-server-types'; +import { + GraphQLRequestContext, + WithRequired, + Logger, + SchemaHash, +} from 'apollo-server-types'; import { Request, Headers } from 'apollo-server-env'; import { GraphQLResolveInfo, @@ -42,7 +47,7 @@ export class EngineReportingExtension public constructor( options: EngineReportingOptions, addTrace: (args: AddTraceArgs) => Promise, - private schemaHash: string, + private schemaHash: SchemaHash, ) { this.options = { ...options, diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 5b94b5ea022..e3a6b4d74b1 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -69,7 +69,7 @@ import { import { Headers } from 'apollo-server-env'; import { buildServiceDefinition } from '@apollographql/apollo-tools'; -import { Logger } from "apollo-server-types"; +import { Logger, SchemaHash } from "apollo-server-types"; const NoIntrospection = (context: ValidationContext) => ({ Field(node: FieldDefinitionNode) { @@ -134,7 +134,7 @@ type SchemaDerivedData = { // on the same operation to be executed immediately. documentStore?: InMemoryLRUCache; schema: GraphQLSchema; - schemaHash: string; + schemaHash: SchemaHash; extensions: Array<() => GraphQLExtension>; }; diff --git a/packages/apollo-server-core/src/utils/schemaHash.ts b/packages/apollo-server-core/src/utils/schemaHash.ts index 7c30c6e751d..7137b9dac6c 100644 --- a/packages/apollo-server-core/src/utils/schemaHash.ts +++ b/packages/apollo-server-core/src/utils/schemaHash.ts @@ -4,8 +4,9 @@ import { getIntrospectionQuery, IntrospectionSchema } from 'graphql/utilities'; import stableStringify from 'fast-json-stable-stringify'; import { GraphQLSchema } from 'graphql/type'; import createSHA from './createSHA'; +import { SchemaHash } from "apollo-server-types"; -export function generateSchemaHash(schema: GraphQLSchema): string { +export function generateSchemaHash(schema: GraphQLSchema): SchemaHash { const introspectionQuery = getIntrospectionQuery(); const documentAST = parse(introspectionQuery); const result = execute(schema, documentAST) as ExecutionResult; @@ -40,5 +41,5 @@ export function generateSchemaHash(schema: GraphQLSchema): string { return createSHA('sha512') .update(stringifiedSchema) - .digest('hex'); + .digest('hex') as SchemaHash; } diff --git a/packages/apollo-server-types/src/index.ts b/packages/apollo-server-types/src/index.ts index 40d2639e4ff..5602aeea762 100644 --- a/packages/apollo-server-types/src/index.ts +++ b/packages/apollo-server-types/src/index.ts @@ -18,10 +18,23 @@ export type WithRequired = T & Required>; type Mutable = { -readonly [P in keyof T]: T[P] }; + // By default, TypeScript uses structural typing (as opposed to nominal typing) + // Put another way, if it looks like the type and walks like that type, then + // TypeScript lets it be a type. + // + // That's often okay, but it leaves a lot to be desired since a `string` of one + // type can just be passed in as `string` for that type and TypeScript won't + // complain. Flow offers opaque types which solve this, but TypeScript doesn't + // offer this (yet?). This Faux-paque type can be used to gain nominal-esque + // typing, which is incredibly beneficial during re-factors! + type Fauxpaque = K & { __fauxpaque: T }; + + export type SchemaHash = Fauxpaque; + export interface GraphQLServiceContext { logger: Logger; schema: GraphQLSchema; - schemaHash: string; + schemaHash: SchemaHash; engine: { serviceID?: string; apiKeyHash?: string; From ab3855db6f2442ceb553a1b7d27f0dd52ee403cc Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 14 Apr 2020 20:38:01 +0300 Subject: [PATCH 04/98] Introduce a plugin test harness to facilitate testing of plugins. This test harness is meant to avoid the need to do the more heavy execution which the request pipeline itself does within `processGraphQLRequest`. I'm not prepared to make this a public-facing harness just yet, but I have reason to believe that it could be beneficial for external plugin authors to take advantage of something like this - possibly within the context of `apollo-server-plugin-base`. There's perhaps a best-of-both-worlds approach here where the request pipeline could be tested against a more precise plugin API contract, but I'm deferring that work for now. --- .../src/utils/pluginTestHarness.ts | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 packages/apollo-server-core/src/utils/pluginTestHarness.ts diff --git a/packages/apollo-server-core/src/utils/pluginTestHarness.ts b/packages/apollo-server-core/src/utils/pluginTestHarness.ts new file mode 100644 index 00000000000..daff8f35dce --- /dev/null +++ b/packages/apollo-server-core/src/utils/pluginTestHarness.ts @@ -0,0 +1,145 @@ +import { + WithRequired, + GraphQLRequest, + GraphQLRequestContextExecutionDidStart, + GraphQLResponse, + ValueOrPromise, + GraphQLRequestContextWillSendResponse, + GraphQLRequestContext, +} from 'apollo-server-types'; +import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql/type'; +import { CacheHint } from 'apollo-cache-control'; +import { + enablePluginsForSchemaResolvers, + symbolRequestListenerDispatcher, +} from '../requestPipelineAPI'; +import { ApolloServerPlugin } from 'apollo-server-plugin-base'; +import { InMemoryLRUCache } from 'apollo-server-caching'; +import { Dispatcher } from './dispatcher'; + +// This test harness guarantees the presence of `query`. +type IPluginTestHarnessGraphqlRequest = WithRequired; +type IPluginTestHarnessExecutionDidStart = + GraphQLRequestContextExecutionDidStart & { + request: IPluginTestHarnessGraphqlRequest, + }; + +export default async function pluginTestHarness({ + pluginInstance, + schema, + graphqlRequest, + overallCachePolicy, + executor, + context = Object.create(null) +}: { + /** + * An instance of the plugin to test. + */ + pluginInstance: ApolloServerPlugin, + + /** + * The optional schema that will be received by the executor. If not + * specified, a simple default schema will be created. In either case, + * the schema will be mutated by wrapping the resolvers with the + * `willResolveField` instrumentation that will allow it to respond to + * that lifecycle hook's implementations plugins. + */ + schema?: GraphQLSchema; + + /** + * The `GraphQLRequest` which will be received by the `executor`. The + * `query` is required, and this doesn't support anything more exotic, + * like automated persisted queries (APQ). + */ + graphqlRequest: IPluginTestHarnessGraphqlRequest; + + /** + * Overall cache control policy. + */ + overallCachePolicy?: Required; + + /** + * This method will be executed to retrieve the response. + */ + executor: ( + requestContext: IPluginTestHarnessExecutionDidStart, + ) => ValueOrPromise; + + /** + * (optional) To provide a user context, if necessary. + */ + context?: TContext; +}): Promise> { + + if (!schema) { + schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'RootQueryType', + fields: { + hello: { + type: GraphQLString, + resolve() { + return 'hello world'; + } + } + } + }) + }); + } + + enablePluginsForSchemaResolvers(schema); + + const requestContext: GraphQLRequestContext = { + logger: console, + request: graphqlRequest, + metrics: Object.create(null), + source: graphqlRequest.query, + cache: new InMemoryLRUCache(), + context, + }; + + requestContext.overallCachePolicy = overallCachePolicy; + + if (typeof pluginInstance.requestDidStart !== "function") { + throw new Error("Should be impossible as the plugin is defined."); + } + + const listener = pluginInstance.requestDidStart(requestContext); + + if (!listener) { + throw new Error("Should be impossible to not have a listener."); + } + + if (typeof listener.willResolveField !== 'function') { + throw new Error("Should be impossible to not have 'willResolveField'."); + } + + const dispatcher = new Dispatcher([listener]); + + // Put the dispatcher on the context so `willResolveField` can access it. + Object.defineProperty(requestContext.context, symbolRequestListenerDispatcher, { + value: dispatcher, + }); + + const executionDidEnd = dispatcher.invokeDidStartHook( + "executionDidStart", + requestContext as IPluginTestHarnessExecutionDidStart, + ); + + try { + // `response` is readonly, so we'll cast to `any` to assign to it. + (requestContext.response as any) = await executor( + requestContext as IPluginTestHarnessExecutionDidStart, + ); + executionDidEnd(); + } catch (executionError) { + executionDidEnd(executionError); + } + + await dispatcher.invokeHookAsync( + "willSendResponse", + requestContext as GraphQLRequestContextWillSendResponse, + ); + + return requestContext as GraphQLRequestContextWillSendResponse; +} From 6009d8a00278e5d56377887b6a7c0aa902ef2391 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 30 Mar 2020 15:02:44 +0300 Subject: [PATCH 05/98] refactor: Introduce "tracing" plugin and switch request pipeline to use it. This commit introduces a `plugin` (named) export from the `apollo-tracing` package, which uses the new "plugin" hooks rather than the previous implementation (exported as `TracingExtension`) which uses the soon-to-be-deprecated "extensions" hooks. The functionality is intended to be identical, in spirit. Since the delta of the commits was otherwise confusing, I've left the `TracingExtension` present and exported and will remove it in a subsequent commit. Briefly summarizing what the necessary changes were: 1. We no longer use a class instance to house the extension, which was necessitated by the `graphql-extensions` API. This means that uses of `this` have been replaced with function scoped variables by the same name. 2. The logic which actually does the formatting (previously handled by the `format` method in `graphql-extension`, now takes place within the plugin API's `willSendResponse` method. --- package-lock.json | 1 + .../apollo-server-core/src/ApolloServer.ts | 9 +- .../apollo-server-core/src/requestPipeline.ts | 6 -- packages/apollo-tracing/package.json | 1 + packages/apollo-tracing/src/index.ts | 93 ++++++++++++++++++- packages/apollo-tracing/tsconfig.json | 1 + 6 files changed, 101 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 20a701000e3..31bec050eb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4652,6 +4652,7 @@ "version": "file:packages/apollo-tracing", "requires": { "apollo-server-env": "file:packages/apollo-server-env", + "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base", "graphql-extensions": "file:packages/graphql-extensions" } }, diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 5b94b5ea022..73df1ba5673 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -70,6 +70,7 @@ import { import { Headers } from 'apollo-server-env'; import { buildServiceDefinition } from '@apollographql/apollo-tools'; import { Logger } from "apollo-server-types"; +import { plugin as pluginTracing } from "apollo-tracing"; const NoIntrospection = (context: ValidationContext) => ({ Field(node: FieldDefinitionNode) { @@ -783,11 +784,13 @@ export class ApolloServerBase { } private ensurePluginInstantiation(plugins?: PluginDefinition[]): void { - if (!plugins || !plugins.length) { - return; + const pluginsToInit = [...plugins || []]; + + if (this.config.tracing) { + pluginsToInit.push(pluginTracing()) } - this.plugins = plugins.map(plugin => { + this.plugins = pluginsToInit.map(plugin => { if (typeof plugin === 'function') { return plugin(); } diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 6393d82c95a..34ce061a5e1 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -26,7 +26,6 @@ import { CacheControlExtension, CacheControlExtensionOptions, } from 'apollo-cache-control'; -import { TracingExtension } from 'apollo-tracing'; import { ApolloError, fromGraphQLError, @@ -95,7 +94,6 @@ export interface GraphQLRequestPipelineConfig { dataSources?: () => DataSources; extensions?: Array<() => GraphQLExtension>; - tracing?: boolean; persistedQueries?: PersistedQueryOptions; cacheControl?: CacheControlExtensionOptions; @@ -600,10 +598,6 @@ export async function processGraphQLRequest( // objects. const extensions = config.extensions ? config.extensions.map(f => f()) : []; - if (config.tracing) { - extensions.push(new TracingExtension()); - } - if (config.cacheControl) { cacheControlExtension = new CacheControlExtension(config.cacheControl); extensions.push(cacheControlExtension); diff --git a/packages/apollo-tracing/package.json b/packages/apollo-tracing/package.json index 6fe6e4434d9..316cc83aa61 100644 --- a/packages/apollo-tracing/package.json +++ b/packages/apollo-tracing/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "apollo-server-env": "file:../apollo-server-env", + "apollo-server-plugin-base": "file:../apollo-server-plugin-base", "graphql-extensions": "file:../graphql-extensions" }, "peerDependencies": { diff --git a/packages/apollo-tracing/src/index.ts b/packages/apollo-tracing/src/index.ts index 9c31c22ed54..fee8e9792f5 100644 --- a/packages/apollo-tracing/src/index.ts +++ b/packages/apollo-tracing/src/index.ts @@ -4,7 +4,7 @@ import { GraphQLResolveInfo, GraphQLType, } from 'graphql'; - +import { ApolloServerPlugin } from "apollo-server-plugin-base"; import { GraphQLExtension } from 'graphql-extensions'; export interface TracingFormat { @@ -33,6 +33,97 @@ interface ResolverCall { endOffset?: HighResolutionTime; } +export const plugin = (_futureOptions = {}) => (): ApolloServerPlugin => ({ + requestDidStart() { + let startWallTime: Date | undefined; + let endWallTime: Date | undefined; + let startHrTime: HighResolutionTime | undefined; + let duration: HighResolutionTime | undefined; + const resolverCalls: ResolverCall[] = []; + + startWallTime = new Date(); + startHrTime = process.hrtime(); + + return { + executionDidStart() { + // It's a little odd that we record the end time after execution rather + // than at the end of the whole request, but because we need to include + // our formatted trace in the request itself, we have to record it + // before the request is over! It's also odd that we don't do traces for + // parse or validation errors, but runQuery doesn't currently support + // that, as format() is only invoked after execution. + return () => { + duration = process.hrtime(startHrTime); + endWallTime = new Date(); + }; + }, + willResolveField(...args) { + const [, , , info] = args; + + const resolverCall: ResolverCall = { + path: info.path, + fieldName: info.fieldName, + parentType: info.parentType, + returnType: info.returnType, + startOffset: process.hrtime(startHrTime), + }; + + resolverCalls.push(resolverCall); + + return () => { + resolverCall.endOffset = process.hrtime(startHrTime); + }; + }, + willSendResponse({ response }) { + // In the event that we are called prior to the initialization of + // critical date metrics, we'll return undefined to signal that the + // extension did not format properly. Any undefined extension + // results are simply purged by the graphql-extensions module. + if ( + typeof startWallTime === 'undefined' || + typeof endWallTime === 'undefined' || + typeof duration === 'undefined' + ) { + return; + } + + const extensions = + response.extensions || (response.extensions = Object.create(null)); + + if (typeof extensions.tracing !== 'undefined') { + throw new Error("The tracing information already existed."); + } + + // Set the extensions. + extensions.tracing = { + version: 1, + startTime: startWallTime.toISOString(), + endTime: endWallTime.toISOString(), + duration: durationHrTimeToNanos(duration), + execution: { + resolvers: resolverCalls.map(resolverCall => { + const startOffset = durationHrTimeToNanos( + resolverCall.startOffset, + ); + const duration = resolverCall.endOffset + ? durationHrTimeToNanos(resolverCall.endOffset) - startOffset + : 0; + return { + path: [...responsePathAsArray(resolverCall.path)], + parentType: resolverCall.parentType.toString(), + fieldName: resolverCall.fieldName, + returnType: resolverCall.returnType.toString(), + startOffset, + duration, + }; + }), + }, + }; + }, + }; + }, +}) + export class TracingExtension implements GraphQLExtension { private startWallTime?: Date; diff --git a/packages/apollo-tracing/tsconfig.json b/packages/apollo-tracing/tsconfig.json index 0de28001c29..1276353ea04 100644 --- a/packages/apollo-tracing/tsconfig.json +++ b/packages/apollo-tracing/tsconfig.json @@ -8,5 +8,6 @@ "exclude": ["**/__tests__", "**/__mocks__"], "references": [ { "path": "../graphql-extensions" }, + { "path": "../apollo-server-plugin-base" }, ] } From 8ec39c98ffbeb4c695a72a24ec1fc768545be2c8 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 30 Mar 2020 15:58:06 +0300 Subject: [PATCH 06/98] eliminate!: Remove deprecated `TracingExtension`. This follows-up 672fa0953 and removes the no-longer used/necessary `TracingExtension` extension which uses the soon-to-be-deprecated `graphql-extensions` API, now that it has been replaced with an implementation that uses the plugin API. --- package-lock.json | 3 +- packages/apollo-tracing/package.json | 3 +- packages/apollo-tracing/src/index.ts | 91 --------------------------- packages/apollo-tracing/tsconfig.json | 1 - 4 files changed, 2 insertions(+), 96 deletions(-) diff --git a/package-lock.json b/package-lock.json index 31bec050eb2..9264f6152ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4652,8 +4652,7 @@ "version": "file:packages/apollo-tracing", "requires": { "apollo-server-env": "file:packages/apollo-server-env", - "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base", - "graphql-extensions": "file:packages/graphql-extensions" + "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base" } }, "apollo-utilities": { diff --git a/packages/apollo-tracing/package.json b/packages/apollo-tracing/package.json index 316cc83aa61..99a169b435c 100644 --- a/packages/apollo-tracing/package.json +++ b/packages/apollo-tracing/package.json @@ -12,8 +12,7 @@ }, "dependencies": { "apollo-server-env": "file:../apollo-server-env", - "apollo-server-plugin-base": "file:../apollo-server-plugin-base", - "graphql-extensions": "file:../graphql-extensions" + "apollo-server-plugin-base": "file:../apollo-server-plugin-base" }, "peerDependencies": { "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" diff --git a/packages/apollo-tracing/src/index.ts b/packages/apollo-tracing/src/index.ts index fee8e9792f5..31ba20a6f18 100644 --- a/packages/apollo-tracing/src/index.ts +++ b/packages/apollo-tracing/src/index.ts @@ -1,11 +1,9 @@ import { ResponsePath, responsePathAsArray, - GraphQLResolveInfo, GraphQLType, } from 'graphql'; import { ApolloServerPlugin } from "apollo-server-plugin-base"; -import { GraphQLExtension } from 'graphql-extensions'; export interface TracingFormat { version: 1; @@ -124,95 +122,6 @@ export const plugin = (_futureOptions = {}) => (): ApolloServerPlugin => ({ }, }) -export class TracingExtension - implements GraphQLExtension { - private startWallTime?: Date; - private endWallTime?: Date; - private startHrTime?: HighResolutionTime; - private duration?: HighResolutionTime; - - private resolverCalls: ResolverCall[] = []; - - public requestDidStart() { - this.startWallTime = new Date(); - this.startHrTime = process.hrtime(); - } - - public executionDidStart() { - // It's a little odd that we record the end time after execution rather than - // at the end of the whole request, but because we need to include our - // formatted trace in the request itself, we have to record it before the - // request is over! It's also odd that we don't do traces for parse or - // validation errors, but runQuery doesn't currently support that, as - // format() is only invoked after execution. - return () => { - this.duration = process.hrtime(this.startHrTime); - this.endWallTime = new Date(); - }; - } - - public willResolveField( - _source: any, - _args: { [argName: string]: any }, - _context: TContext, - info: GraphQLResolveInfo, - ) { - const resolverCall: ResolverCall = { - path: info.path, - fieldName: info.fieldName, - parentType: info.parentType, - returnType: info.returnType, - startOffset: process.hrtime(this.startHrTime), - }; - - this.resolverCalls.push(resolverCall); - - return () => { - resolverCall.endOffset = process.hrtime(this.startHrTime); - }; - } - - public format(): [string, TracingFormat] | undefined { - // In the event that we are called prior to the initialization of critical - // date metrics, we'll return undefined to signal that the extension did not - // format properly. Any undefined extension results are simply purged by - // the graphql-extensions module. - if ( - typeof this.startWallTime === 'undefined' || - typeof this.endWallTime === 'undefined' || - typeof this.duration === 'undefined' - ) { - return; - } - - return [ - 'tracing', - { - version: 1, - startTime: this.startWallTime.toISOString(), - endTime: this.endWallTime.toISOString(), - duration: durationHrTimeToNanos(this.duration), - execution: { - resolvers: this.resolverCalls.map(resolverCall => { - const startOffset = durationHrTimeToNanos(resolverCall.startOffset); - const duration = resolverCall.endOffset - ? durationHrTimeToNanos(resolverCall.endOffset) - startOffset - : 0; - return { - path: [...responsePathAsArray(resolverCall.path)], - parentType: resolverCall.parentType.toString(), - fieldName: resolverCall.fieldName, - returnType: resolverCall.returnType.toString(), - startOffset, - duration, - }; - }), - }, - }, - ]; - } -} - type HighResolutionTime = [number, number]; // Converts an hrtime array (as returned from process.hrtime) to nanoseconds. diff --git a/packages/apollo-tracing/tsconfig.json b/packages/apollo-tracing/tsconfig.json index 1276353ea04..29dff935854 100644 --- a/packages/apollo-tracing/tsconfig.json +++ b/packages/apollo-tracing/tsconfig.json @@ -7,7 +7,6 @@ "include": ["src/**/*"], "exclude": ["**/__tests__", "**/__mocks__"], "references": [ - { "path": "../graphql-extensions" }, { "path": "../apollo-server-plugin-base" }, ] } From dae59a5bbee72c4f7de8eb71f8fa4f0f3afe1f2f Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 16 Apr 2020 12:23:08 +0300 Subject: [PATCH 07/98] Allow an optional `logger` to be passed into the test harness. Great idea! Ref: https://github.com/apollographql/apollo-server/pull/3990#discussion_r409134354 cc @trevor-scheer --- .../apollo-server-core/src/utils/pluginTestHarness.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/apollo-server-core/src/utils/pluginTestHarness.ts b/packages/apollo-server-core/src/utils/pluginTestHarness.ts index daff8f35dce..0afa7dfde96 100644 --- a/packages/apollo-server-core/src/utils/pluginTestHarness.ts +++ b/packages/apollo-server-core/src/utils/pluginTestHarness.ts @@ -6,6 +6,7 @@ import { ValueOrPromise, GraphQLRequestContextWillSendResponse, GraphQLRequestContext, + Logger, } from 'apollo-server-types'; import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql/type'; import { CacheHint } from 'apollo-cache-control'; @@ -27,6 +28,7 @@ type IPluginTestHarnessExecutionDidStart = export default async function pluginTestHarness({ pluginInstance, schema, + logger, graphqlRequest, overallCachePolicy, executor, @@ -46,6 +48,11 @@ export default async function pluginTestHarness({ */ schema?: GraphQLSchema; + /** + * An optional logger (Defaults to `console`) + */ + logger?: Logger; + /** * The `GraphQLRequest` which will be received by the `executor`. The * `query` is required, and this doesn't support anything more exotic, @@ -90,7 +97,7 @@ export default async function pluginTestHarness({ enablePluginsForSchemaResolvers(schema); const requestContext: GraphQLRequestContext = { - logger: console, + logger: logger || console, request: graphqlRequest, metrics: Object.create(null), source: graphqlRequest.query, From 771683f3205e3b61075e84856a059b24091251aa Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 16 Apr 2020 12:54:51 +0300 Subject: [PATCH 08/98] chore: Convert `schemaHash` from `string` to a faux-paque type. (#3989) By default, TypeScript uses structural typing (as opposed to nominal typing) Put another way, if it looks like the type and walks like that type, then TypeScript lets it be a type. That's often okay, but it leaves a lot to be desired since a `string` of one type can just be passed in as `string` for that type and TypeScript won't complain. Flow offers opaque types which solve this, but TypeScript doesn't offer this (yet?). This Faux-paque type can be used to gain nominal-esque typing, which is incredibly beneficial during re-factors! For the `schemaHash`, in particular, this is very much a string representation that serves a very particular purpose. Passing it incorrectly somewhere could be problematic, but we can avoid that (particularly as I embark on some re-factoring with it momentarily), by typing it as a more-opaque type prior to refactoring. Such passing around of strings can be common, for example, in positional parameters of functions: like a function that receives five strings, but a parameter ends up being misaligned with its destination. With structural typing, it's completely possible to miss that, but `SchemaHash` will _always_ be a `SchemaHash` with this fauxpaque-typing. Happy to not land this, but I think it provides some value. Input appreciated! --- packages/apollo-engine-reporting/src/agent.ts | 6 +++--- packages/apollo-engine-reporting/src/extension.ts | 9 +++++++-- packages/apollo-server-core/src/ApolloServer.ts | 4 ++-- .../apollo-server-core/src/utils/schemaHash.ts | 5 +++-- packages/apollo-server-types/src/index.ts | 15 ++++++++++++++- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index ef5f0f4f076..dd522203b71 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -12,7 +12,7 @@ import { fetch, RequestAgent, Response } from 'apollo-server-env'; import retry from 'async-retry'; import { EngineReportingExtension } from './extension'; -import { GraphQLRequestContext, Logger } from 'apollo-server-types'; +import { GraphQLRequestContext, Logger, SchemaHash } from 'apollo-server-types'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { defaultEngineReportingSignature } from 'apollo-graphql'; @@ -202,7 +202,7 @@ export interface AddTraceArgs { trace: Trace; operationName: string; queryHash: string; - schemaHash: string; + schemaHash: SchemaHash; queryString?: string; documentAST?: DocumentNode; } @@ -278,7 +278,7 @@ export class EngineReportingAgent { handleLegacyOptions(this.options); } - public newExtension(schemaHash: string): EngineReportingExtension { + public newExtension(schemaHash: SchemaHash): EngineReportingExtension { return new EngineReportingExtension( this.options, this.addTrace.bind(this), diff --git a/packages/apollo-engine-reporting/src/extension.ts b/packages/apollo-engine-reporting/src/extension.ts index a9199e88fd4..aacc702dcb1 100644 --- a/packages/apollo-engine-reporting/src/extension.ts +++ b/packages/apollo-engine-reporting/src/extension.ts @@ -1,4 +1,9 @@ -import { GraphQLRequestContext, WithRequired, Logger } from 'apollo-server-types'; +import { + GraphQLRequestContext, + WithRequired, + Logger, + SchemaHash, +} from 'apollo-server-types'; import { Request, Headers } from 'apollo-server-env'; import { GraphQLResolveInfo, @@ -42,7 +47,7 @@ export class EngineReportingExtension public constructor( options: EngineReportingOptions, addTrace: (args: AddTraceArgs) => Promise, - private schemaHash: string, + private schemaHash: SchemaHash, ) { this.options = { ...options, diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 5b94b5ea022..e3a6b4d74b1 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -69,7 +69,7 @@ import { import { Headers } from 'apollo-server-env'; import { buildServiceDefinition } from '@apollographql/apollo-tools'; -import { Logger } from "apollo-server-types"; +import { Logger, SchemaHash } from "apollo-server-types"; const NoIntrospection = (context: ValidationContext) => ({ Field(node: FieldDefinitionNode) { @@ -134,7 +134,7 @@ type SchemaDerivedData = { // on the same operation to be executed immediately. documentStore?: InMemoryLRUCache; schema: GraphQLSchema; - schemaHash: string; + schemaHash: SchemaHash; extensions: Array<() => GraphQLExtension>; }; diff --git a/packages/apollo-server-core/src/utils/schemaHash.ts b/packages/apollo-server-core/src/utils/schemaHash.ts index 7c30c6e751d..7137b9dac6c 100644 --- a/packages/apollo-server-core/src/utils/schemaHash.ts +++ b/packages/apollo-server-core/src/utils/schemaHash.ts @@ -4,8 +4,9 @@ import { getIntrospectionQuery, IntrospectionSchema } from 'graphql/utilities'; import stableStringify from 'fast-json-stable-stringify'; import { GraphQLSchema } from 'graphql/type'; import createSHA from './createSHA'; +import { SchemaHash } from "apollo-server-types"; -export function generateSchemaHash(schema: GraphQLSchema): string { +export function generateSchemaHash(schema: GraphQLSchema): SchemaHash { const introspectionQuery = getIntrospectionQuery(); const documentAST = parse(introspectionQuery); const result = execute(schema, documentAST) as ExecutionResult; @@ -40,5 +41,5 @@ export function generateSchemaHash(schema: GraphQLSchema): string { return createSHA('sha512') .update(stringifiedSchema) - .digest('hex'); + .digest('hex') as SchemaHash; } diff --git a/packages/apollo-server-types/src/index.ts b/packages/apollo-server-types/src/index.ts index 40d2639e4ff..5602aeea762 100644 --- a/packages/apollo-server-types/src/index.ts +++ b/packages/apollo-server-types/src/index.ts @@ -18,10 +18,23 @@ export type WithRequired = T & Required>; type Mutable = { -readonly [P in keyof T]: T[P] }; + // By default, TypeScript uses structural typing (as opposed to nominal typing) + // Put another way, if it looks like the type and walks like that type, then + // TypeScript lets it be a type. + // + // That's often okay, but it leaves a lot to be desired since a `string` of one + // type can just be passed in as `string` for that type and TypeScript won't + // complain. Flow offers opaque types which solve this, but TypeScript doesn't + // offer this (yet?). This Faux-paque type can be used to gain nominal-esque + // typing, which is incredibly beneficial during re-factors! + type Fauxpaque = K & { __fauxpaque: T }; + + export type SchemaHash = Fauxpaque; + export interface GraphQLServiceContext { logger: Logger; schema: GraphQLSchema; - schemaHash: string; + schemaHash: SchemaHash; engine: { serviceID?: string; apiKeyHash?: string; From 0d31d1b452e7e0c38c2292ba9f740e10d610d701 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 16 Apr 2020 14:35:40 +0300 Subject: [PATCH 09/98] Apply suggestions from code review Co-Authored-By: Trevor Scheer --- packages/apollo-server-core/src/requestPipelineAPI.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/apollo-server-core/src/requestPipelineAPI.ts b/packages/apollo-server-core/src/requestPipelineAPI.ts index 39e8857e4f3..734d5277471 100644 --- a/packages/apollo-server-core/src/requestPipelineAPI.ts +++ b/packages/apollo-server-core/src/requestPipelineAPI.ts @@ -144,16 +144,14 @@ export function whenResultIsFinished( function forEachField(schema: GraphQLSchema, fn: FieldIteratorFn): void { const typeMap = schema.getTypeMap(); - Object.keys(typeMap).forEach(typeName => { - const type = typeMap[typeName]; + Object.entries(typeMap).forEach(([typeName, type]) => { if ( !getNamedType(type).name.startsWith('__') && type instanceof GraphQLObjectType ) { const fields = type.getFields(); - Object.keys(fields).forEach(fieldName => { - const field = fields[fieldName]; + Object.entries(fields).forEach(([fieldName, field]) => { fn(field, typeName, fieldName); }); } From 7e4cc4b7d99aeaffe6a27b2cf97b98d7713c2a5c Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 16 Apr 2020 13:55:16 +0300 Subject: [PATCH 10/98] Clear up error message in defense mechanism against existing `tracing`. This package should never have already set it itself prior to this point, but the circumstance exists that something (anything) else could have already used the `extensions` sink to attach a value. This guards against that. --- packages/apollo-tracing/src/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/apollo-tracing/src/index.ts b/packages/apollo-tracing/src/index.ts index 31ba20a6f18..b5cb8ec5cc3 100644 --- a/packages/apollo-tracing/src/index.ts +++ b/packages/apollo-tracing/src/index.ts @@ -5,6 +5,8 @@ import { } from 'graphql'; import { ApolloServerPlugin } from "apollo-server-plugin-base"; +const { PACKAGE_NAME } = require("../package.json").name; + export interface TracingFormat { version: 1; startTime: string; @@ -88,8 +90,11 @@ export const plugin = (_futureOptions = {}) => (): ApolloServerPlugin => ({ const extensions = response.extensions || (response.extensions = Object.create(null)); + // Be defensive and make sure nothing else (other plugin, etc.) has + // already used the `tracing` property on `extensions`. if (typeof extensions.tracing !== 'undefined') { - throw new Error("The tracing information already existed."); + throw new Error(PACKAGE_NAME + ": Could not add `tracing` to " + + "`extensions` since `tracing` was unexpectedly already present."); } // Set the extensions. From 4b59b02b0da2602ea1f0bcb399687ad170699e07 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 16 Apr 2020 15:25:04 +0300 Subject: [PATCH 11/98] Rejigger `ensurePluginInstantiation` to accommodate upcoming changes. Inspired by landing some PRs separately and a merge commit that could have been avoided, but also inspired by the following comment by @trevor-scheer whicih made it clear my organization was just a _bit_ off. Ref: https://github.com/apollographql/apollo-server/pull/3991#discussion_r409502262 --- packages/apollo-server-core/src/ApolloServer.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 5b94b5ea022..bea27fea464 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -782,12 +782,16 @@ export class ApolloServerBase { return sdlFieldType.name == 'String'; } - private ensurePluginInstantiation(plugins?: PluginDefinition[]): void { - if (!plugins || !plugins.length) { - return; - } + private ensurePluginInstantiation(plugins: PluginDefinition[] = []): void { + const pluginsToInit: PluginDefinition[] = []; + + // Internal plugins should be added to `pluginsToInit` here. + // User's plugins, provided as an argument to this method, will be added + // at the end of that list so they take precidence. + // A follow-up commit will actually introduce this. - this.plugins = plugins.map(plugin => { + pluginsToInit.push(...plugins); + this.plugins = pluginsToInit.map(plugin => { if (typeof plugin === 'function') { return plugin(); } From fecea35b7cceaf6aaa1181bea32b34929b649b2b Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 16 Apr 2020 15:34:45 +0300 Subject: [PATCH 12/98] Update README.md for `apollo-tracing`. --- packages/apollo-tracing/README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/apollo-tracing/README.md b/packages/apollo-tracing/README.md index ffaab016414..f23149a579a 100644 --- a/packages/apollo-tracing/README.md +++ b/packages/apollo-tracing/README.md @@ -12,14 +12,13 @@ This data can be consumed by [Apollo Graph Manager](https://www.apollographql.co Apollo Server includes built-in support for tracing from version 1.1.0 onwards. -The only code change required is to add `tracing: true` to the options passed to the Apollo Server middleware function for your framework of choice. For example, for Express: +The only code change required is to add `tracing: true` to the options passed to the `ApolloServer` constructor options for your integration of choice. For example, for [`apollo-server-express`](https://npm.im/apollo-server-express): ```javascript -app.use('/graphql', bodyParser.json(), graphqlExpress({ +const { ApolloServer } = require('apollo-server-express'); + +const server = new ApolloServer({ schema, - context: {}, tracing: true, -})); +}); ``` - -> If you are using `express-graphql`, we recommend you switch to Apollo Server. Both `express-graphql` and Apollo Server are based on the [`graphql-js`](https://github.com/graphql/graphql-js) reference implementation, and switching should only require changing a few lines of code. From 3e9188fb81ad7a103cdb17a6707a29275866c464 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 16 Apr 2020 15:36:48 +0300 Subject: [PATCH 13/98] Add CHANGELOG.md for #3991. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f6db09c1a6..a32e400dd26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The version headers in this history reflect the versions of Apollo Server itself > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the the appropriate changes within that release will be moved into the new section. - `apollo-server-lambda`: Support file uploads on AWS Lambda [Issue #1419](https://github.com/apollographql/apollo-server/issues/1419) [Issue #1703](https://github.com/apollographql/apollo-server/issues/1703) [PR #3926](https://github.com/apollographql/apollo-server/pull/3926) +- `apollo-tracing`: This package's internal integration with Apollo Server has been switched from using the soon-to-be-deprecated`graphql-extensions` API to using [the request pipeline plugin API](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). Behavior should remain otherwise the same. [PR #3991](https://github.com/apollographql/apollo-server/pull/3991) - **breaking** `apollo-engine-reporting-protobuf`: Drop legacy fields that were never used by `apollo-engine-reporting`. Added new fields `StatsContext` to allow `apollo-server` to send summary stats instead of full traces, and renamed `FullTracesReport` to `Report` and `Traces` to `TracesAndStats` since reports now can include stats as well as traces. ### v2.12.0 From 68cbc938ff709078efc8a1e726300e61222f6b8b Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 30 Mar 2020 15:02:44 +0300 Subject: [PATCH 14/98] refactor: Introduce "cache-control" plugin and switch req. pipeline to it. Similar to 6009d8a00278e, which migrated the tracing package to the plugin API, this commit introduces a `plugin` (named) export from the `apollo-cache-control` module. It similarly accomplishes that by duplicating the behavior of the `CacheControlExtension` which leveraged the soon-to-be-deprecated `graphql-extensions`. The functionality, again, is intended to be identical in spirit. Since the delta of the commits was otherwise even _more_ confusing, I've left the `CacheControlExtension` present and exported and will remove it in a subsequent commit. Briefly summarizing what the necessary changes were: 1. We no longer use a class instance to house the extension, which was necessitated by the `graphql-extensions` API. This means that uses of `this` have been replaced with function scoped variables by the same name. 2. The logic which actually does the formatting (previously handled by the `format` method in `graphql-extension`, now takes place within the plugin API's `willSendResponse` method. 3. Rather than leaning on plugin-specific behavior from within the request pipeline, the `apollo-cache-control` plugin now makes a number of assertions throughout its various life-cycle methods to ensure that the `overallCachePolicy` is calculated. 4. Switch tests to use the new `pluginTestHarness` method for testing plugins which was introduced in eec87a6c6dead7fd (#3990). --- package-lock.json | 1 + packages/apollo-cache-control/package.json | 3 +- .../__tests__/cacheControlExtension.test.ts | 162 ++++++++++----- .../src/__tests__/collectCacheControlHints.ts | 33 +-- packages/apollo-cache-control/src/index.ts | 188 ++++++++++++++++++ packages/apollo-cache-control/tsconfig.json | 1 + .../apollo-server-core/src/ApolloServer.ts | 60 +++--- .../apollo-server-core/src/requestPipeline.ts | 25 --- .../apollo-server-core/src/runHttpQuery.ts | 4 - 9 files changed, 353 insertions(+), 124 deletions(-) diff --git a/package-lock.json b/package-lock.json index 20a701000e3..519c1acc2ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4277,6 +4277,7 @@ "version": "file:packages/apollo-cache-control", "requires": { "apollo-server-env": "file:packages/apollo-server-env", + "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base", "graphql-extensions": "file:packages/graphql-extensions" } }, diff --git a/packages/apollo-cache-control/package.json b/packages/apollo-cache-control/package.json index a14de3e4b15..23b4e1f583a 100644 --- a/packages/apollo-cache-control/package.json +++ b/packages/apollo-cache-control/package.json @@ -12,7 +12,8 @@ }, "dependencies": { "apollo-server-env": "file:../apollo-server-env", - "graphql-extensions": "file:../graphql-extensions" + "graphql-extensions": "file:../graphql-extensions", + "apollo-server-plugin-base": "file:../apollo-server-plugin-base" }, "peerDependencies": { "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" diff --git a/packages/apollo-cache-control/src/__tests__/cacheControlExtension.test.ts b/packages/apollo-cache-control/src/__tests__/cacheControlExtension.test.ts index 29a6e0d195d..ff4d1fb75eb 100644 --- a/packages/apollo-cache-control/src/__tests__/cacheControlExtension.test.ts +++ b/packages/apollo-cache-control/src/__tests__/cacheControlExtension.test.ts @@ -1,59 +1,110 @@ import { ResponsePath, GraphQLError } from 'graphql'; import { GraphQLResponse } from 'graphql-extensions'; import { Headers } from 'apollo-server-env'; -import { CacheControlExtension, CacheScope } from '../'; +import { + CacheScope, + CacheControlExtensionOptions, + CacheHint, + __testing__, + plugin, +} from '../'; +const { addHint, computeOverallCachePolicy } = __testing__; +import { + GraphQLRequestContextWillSendResponse, +} from 'apollo-server-plugin-base'; +import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness'; describe('CacheControlExtension', () => { - let cacheControlExtension: CacheControlExtension; - - beforeEach(() => { - cacheControlExtension = new CacheControlExtension(); - }); - describe('willSendResponse', () => { - let graphqlResponse: GraphQLResponse; + function makePluginWithOptions({ + pluginInitializationOptions, + overallCachePolicy, + errors = false, + }: { + pluginInitializationOptions?: CacheControlExtensionOptions; + overallCachePolicy?: Required; + errors?: boolean; + } = Object.create(null)) { + const pluginInstance = plugin(pluginInitializationOptions); + + return pluginTestHarness({ + pluginInstance, + overallCachePolicy, + graphqlRequest: { query: 'does not matter' }, + executor: () => { + const response: GraphQLResponse = { + http: { + headers: new Headers(), + }, + data: { test: 'test' }, + }; + + if (errors) { + response.errors = [new GraphQLError('Test Error')]; + } + + return response; + }, + }); + } - beforeEach(() => { - cacheControlExtension.options.calculateHttpHeaders = true; - cacheControlExtension.computeOverallCachePolicy = () => ({ + describe('HTTP cache-control header', () => { + const overallCachePolicy: Required = { maxAge: 300, scope: CacheScope.Public, - }); - graphqlResponse = { - http: { - headers: new Headers(), - }, - data: { test: 'test' }, }; - }); - it('sets cache-control header', () => { - cacheControlExtension.willSendResponse && - cacheControlExtension.willSendResponse({ graphqlResponse }); - expect(graphqlResponse.http!.headers.get('Cache-Control')).toBe( - 'max-age=300, public', - ); - }); + it('is set when calculateHttpHeaders is set to true', async () => { + const requestContext = await makePluginWithOptions({ + pluginInitializationOptions: { + calculateHttpHeaders: true, + }, + overallCachePolicy, + }); + expect(requestContext.response.http!.headers.get('Cache-Control')).toBe( + 'max-age=300, public', + ); + }); - const shouldNotSetCacheControlHeader = () => { - cacheControlExtension.willSendResponse && - cacheControlExtension.willSendResponse({ graphqlResponse }); - expect(graphqlResponse.http!.headers.get('Cache-Control')).toBeNull(); - }; + const shouldNotSetCacheControlHeader = ( + requestContext: GraphQLRequestContextWillSendResponse, + ) => { + expect( + requestContext.response.http!.headers.get('Cache-Control'), + ).toBeNull(); + }; - it('does not set cache-control header if calculateHttpHeaders is set to false', () => { - cacheControlExtension.options.calculateHttpHeaders = false; - shouldNotSetCacheControlHeader(); - }); + it('is not set when calculateHttpHeaders is set to false', async () => { + const requestContext = await makePluginWithOptions({ + pluginInitializationOptions: { + calculateHttpHeaders: false, + }, + overallCachePolicy, + }); + shouldNotSetCacheControlHeader(requestContext); + }); - it('does not set cache-control header if graphqlResponse has errors', () => { - graphqlResponse.errors = [new GraphQLError('Test Error')]; - shouldNotSetCacheControlHeader(); - }); + it('is not set if response has errors', async () => { + const requestContext = await makePluginWithOptions({ + pluginInitializationOptions: { + calculateHttpHeaders: false, + }, + overallCachePolicy, + errors: true, + }); + shouldNotSetCacheControlHeader(requestContext); + }); - it('does not set cache-control header if there is no overall cache policy', () => { - cacheControlExtension.computeOverallCachePolicy = () => undefined; - shouldNotSetCacheControlHeader(); + it('does not set cache-control header if there is no overall cache policy', async () => { + const requestContext = await makePluginWithOptions({ + pluginInitializationOptions: { + calculateHttpHeaders: false, + }, + overallCachePolicy: undefined, + errors: true, + }); + shouldNotSetCacheControlHeader(requestContext); + }); }); }); @@ -71,46 +122,49 @@ describe('CacheControlExtension', () => { prev: responseSubPath, }; + const hints = new Map(); + afterEach(() => hints.clear()); + it('returns undefined without cache hints', () => { - const cachePolicy = cacheControlExtension.computeOverallCachePolicy(); + const cachePolicy = computeOverallCachePolicy(hints); expect(cachePolicy).toBeUndefined(); }); it('returns lowest max age value', () => { - cacheControlExtension.addHint(responsePath, { maxAge: 10 }); - cacheControlExtension.addHint(responseSubPath, { maxAge: 20 }); + addHint(hints, responsePath, { maxAge: 10 }); + addHint(hints, responseSubPath, { maxAge: 20 }); - const cachePolicy = cacheControlExtension.computeOverallCachePolicy(); + const cachePolicy = computeOverallCachePolicy(hints); expect(cachePolicy).toHaveProperty('maxAge', 10); }); it('returns undefined if any cache hint has a maxAge of 0', () => { - cacheControlExtension.addHint(responsePath, { maxAge: 120 }); - cacheControlExtension.addHint(responseSubPath, { maxAge: 0 }); - cacheControlExtension.addHint(responseSubSubPath, { maxAge: 20 }); + addHint(hints, responsePath, { maxAge: 120 }); + addHint(hints, responseSubPath, { maxAge: 0 }); + addHint(hints, responseSubSubPath, { maxAge: 20 }); - const cachePolicy = cacheControlExtension.computeOverallCachePolicy(); + const cachePolicy = computeOverallCachePolicy(hints); expect(cachePolicy).toBeUndefined(); }); it('returns PUBLIC scope by default', () => { - cacheControlExtension.addHint(responsePath, { maxAge: 10 }); + addHint(hints, responsePath, { maxAge: 10 }); - const cachePolicy = cacheControlExtension.computeOverallCachePolicy(); + const cachePolicy = computeOverallCachePolicy(hints); expect(cachePolicy).toHaveProperty('scope', CacheScope.Public); }); it('returns PRIVATE scope if any cache hint has PRIVATE scope', () => { - cacheControlExtension.addHint(responsePath, { + addHint(hints, responsePath, { maxAge: 10, scope: CacheScope.Public, }); - cacheControlExtension.addHint(responseSubPath, { + addHint(hints, responseSubPath, { maxAge: 10, scope: CacheScope.Private, }); - const cachePolicy = cacheControlExtension.computeOverallCachePolicy(); + const cachePolicy = computeOverallCachePolicy(hints); expect(cachePolicy).toHaveProperty('scope', CacheScope.Private); }); }); diff --git a/packages/apollo-cache-control/src/__tests__/collectCacheControlHints.ts b/packages/apollo-cache-control/src/__tests__/collectCacheControlHints.ts index e69e8bc448e..fdadf7d7ca4 100644 --- a/packages/apollo-cache-control/src/__tests__/collectCacheControlHints.ts +++ b/packages/apollo-cache-control/src/__tests__/collectCacheControlHints.ts @@ -1,38 +1,41 @@ import { GraphQLSchema, graphql } from 'graphql'; - -import { - enableGraphQLExtensions, - GraphQLExtensionStack, -} from 'graphql-extensions'; import { - CacheControlExtension, CacheHint, CacheControlExtensionOptions, + plugin, } from '../'; +import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness'; export async function collectCacheControlHints( schema: GraphQLSchema, source: string, options?: CacheControlExtensionOptions, ): Promise { - enableGraphQLExtensions(schema); // Because this test helper looks at the formatted extensions, we always want - // to include them. - const cacheControlExtension = new CacheControlExtension({ + // to include them in the response rather than allow them to be stripped + // out. + const pluginInstance = plugin({ ...options, stripFormattedExtensions: false, }); - const response = await graphql({ + const requestContext = await pluginTestHarness({ + pluginInstance, schema, - source, - contextValue: { - _extensionStack: new GraphQLExtensionStack([cacheControlExtension]), + graphqlRequest: { + query: source, }, + executor: async (requestContext) => { + return await graphql({ + schema, + source: requestContext.request.query, + contextValue: requestContext.context, + }); + } }); - expect(response.errors).toBeUndefined(); + expect(requestContext.response.errors).toBeUndefined(); - return cacheControlExtension.format()[1].hints; + return requestContext.response.extensions!.cacheControl.hints; } diff --git a/packages/apollo-cache-control/src/index.ts b/packages/apollo-cache-control/src/index.ts index fa37370f03e..f1a9a944e42 100644 --- a/packages/apollo-cache-control/src/index.ts +++ b/packages/apollo-cache-control/src/index.ts @@ -7,6 +7,7 @@ import { ResponsePath, responsePathAsArray, } from 'graphql'; +import { ApolloServerPlugin } from "apollo-server-plugin-base"; import { GraphQLExtension, GraphQLResponse } from 'graphql-extensions'; @@ -49,6 +50,151 @@ declare module 'apollo-server-types' { } } +type MapResponsePathHints = Map; + +export const plugin = ( + options: CacheControlExtensionOptions = Object.create(null), +): ApolloServerPlugin => ({ + requestDidStart(requestContext) { + const defaultMaxAge: number = options.defaultMaxAge || 0; + const hints: MapResponsePathHints = new Map(); + + + function setOverallCachePolicyWhenUnset() { + if (!requestContext.overallCachePolicy) { + requestContext.overallCachePolicy = computeOverallCachePolicy(hints); + } + } + + return { + willResolveField(...args) { + const [, , , info] = args; + let hint: CacheHint = {}; + + // If this field's resolver returns an object or interface, look for + // hints on that return type. + const targetType = getNamedType(info.returnType); + if ( + targetType instanceof GraphQLObjectType || + targetType instanceof GraphQLInterfaceType + ) { + if (targetType.astNode) { + hint = mergeHints( + hint, + cacheHintFromDirectives(targetType.astNode.directives), + ); + } + } + + // Look for hints on the field itself (on its parent type), taking + // precedence over previously calculated hints. + const fieldDef = info.parentType.getFields()[info.fieldName]; + if (fieldDef.astNode) { + hint = mergeHints( + hint, + cacheHintFromDirectives(fieldDef.astNode.directives), + ); + } + + // If this resolver returns an object or is a root field and we haven't + // seen an explicit maxAge hint, set the maxAge to 0 (uncached) or the + // default if specified in the constructor. (Non-object fields by + // default are assumed to inherit their cacheability from their parents. + // But on the other hand, while root non-object fields can get explicit + // hints from their definition on the Query/Mutation object, if that + // doesn't exist then there's no parent field that would assign the + // default maxAge, so we do it here.) + if ( + (targetType instanceof GraphQLObjectType || + targetType instanceof GraphQLInterfaceType || + !info.path.prev) && + hint.maxAge === undefined + ) { + hint.maxAge = defaultMaxAge; + } + + if (hint.maxAge !== undefined || hint.scope !== undefined) { + addHint(hints, info.path, hint); + } + + info.cacheControl = { + setCacheHint: (hint: CacheHint) => { + addHint(hints, info.path, hint); + }, + cacheHint: hint, + }; + }, + + responseForOperation() { + // We are not supplying an answer, we are only setting the cache + // policy if it's not set! Therefore, we return null. + setOverallCachePolicyWhenUnset(); + return null; + }, + + executionDidStart() { + return () => setOverallCachePolicyWhenUnset(); + }, + + willSendResponse(requestContext) { + const { + response, + overallCachePolicy: overallCachePolicyOverride, + } = requestContext; + + // If there are any errors, we don't consider this cacheable. + if (response.errors) { + return; + } + + // Use the override by default, but if it's not overridden, set our + // own computation onto the `requestContext` for other plugins to read. + const overallCachePolicy = + overallCachePolicyOverride || + (requestContext.overallCachePolicy = + computeOverallCachePolicy(hints)); + + if ( + overallCachePolicy && + options.calculateHttpHeaders && + response.http + ) { + response.http.headers.set( + 'Cache-Control', + `max-age=${ + overallCachePolicy.maxAge + }, ${overallCachePolicy.scope.toLowerCase()}`, + ); + } + + // We should have to explicitly ask to leave the formatted extension in, + // or pass the old-school `cacheControl: true` (as interpreted by + // apollo-server-core/ApolloServer), in order to include the + // engineproxy-aimed extensions. Specifically, we want users of + // apollo-server-plugin-response-cache to be able to specify + // `cacheControl: {defaultMaxAge: 600}` without accidentally turning on + // the extension formatting. + if (options.stripFormattedExtensions !== false) return; + + const extensions = + response.extensions || (response.extensions = Object.create(null)); + + if (typeof extensions.cacheControl !== 'undefined') { + throw new Error("The cacheControl information already existed."); + } + + extensions.cacheControl = { + version: 1, + hints: Array.from(hints).map(([path, hint]) => ({ + path: [...responsePathAsArray(path)], + ...hint, + })), + }; + } + } + } +}); + export class CacheControlExtension implements GraphQLExtension { private defaultMaxAge: number; @@ -255,3 +401,45 @@ function mergeHints( scope: otherHint.scope || hint.scope, }; } + +function computeOverallCachePolicy( + hints: MapResponsePathHints, +): Required | undefined { + let lowestMaxAge: number | undefined = undefined; + let scope: CacheScope = CacheScope.Public; + + for (const hint of hints.values()) { + if (hint.maxAge !== undefined) { + lowestMaxAge = + lowestMaxAge !== undefined + ? Math.min(lowestMaxAge, hint.maxAge) + : hint.maxAge; + } + if (hint.scope === CacheScope.Private) { + scope = CacheScope.Private; + } + } + + // If maxAge is 0, then we consider it uncacheable so it doesn't matter what + // the scope was. + return lowestMaxAge + ? { + maxAge: lowestMaxAge, + scope, + } + : undefined; +} + +function addHint(hints: MapResponsePathHints, path: ResponsePath, hint: CacheHint) { + const existingCacheHint = hints.get(path); + if (existingCacheHint) { + hints.set(path, mergeHints(existingCacheHint, hint)); + } else { + hints.set(path, hint); + } +} + +export const __testing__ = { + addHint, + computeOverallCachePolicy, +}; diff --git a/packages/apollo-cache-control/tsconfig.json b/packages/apollo-cache-control/tsconfig.json index 0de28001c29..1276353ea04 100644 --- a/packages/apollo-cache-control/tsconfig.json +++ b/packages/apollo-cache-control/tsconfig.json @@ -8,5 +8,6 @@ "exclude": ["**/__tests__", "**/__mocks__"], "references": [ { "path": "../graphql-extensions" }, + { "path": "../apollo-server-plugin-base" }, ] } diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index bea27fea464..8bddf49873a 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -70,6 +70,10 @@ import { import { Headers } from 'apollo-server-env'; import { buildServiceDefinition } from '@apollographql/apollo-tools'; import { Logger } from "apollo-server-types"; +import { + plugin as pluginCacheControl, + CacheControlExtensionOptions, +} from 'apollo-cache-control'; const NoIntrospection = (context: ValidationContext) => ({ Field(node: FieldDefinitionNode) { @@ -190,6 +194,7 @@ export class ApolloServerBase { playground, plugins, gateway, + cacheControl, experimental_approximateDocumentStoreMiB, ...requestOptions } = config; @@ -249,31 +254,6 @@ export class ApolloServerBase { : noIntro; } - if (requestOptions.cacheControl !== false) { - if ( - typeof requestOptions.cacheControl === 'boolean' && - requestOptions.cacheControl === true - ) { - // cacheControl: true means that the user needs the cache-control - // extensions. This means we are running the proxy, so we should not - // strip out the cache control extension and not add cache-control headers - requestOptions.cacheControl = { - stripFormattedExtensions: false, - calculateHttpHeaders: false, - defaultMaxAge: 0, - }; - } else { - // Default behavior is to run default header calculation and return - // no cacheControl extensions - requestOptions.cacheControl = { - stripFormattedExtensions: true, - calculateHttpHeaders: true, - defaultMaxAge: 0, - ...requestOptions.cacheControl, - }; - } - } - if (!requestOptions.cache) { requestOptions.cache = new InMemoryLRUCache(); } @@ -790,7 +770,37 @@ export class ApolloServerBase { // at the end of that list so they take precidence. // A follow-up commit will actually introduce this. + // Enable cache control unless it was explicitly disabled. + if (this.config.cacheControl !== false) { + let cacheControlOptions: CacheControlExtensionOptions = {}; + if ( + typeof this.config.cacheControl === 'boolean' && + this.config.cacheControl === true + ) { + // cacheControl: true means that the user needs the cache-control + // extensions. This means we are running the proxy, so we should not + // strip out the cache control extension and not add cache-control headers + cacheControlOptions = { + stripFormattedExtensions: false, + calculateHttpHeaders: false, + defaultMaxAge: 0, + }; + } else { + // Default behavior is to run default header calculation and return + // no cacheControl extensions + cacheControlOptions = { + stripFormattedExtensions: true, + calculateHttpHeaders: true, + defaultMaxAge: 0, + ...this.config.cacheControl, + }; + } + + pluginsToInit.push(pluginCacheControl(cacheControlOptions)); + } + pluginsToInit.push(...plugins); + this.plugins = pluginsToInit.map(plugin => { if (typeof plugin === 'function') { return plugin(); diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 6393d82c95a..4751f44eb8f 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -22,10 +22,6 @@ import { symbolRequestListenerDispatcher, enablePluginsForSchemaResolvers, } from '.'; -import { - CacheControlExtension, - CacheControlExtensionOptions, -} from 'apollo-cache-control'; import { TracingExtension } from 'apollo-tracing'; import { ApolloError, @@ -97,7 +93,6 @@ export interface GraphQLRequestPipelineConfig { extensions?: Array<() => GraphQLExtension>; tracing?: boolean; persistedQueries?: PersistedQueryOptions; - cacheControl?: CacheControlExtensionOptions; formatError?: (error: GraphQLError) => GraphQLFormattedError; formatResponse?: ( @@ -126,7 +121,6 @@ export async function processGraphQLRequest( // all of our own machinery will certainly set it now. const logger = requestContext.logger || console; - let cacheControlExtension: CacheControlExtension | undefined; const extensionStack = initializeExtensionStack(); (requestContext.context as any)._extensionStack = extensionStack; @@ -390,20 +384,6 @@ export async function processGraphQLRequest( } } - if (cacheControlExtension) { - if (requestContext.overallCachePolicy) { - // If we read this response from a cache and it already has its own - // policy, teach that to cacheControlExtension so that it'll use the - // saved policy for HTTP headers. (If cacheControlExtension was a - // plugin, it could just read from the requestContext, but it isn't.) - cacheControlExtension.overrideOverallCachePolicy( - requestContext.overallCachePolicy, - ); - } else { - requestContext.overallCachePolicy = cacheControlExtension.computeOverallCachePolicy(); - } - } - const formattedExtensions = extensionStack.format(); if (Object.keys(formattedExtensions).length > 0) { response.extensions = formattedExtensions; @@ -604,11 +584,6 @@ export async function processGraphQLRequest( extensions.push(new TracingExtension()); } - if (config.cacheControl) { - cacheControlExtension = new CacheControlExtension(config.cacheControl); - extensions.push(cacheControlExtension); - } - return new GraphQLExtensionStack(extensions); } diff --git a/packages/apollo-server-core/src/runHttpQuery.ts b/packages/apollo-server-core/src/runHttpQuery.ts index c229fe87cf7..b02ce408fb0 100644 --- a/packages/apollo-server-core/src/runHttpQuery.ts +++ b/packages/apollo-server-core/src/runHttpQuery.ts @@ -17,7 +17,6 @@ import { GraphQLRequestContext, GraphQLResponse, } from './requestPipeline'; -import { CacheControlExtensionOptions } from 'apollo-cache-control'; import { ApolloServerPlugin } from 'apollo-server-plugin-base'; import { WithRequired, GraphQLExecutionResult } from 'apollo-server-types'; @@ -173,9 +172,6 @@ export async function runHttpQuery( // cacheControl defaults will also have been set if a boolean argument is // passed in. cache: options.cache!, - cacheControl: options.cacheControl as - | CacheControlExtensionOptions - | undefined, dataSources: options.dataSources, documentStore: options.documentStore, From 8db08b3b4d8be774be30178542b58a861d4bae3b Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 6 Apr 2020 20:46:55 +0300 Subject: [PATCH 15/98] eliminate! Remove now-unnecessary `apollo-cache-control` extension. Which has been replaced by the plugin API introduced in 68cbc938ff709078. --- package-lock.json | 3 +- packages/apollo-cache-control/package.json | 1 - ...ion.test.ts => cacheControlPlugin.test.ts} | 4 +- packages/apollo-cache-control/src/index.ts | 162 ------------------ packages/apollo-cache-control/tsconfig.json | 1 - 5 files changed, 3 insertions(+), 168 deletions(-) rename packages/apollo-cache-control/src/__tests__/{cacheControlExtension.test.ts => cacheControlPlugin.test.ts} (98%) diff --git a/package-lock.json b/package-lock.json index 519c1acc2ed..81f495d1916 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4277,8 +4277,7 @@ "version": "file:packages/apollo-cache-control", "requires": { "apollo-server-env": "file:packages/apollo-server-env", - "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base", - "graphql-extensions": "file:packages/graphql-extensions" + "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base" } }, "apollo-datasource": { diff --git a/packages/apollo-cache-control/package.json b/packages/apollo-cache-control/package.json index 23b4e1f583a..e4bb0cf3fbd 100644 --- a/packages/apollo-cache-control/package.json +++ b/packages/apollo-cache-control/package.json @@ -12,7 +12,6 @@ }, "dependencies": { "apollo-server-env": "file:../apollo-server-env", - "graphql-extensions": "file:../graphql-extensions", "apollo-server-plugin-base": "file:../apollo-server-plugin-base" }, "peerDependencies": { diff --git a/packages/apollo-cache-control/src/__tests__/cacheControlExtension.test.ts b/packages/apollo-cache-control/src/__tests__/cacheControlPlugin.test.ts similarity index 98% rename from packages/apollo-cache-control/src/__tests__/cacheControlExtension.test.ts rename to packages/apollo-cache-control/src/__tests__/cacheControlPlugin.test.ts index ff4d1fb75eb..a29cbbf02b1 100644 --- a/packages/apollo-cache-control/src/__tests__/cacheControlExtension.test.ts +++ b/packages/apollo-cache-control/src/__tests__/cacheControlPlugin.test.ts @@ -1,5 +1,4 @@ import { ResponsePath, GraphQLError } from 'graphql'; -import { GraphQLResponse } from 'graphql-extensions'; import { Headers } from 'apollo-server-env'; import { CacheScope, @@ -11,10 +10,11 @@ import { const { addHint, computeOverallCachePolicy } = __testing__; import { GraphQLRequestContextWillSendResponse, + GraphQLResponse, } from 'apollo-server-plugin-base'; import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness'; -describe('CacheControlExtension', () => { +describe('plugin', () => { describe('willSendResponse', () => { function makePluginWithOptions({ pluginInitializationOptions, diff --git a/packages/apollo-cache-control/src/index.ts b/packages/apollo-cache-control/src/index.ts index f1a9a944e42..de847c31106 100644 --- a/packages/apollo-cache-control/src/index.ts +++ b/packages/apollo-cache-control/src/index.ts @@ -3,14 +3,11 @@ import { getNamedType, GraphQLInterfaceType, GraphQLObjectType, - GraphQLResolveInfo, ResponsePath, responsePathAsArray, } from 'graphql'; import { ApolloServerPlugin } from "apollo-server-plugin-base"; -import { GraphQLExtension, GraphQLResponse } from 'graphql-extensions'; - export interface CacheControlFormat { version: 1; hints: ({ path: (string | number)[] } & CacheHint)[]; @@ -195,165 +192,6 @@ export const plugin = ( } }); -export class CacheControlExtension - implements GraphQLExtension { - private defaultMaxAge: number; - - constructor(public options: CacheControlExtensionOptions = {}) { - this.defaultMaxAge = options.defaultMaxAge || 0; - } - - private hints: Map = new Map(); - private overallCachePolicyOverride?: Required; - - willResolveField( - _source: any, - _args: { [argName: string]: any }, - _context: TContext, - info: GraphQLResolveInfo, - ) { - let hint: CacheHint = {}; - - // If this field's resolver returns an object or interface, look for hints - // on that return type. - const targetType = getNamedType(info.returnType); - if ( - targetType instanceof GraphQLObjectType || - targetType instanceof GraphQLInterfaceType - ) { - if (targetType.astNode) { - hint = mergeHints( - hint, - cacheHintFromDirectives(targetType.astNode.directives), - ); - } - } - - // Look for hints on the field itself (on its parent type), taking - // precedence over previously calculated hints. - const fieldDef = info.parentType.getFields()[info.fieldName]; - if (fieldDef.astNode) { - hint = mergeHints( - hint, - cacheHintFromDirectives(fieldDef.astNode.directives), - ); - } - - // If this resolver returns an object or is a root field and we haven't seen - // an explicit maxAge hint, set the maxAge to 0 (uncached) or the default if - // specified in the constructor. (Non-object fields by default are assumed - // to inherit their cacheability from their parents. But on the other hand, - // while root non-object fields can get explicit hints from their definition - // on the Query/Mutation object, if that doesn't exist then there's no - // parent field that would assign the default maxAge, so we do it here.) - if ( - (targetType instanceof GraphQLObjectType || - targetType instanceof GraphQLInterfaceType || - !info.path.prev) && - hint.maxAge === undefined - ) { - hint.maxAge = this.defaultMaxAge; - } - - if (hint.maxAge !== undefined || hint.scope !== undefined) { - this.addHint(info.path, hint); - } - - info.cacheControl = { - setCacheHint: (hint: CacheHint) => { - this.addHint(info.path, hint); - }, - cacheHint: hint, - }; - } - - addHint(path: ResponsePath, hint: CacheHint) { - const existingCacheHint = this.hints.get(path); - if (existingCacheHint) { - this.hints.set(path, mergeHints(existingCacheHint, hint)); - } else { - this.hints.set(path, hint); - } - } - - format(): [string, CacheControlFormat] | undefined { - // We should have to explicitly ask to leave the formatted extension in, or - // pass the old-school `cacheControl: true` (as interpreted by - // apollo-server-core/ApolloServer), in order to include the - // engineproxy-aimed extensions. Specifically, we want users of - // apollo-server-plugin-response-cache to be able to specify - // `cacheControl: {defaultMaxAge: 600}` without accidentally turning on the - // extension formatting. - if (this.options.stripFormattedExtensions !== false) return; - - return [ - 'cacheControl', - { - version: 1, - hints: Array.from(this.hints).map(([path, hint]) => ({ - path: [...responsePathAsArray(path)], - ...hint, - })), - }, - ]; - } - - public willSendResponse?(o: { graphqlResponse: GraphQLResponse }) { - if ( - !this.options.calculateHttpHeaders || - !o.graphqlResponse.http || - o.graphqlResponse.errors - ) { - return; - } - - const overallCachePolicy = this.computeOverallCachePolicy(); - - if (overallCachePolicy) { - o.graphqlResponse.http.headers.set( - 'Cache-Control', - `max-age=${ - overallCachePolicy.maxAge - }, ${overallCachePolicy.scope.toLowerCase()}`, - ); - } - } - - public overrideOverallCachePolicy(overallCachePolicy: Required) { - this.overallCachePolicyOverride = overallCachePolicy; - } - - computeOverallCachePolicy(): Required | undefined { - if (this.overallCachePolicyOverride) { - return this.overallCachePolicyOverride; - } - - let lowestMaxAge: number | undefined = undefined; - let scope: CacheScope = CacheScope.Public; - - for (const hint of this.hints.values()) { - if (hint.maxAge !== undefined) { - lowestMaxAge = - lowestMaxAge !== undefined - ? Math.min(lowestMaxAge, hint.maxAge) - : hint.maxAge; - } - if (hint.scope === CacheScope.Private) { - scope = CacheScope.Private; - } - } - - // If maxAge is 0, then we consider it uncacheable so it doesn't matter what - // the scope was. - return lowestMaxAge - ? { - maxAge: lowestMaxAge, - scope, - } - : undefined; - } -} - function cacheHintFromDirectives( directives: ReadonlyArray | undefined, ): CacheHint | undefined { diff --git a/packages/apollo-cache-control/tsconfig.json b/packages/apollo-cache-control/tsconfig.json index 1276353ea04..29dff935854 100644 --- a/packages/apollo-cache-control/tsconfig.json +++ b/packages/apollo-cache-control/tsconfig.json @@ -7,7 +7,6 @@ "include": ["src/**/*"], "exclude": ["**/__tests__", "**/__mocks__"], "references": [ - { "path": "../graphql-extensions" }, { "path": "../apollo-server-plugin-base" }, ] } From 0e5619c3dfe57e4cfb8c1a753a30f5645b07cc4f Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 15 Apr 2020 12:18:41 +0300 Subject: [PATCH 16/98] Place `schema` and `schemaHash` on the `GraphQLRequestContext`. Particularly with multiple schemas in-flight at the same time, plugins need a way to understand which schema is being operated on. Presently, while `serverWillStart` can give you the `schema` as part of its `GraphQLServiceContext`, there isn't (yet) an `onSchemaChange` (and I'm not certain there will be, right now!). Even if that did exist though, with the introduction of the gateway, it's absolutely possible that there are multiple overlapping requests with different schemas (even if the server doesn't explicitly support the configuration of multiple independent schemas right now). --- packages/apollo-server-core/src/ApolloServer.ts | 6 +++++- packages/apollo-server-core/src/graphqlOptions.ts | 2 ++ packages/apollo-server-core/src/runHttpQuery.ts | 3 +++ packages/apollo-server-core/src/utils/pluginTestHarness.ts | 3 +++ packages/apollo-server-types/src/index.ts | 3 +++ 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index bea27fea464..2ccc287eefe 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -819,7 +819,8 @@ export class ApolloServerBase { protected async graphQLServerOptions( integrationContextArgument?: Record, ): Promise { - const { schema, documentStore, extensions } = await this.schemaDerivedData; + const { schema, schemaHash, documentStore, extensions } + = await this.schemaDerivedData; let context: Context = this.context ? this.context : {}; @@ -837,6 +838,7 @@ export class ApolloServerBase { return { schema, + schemaHash, logger: this.logger, plugins: this.plugins, documentStore, @@ -866,6 +868,8 @@ export class ApolloServerBase { const requestCtx: GraphQLRequestContext = { logger: this.logger, + schema: options.schema, + schemaHash: options.schemaHash, request, context: options.context || Object.create(null), cache: options.cache!, diff --git a/packages/apollo-server-core/src/graphqlOptions.ts b/packages/apollo-server-core/src/graphqlOptions.ts index e6da70d37b0..b4c6f1a7b86 100644 --- a/packages/apollo-server-core/src/graphqlOptions.ts +++ b/packages/apollo-server-core/src/graphqlOptions.ts @@ -18,6 +18,7 @@ import { GraphQLResponse, GraphQLRequestContext, Logger, + SchemaHash, } from 'apollo-server-types'; /* @@ -42,6 +43,7 @@ export interface GraphQLServerOptions< TRootValue = any > { schema: GraphQLSchema; + schemaHash: SchemaHash; logger?: Logger; formatError?: (error: GraphQLError) => GraphQLFormattedError; rootValue?: ((parsedQuery: DocumentNode) => TRootValue) | TRootValue; diff --git a/packages/apollo-server-core/src/runHttpQuery.ts b/packages/apollo-server-core/src/runHttpQuery.ts index c229fe87cf7..67f3cde40ab 100644 --- a/packages/apollo-server-core/src/runHttpQuery.ts +++ b/packages/apollo-server-core/src/runHttpQuery.ts @@ -161,6 +161,7 @@ export async function runHttpQuery( const config = { schema: options.schema, + schemaHash: options.schemaHash, logger: options.logger, rootValue: options.rootValue, context: options.context || {}, @@ -256,6 +257,8 @@ export async function processHTTPRequest( // exported since perhaps as far back as Apollo Server 1.x. Therefore, // for compatibility reasons, we'll default to `console`. logger: options.logger || console, + schema: options.schema, + schemaHash: options.schemaHash, request, response: { http: { diff --git a/packages/apollo-server-core/src/utils/pluginTestHarness.ts b/packages/apollo-server-core/src/utils/pluginTestHarness.ts index 0afa7dfde96..52277e50f34 100644 --- a/packages/apollo-server-core/src/utils/pluginTestHarness.ts +++ b/packages/apollo-server-core/src/utils/pluginTestHarness.ts @@ -17,6 +17,7 @@ import { import { ApolloServerPlugin } from 'apollo-server-plugin-base'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { Dispatcher } from './dispatcher'; +import { generateSchemaHash } from "./schemaHash"; // This test harness guarantees the presence of `query`. type IPluginTestHarnessGraphqlRequest = WithRequired; @@ -98,6 +99,8 @@ export default async function pluginTestHarness({ const requestContext: GraphQLRequestContext = { logger: logger || console, + schema, + schemaHash: generateSchemaHash(schema), request: graphqlRequest, metrics: Object.create(null), source: graphqlRequest.query, diff --git a/packages/apollo-server-types/src/index.ts b/packages/apollo-server-types/src/index.ts index 40d2639e4ff..edfc9c64f76 100644 --- a/packages/apollo-server-types/src/index.ts +++ b/packages/apollo-server-types/src/index.ts @@ -65,6 +65,9 @@ export interface GraphQLRequestContext> { logger: Logger; + readonly schema: GraphQLSchema; + readonly schemaHash: SchemaHash; + readonly context: TContext; readonly cache: KeyValueCache; From de7ba72b50ff539fc33b59cf88553527b6e99a70 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 30 Mar 2020 15:02:44 +0300 Subject: [PATCH 17/98] refactor: Graph Manager (Engine) reporting "extensions" become "plugins". Similar to 6009d8a00 (#3991) and 68cbc938 (#3997), which migrated the tracing and cache-control extensions to the (newer) request pipeline plugin API, this commit introduces: - Internally, a `plugin` named export which is utilized by the `agent`'s `newExtension` method to provide a plugin which is instrumented to transmit metrics to Apollo Graph Manager. This plugin is meant to replicate the behavior of the `EngineReportingExtension` class which, as of this commit, still lives besides it. - Externally, a `federatedPlugin` exported on the main module of the `apollo-engine-reporting` package. This plugin is meant to replicate the behavior of the `EngineFederatedTracingExtension` class (also exported on the main module) which, again as of this commit, still lives besides it! Again, the delta of the commits seemed more confusing by allowing a natural `diff` to be made of it, I've left the extensions in place so they can be compared - presumably side-by-side in an editor - on the same commit. An (immediate) subsequent commit will remove the extension. --- packages/apollo-engine-reporting/package.json | 1 + .../src/__tests__/extension.test.ts | 56 ++---- packages/apollo-engine-reporting/src/agent.ts | 11 +- .../apollo-engine-reporting/src/extension.ts | 174 ++++++++++++++++++ .../src/federatedExtension.ts | 62 +++++++ packages/apollo-engine-reporting/src/index.ts | 2 +- .../src/treeBuilder.ts | 2 +- .../apollo-engine-reporting/tsconfig.json | 1 + .../apollo-server-core/src/ApolloServer.ts | 70 ++++--- 9 files changed, 297 insertions(+), 82 deletions(-) diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index df8036f35cb..fa666882864 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -18,6 +18,7 @@ "apollo-server-errors": "file:../apollo-server-errors", "apollo-server-types": "file:../apollo-server-types", "async-retry": "^1.2.1", + "apollo-server-plugin-base": "file:../apollo-server-plugin-base", "graphql-extensions": "file:../graphql-extensions" }, "peerDependencies": { diff --git a/packages/apollo-engine-reporting/src/__tests__/extension.test.ts b/packages/apollo-engine-reporting/src/__tests__/extension.test.ts index 14b50c73cfd..8520f902f36 100644 --- a/packages/apollo-engine-reporting/src/__tests__/extension.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/extension.test.ts @@ -1,19 +1,11 @@ import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools'; -import { - GraphQLExtensionStack, - enableGraphQLExtensions, -} from 'graphql-extensions'; import { graphql, GraphQLError } from 'graphql'; import { Request } from 'node-fetch'; -import { - EngineReportingExtension, - makeTraceDetails, - makeHTTPRequestHeaders, -} from '../extension'; +import { makeTraceDetails, makeHTTPRequestHeaders, plugin } from '../extension'; import { Headers } from 'apollo-server-env'; -import { InMemoryLRUCache } from 'apollo-server-caching'; import { AddTraceArgs } from '../agent'; import { Trace } from 'apollo-engine-reporting-protobuf'; +import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness'; it('trace construction', async () => { const typeDefs = ` @@ -53,41 +45,33 @@ it('trace construction', async () => { const schema = makeExecutableSchema({ typeDefs }); addMockFunctionsToSchema({ schema }); - enableGraphQLExtensions(schema); const traces: Array = []; async function addTrace(args: AddTraceArgs) { traces.push(args); } - const reportingExtension = new EngineReportingExtension( - {}, - addTrace, - 'schema-hash', - ); - const stack = new GraphQLExtensionStack([reportingExtension]); - const requestDidEnd = stack.requestDidStart({ - request: new Request('http://localhost:123/foo'), - queryString: query, - requestContext: { - request: { - query, - operationName: 'q', - extensions: { - clientName: 'testing suite', - }, + const pluginInstance = plugin({ /* no options!*/ }, addTrace); + + pluginTestHarness({ + pluginInstance, + schema, + graphqlRequest: { + query, + operationName: 'q', + extensions: { + clientName: 'testing suite', }, - context: {}, - cache: new InMemoryLRUCache(), + http: new Request('http://localhost:123/foo'), + }, + executor: async ({ request: { query: source }}) => { + return await graphql({ + schema, + source, + }); }, - context: {}, - }); - await graphql({ - schema, - source: query, - contextValue: { _extensionStack: stack }, }); - requestDidEnd(); + // XXX actually write some tests }); diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index dd522203b71..d98861bea3b 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -11,10 +11,11 @@ import { import { fetch, RequestAgent, Response } from 'apollo-server-env'; import retry from 'async-retry'; -import { EngineReportingExtension } from './extension'; +import { plugin } from './extension'; import { GraphQLRequestContext, Logger, SchemaHash } from 'apollo-server-types'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { defaultEngineReportingSignature } from 'apollo-graphql'; +import { ApolloServerPlugin } from "apollo-server-plugin-base"; export interface ClientInfo { clientName?: string; @@ -278,12 +279,8 @@ export class EngineReportingAgent { handleLegacyOptions(this.options); } - public newExtension(schemaHash: SchemaHash): EngineReportingExtension { - return new EngineReportingExtension( - this.options, - this.addTrace.bind(this), - schemaHash, - ); + public newExtension(): ApolloServerPlugin { + return plugin(this.options, this.addTrace.bind(this)); } public async addTrace({ diff --git a/packages/apollo-engine-reporting/src/extension.ts b/packages/apollo-engine-reporting/src/extension.ts index aacc702dcb1..00e0a18da6c 100644 --- a/packages/apollo-engine-reporting/src/extension.ts +++ b/packages/apollo-engine-reporting/src/extension.ts @@ -22,6 +22,9 @@ import { SendValuesBaseOptions, } from './agent'; import { EngineReportingTreeBuilder } from './treeBuilder'; +import { ApolloServerPlugin } from "apollo-server-plugin-base"; + +type Mutable = { -readonly [P in keyof T]: T[P] }; const clientNameHeaderKey = 'apollographql-client-name'; const clientReferenceIdHeaderKey = 'apollographql-client-reference-id'; @@ -208,6 +211,177 @@ export class EngineReportingExtension } } +// This plugin is instantiated once at server start-up. Each request that the +// server processes will invoke the `requestDidStart` method which will produce +// a trace (in protobuf Trace format) for that single request. When the request +// is done, it passes the Trace back to its associated EngineReportingAgent via +// the addTrace callback. This class isn't for direct use; its constructor is a +// private API for communicating with EngineReportingAgent. +export const plugin = ( + options: EngineReportingOptions = Object.create(null), + addTrace: (args: AddTraceArgs) => Promise, + // schemaHash: string, +): ApolloServerPlugin => { + const logger: Logger = options.logger || console; + const generateClientInfo: GenerateClientInfo = + options.generateClientInfo || defaultGenerateClientInfo; + + + return { + requestDidStart(requestContext) { + let queryString: string | undefined; + const treeBuilder: EngineReportingTreeBuilder = + new EngineReportingTreeBuilder({ + rewriteError: options.rewriteError, + logger: requestContext.logger || logger, + }); + + const metrics: NonNullable = + ((requestContext as Mutable) + .metrics = requestContext.metrics || Object.create(null)); + + treeBuilder.startTiming(); + metrics.startHrTime = treeBuilder.startHrTime; + + if (requestContext.request.http) { + treeBuilder.trace.http = new Trace.HTTP({ + method: + Trace.HTTP.Method[ + requestContext.request.http + .method as keyof typeof Trace.HTTP.Method + ] || Trace.HTTP.Method.UNKNOWN, + // Host and path are not used anywhere on the backend, so let's not bother + // trying to parse request.url to get them, which is a potential + // source of bugs because integrations have different behavior here. + // On Node's HTTP module, request.url only includes the path + // (see https://nodejs.org/api/http.html#http_message_url) + // The same is true on Lambda (where we pass event.path) + // But on environments like Cloudflare we do get a complete URL. + host: null, + path: null, + }); + } + + let preflightDone: boolean = false; + function ensurePreflight() { + if (preflightDone) return; + preflightDone = true; + + if (options.sendHeaders) { + if (requestContext.request.http && treeBuilder.trace.http) { + makeHTTPRequestHeaders( + treeBuilder.trace.http, + requestContext.request.http.headers, + options.sendHeaders, + ); + } + } + + if (metrics.persistedQueryHit) { + treeBuilder.trace.persistedQueryHit = true; + } + if (metrics.persistedQueryRegister) { + treeBuilder.trace.persistedQueryRegister = true; + } + + // Generally, we'll get queryString here and not parsedQuery; we only get + // parsedQuery if you're using an OperationStore. In normal cases we'll + // get our documentAST in the execution callback after it is parsed. + queryString = requestContext.source; + + if (requestContext.request.variables) { + treeBuilder.trace.details = makeTraceDetails( + requestContext.request.variables, + options.sendVariableValues, + queryString, + ); + } + + const clientInfo = generateClientInfo(requestContext); + if (clientInfo) { + // While clientAddress could be a part of the protobuf, we'll ignore it for + // now, since the backend does not group by it and Engine frontend will not + // support it in the short term + const { clientName, clientVersion, clientReferenceId } = clientInfo; + // the backend makes the choice of mapping clientName => clientReferenceId if + // no custom reference id is provided + treeBuilder.trace.clientVersion = clientVersion || ''; + treeBuilder.trace.clientReferenceId = clientReferenceId || ''; + treeBuilder.trace.clientName = clientName || ''; + } + } + + let endDone: boolean = false; + function didEnd() { + if (endDone) return; + endDone = true; + treeBuilder.stopTiming(); + + treeBuilder.trace.fullQueryCacheHit = !!metrics.responseCacheHit; + treeBuilder.trace.forbiddenOperation = !!metrics.forbiddenOperation; + treeBuilder.trace.registeredOperation = !!metrics.registeredOperation; + + // If the user did not explicitly specify an operation name (which we + // would have saved in `executionDidStart`), but the request pipeline made + // it far enough to figure out what the operation name must be and store + // it on requestContext.operationName, use that name. (Note that this + // depends on the assumption that the RequestContext passed to + // requestDidStart, which does not yet have operationName, will be mutated + // to add operationName later.) + const operationName = requestContext.operationName || ''; + + // If this was a federated operation and we're the gateway, add the query plan + // to the trace. + if (metrics.queryPlanTrace) { + treeBuilder.trace.queryPlan = metrics.queryPlanTrace; + } + + addTrace({ + operationName, + queryHash: requestContext.queryHash!, + documentAST: requestContext.document, + queryString, + trace: treeBuilder.trace, + schemaHash: requestContext.schemaHash, + }); + } + + return { + parsingDidStart() { + ensurePreflight(); + }, + + validationDidStart() { + ensurePreflight(); + }, + + didResolveOperation() { + ensurePreflight(); + }, + + executionDidStart() { + ensurePreflight(); + return didEnd; + }, + + willResolveField(...args) { + const [, , , info] = args; + return treeBuilder.willResolveField(info); + // We could save the error into the trace during the end handler, but + // it won't have all the information that graphql-js adds to it later, + // like 'locations'. + }, + + didEncounterErrors({ errors }) { + ensurePreflight(); + treeBuilder.didEncounterErrors(errors); + didEnd(); + }, + }; + } + }; +}; + // Helpers for producing traces. function defaultGenerateClientInfo({ request }: GraphQLRequestContext) { diff --git a/packages/apollo-engine-reporting/src/federatedExtension.ts b/packages/apollo-engine-reporting/src/federatedExtension.ts index 711c95fbf6e..a811cd04149 100644 --- a/packages/apollo-engine-reporting/src/federatedExtension.ts +++ b/packages/apollo-engine-reporting/src/federatedExtension.ts @@ -4,6 +4,68 @@ import { Trace } from 'apollo-engine-reporting-protobuf'; import { GraphQLRequestContext } from 'apollo-server-types'; import { EngineReportingTreeBuilder } from './treeBuilder'; +import { ApolloServerPlugin } from "apollo-server-plugin-base"; +import { EngineReportingOptions } from "./agent"; + +type FederatedReportingOptions = Pick, 'rewriteError'> + +export const plugin = ( + options: FederatedReportingOptions = Object.create(null), +): ApolloServerPlugin => { + return { + requestDidStart(requestContext) { + const treeBuilder: EngineReportingTreeBuilder = + new EngineReportingTreeBuilder({ + rewriteError: options.rewriteError, + }); + + // XXX Provide a mechanism to customize this logic. + const http = requestContext.request.http; + if ( + !http || + !http.headers || + http.headers.get('apollo-federation-include-trace') !== 'ftv1' + ) { + return; + } + + treeBuilder.startTiming(); + + return { + willResolveField(...args) { + const [ , , , info] = args; + return treeBuilder.willResolveField(info); + }, + + didEncounterErrors({ errors }) { + treeBuilder.didEncounterErrors(errors); + }, + + willSendResponse({ response }) { + // We record the end time at the latest possible time: right before serializing the trace. + // If we wait any longer, the time we record won't actually be sent anywhere! + treeBuilder.stopTiming(); + + const encodedUint8Array = Trace.encode(treeBuilder.trace).finish(); + const encodedBuffer = Buffer.from( + encodedUint8Array, + encodedUint8Array.byteOffset, + encodedUint8Array.byteLength, + ); + + const extensions = + response.extensions || (response.extensions = Object.create(null)); + + if (typeof extensions.ftv1 !== "undefined") { + throw new Error("The `ftv1` `extensions` were already present."); + } + + extensions.ftv1 = encodedBuffer.toString('base64'); + } + } + }, + } +}; export class EngineFederatedTracingExtension implements GraphQLExtension { diff --git a/packages/apollo-engine-reporting/src/index.ts b/packages/apollo-engine-reporting/src/index.ts index 5770edf02c3..36c7e53a85f 100644 --- a/packages/apollo-engine-reporting/src/index.ts +++ b/packages/apollo-engine-reporting/src/index.ts @@ -1,2 +1,2 @@ export { EngineReportingOptions, EngineReportingAgent } from './agent'; -export { EngineFederatedTracingExtension } from './federatedExtension'; +export { plugin as federatedPlugin } from './federatedExtension'; diff --git a/packages/apollo-engine-reporting/src/treeBuilder.ts b/packages/apollo-engine-reporting/src/treeBuilder.ts index a10535bbe23..f38283d572f 100644 --- a/packages/apollo-engine-reporting/src/treeBuilder.ts +++ b/packages/apollo-engine-reporting/src/treeBuilder.ts @@ -78,7 +78,7 @@ export class EngineReportingTreeBuilder { }; } - public didEncounterErrors(errors: GraphQLError[]) { + public didEncounterErrors(errors: readonly GraphQLError[]) { errors.forEach(err => { if ( err instanceof PersistedQueryNotFoundError || diff --git a/packages/apollo-engine-reporting/tsconfig.json b/packages/apollo-engine-reporting/tsconfig.json index ca382f1094c..1e8b57f68f2 100644 --- a/packages/apollo-engine-reporting/tsconfig.json +++ b/packages/apollo-engine-reporting/tsconfig.json @@ -10,5 +10,6 @@ { "path": "../graphql-extensions" }, { "path": "../apollo-server-errors" }, { "path": "../apollo-server-types" }, + { "path": "../apollo-server-plugin-base" }, ] } diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 4ae62ad9889..56afdc34a1c 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -225,10 +225,6 @@ export class ApolloServerBase { this.parseOptions = parseOptions; this.context = context; - // Plugins will be instantiated if they aren't already, and this.plugins - // is populated accordingly. - this.ensurePluginInstantiation(plugins); - // While reading process.env is slow, a server should only be constructed // once per run, so we place the env check inside the constructor. If env // should be used outside of the constructor context, place it as a private @@ -413,6 +409,11 @@ export class ApolloServerBase { } else { throw new Error("Unexpected error: Unable to resolve a valid GraphQLSchema. Please file an issue with a reproduction of this error, if possible."); } + + // Plugins will be instantiated if they aren't already, and this.plugins + // is populated accordingly. + this.ensurePluginInstantiation(plugins); + } // used by integrations to synchronize the path with subscriptions, some @@ -562,39 +563,6 @@ export class ApolloServerBase { const extensions = []; - const schemaIsFederated = this.schemaIsFederated(schema); - const { engine } = this.config; - // Keep this extension second so it wraps everything, except error formatting - if (this.engineReportingAgent) { - if (schemaIsFederated) { - // XXX users can configure a federated Apollo Server to send metrics, but the - // Gateway should be responsible for that. It's possible that users are running - // their own gateway or running a federated service on its own. Nonetheless, in - // the likely case it was accidental, we warn users that they should only report - // metrics from the Gateway. - this.logger.warn( - "It looks like you're running a federated schema and you've configured your service " + - 'to report metrics to Apollo Graph Manager. You should only configure your Apollo gateway ' + - 'to report metrics to Apollo Graph Manager.', - ); - } - extensions.push(() => - this.engineReportingAgent!.newExtension(schemaHash), - ); - } else if (engine !== false && schemaIsFederated) { - // 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 { - EngineFederatedTracingExtension, - } = require('apollo-engine-reporting'); - const rewriteError = - engine && typeof engine === 'object' ? engine.rewriteError : undefined; - extensions.push( - () => new EngineFederatedTracingExtension({ rewriteError }), - ); - } - // Note: doRunQuery will add its own extensions if you set tracing, // or cacheControl. extensions.push(...(_extensions || [])); @@ -789,6 +757,34 @@ export class ApolloServerBase { // User's plugins, provided as an argument to this method, will be added // at the end of that list so they take precidence. // A follow-up commit will actually introduce this. + // Also, TODO, remove this comment. + + const federatedSchema = this.schema && this.schemaIsFederated(this.schema); + const { engine } = this.config; + // Keep this extension second so it wraps everything, except error formatting + if (this.engineReportingAgent) { + if (federatedSchema) { + // XXX users can configure a federated Apollo Server to send metrics, but the + // Gateway should be responsible for that. It's possible that users are running + // their own gateway or running a federated service on its own. Nonetheless, in + // the likely case it was accidental, we warn users that they should only report + // metrics from the Gateway. + this.logger.warn( + "It looks like you're running a federated schema and you've configured your service " + + 'to report metrics to Apollo Graph Manager. You should only configure your Apollo gateway ' + + 'to report metrics to Apollo Graph Manager.', + ); + } + pluginsToInit.push(this.engineReportingAgent!.newExtension()); + } else if (engine !== false && federatedSchema) { + // 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 })); + } pluginsToInit.push(...plugins); this.plugins = pluginsToInit.map(plugin => { From 78a4cb77edc7c7f11b68f5969a6e85b75e75b6e7 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 14 Apr 2020 17:24:42 +0300 Subject: [PATCH 18/98] fix: Keep special-cased errors (e.g. APQ not found) as unreported. This fixes the failing tests which correctly surfaced on the last commit. Previously, prior to the new plugin API, the Apollo Engine Reporting mechanism was implemented using `graphql-extensions`, the API for which didn't invoke `requestDidStart` until _after_ APQ had been negotiated. The new plugin API starts its `requestDidStart` _before_ APQ validation and various other assertions which weren't included in the `requestDidStart` life-cycle, even if they perhaps should be in terms of error reporting. The new plugin API is able to properly capture such errors within its `didEncounterErrors` lifecycle hook (thanks to https://github.com/apollographql/apollo-server/pull/3614, which intentionally captures these failures so plugin authors can accurately react to them), however, for behavioral consistency reasons, we will still special-case those errors and maintain the legacy behavior to avoid a breaking change. We can reconsider this in a future version of Apollo Engine Reporting (AS3, perhaps!). Ref: https://github.com/apollographql/apollo-server/pull/3614 Ref: https://github.com/apollographql/apollo-server/issues/3627 Ref: https://github.com/apollographql/apollo-server/issues/3638 --- .../apollo-engine-reporting/src/extension.ts | 44 +++++++++++++++++++ .../src/treeBuilder.ts | 14 +----- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/packages/apollo-engine-reporting/src/extension.ts b/packages/apollo-engine-reporting/src/extension.ts index 00e0a18da6c..d3964fd0914 100644 --- a/packages/apollo-engine-reporting/src/extension.ts +++ b/packages/apollo-engine-reporting/src/extension.ts @@ -3,6 +3,7 @@ import { WithRequired, Logger, SchemaHash, + InvalidGraphQLRequestError, } from 'apollo-server-types'; import { Request, Headers } from 'apollo-server-env'; import { @@ -23,6 +24,10 @@ import { } from './agent'; import { EngineReportingTreeBuilder } from './treeBuilder'; import { ApolloServerPlugin } from "apollo-server-plugin-base"; +import { + PersistedQueryNotFoundError, + PersistedQueryNotSupportedError, +} from 'apollo-server-errors'; type Mutable = { -readonly [P in keyof T]: T[P] }; @@ -373,6 +378,12 @@ export const plugin = ( }, didEncounterErrors({ errors }) { + // We don't report some special-cased errors to Graph Manager. + // See the definition of this function for the reasons. + if (allUnreportableSpecialCasedErrors(errors)) { + return; + } + ensurePreflight(); treeBuilder.didEncounterErrors(errors); didEnd(); @@ -382,6 +393,39 @@ export const plugin = ( }; }; +/** + * Previously, prior to the new plugin API, the Apollo Engine Reporting + * mechanism was implemented using `graphql-extensions`, the API for which + * didn't invoke `requestDidStart` until _after_ APQ had been negotiated. + * + * The new plugin API starts its `requestDidStart` _before_ APQ validation and + * various other assertions which weren't included in the `requestDidStart` + * life-cycle, even if they perhaps should be in terms of error reporting. + * + * The new plugin API is able to properly capture such errors within its + * `didEncounterErrors` lifecycle hook, however, for behavioral consistency + * reasons, we will still special-case those errors and maintain the legacy + * behavior to avoid a breaking change. We can reconsider this in a future + * version of Apollo Engine Reporting (AS3, perhaps!). + * + * @param errors A list of errors to scan for special-cased instances. + */ +function allUnreportableSpecialCasedErrors( + errors: readonly GraphQLError[], +): boolean { + return errors.every(err => { + if ( + err instanceof PersistedQueryNotFoundError || + err instanceof PersistedQueryNotSupportedError || + err instanceof InvalidGraphQLRequestError + ) { + return true; + } + + return false; + }); +} + // Helpers for producing traces. function defaultGenerateClientInfo({ request }: GraphQLRequestContext) { diff --git a/packages/apollo-engine-reporting/src/treeBuilder.ts b/packages/apollo-engine-reporting/src/treeBuilder.ts index f38283d572f..27bfe9376ba 100644 --- a/packages/apollo-engine-reporting/src/treeBuilder.ts +++ b/packages/apollo-engine-reporting/src/treeBuilder.ts @@ -1,10 +1,6 @@ import { GraphQLError, GraphQLResolveInfo, ResponsePath } from 'graphql'; import { Trace, google } from 'apollo-engine-reporting-protobuf'; -import { - PersistedQueryNotFoundError, - PersistedQueryNotSupportedError, -} from 'apollo-server-errors'; -import { InvalidGraphQLRequestError, Logger } from 'apollo-server-types'; +import { Logger } from 'apollo-server-types'; function internalError(message: string) { return new Error(`[internal apollo-server error] ${message}`); @@ -80,14 +76,6 @@ export class EngineReportingTreeBuilder { public didEncounterErrors(errors: readonly GraphQLError[]) { errors.forEach(err => { - if ( - err instanceof PersistedQueryNotFoundError || - err instanceof PersistedQueryNotSupportedError || - err instanceof InvalidGraphQLRequestError - ) { - return; - } - // This is an error from a federated service. We will already be reporting // it in the nested Trace in the query plan. // From c67a6dfe0c5d352ac92a44a80711b2c89bbc820b Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 15 Apr 2020 13:35:34 +0300 Subject: [PATCH 19/98] eliminate!: Remove now deprecated `EngineReportingExtension`. The plugin implementation brought in de7ba72b50ff5 supersedes the need for this implementation! --- package-lock.json | 4 +- packages/apollo-engine-reporting/package.json | 3 +- .../{extension.test.ts => plugin.test.ts} | 2 +- packages/apollo-engine-reporting/src/agent.ts | 2 +- .../src/federatedExtension.ts | 147 ------------- .../src/federatedPlugin.ts | 66 ++++++ packages/apollo-engine-reporting/src/index.ts | 2 +- .../src/{extension.ts => plugin.ts} | 193 +----------------- .../apollo-engine-reporting/tsconfig.json | 1 - 9 files changed, 74 insertions(+), 346 deletions(-) rename packages/apollo-engine-reporting/src/__tests__/{extension.test.ts => plugin.test.ts} (99%) delete mode 100644 packages/apollo-engine-reporting/src/federatedExtension.ts create mode 100644 packages/apollo-engine-reporting/src/federatedPlugin.ts rename packages/apollo-engine-reporting/src/{extension.ts => plugin.ts} (66%) diff --git a/package-lock.json b/package-lock.json index 20a701000e3..1b638d12e8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4305,9 +4305,9 @@ "apollo-server-caching": "file:packages/apollo-server-caching", "apollo-server-env": "file:packages/apollo-server-env", "apollo-server-errors": "file:packages/apollo-server-errors", + "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base", "apollo-server-types": "file:packages/apollo-server-types", - "async-retry": "^1.2.1", - "graphql-extensions": "file:packages/graphql-extensions" + "async-retry": "^1.2.1" } }, "apollo-engine-reporting-protobuf": { diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index fa666882864..05f26ccdd9a 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -18,8 +18,7 @@ "apollo-server-errors": "file:../apollo-server-errors", "apollo-server-types": "file:../apollo-server-types", "async-retry": "^1.2.1", - "apollo-server-plugin-base": "file:../apollo-server-plugin-base", - "graphql-extensions": "file:../graphql-extensions" + "apollo-server-plugin-base": "file:../apollo-server-plugin-base" }, "peerDependencies": { "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" diff --git a/packages/apollo-engine-reporting/src/__tests__/extension.test.ts b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts similarity index 99% rename from packages/apollo-engine-reporting/src/__tests__/extension.test.ts rename to packages/apollo-engine-reporting/src/__tests__/plugin.test.ts index 8520f902f36..2a8d3313b12 100644 --- a/packages/apollo-engine-reporting/src/__tests__/extension.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts @@ -1,7 +1,7 @@ import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools'; import { graphql, GraphQLError } from 'graphql'; import { Request } from 'node-fetch'; -import { makeTraceDetails, makeHTTPRequestHeaders, plugin } from '../extension'; +import { makeTraceDetails, makeHTTPRequestHeaders, plugin } from '../plugin'; import { Headers } from 'apollo-server-env'; import { AddTraceArgs } from '../agent'; import { Trace } from 'apollo-engine-reporting-protobuf'; diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index d98861bea3b..71de2c2f08c 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -11,7 +11,7 @@ import { import { fetch, RequestAgent, Response } from 'apollo-server-env'; import retry from 'async-retry'; -import { plugin } from './extension'; +import { plugin } from './plugin'; import { GraphQLRequestContext, Logger, SchemaHash } from 'apollo-server-types'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { defaultEngineReportingSignature } from 'apollo-graphql'; diff --git a/packages/apollo-engine-reporting/src/federatedExtension.ts b/packages/apollo-engine-reporting/src/federatedExtension.ts deleted file mode 100644 index a811cd04149..00000000000 --- a/packages/apollo-engine-reporting/src/federatedExtension.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { GraphQLResolveInfo, GraphQLError } from 'graphql'; -import { GraphQLExtension } from 'graphql-extensions'; -import { Trace } from 'apollo-engine-reporting-protobuf'; -import { GraphQLRequestContext } from 'apollo-server-types'; - -import { EngineReportingTreeBuilder } from './treeBuilder'; -import { ApolloServerPlugin } from "apollo-server-plugin-base"; -import { EngineReportingOptions } from "./agent"; - -type FederatedReportingOptions = Pick, 'rewriteError'> - -export const plugin = ( - options: FederatedReportingOptions = Object.create(null), -): ApolloServerPlugin => { - return { - requestDidStart(requestContext) { - const treeBuilder: EngineReportingTreeBuilder = - new EngineReportingTreeBuilder({ - rewriteError: options.rewriteError, - }); - - // XXX Provide a mechanism to customize this logic. - const http = requestContext.request.http; - if ( - !http || - !http.headers || - http.headers.get('apollo-federation-include-trace') !== 'ftv1' - ) { - return; - } - - treeBuilder.startTiming(); - - return { - willResolveField(...args) { - const [ , , , info] = args; - return treeBuilder.willResolveField(info); - }, - - didEncounterErrors({ errors }) { - treeBuilder.didEncounterErrors(errors); - }, - - willSendResponse({ response }) { - // We record the end time at the latest possible time: right before serializing the trace. - // If we wait any longer, the time we record won't actually be sent anywhere! - treeBuilder.stopTiming(); - - const encodedUint8Array = Trace.encode(treeBuilder.trace).finish(); - const encodedBuffer = Buffer.from( - encodedUint8Array, - encodedUint8Array.byteOffset, - encodedUint8Array.byteLength, - ); - - const extensions = - response.extensions || (response.extensions = Object.create(null)); - - if (typeof extensions.ftv1 !== "undefined") { - throw new Error("The `ftv1` `extensions` were already present."); - } - - extensions.ftv1 = encodedBuffer.toString('base64'); - } - } - }, - } -}; - -export class EngineFederatedTracingExtension - implements GraphQLExtension { - private enabled = false; - private done = false; - private treeBuilder: EngineReportingTreeBuilder; - - public constructor(options: { - rewriteError?: (err: GraphQLError) => GraphQLError | null; - }) { - this.treeBuilder = new EngineReportingTreeBuilder({ - rewriteError: options.rewriteError, - }); - } - - public requestDidStart(o: { - requestContext: GraphQLRequestContext; - }) { - // XXX Provide a mechanism to customize this logic. - const http = o.requestContext.request.http; - if ( - http && - http.headers.get('apollo-federation-include-trace') === 'ftv1' - ) { - this.enabled = true; - } - - if (this.enabled) { - this.treeBuilder.startTiming(); - } - } - - public willResolveField( - _source: any, - _args: { [argName: string]: any }, - _context: TContext, - info: GraphQLResolveInfo, - ): ((error: Error | null, result: any) => void) | void { - if (this.enabled) { - return this.treeBuilder.willResolveField(info); - } - } - - public didEncounterErrors(errors: GraphQLError[]) { - if (this.enabled) { - this.treeBuilder.didEncounterErrors(errors); - } - } - - // The ftv1 extension is a base64'd Trace protobuf containing only the - // durationNs, startTime, endTime, and root fields. - // - // Note: format() is only called after executing an operation, and - // specifically isn't called for parse or validation errors. Parse and validation - // errors in a federated backend will get reported to the end user as a downstream - // error but will not get reported to Engine (because Engine filters out downstream - // errors)! See #3091. - public format(): [string, string] | undefined { - if (!this.enabled) { - return; - } - if (this.done) { - throw Error('format called twice?'); - } - - // We record the end time at the latest possible time: right before serializing the trace. - // If we wait any longer, the time we record won't actually be sent anywhere! - this.treeBuilder.stopTiming(); - this.done = true; - - const encodedUint8Array = Trace.encode(this.treeBuilder.trace).finish(); - const encodedBuffer = Buffer.from( - encodedUint8Array, - encodedUint8Array.byteOffset, - encodedUint8Array.byteLength, - ); - return ['ftv1', encodedBuffer.toString('base64')]; - } -} diff --git a/packages/apollo-engine-reporting/src/federatedPlugin.ts b/packages/apollo-engine-reporting/src/federatedPlugin.ts new file mode 100644 index 00000000000..5bad85c252b --- /dev/null +++ b/packages/apollo-engine-reporting/src/federatedPlugin.ts @@ -0,0 +1,66 @@ +import { Trace } from 'apollo-engine-reporting-protobuf'; +import { EngineReportingTreeBuilder } from './treeBuilder'; +import { ApolloServerPlugin } from "apollo-server-plugin-base"; +import { EngineReportingOptions } from "./agent"; + +type FederatedReportingOptions = Pick, 'rewriteError'> + +const federatedPlugin = ( + options: FederatedReportingOptions = Object.create(null), +): ApolloServerPlugin => { + return { + requestDidStart(requestContext) { + const treeBuilder: EngineReportingTreeBuilder = + new EngineReportingTreeBuilder({ + rewriteError: options.rewriteError, + }); + + // XXX Provide a mechanism to customize this logic. + const http = requestContext.request.http; + if ( + !http || + !http.headers || + http.headers.get('apollo-federation-include-trace') !== 'ftv1' + ) { + return; + } + + treeBuilder.startTiming(); + + return { + willResolveField(...args) { + const [ , , , info] = args; + return treeBuilder.willResolveField(info); + }, + + didEncounterErrors({ errors }) { + treeBuilder.didEncounterErrors(errors); + }, + + willSendResponse({ response }) { + // We record the end time at the latest possible time: right before serializing the trace. + // If we wait any longer, the time we record won't actually be sent anywhere! + treeBuilder.stopTiming(); + + const encodedUint8Array = Trace.encode(treeBuilder.trace).finish(); + const encodedBuffer = Buffer.from( + encodedUint8Array, + encodedUint8Array.byteOffset, + encodedUint8Array.byteLength, + ); + + const extensions = + response.extensions || (response.extensions = Object.create(null)); + + if (typeof extensions.ftv1 !== "undefined") { + throw new Error("The `ftv1` `extensions` were already present."); + } + + extensions.ftv1 = encodedBuffer.toString('base64'); + } + } + }, + } +}; + +export default federatedPlugin; diff --git a/packages/apollo-engine-reporting/src/index.ts b/packages/apollo-engine-reporting/src/index.ts index 36c7e53a85f..82b73e73ad9 100644 --- a/packages/apollo-engine-reporting/src/index.ts +++ b/packages/apollo-engine-reporting/src/index.ts @@ -1,2 +1,2 @@ export { EngineReportingOptions, EngineReportingAgent } from './agent'; -export { plugin as federatedPlugin } from './federatedExtension'; +export { default as federatedPlugin } from './federatedPlugin'; diff --git a/packages/apollo-engine-reporting/src/extension.ts b/packages/apollo-engine-reporting/src/plugin.ts similarity index 66% rename from packages/apollo-engine-reporting/src/extension.ts rename to packages/apollo-engine-reporting/src/plugin.ts index d3964fd0914..a0179b30ef6 100644 --- a/packages/apollo-engine-reporting/src/extension.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -1,18 +1,10 @@ import { GraphQLRequestContext, - WithRequired, Logger, - SchemaHash, InvalidGraphQLRequestError, } from 'apollo-server-types'; -import { Request, Headers } from 'apollo-server-env'; -import { - GraphQLResolveInfo, - DocumentNode, - ExecutionArgs, - GraphQLError, -} from 'graphql'; -import { GraphQLExtension, EndHandler } from 'graphql-extensions'; +import { Headers } from 'apollo-server-env'; +import { GraphQLError } from 'graphql'; import { Trace } from 'apollo-engine-reporting-protobuf'; import { @@ -35,187 +27,6 @@ const clientNameHeaderKey = 'apollographql-client-name'; const clientReferenceIdHeaderKey = 'apollographql-client-reference-id'; const clientVersionHeaderKey = 'apollographql-client-version'; -// EngineReportingExtension is the per-request GraphQLExtension which creates a -// trace (in protobuf Trace format) for a single request. When the request is -// done, it passes the Trace back to its associated EngineReportingAgent via the -// addTrace callback in its constructor. This class isn't for direct use; its -// constructor is a private API for communicating with EngineReportingAgent. -// Its public methods all implement the GraphQLExtension interface. -export class EngineReportingExtension - implements GraphQLExtension { - private logger: Logger = console; - private treeBuilder: EngineReportingTreeBuilder; - private explicitOperationName?: string | null; - private queryString?: string; - private documentAST?: DocumentNode; - private options: EngineReportingOptions; - private addTrace: (args: AddTraceArgs) => Promise; - private generateClientInfo: GenerateClientInfo; - - public constructor( - options: EngineReportingOptions, - addTrace: (args: AddTraceArgs) => Promise, - private schemaHash: SchemaHash, - ) { - this.options = { - ...options, - }; - if (options.logger) this.logger = options.logger; - this.addTrace = addTrace; - this.generateClientInfo = - options.generateClientInfo || defaultGenerateClientInfo; - - this.treeBuilder = new EngineReportingTreeBuilder({ - rewriteError: options.rewriteError, - logger: this.logger, - }); - } - - public requestDidStart(o: { - request: Request; - queryString?: string; - parsedQuery?: DocumentNode; - variables?: Record; - context: TContext; - extensions?: Record; - requestContext: WithRequired< - GraphQLRequestContext, - 'metrics' | 'queryHash' - >; - }): EndHandler { - this.treeBuilder.startTiming(); - o.requestContext.metrics.startHrTime = this.treeBuilder.startHrTime; - - // Generally, we'll get queryString here and not parsedQuery; we only get - // parsedQuery if you're using an OperationStore. In normal cases we'll get - // our documentAST in the execution callback after it is parsed. - const queryHash = o.requestContext.queryHash; - this.queryString = o.queryString; - this.documentAST = o.parsedQuery; - - this.treeBuilder.trace.http = new Trace.HTTP({ - method: - Trace.HTTP.Method[o.request.method as keyof typeof Trace.HTTP.Method] || - Trace.HTTP.Method.UNKNOWN, - // Host and path are not used anywhere on the backend, so let's not bother - // trying to parse request.url to get them, which is a potential - // source of bugs because integrations have different behavior here. - // On Node's HTTP module, request.url only includes the path - // (see https://nodejs.org/api/http.html#http_message_url) - // The same is true on Lambda (where we pass event.path) - // But on environments like Cloudflare we do get a complete URL. - host: null, - path: null, - }); - - if (this.options.sendHeaders) { - makeHTTPRequestHeaders( - this.treeBuilder.trace.http, - o.request.headers, - this.options.sendHeaders, - ); - - if (o.requestContext.metrics.persistedQueryHit) { - this.treeBuilder.trace.persistedQueryHit = true; - } - if (o.requestContext.metrics.persistedQueryRegister) { - this.treeBuilder.trace.persistedQueryRegister = true; - } - } - - if (o.variables) { - this.treeBuilder.trace.details = makeTraceDetails( - o.variables, - this.options.sendVariableValues, - o.queryString, - ); - } - - const clientInfo = this.generateClientInfo(o.requestContext); - if (clientInfo) { - // While clientAddress could be a part of the protobuf, we'll ignore it for - // now, since the backend does not group by it and Engine frontend will not - // support it in the short term - const { clientName, clientVersion, clientReferenceId } = clientInfo; - // the backend makes the choice of mapping clientName => clientReferenceId if - // no custom reference id is provided - this.treeBuilder.trace.clientVersion = clientVersion || ''; - this.treeBuilder.trace.clientReferenceId = clientReferenceId || ''; - this.treeBuilder.trace.clientName = clientName || ''; - } - - return () => { - this.treeBuilder.stopTiming(); - - this.treeBuilder.trace.fullQueryCacheHit = !!o.requestContext.metrics - .responseCacheHit; - this.treeBuilder.trace.forbiddenOperation = !!o.requestContext.metrics - .forbiddenOperation; - this.treeBuilder.trace.registeredOperation = !!o.requestContext.metrics - .registeredOperation; - - // If the user did not explicitly specify an operation name (which we - // would have saved in `executionDidStart`), but the request pipeline made - // it far enough to figure out what the operation name must be and store - // it on requestContext.operationName, use that name. (Note that this - // depends on the assumption that the RequestContext passed to - // requestDidStart, which does not yet have operationName, will be mutated - // to add operationName later.) - const operationName = - this.explicitOperationName || o.requestContext.operationName || ''; - const documentAST = this.documentAST || o.requestContext.document; - - // If this was a federated operation and we're the gateway, add the query plan - // to the trace. - if (o.requestContext.metrics.queryPlanTrace) { - this.treeBuilder.trace.queryPlan = - o.requestContext.metrics.queryPlanTrace; - } - - this.addTrace({ - operationName, - queryHash, - documentAST, - queryString: this.queryString || '', - trace: this.treeBuilder.trace, - schemaHash: this.schemaHash, - }); - }; - } - - public executionDidStart(o: { executionArgs: ExecutionArgs }) { - // If the operationName is explicitly provided, save it. Note: this is the - // operationName provided by the user. It might be empty if they're relying on - // the "just use the only operation I sent" behavior, even if that operation - // has a name. - // - // It's possible that execution is about to fail because this operation - // isn't actually in the document. We want to know the name in that case - // too, which is why it's important that we save the name now, and not just - // rely on requestContext.operationName (which will be null in this case). - if (o.executionArgs.operationName) { - this.explicitOperationName = o.executionArgs.operationName; - } - this.documentAST = o.executionArgs.document; - } - - public willResolveField( - _source: any, - _args: { [argName: string]: any }, - _context: TContext, - info: GraphQLResolveInfo, - ): ((error: Error | null, result: any) => void) | void { - return this.treeBuilder.willResolveField(info); - // We could save the error into the trace during the end handler, but it - // won't have all the information that graphql-js adds to it later, like - // 'locations'. - } - - public didEncounterErrors(errors: GraphQLError[]) { - this.treeBuilder.didEncounterErrors(errors); - } -} - // This plugin is instantiated once at server start-up. Each request that the // server processes will invoke the `requestDidStart` method which will produce // a trace (in protobuf Trace format) for that single request. When the request diff --git a/packages/apollo-engine-reporting/tsconfig.json b/packages/apollo-engine-reporting/tsconfig.json index 1e8b57f68f2..ea64c712362 100644 --- a/packages/apollo-engine-reporting/tsconfig.json +++ b/packages/apollo-engine-reporting/tsconfig.json @@ -7,7 +7,6 @@ "include": ["src/**/*"], "exclude": ["**/__tests__", "**/__mocks__"], "references": [ - { "path": "../graphql-extensions" }, { "path": "../apollo-server-errors" }, { "path": "../apollo-server-types" }, { "path": "../apollo-server-plugin-base" }, From efbddbfaddf139c5f00467a17673d743584e11b7 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 17 Apr 2020 08:10:41 +0300 Subject: [PATCH 20/98] Update packages/apollo-server-core/src/ApolloServer.ts --- packages/apollo-server-core/src/ApolloServer.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 4ae62ad9889..eaeb1a09f41 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -819,8 +819,12 @@ export class ApolloServerBase { protected async graphQLServerOptions( integrationContextArgument?: Record, ): Promise { - const { schema, schemaHash, documentStore, extensions } - = await this.schemaDerivedData; + const { + schema, + schemaHash, + documentStore, + extensions, + } = await this.schemaDerivedData; let context: Context = this.context ? this.context : {}; From 7ca68407e7eb7ce17ff10bea512f2150fa10aba7 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 27 Apr 2020 11:38:53 +0000 Subject: [PATCH 21/98] Correct typo of "precidence". Ref: https://github.com/apollographql/apollo-server/pull/3988#discussion_r414657251 --- packages/apollo-server-core/src/ApolloServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index e1df311b704..84e6019a908 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -787,7 +787,7 @@ export class ApolloServerBase { // Internal plugins should be added to `pluginsToInit` here. // User's plugins, provided as an argument to this method, will be added - // at the end of that list so they take precidence. + // at the end of that list so they take precedence. // A follow-up commit will actually introduce this. pluginsToInit.push(...plugins); From cd754b0f6c8b1cbf55284439ecc5821b500025b2 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 27 Apr 2020 12:22:21 +0000 Subject: [PATCH 22/98] noop: Remove trailing line. --- packages/apollo-server-core/src/requestPipeline.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 6393d82c95a..f373d38104e 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -642,4 +642,3 @@ export async function processGraphQLRequest( } } } - From 11e885cc1f7e0c007a0af3e877c78e604245761b Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 27 Apr 2020 14:15:36 +0000 Subject: [PATCH 23/98] Relocate schema instrumentation to `utils/schemaInstrumentation.ts`. The `requestPipelineAPI.ts`'s purpose was originally to keep typings by themselves. It was compiled using a separate TypeScript compilation stage to avoid some circular dependencies within the repository itself. However, it still proved to be problematic since it required external packages which depended on the entire `apollo-server-core` just to utilize those types (e.g. plugins!) The work in #2990 offloaded the types to their own package that could be depended on but the assertion in [[1]] correctly notes that introducing new functionality, which is largely incompatible with the original intent of the `requestPipelineAPI` file (even though it is now deprecated) is largely a step backward. Therefore, this moves the functionality to a new file called `schemaInstrumentation`, as suggested in the following comment. [1]: https://github.com/apollographql/apollo-server/pull/3988/files#r414666538 --- .../apollo-server-core/src/requestPipeline.ts | 4 +- .../src/requestPipelineAPI.ts | 153 ------------------ .../src/utils/pluginTestHarness.ts | 2 +- .../src/utils/schemaInstrumentation.ts | 146 +++++++++++++++++ 4 files changed, 149 insertions(+), 156 deletions(-) create mode 100644 packages/apollo-server-core/src/utils/schemaInstrumentation.ts diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index f373d38104e..00ff1c33522 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -17,11 +17,11 @@ import { enableGraphQLExtensions, } from 'graphql-extensions'; import { DataSource } from 'apollo-datasource'; +import { PersistedQueryOptions } from '.'; import { - PersistedQueryOptions, symbolRequestListenerDispatcher, enablePluginsForSchemaResolvers, -} from '.'; +} from "./utils/schemaInstrumentation" import { CacheControlExtension, CacheControlExtensionOptions, diff --git a/packages/apollo-server-core/src/requestPipelineAPI.ts b/packages/apollo-server-core/src/requestPipelineAPI.ts index 734d5277471..47301b3f4fd 100644 --- a/packages/apollo-server-core/src/requestPipelineAPI.ts +++ b/packages/apollo-server-core/src/requestPipelineAPI.ts @@ -1,16 +1,3 @@ -import { - GraphQLField, - getNamedType, - GraphQLObjectType, - GraphQLSchema, - ResponsePath, -} from 'graphql/type'; -import { defaultFieldResolver } from "graphql/execution"; -import { FieldNode } from "graphql/language"; -import { Dispatcher } from "./utils/dispatcher"; -import { GraphQLRequestListener } from "apollo-server-plugin-base"; -import { GraphQLObjectResolver } from "@apollographql/apollo-tools"; - export { GraphQLServiceContext, GraphQLRequest, @@ -23,143 +10,3 @@ export { GraphQLExecutor, GraphQLExecutionResult, } from 'apollo-server-types'; - -export const symbolRequestListenerDispatcher = - Symbol("apolloServerRequestListenerDispatcher"); -export const symbolPluginsEnabled = Symbol("apolloServerPluginsEnabled"); - -export function enablePluginsForSchemaResolvers( - schema: GraphQLSchema & { [symbolPluginsEnabled]?: boolean }, -) { - if (schema[symbolPluginsEnabled]) { - return schema; - } - Object.defineProperty(schema, symbolPluginsEnabled, { - value: true, - }); - - forEachField(schema, wrapField); - - return schema; -} - -function wrapField(field: GraphQLField): void { - const fieldResolver = field.resolve || defaultFieldResolver; - - field.resolve = (source, args, context, info) => { - // This is a bit of a hack, but since `ResponsePath` is a linked list, - // a new object gets created every time a path segment is added. - // So we can use that to share our `whenObjectResolved` promise across - // all field resolvers for the same object. - const parentPath = info.path.prev as ResponsePath & { - __fields?: Record>; - __whenObjectResolved?: Promise; - }; - - // The technique for implementing a "did resolve field" is accomplished by - // returning a function from the `willResolveField` handler. The - // dispatcher will return a callback which will invoke all of those handlers - // and we'll save that to call when the object resolution is complete. - const endHandler = context && context[symbolRequestListenerDispatcher] && - (context[symbolRequestListenerDispatcher] as Dispatcher) - .invokeDidStartHook('willResolveField', source, args, context, info) || - ((_err: Error | null, _result?: any) => { /* do nothing */ }); - - const resolveObject: GraphQLObjectResolver< - any, - any - > = (info.parentType as any).resolveObject; - - let whenObjectResolved: Promise | undefined; - - if (parentPath && resolveObject) { - if (!parentPath.__fields) { - parentPath.__fields = {}; - } - - parentPath.__fields[info.fieldName] = info.fieldNodes; - - whenObjectResolved = parentPath.__whenObjectResolved; - if (!whenObjectResolved) { - // Use `Promise.resolve().then()` to delay executing - // `resolveObject()` so we can collect all the fields first. - whenObjectResolved = Promise.resolve().then(() => { - return resolveObject(source, parentPath.__fields!, context, info); - }); - parentPath.__whenObjectResolved = whenObjectResolved; - } - } - - try { - let result: any; - if (whenObjectResolved) { - result = whenObjectResolved.then((resolvedObject: any) => { - return fieldResolver(resolvedObject, args, context, info); - }); - } else { - result = fieldResolver(source, args, context, info); - } - - // Call the stack's handlers either immediately (if result is not a - // Promise) or once the Promise is done. Then return that same - // maybe-Promise value. - whenResultIsFinished(result, endHandler); - return result; - } catch (error) { - // Normally it's a bad sign to see an error both handled and - // re-thrown. But it is useful to allow extensions to track errors while - // still handling them in the normal GraphQL way. - endHandler(error); - throw error; - } - };; -} - -function isPromise(x: any): boolean { - return x && typeof x.then === 'function'; -} - -// Given result (which may be a Promise or an array some of whose elements are -// promises) Promises, set up 'callback' to be invoked when result is fully -// resolved. -export function whenResultIsFinished( - result: any, - callback: (err: Error | null, result?: any) => void, -) { - if (isPromise(result)) { - result.then((r: any) => callback(null, r), (err: Error) => callback(err)); - } else if (Array.isArray(result)) { - if (result.some(isPromise)) { - Promise.all(result).then( - (r: any) => callback(null, r), - (err: Error) => callback(err), - ); - } else { - callback(null, result); - } - } else { - callback(null, result); - } -} - -function forEachField(schema: GraphQLSchema, fn: FieldIteratorFn): void { - const typeMap = schema.getTypeMap(); - Object.entries(typeMap).forEach(([typeName, type]) => { - - if ( - !getNamedType(type).name.startsWith('__') && - type instanceof GraphQLObjectType - ) { - const fields = type.getFields(); - Object.entries(fields).forEach(([fieldName, field]) => { - fn(field, typeName, fieldName); - }); - } - }); -} - -type FieldIteratorFn = ( - fieldDef: GraphQLField, - typeName: string, - fieldName: string, -) => void; diff --git a/packages/apollo-server-core/src/utils/pluginTestHarness.ts b/packages/apollo-server-core/src/utils/pluginTestHarness.ts index 0afa7dfde96..aed8e007f07 100644 --- a/packages/apollo-server-core/src/utils/pluginTestHarness.ts +++ b/packages/apollo-server-core/src/utils/pluginTestHarness.ts @@ -13,7 +13,7 @@ import { CacheHint } from 'apollo-cache-control'; import { enablePluginsForSchemaResolvers, symbolRequestListenerDispatcher, -} from '../requestPipelineAPI'; +} from './schemaInstrumentation'; import { ApolloServerPlugin } from 'apollo-server-plugin-base'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { Dispatcher } from './dispatcher'; diff --git a/packages/apollo-server-core/src/utils/schemaInstrumentation.ts b/packages/apollo-server-core/src/utils/schemaInstrumentation.ts new file mode 100644 index 00000000000..ba01b452783 --- /dev/null +++ b/packages/apollo-server-core/src/utils/schemaInstrumentation.ts @@ -0,0 +1,146 @@ +import { GraphQLSchema, GraphQLField, ResponsePath, getNamedType, GraphQLObjectType } from "graphql/type"; +import { defaultFieldResolver } from "graphql/execution"; +import { FieldNode } from "graphql/language"; +import { Dispatcher } from "./dispatcher"; +import { GraphQLRequestListener } from "apollo-server-plugin-base"; +import { GraphQLObjectResolver } from "@apollographql/apollo-tools"; + +export const symbolRequestListenerDispatcher = + Symbol("apolloServerRequestListenerDispatcher"); +export const symbolPluginsEnabled = Symbol("apolloServerPluginsEnabled"); + +export function enablePluginsForSchemaResolvers( + schema: GraphQLSchema & { [symbolPluginsEnabled]?: boolean }, +) { + if (schema[symbolPluginsEnabled]) { + return schema; + } + Object.defineProperty(schema, symbolPluginsEnabled, { + value: true, + }); + + forEachField(schema, wrapField); + + return schema; +} + +function wrapField(field: GraphQLField): void { + const fieldResolver = field.resolve || defaultFieldResolver; + + field.resolve = (source, args, context, info) => { + // This is a bit of a hack, but since `ResponsePath` is a linked list, + // a new object gets created every time a path segment is added. + // So we can use that to share our `whenObjectResolved` promise across + // all field resolvers for the same object. + const parentPath = info.path.prev as ResponsePath & { + __fields?: Record>; + __whenObjectResolved?: Promise; + }; + + // The technique for implementing a "did resolve field" is accomplished by + // returning a function from the `willResolveField` handler. The + // dispatcher will return a callback which will invoke all of those handlers + // and we'll save that to call when the object resolution is complete. + const endHandler = context && context[symbolRequestListenerDispatcher] && + (context[symbolRequestListenerDispatcher] as Dispatcher) + .invokeDidStartHook('willResolveField', source, args, context, info) || + ((_err: Error | null, _result?: any) => { /* do nothing */ }); + + const resolveObject: GraphQLObjectResolver< + any, + any + > = (info.parentType as any).resolveObject; + + let whenObjectResolved: Promise | undefined; + + if (parentPath && resolveObject) { + if (!parentPath.__fields) { + parentPath.__fields = {}; + } + + parentPath.__fields[info.fieldName] = info.fieldNodes; + + whenObjectResolved = parentPath.__whenObjectResolved; + if (!whenObjectResolved) { + // Use `Promise.resolve().then()` to delay executing + // `resolveObject()` so we can collect all the fields first. + whenObjectResolved = Promise.resolve().then(() => { + return resolveObject(source, parentPath.__fields!, context, info); + }); + parentPath.__whenObjectResolved = whenObjectResolved; + } + } + + try { + let result: any; + if (whenObjectResolved) { + result = whenObjectResolved.then((resolvedObject: any) => { + return fieldResolver(resolvedObject, args, context, info); + }); + } else { + result = fieldResolver(source, args, context, info); + } + + // Call the stack's handlers either immediately (if result is not a + // Promise) or once the Promise is done. Then return that same + // maybe-Promise value. + whenResultIsFinished(result, endHandler); + return result; + } catch (error) { + // Normally it's a bad sign to see an error both handled and + // re-thrown. But it is useful to allow extensions to track errors while + // still handling them in the normal GraphQL way. + endHandler(error); + throw error; + } + };; +} + +function isPromise(x: any): boolean { + return x && typeof x.then === 'function'; +} + +// Given result (which may be a Promise or an array some of whose elements are +// promises) Promises, set up 'callback' to be invoked when result is fully +// resolved. +export function whenResultIsFinished( + result: any, + callback: (err: Error | null, result?: any) => void, +) { + if (isPromise(result)) { + result.then((r: any) => callback(null, r), (err: Error) => callback(err)); + } else if (Array.isArray(result)) { + if (result.some(isPromise)) { + Promise.all(result).then( + (r: any) => callback(null, r), + (err: Error) => callback(err), + ); + } else { + callback(null, result); + } + } else { + callback(null, result); + } +} + +function forEachField(schema: GraphQLSchema, fn: FieldIteratorFn): void { + const typeMap = schema.getTypeMap(); + Object.entries(typeMap).forEach(([typeName, type]) => { + + if ( + !getNamedType(type).name.startsWith('__') && + type instanceof GraphQLObjectType + ) { + const fields = type.getFields(); + Object.entries(fields).forEach(([fieldName, field]) => { + fn(field, typeName, fieldName); + }); + } + }); +} + +type FieldIteratorFn = ( + fieldDef: GraphQLField, + typeName: string, + fieldName: string, +) => void; From 231817ef2d4a632e5158552b5357195b3d1f308d Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 27 Apr 2020 16:52:09 +0000 Subject: [PATCH 24/98] Import `PersistedQueryOptions` from private, rather than public, API. Ref: https://github.com/apollographql/apollo-server/pull/3988#discussion_r414657906 --- packages/apollo-server-core/src/requestPipeline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 00ff1c33522..89d0d599218 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -17,7 +17,7 @@ import { enableGraphQLExtensions, } from 'graphql-extensions'; import { DataSource } from 'apollo-datasource'; -import { PersistedQueryOptions } from '.'; +import { PersistedQueryOptions } from './graphqlOptions'; import { symbolRequestListenerDispatcher, enablePluginsForSchemaResolvers, From dce4a241435107d463126224d22f403cfd4110e6 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 27 Apr 2020 17:33:00 +0000 Subject: [PATCH 25/98] fix!: Rename AERs `newExtension` to `newPlugin` to match new usage. Ref: #3998 --- packages/apollo-engine-reporting/src/agent.ts | 2 +- packages/apollo-server-core/src/ApolloServer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 71de2c2f08c..6931425e666 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -279,7 +279,7 @@ export class EngineReportingAgent { handleLegacyOptions(this.options); } - public newExtension(): ApolloServerPlugin { + public newPlugin(): ApolloServerPlugin { return plugin(this.options, this.addTrace.bind(this)); } diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 56afdc34a1c..aad31015c9c 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -775,7 +775,7 @@ export class ApolloServerBase { 'to report metrics to Apollo Graph Manager.', ); } - pluginsToInit.push(this.engineReportingAgent!.newExtension()); + pluginsToInit.push(this.engineReportingAgent!.newPlugin()); } else if (engine !== false && federatedSchema) { // 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 From a8ab841e2cdd0ba8030c914329003b8ec30a438b Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 27 Apr 2020 17:41:42 +0000 Subject: [PATCH 26/98] no-op: Add back comment about `ftv1` trace format. This was inadvertently removed in the re-factor. Ref: https://github.com/apollographql/apollo-server/pull/3998/files#r414901517 --- packages/apollo-engine-reporting/src/federatedPlugin.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/apollo-engine-reporting/src/federatedPlugin.ts b/packages/apollo-engine-reporting/src/federatedPlugin.ts index 5bad85c252b..81d168f9b18 100644 --- a/packages/apollo-engine-reporting/src/federatedPlugin.ts +++ b/packages/apollo-engine-reporting/src/federatedPlugin.ts @@ -5,6 +5,8 @@ import { EngineReportingOptions } from "./agent"; type FederatedReportingOptions = Pick, 'rewriteError'> +// This ftv1 plugin produces a base64'd Trace protobuf containing only the +// durationNs, startTime, endTime, and root fields. const federatedPlugin = ( options: FederatedReportingOptions = Object.create(null), ): ApolloServerPlugin => { From fc05e8d6a32973cbca6561eb140d240040117e66 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 28 Apr 2020 11:34:34 +0000 Subject: [PATCH 27/98] Use optional chaining when accessing optional `request.http.headers`. ...and destructuring. --- packages/apollo-engine-reporting/src/federatedPlugin.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/apollo-engine-reporting/src/federatedPlugin.ts b/packages/apollo-engine-reporting/src/federatedPlugin.ts index 81d168f9b18..bf1b04627f6 100644 --- a/packages/apollo-engine-reporting/src/federatedPlugin.ts +++ b/packages/apollo-engine-reporting/src/federatedPlugin.ts @@ -11,19 +11,14 @@ const federatedPlugin = ( options: FederatedReportingOptions = Object.create(null), ): ApolloServerPlugin => { return { - requestDidStart(requestContext) { + requestDidStart({ request: { http } }) { const treeBuilder: EngineReportingTreeBuilder = new EngineReportingTreeBuilder({ rewriteError: options.rewriteError, }); // XXX Provide a mechanism to customize this logic. - const http = requestContext.request.http; - if ( - !http || - !http.headers || - http.headers.get('apollo-federation-include-trace') !== 'ftv1' - ) { + if (http?.headers.get('apollo-federation-include-trace') !== 'ftv1') { return; } From 48eab7efa0893182452680ba383df218da862273 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 28 Apr 2020 12:27:11 +0000 Subject: [PATCH 28/98] Ensure `metrics` is present before plugin initialization. This eliminates the need to guard for the presence of `metrics` on the `requestContext` within plugins who are calling `requestDidStart`. Ref: https://github.com/apollographql/apollo-server/pull/3998#discussion_r414906840 --- packages/apollo-server-core/src/ApolloServer.ts | 1 + .../apollo-server-core/src/requestPipeline.ts | 15 ++++++++++----- packages/apollo-server-types/src/index.ts | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index aad31015c9c..5f020df75e0 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -869,6 +869,7 @@ export class ApolloServerBase { request, context: options.context || Object.create(null), cache: options.cache!, + metrics: {}, response: { http: { headers: new Headers(), diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 6393d82c95a..d7958763c98 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -126,6 +126,16 @@ export async function processGraphQLRequest( // all of our own machinery will certainly set it now. const logger = requestContext.logger || console; + // If request context's `metrics` already exists, preserve it, but _ensure_ it + // exists there and shorthand it for use throughout this function. As of this + // comment, the sole known case where `metrics` already exists is when the + // `captureTraces` property is present and set to the result of the boolean + // `reporting` option on the legacy (V1) server options, here: + // https://git.io/Jfmsb. I suspect this disappears when this is the direct + // entry into request processing, rather than through, e.g. `runHttpQuery`. + const metrics = requestContext.metrics = + requestContext.metrics || Object.create(null); + let cacheControlExtension: CacheControlExtension | undefined; const extensionStack = initializeExtensionStack(); (requestContext.context as any)._extensionStack = extensionStack; @@ -137,11 +147,6 @@ export async function processGraphQLRequest( await initializeDataSources(); - const metrics = requestContext.metrics || Object.create(null); - if (!requestContext.metrics) { - requestContext.metrics = metrics; - } - const request = requestContext.request; let { query, extensions } = request; diff --git a/packages/apollo-server-types/src/index.ts b/packages/apollo-server-types/src/index.ts index fcf42426b1d..e9c7cdd5445 100644 --- a/packages/apollo-server-types/src/index.ts +++ b/packages/apollo-server-types/src/index.ts @@ -104,7 +104,7 @@ export interface GraphQLRequestContext> { */ readonly errors?: ReadonlyArray; - readonly metrics?: GraphQLRequestMetrics; + readonly metrics: GraphQLRequestMetrics; debug?: boolean; } From 98bae446d37704939bd7e0bf010503c66215dfd8 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 28 Apr 2020 15:53:57 +0300 Subject: [PATCH 29/98] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a32e400dd26..361931c8bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The version headers in this history reflect the versions of Apollo Server itself > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the the appropriate changes within that release will be moved into the new section. - `apollo-server-lambda`: Support file uploads on AWS Lambda [Issue #1419](https://github.com/apollographql/apollo-server/issues/1419) [Issue #1703](https://github.com/apollographql/apollo-server/issues/1703) [PR #3926](https://github.com/apollographql/apollo-server/pull/3926) -- `apollo-tracing`: This package's internal integration with Apollo Server has been switched from using the soon-to-be-deprecated`graphql-extensions` API to using [the request pipeline plugin API](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). Behavior should remain otherwise the same. [PR #3991](https://github.com/apollographql/apollo-server/pull/3991) +- `apollo-tracing`: This package is considered deprecated and — along with its `tracing: Boolean` configuration option on the `ApolloServer` constructor options — will cease to exist in Apollo Server 3.x. Until that occurs, we've updated the _internal_ integration of this package with Apollo Server itself to use the newer [request pipeline plugin API](https://www.apollographql.com/docs/apollo-server/integrations/plugins/), rather than the _also_ soon-to-be-deprecated-`graphql-extensions` API it previously leveraged. The behavior should remain otherwise the same. [PR #3991](https://github.com/apollographql/apollo-server/pull/3991) - **breaking** `apollo-engine-reporting-protobuf`: Drop legacy fields that were never used by `apollo-engine-reporting`. Added new fields `StatsContext` to allow `apollo-server` to send summary stats instead of full traces, and renamed `FullTracesReport` to `Report` and `Traces` to `TracesAndStats` since reports now can include stats as well as traces. ### v2.12.0 From 8ca5d112b38796c28a1e2d7211af169136c8bf5b Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 28 Apr 2020 12:33:53 +0000 Subject: [PATCH 30/98] Remove guard around `metrics` which is unnecessary after 48eab7efa. --- packages/apollo-engine-reporting/src/plugin.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index a0179b30ef6..661e0c03e2b 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -21,8 +21,6 @@ import { PersistedQueryNotSupportedError, } from 'apollo-server-errors'; -type Mutable = { -readonly [P in keyof T]: T[P] }; - const clientNameHeaderKey = 'apollographql-client-name'; const clientReferenceIdHeaderKey = 'apollographql-client-reference-id'; const clientVersionHeaderKey = 'apollographql-client-version'; @@ -52,11 +50,9 @@ export const plugin = ( logger: requestContext.logger || logger, }); - const metrics: NonNullable = - ((requestContext as Mutable) - .metrics = requestContext.metrics || Object.create(null)); - treeBuilder.startTiming(); + + const metrics = requestContext.metrics; metrics.startHrTime = treeBuilder.startHrTime; if (requestContext.request.http) { From 0658df5e50283c3985ff3e3717bdff8de2adab9d Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 28 Apr 2020 13:21:58 +0000 Subject: [PATCH 31/98] Move `Trace.HTTP` init inside of `ensurePreflight`. As noted in review within [[1]], there wasn't really a compelling reason for this to be kept separately from the other tree-building bits which existed within `ensurePreflight`. [1]: https://github.com/apollographql/apollo-server/pull/3998#discussion_r414911267 --- .../apollo-engine-reporting/src/plugin.ts | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 661e0c03e2b..dd42f28b434 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -55,32 +55,30 @@ export const plugin = ( const metrics = requestContext.metrics; metrics.startHrTime = treeBuilder.startHrTime; - if (requestContext.request.http) { - treeBuilder.trace.http = new Trace.HTTP({ - method: - Trace.HTTP.Method[ - requestContext.request.http - .method as keyof typeof Trace.HTTP.Method - ] || Trace.HTTP.Method.UNKNOWN, - // Host and path are not used anywhere on the backend, so let's not bother - // trying to parse request.url to get them, which is a potential - // source of bugs because integrations have different behavior here. - // On Node's HTTP module, request.url only includes the path - // (see https://nodejs.org/api/http.html#http_message_url) - // The same is true on Lambda (where we pass event.path) - // But on environments like Cloudflare we do get a complete URL. - host: null, - path: null, - }); - } - let preflightDone: boolean = false; function ensurePreflight() { if (preflightDone) return; preflightDone = true; - if (options.sendHeaders) { - if (requestContext.request.http && treeBuilder.trace.http) { + if (requestContext.request.http) { + treeBuilder.trace.http = new Trace.HTTP({ + method: + Trace.HTTP.Method[ + requestContext.request.http + .method as keyof typeof Trace.HTTP.Method + ] || Trace.HTTP.Method.UNKNOWN, + // Host and path are not used anywhere on the backend, so let's not bother + // trying to parse request.url to get them, which is a potential + // source of bugs because integrations have different behavior here. + // On Node's HTTP module, request.url only includes the path + // (see https://nodejs.org/api/http.html#http_message_url) + // The same is true on Lambda (where we pass event.path) + // But on environments like Cloudflare we do get a complete URL. + host: null, + path: null, + }); + + if (options.sendHeaders) { makeHTTPRequestHeaders( treeBuilder.trace.http, requestContext.request.http.headers, From fc966a4b5d383d7a0d893839e4b05d9c5b87afaa Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 28 Apr 2020 13:34:10 +0000 Subject: [PATCH 32/98] Remove unnecessary `queryString` assignment and related comment. --- packages/apollo-engine-reporting/src/plugin.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index dd42f28b434..4416d422567 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -43,7 +43,6 @@ export const plugin = ( return { requestDidStart(requestContext) { - let queryString: string | undefined; const treeBuilder: EngineReportingTreeBuilder = new EngineReportingTreeBuilder({ rewriteError: options.rewriteError, @@ -94,16 +93,11 @@ export const plugin = ( treeBuilder.trace.persistedQueryRegister = true; } - // Generally, we'll get queryString here and not parsedQuery; we only get - // parsedQuery if you're using an OperationStore. In normal cases we'll - // get our documentAST in the execution callback after it is parsed. - queryString = requestContext.source; - if (requestContext.request.variables) { treeBuilder.trace.details = makeTraceDetails( requestContext.request.variables, options.sendVariableValues, - queryString, + requestContext.source, ); } @@ -150,7 +144,7 @@ export const plugin = ( operationName, queryHash: requestContext.queryHash!, documentAST: requestContext.document, - queryString, + queryString: requestContext.source, trace: treeBuilder.trace, schemaHash: requestContext.schemaHash, }); From 766c7384c8296a5a552973ede24f013fb355dab5 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 28 Apr 2020 13:47:47 +0000 Subject: [PATCH 33/98] Destructure some `requestContext` properties for brevity. --- .../apollo-engine-reporting/src/plugin.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 4416d422567..289114eece4 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -43,15 +43,23 @@ export const plugin = ( return { requestDidStart(requestContext) { + // We still need the entire `requestContext` to pass through to the + // `generateClientInfo` method, but we'll destructure for brevity within. + const { + metrics, + logger: requestLogger, + schemaHash, + request: { http, variables }, + } = requestContext; + const treeBuilder: EngineReportingTreeBuilder = new EngineReportingTreeBuilder({ rewriteError: options.rewriteError, - logger: requestContext.logger || logger, + logger: requestLogger || logger, }); treeBuilder.startTiming(); - const metrics = requestContext.metrics; metrics.startHrTime = treeBuilder.startHrTime; let preflightDone: boolean = false; @@ -59,13 +67,11 @@ export const plugin = ( if (preflightDone) return; preflightDone = true; - if (requestContext.request.http) { + if (http) { treeBuilder.trace.http = new Trace.HTTP({ method: - Trace.HTTP.Method[ - requestContext.request.http - .method as keyof typeof Trace.HTTP.Method - ] || Trace.HTTP.Method.UNKNOWN, + Trace.HTTP.Method[http.method as keyof typeof Trace.HTTP.Method] + || Trace.HTTP.Method.UNKNOWN, // Host and path are not used anywhere on the backend, so let's not bother // trying to parse request.url to get them, which is a potential // source of bugs because integrations have different behavior here. @@ -80,7 +86,7 @@ export const plugin = ( if (options.sendHeaders) { makeHTTPRequestHeaders( treeBuilder.trace.http, - requestContext.request.http.headers, + http.headers, options.sendHeaders, ); } @@ -93,9 +99,9 @@ export const plugin = ( treeBuilder.trace.persistedQueryRegister = true; } - if (requestContext.request.variables) { + if (variables) { treeBuilder.trace.details = makeTraceDetails( - requestContext.request.variables, + variables, options.sendVariableValues, requestContext.source, ); @@ -146,7 +152,7 @@ export const plugin = ( documentAST: requestContext.document, queryString: requestContext.source, trace: treeBuilder.trace, - schemaHash: requestContext.schemaHash, + schemaHash, }); } From f4aad306d1737dc411635c9538aaef992776ccf0 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 28 Apr 2020 13:52:10 +0000 Subject: [PATCH 34/98] chore!: Use `document` / `source` rather than `documentAST` / `queryString`. The use of `document` and `source` is the standard within the plugin API and the request pipeline, so these names should be more natural going forward. This wouldn't have been possible without a breaking change, but we're already doing that. --- packages/apollo-engine-reporting/src/agent.ts | 30 +++++++++---------- .../apollo-engine-reporting/src/plugin.ts | 4 +-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 6931425e666..0bde648580f 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -204,8 +204,8 @@ export interface AddTraceArgs { operationName: string; queryHash: string; schemaHash: SchemaHash; - queryString?: string; - documentAST?: DocumentNode; + source?: string; + document?: DocumentNode; } const serviceHeaderDefaults = { @@ -286,9 +286,9 @@ export class EngineReportingAgent { public async addTrace({ trace, queryHash, - documentAST, + document, operationName, - queryString, + source, schemaHash, }: AddTraceArgs): Promise { // Ignore traces that come in after stop(). @@ -320,8 +320,8 @@ export class EngineReportingAgent { const signature = await this.getTraceSignature({ queryHash, - documentAST, - queryString, + document, + source, operationName, }); @@ -486,17 +486,17 @@ export class EngineReportingAgent { private async getTraceSignature({ queryHash, operationName, - documentAST, - queryString, + document, + source, }: { queryHash: string; operationName: string; - documentAST?: DocumentNode; - queryString?: string; + document?: DocumentNode; + source?: string; }): Promise { - if (!documentAST && !queryString) { + if (!document && !source) { // This shouldn't happen: one of those options must be passed to runQuery. - throw new Error('No queryString or parsedQuery?'); + throw new Error('No document or source?'); } const cacheKey = signatureCacheKey(queryHash, operationName); @@ -511,7 +511,7 @@ export class EngineReportingAgent { return cachedSignature; } - if (!documentAST) { + if (!document) { // We didn't get an AST, possibly because of a parse failure. Let's just // use the full query string. // @@ -519,12 +519,12 @@ export class EngineReportingAgent { // hides literals, you might end up sending literals for queries // that fail parsing or validation. Provide some way to mask them // anyway? - return queryString as string; + return source as string; } const generatedSignature = ( this.options.calculateSignature || defaultEngineReportingSignature - )(documentAST, operationName); + )(document, operationName); // Intentionally not awaited so the cache can be written to at leisure. this.signatureCache.set(cacheKey, generatedSignature); diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 289114eece4..cc950c558eb 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -149,8 +149,8 @@ export const plugin = ( addTrace({ operationName, queryHash: requestContext.queryHash!, - documentAST: requestContext.document, - queryString: requestContext.source, + document: requestContext.document, + source: requestContext.source, trace: treeBuilder.trace, schemaHash, }); From 6b0c83cf3a787d9b3d035a0a171e86cbf5442d9c Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 28 Apr 2020 17:52:33 +0000 Subject: [PATCH 35/98] Expand on comment about the `ftv1` extension. Per @glasser's https://github.com/apollographql/apollo-server/commit/a8ab841e#r38802002 --- packages/apollo-engine-reporting/src/federatedPlugin.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/apollo-engine-reporting/src/federatedPlugin.ts b/packages/apollo-engine-reporting/src/federatedPlugin.ts index bf1b04627f6..e69e6cc92e4 100644 --- a/packages/apollo-engine-reporting/src/federatedPlugin.ts +++ b/packages/apollo-engine-reporting/src/federatedPlugin.ts @@ -6,7 +6,10 @@ import { EngineReportingOptions } from "./agent"; type FederatedReportingOptions = Pick, 'rewriteError'> // This ftv1 plugin produces a base64'd Trace protobuf containing only the -// durationNs, startTime, endTime, and root fields. +// durationNs, startTime, endTime, and root fields. This output is placed +// on the `extensions`.`ftv1` property of the response. The Apollo Gateway +// utilizes this data to construct the full trace and submit it to Apollo +// Graph Manager ingress. const federatedPlugin = ( options: FederatedReportingOptions = Object.create(null), ): ApolloServerPlugin => { From ad9fefa43bc6488fdeb853440631ccb317d366ef Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 6 May 2020 12:36:45 +0000 Subject: [PATCH 36/98] noop: Fix unrelated typing error in `runQuery.test.ts`. --- packages/apollo-server-core/src/__tests__/runQuery.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index d4f4640ca2c..636a0c78198 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -39,6 +39,7 @@ function runQuery(options: QueryOptions): Promise { return processGraphQLRequest(options, { request, + logger: console, context: options.context || {}, debug: options.debug, cache: {} as any, From 6575625838510446aeb7deaa39540343d715f453 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 4 May 2020 12:19:47 +0000 Subject: [PATCH 37/98] Reintroduce genericism to `Dispatcher`. As I reached to introduce a dispatcher for an interface that wasn't a `GraphQLRequestListener`, it dawned on me that the dispatcher that we already had was pretty close to being correct for this prior to the changes in the linked PR/commits below ([[Ref 1]][[Ref 2]]). Within those changesets, the addition of `GrahpQLRequestListener` as a constraint to the `Dispatcher`'s `T` type was done as a matter of necessity so TypeScript could be updated to the latest version. Further analysis, with a motivation to boot, led me to believe that TypeScript was correct in its newfound determination (circa ~v3.7.3) that the `apply` method didn't exist on the keys of the interface being invoked. Concretely, I'm pretty sure there was no constraint that was actually ensuring they were functions and some degree of indirection caused TypeScript to give up trying to reconcile that. This brings back a generic property to the `Dispatcher` by using an object with `string`-key'd properties and functions as values as the constraint for the entire `Dispatcher` interface, rather than `GraphqLRequestListener` itself. Ref 1: 1169db60d61d22eeffc76287ece567774b930e3e Ref 2: https://github.com/apollographql/apollo-server/pull/3618 --- .../apollo-server-core/src/utils/dispatcher.ts | 5 ++--- packages/apollo-server-plugin-base/src/index.ts | 4 +++- packages/apollo-server-types/src/index.ts | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/apollo-server-core/src/utils/dispatcher.ts b/packages/apollo-server-core/src/utils/dispatcher.ts index d311eaf055a..0febb020d07 100644 --- a/packages/apollo-server-core/src/utils/dispatcher.ts +++ b/packages/apollo-server-core/src/utils/dispatcher.ts @@ -1,13 +1,12 @@ -import { GraphQLRequestListener } from "apollo-server-plugin-base"; +import { AnyFunction, AnyFunctionMap } from "apollo-server-types"; -type AnyFunction = (...args: any[]) => any; type Args = F extends (...args: infer A) => any ? A : never; type AsFunction = F extends AnyFunction ? F : never; type UnwrapPromise = T extends Promise ? U : T; type DidEndHook = (...args: TArgs) => void; -export class Dispatcher { +export class Dispatcher { constructor(protected targets: T[]) {} public async invokeHookAsync( diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts index 2f671030974..ffab808f0af 100644 --- a/packages/apollo-server-plugin-base/src/index.ts +++ b/packages/apollo-server-plugin-base/src/index.ts @@ -1,4 +1,5 @@ import { + AnyFunctionMap, GraphQLServiceContext, GraphQLRequestContext, GraphQLRequest, @@ -55,7 +56,8 @@ export type GraphQLRequestListenerExecutionDidEnd = export type GraphQLRequestListenerDidResolveField = ((error: Error | null, result?: any) => void) | void -export interface GraphQLRequestListener> { +export interface GraphQLRequestListener> + extends AnyFunctionMap { parsingDidStart?( requestContext: GraphQLRequestContextParsingDidStart, ): GraphQLRequestListenerParsingDidEnd; diff --git a/packages/apollo-server-types/src/index.ts b/packages/apollo-server-types/src/index.ts index 5602aeea762..196b4b7db55 100644 --- a/packages/apollo-server-types/src/index.ts +++ b/packages/apollo-server-types/src/index.ts @@ -16,6 +16,23 @@ import { Trace } from 'apollo-engine-reporting-protobuf'; export type ValueOrPromise = T | Promise; export type WithRequired = T & Required>; +/** + * It is not recommended to use this `AnyFunction` type further. + * + * This is a legacy type which aims to do what its name suggests (be the type + * for _any_ function) but it should be replaced with something from the + * TypeScript standard lib. It doesn't truly cover "any" function right now, + * and in particular doesn't consider `this`. For now, it has been brought + * here from the Apollo Server `Dispatcher`, where it was first utilized. + */ +export type AnyFunction = (...args: any[]) => any; + +/** + * A map of `AnyFunction`s which are the interface for our plugin API's + * request listeners. (e.g. `GraphQLRequestListener`s). + */ +export type AnyFunctionMap = { [key: string]: AnyFunction | undefined }; + type Mutable = { -readonly [P in keyof T]: T[P] }; // By default, TypeScript uses structural typing (as opposed to nominal typing) From d5408ac772613c5a59456aa870f3033db7bd305a Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 6 May 2020 14:24:44 +0000 Subject: [PATCH 38/98] Correct recently added plugin types to exclude `void`. I recently introduced these in 46211b371718beae696a781aa27c204966cb51e9 within #3985, with the intention of making these more understandable in auto-completion within VSCode. While the effective types that the methods where these types were used remains the same (since I've just shifted the `void` from the wrong place to the right place), removing `void` from these types is arguably a breaking change. Since that's just in the typings (and would be a compilation error, I think), and this was recently introduced recently, I'm going to adjust this for correctness now. --- .../apollo-server-plugin-base/src/index.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts index ffab808f0af..9fa7edd1ac0 100644 --- a/packages/apollo-server-plugin-base/src/index.ts +++ b/packages/apollo-server-plugin-base/src/index.ts @@ -47,23 +47,21 @@ export interface ApolloServerPlugin = Recor ): GraphQLRequestListener | void; } -export type GraphQLRequestListenerParsingDidEnd = - ((err?: Error) => void) | void; +export type GraphQLRequestListenerParsingDidEnd = (err?: Error) => void; export type GraphQLRequestListenerValidationDidEnd = - ((err?: ReadonlyArray) => void) | void; -export type GraphQLRequestListenerExecutionDidEnd = - ((err?: Error) => void) | void; + ((err?: ReadonlyArray) => void); +export type GraphQLRequestListenerExecutionDidEnd = ((err?: Error) => void); export type GraphQLRequestListenerDidResolveField = - ((error: Error | null, result?: any) => void) | void + ((error: Error | null, result?: any) => void); export interface GraphQLRequestListener> extends AnyFunctionMap { parsingDidStart?( requestContext: GraphQLRequestContextParsingDidStart, - ): GraphQLRequestListenerParsingDidEnd; + ): GraphQLRequestListenerParsingDidEnd | void; validationDidStart?( requestContext: GraphQLRequestContextValidationDidStart, - ): GraphQLRequestListenerValidationDidEnd; + ): GraphQLRequestListenerValidationDidEnd | void; didResolveOperation?( requestContext: GraphQLRequestContextDidResolveOperation, ): ValueOrPromise; @@ -80,10 +78,10 @@ export interface GraphQLRequestListener> ): ValueOrPromise; executionDidStart?( requestContext: GraphQLRequestContextExecutionDidStart, - ): GraphQLRequestListenerExecutionDidEnd; + ): GraphQLRequestListenerExecutionDidEnd | void; willResolveField?( ...fieldResolverArgs: Parameters> - ): GraphQLRequestListenerDidResolveField; + ): GraphQLRequestListenerDidResolveField | void; willSendResponse?( requestContext: GraphQLRequestContextWillSendResponse, ): ValueOrPromise; From ab78e4839b0c30ae6eebbdcce75275de78ad4019 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 6 May 2020 14:28:29 +0000 Subject: [PATCH 39/98] Introduce `BaseContext` and `DefaultContext`. We use this notation in so many places to represent the context and the default context that we should stop referring to it as `Record`. This doesn't go so far as to track all of those down, but this at least makes the types exist so we can pursuit that as some point. Luckily, with structural typing, the use of this or an in-line type elsewhere should resolve to a match. --- packages/apollo-server-plugin-base/src/index.ts | 13 ++++++++++--- packages/apollo-server-types/src/index.ts | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts index 9fa7edd1ac0..853edf34070 100644 --- a/packages/apollo-server-plugin-base/src/index.ts +++ b/packages/apollo-server-plugin-base/src/index.ts @@ -1,5 +1,7 @@ import { AnyFunctionMap, + BaseContext, + DefaultContext, GraphQLServiceContext, GraphQLRequestContext, GraphQLRequest, @@ -25,6 +27,8 @@ import { GraphQLFieldResolver } from "graphql"; // In the future, `apollo-server-types` and `apollo-server-plugin-base` will // probably roll into the same "types" package, but that is not today! export { + BaseContext, + DefaultContext, GraphQLServiceContext, GraphQLRequestContext, GraphQLRequest, @@ -40,7 +44,9 @@ export { GraphQLRequestContextWillSendResponse, }; -export interface ApolloServerPlugin = Record> { +export interface ApolloServerPlugin< + TContext extends BaseContext = DefaultContext +> { serverWillStart?(service: GraphQLServiceContext): ValueOrPromise; requestDidStart?( requestContext: GraphQLRequestContext, @@ -54,8 +60,9 @@ export type GraphQLRequestListenerExecutionDidEnd = ((err?: Error) => void); export type GraphQLRequestListenerDidResolveField = ((error: Error | null, result?: any) => void); -export interface GraphQLRequestListener> - extends AnyFunctionMap { +export interface GraphQLRequestListener< + TContext extends BaseContext = DefaultContext +> extends AnyFunctionMap { parsingDidStart?( requestContext: GraphQLRequestContextParsingDidStart, ): GraphQLRequestListenerParsingDidEnd | void; diff --git a/packages/apollo-server-types/src/index.ts b/packages/apollo-server-types/src/index.ts index 196b4b7db55..6e0cf487461 100644 --- a/packages/apollo-server-types/src/index.ts +++ b/packages/apollo-server-types/src/index.ts @@ -13,6 +13,9 @@ import { import { KeyValueCache } from 'apollo-server-caching'; import { Trace } from 'apollo-engine-reporting-protobuf'; +export type BaseContext = Record; +export type DefaultContext = BaseContext; + export type ValueOrPromise = T | Promise; export type WithRequired = T & Required>; From 9b5be32091faeb3d97c75517ecd70ab337d951b1 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 6 May 2020 14:33:00 +0000 Subject: [PATCH 40/98] tests: Add additional plugin API hook tests. These should have accompanied the initial PR which I opened to introduce the `willResolveField` and "did resolve field" hooks to the new plugin API, but alas, I didn't do that. Better late than never, and these will be refactored (in a somewhat minor way) in an upcoming commit. --- .../src/__tests__/runQuery.test.ts | 217 +++++++++++++++++- 1 file changed, 216 insertions(+), 1 deletion(-) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index 636a0c78198..9fe5905beb2 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -19,7 +19,11 @@ import { import { processGraphQLRequest, GraphQLRequest } from '../requestPipeline'; import { Request } from 'apollo-server-env'; import { GraphQLOptions, Context as GraphQLContext } from 'apollo-server-core'; -import { ApolloServerPlugin } from 'apollo-server-plugin-base'; +import { + ApolloServerPlugin, + GraphQLRequestListenerDidResolveField, + GraphQLRequestListenerExecutionDidEnd, +} from 'apollo-server-plugin-base'; import { GraphQLRequestListener } from 'apollo-server-plugin-base'; import { InMemoryLRUCache } from 'apollo-server-caching'; @@ -513,6 +517,144 @@ describe('runQuery', () => { }); }); + describe('executionDidStart', () => { + it('called when execution starts', async () => { + const executionDidStart = jest.fn(); + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart, + }; + }, + }, + ], + request: new MockReq(), + }); + + expect(executionDidStart).toHaveBeenCalledTimes(1); + }); + + it('works as a function returned from "executionDidStart"', async () => { + const executionDidEnd = jest.fn(); + const executionDidStart = jest.fn( + (): GraphQLRequestListenerExecutionDidEnd => executionDidEnd); + + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart, + }; + }, + }, + ], + request: new MockReq(), + }); + + expect(executionDidStart).toHaveBeenCalledTimes(1); + expect(executionDidEnd).toHaveBeenCalledTimes(1); + }); + }); + + describe('willResolveField', () => { + it('called when resolving a field starts', async () => { + const willResolveField = jest.fn(); + + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + willResolveField, + }; + }, + }, + ], + request: new MockReq(), + }); + + expect(willResolveField).toHaveBeenCalledTimes(1); + }); + + it('called once for each field being resolved', async () => { + const willResolveField = jest.fn(); + + await runQuery({ + schema, + queryString: '{ testString again:testString }', + plugins: [ + { + requestDidStart() { + return { + willResolveField, + }; + }, + }, + ], + request: new MockReq(), + }); + + expect(willResolveField).toHaveBeenCalledTimes(2); + }); + + it('calls the end handler', async () => { + const didResolveField: GraphQLRequestListenerDidResolveField = + jest.fn(); + const willResolveField = jest.fn(() => didResolveField); + + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + willResolveField, + }; + }, + }, + ], + request: new MockReq(), + }); + + expect(willResolveField).toHaveBeenCalledTimes(1); + expect(didResolveField).toHaveBeenCalledTimes(1); + }); + + it('calls the end handler for each field being resolved', async () => { + const didResolveField: GraphQLRequestListenerDidResolveField = + jest.fn(); + const willResolveField = jest.fn(() => didResolveField); + + await runQuery({ + schema, + queryString: '{ testString again: testString }', + plugins: [ + { + requestDidStart() { + return { + willResolveField, + }; + }, + }, + ], + request: new MockReq(), + }); + + expect(willResolveField).toHaveBeenCalledTimes(2); + expect(didResolveField).toHaveBeenCalledTimes(2); + }); + }); + describe('didEncounterErrors', () => { const didEncounterErrors = jest.fn(); const plugins: ApolloServerPlugin[] = [ @@ -570,6 +712,79 @@ describe('runQuery', () => { expect(didEncounterErrors).not.toBeCalled(); }); }); + + describe("ordering", () => { + it('calls hooks in the expected order', async () => { + const callOrder: string[] = []; + let stopAwaiting: Function; + const toBeAwaited = new Promise(resolve => stopAwaiting = resolve); + + const didResolveField: GraphQLRequestListenerDidResolveField = + jest.fn(() => callOrder.push("didResolveField")); + + const willResolveField = jest.fn(() => { + callOrder.push("willResolveField"); + return didResolveField; + }); + + const executionDidEnd: GraphQLRequestListenerExecutionDidEnd = + jest.fn(() => callOrder.push('executionDidEnd')); + + const executionDidStart = jest.fn(() => { + callOrder.push("executionDidStart"); + return executionDidEnd; + }, + ); + + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'QueryType', + fields: { + testString: { + type: GraphQLString, + async resolve() { + callOrder.push("beforeAwaiting"); + await toBeAwaited; + callOrder.push("afterAwaiting"); + return "it works"; + }, + }, + } + }) + }); + + Promise.resolve().then(() => stopAwaiting()); + + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart, + willResolveField, + }; + }, + }, + ], + request: new MockReq(), + }); + + expect(executionDidStart).toHaveBeenCalledTimes(1); + expect(executionDidEnd).toHaveBeenCalledTimes(1); + expect(willResolveField).toHaveBeenCalledTimes(1); + expect(didResolveField).toHaveBeenCalledTimes(1); + expect(callOrder).toStrictEqual([ + "executionDidStart", + "willResolveField", + "beforeAwaiting", + "afterAwaiting", + "didResolveField", + "executionDidEnd", + ]); + }); + }) }); describe('parsing and validation cache', () => { From 4fa6ddd4f8d1b8f64c636e563368a955bc5fbe5f Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 6 May 2020 17:04:27 +0000 Subject: [PATCH 41/98] Introduce a `callTargets` method on the `Dispatcher`. This decomposition of the functionality that was previously embedded within the `invokeHookAsync` method will facilitate the addition of two new methods in a follow-up commit: `invokeHookSync` and `reverseInvokeHookSync`. --- .../apollo-server-core/src/utils/dispatcher.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/apollo-server-core/src/utils/dispatcher.ts b/packages/apollo-server-core/src/utils/dispatcher.ts index 0febb020d07..ee5226b1b6e 100644 --- a/packages/apollo-server-core/src/utils/dispatcher.ts +++ b/packages/apollo-server-core/src/utils/dispatcher.ts @@ -9,10 +9,26 @@ type DidEndHook = (...args: TArgs) => void; export class Dispatcher { constructor(protected targets: T[]) {} + private callTargets( + targets: T[], + methodName: TMethodName, + ...args: Args + ) { + return targets.map(target => { + const method = target[methodName]; + if (method && typeof method === 'function') { + return method.apply(target, args); + } + }); + } + public async invokeHookAsync( methodName: TMethodName, ...args: Args ): Promise>>[]> { + return await Promise.all( + this.callTargets(this.targets, methodName, ...args), + ); return await Promise.all( this.targets.map(target => { const method = target[methodName]; From f2a7490fff88d37c6356078b45b8b7ca10a3404e Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 6 May 2020 17:08:16 +0000 Subject: [PATCH 42/98] Switch `willResolveField` to be nested within `executionDidStart`. The original approach I took when introducing the `willResolveField` field-level wrapper to the new plugin API was to put it at the same level as other request life-cycle events, like `validationDidStart`, and `parsingDidStart`. However, that approach didn't pay respect to the structured/tree-shape that the new request pipeline aimed to bring. For example, currently, the more granular life-cycle events that take place within the _request_ (like `parsingDidStart` and `executionDidStart`), are defined by returning them from the `requestDidStart` hook. This creates a tiered structure within a plugin's implemented hooks that allow variables to be scoped appropriately and shared within a phase of the life-cycle (even sub-hooks) without needing to pin things onto the context unnecessarily. Additionally, in TypeScript, it allows us to gradually widen/narrow the scope of the types that are applied to the `requestContext` during various stages. E.g., `operationName` isn't available before the parsing phase and therefore the type of the `requestContext` within the `parsingDidStart` should, ideally, reflect that. This commit makes this real and, I think, embraces the previous spirit of the new plugin API. Further, while the initial approach I took switched from an object attached to the `context` (received by resolvers) to a `Symbol`, this commit further simplifies some of the machinery we aree exposing. Specifically, rather than attaching the entire dispatcher to the `context` (which we use from an onion wrapper that we put the schema's resolvers within) we now only attach a method which will invoke the dispatcher's `willResolveField` hook, rather than the entirety of the dispatcher's instance itself. This should hopefully reduce the category of "odd behavior" which could result if users tampered with that symbol, since it could result in unexpected behavior. --- .../src/__tests__/runQuery.test.ts | 265 +++++++++++------- .../apollo-server-core/src/requestPipeline.ts | 49 +++- .../src/utils/dispatcher.ts | 14 + .../src/utils/pluginTestHarness.ts | 61 ++-- .../src/utils/schemaInstrumentation.ts | 37 ++- .../apollo-server-plugin-base/src/index.ts | 17 +- 6 files changed, 300 insertions(+), 143 deletions(-) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index 9fe5905beb2..9953b120714 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -21,6 +21,7 @@ import { Request } from 'apollo-server-env'; import { GraphQLOptions, Context as GraphQLContext } from 'apollo-server-core'; import { ApolloServerPlugin, + GraphQLRequestExecutionListener, GraphQLRequestListenerDidResolveField, GraphQLRequestListenerExecutionDidEnd, } from 'apollo-server-plugin-base'; @@ -538,123 +539,192 @@ describe('runQuery', () => { expect(executionDidStart).toHaveBeenCalledTimes(1); }); - it('works as a function returned from "executionDidStart"', async () => { - const executionDidEnd = jest.fn(); - const executionDidStart = jest.fn( - (): GraphQLRequestListenerExecutionDidEnd => executionDidEnd); + describe('executionDidEnd', () => { + it('works as a function returned from "executionDidStart"', async () => { + const executionDidEnd = jest.fn(); + const executionDidStart = jest.fn( + (): GraphQLRequestListenerExecutionDidEnd => executionDidEnd); - await runQuery({ - schema, - queryString: '{ testString }', - plugins: [ - { - requestDidStart() { - return { - executionDidStart, - }; + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart, + }; + }, }, - }, - ], - request: new MockReq(), + ], + request: new MockReq(), + }); + + expect(executionDidStart).toHaveBeenCalledTimes(1); + expect(executionDidEnd).toHaveBeenCalledTimes(1); }); + it('works as a listener on an object returned from "executionDidStart"', + async () => { + const executionDidEnd = jest.fn(); + const executionDidStart = jest.fn( + (): GraphQLRequestExecutionListener => ({ + executionDidEnd, + }), + ); + + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart, + }; + }, + }, + ], + request: new MockReq(), + } + ); + expect(executionDidStart).toHaveBeenCalledTimes(1); expect(executionDidEnd).toHaveBeenCalledTimes(1); }); - }); - describe('willResolveField', () => { - it('called when resolving a field starts', async () => { - const willResolveField = jest.fn(); - - await runQuery({ - schema, - queryString: '{ testString }', - plugins: [ - { - requestDidStart() { - return { - willResolveField, - }; - }, - }, - ], - request: new MockReq(), - }); - - expect(willResolveField).toHaveBeenCalledTimes(1); }); - it('called once for each field being resolved', async () => { - const willResolveField = jest.fn(); - - await runQuery({ - schema, - queryString: '{ testString again:testString }', - plugins: [ - { - requestDidStart() { - return { - willResolveField, - }; + describe('willResolveField', () => { + it('called when resolving a field starts', async () => { + const willResolveField = jest.fn(); + const executionDidEnd = jest.fn(); + const executionDidStart = jest.fn( + (): GraphQLRequestExecutionListener => ({ + willResolveField, + executionDidEnd, + }), + ); + + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart, + }; + }, }, - }, - ], - request: new MockReq(), - }); - - expect(willResolveField).toHaveBeenCalledTimes(2); - }); + ], + request: new MockReq(), + }); - it('calls the end handler', async () => { - const didResolveField: GraphQLRequestListenerDidResolveField = - jest.fn(); - const willResolveField = jest.fn(() => didResolveField); + expect(executionDidStart).toHaveBeenCalledTimes(1); + expect(willResolveField).toHaveBeenCalledTimes(1); + expect(executionDidEnd).toHaveBeenCalledTimes(1); + }); - await runQuery({ - schema, - queryString: '{ testString }', - plugins: [ - { - requestDidStart() { - return { - willResolveField, - }; + it('called once for each field being resolved', async () => { + const willResolveField = jest.fn(); + const executionDidEnd = jest.fn(); + const executionDidStart = jest.fn( + (): GraphQLRequestExecutionListener => ({ + willResolveField, + executionDidEnd, + }), + ); + + await runQuery({ + schema, + queryString: '{ testString again:testString }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart, + }; + }, }, - }, - ], - request: new MockReq(), + ], + request: new MockReq(), + }); + + expect(executionDidStart).toHaveBeenCalledTimes(1); + expect(willResolveField).toHaveBeenCalledTimes(2); + expect(executionDidEnd).toHaveBeenCalledTimes(1); }); - expect(willResolveField).toHaveBeenCalledTimes(1); - expect(didResolveField).toHaveBeenCalledTimes(1); - }); + it('calls the end handler', async () => { + const didResolveField: GraphQLRequestListenerDidResolveField = + jest.fn(); + const willResolveField = jest.fn(() => didResolveField); + const executionDidEnd = jest.fn(); + const executionDidStart = jest.fn( + (): GraphQLRequestExecutionListener => ({ + willResolveField, + executionDidEnd, + }), + ); + + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart, + }; + }, + }, + ], + request: new MockReq(), + }); - it('calls the end handler for each field being resolved', async () => { - const didResolveField: GraphQLRequestListenerDidResolveField = - jest.fn(); - const willResolveField = jest.fn(() => didResolveField); + expect(executionDidStart).toHaveBeenCalledTimes(1); + expect(willResolveField).toHaveBeenCalledTimes(1); + expect(didResolveField).toHaveBeenCalledTimes(1); + expect(executionDidEnd).toHaveBeenCalledTimes(1); + }); - await runQuery({ - schema, - queryString: '{ testString again: testString }', - plugins: [ - { - requestDidStart() { - return { - willResolveField, - }; + it('calls the end handler for each field being resolved', async () => { + const didResolveField: GraphQLRequestListenerDidResolveField = + jest.fn(); + const willResolveField = jest.fn(() => didResolveField); + const executionDidEnd = jest.fn(); + const executionDidStart = jest.fn( + (): GraphQLRequestExecutionListener => ({ + willResolveField, + executionDidEnd, + }), + ); + + await runQuery({ + schema, + queryString: '{ testString again: testString }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart, + }; + }, }, - }, - ], - request: new MockReq(), - }); + ], + request: new MockReq(), + }); - expect(willResolveField).toHaveBeenCalledTimes(2); - expect(didResolveField).toHaveBeenCalledTimes(2); + expect(executionDidStart).toHaveBeenCalledTimes(1); + expect(willResolveField).toHaveBeenCalledTimes(2); + expect(didResolveField).toHaveBeenCalledTimes(2); + expect(executionDidEnd).toHaveBeenCalledTimes(1); + }); }); }); + describe('didEncounterErrors', () => { const didEncounterErrors = jest.fn(); const plugins: ApolloServerPlugin[] = [ @@ -730,9 +800,10 @@ describe('runQuery', () => { const executionDidEnd: GraphQLRequestListenerExecutionDidEnd = jest.fn(() => callOrder.push('executionDidEnd')); - const executionDidStart = jest.fn(() => { + const executionDidStart = jest.fn( + (): GraphQLRequestExecutionListener => { callOrder.push("executionDidStart"); - return executionDidEnd; + return { willResolveField, executionDidEnd }; }, ); @@ -763,7 +834,6 @@ describe('runQuery', () => { requestDidStart() { return { executionDidStart, - willResolveField, }; }, }, @@ -772,7 +842,6 @@ describe('runQuery', () => { }); expect(executionDidStart).toHaveBeenCalledTimes(1); - expect(executionDidEnd).toHaveBeenCalledTimes(1); expect(willResolveField).toHaveBeenCalledTimes(1); expect(didResolveField).toHaveBeenCalledTimes(1); expect(callOrder).toStrictEqual([ diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 89d0d599218..79bc9bf0fa2 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -19,7 +19,7 @@ import { import { DataSource } from 'apollo-datasource'; import { PersistedQueryOptions } from './graphqlOptions'; import { - symbolRequestListenerDispatcher, + symbolExecutionDispatcherWillResolveField, enablePluginsForSchemaResolvers, } from "./utils/schemaInstrumentation" import { @@ -56,6 +56,7 @@ import { GraphQLRequestContextValidationDidStart, GraphQLRequestContextWillSendResponse, GraphQLRequestContextDidEncounterErrors, + GraphQLRequestExecutionListener, } from 'apollo-server-plugin-base'; import { Dispatcher } from './utils/dispatcher'; @@ -131,10 +132,6 @@ export async function processGraphQLRequest( (requestContext.context as any)._extensionStack = extensionStack; const dispatcher = initializeRequestListenerDispatcher(); - Object.defineProperty(requestContext.context, symbolRequestListenerDispatcher, { - value: dispatcher, - }); - await initializeDataSources(); const metrics = requestContext.metrics || Object.create(null); @@ -364,11 +361,43 @@ export async function processGraphQLRequest( requestContext as GraphQLRequestContextResponseForOperation, ); if (response == null) { - const executionDidEnd = await dispatcher.invokeDidStartHook( + // This execution dispatcher code is duplicated in `pluginTestHarness` + // right now. + + const executionListeners: GraphQLRequestExecutionListener[] = []; + dispatcher.invokeHookSync( 'executionDidStart', requestContext as GraphQLRequestContextExecutionDidStart, + ).forEach(executionListener => { + if (typeof executionListener === 'function') { + executionListeners.push({ + executionDidEnd: executionListener, + }); + } else if (typeof executionListener === 'object') { + executionListeners.push(executionListener); + } + }); + + const executionDispatcher = new Dispatcher(executionListeners); + + // Create a callback that will trigger the execution dispatcher's + // `willResolveField` hook. We will attach this to the context on a + // symbol so it can be invoked by our `wrapField` method during execution. + const invokeWillResolveField: GraphQLRequestExecutionListener< + TContext + >['willResolveField'] = (...args) => + executionDispatcher.invokeDidStartHook('willResolveField', ...args); + + Object.defineProperty( + requestContext.context, + symbolExecutionDispatcherWillResolveField, + { value: invokeWillResolveField } ); + // If the schema is already enabled, this is a no-op. Otherwise, the + // schema will be augmented so it is able to invoke willResolveField. + enablePluginsForSchemaResolvers(config.schema); + try { const result = await execute( requestContext as GraphQLRequestContextExecutionDidStart, @@ -383,9 +412,9 @@ export async function processGraphQLRequest( errors: result.errors ? formatErrors(result.errors) : undefined, }; - executionDidEnd(); + executionDispatcher.reverseInvokeHookSync("executionDidEnd"); } catch (executionError) { - executionDidEnd(executionError); + executionDispatcher.reverseInvokeHookSync("executionDidEnd", executionError); return await sendErrorResponse(executionError); } } @@ -576,10 +605,8 @@ export async function processGraphQLRequest( } function initializeRequestListenerDispatcher(): Dispatcher< - GraphQLRequestListener + GraphQLRequestListener > { - enablePluginsForSchemaResolvers(config.schema); - const requestListeners: GraphQLRequestListener[] = []; if (config.plugins) { for (const plugin of config.plugins) { diff --git a/packages/apollo-server-core/src/utils/dispatcher.ts b/packages/apollo-server-core/src/utils/dispatcher.ts index ee5226b1b6e..02789591abf 100644 --- a/packages/apollo-server-core/src/utils/dispatcher.ts +++ b/packages/apollo-server-core/src/utils/dispatcher.ts @@ -39,6 +39,20 @@ export class Dispatcher { ); } + public invokeHookSync( + methodName: TMethodName, + ...args: Args + ): ReturnType>[] { + return this.callTargets(this.targets, methodName, ...args); + } + + public reverseInvokeHookSync( + methodName: TMethodName, + ...args: Args + ): ReturnType>[] { + return this.callTargets(this.targets.reverse(), methodName, ...args); + } + public async invokeHooksUntilNonNull( methodName: TMethodName, ...args: Args diff --git a/packages/apollo-server-core/src/utils/pluginTestHarness.ts b/packages/apollo-server-core/src/utils/pluginTestHarness.ts index aed8e007f07..d94c3c5d642 100644 --- a/packages/apollo-server-core/src/utils/pluginTestHarness.ts +++ b/packages/apollo-server-core/src/utils/pluginTestHarness.ts @@ -12,9 +12,12 @@ import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql/type'; import { CacheHint } from 'apollo-cache-control'; import { enablePluginsForSchemaResolvers, - symbolRequestListenerDispatcher, + symbolExecutionDispatcherWillResolveField, } from './schemaInstrumentation'; -import { ApolloServerPlugin } from 'apollo-server-plugin-base'; +import { + ApolloServerPlugin, + GraphQLRequestExecutionListener, +} from 'apollo-server-plugin-base'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { Dispatcher } from './dispatcher'; @@ -94,8 +97,6 @@ export default async function pluginTestHarness({ }); } - enablePluginsForSchemaResolvers(schema); - const requestContext: GraphQLRequestContext = { logger: logger || console, request: graphqlRequest, @@ -117,30 +118,56 @@ export default async function pluginTestHarness({ throw new Error("Should be impossible to not have a listener."); } - if (typeof listener.willResolveField !== 'function') { - throw new Error("Should be impossible to not have 'willResolveField'."); + const dispatcher = new Dispatcher([listener]); + + const executionListeners: GraphQLRequestExecutionListener[] = []; + + if (typeof listener.executionDidStart === 'function') { + // This execution dispatcher logic is duplicated in the request pipeline + // right now. + dispatcher.invokeHookSync( + 'executionDidStart', + requestContext as GraphQLRequestContextExecutionDidStart, + ).forEach(executionListener => { + if (typeof executionListener === 'function') { + executionListeners.push({ + executionDidEnd: executionListener, + }); + } else if (typeof executionListener === 'object') { + executionListeners.push(executionListener); + } + }); } - const dispatcher = new Dispatcher([listener]); + const executionDispatcher = new Dispatcher(executionListeners); - // Put the dispatcher on the context so `willResolveField` can access it. - Object.defineProperty(requestContext.context, symbolRequestListenerDispatcher, { - value: dispatcher, - }); + // Create a callback that will trigger the execution dispatcher's + // `willResolveField` hook. We will attach this to the context on a + // symbol so it can be invoked by our `wrapField` method during execution. + const invokeWillResolveField: GraphQLRequestExecutionListener< + TContext + >['willResolveField'] = (...args) => + executionDispatcher.invokeDidStartHook('willResolveField', ...args); - const executionDidEnd = dispatcher.invokeDidStartHook( - "executionDidStart", - requestContext as IPluginTestHarnessExecutionDidStart, + Object.defineProperty( + requestContext.context, + symbolExecutionDispatcherWillResolveField, + { value: invokeWillResolveField } ); + // If the schema is already enabled, this is a no-op. Otherwise, the + // schema will be augmented so it is able to invoke willResolveField. + enablePluginsForSchemaResolvers(schema); + try { // `response` is readonly, so we'll cast to `any` to assign to it. (requestContext.response as any) = await executor( requestContext as IPluginTestHarnessExecutionDidStart, ); - executionDidEnd(); - } catch (executionError) { - executionDidEnd(executionError); + executionDispatcher.reverseInvokeHookSync("executionDidEnd"); + + } catch (executionErr) { + executionDispatcher.reverseInvokeHookSync("executionDidEnd", executionErr); } await dispatcher.invokeHookAsync( diff --git a/packages/apollo-server-core/src/utils/schemaInstrumentation.ts b/packages/apollo-server-core/src/utils/schemaInstrumentation.ts index ba01b452783..41d3f72dd3c 100644 --- a/packages/apollo-server-core/src/utils/schemaInstrumentation.ts +++ b/packages/apollo-server-core/src/utils/schemaInstrumentation.ts @@ -1,12 +1,11 @@ import { GraphQLSchema, GraphQLField, ResponsePath, getNamedType, GraphQLObjectType } from "graphql/type"; import { defaultFieldResolver } from "graphql/execution"; import { FieldNode } from "graphql/language"; -import { Dispatcher } from "./dispatcher"; -import { GraphQLRequestListener } from "apollo-server-plugin-base"; +import { GraphQLRequestExecutionListener } from "apollo-server-plugin-base"; import { GraphQLObjectResolver } from "@apollographql/apollo-tools"; -export const symbolRequestListenerDispatcher = - Symbol("apolloServerRequestListenerDispatcher"); +export const symbolExecutionDispatcherWillResolveField = + Symbol("apolloServerExecutionDispatcherWillResolveField"); export const symbolPluginsEnabled = Symbol("apolloServerPluginsEnabled"); export function enablePluginsForSchemaResolvers( @@ -37,14 +36,22 @@ function wrapField(field: GraphQLField): void { __whenObjectResolved?: Promise; }; + const willResolveField = + context && + context[symbolExecutionDispatcherWillResolveField] && + (context[symbolExecutionDispatcherWillResolveField] as + | GraphQLRequestExecutionListener['willResolveField'] + | undefined); + // The technique for implementing a "did resolve field" is accomplished by - // returning a function from the `willResolveField` handler. The - // dispatcher will return a callback which will invoke all of those handlers - // and we'll save that to call when the object resolution is complete. - const endHandler = context && context[symbolRequestListenerDispatcher] && - (context[symbolRequestListenerDispatcher] as Dispatcher) - .invokeDidStartHook('willResolveField', source, args, context, info) || - ((_err: Error | null, _result?: any) => { /* do nothing */ }); + // returning a function from the `willResolveField` handler. While there + // may be several callbacks, depending on the number of plugins which have + // implemented a `willResolveField` hook, this hook will call them all + // as dictated by the dispatcher. We will call this when object + // resolution is complete. + const didResolveField = + typeof willResolveField === 'function' && + willResolveField(source, args, context, info); const resolveObject: GraphQLObjectResolver< any, @@ -84,13 +91,17 @@ function wrapField(field: GraphQLField): void { // Call the stack's handlers either immediately (if result is not a // Promise) or once the Promise is done. Then return that same // maybe-Promise value. - whenResultIsFinished(result, endHandler); + if (typeof didResolveField === "function") { + whenResultIsFinished(result, didResolveField); + } return result; } catch (error) { // Normally it's a bad sign to see an error both handled and // re-thrown. But it is useful to allow extensions to track errors while // still handling them in the normal GraphQL way. - endHandler(error); + if (typeof didResolveField === "function") { + didResolveField(error); + } throw error; } };; diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts index 853edf34070..a90786dfd51 100644 --- a/packages/apollo-server-plugin-base/src/index.ts +++ b/packages/apollo-server-plugin-base/src/index.ts @@ -85,11 +85,20 @@ export interface GraphQLRequestListener< ): ValueOrPromise; executionDidStart?( requestContext: GraphQLRequestContextExecutionDidStart, - ): GraphQLRequestListenerExecutionDidEnd | void; - willResolveField?( - ...fieldResolverArgs: Parameters> - ): GraphQLRequestListenerDidResolveField | void; + ): + | GraphQLRequestExecutionListener + | GraphQLRequestListenerExecutionDidEnd + | void; willSendResponse?( requestContext: GraphQLRequestContextWillSendResponse, ): ValueOrPromise; } + +export interface GraphQLRequestExecutionListener< + TContext extends BaseContext = DefaultContext +> extends AnyFunctionMap { + executionDidEnd?: GraphQLRequestListenerExecutionDidEnd; + willResolveField?( + ...fieldResolverArgs: Parameters> + ): GraphQLRequestListenerDidResolveField | void; +} From 60cd32d0cc35b02fcdf57805dd450f4f07cfb55f Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 7 May 2020 07:59:34 +0000 Subject: [PATCH 43/98] tests: Introduce a test which demonstrates `fieldResolver` behavior. This commit relates to a review comment here: https://github.com/apollographql/apollo-server/pull/3988#discussion_r414676152 This test is meant to help reason about the concern brought up during PR review. Specifically, the concern was about losing functionality of passing a custom `fieldResolver` to `processGraphQLRequest`. This test aims to show that passing a `fieldResolver` to `runQuery` (which calls into `processGraphQLRequest` _nearly_ directly) and eventually gets passed directly into `graphql`'s `execute` method (in a non-federated context) will still work as expected after it's been wrapped by `wrapField` (in our schema instrumentation mechanisms) when a `willResolveField` plugin hook is defined. --- .../src/__tests__/runQuery.test.ts | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index 9953b120714..91c5b8ef46a 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -721,6 +721,81 @@ describe('runQuery', () => { expect(didResolveField).toHaveBeenCalledTimes(2); expect(executionDidEnd).toHaveBeenCalledTimes(1); }); + + it('uses the custom "fieldResolver" when defined', async () => { + const schemaWithResolver = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'QueryType', + fields: { + testString: { + type: GraphQLString, + resolve() { + return "using schema-defined resolver"; + }, + }, + } + }) + }); + + const schemaWithoutResolver = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'QueryType', + fields: { + testString: { + type: GraphQLString, + }, + } + }) + }); + + const differentFieldResolver = () => "I'm diffrnt, ya, I'm diffrnt."; + + const queryString = `{ testString } `; + + const didResolveField: GraphQLRequestListenerDidResolveField = + jest.fn(); + const willResolveField = jest.fn(() => didResolveField); + + const plugins: ApolloServerPlugin[] = [ + { + requestDidStart: () => ({ + executionDidStart: () => ({ + willResolveField, + }), + }) + }, + ]; + + const resultFromSchemaWithResolver = await runQuery({ + schema: schemaWithResolver, + queryString, + plugins, + request: new MockReq(), + fieldResolver: differentFieldResolver, + }); + + expect(willResolveField).toHaveBeenCalledTimes(1); + expect(didResolveField).toHaveBeenCalledTimes(1); + + expect(resultFromSchemaWithResolver.data).toEqual({ + testString: "using schema-defined resolver" + }); + + const resultFromSchemaWithoutResolver = await runQuery({ + schema: schemaWithoutResolver, + queryString, + plugins, + request: new MockReq(), + fieldResolver: differentFieldResolver, + }); + + expect(willResolveField).toHaveBeenCalledTimes(2); + expect(didResolveField).toHaveBeenCalledTimes(2); + + expect(resultFromSchemaWithoutResolver.data).toEqual({ + testString: "I'm diffrnt, ya, I'm diffrnt." + }); + }); }); }); From ef4ed58cb5e14bb638e4580760673de347a88de0 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 7 May 2020 09:06:38 +0000 Subject: [PATCH 44/98] Revert "Update CHANGELOG.md" This reverts the public messaging about `tracing` deprecation in commit 98bae446d37704939bd7e0bf010503c66215dfd8, for now, until we can discuss its fate a bit more, and how we intend on making up for that. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 361931c8bef..a32e400dd26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The version headers in this history reflect the versions of Apollo Server itself > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the the appropriate changes within that release will be moved into the new section. - `apollo-server-lambda`: Support file uploads on AWS Lambda [Issue #1419](https://github.com/apollographql/apollo-server/issues/1419) [Issue #1703](https://github.com/apollographql/apollo-server/issues/1703) [PR #3926](https://github.com/apollographql/apollo-server/pull/3926) -- `apollo-tracing`: This package is considered deprecated and — along with its `tracing: Boolean` configuration option on the `ApolloServer` constructor options — will cease to exist in Apollo Server 3.x. Until that occurs, we've updated the _internal_ integration of this package with Apollo Server itself to use the newer [request pipeline plugin API](https://www.apollographql.com/docs/apollo-server/integrations/plugins/), rather than the _also_ soon-to-be-deprecated-`graphql-extensions` API it previously leveraged. The behavior should remain otherwise the same. [PR #3991](https://github.com/apollographql/apollo-server/pull/3991) +- `apollo-tracing`: This package's internal integration with Apollo Server has been switched from using the soon-to-be-deprecated`graphql-extensions` API to using [the request pipeline plugin API](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). Behavior should remain otherwise the same. [PR #3991](https://github.com/apollographql/apollo-server/pull/3991) - **breaking** `apollo-engine-reporting-protobuf`: Drop legacy fields that were never used by `apollo-engine-reporting`. Added new fields `StatsContext` to allow `apollo-server` to send summary stats instead of full traces, and renamed `FullTracesReport` to `Report` and `Traces` to `TracesAndStats` since reports now can include stats as well as traces. ### v2.12.0 From 283e82f307774b68c560786c07cf25c717c69299 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 7 May 2020 09:59:35 +0000 Subject: [PATCH 45/98] Tweak err. message when `ftv1` is already present in `extensions` response. Ref: https://github.com/apollographql/apollo-server/pull/3998/files#r414902801 --- packages/apollo-engine-reporting/src/federatedPlugin.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/apollo-engine-reporting/src/federatedPlugin.ts b/packages/apollo-engine-reporting/src/federatedPlugin.ts index e69e6cc92e4..f02ce267fca 100644 --- a/packages/apollo-engine-reporting/src/federatedPlugin.ts +++ b/packages/apollo-engine-reporting/src/federatedPlugin.ts @@ -52,8 +52,10 @@ const federatedPlugin = ( const extensions = response.extensions || (response.extensions = Object.create(null)); + // This should only happen if another plugin is using the same name- + // space within the `extensions` object and got to it before us. if (typeof extensions.ftv1 !== "undefined") { - throw new Error("The `ftv1` `extensions` were already present."); + throw new Error("The `ftv1` extension was already present."); } extensions.ftv1 = encodedBuffer.toString('base64'); From e0949ec20b0e6eee1dc0f24e4d2e370e40012137 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 7 May 2020 10:10:17 +0000 Subject: [PATCH 46/98] Remove `TODO` comment I suggested I'd remove! Ref: https://github.com/apollographql/apollo-server/pull/3998/files#r409729299 --- packages/apollo-server-core/src/ApolloServer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index d0f935ff36a..3baa43237f7 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -757,7 +757,6 @@ export class ApolloServerBase { // User's plugins, provided as an argument to this method, will be added // at the end of that list so they take precedence. // A follow-up commit will actually introduce this. - // Also, TODO, remove this comment. const federatedSchema = this.schema && this.schemaIsFederated(this.schema); const { engine } = this.config; From 39a59c5487f5340702f0e36a90f6340cd222cf6d Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 7 May 2020 11:21:38 +0000 Subject: [PATCH 47/98] types: Fix uncaught error in `runQuery.test` after #3996. --- packages/apollo-server-core/src/__tests__/runQuery.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index 91c5b8ef46a..aa5b98282ce 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -27,6 +27,7 @@ import { } from 'apollo-server-plugin-base'; import { GraphQLRequestListener } from 'apollo-server-plugin-base'; import { InMemoryLRUCache } from 'apollo-server-caching'; +import { generateSchemaHash } from "../utils/schemaHash"; // This is a temporary kludge to ensure we preserve runQuery behavior with the // GraphQLRequestProcessor refactoring. @@ -42,8 +43,13 @@ function runQuery(options: QueryOptions): Promise { http: options.request, }; + const schemaHash = generateSchemaHash(schema); + return processGraphQLRequest(options, { request, + schema: options.schema, + schemaHash, + metrics: {}, logger: console, context: options.context || {}, debug: options.debug, From efa7131f5787976e809f058114728aeff74b4bfe Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 7 May 2020 11:23:06 +0000 Subject: [PATCH 48/98] tests: Ensure `schema` and `schemaHash` are present on `requestContext`. --- .../src/__tests__/runQuery.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index aa5b98282ce..4b0c257a3b4 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -481,6 +481,24 @@ describe('runQuery', () => { await runOnce(); expect(requestDidStart.mock.calls.length).toBe(2); }); + + it('is called with the schema and schemaHash', async () => { + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart, + }, + ], + request: new MockReq(), + }); + + const invocation = requestDidStart.mock.calls[0][0]; + expect(invocation).toHaveProperty('schema', schema); + expect(invocation).toHaveProperty( /* Shorter as a RegExp */ + 'schemaHash', expect.stringMatching(/^8ff87f3e0/)); + }); }); describe('parsingDidStart', () => { From e813e5b73a2bb14a93af9f91fd2ede6684c1ecae Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 7 May 2020 12:07:04 +0000 Subject: [PATCH 49/98] refactor(tests): Better helpers for APQ tests in intgr. testsuite. --- .../src/index.ts | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/apollo-server-integration-testsuite/src/index.ts b/packages/apollo-server-integration-testsuite/src/index.ts index a5dd026108f..a2618f328b7 100644 --- a/packages/apollo-server-integration-testsuite/src/index.ts +++ b/packages/apollo-server-integration-testsuite/src/index.ts @@ -18,7 +18,12 @@ import { import request from 'supertest'; -import { GraphQLOptions, Config } from 'apollo-server-core'; +import { + GraphQLOptions, + Config, + PersistedQueryOptions, + KeyValueCache, +} from 'apollo-server-core'; import gql from 'graphql-tag'; import { ValueOrPromise } from 'apollo-server-types'; import { GraphQLRequestListener } from "apollo-server-plugin-base"; @@ -1221,12 +1226,7 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }, }; - let didEncounterErrors: jest.Mock< - ReturnType, - Parameters - >; - - function createMockCache() { + function createMockCache(): KeyValueCache { const map = new Map(); return { set: jest.fn(async (key, val) => { @@ -1237,10 +1237,13 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }; } - beforeEach(async () => { - didEncounterErrors = jest.fn(); - const cache = createMockCache(); - app = await createApp({ + let didEncounterErrors: jest.Mock< + ReturnType, + Parameters + >; + + function createApqApp(apqOptions: PersistedQueryOptions = {}) { + return createApp({ graphqlOptions: { schema, plugins: [ @@ -1252,22 +1255,21 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { ], persistedQueries: { cache, + ...apqOptions, }, }, }); + } + + let cache: KeyValueCache; + + beforeEach(async () => { + cache = createMockCache(); + didEncounterErrors = jest.fn(); }); it('when ttlSeconds is set, passes ttl to the apq cache set call', async () => { - const cache = createMockCache(); - app = await createApp({ - graphqlOptions: { - schema, - persistedQueries: { - cache: cache, - ttl: 900, - }, - }, - }); + app = await createApqApp({ ttl: 900 }); await request(app) .post('/graphql') @@ -1287,15 +1289,7 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { it('when ttlSeconds is unset, ttl is not passed to apq cache', async () => { - const cache = createMockCache(); - app = await createApp({ - graphqlOptions: { - schema, - persistedQueries: { - cache: cache, - }, - }, - }); + app = await createApqApp(); await request(app) .post('/graphql') @@ -1315,6 +1309,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { ); it('errors when version is not specified', async () => { + app = await createApqApp(); + const result = await request(app) .get('/graphql') .query({ @@ -1346,6 +1342,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('errors when version is unsupported', async () => { + app = await createApqApp(); + const result = await request(app) .get('/graphql') .query({ @@ -1378,6 +1376,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('errors when hash is mismatched', async () => { + app = await createApqApp(); + const result = await request(app) .get('/graphql') .query({ @@ -1410,6 +1410,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('returns PersistedQueryNotFound on the first try', async () => { + app = await createApqApp(); + const result = await request(app) .post('/graphql') .send({ @@ -1432,6 +1434,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { ); }); it('returns result on the second try', async () => { + app = await createApqApp(); + await request(app) .post('/graphql') .send({ @@ -1465,6 +1469,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('returns with batched persisted queries', async () => { + app = await createApqApp(); + const errors = await request(app) .post('/graphql') .send([ @@ -1510,6 +1516,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('returns result on the persisted query', async () => { + app = await createApqApp(); + await request(app) .post('/graphql') .send({ @@ -1532,6 +1540,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('returns error when hash does not match', async () => { + app = await createApqApp(); + const response = await request(app) .post('/graphql') .send({ @@ -1549,6 +1559,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('returns correct result using get request', async () => { + app = await createApqApp(); + await request(app) .post('/graphql') .send({ From d91358911429b0db4866ae0b43878becb5e49134 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 7 May 2020 12:26:51 +0000 Subject: [PATCH 50/98] wip didResolveSource --- docs/source/integrations/plugins.md | 17 ++++++++++ .../src/__tests__/runQuery.test.ts | 29 ++++++++++++++++ .../apollo-server-core/src/requestPipeline.ts | 11 ++++++ .../src/index.ts | 34 ++++++++++++++++++- .../apollo-server-plugin-base/src/index.ts | 5 +++ packages/apollo-server-types/src/index.ts | 4 ++- 6 files changed, 98 insertions(+), 2 deletions(-) diff --git a/docs/source/integrations/plugins.md b/docs/source/integrations/plugins.md index df4003d8544..2c9c2600381 100644 --- a/docs/source/integrations/plugins.md +++ b/docs/source/integrations/plugins.md @@ -313,6 +313,23 @@ should not return a value. > If you're using TypeScript to create your plugin, implement the [ `GraphQLRequestListener` interface](https://github.com/apollographql/apollo-server/blob/master/packages/apollo-server-plugin-base/src/index.ts) from the `apollo-server-plugin-base` module to define functions for request lifecycle events. +### `didResolveSource` + +The `didResolveSource` event is invoked after Apollo Server has determined the +`String`-representation of the incoming operation that it will act upon. In the +event that this `String` was not directly passed in from the client, this +may be retrieved from a cache store (e.g., Automated Persisted Queries). + +At this stage, there is not a guarantee that the operation is not malformed. + +```typescript +didResolveSource?( + requestContext: WithRequired< + GraphQLRequestContext, 'source' | 'logger'>, + >, +): ValueOrPromise; +``` + ### `parsingDidStart` The `parsingDidStart` event fires whenever Apollo Server will parse a GraphQL diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index 4b0c257a3b4..54e102dae4e 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -501,6 +501,30 @@ describe('runQuery', () => { }); }); + describe('didResolveSource', () => { + const didResolveSource = jest.fn(); + it('called with the source', async () => { + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + didResolveSource, + }; + }, + }, + ], + request: new MockReq(), + }); + + expect(didResolveSource).toHaveBeenCalled(); + expect(didResolveSource.mock.calls[0][0]) + .toHaveProperty('source', '{ testString }'); + }); + }); + describe('parsingDidStart', () => { const parsingDidStart = jest.fn(); it('called when parsing will result in an error', async () => { @@ -888,6 +912,9 @@ describe('runQuery', () => { let stopAwaiting: Function; const toBeAwaited = new Promise(resolve => stopAwaiting = resolve); + const didResolveSource: GraphQLRequestListener['didResolveSource'] = + jest.fn(() => { callOrder.push('didResolveSource') }); + const didResolveField: GraphQLRequestListenerDidResolveField = jest.fn(() => callOrder.push("didResolveField")); @@ -932,6 +959,7 @@ describe('runQuery', () => { { requestDidStart() { return { + didResolveSource, executionDidStart, }; }, @@ -944,6 +972,7 @@ describe('runQuery', () => { expect(willResolveField).toHaveBeenCalledTimes(1); expect(didResolveField).toHaveBeenCalledTimes(1); expect(callOrder).toStrictEqual([ + "didResolveSource", "executionDidStart", "willResolveField", "beforeAwaiting", diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 8ad8536b69c..dd3b283cc43 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -49,6 +49,7 @@ import { import { ApolloServerPlugin, GraphQLRequestListener, + GraphQLRequestContextDidResolveSource, GraphQLRequestContextExecutionDidStart, GraphQLRequestContextResponseForOperation, GraphQLRequestContextDidResolveOperation, @@ -214,6 +215,16 @@ export async function processGraphQLRequest( requestContext.queryHash = queryHash; requestContext.source = query; + // Let the plugins know that we now have a STRING of what we hope will + // parse and validate into a document we can execute on. Unless we have + // retrieved this from our APQ cache, there's no guarantee that it is + // syntactically correct, so this string should not be trusted as a valid + // document until after it's parsed and validated. + await dispatcher.invokeHookAsync( + 'didResolveSource', + requestContext as GraphQLRequestContextDidResolveSource, + ); + const requestDidEnd = extensionStack.requestDidStart({ request: request.http!, queryString: request.query, diff --git a/packages/apollo-server-integration-testsuite/src/index.ts b/packages/apollo-server-integration-testsuite/src/index.ts index a2618f328b7..37aab04fe10 100644 --- a/packages/apollo-server-integration-testsuite/src/index.ts +++ b/packages/apollo-server-integration-testsuite/src/index.ts @@ -1242,6 +1242,11 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { Parameters >; + let didResolveSource: jest.Mock< + ReturnType, + Parameters + >; + function createApqApp(apqOptions: PersistedQueryOptions = {}) { return createApp({ graphqlOptions: { @@ -1249,7 +1254,10 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { plugins: [ { requestDidStart() { - return { didEncounterErrors }; + return { + didResolveSource, + didEncounterErrors, + }; } } ], @@ -1265,6 +1273,7 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { beforeEach(async () => { cache = createMockCache(); + didResolveSource = jest.fn(); didEncounterErrors = jest.fn(); }); @@ -1285,6 +1294,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { ttl: 900, }), ); + expect(didResolveSource.mock.calls[0][0]) + .toHaveProperty('source', query); }); it('when ttlSeconds is unset, ttl is not passed to apq cache', @@ -1305,6 +1316,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { ttl: 900, }), ); + expect(didResolveSource.mock.calls[0][0]) + .toHaveProperty('source', query); } ); @@ -1407,6 +1420,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { })]), }), ); + + expect(didResolveSource).not.toHaveBeenCalled(); }); it('returns PersistedQueryNotFound on the first try', async () => { @@ -1432,6 +1447,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { ]), }), ); + + expect(didResolveSource).not.toHaveBeenCalled(); }); it('returns result on the second try', async () => { app = await createApqApp(); @@ -1452,6 +1469,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }), ); + expect(didResolveSource).not.toHaveBeenCalled(); + const result = await request(app) .post('/graphql') .send({ @@ -1464,6 +1483,9 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { // asserted above. expect(didEncounterErrors).toHaveBeenCalledTimes(1); + expect(didResolveSource.mock.calls[0][0]) + .toHaveProperty('source', query); + expect(result.body.data).toEqual({ testString: 'it works' }); expect(result.body.errors).toBeUndefined(); }); @@ -1523,6 +1545,9 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { .send({ extensions, }); + + expect(didResolveSource).not.toHaveBeenCalled(); + await request(app) .post('/graphql') .send({ @@ -1535,6 +1560,9 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { extensions, }); + expect(didResolveSource.mock.calls[0][0]) + .toHaveProperty('source', query); + expect(result.body.data).toEqual({ testString: 'it works' }); expect(result.body.errors).toBeUndefined(); }); @@ -1556,6 +1584,7 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); expect(response.status).toEqual(400); expect(response.error.text).toMatch(/does not match query/); + expect(didResolveSource).not.toHaveBeenCalled(); }); it('returns correct result using get request', async () => { @@ -1573,6 +1602,9 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { extensions: JSON.stringify(extensions), }); expect(result.body.data).toEqual({ testString: 'it works' }); + expect(didResolveSource.mock.calls[0][0]) + .toHaveProperty('source', query); + }); }); }); diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts index a90786dfd51..aef514d38de 100644 --- a/packages/apollo-server-plugin-base/src/index.ts +++ b/packages/apollo-server-plugin-base/src/index.ts @@ -8,6 +8,7 @@ import { GraphQLResponse, ValueOrPromise, WithRequired, + GraphQLRequestContextDidResolveSource, GraphQLRequestContextParsingDidStart, GraphQLRequestContextValidationDidStart, GraphQLRequestContextDidResolveOperation, @@ -35,6 +36,7 @@ export { GraphQLResponse, ValueOrPromise, WithRequired, + GraphQLRequestContextDidResolveSource, GraphQLRequestContextParsingDidStart, GraphQLRequestContextValidationDidStart, GraphQLRequestContextDidResolveOperation, @@ -63,6 +65,9 @@ export type GraphQLRequestListenerDidResolveField = export interface GraphQLRequestListener< TContext extends BaseContext = DefaultContext > extends AnyFunctionMap { + didResolveSource?( + requestContext: GraphQLRequestContextDidResolveSource, + ): ValueOrPromise; parsingDidStart?( requestContext: GraphQLRequestContextParsingDidStart, ): GraphQLRequestListenerParsingDidEnd | void; diff --git a/packages/apollo-server-types/src/index.ts b/packages/apollo-server-types/src/index.ts index e5707818f9e..f63a8629d8d 100644 --- a/packages/apollo-server-types/src/index.ts +++ b/packages/apollo-server-types/src/index.ts @@ -151,12 +151,14 @@ export type Logger = { error(message?: any): void; } -export type GraphQLRequestContextParsingDidStart = +export type GraphQLRequestContextDidResolveSource = WithRequired, | 'metrics' | 'source' | 'queryHash' >; +export type GraphQLRequestContextParsingDidStart = + GraphQLRequestContextDidResolveSource; export type GraphQLRequestContextValidationDidStart = GraphQLRequestContextParsingDidStart & WithRequired, From 39ff086db696e5148044b4b798e4d5d83aeb5060 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 7 May 2020 12:27:09 +0000 Subject: [PATCH 51/98] other stuff --- packages/apollo-server-core/src/__tests__/runQuery.test.ts | 6 ++++++ packages/apollo-server-integration-testsuite/src/index.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index 54e102dae4e..7e41b081932 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -912,6 +912,9 @@ describe('runQuery', () => { let stopAwaiting: Function; const toBeAwaited = new Promise(resolve => stopAwaiting = resolve); + const validationDidStart: GraphQLRequestListener['validationDidStart'] = + jest.fn(() => { callOrder.push('validationDidStart') }); + const didResolveSource: GraphQLRequestListener['didResolveSource'] = jest.fn(() => { callOrder.push('didResolveSource') }); @@ -960,6 +963,7 @@ describe('runQuery', () => { requestDidStart() { return { didResolveSource, + validationDidStart, executionDidStart, }; }, @@ -968,11 +972,13 @@ describe('runQuery', () => { request: new MockReq(), }); + expect(validationDidStart).toHaveBeenCalledTimes(1); expect(executionDidStart).toHaveBeenCalledTimes(1); expect(willResolveField).toHaveBeenCalledTimes(1); expect(didResolveField).toHaveBeenCalledTimes(1); expect(callOrder).toStrictEqual([ "didResolveSource", + "validationDidStart", "executionDidStart", "willResolveField", "beforeAwaiting", diff --git a/packages/apollo-server-integration-testsuite/src/index.ts b/packages/apollo-server-integration-testsuite/src/index.ts index 37aab04fe10..e39cb84d036 100644 --- a/packages/apollo-server-integration-testsuite/src/index.ts +++ b/packages/apollo-server-integration-testsuite/src/index.ts @@ -1289,7 +1289,7 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { expect(cache.set).toHaveBeenCalledWith( expect.stringMatching(/^apq:/), - '{testString}', + query, expect.objectContaining({ ttl: 900, }), From 129c255c766dead440412b242e93c236d21bbef3 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 7 May 2020 12:27:09 +0000 Subject: [PATCH 52/98] tests: Add further life-cycle ordering tests for parsing and validation. --- .../src/__tests__/runQuery.test.ts | 28 +++++++++++++++++++ .../src/index.ts | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index 91c5b8ef46a..a788c6351dc 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -24,6 +24,8 @@ import { GraphQLRequestExecutionListener, GraphQLRequestListenerDidResolveField, GraphQLRequestListenerExecutionDidEnd, + GraphQLRequestListenerParsingDidEnd, + GraphQLRequestListenerValidationDidEnd, } from 'apollo-server-plugin-base'; import { GraphQLRequestListener } from 'apollo-server-plugin-base'; import { InMemoryLRUCache } from 'apollo-server-caching'; @@ -864,6 +866,22 @@ describe('runQuery', () => { let stopAwaiting: Function; const toBeAwaited = new Promise(resolve => stopAwaiting = resolve); + const parsingDidEnd: GraphQLRequestListenerParsingDidEnd = + jest.fn(() => callOrder.push('parsingDidEnd')); + const parsingDidStart: GraphQLRequestListener['parsingDidStart'] = + jest.fn(() => { + callOrder.push('parsingDidStart'); + return parsingDidEnd; + }); + + const validationDidEnd: GraphQLRequestListenerValidationDidEnd = + jest.fn(() => callOrder.push('validationDidEnd')); + const validationDidStart: GraphQLRequestListener['validationDidStart'] = + jest.fn(() => { + callOrder.push('validationDidStart'); + return validationDidEnd; + }); + const didResolveField: GraphQLRequestListenerDidResolveField = jest.fn(() => callOrder.push("didResolveField")); @@ -908,6 +926,8 @@ describe('runQuery', () => { { requestDidStart() { return { + parsingDidStart, + validationDidStart, executionDidStart, }; }, @@ -916,10 +936,18 @@ describe('runQuery', () => { request: new MockReq(), }); + expect(parsingDidStart).toHaveBeenCalledTimes(1); + expect(parsingDidEnd).toHaveBeenCalledTimes(1); + expect(validationDidStart).toHaveBeenCalledTimes(1); + expect(validationDidEnd).toHaveBeenCalledTimes(1); expect(executionDidStart).toHaveBeenCalledTimes(1); expect(willResolveField).toHaveBeenCalledTimes(1); expect(didResolveField).toHaveBeenCalledTimes(1); expect(callOrder).toStrictEqual([ + "parsingDidStart", + "parsingDidEnd", + "validationDidStart", + "validationDidEnd", "executionDidStart", "willResolveField", "beforeAwaiting", diff --git a/packages/apollo-server-integration-testsuite/src/index.ts b/packages/apollo-server-integration-testsuite/src/index.ts index a5dd026108f..4270b2bda31 100644 --- a/packages/apollo-server-integration-testsuite/src/index.ts +++ b/packages/apollo-server-integration-testsuite/src/index.ts @@ -1278,7 +1278,7 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { expect(cache.set).toHaveBeenCalledWith( expect.stringMatching(/^apq:/), - '{testString}', + query, expect.objectContaining({ ttl: 900, }), From 66d586924821094f55ac5ca966a7cbf07b54b52f Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 7 May 2020 12:07:04 +0000 Subject: [PATCH 53/98] refactor(tests): Better helpers for APQ tests in intgr. testsuite. To be leveraged soon: https://github.com/apollographql/apollo-server/pull/3998 --- .../src/index.ts | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/apollo-server-integration-testsuite/src/index.ts b/packages/apollo-server-integration-testsuite/src/index.ts index 4270b2bda31..99a8490d60a 100644 --- a/packages/apollo-server-integration-testsuite/src/index.ts +++ b/packages/apollo-server-integration-testsuite/src/index.ts @@ -18,7 +18,12 @@ import { import request from 'supertest'; -import { GraphQLOptions, Config } from 'apollo-server-core'; +import { + GraphQLOptions, + Config, + PersistedQueryOptions, + KeyValueCache, +} from 'apollo-server-core'; import gql from 'graphql-tag'; import { ValueOrPromise } from 'apollo-server-types'; import { GraphQLRequestListener } from "apollo-server-plugin-base"; @@ -1221,12 +1226,7 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }, }; - let didEncounterErrors: jest.Mock< - ReturnType, - Parameters - >; - - function createMockCache() { + function createMockCache(): KeyValueCache { const map = new Map(); return { set: jest.fn(async (key, val) => { @@ -1237,10 +1237,13 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }; } - beforeEach(async () => { - didEncounterErrors = jest.fn(); - const cache = createMockCache(); - app = await createApp({ + let didEncounterErrors: jest.Mock< + ReturnType, + Parameters + >; + + function createApqApp(apqOptions: PersistedQueryOptions = {}) { + return createApp({ graphqlOptions: { schema, plugins: [ @@ -1252,22 +1255,21 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { ], persistedQueries: { cache, + ...apqOptions, }, }, }); + } + + let cache: KeyValueCache; + + beforeEach(async () => { + cache = createMockCache(); + didEncounterErrors = jest.fn(); }); it('when ttlSeconds is set, passes ttl to the apq cache set call', async () => { - const cache = createMockCache(); - app = await createApp({ - graphqlOptions: { - schema, - persistedQueries: { - cache: cache, - ttl: 900, - }, - }, - }); + app = await createApqApp({ ttl: 900 }); await request(app) .post('/graphql') @@ -1287,15 +1289,7 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { it('when ttlSeconds is unset, ttl is not passed to apq cache', async () => { - const cache = createMockCache(); - app = await createApp({ - graphqlOptions: { - schema, - persistedQueries: { - cache: cache, - }, - }, - }); + app = await createApqApp(); await request(app) .post('/graphql') @@ -1315,6 +1309,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { ); it('errors when version is not specified', async () => { + app = await createApqApp(); + const result = await request(app) .get('/graphql') .query({ @@ -1346,6 +1342,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('errors when version is unsupported', async () => { + app = await createApqApp(); + const result = await request(app) .get('/graphql') .query({ @@ -1378,6 +1376,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('errors when hash is mismatched', async () => { + app = await createApqApp(); + const result = await request(app) .get('/graphql') .query({ @@ -1410,6 +1410,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('returns PersistedQueryNotFound on the first try', async () => { + app = await createApqApp(); + const result = await request(app) .post('/graphql') .send({ @@ -1432,6 +1434,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { ); }); it('returns result on the second try', async () => { + app = await createApqApp(); + await request(app) .post('/graphql') .send({ @@ -1465,6 +1469,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('returns with batched persisted queries', async () => { + app = await createApqApp(); + const errors = await request(app) .post('/graphql') .send([ @@ -1510,6 +1516,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('returns result on the persisted query', async () => { + app = await createApqApp(); + await request(app) .post('/graphql') .send({ @@ -1532,6 +1540,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('returns error when hash does not match', async () => { + app = await createApqApp(); + const response = await request(app) .post('/graphql') .send({ @@ -1549,6 +1559,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); it('returns correct result using get request', async () => { + app = await createApqApp(); + await request(app) .post('/graphql') .send({ From 570bc4e59f8fd9df727df2de52964cbbe216ae62 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 7 May 2020 12:26:51 +0000 Subject: [PATCH 54/98] feat(plugins): Intro. `didResolveSource` to indicate availability of "source". This PR targets https://github.com/apollographql/apollo-server/pull/3988 but was prompted by a need identified during review in the following comment: https://github.com/apollographql/apollo-server/pull/3998/files#r414911049 As noted, this is tangible clunkiness which could make the implementation of the `ensurePreflight` invocations unnecessary. That defensiveness was only necessary because the extensions API - which #3988 + #3998 aims to render unnecessary - guaranteed the presence of the "source" (the text of the incoming operation) prior to invoking its `requestDidStart` method (which was actually a bit after the request had started, in reality). Hopefully this proves to be a useful life-cycle for other cases! --- docs/source/integrations/plugins.md | 17 ++++++++++ .../src/__tests__/runQuery.test.ts | 29 ++++++++++++++++ .../apollo-server-core/src/requestPipeline.ts | 11 ++++++ .../src/index.ts | 34 ++++++++++++++++++- .../apollo-server-plugin-base/src/index.ts | 5 +++ packages/apollo-server-types/src/index.ts | 4 ++- 6 files changed, 98 insertions(+), 2 deletions(-) diff --git a/docs/source/integrations/plugins.md b/docs/source/integrations/plugins.md index df4003d8544..2c9c2600381 100644 --- a/docs/source/integrations/plugins.md +++ b/docs/source/integrations/plugins.md @@ -313,6 +313,23 @@ should not return a value. > If you're using TypeScript to create your plugin, implement the [ `GraphQLRequestListener` interface](https://github.com/apollographql/apollo-server/blob/master/packages/apollo-server-plugin-base/src/index.ts) from the `apollo-server-plugin-base` module to define functions for request lifecycle events. +### `didResolveSource` + +The `didResolveSource` event is invoked after Apollo Server has determined the +`String`-representation of the incoming operation that it will act upon. In the +event that this `String` was not directly passed in from the client, this +may be retrieved from a cache store (e.g., Automated Persisted Queries). + +At this stage, there is not a guarantee that the operation is not malformed. + +```typescript +didResolveSource?( + requestContext: WithRequired< + GraphQLRequestContext, 'source' | 'logger'>, + >, +): ValueOrPromise; +``` + ### `parsingDidStart` The `parsingDidStart` event fires whenever Apollo Server will parse a GraphQL diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index a788c6351dc..3464c7ec455 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -479,6 +479,30 @@ describe('runQuery', () => { }); }); + describe('didResolveSource', () => { + const didResolveSource = jest.fn(); + it('called with the source', async () => { + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + didResolveSource, + }; + }, + }, + ], + request: new MockReq(), + }); + + expect(didResolveSource).toHaveBeenCalled(); + expect(didResolveSource.mock.calls[0][0]) + .toHaveProperty('source', '{ testString }'); + }); + }); + describe('parsingDidStart', () => { const parsingDidStart = jest.fn(); it('called when parsing will result in an error', async () => { @@ -882,6 +906,9 @@ describe('runQuery', () => { return validationDidEnd; }); + const didResolveSource: GraphQLRequestListener['didResolveSource'] = + jest.fn(() => { callOrder.push('didResolveSource') }); + const didResolveField: GraphQLRequestListenerDidResolveField = jest.fn(() => callOrder.push("didResolveField")); @@ -928,6 +955,7 @@ describe('runQuery', () => { return { parsingDidStart, validationDidStart, + didResolveSource, executionDidStart, }; }, @@ -944,6 +972,7 @@ describe('runQuery', () => { expect(willResolveField).toHaveBeenCalledTimes(1); expect(didResolveField).toHaveBeenCalledTimes(1); expect(callOrder).toStrictEqual([ + "didResolveSource", "parsingDidStart", "parsingDidEnd", "validationDidStart", diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 79bc9bf0fa2..074f7099528 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -49,6 +49,7 @@ import { import { ApolloServerPlugin, GraphQLRequestListener, + GraphQLRequestContextDidResolveSource, GraphQLRequestContextExecutionDidStart, GraphQLRequestContextResponseForOperation, GraphQLRequestContextDidResolveOperation, @@ -209,6 +210,16 @@ export async function processGraphQLRequest( requestContext.queryHash = queryHash; requestContext.source = query; + // Let the plugins know that we now have a STRING of what we hope will + // parse and validate into a document we can execute on. Unless we have + // retrieved this from our APQ cache, there's no guarantee that it is + // syntactically correct, so this string should not be trusted as a valid + // document until after it's parsed and validated. + await dispatcher.invokeHookAsync( + 'didResolveSource', + requestContext as GraphQLRequestContextDidResolveSource, + ); + const requestDidEnd = extensionStack.requestDidStart({ request: request.http!, queryString: request.query, diff --git a/packages/apollo-server-integration-testsuite/src/index.ts b/packages/apollo-server-integration-testsuite/src/index.ts index 99a8490d60a..e39cb84d036 100644 --- a/packages/apollo-server-integration-testsuite/src/index.ts +++ b/packages/apollo-server-integration-testsuite/src/index.ts @@ -1242,6 +1242,11 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { Parameters >; + let didResolveSource: jest.Mock< + ReturnType, + Parameters + >; + function createApqApp(apqOptions: PersistedQueryOptions = {}) { return createApp({ graphqlOptions: { @@ -1249,7 +1254,10 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { plugins: [ { requestDidStart() { - return { didEncounterErrors }; + return { + didResolveSource, + didEncounterErrors, + }; } } ], @@ -1265,6 +1273,7 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { beforeEach(async () => { cache = createMockCache(); + didResolveSource = jest.fn(); didEncounterErrors = jest.fn(); }); @@ -1285,6 +1294,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { ttl: 900, }), ); + expect(didResolveSource.mock.calls[0][0]) + .toHaveProperty('source', query); }); it('when ttlSeconds is unset, ttl is not passed to apq cache', @@ -1305,6 +1316,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { ttl: 900, }), ); + expect(didResolveSource.mock.calls[0][0]) + .toHaveProperty('source', query); } ); @@ -1407,6 +1420,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { })]), }), ); + + expect(didResolveSource).not.toHaveBeenCalled(); }); it('returns PersistedQueryNotFound on the first try', async () => { @@ -1432,6 +1447,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { ]), }), ); + + expect(didResolveSource).not.toHaveBeenCalled(); }); it('returns result on the second try', async () => { app = await createApqApp(); @@ -1452,6 +1469,8 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }), ); + expect(didResolveSource).not.toHaveBeenCalled(); + const result = await request(app) .post('/graphql') .send({ @@ -1464,6 +1483,9 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { // asserted above. expect(didEncounterErrors).toHaveBeenCalledTimes(1); + expect(didResolveSource.mock.calls[0][0]) + .toHaveProperty('source', query); + expect(result.body.data).toEqual({ testString: 'it works' }); expect(result.body.errors).toBeUndefined(); }); @@ -1523,6 +1545,9 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { .send({ extensions, }); + + expect(didResolveSource).not.toHaveBeenCalled(); + await request(app) .post('/graphql') .send({ @@ -1535,6 +1560,9 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { extensions, }); + expect(didResolveSource.mock.calls[0][0]) + .toHaveProperty('source', query); + expect(result.body.data).toEqual({ testString: 'it works' }); expect(result.body.errors).toBeUndefined(); }); @@ -1556,6 +1584,7 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); expect(response.status).toEqual(400); expect(response.error.text).toMatch(/does not match query/); + expect(didResolveSource).not.toHaveBeenCalled(); }); it('returns correct result using get request', async () => { @@ -1573,6 +1602,9 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { extensions: JSON.stringify(extensions), }); expect(result.body.data).toEqual({ testString: 'it works' }); + expect(didResolveSource.mock.calls[0][0]) + .toHaveProperty('source', query); + }); }); }); diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts index a90786dfd51..aef514d38de 100644 --- a/packages/apollo-server-plugin-base/src/index.ts +++ b/packages/apollo-server-plugin-base/src/index.ts @@ -8,6 +8,7 @@ import { GraphQLResponse, ValueOrPromise, WithRequired, + GraphQLRequestContextDidResolveSource, GraphQLRequestContextParsingDidStart, GraphQLRequestContextValidationDidStart, GraphQLRequestContextDidResolveOperation, @@ -35,6 +36,7 @@ export { GraphQLResponse, ValueOrPromise, WithRequired, + GraphQLRequestContextDidResolveSource, GraphQLRequestContextParsingDidStart, GraphQLRequestContextValidationDidStart, GraphQLRequestContextDidResolveOperation, @@ -63,6 +65,9 @@ export type GraphQLRequestListenerDidResolveField = export interface GraphQLRequestListener< TContext extends BaseContext = DefaultContext > extends AnyFunctionMap { + didResolveSource?( + requestContext: GraphQLRequestContextDidResolveSource, + ): ValueOrPromise; parsingDidStart?( requestContext: GraphQLRequestContextParsingDidStart, ): GraphQLRequestListenerParsingDidEnd | void; diff --git a/packages/apollo-server-types/src/index.ts b/packages/apollo-server-types/src/index.ts index 6e0cf487461..1a5ecd52522 100644 --- a/packages/apollo-server-types/src/index.ts +++ b/packages/apollo-server-types/src/index.ts @@ -148,12 +148,14 @@ export type Logger = { error(message?: any): void; } -export type GraphQLRequestContextParsingDidStart = +export type GraphQLRequestContextDidResolveSource = WithRequired, | 'metrics' | 'source' | 'queryHash' >; +export type GraphQLRequestContextParsingDidStart = + GraphQLRequestContextDidResolveSource; export type GraphQLRequestContextValidationDidStart = GraphQLRequestContextParsingDidStart & WithRequired, From fe971b3f332e9768b9c2ae1594239fb6b618a129 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 10:01:37 +0000 Subject: [PATCH 55/98] docs: Add `didResolveSource` to plugin Mermaid workflow. --- docs/source/integrations/plugins.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/source/integrations/plugins.md b/docs/source/integrations/plugins.md index 2c9c2600381..578e62f6a3e 100644 --- a/docs/source/integrations/plugins.md +++ b/docs/source/integrations/plugins.md @@ -121,17 +121,18 @@ The following diagram illustrates the sequence of events that fire for each requ ```mermaid graph TB; - request(requestDidStart) --> parsing(parsingDidStart*); + request(requestDidStart) --> resolveSource(didResolveSource); + resolveSource --"Success"--> parsing(parsingDidStart*); parsing --"Success"--> validation(validationDidStart*); - validation --"Success"--> resolve(didResolveOperation); - resolve --"Success"--> response(responseForOperation); + validation --"Success"--> resolveOperation(didResolveOperation); + resolveOperation --"Success"--> response(responseForOperation); execution(executionDidStart*); errors(didEncounterErrors); response --"Response provided"--> send; response --"No response provided"--> execution; execution --"Success"--> send(willSendResponse); - execution & resolve & parsing & validation --"Failure"--> errors; + execution & resolveSource & resolveOperation & parsing & validation --"Failure"--> errors; errors --> send; class server,request secondary; ``` From 464e4f2128fcd5bad39aab22b8b7755c9e53a29f Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 10:14:57 +0000 Subject: [PATCH 56/98] noop: Add comment indicating where to find `didResolveSource` APQ tests. --- .../apollo-server-core/src/__tests__/runQuery.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index 3464c7ec455..228ea74f46d 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -479,6 +479,15 @@ describe('runQuery', () => { }); }); + /** + * This tests the simple invocation of the "didResolveSource" hook, but + * doesn't test one of the primary reasons why "source" isn't guaranteed + * sooner in the request life-cycle: when "source" is populated via an APQ + * cache HIT. + * + * That functionality is tested in `apollo-server-integration-testsuite`, + * within the "Persisted Queries" tests. (Search for "didResolveSource"). + */ describe('didResolveSource', () => { const didResolveSource = jest.fn(); it('called with the source', async () => { From ba37e68112266354a2504ead6bbe94fd0dc18653 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 11:48:46 +0000 Subject: [PATCH 57/98] changelog: #3998 I may re-visit this consideration. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1aee8a1900..1e12834e9c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ The version headers in this history reflect the versions of Apollo Server itself > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. +- `apollo-engine-reporting`: The underlying integration of this plugin, which instruments and traces the graph's resolver performance and transmits these metrics to [Apollo Graph Manager](https://engine.apollographql.com/), has been changed from the (soon to be deprecated) `graphql-extensions` API to the new [request pipeline `plugins` API](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). [PR #3998](https://github.com/apollographql/apollo-server/pull/3998) + + _This change should be purely an implementation detail for a majority of users_. There are, however, some special considerations which are worth noting: + + - The federated tracing plugin's `ftv1` response on `extensions` (which is present on the response from an implementing service to the gateway) is now placed on the `extensions` _after_ the `formatResponse` hook. Anyone leveraging the `extensions`.`ftv1` data from the `formatResponse` hook will find that it is no longer present at that phase. + - _Nothing yet! Stay tuned!_ ### v2.13.0 From e9edd9aef186c0185cd9ec9bc4896f6ac9c9524c Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 12:25:15 +0000 Subject: [PATCH 58/98] types(e-r): Improve the typings of `didEnd`. --- .../apollo-engine-reporting/src/plugin.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 2e963c247df..1b9715235ac 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -2,6 +2,8 @@ import { GraphQLRequestContext, Logger, InvalidGraphQLRequestError, + GraphQLRequestContextExecutionDidStart, + GraphQLRequestContextDidEncounterErrors, } from 'apollo-server-types'; import { Headers } from 'apollo-server-env'; import { GraphQLError } from 'graphql'; @@ -122,7 +124,11 @@ export const plugin = ( } let endDone: boolean = false; - function didEnd() { + function didEnd( + requestContext: + | GraphQLRequestContextExecutionDidStart + | GraphQLRequestContextDidEncounterErrors, + ) { if (endDone) return; endDone = true; treeBuilder.stopTiming(); @@ -169,11 +175,11 @@ export const plugin = ( ensurePreflight(); }, - executionDidStart() { + executionDidStart(requestContext) { ensurePreflight(); return { - executionDidEnd: didEnd, + executionDidEnd: () => didEnd(requestContext), willResolveField(...args) { const [, , , info] = args; return treeBuilder.willResolveField(info); @@ -184,16 +190,16 @@ export const plugin = ( }; }, - didEncounterErrors({ errors }) { + didEncounterErrors(requestContext) { // We don't report some special-cased errors to Graph Manager. // See the definition of this function for the reasons. - if (allUnreportableSpecialCasedErrors(errors)) { + if (allUnreportableSpecialCasedErrors(requestContext.errors)) { return; } ensurePreflight(); - treeBuilder.didEncounterErrors(errors); - didEnd(); + treeBuilder.didEncounterErrors(requestContext.errors); + didEnd(requestContext); }, }; } From b981b590328a6a321956ca851fb34d19f973aa25 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 12:26:25 +0000 Subject: [PATCH 59/98] chore(e-r): Eliminate `ensurePreflight` by using (new) `didResolveSource`. Leverages new life-cycle `didResolveSource` which was introduced by [[PR #4076]] and inspired by this [[comment]]. This is much nicer! [PR #4076]: https://github.com/apollographql/apollo-server/pull/4076 [Comment]: https://github.com/apollographql/apollo-server/pull/3998/files#r414911049 --- .../apollo-engine-reporting/src/plugin.ts | 134 ++++++++---------- 1 file changed, 56 insertions(+), 78 deletions(-) diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 1b9715235ac..5faa1390956 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -44,83 +44,46 @@ export const plugin = ( return { - requestDidStart(requestContext) { - // We still need the entire `requestContext` to pass through to the - // `generateClientInfo` method, but we'll destructure for brevity within. - const { - metrics, - logger: requestLogger, - schemaHash, - request: { http, variables }, - } = requestContext; - - const treeBuilder: EngineReportingTreeBuilder = - new EngineReportingTreeBuilder({ + requestDidStart({ + logger: requestLogger, + schemaHash, + metrics, + request: { http, variables }, + }) { + const treeBuilder: EngineReportingTreeBuilder = new EngineReportingTreeBuilder( + { rewriteError: options.rewriteError, logger: requestLogger || logger, - }); + }, + ); treeBuilder.startTiming(); metrics.startHrTime = treeBuilder.startHrTime; - let preflightDone: boolean = false; - function ensurePreflight() { - if (preflightDone) return; - preflightDone = true; - - if (http) { - treeBuilder.trace.http = new Trace.HTTP({ - method: - Trace.HTTP.Method[http.method as keyof typeof Trace.HTTP.Method] - || Trace.HTTP.Method.UNKNOWN, - // Host and path are not used anywhere on the backend, so let's not bother - // trying to parse request.url to get them, which is a potential - // source of bugs because integrations have different behavior here. - // On Node's HTTP module, request.url only includes the path - // (see https://nodejs.org/api/http.html#http_message_url) - // The same is true on Lambda (where we pass event.path) - // But on environments like Cloudflare we do get a complete URL. - host: null, - path: null, - }); - - if (options.sendHeaders) { - makeHTTPRequestHeaders( - treeBuilder.trace.http, - http.headers, - options.sendHeaders, - ); - } - } - - if (metrics.persistedQueryHit) { - treeBuilder.trace.persistedQueryHit = true; - } - if (metrics.persistedQueryRegister) { - treeBuilder.trace.persistedQueryRegister = true; - } + if (http) { + treeBuilder.trace.http = new Trace.HTTP({ + method: + Trace.HTTP.Method[http.method as keyof typeof Trace.HTTP.Method] || + Trace.HTTP.Method.UNKNOWN, + // Host and path are not used anywhere on the backend, so let's not bother + // trying to parse request.url to get them, which is a potential + // source of bugs because integrations have different behavior here. + // On Node's HTTP module, request.url only includes the path + // (see https://nodejs.org/api/http.html#http_message_url) + // The same is true on Lambda (where we pass event.path) + // But on environments like Cloudflare we do get a complete URL. + host: null, + path: null, + }); - if (variables) { - treeBuilder.trace.details = makeTraceDetails( - variables, - options.sendVariableValues, - requestContext.source, + if (options.sendHeaders) { + makeHTTPRequestHeaders( + treeBuilder.trace.http, + http.headers, + options.sendHeaders, ); } - - const clientInfo = generateClientInfo(requestContext); - if (clientInfo) { - // While clientAddress could be a part of the protobuf, we'll ignore it for - // now, since the backend does not group by it and Engine frontend will not - // support it in the short term - const { clientName, clientVersion, clientReferenceId } = clientInfo; - // the backend makes the choice of mapping clientName => clientReferenceId if - // no custom reference id is provided - treeBuilder.trace.clientVersion = clientVersion || ''; - treeBuilder.trace.clientReferenceId = clientReferenceId || ''; - treeBuilder.trace.clientName = clientName || ''; - } } let endDone: boolean = false; @@ -163,21 +126,37 @@ export const plugin = ( } return { - parsingDidStart() { - ensurePreflight(); - }, + didResolveSource(requestContext) { + if (metrics.persistedQueryHit) { + treeBuilder.trace.persistedQueryHit = true; + } + if (metrics.persistedQueryRegister) { + treeBuilder.trace.persistedQueryRegister = true; + } - validationDidStart() { - ensurePreflight(); - }, + if (variables) { + treeBuilder.trace.details = makeTraceDetails( + variables, + options.sendVariableValues, + requestContext.source, + ); + } - didResolveOperation() { - ensurePreflight(); + const clientInfo = generateClientInfo(requestContext); + if (clientInfo) { + // While clientAddress could be a part of the protobuf, we'll ignore + // it for now, since the backend does not group by it and Graph + // Manager will not support it in the short term + const { clientName, clientVersion, clientReferenceId } = clientInfo; + // the backend makes the choice of mapping clientName => clientReferenceId if + // no custom reference id is provided + treeBuilder.trace.clientVersion = clientVersion || ''; + treeBuilder.trace.clientReferenceId = clientReferenceId || ''; + treeBuilder.trace.clientName = clientName || ''; + } }, executionDidStart(requestContext) { - ensurePreflight(); - return { executionDidEnd: () => didEnd(requestContext), willResolveField(...args) { @@ -197,7 +176,6 @@ export const plugin = ( return; } - ensurePreflight(); treeBuilder.didEncounterErrors(requestContext.errors); didEnd(requestContext); }, From 6d4777da1f4dfaab3bf23523e9106e6c2abbab08 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 12:34:47 +0000 Subject: [PATCH 60/98] changelog: Add note that I believe some new APQ errors are now traced. @glasser, are you able to confirm this belief and that it should be okay? --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e12834e9c6..02bef1a8293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The version headers in this history reflect the versions of Apollo Server itself _This change should be purely an implementation detail for a majority of users_. There are, however, some special considerations which are worth noting: - The federated tracing plugin's `ftv1` response on `extensions` (which is present on the response from an implementing service to the gateway) is now placed on the `extensions` _after_ the `formatResponse` hook. Anyone leveraging the `extensions`.`ftv1` data from the `formatResponse` hook will find that it is no longer present at that phase. + - Automated persisted query (APQ) errors [`PERSISTED_QUERY_NOT_SUPPORTED`](https://github.com/apollographql/apollo-server/blob/b981b590328a6a321956ca851fb34d19f973aa25/packages/apollo-server-errors/src/index.ts#L214-L222) and [`Unsupported persisted query version`](https://github.com/apollographql/apollo-server/blob/b981b590328a6a321956ca851fb34d19f973aa25/packages/apollo-server-core/src/requestPipeline.ts#L165) may now be reported to Graph Manager, whereas previously they were excluded from transmission. - _Nothing yet! Stay tuned!_ From bacbb175f38ab8e896e30ba5b82c6b4964535b4b Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 12:45:32 +0000 Subject: [PATCH 61/98] Revert "changelog: Add note that I believe some new APQ errors are now traced." This reverts commit 6d4777da1f4dfaab3bf23523e9106e6c2abbab08. --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02bef1a8293..1e12834e9c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,6 @@ The version headers in this history reflect the versions of Apollo Server itself _This change should be purely an implementation detail for a majority of users_. There are, however, some special considerations which are worth noting: - The federated tracing plugin's `ftv1` response on `extensions` (which is present on the response from an implementing service to the gateway) is now placed on the `extensions` _after_ the `formatResponse` hook. Anyone leveraging the `extensions`.`ftv1` data from the `formatResponse` hook will find that it is no longer present at that phase. - - Automated persisted query (APQ) errors [`PERSISTED_QUERY_NOT_SUPPORTED`](https://github.com/apollographql/apollo-server/blob/b981b590328a6a321956ca851fb34d19f973aa25/packages/apollo-server-errors/src/index.ts#L214-L222) and [`Unsupported persisted query version`](https://github.com/apollographql/apollo-server/blob/b981b590328a6a321956ca851fb34d19f973aa25/packages/apollo-server-core/src/requestPipeline.ts#L165) may now be reported to Graph Manager, whereas previously they were excluded from transmission. - _Nothing yet! Stay tuned!_ From 59b401330064551b20339417ae26fde37ffe3857 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 15:13:57 +0000 Subject: [PATCH 62/98] fix(e-r): Do not keep traces unless we resolve the "source". Previously, I introduced a work-around for this in 78a4cb77edc7c7f, though that is no longer necessary with the introduction of `didResolveSource` in https://github.com/apollographql/apollo-server/pull/4076 I'm thrilled to remove this! Ref: https://github.com/apollographql/apollo-server/pull/3998#discussion_r414916905 --- .../apollo-engine-reporting/src/plugin.ts | 59 +++++-------------- 1 file changed, 14 insertions(+), 45 deletions(-) diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 5faa1390956..36915c2466c 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -1,12 +1,10 @@ import { GraphQLRequestContext, Logger, - InvalidGraphQLRequestError, GraphQLRequestContextExecutionDidStart, GraphQLRequestContextDidEncounterErrors, } from 'apollo-server-types'; import { Headers } from 'apollo-server-env'; -import { GraphQLError } from 'graphql'; import { Trace } from 'apollo-engine-reporting-protobuf'; import { @@ -18,10 +16,6 @@ import { } from './agent'; import { EngineReportingTreeBuilder } from './treeBuilder'; import { ApolloServerPlugin } from "apollo-server-plugin-base"; -import { - PersistedQueryNotFoundError, - PersistedQueryNotSupportedError, -} from 'apollo-server-errors'; const clientNameHeaderKey = 'apollographql-client-name'; const clientReferenceIdHeaderKey = 'apollographql-client-reference-id'; @@ -125,8 +119,19 @@ export const plugin = ( }); } + // While we start the tracing as soon as possible, we only actually report + // traces when we have resolved the source. This is largely because of + // the APQ negotiation that takes place before that resolution happens. + // This is effectively bypassing the reporting of: + // - PersistedQueryNotFoundError + // - PersistedQueryNotSupportedError + // - InvalidGraphQLRequestError + let didResolveSource: boolean = false; + return { didResolveSource(requestContext) { + didResolveSource = true; + if (metrics.persistedQueryHit) { treeBuilder.trace.persistedQueryHit = true; } @@ -170,12 +175,9 @@ export const plugin = ( }, didEncounterErrors(requestContext) { - // We don't report some special-cased errors to Graph Manager. - // See the definition of this function for the reasons. - if (allUnreportableSpecialCasedErrors(requestContext.errors)) { - return; - } - + // Search above for a comment about "didResolveSource" to see which + // of the pre-source-resolution errors we are intentionally avoiding. + if (!didResolveSource) return; treeBuilder.didEncounterErrors(requestContext.errors); didEnd(requestContext); }, @@ -184,39 +186,6 @@ export const plugin = ( }; }; -/** - * Previously, prior to the new plugin API, the Apollo Engine Reporting - * mechanism was implemented using `graphql-extensions`, the API for which - * didn't invoke `requestDidStart` until _after_ APQ had been negotiated. - * - * The new plugin API starts its `requestDidStart` _before_ APQ validation and - * various other assertions which weren't included in the `requestDidStart` - * life-cycle, even if they perhaps should be in terms of error reporting. - * - * The new plugin API is able to properly capture such errors within its - * `didEncounterErrors` lifecycle hook, however, for behavioral consistency - * reasons, we will still special-case those errors and maintain the legacy - * behavior to avoid a breaking change. We can reconsider this in a future - * version of Apollo Engine Reporting (AS3, perhaps!). - * - * @param errors A list of errors to scan for special-cased instances. - */ -function allUnreportableSpecialCasedErrors( - errors: readonly GraphQLError[], -): boolean { - return errors.every(err => { - if ( - err instanceof PersistedQueryNotFoundError || - err instanceof PersistedQueryNotSupportedError || - err instanceof InvalidGraphQLRequestError - ) { - return true; - } - - return false; - }); -} - // Helpers for producing traces. function defaultGenerateClientInfo({ request }: GraphQLRequestContext) { From 112178503611ec1275889cc121f9e43af038825f Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 16:03:07 +0000 Subject: [PATCH 63/98] chore(types): Remove `DefaultContext` and just leverage `BaseContext`. They were the same structurally anyway. For some reason I thought that it looked better in the definitions within the [[Plugin types]] when I first wrote it, but I'm not caught up on it. [Plugin types]: https://github.com/apollographql/apollo-server/blob/1356f008/packages/apollo-server-plugin-base/src/index.ts#L48 --- packages/apollo-server-plugin-base/src/index.ts | 8 +++----- packages/apollo-server-types/src/index.ts | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts index aef514d38de..448890848a6 100644 --- a/packages/apollo-server-plugin-base/src/index.ts +++ b/packages/apollo-server-plugin-base/src/index.ts @@ -1,7 +1,6 @@ import { AnyFunctionMap, BaseContext, - DefaultContext, GraphQLServiceContext, GraphQLRequestContext, GraphQLRequest, @@ -29,7 +28,6 @@ import { GraphQLFieldResolver } from "graphql"; // probably roll into the same "types" package, but that is not today! export { BaseContext, - DefaultContext, GraphQLServiceContext, GraphQLRequestContext, GraphQLRequest, @@ -47,7 +45,7 @@ export { }; export interface ApolloServerPlugin< - TContext extends BaseContext = DefaultContext + TContext extends BaseContext = BaseContext > { serverWillStart?(service: GraphQLServiceContext): ValueOrPromise; requestDidStart?( @@ -63,7 +61,7 @@ export type GraphQLRequestListenerDidResolveField = ((error: Error | null, result?: any) => void); export interface GraphQLRequestListener< - TContext extends BaseContext = DefaultContext + TContext extends BaseContext = BaseContext > extends AnyFunctionMap { didResolveSource?( requestContext: GraphQLRequestContextDidResolveSource, @@ -100,7 +98,7 @@ export interface GraphQLRequestListener< } export interface GraphQLRequestExecutionListener< - TContext extends BaseContext = DefaultContext + TContext extends BaseContext = BaseContext > extends AnyFunctionMap { executionDidEnd?: GraphQLRequestListenerExecutionDidEnd; willResolveField?( diff --git a/packages/apollo-server-types/src/index.ts b/packages/apollo-server-types/src/index.ts index 1a5ecd52522..a4ef63ed921 100644 --- a/packages/apollo-server-types/src/index.ts +++ b/packages/apollo-server-types/src/index.ts @@ -14,7 +14,6 @@ import { KeyValueCache } from 'apollo-server-caching'; import { Trace } from 'apollo-engine-reporting-protobuf'; export type BaseContext = Record; -export type DefaultContext = BaseContext; export type ValueOrPromise = T | Promise; export type WithRequired = T & Required>; From b0948a826cc31bec79f0a54ea46fa2db884ac95b Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 20:37:15 +0300 Subject: [PATCH 64/98] fix: Preserve client-requested `operationName` on op. name resolution failure. Addresses feedback in below referenced [[Comment]]. If operation resolution (parsing and validating the document followed by selecting the correct operation) resulted in the population of the `operationName`, we'll use that. (For anonymous operations, `requestContext.operationName` is null, which we represent here as the empty string.) If the user explicitly specified an `operationName` in their request but operation resolution failed (due to parse or validation errors or because there is no operation with that name in the document), we still put _that_ user-supplied `operationName` in the trace. This allows the error to be better understood in Graph Manager. (We are considering changing the behavior of `operationName` in these three error cases; see [[#3465]] below for details.) [Comment]: https://github.com/apollographql/apollo-server/pull/3998#discussion_r422260422 [#3465]: https://github.com/apollographql/apollo-server/pull/3465 --- .../apollo-engine-reporting/src/plugin.ts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 36915c2466c..59a483dcd0a 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -94,14 +94,23 @@ export const plugin = ( treeBuilder.trace.forbiddenOperation = !!metrics.forbiddenOperation; treeBuilder.trace.registeredOperation = !!metrics.registeredOperation; - // If the user did not explicitly specify an operation name (which we - // would have saved in `executionDidStart`), but the request pipeline made - // it far enough to figure out what the operation name must be and store - // it on requestContext.operationName, use that name. (Note that this - // depends on the assumption that the RequestContext passed to - // requestDidStart, which does not yet have operationName, will be mutated - // to add operationName later.) - const operationName = requestContext.operationName || ''; + // If operation resolution (parsing and validating the document followed + // by selecting the correct operation) resulted in the population of the + // `operationName`, we'll use that. (For anonymous operations, + // `requestContext.operationName` is null, which we represent here as + // the empty string.) + // + // If the user explicitly specified an `operationName` in their request + // but operation resolution failed (due to parse or validation errors or + // because there is no operation with that name in the document), we + // still put _that_ user-supplied `operationName` in the trace. This + // allows the error to be better understood in Graph Manager. (We are + // considering changing the behavior of `operationName` in these three + // error cases; https://github.com/apollographql/apollo-server/pull/3465 + const operationName = + requestContext.operationName || + requestContext.request.operationName || + ''; // If this was a federated operation and we're the gateway, add the query plan // to the trace. From a926b7eedbb87abab2ec70fb03d71743985cb18d Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 18:08:36 +0000 Subject: [PATCH 65/98] Use an object, rather than positional params for `willResolveField`. This should prove to be more ergonomic, and also allow us to attach other useful properties to this object (for either external or internal use) in the future. Also, adds a test to make sure we're passing _something_! Ref: https://github.com/apollographql/apollo-server/pull/3998#discussion_r414900290 --- .../src/__tests__/runQuery.test.ts | 37 +++++++++++++++++++ .../src/utils/schemaInstrumentation.ts | 2 +- .../apollo-server-plugin-base/src/index.ts | 5 ++- packages/apollo-server-types/src/index.ts | 22 +++++++++++ 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index 228ea74f46d..53f31e64e17 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -691,6 +691,43 @@ describe('runQuery', () => { expect(executionDidEnd).toHaveBeenCalledTimes(1); }); + it('receives an object with the resolver arguments', async () => { + const willResolveField = jest.fn(); + const executionDidEnd = jest.fn(); + const executionDidStart = jest.fn( + (): GraphQLRequestExecutionListener => ({ + willResolveField, + executionDidEnd, + }), + ); + + await runQuery({ + schema, + context: { ourSpecialContext: true }, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart, + }; + }, + }, + ], + request: new MockReq(), + }); + + expect(executionDidStart).toHaveBeenCalledTimes(1); + expect(willResolveField).toHaveBeenCalledTimes(1); + expect(willResolveField).toHaveBeenNthCalledWith(1, { + source: undefined, + args: {}, + context: expect.objectContaining({ ourSpecialContext: true }), + info: expect.objectContaining({ fieldName: 'testString' }), + }); + expect(executionDidEnd).toHaveBeenCalledTimes(1); + }); + it('calls the end handler', async () => { const didResolveField: GraphQLRequestListenerDidResolveField = jest.fn(); diff --git a/packages/apollo-server-core/src/utils/schemaInstrumentation.ts b/packages/apollo-server-core/src/utils/schemaInstrumentation.ts index 41d3f72dd3c..119320b3de9 100644 --- a/packages/apollo-server-core/src/utils/schemaInstrumentation.ts +++ b/packages/apollo-server-core/src/utils/schemaInstrumentation.ts @@ -51,7 +51,7 @@ function wrapField(field: GraphQLField): void { // resolution is complete. const didResolveField = typeof willResolveField === 'function' && - willResolveField(source, args, context, info); + willResolveField({ source, args, context, info }); const resolveObject: GraphQLObjectResolver< any, diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts index 448890848a6..91fedfcd2ca 100644 --- a/packages/apollo-server-plugin-base/src/index.ts +++ b/packages/apollo-server-plugin-base/src/index.ts @@ -7,6 +7,7 @@ import { GraphQLResponse, ValueOrPromise, WithRequired, + GraphQLFieldResolverParams, GraphQLRequestContextDidResolveSource, GraphQLRequestContextParsingDidStart, GraphQLRequestContextValidationDidStart, @@ -16,7 +17,6 @@ import { GraphQLRequestContextExecutionDidStart, GraphQLRequestContextWillSendResponse, } from 'apollo-server-types'; -import { GraphQLFieldResolver } from "graphql"; // We re-export all of these so plugin authors only need to depend on a single // package. The overall concept of `apollo-server-types` and this package @@ -34,6 +34,7 @@ export { GraphQLResponse, ValueOrPromise, WithRequired, + GraphQLFieldResolverParams, GraphQLRequestContextDidResolveSource, GraphQLRequestContextParsingDidStart, GraphQLRequestContextValidationDidStart, @@ -102,6 +103,6 @@ export interface GraphQLRequestExecutionListener< > extends AnyFunctionMap { executionDidEnd?: GraphQLRequestListenerExecutionDidEnd; willResolveField?( - ...fieldResolverArgs: Parameters> + fieldResolverParams: GraphQLFieldResolverParams ): GraphQLRequestListenerDidResolveField | void; } diff --git a/packages/apollo-server-types/src/index.ts b/packages/apollo-server-types/src/index.ts index a4ef63ed921..9dc59c5ffac 100644 --- a/packages/apollo-server-types/src/index.ts +++ b/packages/apollo-server-types/src/index.ts @@ -7,6 +7,7 @@ import { OperationDefinitionNode, DocumentNode, GraphQLError, + GraphQLResolveInfo, } from 'graphql'; // This seems like it could live in this package too. @@ -147,6 +148,27 @@ export type Logger = { error(message?: any): void; } +/** + * This is an object form of the parameters received by typical + * `graphql-js` resolvers. The function type is `GraphQLFieldResolver` + * and normally uses positional parameters. In order to facilitate better + * ergonomics in the Apollo Server plugin API, these have been converted to + * named properties on the object using their names from the upstream + * `GraphQLFieldResolver` type signature. Ergonomic wins, in this case, + * include not needing to have three unused variables in scope just because + * there was a need to access the `info` property in a wrapped plugin. + */ +export type GraphQLFieldResolverParams< + TSource, + TContext, + TArgs = { [argName: string]: any } +> = { + source: TSource; + args: TArgs; + context: TContext; + info: GraphQLResolveInfo; +}; + export type GraphQLRequestContextDidResolveSource = WithRequired, | 'metrics' From b7ea44792af147a9fad6349537753ea9184788eb Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 18:12:36 +0000 Subject: [PATCH 66/98] Remove unreachable code after `callTargets` decomposition. The `callTargets` method was introduced in 4fa6ddd4f8d, but I forgot to remove the code it rendered unnecessary/aimed to replace. --- packages/apollo-server-core/src/utils/dispatcher.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/apollo-server-core/src/utils/dispatcher.ts b/packages/apollo-server-core/src/utils/dispatcher.ts index 02789591abf..0ea761700d3 100644 --- a/packages/apollo-server-core/src/utils/dispatcher.ts +++ b/packages/apollo-server-core/src/utils/dispatcher.ts @@ -29,14 +29,6 @@ export class Dispatcher { return await Promise.all( this.callTargets(this.targets, methodName, ...args), ); - return await Promise.all( - this.targets.map(target => { - const method = target[methodName]; - if (method && typeof method === 'function') { - return method.apply(target, args); - } - }), - ); } public invokeHookSync( From 6f0dee7871d8cf06fa24b4242bb03f318a0697a8 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 18:25:25 +0000 Subject: [PATCH 67/98] Switch to new `willResolveField` object parameter, rather position. Implements pattern gained by a926b7eedbb87abab2ec70fb03d7174398 in #3988. Ref: https://github.com/apollographql/apollo-server/pull/3998 --- packages/apollo-cache-control/src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/apollo-cache-control/src/index.ts b/packages/apollo-cache-control/src/index.ts index 5e2277c2e7c..53f8e4272fd 100644 --- a/packages/apollo-cache-control/src/index.ts +++ b/packages/apollo-cache-control/src/index.ts @@ -66,8 +66,7 @@ export const plugin = ( return { executionDidStart: () => ({ executionDidEnd: () => setOverallCachePolicyWhenUnset(), - willResolveField(...args) { - const [, , , info] = args; + willResolveField({ info }) { let hint: CacheHint = {}; // If this field's resolver returns an object or interface, look for From d85276f354b88f6bf8d26e73b7187cb22246d2da Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 18:27:38 +0000 Subject: [PATCH 68/98] Switch to new `willResolveField` object parameter, rather position. Implements pattern gained by a926b7eedbb87abab2ec70fb03d7174398 in #3988. Ref: https://github.com/apollographql/apollo-server/pull/3998 --- packages/apollo-tracing/src/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/apollo-tracing/src/index.ts b/packages/apollo-tracing/src/index.ts index 70c541ff7bb..f6ec8a06765 100644 --- a/packages/apollo-tracing/src/index.ts +++ b/packages/apollo-tracing/src/index.ts @@ -62,9 +62,7 @@ export const plugin = (_futureOptions = {}) => (): ApolloServerPlugin => ({ endWallTime = new Date(); }, - willResolveField(...args) { - const [, , , info] = args; - + willResolveField({ info }) { const resolverCall: ResolverCall = { path: info.path, fieldName: info.fieldName, From 62fc270641faa1d5a8b023768d1b2a7c7f2e4ece Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 8 May 2020 18:28:51 +0000 Subject: [PATCH 69/98] Switch to new `willResolveField` object parameter, rather position. Implements pattern gained by a926b7eedbb87abab2ec70fb03d7174398 in #3988. Ref: https://github.com/apollographql/apollo-server/pull/3998 --- packages/apollo-engine-reporting/src/federatedPlugin.ts | 3 +-- packages/apollo-engine-reporting/src/plugin.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/apollo-engine-reporting/src/federatedPlugin.ts b/packages/apollo-engine-reporting/src/federatedPlugin.ts index b5660f7183f..ce2a5580668 100644 --- a/packages/apollo-engine-reporting/src/federatedPlugin.ts +++ b/packages/apollo-engine-reporting/src/federatedPlugin.ts @@ -29,8 +29,7 @@ const federatedPlugin = ( return { executionDidStart: () => ({ - willResolveField(...args) { - const [ , , , info] = args; + willResolveField({ info }) { return treeBuilder.willResolveField(info); }, }), diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 59a483dcd0a..9c75aba803b 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -173,8 +173,7 @@ export const plugin = ( executionDidStart(requestContext) { return { executionDidEnd: () => didEnd(requestContext), - willResolveField(...args) { - const [, , , info] = args; + willResolveField({ info }) { return treeBuilder.willResolveField(info); // We could save the error into the trace during the end handler, but // it won't have all the information that graphql-js adds to it later, From c73cd163dc78ebfb25e46398339782c3a6d52801 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 11 May 2020 08:11:00 +0000 Subject: [PATCH 70/98] nit: Add missing closing paren on comment Ref: b0948a826cc31bec79f0a54ea46fa2db884ac95b --- packages/apollo-engine-reporting/src/plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 9c75aba803b..84ead380e19 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -105,8 +105,8 @@ export const plugin = ( // because there is no operation with that name in the document), we // still put _that_ user-supplied `operationName` in the trace. This // allows the error to be better understood in Graph Manager. (We are - // considering changing the behavior of `operationName` in these three - // error cases; https://github.com/apollographql/apollo-server/pull/3465 + // considering changing the behavior of `operationName` in these 3 error + // cases; https://github.com/apollographql/apollo-server/pull/3465) const operationName = requestContext.operationName || requestContext.request.operationName || From 41b103ebc730f2875355fc4c253c9077900ead61 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 11 May 2020 10:10:01 +0000 Subject: [PATCH 71/98] tests: Expand on tests for `willResolveField` parameters. Follows-up: a926b7eedbb87abab2ec70fb03d71743985cb18d Ref: https://github.com/apollographql/apollo-server/pull/3988#commitcomment-39044492 --- .../src/__tests__/runQuery.test.ts | 142 ++++++++++++++---- 1 file changed, 112 insertions(+), 30 deletions(-) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index 53f31e64e17..e6c4ec5124a 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -691,41 +691,123 @@ describe('runQuery', () => { expect(executionDidEnd).toHaveBeenCalledTimes(1); }); - it('receives an object with the resolver arguments', async () => { - const willResolveField = jest.fn(); - const executionDidEnd = jest.fn(); - const executionDidStart = jest.fn( - (): GraphQLRequestExecutionListener => ({ - willResolveField, - executionDidEnd, - }), - ); + describe('receives correct resolver parameter object', () => { + it('receives undefined parent when there is no parent', async () => { + const willResolveField = jest.fn(); - await runQuery({ - schema, - context: { ourSpecialContext: true }, - queryString: '{ testString }', - plugins: [ - { - requestDidStart() { - return { - executionDidStart, - }; + await runQuery({ + schema, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart: () => ({ + willResolveField, + }), + }; + }, }, - }, - ], - request: new MockReq(), + ], + request: new MockReq(), + }); + + // It is called only once. + expect(willResolveField).toHaveBeenCalledTimes(1); + const call = willResolveField.mock.calls[0]; + expect(call[0]).toHaveProperty("source", undefined); + expect(call[0]).toHaveProperty("info.path.key", "testString"); + expect(call[0]).toHaveProperty("info.path.prev", undefined); }); - expect(executionDidStart).toHaveBeenCalledTimes(1); - expect(willResolveField).toHaveBeenCalledTimes(1); - expect(willResolveField).toHaveBeenNthCalledWith(1, { - source: undefined, - args: {}, - context: expect.objectContaining({ ourSpecialContext: true }), - info: expect.objectContaining({ fieldName: 'testString' }), + it('receives the parent when there is one', async () => { + const willResolveField = jest.fn(); + + await runQuery({ + schema, + queryString: '{ testObject { testString } }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart: () => ({ + willResolveField, + }), + }; + }, + }, + ], + request: new MockReq(), + }); + + // It is called 1st for `testObject` and then 2nd for `testString`. + expect(willResolveField).toHaveBeenCalledTimes(2); + const [firstCall, secondCall] = willResolveField.mock.calls; + expect(firstCall[0]).toHaveProperty("source", undefined); + expect(firstCall[0]).toHaveProperty("info.path.key", "testObject"); + expect(firstCall[0]).toHaveProperty("info.path.prev", undefined); + + expect(secondCall[0]).toHaveProperty('source', { + testString: 'a very test string', + }); + expect(secondCall[0]).toHaveProperty("info.path.key", "testString"); + expect(secondCall[0]).toHaveProperty('info.path.prev', { + key: 'testObject', + prev: undefined, + }); + }); + + it('receives context', async () => { + const willResolveField = jest.fn(); + + await runQuery({ + schema, + context: { ourSpecialContext: true }, + queryString: '{ testString }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart: () => ({ + willResolveField, + }), + }; + }, + }, + ], + request: new MockReq(), + }); + + expect(willResolveField).toHaveBeenCalledTimes(1); + expect(willResolveField.mock.calls[0][0]).toHaveProperty("context", + expect.objectContaining({ ourSpecialContext: true }), + ); + }); + + it('receives arguments', async () => { + const willResolveField = jest.fn(); + + await runQuery({ + schema, + queryString: '{ testArgumentValue(base: 99) }', + plugins: [ + { + requestDidStart() { + return { + executionDidStart: () => ({ + willResolveField, + }), + }; + }, + }, + ], + request: new MockReq(), + }); + + expect(willResolveField).toHaveBeenCalledTimes(1); + expect(willResolveField.mock.calls[0][0]) + .toHaveProperty("args.base", 99); }); - expect(executionDidEnd).toHaveBeenCalledTimes(1); }); it('calls the end handler', async () => { From 6564081240ee884f3d6c5522ed17f1b480a9451c Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 11 May 2020 12:30:01 +0000 Subject: [PATCH 72/98] Attach user-defined `fieldResolver` to context. Previously, the "wrapped"-ness of the new plugin API's ability to invoke the user-defined `fieldResolver` which is provided in the `GraphQLServerOptions` interface was only able to invoke a user's own `fieldResolve` by leveraging an additional wrapping of fields by `graphql-extensions`'s own `wrapField` which was an inner-layer of the onion-style wrapping. Since it was closest to the resolver, it was able obtain a reference to the `fieldResolver` which lives on an instance of the `GraphQLExtensionStack` and properly invoke the user's `fieldResolver`. This also attaches the `fieldResolver` provided by the user on a `Symbol` that is attached to the user's `context`, so we can invoke it (when defined) within the new plugin API's `wrapField`'s `field.resolve` function. Ref: https://github.com/apollographql/apollo-server/pull/3988#discussion_r414676152 --- .../apollo-server-core/src/requestPipeline.ts | 13 +++++++++++ .../src/utils/schemaInstrumentation.ts | 23 +++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 074f7099528..eb287cb9516 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -21,6 +21,7 @@ import { PersistedQueryOptions } from './graphqlOptions'; import { symbolExecutionDispatcherWillResolveField, enablePluginsForSchemaResolvers, + symbolUserFieldResolver, } from "./utils/schemaInstrumentation" import { CacheControlExtension, @@ -405,6 +406,18 @@ export async function processGraphQLRequest( { value: invokeWillResolveField } ); + // If the user has provided a custom field resolver, we will attach + // it to the context so we can still invoke it after we've wrapped the + // fields with `wrapField` within `enablePluginsForSchemaResolvers` of + // the `schemaInstrumentation` module. + if (config.fieldResolver) { + Object.defineProperty( + requestContext.context, + symbolUserFieldResolver, + { value: config.fieldResolver } + ); + } + // If the schema is already enabled, this is a no-op. Otherwise, the // schema will be augmented so it is able to invoke willResolveField. enablePluginsForSchemaResolvers(config.schema); diff --git a/packages/apollo-server-core/src/utils/schemaInstrumentation.ts b/packages/apollo-server-core/src/utils/schemaInstrumentation.ts index 119320b3de9..d2393857e42 100644 --- a/packages/apollo-server-core/src/utils/schemaInstrumentation.ts +++ b/packages/apollo-server-core/src/utils/schemaInstrumentation.ts @@ -1,4 +1,11 @@ -import { GraphQLSchema, GraphQLField, ResponsePath, getNamedType, GraphQLObjectType } from "graphql/type"; +import { + GraphQLSchema, + GraphQLField, + ResponsePath, + getNamedType, + GraphQLObjectType, + GraphQLFieldResolver, +} from 'graphql/type'; import { defaultFieldResolver } from "graphql/execution"; import { FieldNode } from "graphql/language"; import { GraphQLRequestExecutionListener } from "apollo-server-plugin-base"; @@ -6,6 +13,8 @@ import { GraphQLObjectResolver } from "@apollographql/apollo-tools"; export const symbolExecutionDispatcherWillResolveField = Symbol("apolloServerExecutionDispatcherWillResolveField"); +export const symbolUserFieldResolver = + Symbol("apolloServerUserFieldResolver"); export const symbolPluginsEnabled = Symbol("apolloServerPluginsEnabled"); export function enablePluginsForSchemaResolvers( @@ -24,7 +33,7 @@ export function enablePluginsForSchemaResolvers( } function wrapField(field: GraphQLField): void { - const fieldResolver = field.resolve || defaultFieldResolver; + const originalFieldResolve = field.resolve; field.resolve = (source, args, context, info) => { // This is a bit of a hack, but since `ResponsePath` is a linked list, @@ -43,6 +52,13 @@ function wrapField(field: GraphQLField): void { | GraphQLRequestExecutionListener['willResolveField'] | undefined); + const userFieldResolver = + context && + context[symbolUserFieldResolver] && + (context[symbolUserFieldResolver] as + | GraphQLFieldResolver + | undefined); + // The technique for implementing a "did resolve field" is accomplished by // returning a function from the `willResolveField` handler. While there // may be several callbacks, depending on the number of plugins which have @@ -78,6 +94,9 @@ function wrapField(field: GraphQLField): void { } } + const fieldResolver = + originalFieldResolve || userFieldResolver || defaultFieldResolver; + try { let result: any; if (whenObjectResolved) { From f905eb1079873843844340c2d0eefa4cc7c49743 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 12 May 2020 09:21:23 +0000 Subject: [PATCH 73/98] Condense guards in `schemaInstrumentation`. Both through modern ECMAScript and removing superfluous lines. Ref: https://github.com/apollographql/apollo-server/commit/6564081240#r39093122 --- .../src/utils/schemaInstrumentation.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/apollo-server-core/src/utils/schemaInstrumentation.ts b/packages/apollo-server-core/src/utils/schemaInstrumentation.ts index d2393857e42..e20f94a9311 100644 --- a/packages/apollo-server-core/src/utils/schemaInstrumentation.ts +++ b/packages/apollo-server-core/src/utils/schemaInstrumentation.ts @@ -46,18 +46,14 @@ function wrapField(field: GraphQLField): void { }; const willResolveField = - context && - context[symbolExecutionDispatcherWillResolveField] && - (context[symbolExecutionDispatcherWillResolveField] as + context?.[symbolExecutionDispatcherWillResolveField] as | GraphQLRequestExecutionListener['willResolveField'] - | undefined); + | undefined; const userFieldResolver = - context && - context[symbolUserFieldResolver] && - (context[symbolUserFieldResolver] as + context?.[symbolUserFieldResolver] as | GraphQLFieldResolver - | undefined); + | undefined; // The technique for implementing a "did resolve field" is accomplished by // returning a function from the `willResolveField` handler. While there From db0e3780e4e2691ac675dcb7018bb4077c2de541 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 12 May 2020 09:36:03 +0000 Subject: [PATCH 74/98] types: Improve `Dispatcher`'s `callTargets` and `invokeHookAsync` types. This came up as a matter of discussion within the referenced [[Comment]] when reviewing #3988. The return value of `targets.map` was previously `any[]`'d which, we would hope, could be improved upon. I took a shot at improving it and struggled with the `UnwrapPromise`s when coupled with an expected `Promise` return of `invokeHookAsync`. Removing the unwrapping would seem to still get us the correct type (i.e. an array of the return value of the stingly-typed life-cycle hook, e.g. `wilSendResponse` or `didEncounterErrors`) at the invocation sites of `invokeHookAsync` within the request pipeline (e.g. [[Example]]), and seems to rid us of the `any[]` within the `Dispatcher`. Perhaps I'm missing something, but this would seem to work. I'm perhaps missing some unexpected conflict with the `ValueOrPromise` return types from the `GraphQLRequestListener` methods, but they all return `ValueOrPromise` right now any time that `invokeHookAsync` is called. [Comment]: https://github.com/apollographql/apollo-server/pull/3988/files#r421665333 [Example]: https://github.com/apollographql/apollo-server/blob/f905eb10/packages/apollo-server-core/src/requestPipeline.ts#L591-L594 --- packages/apollo-server-core/src/utils/dispatcher.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/apollo-server-core/src/utils/dispatcher.ts b/packages/apollo-server-core/src/utils/dispatcher.ts index 0ea761700d3..122fa9aa408 100644 --- a/packages/apollo-server-core/src/utils/dispatcher.ts +++ b/packages/apollo-server-core/src/utils/dispatcher.ts @@ -13,7 +13,7 @@ export class Dispatcher { targets: T[], methodName: TMethodName, ...args: Args - ) { + ): ReturnType>[] { return targets.map(target => { const method = target[methodName]; if (method && typeof method === 'function') { @@ -25,10 +25,9 @@ export class Dispatcher { public async invokeHookAsync( methodName: TMethodName, ...args: Args - ): Promise>>[]> { + ): Promise>[]> { return await Promise.all( - this.callTargets(this.targets, methodName, ...args), - ); + this.callTargets(this.targets, methodName, ...args)); } public invokeHookSync( From 52418a4f3732b004f581655d426d20df21c9e0be Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 12 May 2020 10:02:23 +0000 Subject: [PATCH 75/98] comment: Add note about typing question and reference to issue. Possibly fix-able in AS3. Ref: https://github.com/apollographql/apollo-server/pull/3988/files#r421632153 Ref: https://github.com/apollographql/apollo-server/issues/4103 --- packages/apollo-server-plugin-base/src/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/apollo-server-plugin-base/src/index.ts b/packages/apollo-server-plugin-base/src/index.ts index 91fedfcd2ca..a77d6556c16 100644 --- a/packages/apollo-server-plugin-base/src/index.ts +++ b/packages/apollo-server-plugin-base/src/index.ts @@ -45,6 +45,14 @@ export { GraphQLRequestContextWillSendResponse, }; +// Typings Note! (Fix in AS3?) +// +// There are a number of types in this module which are specifying `void` as +// their return type, despite the fact that we _are_ observing the value. +// It's possible those should instead be `undefined`. For more details, see +// the issue that was logged as a result of this discovery during (unrelated) PR +// review: https://github.com/apollographql/apollo-server/issues/4103 + export interface ApolloServerPlugin< TContext extends BaseContext = BaseContext > { From 18d95fd7f30bba196f7b0b62abdcff6bcb0eae18 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 12 May 2020 10:50:35 +0000 Subject: [PATCH 76/98] comment: Leave traces/suggestions about future work. Ref: https://github.com/apollographql/apollo-server/pull/3988#discussion_r414676152 --- packages/apollo-gateway/src/executeQueryPlan.ts | 8 +++----- packages/apollo-gateway/src/index.ts | 4 ++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/apollo-gateway/src/executeQueryPlan.ts b/packages/apollo-gateway/src/executeQueryPlan.ts index 72116c8e83d..2c9c3c43787 100644 --- a/packages/apollo-gateway/src/executeQueryPlan.ts +++ b/packages/apollo-gateway/src/executeQueryPlan.ts @@ -88,11 +88,9 @@ export async function executeQueryPlan( }, rootValue: data, variableValues: requestContext.request.variables, - // FIXME: GraphQL extensions currently wraps every field and creates - // a field resolver. Because of this, when using with ApolloServer - // the defaultFieldResolver isn't called. We keep this here - // because it is the correct solution and when ApolloServer removes - // GraphQLExtensions this will be how alias support is maintained + // We have a special field resolver which ensures we support aliases. + // FIXME: It's _possible_ this will change after `graphql-extensions` is + // deprecated, though not certain. See here, also: https://git.io/Jf8cS. fieldResolver: defaultFieldResolverWithAliasSupport, })); } catch (error) { diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 1daf6097d69..d74269f0299 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -476,6 +476,10 @@ export class ApolloGateway implements GraphQLService { this.logger.debug('Schema loaded and ready for execution'); + // FIXME: The comment below may change when `graphql-extensions` is + // removed, as it will be soon. It's not clear if this will be temporary, + // as is suggested, after that time, because we still very much need to + // do this special alias resolving. Original comment: // this is a temporary workaround for GraphQLFieldExtensions automatic // wrapping of all fields when using ApolloServer. Here we wrap all fields // with support for resolving aliases as part of the root value which From bfef89b8cc44d4bd94f9d5fcd3df8941de526360 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 13 May 2020 13:00:30 +0300 Subject: [PATCH 77/98] Release - apollo-cache-control@0.11.0-alpha.0 - apollo-datasource-rest@0.9.2-alpha.0 - apollo-engine-reporting@2.0.0-alpha.0 - @apollo/federation@0.16.1-alpha.0 - @apollo/gateway@0.16.1-alpha.0 - apollo-server-azure-functions@2.14.0-alpha.0 - apollo-server-cloud-functions@2.14.0-alpha.0 - apollo-server-cloudflare@2.14.0-alpha.0 - apollo-server-core@2.14.0-alpha.0 - apollo-server-express@2.14.0-alpha.0 - apollo-server-fastify@2.14.0-alpha.0 - apollo-server-hapi@2.14.0-alpha.0 - apollo-server-integration-testsuite@2.14.0-alpha.0 - apollo-server-koa@2.14.0-alpha.0 - apollo-server-lambda@2.14.0-alpha.0 - apollo-server-micro@2.14.0-alpha.0 - apollo-server-plugin-base@0.9.0-alpha.0 - apollo-server-plugin-response-cache@0.5.2-alpha.0 - apollo-server-testing@2.14.0-alpha.0 - apollo-server-types@0.5.0-alpha.0 - apollo-server@2.14.0-alpha.0 - apollo-tracing@0.11.0-alpha.0 - graphql-extensions@0.12.2-alpha.0 --- packages/apollo-cache-control/package.json | 2 +- packages/apollo-datasource-rest/package.json | 2 +- packages/apollo-engine-reporting/package.json | 6 +++--- packages/apollo-federation/package.json | 2 +- packages/apollo-gateway/package.json | 2 +- packages/apollo-server-azure-functions/package.json | 2 +- packages/apollo-server-cloud-functions/package.json | 2 +- packages/apollo-server-cloudflare/package.json | 2 +- packages/apollo-server-core/package.json | 2 +- packages/apollo-server-express/package.json | 2 +- packages/apollo-server-fastify/package.json | 2 +- packages/apollo-server-hapi/package.json | 2 +- packages/apollo-server-integration-testsuite/package.json | 2 +- packages/apollo-server-koa/package.json | 2 +- packages/apollo-server-lambda/package.json | 2 +- packages/apollo-server-micro/package.json | 2 +- packages/apollo-server-plugin-base/package.json | 2 +- packages/apollo-server-plugin-response-cache/package.json | 2 +- packages/apollo-server-testing/package.json | 2 +- packages/apollo-server-types/package.json | 2 +- packages/apollo-server/package.json | 2 +- packages/apollo-tracing/package.json | 2 +- packages/graphql-extensions/package.json | 2 +- 23 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/apollo-cache-control/package.json b/packages/apollo-cache-control/package.json index bd4bf8fb3c8..b67eefeb1f5 100644 --- a/packages/apollo-cache-control/package.json +++ b/packages/apollo-cache-control/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache-control", - "version": "0.10.1-alpha.0", + "version": "0.11.0-alpha.0", "description": "A GraphQL extension for cache control", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/apollo-datasource-rest/package.json b/packages/apollo-datasource-rest/package.json index 227a16649df..3bc114c0c97 100644 --- a/packages/apollo-datasource-rest/package.json +++ b/packages/apollo-datasource-rest/package.json @@ -1,6 +1,6 @@ { "name": "apollo-datasource-rest", - "version": "0.9.1-alpha.0", + "version": "0.9.2-alpha.0", "author": "opensource@apollographql.com", "license": "MIT", "repository": { diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index d8d3be3fc01..93259c7143f 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -1,6 +1,6 @@ { "name": "apollo-engine-reporting", - "version": "1.8.1-alpha.0", + "version": "2.0.0-alpha.0", "description": "Send reports about your GraphQL services to Apollo Graph Manager (previously known as Apollo Engine)", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -16,9 +16,9 @@ "apollo-server-caching": "file:../apollo-server-caching", "apollo-server-env": "file:../apollo-server-env", "apollo-server-errors": "file:../apollo-server-errors", + "apollo-server-plugin-base": "file:../apollo-server-plugin-base", "apollo-server-types": "file:../apollo-server-types", - "async-retry": "^1.2.1", - "apollo-server-plugin-base": "file:../apollo-server-plugin-base" + "async-retry": "^1.2.1" }, "peerDependencies": { "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" diff --git a/packages/apollo-federation/package.json b/packages/apollo-federation/package.json index 4017608c051..520e31a6d76 100644 --- a/packages/apollo-federation/package.json +++ b/packages/apollo-federation/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/federation", - "version": "0.16.0", + "version": "0.16.1-alpha.0", "description": "Apollo Federation Utilities", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-gateway/package.json b/packages/apollo-gateway/package.json index 85a18204116..f15746178ff 100644 --- a/packages/apollo-gateway/package.json +++ b/packages/apollo-gateway/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/gateway", - "version": "0.16.0", + "version": "0.16.1-alpha.0", "description": "Apollo Gateway", "author": "opensource@apollographql.com", "main": "dist/index.js", diff --git a/packages/apollo-server-azure-functions/package.json b/packages/apollo-server-azure-functions/package.json index eadf0e112d7..92b7e2dc889 100644 --- a/packages/apollo-server-azure-functions/package.json +++ b/packages/apollo-server-azure-functions/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-azure-functions", - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Production-ready Node.js GraphQL server for Azure Functions", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-cloud-functions/package.json b/packages/apollo-server-cloud-functions/package.json index 5a052be9758..512372a4f92 100644 --- a/packages/apollo-server-cloud-functions/package.json +++ b/packages/apollo-server-cloud-functions/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cloud-functions", - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Production-ready Node.js GraphQL server for Google Cloud Functions", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-cloudflare/package.json b/packages/apollo-server-cloudflare/package.json index 269af38d556..3aa62a4c99a 100644 --- a/packages/apollo-server-cloudflare/package.json +++ b/packages/apollo-server-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cloudflare", - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Production-ready Node.js GraphQL server for Cloudflare workers", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-core/package.json b/packages/apollo-server-core/package.json index 941fc28ffb3..e9455c0c9bb 100644 --- a/packages/apollo-server-core/package.json +++ b/packages/apollo-server-core/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-core", - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Core engine for Apollo GraphQL server", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-express/package.json b/packages/apollo-server-express/package.json index d7d902f7796..e05007504ca 100644 --- a/packages/apollo-server-express/package.json +++ b/packages/apollo-server-express/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-express", - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Production-ready Node.js GraphQL server for Express and Connect", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-fastify/package.json b/packages/apollo-server-fastify/package.json index dff9ad392ac..486b37b7f42 100644 --- a/packages/apollo-server-fastify/package.json +++ b/packages/apollo-server-fastify/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-fastify", - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Production-ready Node.js GraphQL server for Fastify", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-hapi/package.json b/packages/apollo-server-hapi/package.json index d5452701029..d3ea90a65ac 100644 --- a/packages/apollo-server-hapi/package.json +++ b/packages/apollo-server-hapi/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-hapi", - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Production-ready Node.js GraphQL server for Hapi", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-integration-testsuite/package.json b/packages/apollo-server-integration-testsuite/package.json index bd38bf50708..2209aa1e452 100644 --- a/packages/apollo-server-integration-testsuite/package.json +++ b/packages/apollo-server-integration-testsuite/package.json @@ -1,7 +1,7 @@ { "name": "apollo-server-integration-testsuite", "private": true, - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Apollo Server Integrations testsuite", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-koa/package.json b/packages/apollo-server-koa/package.json index 47eefed0f61..54d9afa6a36 100644 --- a/packages/apollo-server-koa/package.json +++ b/packages/apollo-server-koa/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-koa", - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Production-ready Node.js GraphQL server for Koa", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-lambda/package.json b/packages/apollo-server-lambda/package.json index f1280b4acf6..6ad05a03219 100644 --- a/packages/apollo-server-lambda/package.json +++ b/packages/apollo-server-lambda/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-lambda", - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Production-ready Node.js GraphQL server for AWS Lambda", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-micro/package.json b/packages/apollo-server-micro/package.json index 716036e45ac..ca221e1dcb0 100644 --- a/packages/apollo-server-micro/package.json +++ b/packages/apollo-server-micro/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-micro", - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Production-ready Node.js GraphQL server for Micro", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-plugin-base/package.json b/packages/apollo-server-plugin-base/package.json index ebb16d406bc..fa28b8d7560 100644 --- a/packages/apollo-server-plugin-base/package.json +++ b/packages/apollo-server-plugin-base/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-plugin-base", - "version": "0.8.1-alpha.0", + "version": "0.9.0-alpha.0", "description": "Apollo Server plugin base classes", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-plugin-response-cache/package.json b/packages/apollo-server-plugin-response-cache/package.json index bfcfd106111..698913fcbda 100644 --- a/packages/apollo-server-plugin-response-cache/package.json +++ b/packages/apollo-server-plugin-response-cache/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-plugin-response-cache", - "version": "0.5.1-alpha.0", + "version": "0.5.2-alpha.0", "description": "Apollo Server full query response cache", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-testing/package.json b/packages/apollo-server-testing/package.json index f597a7a05ce..b2048935b61 100644 --- a/packages/apollo-server-testing/package.json +++ b/packages/apollo-server-testing/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-testing", - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Test utils for apollo-server", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-types/package.json b/packages/apollo-server-types/package.json index 9008aab1402..881f449c6f8 100644 --- a/packages/apollo-server-types/package.json +++ b/packages/apollo-server-types/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-types", - "version": "0.4.1-alpha.0", + "version": "0.5.0-alpha.0", "description": "Apollo Server shared types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server/package.json b/packages/apollo-server/package.json index caa6121936e..c18a311839b 100644 --- a/packages/apollo-server/package.json +++ b/packages/apollo-server/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server", - "version": "2.13.1", + "version": "2.14.0-alpha.0", "description": "Production ready GraphQL Server", "author": "opensource@apollographql.com", "main": "dist/index.js", diff --git a/packages/apollo-tracing/package.json b/packages/apollo-tracing/package.json index a27bd152107..5de113417dd 100644 --- a/packages/apollo-tracing/package.json +++ b/packages/apollo-tracing/package.json @@ -1,6 +1,6 @@ { "name": "apollo-tracing", - "version": "0.10.1-alpha.0", + "version": "0.11.0-alpha.0", "description": "Collect and expose trace data for GraphQL requests", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/graphql-extensions/package.json b/packages/graphql-extensions/package.json index c914bf53c2a..14d98ed0e13 100644 --- a/packages/graphql-extensions/package.json +++ b/packages/graphql-extensions/package.json @@ -1,6 +1,6 @@ { "name": "graphql-extensions", - "version": "0.12.1-alpha.0", + "version": "0.12.2-alpha.0", "description": "Add extensions to GraphQL servers", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 8727e884d9721be92b455ba91f4d66bd5b4a20da Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 18 May 2020 13:30:55 +0000 Subject: [PATCH 78/98] no-op: Fix typo in test describe. --- packages/apollo-server-core/src/__tests__/runHttpQuery.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-server-core/src/__tests__/runHttpQuery.test.ts b/packages/apollo-server-core/src/__tests__/runHttpQuery.test.ts index 758e38c154f..e383dfbc334 100644 --- a/packages/apollo-server-core/src/__tests__/runHttpQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runHttpQuery.test.ts @@ -78,7 +78,7 @@ describe('runHttpQuery', () => { } }); - it('should not add no-cache headers if error is not a PersitedQuery error', () => { + it('should not add no-cache headers if error is not a PersistedQuery error', () => { try { throwHttpGraphQLError(200, [new ForbiddenError('401')]); } catch (err) { From 46ed16e024e7a32198db28ce3cce08c06cb2a9dd Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 18 May 2020 13:58:47 +0000 Subject: [PATCH 79/98] Merge duplicated imports from same module. --- packages/apollo-server-core/src/__tests__/runQuery.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index 04ea6078e16..b4849a92b0d 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -22,12 +22,12 @@ import { GraphQLOptions, Context as GraphQLContext } from 'apollo-server-core'; import { ApolloServerPlugin, GraphQLRequestExecutionListener, + GraphQLRequestListener, GraphQLRequestListenerDidResolveField, GraphQLRequestListenerExecutionDidEnd, GraphQLRequestListenerParsingDidEnd, GraphQLRequestListenerValidationDidEnd, } from 'apollo-server-plugin-base'; -import { GraphQLRequestListener } from 'apollo-server-plugin-base'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { generateSchemaHash } from "../utils/schemaHash"; From 736736e9b6c7c8e5406ce37e52f9496e82673705 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 18 May 2020 13:59:19 +0000 Subject: [PATCH 80/98] tests: Add ability to override request context with, e.g., `logger`. --- packages/apollo-server-core/src/__tests__/runQuery.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index b4849a92b0d..69f50590d3f 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -27,6 +27,7 @@ import { GraphQLRequestListenerExecutionDidEnd, GraphQLRequestListenerParsingDidEnd, GraphQLRequestListenerValidationDidEnd, + GraphQLRequestContext, } from 'apollo-server-plugin-base'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { generateSchemaHash } from "../utils/schemaHash"; @@ -36,7 +37,10 @@ import { generateSchemaHash } from "../utils/schemaHash"; // These tests will be rewritten as GraphQLRequestProcessor tests after the // refactoring is complete. -function runQuery(options: QueryOptions): Promise { +function runQuery( + options: QueryOptions, + requestContextExtra?: Partial, +): Promise { const request: GraphQLRequest = { query: options.queryString, operationName: options.operationName, @@ -56,6 +60,7 @@ function runQuery(options: QueryOptions): Promise { context: options.context || {}, debug: options.debug, cache: {} as any, + ...requestContextExtra, }); } From 7dd68b31b1be56789b60faed0edbfe9750e9d389 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 19 May 2020 10:53:02 +0300 Subject: [PATCH 81/98] Add deprecation warnings for `GraphQLExtension` usage. (#4135) --- .../src/__tests__/runQuery.test.ts | 83 +++++++++++++++++++ .../apollo-server-core/src/requestPipeline.ts | 46 ++++++++++ 2 files changed, 129 insertions(+) diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index 69f50590d3f..ec6d83477dc 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -31,6 +31,7 @@ import { } from 'apollo-server-plugin-base'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { generateSchemaHash } from "../utils/schemaHash"; +import { Logger } from "apollo-server-types"; // This is a temporary kludge to ensure we preserve runQuery behavior with the // GraphQLRequestProcessor refactoring. @@ -399,6 +400,88 @@ describe('runQuery', () => { } } + describe('deprecation warnings', () => { + const queryString = `{ testString }`; + async function runWithExtAndReturnLogger( + extensions: QueryOptions['extensions'], + ): Promise { + const logger = { + warn: jest.fn(() => {}), + info: console.info, + debug: console.debug, + error: console.error, + }; + + await runQuery( + { + schema, + queryString, + extensions, + request: new MockReq(), + }, + { + logger, + }, + ); + + return logger; + } + + it('warns about named extensions', async () => { + const logger = await runWithExtAndReturnLogger([ + () => new (class NamedExtension implements GraphQLExtension {})(), + ]); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringMatching(/^\[deprecated\] A "NamedExtension" was/)); + }); + + it('warns about anonymous extensions', async () => { + const logger = await runWithExtAndReturnLogger([ + () => new (class implements GraphQLExtension {})(), + ]); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringMatching(/^\[deprecated\] An anonymous extension was/)); + }); + + it('warns about anonymous class expressions', async () => { + // In other words, when the name is the name of the variable. + const anon = class implements GraphQLExtension {}; + const logger = await runWithExtAndReturnLogger([ + () => new anon(), + ]); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringMatching(/^\[deprecated\] A "anon" was/)); + }); + + it('warns for multiple extensions', async () => { + const logger = await runWithExtAndReturnLogger([ + () => new (class Name1Ext implements GraphQLExtension {})(), + () => new (class Name2Ext implements GraphQLExtension {})(), + ]); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringMatching(/^\[deprecated\] A "Name1Ext" was/)); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringMatching(/^\[deprecated\] A "Name2Ext" was/)); + }); + + it('warns only once', async () => { + // Will use the same extension across two invocations. + class NameExt implements GraphQLExtension {}; + + const logger1 = await runWithExtAndReturnLogger([ + () => new NameExt, + ]); + expect(logger1.warn).toHaveBeenCalledWith( + expect.stringMatching(/^\[deprecated\] A "NameExt" was/)); + + const logger2 = await runWithExtAndReturnLogger([ + () => new NameExt, + ]); + expect(logger2.warn).not.toHaveBeenCalledWith( + expect.stringMatching(/^\[deprecated\] A "NameExt" was/)); + }); + }); + it('creates the extension stack', async () => { const queryString = `{ testString }`; const extensions = [() => new CustomExtension()]; diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 09d4d4517fe..60962277b0c 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -113,6 +113,14 @@ export type DataSources = { type Mutable = { -readonly [P in keyof T]: T[P] }; +/** + * We attach this symbol to the constructor of extensions to mark that we've + * already warned about the deprecation of the `graphql-extensions` API for that + * particular definition. + */ +const symbolExtensionDeprecationDone = + Symbol("apolloServerExtensionDeprecationDone"); + export async function processGraphQLRequest( config: GraphQLRequestPipelineConfig, requestContext: Mutable>, @@ -634,6 +642,44 @@ export async function processGraphQLRequest( // objects. const extensions = config.extensions ? config.extensions.map(f => f()) : []; + // Warn about usage of (deprecated) `graphql-extensions` implementations. + // Since extensions are often provided as factory functions which + // instantiate an extension on each request, we'll attach a symbol to the + // constructor after we've warned to ensure that we don't do it on each + // request. Another option here might be to keep a `Map` of constructor + // instances within this module, but I hope this will do the trick. + const hasOwn = Object.prototype.hasOwnProperty; + extensions.forEach((extension) => { + // Using `hasOwn` just in case there is a user-land `hasOwnProperty` + // defined on the `constructor` object. + if ( + !extension.constructor || + hasOwn.call(extension.constructor, symbolExtensionDeprecationDone) + ) { + return; + } + + Object.defineProperty( + extension.constructor, + symbolExtensionDeprecationDone, + { value: true } + ); + + const extensionName = extension.constructor.name; + logger.warn( + '[deprecated] ' + + (extensionName + ? 'A "' + extensionName + '" ' + : 'An anonymous extension ') + + 'was defined within the "extensions" configuration for ' + + 'Apollo Server. The API on which this extension is built ' + + '("graphql-extensions") is being deprecated in the next major ' + + 'version of Apollo Server in favor of the new plugin API. See ' + + 'https://go.apollo.dev/s/plugins for the documentation on how ' + + 'these plugins are to be defined and used.', + ); + }); + return new GraphQLExtensionStack(extensions); } From 8b92145d51c3ba0ef8be5b041f9e28bc0ad6a1bc Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Tue, 19 May 2020 05:43:39 -0400 Subject: [PATCH 82/98] feat: Automatic schema reporting to Apollo Graph Manager (#4084) This commit implements the new schema reporting protocol for Apollo Server's reporting facilities and can be enabled by providing a Graph Manager API key available from Apollo Graph Manager in the `APOLLO_KEY` environment variable *and* setting the `experimental_schemaReporting` option to `true` in the Apollo Server constructor options, like so: ```js const server = new ApolloServer({ typeDefs, resolvers, engine: { experimental_schemaReporting: true, /* Other existing options can remain the same. */ }, }); ``` When schema-reporting is enabled, Apollo server will send the running schema and server information to Apollo Graph Manager. The extra runtime information will be used to power _auto-promotion_ which will set a schema as active for a _variant_, based on the algorithm described in the [Preview documentation]. When enabled, a schema reporting interval is initiated by the `apollo-engine-reporting` agent. It will loop until the `ApolloServer` instance is stopped, periodically calling back to Apollo Graph Manager to send information. The life-cycle of this loop is managed by the reporting agent. Additionally, this commit deprecates `schemaHash` in metrics reporting in favor of using the same `executableSchemaId` used within this new schema reporting protocol. [Preview documentation]: https://github.com/apollographql/apollo-schema-reporting-preview-docs#automatic-promotion-in-apollo-graph-manager --- package-lock.json | 16 +- package.json | 1 + .../src/reports.proto | 6 +- packages/apollo-engine-reporting/package.json | 3 +- .../src/__tests__/agent.test.ts | 57 +++- .../src/__tests__/plugin.test.ts | 176 ++++++++++++- .../src/__tests__/schemaReporter.test.ts | 173 +++++++++++++ packages/apollo-engine-reporting/src/agent.ts | 244 +++++++++++++++--- .../apollo-engine-reporting/src/plugin.ts | 40 ++- .../src/reportingOperationTypes.ts | 64 +++++ .../src/schemaReporter.ts | 190 ++++++++++++++ .../src/utils/pluginTestHarness.ts | 12 +- .../src/ApolloServer.ts | 4 +- 13 files changed, 920 insertions(+), 66 deletions(-) create mode 100644 packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts create mode 100644 packages/apollo-engine-reporting/src/reportingOperationTypes.ts create mode 100644 packages/apollo-engine-reporting/src/schemaReporter.ts diff --git a/package-lock.json b/package-lock.json index 646424c70fa..4dbefd14af0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5372,6 +5372,12 @@ "@types/node": "*" } }, + "@types/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-PUdqTZVrNYTNcIhLHkiaYzoOIaUi5LFg/XLerAdgvwQrUCx+oSbtoBze1AMyvYbcwzUSNC+Isl58SM4Sm/6COw==", + "dev": true + }, "@types/ws": { "version": "7.2.4", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.4.tgz", @@ -5588,7 +5594,15 @@ "apollo-server-errors": "file:packages/apollo-server-errors", "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base", "apollo-server-types": "file:packages/apollo-server-types", - "async-retry": "^1.2.1" + "async-retry": "^1.2.1", + "uuid": "^8.0.0" + }, + "dependencies": { + "uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" + } } }, "apollo-engine-reporting-protobuf": { diff --git a/package.json b/package.json index 3454ef8a065..30b9b00e3b8 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "@types/supertest": "^2.0.8", "@types/test-listen": "1.1.0", "@types/type-is": "1.6.3", + "@types/uuid": "^7.0.3", "@types/ws": "7.2.4", "apollo-fetch": "0.7.0", "apollo-link": "1.2.14", diff --git a/packages/apollo-engine-reporting-protobuf/src/reports.proto b/packages/apollo-engine-reporting-protobuf/src/reports.proto index 9f106f1c50b..0c6bc319d78 100644 --- a/packages/apollo-engine-reporting-protobuf/src/reports.proto +++ b/packages/apollo-engine-reporting-protobuf/src/reports.proto @@ -269,8 +269,10 @@ message ReportHeader { string uname = 9; // eg "current", "prod" string schema_tag = 10; - // The hex representation of the sha512 of the introspection response - string schema_hash = 11; + // An id that is used to represent the schema to Apollo Graph Manager + // Using this in place of what used to be schema_hash, since that is no longer + // attached to a schema in the backend. + string executable_schema_id = 11; reserved 3; // removed string service = 3; } diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index 93259c7143f..9ce287bb57f 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -18,7 +18,8 @@ "apollo-server-errors": "file:../apollo-server-errors", "apollo-server-plugin-base": "file:../apollo-server-plugin-base", "apollo-server-types": "file:../apollo-server-types", - "async-retry": "^1.2.1" + "async-retry": "^1.2.1", + "uuid": "^8.0.0" }, "peerDependencies": { "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" diff --git a/packages/apollo-engine-reporting/src/__tests__/agent.test.ts b/packages/apollo-engine-reporting/src/__tests__/agent.test.ts index ff6ac43ab6a..565cc5e50d5 100644 --- a/packages/apollo-engine-reporting/src/__tests__/agent.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/agent.test.ts @@ -2,7 +2,9 @@ import { signatureCacheKey, handleLegacyOptions, EngineReportingOptions, -} from '../agent'; + computeExecutableSchemaId +} from "../agent"; +import { buildSchema } from "graphql"; describe('signature cache key', () => { it('generates without the operationName', () => { @@ -16,6 +18,59 @@ describe('signature cache key', () => { }); }); +describe('Executable Schema Id', () => { + const unsortedGQLSchemaDocument = ` + directive @example on FIELD + union AccountOrUser = Account | User + type Query { + userOrAccount(name: String, id: String): AccountOrUser + } + + type User { + accounts: [Account!] + email: String + name: String! + } + + type Account { + name: String! + id: ID! + } + `; + + const sortedGQLSchemaDocument = ` + directive @example on FIELD + union AccountOrUser = Account | User + + type Account { + name: String! + id: ID! + } + + type Query { + userOrAccount(id: String, name: String): AccountOrUser + } + + type User { + accounts: [Account!] + email: String + name: String! + } + + `; + it('does not normalize GraphQL schemas', () => { + expect(computeExecutableSchemaId(buildSchema(unsortedGQLSchemaDocument))).not.toEqual( + computeExecutableSchemaId(buildSchema(sortedGQLSchemaDocument)) + ); + }); + it('does not normalize strings', () => { + expect(computeExecutableSchemaId(unsortedGQLSchemaDocument)).not.toEqual( + computeExecutableSchemaId(sortedGQLSchemaDocument) + ); + }); +}); + + describe("test handleLegacyOptions(), which converts the deprecated privateVariable and privateHeaders options to the new options' formats", () => { it('Case 1: privateVariables/privateHeaders == False; same as all', () => { const optionsPrivateFalse: EngineReportingOptions = { diff --git a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts index 2a8d3313b12..56c2809a3db 100644 --- a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts @@ -1,14 +1,13 @@ import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools'; -import { graphql, GraphQLError } from 'graphql'; +import { graphql, GraphQLError, printSchema } from 'graphql'; import { Request } from 'node-fetch'; import { makeTraceDetails, makeHTTPRequestHeaders, plugin } from '../plugin'; import { Headers } from 'apollo-server-env'; -import { AddTraceArgs } from '../agent'; +import { AddTraceArgs, computeExecutableSchemaId } from '../agent'; import { Trace } from 'apollo-engine-reporting-protobuf'; import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness'; -it('trace construction', async () => { - const typeDefs = ` +const typeDefs = ` type User { id: Int name: String @@ -31,7 +30,7 @@ it('trace construction', async () => { } `; - const query = ` +const query = ` query q { author(id: 5) { name @@ -43,6 +42,154 @@ it('trace construction', async () => { } `; +describe('schema reporting', () => { + const schema = makeExecutableSchema({ typeDefs }); + addMockFunctionsToSchema({ schema }); + + const addTrace = jest.fn(); + const startSchemaReporting = jest.fn(); + const executableSchemaIdGenerator = jest.fn(computeExecutableSchemaId); + + beforeEach(() => { + addTrace.mockClear(); + startSchemaReporting.mockClear(); + executableSchemaIdGenerator.mockClear(); + }); + + it('starts reporting if enabled', async () => { + const pluginInstance = plugin( + { + experimental_schemaReporting: true, + }, + addTrace, + { + startSchemaReporting, + executableSchemaIdGenerator, + }, + ); + + await pluginTestHarness({ + pluginInstance, + schema, + graphqlRequest: { + query, + operationName: 'q', + extensions: { + clientName: 'testing suite', + }, + http: new Request('http://localhost:123/foo'), + }, + executor: async ({ request: { query: source } }) => { + return await graphql({ + schema, + source, + }); + }, + }); + + expect(startSchemaReporting).toBeCalledTimes(1); + expect(startSchemaReporting).toBeCalledWith({ + executableSchema: printSchema(schema), + executableSchemaId: executableSchemaIdGenerator(schema), + }); + }); + + it('uses the override schema', async () => { + const pluginInstance = plugin( + { + experimental_schemaReporting: true, + experimental_overrideReportedSchema: typeDefs, + }, + addTrace, + { + startSchemaReporting, + executableSchemaIdGenerator, + }, + ); + + await pluginTestHarness({ + pluginInstance, + schema, + graphqlRequest: { + query, + operationName: 'q', + extensions: { + clientName: 'testing suite', + }, + http: new Request('http://localhost:123/foo'), + }, + executor: async ({ request: { query: source } }) => { + return await graphql({ + schema, + source, + }); + }, + }); + + const expectedExecutableSchemaId = executableSchemaIdGenerator(typeDefs); + expect(startSchemaReporting).toBeCalledTimes(1); + expect(startSchemaReporting).toBeCalledWith({ + executableSchema: typeDefs, + executableSchemaId: expectedExecutableSchemaId, + }); + + // Get the first argument from the first time this is called. + // Not using called with because that has to be exhaustive and this isn't + // testing trace generation + expect(addTrace).toBeCalledWith( + expect.objectContaining({ + executableSchemaId: expectedExecutableSchemaId, + }), + ); + }); + + it('uses the same executable schema id for metric reporting', async () => { + const pluginInstance = plugin( + { + experimental_schemaReporting: true, + }, + addTrace, + { + startSchemaReporting, + executableSchemaIdGenerator, + }, + ); + + await pluginTestHarness({ + pluginInstance, + schema, + graphqlRequest: { + query, + operationName: 'q', + extensions: { + clientName: 'testing suite', + }, + http: new Request('http://localhost:123/foo'), + }, + executor: async ({ request: { query: source } }) => { + return await graphql({ + schema, + source, + }); + }, + }); + + const expectedExecutableSchemaId = executableSchemaIdGenerator(schema); + expect(startSchemaReporting).toBeCalledTimes(1); + expect(startSchemaReporting).toBeCalledWith({ + executableSchema: printSchema(schema), + executableSchemaId: expectedExecutableSchemaId, + }); + // Get the first argument from the first time this is called. + // Not using called with because that has to be exhaustive and this isn't + // testing trace generation + expect(addTrace.mock.calls[0][0].executableSchemaId).toBe( + expectedExecutableSchemaId, + ); + }); +}); + +it('trace construction', async () => { const schema = makeExecutableSchema({ typeDefs }); addMockFunctionsToSchema({ schema }); @@ -50,10 +197,21 @@ it('trace construction', async () => { async function addTrace(args: AddTraceArgs) { traces.push(args); } + const startSchemaReporting = jest.fn(); + const executableSchemaIdGenerator = jest.fn(); - const pluginInstance = plugin({ /* no options!*/ }, addTrace); + const pluginInstance = plugin( + { + /* no options!*/ + }, + addTrace, + { + startSchemaReporting, + executableSchemaIdGenerator, + }, + ); - pluginTestHarness({ + await pluginTestHarness({ pluginInstance, schema, graphqlRequest: { @@ -64,7 +222,7 @@ it('trace construction', async () => { }, http: new Request('http://localhost:123/foo'), }, - executor: async ({ request: { query: source }}) => { + executor: async ({ request: { query: source } }) => { return await graphql({ schema, source, @@ -260,7 +418,7 @@ describe('variableJson output for sendVariableValues transform: custom function ).toEqual(JSON.stringify(null)); }); - const errorThrowingModifier = (input: { + const errorThrowingModifier = (_input: { variables: Record; }): Record => { throw new GraphQLError('testing error handling'); diff --git a/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts b/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts new file mode 100644 index 00000000000..44b317c7fd0 --- /dev/null +++ b/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts @@ -0,0 +1,173 @@ +import nock from 'nock'; +import { reportServerInfoGql, SchemaReporter } from '../schemaReporter'; + +function mockReporterRequest(url: any, variables?: any) { + if (variables) + return nock(url).post( + '/', + JSON.stringify({ + query: reportServerInfoGql, + operationName: 'ReportServerInfo', + variables, + }), + ); + return nock(url).post('/'); +} + +beforeEach(() => { + if (!nock.isActive()) nock.activate(); +}); + +afterEach(() => { + expect(nock.isDone()).toBeTruthy(); + nock.cleanAll(); + nock.restore(); +}); + +const serverInfo = { + bootId: 'string', + executableSchemaId: 'string', + graphVariant: 'string', +}; + +const url = 'http://localhost:4000'; + +describe('Schema reporter', () => { + it('return correct values if no errors', async () => { + const schemaReporter = new SchemaReporter( + serverInfo, + 'schemaSdl', + 'apiKey', + url, + ); + mockReporterRequest(url).reply(200, { + data: { + me: { + __typename: 'ServiceMutation', + reportServerInfo: { + __typename: 'ReportServerInfoResponse', + inSeconds: 30, + withExecutableSchema: false, + }, + }, + }, + }); + + let { + inSeconds, + withExecutableSchema, + } = await schemaReporter.reportServerInfo(false); + expect(inSeconds).toBe(30); + expect(withExecutableSchema).toBe(false); + + mockReporterRequest(url).reply(200, { + data: { + me: { + __typename: 'ServiceMutation', + reportServerInfo: { + __typename: 'ReportServerInfoResponse', + inSeconds: 60, + withExecutableSchema: true, + }, + }, + }, + }); + ({ + inSeconds, + withExecutableSchema, + } = await schemaReporter.reportServerInfo(false)); + expect(inSeconds).toBe(60); + expect(withExecutableSchema).toBe(true); + }); + + it('throws on 500 response', async () => { + const schemaReporter = new SchemaReporter( + serverInfo, + 'schemaSdl', + 'apiKey', + url, + ); + mockReporterRequest(url).reply(500, { + data: { + me: { + reportServerInfo: { + __typename: 'ReportServerInfoResponse', + inSeconds: 30, + withExecutableSchema: false, + }, + }, + }, + }); + + await expect( + schemaReporter.reportServerInfo(false), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"An unexpected HTTP status code (500) was encountered during schema reporting."`, + ); + }); + + it('throws on 200 malformed response', async () => { + const schemaReporter = new SchemaReporter( + serverInfo, + 'schemaSdl', + 'apiKey', + url, + ); + mockReporterRequest(url).reply(200, { + data: { + me: { + reportServerInfo: { + __typename: 'ReportServerInfoResponse', + }, + }, + }, + }); + + await expect( + schemaReporter.reportServerInfo(false), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unexpected response shape from Apollo Graph Manager when reporting server information for schema reporting. If this continues, please reach out to support@apollographql.com. Received response: {\\"me\\":{\\"reportServerInfo\\":{\\"__typename\\":\\"ReportServerInfoResponse\\"}}}"`, + ); + + mockReporterRequest(url).reply(200, { + data: { + me: { + __typename: 'UserMutation', + }, + }, + }); + await expect( + schemaReporter.reportServerInfo(false), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"This server was configured with an API key for a user. Only a service's API key may be used for schema reporting. Please visit the settings for this graph at https://engine.apollographql.com/ to obtain an API key for a service."`, + ); + }); + + it('sends schema if withExecutableSchema is true.', async () => { + const schemaReporter = new SchemaReporter( + serverInfo, + 'schemaSdl', + 'apiKey', + url, + ); + + const variables = { + info: serverInfo, + executableSchema: 'schemaSdl' + }; + mockReporterRequest(url, variables).reply(200, { + data: { + me: { + __typename: 'ServiceMutation', + reportServerInfo: { + __typename: 'ReportServerInfoResponse', + inSeconds: 30, + withExecutableSchema: false, + }, + }, + }, + }); + + await schemaReporter.reportServerInfo(true); + }); +}); diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index c7e6a28d083..bd3a13b327f 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -1,21 +1,29 @@ import os from 'os'; import { gzip } from 'zlib'; -import { DocumentNode, GraphQLError } from 'graphql'; +import { + DocumentNode, + GraphQLError, + GraphQLSchema, + printSchema, +} from 'graphql'; import { ReportHeader, Trace, Report, - TracesAndStats + TracesAndStats, } from 'apollo-engine-reporting-protobuf'; import { fetch, RequestAgent, Response } from 'apollo-server-env'; import retry from 'async-retry'; import { plugin } from './plugin'; -import { GraphQLRequestContext, Logger, SchemaHash } from 'apollo-server-types'; +import { GraphQLRequestContext, Logger } from 'apollo-server-types'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { defaultEngineReportingSignature } from 'apollo-graphql'; -import { ApolloServerPlugin } from "apollo-server-plugin-base"; +import { ApolloServerPlugin } from 'apollo-server-plugin-base'; +import { reportingLoop, SchemaReporter } from './schemaReporter'; +import { v4 as uuidv4 } from 'uuid'; +import { createHash } from 'crypto'; let warnedOnDeprecatedApiKey = false; @@ -120,9 +128,15 @@ export interface EngineReportingOptions { */ maxUncompressedReportSize?: number; /** + * [DEPRECATED] this option was replaced by tracesEndpointUrl * The URL of the Engine report ingress server. */ endpointUrl?: string; + /** + * The URL to the Apollo Graph Manager ingress endpoint. + * (Previously, this was `endpointUrl`, which will be removed in AS3). + */ + tracesEndpointUrl?: string; /** * If set, prints all reports as JSON when they are sent. */ @@ -239,6 +253,47 @@ export interface EngineReportingOptions { */ generateClientInfo?: GenerateClientInfo; + /** + * **(Experimental)** Enable schema reporting from this server with + * Apollo Graph Manager. + * + * The use of this option avoids the need to rgister schemas manually within + * CI deployment pipelines using `apollo schema:push` by periodically + * reporting this server's schema (when changes are detected) along with + * additional details about its runtime environment to Apollo Graph Manager. + * + * See [our _preview + * documentation_](https://github.com/apollographql/apollo-schema-reporting-preview-docs) + * for more information. + */ + experimental_schemaReporting?: boolean; + + /** + * Override the reported schema that is reported to AGM. + * This schema does not go through any normalizations and the string is directly sent to Apollo Graph Manager. + * This would be useful for comments or other ordering and whitespace changes that get stripped when generating a `GraphQLSchema` + */ + experimental_overrideReportedSchema?: string; + + /** + * The schema reporter waits before starting reporting. + * By default, the report waits some random amount of time between 0 and 10 seconds. + * A longer interval leads to more staggered starts which means it is less likely + * multiple servers will get asked to upload the same schema. + * + * If this server runs in lambda or in other constrained environments it would be useful + * to decrease the schema reporting max wait time to be less than default. + * + * This number will be the max for the range in ms that the schema reporter will + * wait before starting to report. + */ + experimental_schemaReportingInitialDelayMaxMs?: number; + + /** + * The URL to use for reporting schemas. + */ + schemaReportingUrl?: string; + /** * A logger interface to be used for output and errors. When not provided * it will default to the server's own `logger` implementation and use @@ -251,7 +306,7 @@ export interface AddTraceArgs { trace: Trace; operationName: string; queryHash: string; - schemaHash: SchemaHash; + executableSchemaId: string; source?: string; document?: DocumentNode; } @@ -270,27 +325,42 @@ const serviceHeaderDefaults = { export class EngineReportingAgent { private readonly options: EngineReportingOptions; private readonly apiKey: string; - private logger: Logger = console; - private graphVariant: string; - private reports: { [schemaHash: string]: Report } = Object.create( + private readonly logger: Logger = console; + private readonly graphVariant: string; + + private reports: { [executableSchemaId: string]: Report } = Object.create( + null, + ); + private reportSizes: { [executableSchemaId: string]: number } = Object.create( null, ); - private reportSizes: { [schemaHash: string]: number } = Object.create(null); + private reportTimer: any; // timer typing is weird and node-specific private readonly sendReportsImmediately?: boolean; private stopped: boolean = false; - private reportHeaders: { [schemaHash: string]: ReportHeader } = Object.create( - null, - ); + private reportHeaders: { + [executableSchemaId: string]: ReportHeader; + } = Object.create(null); private signatureCache: InMemoryLRUCache; private signalHandlers = new Map(); + private currentSchemaReporter?: SchemaReporter; + private readonly bootId: string; + private lastSeenExecutableSchemaToId?: { + executableSchema: string | GraphQLSchema; + executableSchemaId: string; + }; + + private readonly tracesEndpointUrl: string; + public constructor(options: EngineReportingOptions = {}) { 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.`, @@ -325,12 +395,41 @@ export class EngineReportingAgent { }); } + if (this.options.endpointUrl) { + this.logger.warn( + '[deprecated] The `endpointUrl` option within `engine` has been renamed to `tracesEndpointUrl`.', + ); + } + this.tracesEndpointUrl = + (this.options.endpointUrl || + this.options.tracesEndpointUrl || + 'https://engine-report.apollodata.com') + '/api/ingress/traces'; + // Handle the legacy options: privateVariables and privateHeaders handleLegacyOptions(this.options); } + private executableSchemaIdGenerator(schema: string | GraphQLSchema) { + if (this.lastSeenExecutableSchemaToId?.executableSchema === schema) { + return this.lastSeenExecutableSchemaToId.executableSchemaId; + } + const id = computeExecutableSchemaId(schema); + + // We override this variable every time we get a new schema so we cache + // the last seen value. It mostly a cached pair. + this.lastSeenExecutableSchemaToId = { + executableSchema: schema, + executableSchemaId: id, + }; + + return id; + } + public newPlugin(): ApolloServerPlugin { - return plugin(this.options, this.addTrace.bind(this)); + return plugin(this.options, this.addTrace.bind(this), { + startSchemaReporting: this.startSchemaReporting.bind(this), + executableSchemaIdGenerator: this.executableSchemaIdGenerator.bind(this), + }); } public async addTrace({ @@ -339,23 +438,23 @@ export class EngineReportingAgent { document, operationName, source, - schemaHash, + executableSchemaId, }: AddTraceArgs): Promise { // Ignore traces that come in after stop(). if (this.stopped) { return; } - if (!(schemaHash in this.reports)) { - this.reportHeaders[schemaHash] = new ReportHeader({ + if (!(executableSchemaId in this.reports)) { + this.reportHeaders[executableSchemaId] = new ReportHeader({ ...serviceHeaderDefaults, - schemaHash, + executableSchemaId: executableSchemaId, schemaTag: this.graphVariant, }); // initializes this.reports[reportHash] - this.resetReport(schemaHash); + this.resetReport(executableSchemaId); } - const report = this.reports[schemaHash]; + const report = this.reports[executableSchemaId]; const protobufError = Trace.verify(trace); if (protobufError) { @@ -380,28 +479,26 @@ export class EngineReportingAgent { (report.tracesPerQuery[statsReportKey] as any).encodedTraces.push( encodedTrace, ); - this.reportSizes[schemaHash] += + this.reportSizes[executableSchemaId] += encodedTrace.length + Buffer.byteLength(statsReportKey); // If the buffer gets big (according to our estimate), send. if ( this.sendReportsImmediately || - this.reportSizes[schemaHash] >= + this.reportSizes[executableSchemaId] >= (this.options.maxUncompressedReportSize || 4 * 1024 * 1024) ) { - await this.sendReportAndReportErrors(schemaHash); + await this.sendReportAndReportErrors(executableSchemaId); } } public async sendAllReports(): Promise { - await Promise.all( - Object.keys(this.reports).map(hash => this.sendReport(hash)), - ); + await Promise.all(Object.keys(this.reports).map(id => this.sendReport(id))); } - public async sendReport(schemaHash: string): Promise { - const report = this.reports[schemaHash]; - this.resetReport(schemaHash); + public async sendReport(executableSchemaId: string): Promise { + const report = this.reports[executableSchemaId]; + this.resetReport(executableSchemaId); if (Object.keys(report.tracesPerQuery).length === 0) { return; @@ -449,16 +546,12 @@ export class EngineReportingAgent { }); }); - const endpointUrl = - (this.options.endpointUrl || 'https://engine-report.apollodata.com') + - '/api/ingress/traces'; - // Wrap fetch with async-retry for automatic retrying const response: Response = await retry( // Retry on network errors and 5xx HTTP // responses. async () => { - const curResponse = await fetch(endpointUrl, { + const curResponse = await fetch(this.tracesEndpointUrl, { method: 'POST', headers: { 'user-agent': 'apollo-engine-reporting', @@ -511,6 +604,59 @@ export class EngineReportingAgent { } } + public startSchemaReporting({ + executableSchemaId, + executableSchema, + }: { + executableSchemaId: string; + executableSchema: string; + }) { + if (this.currentSchemaReporter) { + this.currentSchemaReporter.stop(); + } + + const serverInfo = { + bootId: this.bootId, + graphVariant: this.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', + runtimeVersion: `node ${process.version}`, + executableSchemaId: executableSchemaId, + // An identifier used to distinguish the version of the server code such as git or docker sha. + // Length must be <= 256 charecters + userVersion: process.env.APOLLO_SERVER_USER_VERSION, + // "An identifier for the server instance. Length must be <= 256 characters. + serverId: + process.env.APOLLO_SERVER_ID || process.env.HOSTNAME || os.hostname(), + libraryVersion: `apollo-engine-reporting@${ + require('../package.json').version + }`, + }; + + // Jitter the startup between 0 and 10 seconds + const delay = Math.floor( + Math.random() * + (this.options.experimental_schemaReportingInitialDelayMaxMs || 10_000), + ); + + const schemaReporter = new SchemaReporter( + serverInfo, + executableSchema, + this.apiKey, + this.options.schemaReportingUrl, + ); + + const fallbackReportingDelayInMs = 20_000; + + this.currentSchemaReporter = schemaReporter; + const logger = this.logger; + + setTimeout(function() { + reportingLoop(schemaReporter, logger, false, fallbackReportingDelayInMs); + }, delay); + } + // Stop prevents reports from being sent automatically due to time or buffer // size, and stop buffering new traces. You may still manually send a last // report by calling sendReport(). @@ -525,6 +671,10 @@ export class EngineReportingAgent { this.reportTimer = undefined; } + if (this.currentSchemaReporter) { + this.currentSchemaReporter.stop(); + } + this.stopped = true; } @@ -579,14 +729,14 @@ export class EngineReportingAgent { private async sendAllReportsAndReportErrors(): Promise { await Promise.all( - Object.keys(this.reports).map(schemaHash => - this.sendReportAndReportErrors(schemaHash), + Object.keys(this.reports).map(executableSchemaId => + this.sendReportAndReportErrors(executableSchemaId), ), ); } - private sendReportAndReportErrors(schemaHash: string): Promise { - return this.sendReport(schemaHash).catch(err => { + private sendReportAndReportErrors(executableSchemaId: string): Promise { + 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. @@ -598,11 +748,11 @@ export class EngineReportingAgent { }); } - private resetReport(schemaHash: string) { - this.reports[schemaHash] = new Report({ - header: this.reportHeaders[schemaHash], + private resetReport(executableSchemaId: string) { + this.reports[executableSchemaId] = new Report({ + header: this.reportHeaders[executableSchemaId], }); - this.reportSizes[schemaHash] = 0; + this.reportSizes[executableSchemaId] = 0; } } @@ -713,3 +863,15 @@ function makeSendValuesBaseOptionsFromLegacy( ? { none: true } : { all: true }; } + +export function computeExecutableSchemaId( + schema: string | GraphQLSchema, +): string { + // Can't call digest on this object twice. Creating new object each function call + const sha256 = createHash('sha256'); + const schemaDocument = + typeof schema === 'string' + ? schema + : printSchema(schema); + return sha256.update(schemaDocument).digest('hex'); +} diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 84ead380e19..4b8217813f6 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -5,17 +5,18 @@ import { GraphQLRequestContextDidEncounterErrors, } from 'apollo-server-types'; import { Headers } from 'apollo-server-env'; +import { GraphQLSchema, printSchema } from 'graphql'; import { Trace } from 'apollo-engine-reporting-protobuf'; import { + AddTraceArgs, EngineReportingOptions, GenerateClientInfo, - AddTraceArgs, - VariableValueOptions, SendValuesBaseOptions, + VariableValueOptions, } from './agent'; import { EngineReportingTreeBuilder } from './treeBuilder'; -import { ApolloServerPlugin } from "apollo-server-plugin-base"; +import { ApolloServerPlugin } from 'apollo-server-plugin-base'; const clientNameHeaderKey = 'apollographql-client-name'; const clientReferenceIdHeaderKey = 'apollographql-client-reference-id'; @@ -30,18 +31,39 @@ const clientVersionHeaderKey = 'apollographql-client-version'; export const plugin = ( options: EngineReportingOptions = Object.create(null), addTrace: (args: AddTraceArgs) => Promise, - // schemaHash: string, + { + startSchemaReporting, + executableSchemaIdGenerator, + }: { + startSchemaReporting: ({ + executableSchema, + executableSchemaId, + }: { + executableSchema: string; + executableSchemaId: string; + }) => void; + executableSchemaIdGenerator: (schema: string | GraphQLSchema) => string; + }, ): ApolloServerPlugin => { const logger: Logger = options.logger || console; const generateClientInfo: GenerateClientInfo = options.generateClientInfo || defaultGenerateClientInfo; - return { + serverWillStart: function({ schema }) { + if (!options.experimental_schemaReporting) return; + startSchemaReporting({ + executableSchema: + options.experimental_overrideReportedSchema || printSchema(schema), + executableSchemaId: executableSchemaIdGenerator( + options.experimental_overrideReportedSchema || schema, + ), + }); + }, requestDidStart({ logger: requestLogger, - schemaHash, metrics, + schema, request: { http, variables }, }) { const treeBuilder: EngineReportingTreeBuilder = new EngineReportingTreeBuilder( @@ -124,7 +146,9 @@ export const plugin = ( document: requestContext.document, source: requestContext.source, trace: treeBuilder.trace, - schemaHash, + executableSchemaId: executableSchemaIdGenerator( + options.experimental_overrideReportedSchema || schema, + ), }); } @@ -190,7 +214,7 @@ export const plugin = ( didEnd(requestContext); }, }; - } + }, }; }; diff --git a/packages/apollo-engine-reporting/src/reportingOperationTypes.ts b/packages/apollo-engine-reporting/src/reportingOperationTypes.ts new file mode 100644 index 00000000000..1bf7b182d51 --- /dev/null +++ b/packages/apollo-engine-reporting/src/reportingOperationTypes.ts @@ -0,0 +1,64 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: AutoregReportServerInfo +// ==================================================== + +import { GraphQLFormattedError } from 'graphql'; + +export interface ReportServerInfo_me_UserMutation { + __typename: 'UserMutation'; +} + +export interface ReportServerInfo_me_ServiceMutation_reportServerInfo { + __typename: 'ReportServerInfoResponse'; + inSeconds: number; + withExecutableSchema: boolean; +} + +export interface ReportServerInfo_me_ServiceMutation { + __typename: 'ServiceMutation'; + /** + * Schema auto-registration. Private alpha. + */ + reportServerInfo: ReportServerInfo_me_ServiceMutation_reportServerInfo | null; +} + +export type ReportServerInfo_me = + | ReportServerInfo_me_UserMutation + | ReportServerInfo_me_ServiceMutation; + +export interface SchemaReportingServerInfo { + me: ReportServerInfo_me | null; +} + +export interface SchemaReportingServerInfoResult { + data?: SchemaReportingServerInfo; + errors?: ReadonlyArray; +} + +export interface ReportServerInfoVariables { + info: EdgeServerInfo; + executableSchema?: string | null; +} + +/** + * Edge server info + */ +export interface EdgeServerInfo { + bootId: string; + executableSchemaId: string; + graphVariant: string; + libraryVersion?: string | null; + platform?: string | null; + runtimeVersion?: string | null; + serverId?: string | null; + userVersion?: string | null; +} + +//============================================================== +// END Enums and Input Objects +//============================================================== diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts new file mode 100644 index 00000000000..5dae15d7b61 --- /dev/null +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -0,0 +1,190 @@ +import { + ReportServerInfoVariables, + EdgeServerInfo, + SchemaReportingServerInfoResult, +} from './reportingOperationTypes'; +import { fetch, Headers, Request } from 'apollo-server-env'; +import { GraphQLRequest, Logger } from 'apollo-server-types'; + +export const reportServerInfoGql = ` + mutation ReportServerInfo($info: EdgeServerInfo!, $executableSchema: String) { + me { + __typename + ... on ServiceMutation { + reportServerInfo(info: $info, executableSchema: $executableSchema) { + inSeconds + withExecutableSchema + } + } + } + } +`; + +export function reportingLoop( + schemaReporter: SchemaReporter, + logger: Logger, + sendNextWithExecutableSchema: boolean, + fallbackReportingDelayInMs: number, +) { + function inner() { + // Bail out permanently + if (schemaReporter.stopped()) return; + + // Not awaiting this. The callback is handled in the `then` and it calls inner() + // to report the server info in however many seconds we were told to wait from + // Apollo Graph Manager + schemaReporter + .reportServerInfo(sendNextWithExecutableSchema) + .then(({ inSeconds, withExecutableSchema }) => { + sendNextWithExecutableSchema = withExecutableSchema; + setTimeout(inner, inSeconds * 1000); + }) + .catch((error: any) => { + // In the case of an error we want to continue looping + // We can add hardcoded backoff in the future, + // or on repeated failures stop responding reporting. + logger.error( + `Error reporting server info to Apollo Graph Manager during schema reporting: ${error}`, + ); + sendNextWithExecutableSchema = false; + setTimeout(inner, fallbackReportingDelayInMs); + }); + } + + inner(); +} + +interface ReportServerInfoReturnVal { + inSeconds: number; + withExecutableSchema: boolean; +} + +// This class is meant to be a thin shim around the gql mutations. +export class SchemaReporter { + // These mirror the gql variables + private readonly serverInfo: EdgeServerInfo; + private readonly executableSchemaDocument: any; + private readonly url: string; + + private isStopped: boolean; + private readonly headers: Headers; + + constructor( + serverInfo: EdgeServerInfo, + schemaSdl: string, + apiKey: string, + schemaReportingEndpoint: string | undefined, + ) { + this.headers = new Headers(); + this.headers.set('Content-Type', 'application/json'); + this.headers.set('x-api-key', apiKey); + this.headers.set('apollographql-client-name', 'apollo-engine-reporting'); + this.headers.set( + 'apollographql-client-version', + require('../package.json').version, + ); + + this.url = + schemaReportingEndpoint || + 'https://engine-graphql.apollographql.com/api/graphql'; + + this.serverInfo = serverInfo; + this.executableSchemaDocument = schemaSdl; + this.isStopped = false; + } + + public stopped(): Boolean { + return this.isStopped; + } + + public stop() { + this.isStopped = true; + } + + public async reportServerInfo( + withExecutableSchema: boolean, + ): Promise { + const { data, errors } = await this.graphManagerQuery({ + info: this.serverInfo, + executableSchema: withExecutableSchema + ? this.executableSchemaDocument + : null, + }); + + if (errors) { + throw new Error((errors || []).map((x: any) => x.message).join('\n')); + } + + function msgForUnexpectedResponse(data: any): string { + return [ + 'Unexpected response shape from Apollo Graph Manager when', + 'reporting server information for schema reporting. If', + 'this continues, please reach out to support@apollographql.com.', + 'Received response:', + JSON.stringify(data), + ].join(' '); + } + + if (!data || !data.me || !data.me.__typename) { + throw new Error(msgForUnexpectedResponse(data)); + } + + if (data.me.__typename === 'UserMutation') { + this.isStopped = true; + throw new Error( + [ + 'This server was configured with an API key for a user.', + "Only a service's API key may be used for schema reporting.", + 'Please visit the settings for this graph at', + 'https://engine.apollographql.com/ to obtain an API key for a service.', + ].join(' '), + ); + } else if (data.me.__typename === 'ServiceMutation') { + if (!data.me.reportServerInfo) { + throw new Error(msgForUnexpectedResponse(data)); + } + return data.me.reportServerInfo; + } else { + throw new Error(msgForUnexpectedResponse(data)); + } + } + + private async graphManagerQuery( + variables: ReportServerInfoVariables, + ): Promise { + const request: GraphQLRequest = { + query: reportServerInfoGql, + operationName: 'ReportServerInfo', + variables: variables, + }; + const httpRequest = new Request(this.url, { + method: 'POST', + headers: this.headers, + body: JSON.stringify(request), + }); + + const httpResponse = await fetch(httpRequest); + + if (!httpResponse.ok) { + throw new Error([ + `An unexpected HTTP status code (${httpResponse.status}) was`, + 'encountered during schema reporting.' + ].join(' ')); + } + + try { + // JSON parsing failure due to malformed data is the likely failure case + // here. Any non-JSON response (e.g. HTML) is usually the suspect. + return await httpResponse.json(); + } catch (error) { + throw new Error( + [ + "Couldn't report server info to Apollo Graph Manager.", + 'Parsing response as JSON failed.', + 'If this continues please reach out to support@apollographql.com', + error + ].join(' '), + ); + } + } +} diff --git a/packages/apollo-server-core/src/utils/pluginTestHarness.ts b/packages/apollo-server-core/src/utils/pluginTestHarness.ts index 5c790c70f6c..86dd63460c9 100644 --- a/packages/apollo-server-core/src/utils/pluginTestHarness.ts +++ b/packages/apollo-server-core/src/utils/pluginTestHarness.ts @@ -81,7 +81,6 @@ export default async function pluginTestHarness({ */ context?: TContext; }): Promise> { - if (!schema) { schema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -98,6 +97,17 @@ export default async function pluginTestHarness({ }); } + const schemaHash = generateSchemaHash(schema); + if (typeof pluginInstance.serverWillStart === 'function') { + pluginInstance.serverWillStart({ + logger: logger || console, + schema, + schemaHash, + engine: {}, + }); + } + + const requestContext: GraphQLRequestContext = { logger: logger || console, schema, diff --git a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts index 521c52bcabd..85257054458 100644 --- a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts +++ b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts @@ -852,7 +852,7 @@ export function testApolloServer( public engineOptions(): Partial> { return { - endpointUrl: this.getUrl(), + tracesEndpointUrl: this.getUrl(), }; } @@ -2170,7 +2170,7 @@ export function testApolloServer( resolvers: { Query: { something: () => 'hello' } }, engine: { apiKey: 'service:my-app:secret', - endpointUrl: fakeEngineUrl, + tracesEndpointUrl: fakeEngineUrl, reportIntervalMs: 1, maxAttempts: 3, requestAgent, From 6b8a4366363c27a8d828e733edef4d06ecc1cd55 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 19 May 2020 12:47:19 +0300 Subject: [PATCH 83/98] Release - apollo-cache-control@0.11.0-alpha.1 - apollo-datasource-rest@0.9.2-alpha.1 - apollo-engine-reporting-protobuf@0.5.1-alpha.1 - apollo-engine-reporting@2.0.0-alpha.1 - @apollo/gateway@0.16.1-alpha.1 - apollo-server-azure-functions@2.14.0-alpha.1 - apollo-server-cloud-functions@2.14.0-alpha.1 - apollo-server-cloudflare@2.14.0-alpha.1 - apollo-server-core@2.14.0-alpha.1 - apollo-server-express@2.14.0-alpha.1 - apollo-server-fastify@2.14.0-alpha.1 - apollo-server-hapi@2.14.0-alpha.1 - apollo-server-integration-testsuite@2.14.0-alpha.1 - apollo-server-koa@2.14.0-alpha.1 - apollo-server-lambda@2.14.0-alpha.1 - apollo-server-micro@2.14.0-alpha.1 - apollo-server-plugin-base@0.9.0-alpha.1 - apollo-server-plugin-operation-registry@0.3.2-alpha.0 - apollo-server-plugin-response-cache@0.5.2-alpha.1 - apollo-server-testing@2.14.0-alpha.1 - apollo-server-types@0.5.0-alpha.1 - apollo-server@2.14.0-alpha.1 - apollo-tracing@0.11.0-alpha.1 - graphql-extensions@0.12.2-alpha.1 --- packages/apollo-cache-control/package.json | 2 +- packages/apollo-datasource-rest/package.json | 2 +- packages/apollo-engine-reporting-protobuf/package-lock.json | 2 +- packages/apollo-engine-reporting-protobuf/package.json | 2 +- packages/apollo-engine-reporting/package.json | 2 +- packages/apollo-gateway/package.json | 2 +- packages/apollo-server-azure-functions/package.json | 2 +- packages/apollo-server-cloud-functions/package.json | 2 +- packages/apollo-server-cloudflare/package.json | 2 +- packages/apollo-server-core/package.json | 2 +- packages/apollo-server-express/package.json | 2 +- packages/apollo-server-fastify/package.json | 2 +- packages/apollo-server-hapi/package.json | 2 +- packages/apollo-server-integration-testsuite/package.json | 2 +- packages/apollo-server-koa/package.json | 2 +- packages/apollo-server-lambda/package.json | 2 +- packages/apollo-server-micro/package.json | 2 +- packages/apollo-server-plugin-base/package.json | 2 +- packages/apollo-server-plugin-operation-registry/package.json | 3 +-- packages/apollo-server-plugin-response-cache/package.json | 2 +- packages/apollo-server-testing/package.json | 2 +- packages/apollo-server-types/package.json | 2 +- packages/apollo-server/package.json | 2 +- packages/apollo-tracing/package.json | 2 +- packages/graphql-extensions/package.json | 2 +- 25 files changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/apollo-cache-control/package.json b/packages/apollo-cache-control/package.json index b67eefeb1f5..ca3b1f1218a 100644 --- a/packages/apollo-cache-control/package.json +++ b/packages/apollo-cache-control/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache-control", - "version": "0.11.0-alpha.0", + "version": "0.11.0-alpha.1", "description": "A GraphQL extension for cache control", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/apollo-datasource-rest/package.json b/packages/apollo-datasource-rest/package.json index 3bc114c0c97..c3e4e6a028a 100644 --- a/packages/apollo-datasource-rest/package.json +++ b/packages/apollo-datasource-rest/package.json @@ -1,6 +1,6 @@ { "name": "apollo-datasource-rest", - "version": "0.9.2-alpha.0", + "version": "0.9.2-alpha.1", "author": "opensource@apollographql.com", "license": "MIT", "repository": { diff --git a/packages/apollo-engine-reporting-protobuf/package-lock.json b/packages/apollo-engine-reporting-protobuf/package-lock.json index cdeebe199a6..db3c36e4168 100644 --- a/packages/apollo-engine-reporting-protobuf/package-lock.json +++ b/packages/apollo-engine-reporting-protobuf/package-lock.json @@ -1,6 +1,6 @@ { "name": "apollo-engine-reporting-protobuf", - "version": "0.5.1-alpha.0", + "version": "0.5.1-alpha.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/apollo-engine-reporting-protobuf/package.json b/packages/apollo-engine-reporting-protobuf/package.json index 3812458bbb2..52026c09f69 100644 --- a/packages/apollo-engine-reporting-protobuf/package.json +++ b/packages/apollo-engine-reporting-protobuf/package.json @@ -1,6 +1,6 @@ { "name": "apollo-engine-reporting-protobuf", - "version": "0.5.1-alpha.0", + "version": "0.5.1-alpha.1", "description": "Protobuf format for Apollo Engine", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index 9ce287bb57f..e5e398e6dcd 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -1,6 +1,6 @@ { "name": "apollo-engine-reporting", - "version": "2.0.0-alpha.0", + "version": "2.0.0-alpha.1", "description": "Send reports about your GraphQL services to Apollo Graph Manager (previously known as Apollo Engine)", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/apollo-gateway/package.json b/packages/apollo-gateway/package.json index 1f578157609..23d5b32986b 100644 --- a/packages/apollo-gateway/package.json +++ b/packages/apollo-gateway/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/gateway", - "version": "0.16.1-alpha.0", + "version": "0.16.1-alpha.1", "description": "Apollo Gateway", "author": "opensource@apollographql.com", "main": "dist/index.js", diff --git a/packages/apollo-server-azure-functions/package.json b/packages/apollo-server-azure-functions/package.json index 92b7e2dc889..8f0779b94f8 100644 --- a/packages/apollo-server-azure-functions/package.json +++ b/packages/apollo-server-azure-functions/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-azure-functions", - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Production-ready Node.js GraphQL server for Azure Functions", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-cloud-functions/package.json b/packages/apollo-server-cloud-functions/package.json index 512372a4f92..43512c6fc18 100644 --- a/packages/apollo-server-cloud-functions/package.json +++ b/packages/apollo-server-cloud-functions/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cloud-functions", - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Production-ready Node.js GraphQL server for Google Cloud Functions", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-cloudflare/package.json b/packages/apollo-server-cloudflare/package.json index 3aa62a4c99a..b915fc84763 100644 --- a/packages/apollo-server-cloudflare/package.json +++ b/packages/apollo-server-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cloudflare", - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Production-ready Node.js GraphQL server for Cloudflare workers", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-core/package.json b/packages/apollo-server-core/package.json index e9455c0c9bb..3660dbad91c 100644 --- a/packages/apollo-server-core/package.json +++ b/packages/apollo-server-core/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-core", - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Core engine for Apollo GraphQL server", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-express/package.json b/packages/apollo-server-express/package.json index e05007504ca..00add1126aa 100644 --- a/packages/apollo-server-express/package.json +++ b/packages/apollo-server-express/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-express", - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Production-ready Node.js GraphQL server for Express and Connect", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-fastify/package.json b/packages/apollo-server-fastify/package.json index 486b37b7f42..996a83650ad 100644 --- a/packages/apollo-server-fastify/package.json +++ b/packages/apollo-server-fastify/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-fastify", - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Production-ready Node.js GraphQL server for Fastify", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-hapi/package.json b/packages/apollo-server-hapi/package.json index d3ea90a65ac..69561ae7df2 100644 --- a/packages/apollo-server-hapi/package.json +++ b/packages/apollo-server-hapi/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-hapi", - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Production-ready Node.js GraphQL server for Hapi", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-integration-testsuite/package.json b/packages/apollo-server-integration-testsuite/package.json index 2209aa1e452..9f86012a830 100644 --- a/packages/apollo-server-integration-testsuite/package.json +++ b/packages/apollo-server-integration-testsuite/package.json @@ -1,7 +1,7 @@ { "name": "apollo-server-integration-testsuite", "private": true, - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Apollo Server Integrations testsuite", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-koa/package.json b/packages/apollo-server-koa/package.json index 15ec84f7ef6..94c1fe4f652 100644 --- a/packages/apollo-server-koa/package.json +++ b/packages/apollo-server-koa/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-koa", - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Production-ready Node.js GraphQL server for Koa", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-lambda/package.json b/packages/apollo-server-lambda/package.json index 6ad05a03219..06bd618f460 100644 --- a/packages/apollo-server-lambda/package.json +++ b/packages/apollo-server-lambda/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-lambda", - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Production-ready Node.js GraphQL server for AWS Lambda", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-micro/package.json b/packages/apollo-server-micro/package.json index ca221e1dcb0..1c9317338cf 100644 --- a/packages/apollo-server-micro/package.json +++ b/packages/apollo-server-micro/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-micro", - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Production-ready Node.js GraphQL server for Micro", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-plugin-base/package.json b/packages/apollo-server-plugin-base/package.json index fa28b8d7560..06d3c8ffe4c 100644 --- a/packages/apollo-server-plugin-base/package.json +++ b/packages/apollo-server-plugin-base/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-plugin-base", - "version": "0.9.0-alpha.0", + "version": "0.9.0-alpha.1", "description": "Apollo Server plugin base classes", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-plugin-operation-registry/package.json b/packages/apollo-server-plugin-operation-registry/package.json index bebfa7b9c6a..c0e412e0358 100644 --- a/packages/apollo-server-plugin-operation-registry/package.json +++ b/packages/apollo-server-plugin-operation-registry/package.json @@ -1,11 +1,10 @@ { "name": "apollo-server-plugin-operation-registry", - "version": "0.3.1", + "version": "0.3.2-alpha.0", "description": "Apollo Server operation registry", "main": "dist/index.js", "types": "dist/index.d.ts", "keywords": [], - "author": "Apollo ", "repository": { "type": "git", diff --git a/packages/apollo-server-plugin-response-cache/package.json b/packages/apollo-server-plugin-response-cache/package.json index 698913fcbda..1e3bdbfac02 100644 --- a/packages/apollo-server-plugin-response-cache/package.json +++ b/packages/apollo-server-plugin-response-cache/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-plugin-response-cache", - "version": "0.5.2-alpha.0", + "version": "0.5.2-alpha.1", "description": "Apollo Server full query response cache", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-testing/package.json b/packages/apollo-server-testing/package.json index b2048935b61..e6cc267ba08 100644 --- a/packages/apollo-server-testing/package.json +++ b/packages/apollo-server-testing/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-testing", - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Test utils for apollo-server", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-types/package.json b/packages/apollo-server-types/package.json index 881f449c6f8..146c687aec0 100644 --- a/packages/apollo-server-types/package.json +++ b/packages/apollo-server-types/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-types", - "version": "0.5.0-alpha.0", + "version": "0.5.0-alpha.1", "description": "Apollo Server shared types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server/package.json b/packages/apollo-server/package.json index c18a311839b..8d62350d7f4 100644 --- a/packages/apollo-server/package.json +++ b/packages/apollo-server/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server", - "version": "2.14.0-alpha.0", + "version": "2.14.0-alpha.1", "description": "Production ready GraphQL Server", "author": "opensource@apollographql.com", "main": "dist/index.js", diff --git a/packages/apollo-tracing/package.json b/packages/apollo-tracing/package.json index 5de113417dd..71165100e64 100644 --- a/packages/apollo-tracing/package.json +++ b/packages/apollo-tracing/package.json @@ -1,6 +1,6 @@ { "name": "apollo-tracing", - "version": "0.11.0-alpha.0", + "version": "0.11.0-alpha.1", "description": "Collect and expose trace data for GraphQL requests", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/graphql-extensions/package.json b/packages/graphql-extensions/package.json index 14d98ed0e13..a3ed8d41d17 100644 --- a/packages/graphql-extensions/package.json +++ b/packages/graphql-extensions/package.json @@ -1,6 +1,6 @@ { "name": "graphql-extensions", - "version": "0.12.2-alpha.0", + "version": "0.12.2-alpha.1", "description": "Add extensions to GraphQL servers", "main": "./dist/index.js", "types": "./dist/index.d.ts", From f970b8a64ce9ab4b2c089cb3d5774f65f76d56b5 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 19 May 2020 15:51:34 -0700 Subject: [PATCH 84/98] apollo-engine-reporting: apply prettier to agent.ts --- packages/apollo-engine-reporting/src/agent.ts | 80 +++++++++++++------ 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index bd3a13b327f..e96e01474ae 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -57,10 +57,15 @@ export type GenerateClientInfo = ( ) => 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 } - ) { +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; @@ -69,34 +74,52 @@ export function getEngineApiKey( 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 && 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`."); + if (legacyApiKeyFromEnv && !warnedOnDeprecatedApiKey && !skipWarn) { + logger.warn( + '[deprecated] The `ENGINE_API_KEY` environment variable has been renamed to `APOLLO_KEY`.', + ); warnedOnDeprecatedApiKey = true; } - return apiKeyFromEnv || legacyApiKeyFromEnv || '' + return apiKeyFromEnv || legacyApiKeyFromEnv || ''; } // AS3: Drop support for deprecated `ENGINE_SCHEMA_TAG`. -export function getEngineGraphVariant(engine: EngineReportingOptions | boolean | undefined, logger: Logger = console): string | undefined { +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)) { + } 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.'); + 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`.'); + 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`.'); + 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.') + 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; } @@ -356,7 +379,11 @@ export class EngineReportingAgent { public constructor(options: EngineReportingOptions = {}) { this.options = options; - this.apiKey = getEngineApiKey({engine: this.options, skipWarn: false, logger: this.logger}); + 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) || ''; @@ -382,7 +409,7 @@ export class EngineReportingAgent { if (this.options.handleSignals !== false) { const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM']; - signals.forEach(signal => { + signals.forEach((signal) => { // Note: Node only started sending signal names to signal events with // Node v10 so we can't use that feature here. const handler: NodeJS.SignalsListener = async () => { @@ -493,7 +520,9 @@ export class EngineReportingAgent { } public async sendAllReports(): Promise { - await Promise.all(Object.keys(this.reports).map(id => this.sendReport(id))); + await Promise.all( + Object.keys(this.reports).map((id) => this.sendReport(id)), + ); } public async sendReport(executableSchemaId: string): Promise { @@ -564,8 +593,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; @@ -652,7 +682,7 @@ export class EngineReportingAgent { this.currentSchemaReporter = schemaReporter; const logger = this.logger; - setTimeout(function() { + setTimeout(function () { reportingLoop(schemaReporter, logger, false, fallbackReportingDelayInMs); }, delay); } @@ -729,14 +759,14 @@ export class EngineReportingAgent { private async sendAllReportsAndReportErrors(): Promise { await Promise.all( - Object.keys(this.reports).map(executableSchemaId => + Object.keys(this.reports).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. @@ -870,8 +900,6 @@ export function computeExecutableSchemaId( // Can't call digest on this object twice. Creating new object each function call const sha256 = createHash('sha256'); const schemaDocument = - typeof schema === 'string' - ? schema - : printSchema(schema); + typeof schema === 'string' ? schema : printSchema(schema); return sha256.update(schemaDocument).digest('hex'); } From 8b72e5052bd87218f9b5b2dc4fea34f96f145a83 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 19 May 2020 16:10:35 -0700 Subject: [PATCH 85/98] apollo-engine-reporting: fix TypeError on sendReport("unknown") EngineReportingAgent had three different fields all of which were objects keyed by executableSchemaId. Their key sets were identical but this was not represented explicitly in the code. In addition, their TypeScript typings were of the form `{[x: string]: Foo}`, which means TypeScript will inaccurately assume that looking up *any* key on the object will return an actual Foo and not undefined. This refactor merges the three maps into a single map whose value is a ReportData with three fields. Additionally, it fixes the type to reflect that not every executableSchemaId is always in the map. Mostly this just leads to code whose properties (an executableSchemaId may or may not have these three pieces of data associated with them, and if they do then all three are there) can be checked at compile time rather than runtime. This does actually fix a minor bug: if `sendReport` is called manually with an executableSchemaId which is not associated with a trace that has been reported yet (perhaps because it is called before any traces are captured?), the code used to crash when it tried to evaluate `report.tracesPerQuery`, but now it will create an empty report for that id and not crash (but still not send it because it is empty). We do actually document that you can call `sendReport` yourself so this seems reasonable. --- packages/apollo-engine-reporting/src/agent.ts | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index e96e01474ae..9e4d8983ad8 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -342,6 +342,24 @@ const serviceHeaderDefaults = { uname: `${os.platform()}, ${os.type()}, ${os.release()}, ${os.arch()})`, }; +class ReportData { + report!: Report; + size!: number; + readonly header: ReportHeader; + constructor(executableSchemaId: string, graphVariant: string) { + this.header = new ReportHeader({ + ...serviceHeaderDefaults, + executableSchemaId, + schemaTag: graphVariant, + }); + this.reset(); + } + reset() { + this.report = new Report({ header: this.header }); + this.size = 0; + } +} + // EngineReportingAgent is a persistent object which creates // EngineReportingExtensions for each request and sends batches of trace reports // to the Engine server. @@ -351,19 +369,13 @@ export class EngineReportingAgent { private readonly logger: Logger = console; private readonly graphVariant: string; - private reports: { [executableSchemaId: string]: Report } = Object.create( - null, - ); - private reportSizes: { [executableSchemaId: string]: number } = Object.create( - null, - ); + private readonly reportDataByExecutableSchemaId: { + [executableSchemaId: string]: ReportData | undefined; + } = Object.create(null); private reportTimer: any; // timer typing is weird and node-specific private readonly sendReportsImmediately?: boolean; private stopped: boolean = false; - private reportHeaders: { - [executableSchemaId: string]: ReportHeader; - } = Object.create(null); private signatureCache: InMemoryLRUCache; private signalHandlers = new Map(); @@ -459,6 +471,16 @@ export class EngineReportingAgent { }); } + private getReportData(executableSchemaId: string): ReportData { + const existing = this.reportDataByExecutableSchemaId[executableSchemaId]; + if (existing) { + return existing; + } + const reportData = new ReportData(executableSchemaId, this.graphVariant); + this.reportDataByExecutableSchemaId[executableSchemaId] = reportData; + return reportData; + } + public async addTrace({ trace, queryHash, @@ -472,16 +494,8 @@ export class EngineReportingAgent { return; } - if (!(executableSchemaId in this.reports)) { - this.reportHeaders[executableSchemaId] = new ReportHeader({ - ...serviceHeaderDefaults, - executableSchemaId: executableSchemaId, - schemaTag: this.graphVariant, - }); - // initializes this.reports[reportHash] - this.resetReport(executableSchemaId); - } - const report = this.reports[executableSchemaId]; + const reportData = this.getReportData(executableSchemaId); + const { report } = reportData; const protobufError = Trace.verify(trace); if (protobufError) { @@ -506,13 +520,12 @@ export class EngineReportingAgent { (report.tracesPerQuery[statsReportKey] as any).encodedTraces.push( encodedTrace, ); - this.reportSizes[executableSchemaId] += - encodedTrace.length + Buffer.byteLength(statsReportKey); + reportData.size += encodedTrace.length + Buffer.byteLength(statsReportKey); // If the buffer gets big (according to our estimate), send. if ( this.sendReportsImmediately || - this.reportSizes[executableSchemaId] >= + reportData.size >= (this.options.maxUncompressedReportSize || 4 * 1024 * 1024) ) { await this.sendReportAndReportErrors(executableSchemaId); @@ -521,13 +534,16 @@ export class EngineReportingAgent { public async sendAllReports(): Promise { await Promise.all( - Object.keys(this.reports).map((id) => this.sendReport(id)), + Object.keys(this.reportDataByExecutableSchemaId).map((id) => + this.sendReport(id), + ), ); } public async sendReport(executableSchemaId: string): Promise { - const report = this.reports[executableSchemaId]; - this.resetReport(executableSchemaId); + const reportData = this.getReportData(executableSchemaId); + const { report } = reportData; + reportData.reset(); if (Object.keys(report.tracesPerQuery).length === 0) { return; @@ -759,7 +775,9 @@ export class EngineReportingAgent { private async sendAllReportsAndReportErrors(): Promise { await Promise.all( - Object.keys(this.reports).map((executableSchemaId) => + Object.keys( + this.reportDataByExecutableSchemaId, + ).map((executableSchemaId) => this.sendReportAndReportErrors(executableSchemaId), ), ); @@ -777,13 +795,6 @@ export class EngineReportingAgent { } }); } - - private resetReport(executableSchemaId: string) { - this.reports[executableSchemaId] = new Report({ - header: this.reportHeaders[executableSchemaId], - }); - this.reportSizes[executableSchemaId] = 0; - } } function createSignatureCache({ From e7b760bb8a79eab1f49a5bfba1047ea66d14a6cb Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 20 May 2020 10:16:56 +0000 Subject: [PATCH 86/98] Try to improve clarity of intent of various loggers via naming/commentary. --- packages/apollo-engine-reporting/src/plugin.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 4b8217813f6..56efc62d9bf 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -45,7 +45,13 @@ export const plugin = ( executableSchemaIdGenerator: (schema: string | GraphQLSchema) => string; }, ): ApolloServerPlugin => { - const logger: Logger = options.logger || console; + /** + * Non request-specific logging will go into this general logger. Request- + * specific log output (where the log output is only a result of a specific + * request) will go to the `logger` which we get from the request context. + */ + const loggerForPlugin: Logger = options.logger || console; + const generateClientInfo: GenerateClientInfo = options.generateClientInfo || defaultGenerateClientInfo; @@ -66,10 +72,16 @@ export const plugin = ( schema, request: { http, variables }, }) { + /** + * Request specific log output should go into the `logger` from the + * request context when it's provided. + */ + const logger = requestLogger || loggerForPlugin; + const treeBuilder: EngineReportingTreeBuilder = new EngineReportingTreeBuilder( { rewriteError: options.rewriteError, - logger: requestLogger || logger, + logger, }, ); From 9d4f92d43ddeb3015bc58aa934683f68dd22b49e Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 20 May 2020 10:34:26 +0000 Subject: [PATCH 87/98] Surface `logger` within `getTraceSignature` to "catch" & log cache errors. While a failure to write to the cache in its current state would be unlikely since it's tied to an `InMemoryCacheLRU`, we should be able to have this cache backed by a distributed store (e.g., Memcached, Redis) in the future, where these writes _could_ fail. --- packages/apollo-engine-reporting/src/agent.ts | 33 ++++++++++++++++++- .../apollo-engine-reporting/src/plugin.ts | 1 + 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 9e4d8983ad8..b6b91f830d1 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -332,6 +332,7 @@ export interface AddTraceArgs { executableSchemaId: string; source?: string; document?: DocumentNode; + logger: Logger, } const serviceHeaderDefaults = { @@ -488,6 +489,16 @@ export class EngineReportingAgent { operationName, source, executableSchemaId, + /** + * Since this agent instruments the plugin with its `options.logger`, but + * also passes off a reference to this `addTrace` method which is invoked + * with the availability of a per-request `logger`, this `logger` (in this + * destructuring) is already conditionally either: + * + * 1. The `logger` that was passed into the `options` for the agent. + * 2. The request-specific `logger`. + */ + logger, }: AddTraceArgs): Promise { // Ignore traces that come in after stop(). if (this.stopped) { @@ -508,6 +519,7 @@ export class EngineReportingAgent { document, source, operationName, + logger, }); const statsReportKey = `# ${operationName || '-'}\n${signature}`; @@ -729,11 +741,13 @@ export class EngineReportingAgent { operationName, document, source, + logger, }: { queryHash: string; operationName: string; document?: DocumentNode; source?: string; + logger: Logger; }): Promise { if (!document && !source) { // This shouldn't happen: one of those options must be passed to runQuery. @@ -768,7 +782,24 @@ export class EngineReportingAgent { )(document, operationName); // Intentionally not awaited so the cache can be written to at leisure. - this.signatureCache.set(cacheKey, generatedSignature); + // + // As of the writing of this comment, this signature cache is exclusively + // backed by an `InMemoryLRUCache` which cannot do anything + // non-synchronously, though that will probably change in the future, + // and a distributed cache store, like Redis, doesn't seem unfathomable. + // + // Due in part to the plugin being separate from the `EngineReportingAgent`, + // the loggers are difficult to track down here. Errors will be logged to + // 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 => { + logger.warn( + 'Could not store signature cache. ' + + (err && err.message) || err + ) + }); return generatedSignature; } diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 56efc62d9bf..3033ac154a5 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -161,6 +161,7 @@ export const plugin = ( executableSchemaId: executableSchemaIdGenerator( options.experimental_overrideReportedSchema || schema, ), + logger, }); } From 96dbccc1166f832216c864fcb82e9517c6856938 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 20 May 2020 10:40:28 +0000 Subject: [PATCH 88/98] Apply a `.catch` to the un-`await`'d call to `addTrace`. Also, add a comment indicating the intent. The `logger` in this case will be a request-specific logger when available or the general "Engine" logger otherwise. Also adjust recently introduced test to accurately return a `Promise` in its mock. Related: https://github.com/apollographql/apollo-server/pull/2670 --- .../apollo-engine-reporting/src/__tests__/plugin.test.ts | 2 +- packages/apollo-engine-reporting/src/plugin.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts index 56c2809a3db..f56038aa375 100644 --- a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts @@ -46,7 +46,7 @@ describe('schema reporting', () => { const schema = makeExecutableSchema({ typeDefs }); addMockFunctionsToSchema({ schema }); - const addTrace = jest.fn(); + const addTrace = jest.fn().mockResolvedValue(undefined); const startSchemaReporting = jest.fn(); const executableSchemaIdGenerator = jest.fn(computeExecutableSchemaId); diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 3033ac154a5..ab6c22220bb 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -152,6 +152,13 @@ export const plugin = ( treeBuilder.trace.queryPlan = metrics.queryPlanTrace; } + // Intentionally un-awaited so as not to block the response. Any + // errors will be logged, but will not manifest a user-facing error. + // The logger in this case is a request specific logger OR the logger + // defined by the plugin if that's unavailable. The request-specific + // logger is preferred since this is very much coupled directly to a + // client-triggered action which might be more granularly tagged by + // logging implementations. addTrace({ operationName, queryHash: requestContext.queryHash!, @@ -162,7 +169,7 @@ export const plugin = ( options.experimental_overrideReportedSchema || schema, ), logger, - }); + }).catch(logger.error); } // While we start the tracing as soon as possible, we only actually report From cd4844ed728b036df9ffc32d7fd8bf0acafe849e Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 20 May 2020 12:27:39 +0000 Subject: [PATCH 89/98] Add comments about preservation of legacy behaviors in request pipeline. These comments hope to better explain the behavior which was present in the 2.x series of Apollo Server prior to the introduction of `didEncounterErrors`. The behavior which this commentary applies to -- in which we `throw` prior to finishing subsequent errors, has been this way since AS2 was released and was a pattern which we locked ourselves into for the duration of the AS2 release when we put out the request pipeline plugins. It should _not_ stay this way in the future, and in AS3 we will resolve this. --- .../apollo-server-core/src/requestPipeline.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index 60962277b0c..d3588f04e1a 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -160,8 +160,16 @@ export async function processGraphQLRequest( // It looks like we've received a persisted query. Check if we // support them. if (!config.persistedQueries || !config.persistedQueries.cache) { + // We are returning to `runHttpQuery` to preserve legacy behavior while + // still delivering observability to the `didEncounterErrors` hook. + // This particular error will _not_ trigger `willSendResponse`. + // See comment on `emitErrorAndThrow` for more details. return await emitErrorAndThrow(new PersistedQueryNotSupportedError()); } else if (extensions.persistedQuery.version !== 1) { + // We are returning to `runHttpQuery` to preserve legacy behavior while + // still delivering observability to the `didEncounterErrors` hook. + // This particular error will _not_ trigger `willSendResponse`. + // See comment on `emitErrorAndThrow` for more details. return await emitErrorAndThrow( new InvalidGraphQLRequestError('Unsupported persisted query version')); } @@ -188,12 +196,20 @@ export async function processGraphQLRequest( if (query) { metrics.persistedQueryHit = true; } else { + // We are returning to `runHttpQuery` to preserve legacy behavior while + // still delivering observability to the `didEncounterErrors` hook. + // This particular error will _not_ trigger `willSendResponse`. + // See comment on `emitErrorAndThrow` for more details. return await emitErrorAndThrow(new PersistedQueryNotFoundError()); } } else { const computedQueryHash = computeQueryHash(query); if (queryHash !== computedQueryHash) { + // We are returning to `runHttpQuery` to preserve legacy behavior while + // still delivering observability to the `didEncounterErrors` hook. + // This particular error will _not_ trigger `willSendResponse`. + // See comment on `emitErrorAndThrow` for more details. return await emitErrorAndThrow( new InvalidGraphQLRequestError('provided sha does not match query')); } @@ -209,6 +225,10 @@ export async function processGraphQLRequest( // now, but this should be replaced with the new operation ID algorithm. queryHash = computeQueryHash(query); } else { + // We are returning to `runHttpQuery` to preserve legacy behavior + // while still delivering observability to the `didEncounterErrors` hook. + // This particular error will _not_ trigger `willSendResponse`. + // See comment on `emitErrorAndThrow` for more details. return await emitErrorAndThrow( new InvalidGraphQLRequestError('Must provide query string.')); } @@ -359,6 +379,9 @@ export async function processGraphQLRequest( // an error) and not actually write, we'll write to the cache if it was // determined earlier in the request pipeline that we should do so. if (metrics.persistedQueryRegister && persistedQueryCache) { + // While it shouldn't normally be necessary to wrap this `Promise` in a + // `Promise.resolve` invocation, it seems that the underlying cache store + // is returning a non-native `Promise` (e.g. Bluebird, etc.). Promise.resolve( persistedQueryCache.set( queryHash, @@ -556,7 +579,15 @@ export async function processGraphQLRequest( } /** - * Report an error via `didEncounterErrors` and then `throw` it. + * HEREIN LIE LEGACY COMPATIBILITY + * + * DO NOT PERPETUATE THE USE OF THIS METHOD IN NEWLY INTRODUCED CODE. + * + * Report an error via `didEncounterErrors` and then `throw` it again, + * ENTIRELY BYPASSING the rest of the request pipeline and returning + * control to `runHttpQuery.ts`. + * + * Any number of other life-cycle events may not be invoked in this case. * * Prior to the introduction of this function, some errors were being thrown * within the request pipeline and going directly to handling within From 5384dfedbd9619d5d8402f08eb71e1e334934cfd Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 20 May 2020 12:54:23 +0000 Subject: [PATCH 90/98] fix(reporting): Move invocation of `didEnd` to `willSendResponse`. This fixes a 'addProtobufError called after stopTiming' condition. While this will be fixed in the next generation of the Apollo Server plugin API (in AS3), there are currently error-condition cases where `didEncounterErrors` is called, before execution has ever happened, but `willSendResponse` is not invoked. These are noted in the request pipeline and are special-cased to preserve existing patterns. --- packages/apollo-engine-reporting/src/plugin.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index ab6c22220bb..b86c414adbd 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -1,8 +1,8 @@ import { GraphQLRequestContext, Logger, - GraphQLRequestContextExecutionDidStart, GraphQLRequestContextDidEncounterErrors, + GraphQLRequestContextWillSendResponse, } from 'apollo-server-types'; import { Headers } from 'apollo-server-env'; import { GraphQLSchema, printSchema } from 'graphql'; @@ -117,7 +117,7 @@ export const plugin = ( let endDone: boolean = false; function didEnd( requestContext: - | GraphQLRequestContextExecutionDidStart + | GraphQLRequestContextWillSendResponse | GraphQLRequestContextDidEncounterErrors, ) { if (endDone) return; @@ -214,9 +214,8 @@ export const plugin = ( } }, - executionDidStart(requestContext) { + executionDidStart() { return { - executionDidEnd: () => didEnd(requestContext), willResolveField({ info }) { return treeBuilder.willResolveField(info); // We could save the error into the trace during the end handler, but @@ -226,6 +225,10 @@ export const plugin = ( }; }, + willSendResponse(requestContext) { + didEnd(requestContext); + }, + didEncounterErrors(requestContext) { // Search above for a comment about "didResolveSource" to see which // of the pre-source-resolution errors we are intentionally avoiding. From 52c36d5fe4a5b6c76346e9e39aa21234c6ee41b5 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 20 May 2020 13:27:05 +0000 Subject: [PATCH 91/98] Add comments about necessity to call `didEnd` in two places. --- packages/apollo-engine-reporting/src/plugin.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index b86c414adbd..b960df381df 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -114,6 +114,16 @@ export const plugin = ( } } + /** + * Due to a number of exceptions in the request pipeline — which are + * intended to preserve backwards compatible behavior with the + * first generation of the request pipeline plugins prior to the + * introduction of `didEncounterErrors` — we need to have this "didEnd" + * functionality invoked from two places. This accounts for the fact + * that sometimes, under some special-cased error conditions, + * `willSendResponse` is not invoked. To zoom in on some of these cases, + * check the `requestPipeline.ts` for `emitErrorAndThrow`. + */ let endDone: boolean = false; function didEnd( requestContext: @@ -226,6 +236,7 @@ export const plugin = ( }, willSendResponse(requestContext) { + // See comment above for why `didEnd` must be called in two hooks. didEnd(requestContext); }, @@ -234,6 +245,8 @@ export const plugin = ( // of the pre-source-resolution errors we are intentionally avoiding. if (!didResolveSource) return; treeBuilder.didEncounterErrors(requestContext.errors); + + // See comment above for why `didEnd` must be called in two hooks. didEnd(requestContext); }, }; From 36afcd616858a772c6c46ee4fe4544f54c69ab75 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 20 May 2020 17:28:11 +0300 Subject: [PATCH 92/98] Release - apollo-engine-reporting@2.0.0-alpha.2 - @apollo/gateway@0.16.1-alpha.2 - apollo-server-azure-functions@2.14.0-alpha.2 - apollo-server-cloud-functions@2.14.0-alpha.2 - apollo-server-cloudflare@2.14.0-alpha.2 - apollo-server-core@2.14.0-alpha.2 - apollo-server-express@2.14.0-alpha.2 - apollo-server-fastify@2.14.0-alpha.2 - apollo-server-hapi@2.14.0-alpha.2 - apollo-server-integration-testsuite@2.14.0-alpha.2 - apollo-server-koa@2.14.0-alpha.2 - apollo-server-lambda@2.14.0-alpha.2 - apollo-server-micro@2.14.0-alpha.2 - apollo-server-testing@2.14.0-alpha.2 - apollo-server@2.14.0-alpha.2 --- packages/apollo-engine-reporting/package.json | 2 +- packages/apollo-gateway/package.json | 2 +- packages/apollo-server-azure-functions/package.json | 2 +- packages/apollo-server-cloud-functions/package.json | 2 +- packages/apollo-server-cloudflare/package.json | 2 +- packages/apollo-server-core/package.json | 2 +- packages/apollo-server-express/package.json | 2 +- packages/apollo-server-fastify/package.json | 2 +- packages/apollo-server-hapi/package.json | 2 +- packages/apollo-server-integration-testsuite/package.json | 2 +- packages/apollo-server-koa/package.json | 2 +- packages/apollo-server-lambda/package.json | 2 +- packages/apollo-server-micro/package.json | 2 +- packages/apollo-server-testing/package.json | 2 +- packages/apollo-server/package.json | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index e5e398e6dcd..7d351372131 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -1,6 +1,6 @@ { "name": "apollo-engine-reporting", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "Send reports about your GraphQL services to Apollo Graph Manager (previously known as Apollo Engine)", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/apollo-gateway/package.json b/packages/apollo-gateway/package.json index 23d5b32986b..8d9088496ef 100644 --- a/packages/apollo-gateway/package.json +++ b/packages/apollo-gateway/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/gateway", - "version": "0.16.1-alpha.1", + "version": "0.16.1-alpha.2", "description": "Apollo Gateway", "author": "opensource@apollographql.com", "main": "dist/index.js", diff --git a/packages/apollo-server-azure-functions/package.json b/packages/apollo-server-azure-functions/package.json index 8f0779b94f8..dce4aadc4c7 100644 --- a/packages/apollo-server-azure-functions/package.json +++ b/packages/apollo-server-azure-functions/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-azure-functions", - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Production-ready Node.js GraphQL server for Azure Functions", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-cloud-functions/package.json b/packages/apollo-server-cloud-functions/package.json index 43512c6fc18..44157166a2a 100644 --- a/packages/apollo-server-cloud-functions/package.json +++ b/packages/apollo-server-cloud-functions/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cloud-functions", - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Production-ready Node.js GraphQL server for Google Cloud Functions", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-cloudflare/package.json b/packages/apollo-server-cloudflare/package.json index b915fc84763..1b516ffa82a 100644 --- a/packages/apollo-server-cloudflare/package.json +++ b/packages/apollo-server-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cloudflare", - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Production-ready Node.js GraphQL server for Cloudflare workers", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-core/package.json b/packages/apollo-server-core/package.json index 3660dbad91c..aceb37283bb 100644 --- a/packages/apollo-server-core/package.json +++ b/packages/apollo-server-core/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-core", - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Core engine for Apollo GraphQL server", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-express/package.json b/packages/apollo-server-express/package.json index 00add1126aa..3150adecb4a 100644 --- a/packages/apollo-server-express/package.json +++ b/packages/apollo-server-express/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-express", - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Production-ready Node.js GraphQL server for Express and Connect", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-fastify/package.json b/packages/apollo-server-fastify/package.json index 996a83650ad..e38bc3510e7 100644 --- a/packages/apollo-server-fastify/package.json +++ b/packages/apollo-server-fastify/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-fastify", - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Production-ready Node.js GraphQL server for Fastify", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-hapi/package.json b/packages/apollo-server-hapi/package.json index 69561ae7df2..a9f07414461 100644 --- a/packages/apollo-server-hapi/package.json +++ b/packages/apollo-server-hapi/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-hapi", - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Production-ready Node.js GraphQL server for Hapi", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-integration-testsuite/package.json b/packages/apollo-server-integration-testsuite/package.json index 9f86012a830..3e3b976ba86 100644 --- a/packages/apollo-server-integration-testsuite/package.json +++ b/packages/apollo-server-integration-testsuite/package.json @@ -1,7 +1,7 @@ { "name": "apollo-server-integration-testsuite", "private": true, - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Apollo Server Integrations testsuite", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-koa/package.json b/packages/apollo-server-koa/package.json index 94c1fe4f652..d8d5800d4db 100644 --- a/packages/apollo-server-koa/package.json +++ b/packages/apollo-server-koa/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-koa", - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Production-ready Node.js GraphQL server for Koa", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-lambda/package.json b/packages/apollo-server-lambda/package.json index 06bd618f460..f45abba3f79 100644 --- a/packages/apollo-server-lambda/package.json +++ b/packages/apollo-server-lambda/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-lambda", - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Production-ready Node.js GraphQL server for AWS Lambda", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-micro/package.json b/packages/apollo-server-micro/package.json index 1c9317338cf..c7a09032b0c 100644 --- a/packages/apollo-server-micro/package.json +++ b/packages/apollo-server-micro/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-micro", - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Production-ready Node.js GraphQL server for Micro", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-testing/package.json b/packages/apollo-server-testing/package.json index e6cc267ba08..f95cf2939a0 100644 --- a/packages/apollo-server-testing/package.json +++ b/packages/apollo-server-testing/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-testing", - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Test utils for apollo-server", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server/package.json b/packages/apollo-server/package.json index 8d62350d7f4..794ca7d3578 100644 --- a/packages/apollo-server/package.json +++ b/packages/apollo-server/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server", - "version": "2.14.0-alpha.1", + "version": "2.14.0-alpha.2", "description": "Production ready GraphQL Server", "author": "opensource@apollographql.com", "main": "dist/index.js", From 23dfacad8edb514f1bc8a4d8df30819e1624a57a Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Thu, 21 May 2020 07:00:25 -0400 Subject: [PATCH 93/98] docs+changelog: For #4084 and `graphVariant`. (#4138) --- CHANGELOG.md | 35 ++++++++++++++++++++++++++++++++ docs/source/api/apollo-server.md | 10 +++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81a41745010..dcdc83f6b78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,39 @@ The version headers in this history reflect the versions of Apollo Server itself > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. +- `apollo-engine-reporting`: Added an _experimental_ schema reporting option, + `experimental_schemaReporting`, for Apollo Graph Manager users. **During this + experiment, we'd appreciate testing and feedback from current and new + users of the schema registry!** + + Prior to the introduction of this feature, the only way to get schemas into + the schema registry in Apollo Graph Manager was to use the CLI and run + `apollo schema:push`. _Apollo schema reporting protocol_ is a *new* + specification for GraphQL servers to automatically report schemas to the + Apollo Graph Manager schema registry. + + **To enable schema reporting,** provide a Graph Manager API key (available + free from [Apollo Graph Manager](https://engine.apollographql.com/)) in the + `APOLLO_KEY` environment variable *and* set the `experimental_schemaReporting` + option to `true` in the Apollo Server constructor options, like so: + + ```js + const server = new ApolloServer({ + typeDefs, + resolvers, + engine: { + experimental_schemaReporting: true, + /* Other existing options can remain the same. */ + }, + }); + ``` + + > When enabled, a schema reporter is initiated by the `apollo-engine-reporting` agent. It will loop until the `ApolloServer` instance is stopped, periodically calling back to Apollo Graph Manager to send information. The life-cycle of this reporter is managed by the agent. + + For more details on the implementation of this new protocol, see the PR which + introduced it to Apollo Server and the [preview documentation](https://github.com/apollographql/apollo-schema-reporting-preview-docs). + + [PR #4084](https://github.com/apollographql/apollo-server/pull/4084) - `apollo-engine-reporting`: The underlying integration of this plugin, which instruments and traces the graph's resolver performance and transmits these metrics to [Apollo Graph Manager](https://engine.apollographql.com/), has been changed from the (soon to be deprecated) `graphql-extensions` API to the new [request pipeline `plugins` API](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). [PR #3998](https://github.com/apollographql/apollo-server/pull/3998) _This change should be purely an implementation detail for a majority of users_. There are, however, some special considerations which are worth noting: @@ -16,6 +49,8 @@ The version headers in this history reflect the versions of Apollo Server itself - The federated tracing plugin's `ftv1` response on `extensions` (which is present on the response from an implementing service to the gateway) is now placed on the `extensions` _after_ the `formatResponse` hook. Anyone leveraging the `extensions`.`ftv1` data from the `formatResponse` hook will find that it is no longer present at that phase. - `apollo-tracing`: This package's internal integration with Apollo Server has been switched from using the soon-to-be-deprecated`graphql-extensions` API to using [the request pipeline plugin API](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). Behavior should remain otherwise the same. [PR #3991](https://github.com/apollographql/apollo-server/pull/3991) + + ### v2.13.0 > [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/e37384a49b2bf474eed0de3e9f4a1bebaeee64c7) diff --git a/docs/source/api/apollo-server.md b/docs/source/api/apollo-server.md index c5ba9fc92a6..3d3f142f8d7 100644 --- a/docs/source/api/apollo-server.md +++ b/docs/source/api/apollo-server.md @@ -404,7 +404,7 @@ addMockFunctionsToSchema({ - `{ none: true }`: Don't send any variable values. **(DEFAULT)** - `{ all: true }`: Send all variable values. - - `{ transform: ({ variables, operationString}) => { ... } }`: A custom function for modifying variable values. Keys added by the custom function will be removed, and keys removed will be added back with an empty value. For security reasons, if an error occurs within this function, all variable values will be replaced with `[PREDICATE_FUNCTION_ERROR]`. + - `{ transform: ({ variables, operationString}) => { ... } }`: A custom function for modifying variable values. Keys added by the custom function will be removed, and keys removed will be added back with an empty value. For security reasons, if an error occurs within this function, all variable values will be replaced with `[PREDICATE_FUNCTION_ERROR]`. - `{ exceptNames: [...] }`: A case-sensitive list of names of variables whose values should not be sent to Apollo servers. - `{ onlyNames: [...] }`: A case-sensitive list of names of variables whose values will be sent to Apollo servers. @@ -472,7 +472,13 @@ addMockFunctionsToSchema({ * `schemaTag`: String - A human-readable name to tag this variant of a schema (i.e. staging, EU). Setting this value will cause metrics to be segmented in the Apollo Platform's UI. Additionally schema validation with a schema tag will only check metrics associate with the same string. + > Will be deprecated in 3.0. Use the option `graphVariant` instead. + + A human-readable name to tag this variant of a schema (i.e. staging, EU). Setting this value will cause metrics to be segmented in the Apollo Platform's UI. Additionally schema validation with a schema tag will only check metrics associated with the same string. + +* `graphVariant`: String + + A human-readable name for the variant of a schema (i.e. staging, EU). Setting this value will cause metrics to be segmented in the Apollo Graph Manager UI. Additionally schema validation with a graph variant will only check metrics associated with the same string. * `generateClientInfo`: (GraphQLRequestContext) => ClientInfo **AS 2.2** From b18808d200832735ac894bd956a7371732d4bdbe Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 21 May 2020 11:28:13 +0000 Subject: [PATCH 94/98] tests: Loosen the guards in the `pluginTestHarness`. This more accurately reflects the request-pipeline. --- .../src/utils/pluginTestHarness.ts | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/packages/apollo-server-core/src/utils/pluginTestHarness.ts b/packages/apollo-server-core/src/utils/pluginTestHarness.ts index 86dd63460c9..9af86a5326a 100644 --- a/packages/apollo-server-core/src/utils/pluginTestHarness.ts +++ b/packages/apollo-server-core/src/utils/pluginTestHarness.ts @@ -122,35 +122,29 @@ export default async function pluginTestHarness({ requestContext.overallCachePolicy = overallCachePolicy; if (typeof pluginInstance.requestDidStart !== "function") { - throw new Error("Should be impossible as the plugin is defined."); + throw new Error("This test harness expects this to be defined."); } const listener = pluginInstance.requestDidStart(requestContext); - if (!listener) { - throw new Error("Should be impossible to not have a listener."); - } - - const dispatcher = new Dispatcher([listener]); + const dispatcher = new Dispatcher(listener ? [listener] : []); const executionListeners: GraphQLRequestExecutionListener[] = []; - if (typeof listener.executionDidStart === 'function') { - // This execution dispatcher logic is duplicated in the request pipeline - // right now. - dispatcher.invokeHookSync( - 'executionDidStart', - requestContext as GraphQLRequestContextExecutionDidStart, - ).forEach(executionListener => { - if (typeof executionListener === 'function') { - executionListeners.push({ - executionDidEnd: executionListener, - }); - } else if (typeof executionListener === 'object') { - executionListeners.push(executionListener); - } - }); - } + // This execution dispatcher logic is duplicated in the request pipeline + // right now. + dispatcher.invokeHookSync( + 'executionDidStart', + requestContext as GraphQLRequestContextExecutionDidStart, + ).forEach(executionListener => { + if (typeof executionListener === 'function') { + executionListeners.push({ + executionDidEnd: executionListener, + }); + } else if (typeof executionListener === 'object') { + executionListeners.push(executionListener); + } + }); const executionDispatcher = new Dispatcher(executionListeners); From 960e073252975149152e050f51107c14a3cc430b Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 21 May 2020 14:15:53 +0000 Subject: [PATCH 95/98] Add disclaimer about schema reporting being for non-federated graphs. Follows-up: https://github.com/apollographql/apollo-server/pull/3998 --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcdc83f6b78..f36eb095a03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,10 @@ The version headers in this history reflect the versions of Apollo Server itself > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. -- `apollo-engine-reporting`: Added an _experimental_ schema reporting option, - `experimental_schemaReporting`, for Apollo Graph Manager users. **During this - experiment, we'd appreciate testing and feedback from current and new +- `apollo-engine-reporting`: **Currently only for non-federated graphs.** + Added an _experimental_ schema reporting option, + `experimental_schemaReporting`, for Apollo Graph Manager users. **During + this experiment, we'd appreciate testing and feedback from current and new users of the schema registry!** Prior to the introduction of this feature, the only way to get schemas into From 489139c7bb0e112f4eacfb8f9a88b0fb80874013 Mon Sep 17 00:00:00 2001 From: Jeff Hampton Date: Wed, 27 May 2020 07:35:45 -0500 Subject: [PATCH 96/98] gateway: Support custom `fetcher` for RemoteGraphQLDataSource. (#4149) Co-authored-by: Jesse Rosenberger --- packages/apollo-gateway/CHANGELOG.md | 6 ++++- .../datasources/RemoteGraphQLDataSource.ts | 5 +++- .../__tests__/RemoteGraphQLDataSource.test.ts | 24 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/apollo-gateway/CHANGELOG.md b/packages/apollo-gateway/CHANGELOG.md index 6d60093e030..0ce314f654d 100644 --- a/packages/apollo-gateway/CHANGELOG.md +++ b/packages/apollo-gateway/CHANGELOG.md @@ -4,7 +4,11 @@ > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. -- _Nothing yet! Stay tuned._ +- __NEW__: Provide the ability to pass a custom `fetcher` during `RemoteGraphQLDataSource` construction to be used when executing operations against downstream services. Providing a custom `fetcher` may be necessary to accommodate more advanced needs, e.g., configuring custom TLS certificates for internal services. [PR #4149](https://github.com/apollographql/apollo-server/pull/4149) + + The `fetcher` specified should be a compliant implementor of the [Fetch API standard](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). This addition compliments, though is still orthognonal to, similar behavior originally introduced in [#3783](https://github.com/apollographql/apollo-server/pull/3783), which allowed customization of the implementation used to fetch _gateway configuration and federated SDL from services_ in managed and unmanaged modes, but didn't affect the communication that takes place during _operation execution_. + + For now, the default `fetcher` will remain the same ([`node-fetch`](https://npm.im/node-fetch)) implementation. A future major-version bump will update it to be consistent with other feature-rich implementations of the Fetch API which are used elsewhere in the Apollo Server stack where we use [`make-fetch-happen`](https://npm.im/make-fetch-happen). In all likelihood, `ApolloGateway` will pass its own `fetcher` to the `RemoteGraphQLDataSource` during service initialization. ## 0.16.0 diff --git a/packages/apollo-gateway/src/datasources/RemoteGraphQLDataSource.ts b/packages/apollo-gateway/src/datasources/RemoteGraphQLDataSource.ts index feffde81b2d..700e78e2f47 100644 --- a/packages/apollo-gateway/src/datasources/RemoteGraphQLDataSource.ts +++ b/packages/apollo-gateway/src/datasources/RemoteGraphQLDataSource.ts @@ -20,6 +20,8 @@ import { GraphQLDataSource } from './types'; import createSHA from 'apollo-server-core/dist/utils/createSHA'; export class RemoteGraphQLDataSource = Record> implements GraphQLDataSource { + fetcher: typeof fetch = fetch; + constructor( config?: Partial> & object & @@ -144,7 +146,8 @@ export class RemoteGraphQLDataSource = Reco }); try { - const httpResponse = await fetch(httpRequest); + // Use our local `fetcher` to allow for fetch injection + const httpResponse = await this.fetcher(httpRequest); if (!httpResponse.ok) { throw await this.errorFromResponse(httpResponse); diff --git a/packages/apollo-gateway/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts b/packages/apollo-gateway/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts index ff5309dc000..f81b39afb18 100644 --- a/packages/apollo-gateway/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts +++ b/packages/apollo-gateway/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts @@ -9,6 +9,7 @@ import { import { RemoteGraphQLDataSource } from '../RemoteGraphQLDataSource'; import { Headers } from 'apollo-server-env'; import { GraphQLRequestContext } from 'apollo-server-types'; +import { Response } from '../../../../../../apollo-tooling/packages/apollo-env/lib'; beforeEach(() => { fetch.mockReset(); @@ -238,6 +239,29 @@ describe('constructing requests', () => { }); }); +describe('fetcher', () => { + it('uses a custom provided `fetcher`', async () => { + const injectedFetch = fetch.mockJSONResponseOnce({ data: { injected: true } }); + const DataSource = new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + fetcher: injectedFetch, + }); + + const { data } = await DataSource.process({ + request: { + query: '{ me { name } }', + variables: { id: '1' }, + }, + context: {}, + }); + + expect(injectedFetch).toHaveBeenCalled(); + expect(data).toEqual({injected: true}); + + }); + +}); + describe('willSendRequest', () => { it('allows for modifying variables', async () => { const DataSource = new RemoteGraphQLDataSource({ From bbf3df19b8c4fdb5d4ae5aa9b037c65190217f14 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 27 May 2020 12:54:20 +0000 Subject: [PATCH 97/98] Update CHANGELOG.md files prior to publishing. --- CHANGELOG.md | 11 ++++++++--- packages/apollo-federation/CHANGELOG.md | 4 ++++ packages/apollo-gateway/CHANGELOG.md | 4 ++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f36eb095a03..61132a08c98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ The version headers in this history reflect the versions of Apollo Server itself > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. +- _Nothing yet! Stay tuned!_ + +### v2.14.0 + +- `apollo-server-core` / `apollo-server-plugin-base`: Add support for `willResolveField` and corresponding end-handler within `executionDidStart`. This brings the remaining bit of functionality that was previously only available from `graphql-extensions` to the new plugin API. The `graphql-extensions` API (which was never documented) will be deprecated in Apollo Server 3.x. To see the documentation for the request pipeline API, see [its documentation](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). For more details, see the attached PR. [PR #3988](https://github.com/apollographql/apollo-server/pull/3988) +- `apollo-server-core`: Deprecate `graphql-extensions`. All internal usages of the `graphql-extensions` API have been migrated to the request pipeline plugin API. For any implementor-supplied `extensions`, a deprecation warning will be printed once per-extension, per-server-startup, notifying of the intention to deprecate. Extensions should migrate to the plugin API, which is outlined in [its documentation](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). [PR #4135](https://github.com/apollographql/apollo-server/pull/4135) - `apollo-engine-reporting`: **Currently only for non-federated graphs.** Added an _experimental_ schema reporting option, `experimental_schemaReporting`, for Apollo Graph Manager users. **During @@ -48,9 +54,8 @@ The version headers in this history reflect the versions of Apollo Server itself _This change should be purely an implementation detail for a majority of users_. There are, however, some special considerations which are worth noting: - The federated tracing plugin's `ftv1` response on `extensions` (which is present on the response from an implementing service to the gateway) is now placed on the `extensions` _after_ the `formatResponse` hook. Anyone leveraging the `extensions`.`ftv1` data from the `formatResponse` hook will find that it is no longer present at that phase. -- `apollo-tracing`: This package's internal integration with Apollo Server has been switched from using the soon-to-be-deprecated`graphql-extensions` API to using [the request pipeline plugin API](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). Behavior should remain otherwise the same. [PR #3991](https://github.com/apollographql/apollo-server/pull/3991) - - +- `apollo-tracing`: This package's internal integration with Apollo Server has been switched from using the soon-to-be-deprecated `graphql-extensions` API to using [the request pipeline plugin API](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). Behavior should remain otherwise the same. [PR #3991](https://github.com/apollographql/apollo-server/pull/3991) +- `apollo-cache-control`: This package's internal integration with Apollo Server has been switched from using the soon-to-be-deprecated `graphql-extensions` API to using [the request pipeline plugin API](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). Behavior should remain otherwise the same. [PR #3997](https://github.com/apollographql/apollo-server/pull/3997) ### v2.13.0 diff --git a/packages/apollo-federation/CHANGELOG.md b/packages/apollo-federation/CHANGELOG.md index de1b2d3c5d5..a0dc25f3aef 100644 --- a/packages/apollo-federation/CHANGELOG.md +++ b/packages/apollo-federation/CHANGELOG.md @@ -6,6 +6,10 @@ - _Nothing yet! Stay tuned._ +## 0.16.1 + +- Only changes in the similarly versioned `@apollo/gateway` package. + ## 0.16.0 - No changes. This package was major versioned to maintain lockstep versioning with @apollo/gateway. diff --git a/packages/apollo-gateway/CHANGELOG.md b/packages/apollo-gateway/CHANGELOG.md index 0ce314f654d..4f3fa744a77 100644 --- a/packages/apollo-gateway/CHANGELOG.md +++ b/packages/apollo-gateway/CHANGELOG.md @@ -4,6 +4,10 @@ > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. +- _Nothing yet! Stay tuned!_ + +## 0.16.1 + - __NEW__: Provide the ability to pass a custom `fetcher` during `RemoteGraphQLDataSource` construction to be used when executing operations against downstream services. Providing a custom `fetcher` may be necessary to accommodate more advanced needs, e.g., configuring custom TLS certificates for internal services. [PR #4149](https://github.com/apollographql/apollo-server/pull/4149) The `fetcher` specified should be a compliant implementor of the [Fetch API standard](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). This addition compliments, though is still orthognonal to, similar behavior originally introduced in [#3783](https://github.com/apollographql/apollo-server/pull/3783), which allowed customization of the implementation used to fetch _gateway configuration and federated SDL from services_ in managed and unmanaged modes, but didn't affect the communication that takes place during _operation execution_. From d159e320879f594ba2d04036e3e1aa0653ff164d Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 27 May 2020 14:18:05 +0000 Subject: [PATCH 98/98] Release - apollo-cache-control@0.11.0 - apollo-datasource-rest@0.9.2 - apollo-datasource@0.7.1 - apollo-engine-reporting-protobuf@0.5.1 - apollo-engine-reporting@2.0.0 - @apollo/federation@0.16.1 - @apollo/gateway@0.16.1 - apollo-server-azure-functions@2.14.0 - apollo-server-cache-memcached@0.6.5 - apollo-server-cache-redis@1.2.1 - apollo-server-cloud-functions@2.14.0 - apollo-server-cloudflare@2.14.0 - apollo-server-core@2.14.0 - apollo-server-env@2.4.4 - apollo-server-express@2.14.0 - apollo-server-fastify@2.14.0 - apollo-server-hapi@2.14.0 - apollo-server-integration-testsuite@2.14.0 - apollo-server-koa@2.14.0 - apollo-server-lambda@2.14.0 - apollo-server-micro@2.14.0 - apollo-server-plugin-base@0.9.0 - apollo-server-plugin-operation-registry@0.3.2 - apollo-server-plugin-response-cache@0.5.2 - apollo-server-testing@2.14.0 - apollo-server-types@0.5.0 - apollo-server@2.14.0 - apollo-tracing@0.11.0 - graphql-extensions@0.12.2 --- packages/apollo-cache-control/package.json | 2 +- packages/apollo-datasource-rest/package.json | 2 +- packages/apollo-datasource/package.json | 2 +- packages/apollo-engine-reporting-protobuf/package-lock.json | 2 +- packages/apollo-engine-reporting-protobuf/package.json | 2 +- packages/apollo-engine-reporting/package.json | 2 +- packages/apollo-federation/package.json | 2 +- packages/apollo-gateway/package.json | 2 +- packages/apollo-server-azure-functions/package.json | 2 +- packages/apollo-server-cache-memcached/package.json | 2 +- packages/apollo-server-cache-redis/package.json | 2 +- packages/apollo-server-cloud-functions/package.json | 2 +- packages/apollo-server-cloudflare/package.json | 2 +- packages/apollo-server-core/package.json | 2 +- packages/apollo-server-env/package.json | 2 +- packages/apollo-server-express/package.json | 2 +- packages/apollo-server-fastify/package.json | 2 +- packages/apollo-server-hapi/package.json | 2 +- packages/apollo-server-integration-testsuite/package.json | 2 +- packages/apollo-server-koa/package.json | 2 +- packages/apollo-server-lambda/package.json | 2 +- packages/apollo-server-micro/package.json | 2 +- packages/apollo-server-plugin-base/package.json | 2 +- packages/apollo-server-plugin-operation-registry/package.json | 2 +- packages/apollo-server-plugin-response-cache/package.json | 2 +- packages/apollo-server-testing/package.json | 2 +- packages/apollo-server-types/package.json | 2 +- packages/apollo-server/package.json | 2 +- packages/apollo-tracing/package.json | 2 +- packages/graphql-extensions/package.json | 2 +- 30 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/apollo-cache-control/package.json b/packages/apollo-cache-control/package.json index ca3b1f1218a..b0366f16e2a 100644 --- a/packages/apollo-cache-control/package.json +++ b/packages/apollo-cache-control/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache-control", - "version": "0.11.0-alpha.1", + "version": "0.11.0", "description": "A GraphQL extension for cache control", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/apollo-datasource-rest/package.json b/packages/apollo-datasource-rest/package.json index c3e4e6a028a..2c70ce1a3c1 100644 --- a/packages/apollo-datasource-rest/package.json +++ b/packages/apollo-datasource-rest/package.json @@ -1,6 +1,6 @@ { "name": "apollo-datasource-rest", - "version": "0.9.2-alpha.1", + "version": "0.9.2", "author": "opensource@apollographql.com", "license": "MIT", "repository": { diff --git a/packages/apollo-datasource/package.json b/packages/apollo-datasource/package.json index 5dc85e7bfff..96c6ceda088 100644 --- a/packages/apollo-datasource/package.json +++ b/packages/apollo-datasource/package.json @@ -1,6 +1,6 @@ { "name": "apollo-datasource", - "version": "0.7.1-alpha.0", + "version": "0.7.1", "author": "opensource@apollographql.com", "license": "MIT", "repository": { diff --git a/packages/apollo-engine-reporting-protobuf/package-lock.json b/packages/apollo-engine-reporting-protobuf/package-lock.json index db3c36e4168..96f69fcc989 100644 --- a/packages/apollo-engine-reporting-protobuf/package-lock.json +++ b/packages/apollo-engine-reporting-protobuf/package-lock.json @@ -1,6 +1,6 @@ { "name": "apollo-engine-reporting-protobuf", - "version": "0.5.1-alpha.1", + "version": "0.5.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/apollo-engine-reporting-protobuf/package.json b/packages/apollo-engine-reporting-protobuf/package.json index 52026c09f69..c022754150f 100644 --- a/packages/apollo-engine-reporting-protobuf/package.json +++ b/packages/apollo-engine-reporting-protobuf/package.json @@ -1,6 +1,6 @@ { "name": "apollo-engine-reporting-protobuf", - "version": "0.5.1-alpha.1", + "version": "0.5.1", "description": "Protobuf format for Apollo Engine", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index 7d351372131..5bdc6047d34 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -1,6 +1,6 @@ { "name": "apollo-engine-reporting", - "version": "2.0.0-alpha.2", + "version": "2.0.0", "description": "Send reports about your GraphQL services to Apollo Graph Manager (previously known as Apollo Engine)", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/apollo-federation/package.json b/packages/apollo-federation/package.json index 520e31a6d76..f978561161e 100644 --- a/packages/apollo-federation/package.json +++ b/packages/apollo-federation/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/federation", - "version": "0.16.1-alpha.0", + "version": "0.16.1", "description": "Apollo Federation Utilities", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-gateway/package.json b/packages/apollo-gateway/package.json index 8d9088496ef..ffa8c1c9b73 100644 --- a/packages/apollo-gateway/package.json +++ b/packages/apollo-gateway/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/gateway", - "version": "0.16.1-alpha.2", + "version": "0.16.1", "description": "Apollo Gateway", "author": "opensource@apollographql.com", "main": "dist/index.js", diff --git a/packages/apollo-server-azure-functions/package.json b/packages/apollo-server-azure-functions/package.json index dce4aadc4c7..eff4023a287 100644 --- a/packages/apollo-server-azure-functions/package.json +++ b/packages/apollo-server-azure-functions/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-azure-functions", - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Production-ready Node.js GraphQL server for Azure Functions", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-cache-memcached/package.json b/packages/apollo-server-cache-memcached/package.json index f83ec47ec35..233ac509ecd 100644 --- a/packages/apollo-server-cache-memcached/package.json +++ b/packages/apollo-server-cache-memcached/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cache-memcached", - "version": "0.6.5-alpha.0", + "version": "0.6.5", "author": "opensource@apollographql.com", "license": "MIT", "repository": { diff --git a/packages/apollo-server-cache-redis/package.json b/packages/apollo-server-cache-redis/package.json index b0b57a1f926..3d1cef1cdef 100644 --- a/packages/apollo-server-cache-redis/package.json +++ b/packages/apollo-server-cache-redis/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cache-redis", - "version": "1.2.1-alpha.0", + "version": "1.2.1", "author": "opensource@apollographql.com", "license": "MIT", "repository": { diff --git a/packages/apollo-server-cloud-functions/package.json b/packages/apollo-server-cloud-functions/package.json index 44157166a2a..03bb8b3f0c9 100644 --- a/packages/apollo-server-cloud-functions/package.json +++ b/packages/apollo-server-cloud-functions/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cloud-functions", - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Production-ready Node.js GraphQL server for Google Cloud Functions", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-cloudflare/package.json b/packages/apollo-server-cloudflare/package.json index 1b516ffa82a..27a50cda92f 100644 --- a/packages/apollo-server-cloudflare/package.json +++ b/packages/apollo-server-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cloudflare", - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Production-ready Node.js GraphQL server for Cloudflare workers", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-core/package.json b/packages/apollo-server-core/package.json index aceb37283bb..0d7b007db9e 100644 --- a/packages/apollo-server-core/package.json +++ b/packages/apollo-server-core/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-core", - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Core engine for Apollo GraphQL server", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-env/package.json b/packages/apollo-server-env/package.json index a09556b2c5f..0a6028862a6 100644 --- a/packages/apollo-server-env/package.json +++ b/packages/apollo-server-env/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-env", - "version": "2.4.4-alpha.0", + "version": "2.4.4", "author": "opensource@apollographql.com", "license": "MIT", "repository": { diff --git a/packages/apollo-server-express/package.json b/packages/apollo-server-express/package.json index 3150adecb4a..f561839d771 100644 --- a/packages/apollo-server-express/package.json +++ b/packages/apollo-server-express/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-express", - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Production-ready Node.js GraphQL server for Express and Connect", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-fastify/package.json b/packages/apollo-server-fastify/package.json index e38bc3510e7..a3faffc9179 100644 --- a/packages/apollo-server-fastify/package.json +++ b/packages/apollo-server-fastify/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-fastify", - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Production-ready Node.js GraphQL server for Fastify", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-hapi/package.json b/packages/apollo-server-hapi/package.json index a9f07414461..e736ddaa328 100644 --- a/packages/apollo-server-hapi/package.json +++ b/packages/apollo-server-hapi/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-hapi", - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Production-ready Node.js GraphQL server for Hapi", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-integration-testsuite/package.json b/packages/apollo-server-integration-testsuite/package.json index 3e3b976ba86..fc00b73707a 100644 --- a/packages/apollo-server-integration-testsuite/package.json +++ b/packages/apollo-server-integration-testsuite/package.json @@ -1,7 +1,7 @@ { "name": "apollo-server-integration-testsuite", "private": true, - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Apollo Server Integrations testsuite", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-koa/package.json b/packages/apollo-server-koa/package.json index d8d5800d4db..30acbc55cf3 100644 --- a/packages/apollo-server-koa/package.json +++ b/packages/apollo-server-koa/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-koa", - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Production-ready Node.js GraphQL server for Koa", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-lambda/package.json b/packages/apollo-server-lambda/package.json index f45abba3f79..888623d918b 100644 --- a/packages/apollo-server-lambda/package.json +++ b/packages/apollo-server-lambda/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-lambda", - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Production-ready Node.js GraphQL server for AWS Lambda", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-micro/package.json b/packages/apollo-server-micro/package.json index c7a09032b0c..f7e5f6dbd85 100644 --- a/packages/apollo-server-micro/package.json +++ b/packages/apollo-server-micro/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-micro", - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Production-ready Node.js GraphQL server for Micro", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-plugin-base/package.json b/packages/apollo-server-plugin-base/package.json index 06d3c8ffe4c..9d71b829839 100644 --- a/packages/apollo-server-plugin-base/package.json +++ b/packages/apollo-server-plugin-base/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-plugin-base", - "version": "0.9.0-alpha.1", + "version": "0.9.0", "description": "Apollo Server plugin base classes", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-plugin-operation-registry/package.json b/packages/apollo-server-plugin-operation-registry/package.json index c0e412e0358..fbbd9e4a2f5 100644 --- a/packages/apollo-server-plugin-operation-registry/package.json +++ b/packages/apollo-server-plugin-operation-registry/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-plugin-operation-registry", - "version": "0.3.2-alpha.0", + "version": "0.3.2", "description": "Apollo Server operation registry", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-plugin-response-cache/package.json b/packages/apollo-server-plugin-response-cache/package.json index 1e3bdbfac02..d17a5f7cbbf 100644 --- a/packages/apollo-server-plugin-response-cache/package.json +++ b/packages/apollo-server-plugin-response-cache/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-plugin-response-cache", - "version": "0.5.2-alpha.1", + "version": "0.5.2", "description": "Apollo Server full query response cache", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-testing/package.json b/packages/apollo-server-testing/package.json index f95cf2939a0..7a79f0b69db 100644 --- a/packages/apollo-server-testing/package.json +++ b/packages/apollo-server-testing/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-testing", - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Test utils for apollo-server", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-types/package.json b/packages/apollo-server-types/package.json index 146c687aec0..f9c05f83b1e 100644 --- a/packages/apollo-server-types/package.json +++ b/packages/apollo-server-types/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-types", - "version": "0.5.0-alpha.1", + "version": "0.5.0", "description": "Apollo Server shared types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server/package.json b/packages/apollo-server/package.json index 794ca7d3578..738838bc912 100644 --- a/packages/apollo-server/package.json +++ b/packages/apollo-server/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server", - "version": "2.14.0-alpha.2", + "version": "2.14.0", "description": "Production ready GraphQL Server", "author": "opensource@apollographql.com", "main": "dist/index.js", diff --git a/packages/apollo-tracing/package.json b/packages/apollo-tracing/package.json index 71165100e64..db4bf3abc66 100644 --- a/packages/apollo-tracing/package.json +++ b/packages/apollo-tracing/package.json @@ -1,6 +1,6 @@ { "name": "apollo-tracing", - "version": "0.11.0-alpha.1", + "version": "0.11.0", "description": "Collect and expose trace data for GraphQL requests", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/graphql-extensions/package.json b/packages/graphql-extensions/package.json index a3ed8d41d17..1053a7f68ab 100644 --- a/packages/graphql-extensions/package.json +++ b/packages/graphql-extensions/package.json @@ -1,6 +1,6 @@ { "name": "graphql-extensions", - "version": "0.12.2-alpha.1", + "version": "0.12.2", "description": "Add extensions to GraphQL servers", "main": "./dist/index.js", "types": "./dist/index.d.ts",