Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Replace "cache-control" extension with plugin. #3997

Merged
merged 9 commits into from May 12, 2020
2 changes: 1 addition & 1 deletion package-lock.json

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

2 changes: 1 addition & 1 deletion packages/apollo-cache-control/package.json
Expand Up @@ -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"
Expand Down

This file was deleted.

171 changes: 171 additions & 0 deletions 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<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;
},
});
}

describe('HTTP cache-control header', () => {
const overallCachePolicy: Required<CacheHint> = {
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<any>,
) => {
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);
});
});
});
@@ -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;
}