diff --git a/CHANGELOG.md b/CHANGELOG.md index a88e999bbc9..d33874d27cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The version headers in this history reflect the versions of Apollo Server itself ## vNEXT - Add `document`, `variables`, `headers` as an option in the `ApolloServerPluginLandingPageLocalDefault` plugins. The embedded version of Apollo Sandbox can now use these options as an initial state. [PR #6628](https://github.com/apollographql/apollo-server/pull/6628) +- Add `generateCacheKey` to `ApolloServerPluginResponseCache` to allow for custom cache keys. [PR #6655](https://github.com/apollographql/apollo-server/pull/6655) ## v3.9.0 diff --git a/docs/source/performance/caching.md b/docs/source/performance/caching.md index e362c3de3f7..b6d47385acd 100644 --- a/docs/source/performance/caching.md +++ b/docs/source/performance/caching.md @@ -472,3 +472,4 @@ In addition to [the `sessionId` function](#identifying-users-for-private-respons | `extraCacheKeyData` | This function's return value (any JSON-stringifiable object) is added to the key for the cached response. For example, if your API includes translatable text, this function can return a string derived from `requestContext.request.http.headers.get('Accept-Language')`. | | `shouldReadFromCache` | If this function returns `false`, Apollo Server _skips_ the cache for the incoming operation, even if a valid response is available. | | `shouldWriteToCache` | If this function returns `false`, Apollo Server doesn't cache its response for the incoming operation, even if the response's `maxAge` is greater than `0`. | +| `generateCacheKey` | Customize generation of the cache key. By default, this is the SHA256 hash of the JSON encoding of an object containing relevant data. | diff --git a/packages/apollo-server-plugin-response-cache/src/ApolloServerPluginResponseCache.ts b/packages/apollo-server-plugin-response-cache/src/ApolloServerPluginResponseCache.ts index 45009f88c65..2f5f123cf45 100644 --- a/packages/apollo-server-plugin-response-cache/src/ApolloServerPluginResponseCache.ts +++ b/packages/apollo-server-plugin-response-cache/src/ApolloServerPluginResponseCache.ts @@ -86,8 +86,23 @@ interface Options> { shouldWriteToCache?( requestContext: GraphQLRequestContext, ): ValueOrPromise; + + // This hook allows one to replace the function that is used to create a cache + // key. By default, it is the SHA-256 (from the Node `crypto` package) of the result of + // calling `JSON.stringify(keyData)`. You can override this to customize the serialization + // or the hash, or to make other changes like adding a prefix to keys to allow for + // app-specific prefix-based cache invalidation. You may assume that `keyData` is an object + // and that all relevant data will be found by the kind of iteration performed by + // `JSON.stringify`, but you should not assume anything about the particular fields on + // `keyData`. + generateCacheKey?: GenerateCacheKeyFunction; } +type GenerateCacheKeyFunction = ( + requestContext: GraphQLRequestContext>, + keyData: unknown, +) => string; + enum SessionMode { NoSession, Private, @@ -126,12 +141,6 @@ interface CacheValue { cacheTime: number; // epoch millis, used to calculate Age header } -type CacheKey = BaseCacheKey & ContextualCacheKey; - -function cacheKeyString(key: CacheKey) { - return sha(JSON.stringify(key)); -} - function isGraphQLQuery(requestContext: GraphQLRequestContext) { return requestContext.operation?.operation === 'query'; } @@ -148,6 +157,9 @@ export default function plugin( 'fqc:', ); + const generateCacheKey: GenerateCacheKeyFunction = + options.generateCacheKey ?? ((_, key) => sha(JSON.stringify(key))); + let sessionId: string | null = null; let baseCacheKey: BaseCacheKey | null = null; let age: number | null = null; @@ -165,10 +177,13 @@ export default function plugin( async function cacheGet( contextualCacheKeyFields: ContextualCacheKey, ): Promise { - const key = cacheKeyString({ + const cacheKeyData = { ...baseCacheKey!, ...contextualCacheKeyFields, - }); + }; + + const key = generateCacheKey(requestContext, cacheKeyData); + const serializedValue = await cache.get(key); if (serializedValue === undefined) { return null; @@ -278,10 +293,13 @@ export default function plugin( const cacheSetInBackground = ( contextualCacheKeyFields: ContextualCacheKey, ): void => { - const key = cacheKeyString({ + const cacheKeyData = { ...baseCacheKey!, ...contextualCacheKeyFields, - }); + }; + + const key = generateCacheKey(requestContext, cacheKeyData); + const value: CacheValue = { data, cachePolicy: policyIfCacheable,