@apollo/server
Version:
Core engine for Apollo GraphQL server
220 lines • 10.2 kB
JavaScript
import { getNamedType, isCompositeType, isInterfaceType, isObjectType, responsePathAsArray, } from 'graphql';
import { newCachePolicy } from '../../cachePolicy.js';
import { internalPlugin } from '../../internalPlugin.js';
import { LRUCache } from 'lru-cache';
export function ApolloServerPluginCacheControl(options = Object.create(null)) {
let typeAnnotationCache;
let fieldAnnotationCache;
return internalPlugin({
__internal_plugin_id__: 'CacheControl',
__is_disabled_plugin__: false,
async serverWillStart({ schema }) {
typeAnnotationCache = new LRUCache({
max: Object.values(schema.getTypeMap()).filter(isCompositeType)
.length,
});
fieldAnnotationCache = new LRUCache({
max: Object.values(schema.getTypeMap())
.filter(isObjectType)
.flatMap((t) => Object.values(t.getFields())).length +
Object.values(schema.getTypeMap())
.filter(isInterfaceType)
.flatMap((t) => Object.values(t.getFields())).length,
});
return undefined;
},
async requestDidStart(requestContext) {
function memoizedCacheAnnotationFromType(t) {
const existing = typeAnnotationCache.get(t);
if (existing) {
return existing;
}
const annotation = cacheAnnotationFromType(t);
typeAnnotationCache.set(t, annotation);
return annotation;
}
function memoizedCacheAnnotationFromField(field) {
const existing = fieldAnnotationCache.get(field);
if (existing) {
return existing;
}
const annotation = cacheAnnotationFromField(field);
fieldAnnotationCache.set(field, annotation);
return annotation;
}
const defaultMaxAge = options.defaultMaxAge ?? 0;
const calculateHttpHeaders = options.calculateHttpHeaders ?? true;
const { __testing__cacheHints } = options;
return {
async executionDidStart() {
if (isRestricted(requestContext.overallCachePolicy)) {
const fakeFieldPolicy = newCachePolicy();
return {
willResolveField({ info }) {
info.cacheControl = {
setCacheHint: (dynamicHint) => {
fakeFieldPolicy.replace(dynamicHint);
},
cacheHint: fakeFieldPolicy,
cacheHintFromType: memoizedCacheAnnotationFromType,
};
},
};
}
return {
willResolveField({ info }) {
const fieldPolicy = newCachePolicy();
let inheritMaxAge = false;
const targetType = getNamedType(info.returnType);
if (isCompositeType(targetType)) {
const typeAnnotation = memoizedCacheAnnotationFromType(targetType);
fieldPolicy.replace(typeAnnotation);
inheritMaxAge = !!typeAnnotation.inheritMaxAge;
}
const fieldAnnotation = memoizedCacheAnnotationFromField(info.parentType.getFields()[info.fieldName]);
if (fieldAnnotation.inheritMaxAge &&
fieldPolicy.maxAge === undefined) {
inheritMaxAge = true;
if (fieldAnnotation.scope) {
fieldPolicy.replace({ scope: fieldAnnotation.scope });
}
}
else {
fieldPolicy.replace(fieldAnnotation);
}
info.cacheControl = {
setCacheHint: (dynamicHint) => {
fieldPolicy.replace(dynamicHint);
},
cacheHint: fieldPolicy,
cacheHintFromType: memoizedCacheAnnotationFromType,
};
return () => {
if (fieldPolicy.maxAge === undefined &&
((isCompositeType(targetType) && !inheritMaxAge) ||
!info.path.prev)) {
fieldPolicy.restrict({ maxAge: defaultMaxAge });
}
if (__testing__cacheHints && isRestricted(fieldPolicy)) {
const path = responsePathAsArray(info.path).join('.');
if (__testing__cacheHints.has(path)) {
throw Error("shouldn't happen: addHint should only be called once per path");
}
__testing__cacheHints.set(path, {
maxAge: fieldPolicy.maxAge,
scope: fieldPolicy.scope,
});
}
requestContext.overallCachePolicy.restrict(fieldPolicy);
};
},
};
},
async willSendResponse(requestContext) {
if (!calculateHttpHeaders) {
return;
}
const { response, overallCachePolicy } = requestContext;
const existingCacheControlHeader = parseExistingCacheControlHeader(response.http.headers.get('cache-control'));
if (existingCacheControlHeader.kind === 'unparsable') {
return;
}
const cachePolicy = newCachePolicy();
cachePolicy.replace(overallCachePolicy);
if (existingCacheControlHeader.kind === 'parsable-and-cacheable') {
cachePolicy.restrict(existingCacheControlHeader.hint);
}
const policyIfCacheable = cachePolicy.policyIfCacheable();
if (policyIfCacheable &&
existingCacheControlHeader.kind !== 'uncacheable' &&
response.body.kind === 'single' &&
!response.body.singleResult.errors) {
response.http.headers.set('cache-control', `max-age=${policyIfCacheable.maxAge}, ${policyIfCacheable.scope.toLowerCase()}`);
}
else if (calculateHttpHeaders !== 'if-cacheable') {
response.http.headers.set('cache-control', CACHE_CONTROL_HEADER_UNCACHEABLE);
}
},
};
},
});
}
const CACHE_CONTROL_HEADER_CACHEABLE_REGEXP = /^max-age=(\d+), (public|private)$/;
const CACHE_CONTROL_HEADER_UNCACHEABLE = 'no-store';
function parseExistingCacheControlHeader(header) {
if (!header) {
return { kind: 'no-header' };
}
if (header === CACHE_CONTROL_HEADER_UNCACHEABLE) {
return { kind: 'uncacheable' };
}
const match = CACHE_CONTROL_HEADER_CACHEABLE_REGEXP.exec(header);
if (!match) {
return { kind: 'unparsable' };
}
return {
kind: 'parsable-and-cacheable',
hint: {
maxAge: +match[1],
scope: match[2] === 'public' ? 'PUBLIC' : 'PRIVATE',
},
};
}
function cacheAnnotationFromDirectives(directives) {
if (!directives)
return undefined;
const cacheControlDirective = directives.find((directive) => directive.name.value === 'cacheControl');
if (!cacheControlDirective)
return undefined;
if (!cacheControlDirective.arguments)
return undefined;
const maxAgeArgument = cacheControlDirective.arguments.find((argument) => argument.name.value === 'maxAge');
const scopeArgument = cacheControlDirective.arguments.find((argument) => argument.name.value === 'scope');
const inheritMaxAgeArgument = cacheControlDirective.arguments.find((argument) => argument.name.value === 'inheritMaxAge');
const scopeString = scopeArgument?.value?.kind === 'EnumValue'
? scopeArgument.value.value
: undefined;
const scope = scopeString === 'PUBLIC' || scopeString === 'PRIVATE'
? scopeString
: undefined;
if (inheritMaxAgeArgument?.value?.kind === 'BooleanValue' &&
inheritMaxAgeArgument.value.value) {
return { inheritMaxAge: true, scope };
}
return {
maxAge: maxAgeArgument?.value?.kind === 'IntValue'
? parseInt(maxAgeArgument.value.value)
: undefined,
scope,
};
}
function cacheAnnotationFromType(t) {
if (t.astNode) {
const hint = cacheAnnotationFromDirectives(t.astNode.directives);
if (hint) {
return hint;
}
}
if (t.extensionASTNodes) {
for (const node of t.extensionASTNodes) {
const hint = cacheAnnotationFromDirectives(node.directives);
if (hint) {
return hint;
}
}
}
return {};
}
function cacheAnnotationFromField(field) {
if (field.astNode) {
const hint = cacheAnnotationFromDirectives(field.astNode.directives);
if (hint) {
return hint;
}
}
return {};
}
function isRestricted(hint) {
return hint.maxAge !== undefined || hint.scope !== undefined;
}
//# sourceMappingURL=index.js.map