From 6ebc09366a12a6ecd45a1d5388074cac330e12cd Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 8 Dec 2022 10:45:10 -0800 Subject: [PATCH] Allow specifying `cacheKey` explicitly in request options (#114) Also add a changeset for #112 that was merged today. Fixes #65. Co-authored-by: Trevor Scheer --- .changeset/beige-teachers-think.md | 5 ++++ .changeset/light-dolls-design.md | 5 ++++ README.md | 4 +-- src/RESTDataSource.ts | 25 +++++++++++------ src/__tests__/RESTDataSource.test.ts | 40 +++++++++++++++++++++++++++- 5 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 .changeset/beige-teachers-think.md create mode 100644 .changeset/light-dolls-design.md diff --git a/.changeset/beige-teachers-think.md b/.changeset/beige-teachers-think.md new file mode 100644 index 0000000..84e3784 --- /dev/null +++ b/.changeset/beige-teachers-think.md @@ -0,0 +1,5 @@ +--- +'@apollo/datasource-rest': minor +--- + +Allow specifying the cache key directly as a `cacheKey` option in the request options. This is read by the default implementation of `cacheKeyFor` (which is still called). diff --git a/.changeset/light-dolls-design.md b/.changeset/light-dolls-design.md new file mode 100644 index 0000000..5efd632 --- /dev/null +++ b/.changeset/light-dolls-design.md @@ -0,0 +1,5 @@ +--- +'@apollo/datasource-rest': minor +--- + +Allow specifying the options passed to `new CachePolicy()` via a `httpCacheSemanticsCachePolicyOptions` option in the request options. diff --git a/README.md b/README.md index ba4f69e..6f3091d 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ If a resource's path starts with something that looks like an URL because it con #### Methods ##### `cacheKeyFor` -By default, `RESTDatasource` uses the full request URL as a cache key when saving information about the request to the `KeyValueCache`. Override this method to remove query parameters or compute a custom cache key. +By default, `RESTDatasource` uses the `cacheKey` option from the request as the cache key, or the full request URL otherwise when saving information about the request to the `KeyValueCache`. Override this method to remove query parameters or compute a custom cache key. For example, you could use this to use header fields or the HTTP method as part of the cache key. Even though we do validate header fields and don't serve responses from cache when they don't match, new responses overwrite old ones with different header fields. (For the HTTP method, this might be a positive thing, as you may want a `POST /foo` request to stop a previously cached `GET /foo` from being returned.) @@ -207,7 +207,7 @@ class MoviesAPI extends RESTDataSource { } ``` -All of the HTTP helper functions (`get`, `put`, `post`, `patch`, and `delete`) accept a second parameter for setting the `body`, `headers`, `params`, and `cacheOptions`. +All of the HTTP helper functions (`get`, `put`, `post`, `patch`, and `delete`) accept a second parameter for setting the `body`, `headers`, `params`, `cacheKey`, and `cacheOptions`. ### Intercepting fetches diff --git a/src/RESTDataSource.ts b/src/RESTDataSource.ts index ca35dcc..9673f9b 100644 --- a/src/RESTDataSource.ts +++ b/src/RESTDataSource.ts @@ -22,6 +22,12 @@ export type RequestOptions = FetcherRequestInit & { * TypeScript by @types/node.) */ params?: Record | URLSearchParams; + /** + * The default implementation of `cacheKeyFor` returns this value if it is + * provided. This is used both as part of the request deduplication key and as + * the key in the shared HTTP-header-sensitive cache. + */ + cacheKey?: string; /** * This can be a `CacheOptions` object or a function returning such an object. * The details of what its fields mean are documented under `CacheOptions`. @@ -39,7 +45,7 @@ export type RequestOptions = FetcherRequestInit & { /** * If provided, this is passed through as the third argument to `new * CachePolicy()` from the `http-cache-semantics` npm package as part of the - * HTTP header-sensitive cache. + * HTTP-header-sensitive cache. */ httpCacheSemanticsCachePolicyOptions?: HttpCacheSemanticsOptions; }; @@ -130,13 +136,16 @@ export abstract class RESTDataSource { this.httpCache = new HTTPCache(config?.cache, config?.fetch); } - // By default, we use the full request URL as the cache key. - // You can override this to remove query parameters or compute a cache key in any way that makes sense. - // For example, you could use this to take Vary header fields into account. - // Although we do validate header fields and don't serve responses from cache when they don't match, - // new responses overwrite old ones with different vary header fields. - protected cacheKeyFor(url: URL, _request: RequestOptions): string { - return url.toString(); + // By default, we use `cacheKey` from the request if provided, or the full + // request URL. You can override this to remove query parameters or compute a + // cache key in any way that makes sense. For example, you could use this to + // take header fields into account (the kinds of fields you expect to show up + // in Vary in the response). Although we do parse Vary in responses so that we + // won't return a cache entry whose Vary-ed header field doesn't match, new + // responses can overwrite old ones with different Vary-ed header fields if + // you don't take the header into account in the cache key. + protected cacheKeyFor(url: URL, request: RequestOptions): string { + return request.cacheKey ?? url.toString(); } /** diff --git a/src/__tests__/RESTDataSource.test.ts b/src/__tests__/RESTDataSource.test.ts index 949be71..eb383cf 100644 --- a/src/__tests__/RESTDataSource.test.ts +++ b/src/__tests__/RESTDataSource.test.ts @@ -755,7 +755,7 @@ describe('RESTDataSource', () => { await dataSource.getFoo(1); }); - it('allows specifying a custom cache key', async () => { + it('allows specifying a custom cache key via cacheKeyFor', async () => { const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com'; @@ -780,6 +780,44 @@ describe('RESTDataSource', () => { ]); }); + it('allows specifying a custom cache key via cacheKey used for deduplication', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; + + getFoo(id: number) { + return this.get(`foo/${id}`, { + cacheKey: 'constant', + }); + } + })(); + + nock(apiUrl).get('/foo/1').reply(200); + + await Promise.all([dataSource.getFoo(1), dataSource.getFoo(2)]); + }); + + it('allows specifying a custom cache key via cacheKey used for HTTP-header-sensitive cache', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; + protected override requestDeduplicationPolicyFor() { + return { policy: 'do-not-deduplicate' } as const; + } + + getFoo(id: number) { + return this.get(`foo/${id}`, { + cacheKey: 'constant', + }); + } + })(); + + nock(apiUrl) + .get('/foo/1') + .reply(200, '{}', { 'cache-control': 'max-age=60' }); + + await dataSource.getFoo(1); + await dataSource.getFoo(2); + }); + it('allows disabling deduplication', async () => { const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com';