index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.ApolloServerPluginCacheControl = void 0;
  7. const graphql_1 = require("graphql");
  8. const cachePolicy_js_1 = require("../../cachePolicy.js");
  9. const internalPlugin_js_1 = require("../../internalPlugin.js");
  10. const lru_cache_1 = __importDefault(require("lru-cache"));
  11. function ApolloServerPluginCacheControl(options = Object.create(null)) {
  12. let typeAnnotationCache;
  13. let fieldAnnotationCache;
  14. return (0, internalPlugin_js_1.internalPlugin)({
  15. __internal_plugin_id__: 'CacheControl',
  16. __is_disabled_plugin__: false,
  17. async serverWillStart({ schema }) {
  18. typeAnnotationCache = new lru_cache_1.default({
  19. max: Object.values(schema.getTypeMap()).filter(graphql_1.isCompositeType)
  20. .length,
  21. });
  22. fieldAnnotationCache = new lru_cache_1.default({
  23. max: Object.values(schema.getTypeMap())
  24. .filter(graphql_1.isObjectType)
  25. .flatMap((t) => Object.values(t.getFields())).length +
  26. Object.values(schema.getTypeMap())
  27. .filter(graphql_1.isInterfaceType)
  28. .flatMap((t) => Object.values(t.getFields())).length,
  29. });
  30. return undefined;
  31. },
  32. async requestDidStart(requestContext) {
  33. function memoizedCacheAnnotationFromType(t) {
  34. const existing = typeAnnotationCache.get(t);
  35. if (existing) {
  36. return existing;
  37. }
  38. const annotation = cacheAnnotationFromType(t);
  39. typeAnnotationCache.set(t, annotation);
  40. return annotation;
  41. }
  42. function memoizedCacheAnnotationFromField(field) {
  43. const existing = fieldAnnotationCache.get(field);
  44. if (existing) {
  45. return existing;
  46. }
  47. const annotation = cacheAnnotationFromField(field);
  48. fieldAnnotationCache.set(field, annotation);
  49. return annotation;
  50. }
  51. const defaultMaxAge = options.defaultMaxAge ?? 0;
  52. const calculateHttpHeaders = options.calculateHttpHeaders ?? true;
  53. const { __testing__cacheHints } = options;
  54. return {
  55. async executionDidStart() {
  56. if (isRestricted(requestContext.overallCachePolicy)) {
  57. const fakeFieldPolicy = (0, cachePolicy_js_1.newCachePolicy)();
  58. return {
  59. willResolveField({ info }) {
  60. info.cacheControl = {
  61. setCacheHint: (dynamicHint) => {
  62. fakeFieldPolicy.replace(dynamicHint);
  63. },
  64. cacheHint: fakeFieldPolicy,
  65. cacheHintFromType: memoizedCacheAnnotationFromType,
  66. };
  67. },
  68. };
  69. }
  70. return {
  71. willResolveField({ info }) {
  72. const fieldPolicy = (0, cachePolicy_js_1.newCachePolicy)();
  73. let inheritMaxAge = false;
  74. const targetType = (0, graphql_1.getNamedType)(info.returnType);
  75. if ((0, graphql_1.isCompositeType)(targetType)) {
  76. const typeAnnotation = memoizedCacheAnnotationFromType(targetType);
  77. fieldPolicy.replace(typeAnnotation);
  78. inheritMaxAge = !!typeAnnotation.inheritMaxAge;
  79. }
  80. const fieldAnnotation = memoizedCacheAnnotationFromField(info.parentType.getFields()[info.fieldName]);
  81. if (fieldAnnotation.inheritMaxAge &&
  82. fieldPolicy.maxAge === undefined) {
  83. inheritMaxAge = true;
  84. if (fieldAnnotation.scope) {
  85. fieldPolicy.replace({ scope: fieldAnnotation.scope });
  86. }
  87. }
  88. else {
  89. fieldPolicy.replace(fieldAnnotation);
  90. }
  91. info.cacheControl = {
  92. setCacheHint: (dynamicHint) => {
  93. fieldPolicy.replace(dynamicHint);
  94. },
  95. cacheHint: fieldPolicy,
  96. cacheHintFromType: memoizedCacheAnnotationFromType,
  97. };
  98. return () => {
  99. if (fieldPolicy.maxAge === undefined &&
  100. (((0, graphql_1.isCompositeType)(targetType) && !inheritMaxAge) ||
  101. !info.path.prev)) {
  102. fieldPolicy.restrict({ maxAge: defaultMaxAge });
  103. }
  104. if (__testing__cacheHints && isRestricted(fieldPolicy)) {
  105. const path = (0, graphql_1.responsePathAsArray)(info.path).join('.');
  106. if (__testing__cacheHints.has(path)) {
  107. throw Error("shouldn't happen: addHint should only be called once per path");
  108. }
  109. __testing__cacheHints.set(path, {
  110. maxAge: fieldPolicy.maxAge,
  111. scope: fieldPolicy.scope,
  112. });
  113. }
  114. requestContext.overallCachePolicy.restrict(fieldPolicy);
  115. };
  116. },
  117. };
  118. },
  119. async willSendResponse(requestContext) {
  120. if (!calculateHttpHeaders) {
  121. return;
  122. }
  123. const { response, overallCachePolicy } = requestContext;
  124. const existingCacheControlHeader = parseExistingCacheControlHeader(response.http.headers.get('cache-control'));
  125. if (existingCacheControlHeader.kind === 'unparsable') {
  126. return;
  127. }
  128. const cachePolicy = (0, cachePolicy_js_1.newCachePolicy)();
  129. cachePolicy.replace(overallCachePolicy);
  130. if (existingCacheControlHeader.kind === 'parsable-and-cacheable') {
  131. cachePolicy.restrict(existingCacheControlHeader.hint);
  132. }
  133. const policyIfCacheable = cachePolicy.policyIfCacheable();
  134. if (policyIfCacheable &&
  135. existingCacheControlHeader.kind !== 'uncacheable' &&
  136. response.body.kind === 'single' &&
  137. !response.body.singleResult.errors) {
  138. response.http.headers.set('cache-control', `max-age=${policyIfCacheable.maxAge}, ${policyIfCacheable.scope.toLowerCase()}`);
  139. }
  140. else if (calculateHttpHeaders !== 'if-cacheable') {
  141. response.http.headers.set('cache-control', CACHE_CONTROL_HEADER_UNCACHEABLE);
  142. }
  143. },
  144. };
  145. },
  146. });
  147. }
  148. exports.ApolloServerPluginCacheControl = ApolloServerPluginCacheControl;
  149. const CACHE_CONTROL_HEADER_CACHEABLE_REGEXP = /^max-age=(\d+), (public|private)$/;
  150. const CACHE_CONTROL_HEADER_UNCACHEABLE = 'no-store';
  151. function parseExistingCacheControlHeader(header) {
  152. if (!header) {
  153. return { kind: 'no-header' };
  154. }
  155. if (header === CACHE_CONTROL_HEADER_UNCACHEABLE) {
  156. return { kind: 'uncacheable' };
  157. }
  158. const match = CACHE_CONTROL_HEADER_CACHEABLE_REGEXP.exec(header);
  159. if (!match) {
  160. return { kind: 'unparsable' };
  161. }
  162. return {
  163. kind: 'parsable-and-cacheable',
  164. hint: {
  165. maxAge: +match[1],
  166. scope: match[2] === 'public' ? 'PUBLIC' : 'PRIVATE',
  167. },
  168. };
  169. }
  170. function cacheAnnotationFromDirectives(directives) {
  171. if (!directives)
  172. return undefined;
  173. const cacheControlDirective = directives.find((directive) => directive.name.value === 'cacheControl');
  174. if (!cacheControlDirective)
  175. return undefined;
  176. if (!cacheControlDirective.arguments)
  177. return undefined;
  178. const maxAgeArgument = cacheControlDirective.arguments.find((argument) => argument.name.value === 'maxAge');
  179. const scopeArgument = cacheControlDirective.arguments.find((argument) => argument.name.value === 'scope');
  180. const inheritMaxAgeArgument = cacheControlDirective.arguments.find((argument) => argument.name.value === 'inheritMaxAge');
  181. const scopeString = scopeArgument?.value?.kind === 'EnumValue'
  182. ? scopeArgument.value.value
  183. : undefined;
  184. const scope = scopeString === 'PUBLIC' || scopeString === 'PRIVATE'
  185. ? scopeString
  186. : undefined;
  187. if (inheritMaxAgeArgument?.value?.kind === 'BooleanValue' &&
  188. inheritMaxAgeArgument.value.value) {
  189. return { inheritMaxAge: true, scope };
  190. }
  191. return {
  192. maxAge: maxAgeArgument?.value?.kind === 'IntValue'
  193. ? parseInt(maxAgeArgument.value.value)
  194. : undefined,
  195. scope,
  196. };
  197. }
  198. function cacheAnnotationFromType(t) {
  199. if (t.astNode) {
  200. const hint = cacheAnnotationFromDirectives(t.astNode.directives);
  201. if (hint) {
  202. return hint;
  203. }
  204. }
  205. if (t.extensionASTNodes) {
  206. for (const node of t.extensionASTNodes) {
  207. const hint = cacheAnnotationFromDirectives(node.directives);
  208. if (hint) {
  209. return hint;
  210. }
  211. }
  212. }
  213. return {};
  214. }
  215. function cacheAnnotationFromField(field) {
  216. if (field.astNode) {
  217. const hint = cacheAnnotationFromDirectives(field.astNode.directives);
  218. if (hint) {
  219. return hint;
  220. }
  221. }
  222. return {};
  223. }
  224. function isRestricted(hint) {
  225. return hint.maxAge !== undefined || hint.scope !== undefined;
  226. }
  227. //# sourceMappingURL=index.js.map