diff --git a/package-lock.json b/package-lock.json index 0eee2493ad1..94e3dfbdbe9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5626,7 +5626,7 @@ "version": "file:packages/apollo-cache-control", "requires": { "apollo-server-env": "file:packages/apollo-server-env", - "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 630c1e8d983..bd4bf8fb3c8 100644 --- a/packages/apollo-cache-control/package.json +++ b/packages/apollo-cache-control/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "apollo-server-env": "file:../apollo-server-env", - "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 deleted file mode 100644 index 29a6e0d195d..00000000000 --- a/packages/apollo-cache-control/src/__tests__/cacheControlExtension.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ResponsePath, GraphQLError } from 'graphql'; -import { GraphQLResponse } from 'graphql-extensions'; -import { Headers } from 'apollo-server-env'; -import { CacheControlExtension, CacheScope } from '../'; - -describe('CacheControlExtension', () => { - let cacheControlExtension: CacheControlExtension; - - beforeEach(() => { - cacheControlExtension = new CacheControlExtension(); - }); - - describe('willSendResponse', () => { - let graphqlResponse: GraphQLResponse; - - beforeEach(() => { - cacheControlExtension.options.calculateHttpHeaders = true; - cacheControlExtension.computeOverallCachePolicy = () => ({ - 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', - ); - }); - - const shouldNotSetCacheControlHeader = () => { - cacheControlExtension.willSendResponse && - cacheControlExtension.willSendResponse({ graphqlResponse }); - expect(graphqlResponse.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('does not set cache-control header if graphqlResponse has errors', () => { - graphqlResponse.errors = [new GraphQLError('Test Error')]; - shouldNotSetCacheControlHeader(); - }); - - it('does not set cache-control header if there is no overall cache policy', () => { - cacheControlExtension.computeOverallCachePolicy = () => undefined; - shouldNotSetCacheControlHeader(); - }); - }); - - describe('computeOverallCachePolicy', () => { - const responsePath: ResponsePath = { - key: 'test', - prev: undefined, - }; - const responseSubPath: ResponsePath = { - key: 'subTest', - prev: responsePath, - }; - const responseSubSubPath: ResponsePath = { - key: 'subSubTest', - prev: responseSubPath, - }; - - it('returns undefined without cache hints', () => { - const cachePolicy = cacheControlExtension.computeOverallCachePolicy(); - expect(cachePolicy).toBeUndefined(); - }); - - it('returns lowest max age value', () => { - cacheControlExtension.addHint(responsePath, { maxAge: 10 }); - cacheControlExtension.addHint(responseSubPath, { maxAge: 20 }); - - const cachePolicy = cacheControlExtension.computeOverallCachePolicy(); - 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 }); - - const cachePolicy = cacheControlExtension.computeOverallCachePolicy(); - expect(cachePolicy).toBeUndefined(); - }); - - it('returns PUBLIC scope by default', () => { - cacheControlExtension.addHint(responsePath, { maxAge: 10 }); - - const cachePolicy = cacheControlExtension.computeOverallCachePolicy(); - expect(cachePolicy).toHaveProperty('scope', CacheScope.Public); - }); - - it('returns PRIVATE scope if any cache hint has PRIVATE scope', () => { - cacheControlExtension.addHint(responsePath, { - maxAge: 10, - scope: CacheScope.Public, - }); - cacheControlExtension.addHint(responseSubPath, { - maxAge: 10, - scope: CacheScope.Private, - }); - - const cachePolicy = cacheControlExtension.computeOverallCachePolicy(); - expect(cachePolicy).toHaveProperty('scope', CacheScope.Private); - }); - }); -}); diff --git a/packages/apollo-cache-control/src/__tests__/cacheControlPlugin.test.ts b/packages/apollo-cache-control/src/__tests__/cacheControlPlugin.test.ts new file mode 100644 index 00000000000..a29cbbf02b1 --- /dev/null +++ b/packages/apollo-cache-control/src/__tests__/cacheControlPlugin.test.ts @@ -0,0 +1,171 @@ +import { ResponsePath, GraphQLError } from 'graphql'; +import { Headers } from 'apollo-server-env'; +import { + CacheScope, + CacheControlExtensionOptions, + CacheHint, + __testing__, + plugin, +} from '../'; +const { addHint, computeOverallCachePolicy } = __testing__; +import { + GraphQLRequestContextWillSendResponse, + GraphQLResponse, +} from 'apollo-server-plugin-base'; +import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness'; + +describe('plugin', () => { + describe('willSendResponse', () => { + 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; + }, + }); + } + + describe('HTTP cache-control header', () => { + const overallCachePolicy: Required = { + maxAge: 300, + scope: CacheScope.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 = ( + requestContext: GraphQLRequestContextWillSendResponse, + ) => { + expect( + requestContext.response.http!.headers.get('Cache-Control'), + ).toBeNull(); + }; + + it('is not set when calculateHttpHeaders is set to false', async () => { + const requestContext = await makePluginWithOptions({ + pluginInitializationOptions: { + calculateHttpHeaders: false, + }, + overallCachePolicy, + }); + shouldNotSetCacheControlHeader(requestContext); + }); + + 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', async () => { + const requestContext = await makePluginWithOptions({ + pluginInitializationOptions: { + calculateHttpHeaders: false, + }, + overallCachePolicy: undefined, + errors: true, + }); + shouldNotSetCacheControlHeader(requestContext); + }); + }); + }); + + describe('computeOverallCachePolicy', () => { + const responsePath: ResponsePath = { + key: 'test', + prev: undefined, + }; + const responseSubPath: ResponsePath = { + key: 'subTest', + prev: responsePath, + }; + const responseSubSubPath: ResponsePath = { + key: 'subSubTest', + prev: responseSubPath, + }; + + const hints = new Map(); + afterEach(() => hints.clear()); + + it('returns undefined without cache hints', () => { + const cachePolicy = computeOverallCachePolicy(hints); + expect(cachePolicy).toBeUndefined(); + }); + + it('returns lowest max age value', () => { + addHint(hints, responsePath, { maxAge: 10 }); + addHint(hints, responseSubPath, { maxAge: 20 }); + + const cachePolicy = computeOverallCachePolicy(hints); + expect(cachePolicy).toHaveProperty('maxAge', 10); + }); + + it('returns undefined if any cache hint has a maxAge of 0', () => { + addHint(hints, responsePath, { maxAge: 120 }); + addHint(hints, responseSubPath, { maxAge: 0 }); + addHint(hints, responseSubSubPath, { maxAge: 20 }); + + const cachePolicy = computeOverallCachePolicy(hints); + expect(cachePolicy).toBeUndefined(); + }); + + it('returns PUBLIC scope by default', () => { + addHint(hints, responsePath, { maxAge: 10 }); + + const cachePolicy = computeOverallCachePolicy(hints); + expect(cachePolicy).toHaveProperty('scope', CacheScope.Public); + }); + + it('returns PRIVATE scope if any cache hint has PRIVATE scope', () => { + addHint(hints, responsePath, { + maxAge: 10, + scope: CacheScope.Public, + }); + addHint(hints, responseSubPath, { + maxAge: 10, + scope: CacheScope.Private, + }); + + 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..53f8e4272fd 100644 --- a/packages/apollo-cache-control/src/index.ts +++ b/packages/apollo-cache-control/src/index.ts @@ -3,12 +3,10 @@ import { getNamedType, GraphQLInterfaceType, GraphQLObjectType, - GraphQLResolveInfo, ResponsePath, responsePathAsArray, } from 'graphql'; - -import { GraphQLExtension, GraphQLResponse } from 'graphql-extensions'; +import { ApolloServerPlugin } from "apollo-server-plugin-base"; export interface CacheControlFormat { version: 1; @@ -49,164 +47,148 @@ declare module 'apollo-server-types' { } } -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; - } +type MapResponsePathHints = Map; - if (hint.maxAge !== undefined || hint.scope !== undefined) { - this.addHint(info.path, hint); - } +export const plugin = ( + options: CacheControlExtensionOptions = Object.create(null), +): ApolloServerPlugin => ({ + requestDidStart(requestContext) { + const defaultMaxAge: number = options.defaultMaxAge || 0; + const hints: MapResponsePathHints = new Map(); - 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); + function setOverallCachePolicyWhenUnset() { + if (!requestContext.overallCachePolicy) { + requestContext.overallCachePolicy = computeOverallCachePolicy(hints); + } } - } - 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, - })), + return { + executionDidStart: () => ({ + executionDidEnd: () => setOverallCachePolicyWhenUnset(), + willResolveField({ info }) { + 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; }, - ]; - } - public willSendResponse?(o: { graphqlResponse: GraphQLResponse }) { - if ( - !this.options.calculateHttpHeaders || - !o.graphqlResponse.http || - o.graphqlResponse.errors - ) { - return; - } + willSendResponse(requestContext) { + const { + response, + overallCachePolicy: overallCachePolicyOverride, + } = requestContext; - const overallCachePolicy = this.computeOverallCachePolicy(); + // If there are any errors, we don't consider this cacheable. + if (response.errors) { + return; + } - if (overallCachePolicy) { - o.graphqlResponse.http.headers.set( - 'Cache-Control', - `max-age=${ - overallCachePolicy.maxAge - }, ${overallCachePolicy.scope.toLowerCase()}`, - ); - } - } + // 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()}`, + ); + } - public overrideOverallCachePolicy(overallCachePolicy: Required) { - this.overallCachePolicyOverride = overallCachePolicy; - } + // 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; - computeOverallCachePolicy(): Required | undefined { - if (this.overallCachePolicyOverride) { - return this.overallCachePolicyOverride; - } + const extensions = + response.extensions || (response.extensions = Object.create(null)); - let lowestMaxAge: number | undefined = undefined; - let scope: CacheScope = CacheScope.Public; + if (typeof extensions.cacheControl !== 'undefined') { + throw new Error("The cacheControl information already existed."); + } - 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; + extensions.cacheControl = { + version: 1, + hints: Array.from(hints).map(([path, hint]) => ({ + path: [...responsePathAsArray(path)], + ...hint, + })), + }; } } - - // 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, @@ -255,3 +237,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..29dff935854 100644 --- a/packages/apollo-cache-control/tsconfig.json +++ b/packages/apollo-cache-control/tsconfig.json @@ -7,6 +7,6 @@ "include": ["src/**/*"], "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 4b48feef85a..720a9822a60 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -73,6 +73,10 @@ import { Headers } from 'apollo-server-env'; import { buildServiceDefinition } from '@apollographql/apollo-tools'; import { plugin as pluginTracing } from "apollo-tracing"; import { Logger, SchemaHash } from "apollo-server-types"; +import { + plugin as pluginCacheControl, + CacheControlExtensionOptions, +} from 'apollo-cache-control'; import { getEngineApiKey, getEngineGraphVariant } from "apollo-engine-reporting/dist/agent"; const NoIntrospection = (context: ValidationContext) => ({ @@ -166,6 +170,7 @@ export class ApolloServerBase { playground, plugins, gateway, + cacheControl, experimental_approximateDocumentStoreMiB, ...requestOptions } = config; @@ -225,31 +230,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(); } @@ -772,7 +752,37 @@ export class ApolloServerBase { pluginsToInit.push(pluginTracing()) } + // 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 88655a85a1d..7b882371665 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -23,10 +23,6 @@ import { enablePluginsForSchemaResolvers, symbolUserFieldResolver, } from "./utils/schemaInstrumentation" -import { - CacheControlExtension, - CacheControlExtensionOptions, -} from 'apollo-cache-control'; import { ApolloError, fromGraphQLError, @@ -98,7 +94,6 @@ export interface GraphQLRequestPipelineConfig { extensions?: Array<() => GraphQLExtension>; persistedQueries?: PersistedQueryOptions; - cacheControl?: CacheControlExtensionOptions; formatError?: (error: GraphQLError) => GraphQLFormattedError; formatResponse?: ( @@ -127,7 +122,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; @@ -441,20 +435,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; @@ -649,11 +629,6 @@ export async function processGraphQLRequest( // objects. const extensions = config.extensions ? config.extensions.map(f => f()) : []; - 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 67f3cde40ab..90053cc2c28 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'; @@ -174,9 +173,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,