123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- 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
|