Skip to content

Commit

Permalink
refactor: Introduce "cache-control" plugin and switch req. pipeline t…
Browse files Browse the repository at this point in the history
…o it.

Similar to 6009d8a, 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 eec87a6 (#3990).
  • Loading branch information
abernix committed Apr 16, 2020
1 parent 4b59b02 commit 7ef998b
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 125 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/apollo-cache-control/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CacheHint>;
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<CacheHint> = {
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<any>,
) => {
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);
});
});
});

Expand All @@ -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);
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CacheHint[]> {
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;
}
Loading

0 comments on commit 7ef998b

Please sign in to comment.