Skip to content

Commit

Permalink
Allow specifying cacheKey explicitly in request options (#114)
Browse files Browse the repository at this point in the history
Also add a changeset for #112 that was merged today.

Fixes #65.

Co-authored-by: Trevor Scheer <trevor.scheer@gmail.com>
  • Loading branch information
glasser and trevor-scheer authored Dec 8, 2022
1 parent c384821 commit 6ebc093
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-teachers-think.md
Original file line number Diff line number Diff line change
@@ -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).
5 changes: 5 additions & 0 deletions .changeset/light-dolls-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@apollo/datasource-rest': minor
---

Allow specifying the options passed to `new CachePolicy()` via a `httpCacheSemanticsCachePolicyOptions` option in the request options.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.)

Expand Down Expand Up @@ -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
Expand Down
25 changes: 17 additions & 8 deletions src/RESTDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export type RequestOptions = FetcherRequestInit & {
* TypeScript by @types/node.)
*/
params?: Record<string, string | undefined> | 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`.
Expand All @@ -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;
};
Expand Down Expand Up @@ -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();
}

/**
Expand Down
40 changes: 39 additions & 1 deletion src/__tests__/RESTDataSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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';
Expand Down

0 comments on commit 6ebc093

Please sign in to comment.