From 00292a78417d212b7b772996b15107d687cd7dbb Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Thu, 7 Jul 2022 10:28:27 -0700 Subject: [PATCH 01/17] Add options to disable GET memoization --- .../src/RESTDataSource.ts | 28 +++--- .../src/__tests__/RESTDataSource.test.ts | 91 ++++++++++++++++++- 2 files changed, 107 insertions(+), 12 deletions(-) diff --git a/packages/apollo-datasource-rest/src/RESTDataSource.ts b/packages/apollo-datasource-rest/src/RESTDataSource.ts index 4d016dcd4a9..45516bac1c1 100644 --- a/packages/apollo-datasource-rest/src/RESTDataSource.ts +++ b/packages/apollo-datasource-rest/src/RESTDataSource.ts @@ -49,6 +49,9 @@ export abstract class RESTDataSource extends DataSource { httpCache!: HTTPCache; context!: TContext; memoizedResults = new Map>(); + baseURL?: string; + getCacheEnabled: boolean = true; + getCacheTTL?: number; constructor(private httpFetch?: typeof fetch) { super(); @@ -59,8 +62,6 @@ export abstract class RESTDataSource extends DataSource { this.httpCache = new HTTPCache(config.cache, this.httpFetch); } - baseURL?: string; - // 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. @@ -246,7 +247,6 @@ export abstract class RESTDataSource extends DataSource { } const request = new Request(String(url), options); - const cacheKey = this.cacheKeyFor(request); const performRequest = async () => { @@ -266,15 +266,21 @@ export abstract class RESTDataSource extends DataSource { }); }; - if (request.method === 'GET') { - let promise = this.memoizedResults.get(cacheKey); - if (promise) return promise; - - promise = performRequest(); - this.memoizedResults.set(cacheKey, promise); - return promise; + // Cache GET requests based on the calculated cache key + // You can disable the entire cache with getCacheEnabled=false + if (this.getCacheEnabled) { + if (request.method === 'GET') { + let promise = this.memoizedResults.get(cacheKey); + if (promise) return promise; + + promise = performRequest(); + this.memoizedResults.set(cacheKey, promise); + return promise; + } else { + this.memoizedResults.delete(cacheKey); + return performRequest(); + } } else { - this.memoizedResults.delete(cacheKey); return performRequest(); } } diff --git a/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts b/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts index 50761f55715..a11e120b96a 100644 --- a/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts +++ b/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts @@ -5,7 +5,11 @@ import { AuthenticationError, ForbiddenError, } from 'apollo-server-errors'; -import { RESTDataSource, RequestOptions } from '../RESTDataSource'; +import { + RESTDataSource, + RequestOptions, + CacheOptions +} from "../RESTDataSource"; import { HTTPCache } from '../HTTPCache'; import { MapKeyValueCache } from './MapKeyValueCache'; @@ -631,6 +635,26 @@ describe('RESTDataSource', () => { 'https://api.example.com/foo/1?api_key=secret', ); }); + + it('allows disabling the GET cache', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; + override getCacheEnabled = false; + + getFoo(id: number) { + return this.get(`foo/${id}`); + } + })(); + + dataSource.httpCache = httpCache; + + fetch.mockJSONResponseOnce(); + fetch.mockJSONResponseOnce(); + + await Promise.all([dataSource.getFoo(1), dataSource.getFoo(1)]); + + expect(fetch).toBeCalledTimes(2); + }); }); describe('error handling', () => { @@ -779,4 +803,69 @@ describe('RESTDataSource', () => { ); }); }); + + describe('http cache', () => { + it('allows setting cache options for each request', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; + override getCacheEnabled = false; + + getFoo(id: number) { + return this.get(`foo/${id}`); + } + + // Set a long TTL for every request + // @ts-ignore + override cacheOptionsFor(_, __): CacheOptions | undefined { + return { + ttl: 1000000 + }; + } + })(); + + dataSource.httpCache = httpCache; + + fetch.mockJSONResponseOnce(); + await dataSource.getFoo(1); + + // Call a second time which should be cached + fetch.mockJSONResponseOnce(); + await dataSource.getFoo(1); + + expect(fetch).toBeCalledTimes(1); + }); + + it('allows setting a short TTL for the cache', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; + override getCacheEnabled = false; + + getFoo(id: number) { + return this.get(`foo/${id}`); + } + + // Set a short TTL for every request + // @ts-ignore + override cacheOptionsFor(_, __): CacheOptions | undefined { + return { + ttl: 1 + }; + } + })(); + + dataSource.httpCache = httpCache; + + fetch.mockJSONResponseOnce(); + await dataSource.getFoo(1); + + // Sleep for a little to expire cache + await new Promise((r) => setTimeout(r, 2000)); + + // Call a second time which should be invalid now + fetch.mockJSONResponseOnce(); + await dataSource.getFoo(1); + + expect(fetch).toBeCalledTimes(2); + }); + }); }); From b0a43f612bc41e99dbefb12b83848730a61d1bff Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Thu, 7 Jul 2022 10:30:31 -0700 Subject: [PATCH 02/17] Remove unnessecary changes --- packages/apollo-datasource-rest/src/RESTDataSource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-datasource-rest/src/RESTDataSource.ts b/packages/apollo-datasource-rest/src/RESTDataSource.ts index 45516bac1c1..9a286710ed3 100644 --- a/packages/apollo-datasource-rest/src/RESTDataSource.ts +++ b/packages/apollo-datasource-rest/src/RESTDataSource.ts @@ -51,7 +51,6 @@ export abstract class RESTDataSource extends DataSource { memoizedResults = new Map>(); baseURL?: string; getCacheEnabled: boolean = true; - getCacheTTL?: number; constructor(private httpFetch?: typeof fetch) { super(); @@ -247,6 +246,7 @@ export abstract class RESTDataSource extends DataSource { } const request = new Request(String(url), options); + const cacheKey = this.cacheKeyFor(request); const performRequest = async () => { From 6b890aa4eb8284c8a0e0653378460dc64fc12ed9 Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Thu, 7 Jul 2022 14:03:04 -0700 Subject: [PATCH 03/17] WIP: Start updating docs: --- docs/source/data/data-sources.mdx | 41 ++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/docs/source/data/data-sources.mdx b/docs/source/data/data-sources.mdx index ca0ef2edc32..4a2087755bf 100644 --- a/docs/source/data/data-sources.mdx +++ b/docs/source/data/data-sources.mdx @@ -1,6 +1,7 @@ --- title: Data sources description: Manage connections to databases and REST APIs +api_reference: true --- **Data sources** are classes that Apollo Server can use to encapsulate fetching data from a particular source, such as a database or a REST API. These classes help handle caching, deduplication, and errors while resolving operations. @@ -96,6 +97,14 @@ By default, data source implementations use Apollo Server's in-memory cache to s When you initialize Apollo Server, you can provide its constructor a _different_ cache object that implements the [`KeyValueCache` interface](https://github.com/apollographql/apollo-utils/tree/main/packages/keyValueCache#keyvaluecache-interface). This enables you to back your cache with shared stores like Memcached or Redis. +```js title=server.js +const server = new ApolloServer({ + typeDefs, + resolvers, + cache: new MyCustomKeyValueCache() +}); +``` + ### Using an external cache backend When running multiple instances of your server, you should use a shared cache backend. This enables one server instance to use the cached result from _another_ instance. @@ -104,7 +113,7 @@ Apollo Server supports using [Memcached](https://memcached.org/), [Redis](https: You can also choose to implement your own cache backend. For more information, see [Implementing your own cache backend](../performance/cache-backends#implementing-your-own-cache-backend). -## `RESTDataSource` reference +## `RESTDataSource` The `RESTDataSource` abstract class helps you fetch data from REST APIs. Your server defines a separate subclass of `RESTDataSource` for each REST API it communicates with. @@ -116,6 +125,36 @@ npm install apollo-datasource-rest You then extend the `RESTDataSource` class and implement whatever data-fetching methods your resolvers need. These methods can use built-in convenience methods (like `get` and `post`) to perform HTTP requests, helping you add query parameters, parse JSON results, and handle errors. +### API reference + + + + + + + + + + + + + + + + + + +
Name /
Type
Description
+ + **Constructor options** +
+ ##### `httpFetch` + + typeof `fetch` + + Override the `fetch` implementation used when sending requests to the datasource +
+ ### Example Here's an example `RESTDataSource` subclass that defines two data-fetching methods, `getMovie` and `getMostViewedMovies`: From d783e5b56281d61632f3ad4b950ed5798a0a446e Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Thu, 7 Jul 2022 16:05:52 -0700 Subject: [PATCH 04/17] Copy table from existing page --- docs/source/data/data-sources.mdx | 92 ++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 21 deletions(-) diff --git a/docs/source/data/data-sources.mdx b/docs/source/data/data-sources.mdx index 4a2087755bf..bd7d07000c7 100644 --- a/docs/source/data/data-sources.mdx +++ b/docs/source/data/data-sources.mdx @@ -129,29 +129,79 @@ You then extend the `RESTDataSource` class and implement whatever data-fetching - - - - + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + +
Name /
Type
Description
Name /
Type
Description
- - **Constructor options** -
- ##### `httpFetch` - - typeof `fetch` - - Override the `fetch` implementation used when sending requests to the datasource -
+ + **Constructor options** +
+ + ##### `healthCheckPath` + + `string` or `null` + + + Disable [HTTP-level health checks](../monitoring/health-checks/#http-level-health-checks) by passing `null`, or change the path on which it is served from the default of `/.well-known/apollo/server-health`. + +
+ + ##### `onHealthCheck` + + `Function` + + + A custom function to execute when Apollo Server receives a request at the [HTTP-level health check](../monitoring/health-checks/#http-level-health-checks) endpoint. + +
+ + ##### `cors` + + `Object` or `Boolean` + + + An `Object` containing [configuration options](https://github.com/expressjs/cors#configuration-options) for the server's CORS behavior. Provide `false` to remove CORS middleware entirely. + +
+ + ##### `stopGracePeriodMillis` + + `number` + + + The amount of time to wait after [`ApolloServer.stop()`](#stop) is called (including via a [termination signal](#stoponterminationsignals)) before forcefully closing all active connections. If you pass `Infinity`, Apollo Server waits indefinitely for all active connections to go idle. + + The default value is `10_000` (10 seconds). + +
From 60ca0fbc9892c74402c902d1601c9e53a7bcbc67 Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Thu, 7 Jul 2022 16:24:08 -0700 Subject: [PATCH 05/17] Update table docs --- docs/source/api/apollo-server.mdx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/source/api/apollo-server.mdx b/docs/source/api/apollo-server.mdx index 5ec10853c53..3c55aaba6fa 100644 --- a/docs/source/api/apollo-server.mdx +++ b/docs/source/api/apollo-server.mdx @@ -44,20 +44,16 @@ const server = new ApolloServer({ **Schema options** - + -##### `typeDefs` +##### `httpFetch` -`DocumentNode` or `Array` +`fetch` implementation - -Document or documents that represent your server's GraphQL schema, generated by applying the `gql` tag to valid Schema Definition Language (SDL) strings. - -**Required** unless you provide [`schema`](#schema). - -For an example, see [Define your GraphQL schema](../getting-started/#step-3-define-your-graphql-schema). +**Optional** +Provide a custom `fetch` implementation to be used when making REST calls. From 9c1b48aaf912ecb967a0f858346cb86c9c00e4d0 Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Fri, 15 Jul 2022 10:36:41 -0700 Subject: [PATCH 06/17] Create lemon-goats-happen.md --- .changeset/lemon-goats-happen.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lemon-goats-happen.md diff --git a/.changeset/lemon-goats-happen.md b/.changeset/lemon-goats-happen.md new file mode 100644 index 00000000000..f21d3a536fb --- /dev/null +++ b/.changeset/lemon-goats-happen.md @@ -0,0 +1,5 @@ +--- +"apollo-datasource-rest": patch +--- + +Add option to disable RESTDatasource GET cache From fc14245723b1936217e07b3c4533d6a92032941f Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Fri, 15 Jul 2022 11:22:03 -0700 Subject: [PATCH 07/17] Update docs and property name --- docs/source/api/apollo-server.mdx | 13 +- docs/source/data/data-sources.mdx | 119 ++++++++---------- .../src/RESTDataSource.ts | 4 +- .../src/__tests__/RESTDataSource.test.ts | 10 +- 4 files changed, 70 insertions(+), 76 deletions(-) diff --git a/docs/source/api/apollo-server.mdx b/docs/source/api/apollo-server.mdx index 3c55aaba6fa..ef1a5abd73b 100644 --- a/docs/source/api/apollo-server.mdx +++ b/docs/source/api/apollo-server.mdx @@ -44,16 +44,19 @@ const server = new ApolloServer({ **Schema options** - + -##### `httpFetch` +##### `typeDefs` -`fetch` implementation + `DocumentNode` or `Array` -**Optional** -Provide a custom `fetch` implementation to be used when making REST calls. + Document or documents that represent your server's GraphQL schema, generated by applying the `gql` tag to valid Schema Definition Language (SDL) strings. + + **Required** unless you provide [`schema`](#schema). + + For an example, see [Define your GraphQL schema](../getting-started/#step-3-define-your-graphql-schema). diff --git a/docs/source/data/data-sources.mdx b/docs/source/data/data-sources.mdx index bd7d07000c7..b08a38a544d 100644 --- a/docs/source/data/data-sources.mdx +++ b/docs/source/data/data-sources.mdx @@ -1,7 +1,6 @@ --- title: Data sources description: Manage connections to databases and REST APIs -api_reference: true --- **Data sources** are classes that Apollo Server can use to encapsulate fetching data from a particular source, such as a database or a REST API. These classes help handle caching, deduplication, and errors while resolving operations. @@ -97,7 +96,7 @@ By default, data source implementations use Apollo Server's in-memory cache to s When you initialize Apollo Server, you can provide its constructor a _different_ cache object that implements the [`KeyValueCache` interface](https://github.com/apollographql/apollo-utils/tree/main/packages/keyValueCache#keyvaluecache-interface). This enables you to back your cache with shared stores like Memcached or Redis. -```js title=server.js +```js title="server.js" const server = new ApolloServer({ typeDefs, resolvers, @@ -125,85 +124,77 @@ npm install apollo-datasource-rest You then extend the `RESTDataSource` class and implement whatever data-fetching methods your resolvers need. These methods can use built-in convenience methods (like `get` and `post`) to perform HTTP requests, helping you add query parameters, parse JSON results, and handle errors. -### API reference +### API Reference +To see the all the properties and functions that can be overridden, the [source code](https://github.com/apollographql/apollo-server/tree/main/packages/apollo-datasource-rest) is always the best option. - - - - - - - - - - - +Optional constructor option which allows overriding the `fetch` implementation used when calling data sources. - - - - - - - - - +#### Overridable Properties +##### `baseURL` +Optional value to use for all the REST calls. If it is set in your class implementation, this base URL is used as the prefix for all calls. If it is not set, then the value passed to the REST call is exactly the value used. +```js title="baseURL.js" +class MoviesAPI extends RESTDataSource { + constructor() { + super(); + this.baseURL = 'https://movies-api.example.com/'; + } - - - - + // Outgoing requests are never cached, however the response cache is still enabled + async getMovie(id) { + return this.get( + `https://movies-api.example.com/movies/${encodeURIComponent(id)}` // path + ); + } +} +``` - - - - +##### `didReceiveResponse` +By default, this method checks if the response was returned successfully and parses the response into the result object. If the response had an error, it detects which type of HTTP error and throws the error result. +If you override this behaviour, be sure to implement the proper error handling. - -
Name /
Type
Description
+#### Constructor Parameters +##### `httpFetch` - **Constructor options** -
- - ##### `healthCheckPath` - - `string` or `null` - - - Disable [HTTP-level health checks](../monitoring/health-checks/#http-level-health-checks) by passing `null`, or change the path on which it is served from the default of `/.well-known/apollo/server-health`. - -
- - ##### `onHealthCheck` - - `Function` - - - A custom function to execute when Apollo Server receives a request at the [HTTP-level health check](../monitoring/health-checks/#http-level-health-checks) endpoint. - -
+ // GET + async getMovie(id) { + return this.get( + `movies/${encodeURIComponent(id)}` // path + ); + } +} +``` - ##### `cors` +##### `requestCacheEnabled` +By default, `RESTDataSource` caches all outgoing GET **requests** in a separate memoized cache from the regular response cache. It does this to prevent duplicate calls that might happen in rapid succession. +If a request is made with the same cache key (URL by default) with an HTTP method other than GET, the cached request is then cleared. - `Object` or `Boolean` - +If you would like to disable the GET request cache, set the `requestCacheEnabled` property to `false`. - An `Object` containing [configuration options](https://github.com/expressjs/cors#configuration-options) for the server's CORS behavior. Provide `false` to remove CORS middleware entirely. +```js title="requestCacheEnabled.js" +class MoviesAPI extends RESTDataSource { + constructor() { + super(); + // Defaults to true + this.requestCacheEnabled = false; + } -
+#### Overridable methods - ##### `stopGracePeriodMillis` +##### `cacheKeyFor` +By default, `RESTDatasource` uses the full request URL as the cache key. Override this method to remove query parameters or compute a custom cache key. - `number` - +For example, you could use this to use header fields 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. - The amount of time to wait after [`ApolloServer.stop()`](#stop) is called (including via a [termination signal](#stoponterminationsignals)) before forcefully closing all active connections. If you pass `Infinity`, Apollo Server waits indefinitely for all active connections to go idle. +##### `willSendRequest` +This method is invoked just before the fetch call is made. If a `Promise` is returned from this method it will wait until the promise is completed to continue executing the request. - The default value is `10_000` (10 seconds). +##### `cacheOptionsFor` +Allows setting the `CacheOptions` to be used for each request/response in the HTTPCache. This is separate from the request-only cache. -
+##### `didEncounterError` +By default, this method just throws the `error` it was given. If you override this method, you can choose to either perform some additional logic and still throw, or to swallow the error by not throwing the error result. ### Example diff --git a/packages/apollo-datasource-rest/src/RESTDataSource.ts b/packages/apollo-datasource-rest/src/RESTDataSource.ts index 9a286710ed3..6f086fb1139 100644 --- a/packages/apollo-datasource-rest/src/RESTDataSource.ts +++ b/packages/apollo-datasource-rest/src/RESTDataSource.ts @@ -50,7 +50,7 @@ export abstract class RESTDataSource extends DataSource { context!: TContext; memoizedResults = new Map>(); baseURL?: string; - getCacheEnabled: boolean = true; + requestCacheEnabled: boolean = true; constructor(private httpFetch?: typeof fetch) { super(); @@ -268,7 +268,7 @@ export abstract class RESTDataSource extends DataSource { // Cache GET requests based on the calculated cache key // You can disable the entire cache with getCacheEnabled=false - if (this.getCacheEnabled) { + if (this.requestCacheEnabled) { if (request.method === 'GET') { let promise = this.memoizedResults.get(cacheKey); if (promise) return promise; diff --git a/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts b/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts index a11e120b96a..b97241b9e91 100644 --- a/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts +++ b/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts @@ -506,8 +506,8 @@ describe('RESTDataSource', () => { }); }); - describe('memoization', () => { - it('deduplicates requests with the same cache key', async () => { + describe('memoization/request cache', () => { + it('de-duplicates requests with the same cache key', async () => { const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com'; @@ -639,7 +639,7 @@ describe('RESTDataSource', () => { it('allows disabling the GET cache', async () => { const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com'; - override getCacheEnabled = false; + override requestCacheEnabled = false; getFoo(id: number) { return this.get(`foo/${id}`); @@ -808,7 +808,7 @@ describe('RESTDataSource', () => { it('allows setting cache options for each request', async () => { const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com'; - override getCacheEnabled = false; + override requestCacheEnabled = false; getFoo(id: number) { return this.get(`foo/${id}`); @@ -838,7 +838,7 @@ describe('RESTDataSource', () => { it('allows setting a short TTL for the cache', async () => { const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com'; - override getCacheEnabled = false; + override requestCacheEnabled = false; getFoo(id: number) { return this.get(`foo/${id}`); From acd1de369fe7564fa4849dc0328830482a948ae5 Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Fri, 15 Jul 2022 11:27:15 -0700 Subject: [PATCH 08/17] Fix linter and spellcheck --- docs/source/data/data-sources.mdx | 6 +++--- .../src/__tests__/RESTDataSource.test.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/source/data/data-sources.mdx b/docs/source/data/data-sources.mdx index b08a38a544d..a046ada9942 100644 --- a/docs/source/data/data-sources.mdx +++ b/docs/source/data/data-sources.mdx @@ -132,7 +132,7 @@ To see the all the properties and functions that can be overridden, the [source Optional constructor option which allows overriding the `fetch` implementation used when calling data sources. -#### Overridable Properties +#### Properties ##### `baseURL` Optional value to use for all the REST calls. If it is set in your class implementation, this base URL is used as the prefix for all calls. If it is not set, then the value passed to the REST call is exactly the value used. @@ -175,7 +175,7 @@ class MoviesAPI extends RESTDataSource { } ``` -#### Overridable methods +#### Methods ##### `cacheKeyFor` By default, `RESTDatasource` uses the full request URL as the cache key. Override this method to remove query parameters or compute a custom cache key. @@ -191,7 +191,7 @@ Allows setting the `CacheOptions` to be used for each request/response in the HT ##### `didReceiveResponse` By default, this method checks if the response was returned successfully and parses the response into the result object. If the response had an error, it detects which type of HTTP error and throws the error result. -If you override this behaviour, be sure to implement the proper error handling. +If you override this behavior, be sure to implement the proper error handling. ##### `didEncounterError` By default, this method just throws the `error` it was given. If you override this method, you can choose to either perform some additional logic and still throw, or to swallow the error by not throwing the error result. diff --git a/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts b/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts index b97241b9e91..fbbec5d5025 100644 --- a/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts +++ b/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts @@ -8,8 +8,8 @@ import { import { RESTDataSource, RequestOptions, - CacheOptions -} from "../RESTDataSource"; + CacheOptions, +} from '../RESTDataSource'; import { HTTPCache } from '../HTTPCache'; import { MapKeyValueCache } from './MapKeyValueCache'; @@ -818,7 +818,7 @@ describe('RESTDataSource', () => { // @ts-ignore override cacheOptionsFor(_, __): CacheOptions | undefined { return { - ttl: 1000000 + ttl: 1000000, }; } })(); @@ -848,7 +848,7 @@ describe('RESTDataSource', () => { // @ts-ignore override cacheOptionsFor(_, __): CacheOptions | undefined { return { - ttl: 1 + ttl: 1, }; } })(); From 6ec89e8ab90c112ce9e5a941ea1d744596960fda Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Fri, 15 Jul 2022 11:41:21 -0700 Subject: [PATCH 09/17] Revert accidental doc changes --- docs/source/api/apollo-server.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/api/apollo-server.mdx b/docs/source/api/apollo-server.mdx index ef1a5abd73b..8e6fe695b52 100644 --- a/docs/source/api/apollo-server.mdx +++ b/docs/source/api/apollo-server.mdx @@ -49,14 +49,14 @@ const server = new ApolloServer({ ##### `typeDefs` - `DocumentNode` or `Array` +`DocumentNode` or `Array` - Document or documents that represent your server's GraphQL schema, generated by applying the `gql` tag to valid Schema Definition Language (SDL) strings. +Document or documents that represent your server's GraphQL schema, generated by applying the `gql` tag to valid Schema Definition Language (SDL) strings. - **Required** unless you provide [`schema`](#schema). +**Required** unless you provide [`schema`](#schema). - For an example, see [Define your GraphQL schema](../getting-started/#step-3-define-your-graphql-schema). +For an example, see [Define your GraphQL schema](../getting-started/#step-3-define-your-graphql-schema). From 6e8aed007cf891cc4ed3f91a4c97a972deb4a842 Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Fri, 15 Jul 2022 11:43:54 -0700 Subject: [PATCH 10/17] Revert new line --- docs/source/api/apollo-server.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/api/apollo-server.mdx b/docs/source/api/apollo-server.mdx index 8e6fe695b52..5ec10853c53 100644 --- a/docs/source/api/apollo-server.mdx +++ b/docs/source/api/apollo-server.mdx @@ -52,6 +52,7 @@ const server = new ApolloServer({ `DocumentNode` or `Array` + Document or documents that represent your server's GraphQL schema, generated by applying the `gql` tag to valid Schema Definition Language (SDL) strings. **Required** unless you provide [`schema`](#schema). From 4487ba05a6d3bee7ac51ad4e99c8e23312087174 Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Fri, 15 Jul 2022 12:13:29 -0700 Subject: [PATCH 11/17] Update README too with doc updates --- packages/apollo-datasource-rest/README.md | 74 +++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/packages/apollo-datasource-rest/README.md b/packages/apollo-datasource-rest/README.md index 12a5121f784..fc55713182f 100644 --- a/packages/apollo-datasource-rest/README.md +++ b/packages/apollo-datasource-rest/README.md @@ -41,6 +41,80 @@ class MoviesAPI extends RESTDataSource { } ``` +## API Reference +View the source code to see the all the properties and functions that can be overridden and their specific parameters. This section lists the usage and default behavior of the `RESTDatasource` class. + +### Constructor Parameters +#### `httpFetch` + +Optional constructor option which allows overriding the `fetch` implementation used when calling data sources. + +### Properties +#### `baseURL` +Optional value to use for all the REST calls. If it is set in your class implementation, this base URL is used as the prefix for all calls. If it is not set, then the value passed to the REST call is exactly the value used. + +```js title="baseURL.js" +class MoviesAPI extends RESTDataSource { + constructor() { + super(); + this.baseURL = 'https://movies-api.example.com/'; + } + + // GET + async getMovie(id) { + return this.get( + `movies/${encodeURIComponent(id)}` // path + ); + } +} +``` + +#### `requestCacheEnabled` +By default, `RESTDataSource` caches all outgoing GET **requests** in a separate memoized cache from the regular response cache. It does this to prevent duplicate calls that might happen in rapid succession. +If a request is made with the same cache key (URL by default) with an HTTP method other than GET, the cached request is then cleared. + +If you would like to disable the GET request cache, set the `requestCacheEnabled` property to `false`. + +```js title="requestCacheEnabled.js" +class MoviesAPI extends RESTDataSource { + constructor() { + super(); + // Defaults to true + this.requestCacheEnabled = false; + } + + // Outgoing requests are never cached, however the response cache is still enabled + async getMovie(id) { + return this.get( + `https://movies-api.example.com/movies/${encodeURIComponent(id)}` // path + ); + } +} +``` + +### Methods + +#### `cacheKeyFor` +By default, `RESTDatasource` uses the full request URL as the cache key. Override this method to remove query parameters or compute a custom cache key. + +For example, you could use this to use header fields 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. + +#### `willSendRequest` +This method is invoked just before the fetch call is made. If a `Promise` is returned from this method it will wait until the promise is completed to continue executing the request. + +#### `cacheOptionsFor` +Allows setting the `CacheOptions` to be used for each request/response in the HTTPCache. This is separate from the request-only cache. + +#### `didReceiveResponse` +By default, this method checks if the response was returned successfully and parses the response into the result object. If the response had an error, it detects which type of HTTP error and throws the error result. + +If you override this behavior, be sure to implement the proper error handling. + +#### `didEncounterError` +By default, this method just throws the `error` it was given. If you override this method, you can choose to either perform some additional logic and still throw, or to swallow the error by not throwing the error result. + + +## Examples ### HTTP Methods The `get` method on the [RESTDataSource](https://github.com/apollographql/apollo-server/tree/main/packages/apollo-datasource-rest) makes an HTTP `GET` request. Similarly, there are methods built-in to allow for POST, PUT, PATCH, and DELETE requests. From defc8151cb5c66df7074419eda5a6b624055243e Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Fri, 15 Jul 2022 12:16:00 -0700 Subject: [PATCH 12/17] Update code comment --- packages/apollo-datasource-rest/src/RESTDataSource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-datasource-rest/src/RESTDataSource.ts b/packages/apollo-datasource-rest/src/RESTDataSource.ts index 6f086fb1139..52be06c8b6a 100644 --- a/packages/apollo-datasource-rest/src/RESTDataSource.ts +++ b/packages/apollo-datasource-rest/src/RESTDataSource.ts @@ -267,7 +267,7 @@ export abstract class RESTDataSource extends DataSource { }; // Cache GET requests based on the calculated cache key - // You can disable the entire cache with getCacheEnabled=false + // Disabling the request cache does not disable the response cache if (this.requestCacheEnabled) { if (request.method === 'GET') { let promise = this.memoizedResults.get(cacheKey); From a022acf2c86592f0e5e52d5bffb67df73cd7e866 Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Fri, 19 Aug 2022 14:29:55 -0700 Subject: [PATCH 13/17] Remove ts-ignore --- .../src/__tests__/RESTDataSource.test.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts b/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts index fbbec5d5025..10fbec9321a 100644 --- a/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts +++ b/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts @@ -806,7 +806,7 @@ describe('RESTDataSource', () => { describe('http cache', () => { it('allows setting cache options for each request', async () => { - const dataSource = new (class extends RESTDataSource { + const dataSource = new class extends RESTDataSource { override baseURL = 'https://api.example.com'; override requestCacheEnabled = false; @@ -815,13 +815,12 @@ describe('RESTDataSource', () => { } // Set a long TTL for every request - // @ts-ignore - override cacheOptionsFor(_, __): CacheOptions | undefined { + override cacheOptionsFor(): CacheOptions | undefined { return { ttl: 1000000, }; } - })(); + }(); dataSource.httpCache = httpCache; @@ -836,7 +835,7 @@ describe('RESTDataSource', () => { }); it('allows setting a short TTL for the cache', async () => { - const dataSource = new (class extends RESTDataSource { + const dataSource = new class extends RESTDataSource { override baseURL = 'https://api.example.com'; override requestCacheEnabled = false; @@ -845,13 +844,12 @@ describe('RESTDataSource', () => { } // Set a short TTL for every request - // @ts-ignore - override cacheOptionsFor(_, __): CacheOptions | undefined { + override cacheOptionsFor(): CacheOptions | undefined { return { ttl: 1, }; } - })(); + }(); dataSource.httpCache = httpCache; From a66e49dde592ff421af651cf6f32cdd6ece13503 Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Fri, 19 Aug 2022 14:36:56 -0700 Subject: [PATCH 14/17] Add README explination --- packages/apollo-datasource-rest/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/apollo-datasource-rest/README.md b/packages/apollo-datasource-rest/README.md index fc55713182f..b445f3a42a0 100644 --- a/packages/apollo-datasource-rest/README.md +++ b/packages/apollo-datasource-rest/README.md @@ -70,10 +70,10 @@ class MoviesAPI extends RESTDataSource { ``` #### `requestCacheEnabled` -By default, `RESTDataSource` caches all outgoing GET **requests** in a separate memoized cache from the regular response cache. It does this to prevent duplicate calls that might happen in rapid succession. -If a request is made with the same cache key (URL by default) with an HTTP method other than GET, the cached request is then cleared. +By default, `RESTDataSource` caches all outgoing GET **requests** in a separate memoized cache from the regular response cache. It makes the assumption that all responses from HTTP GET calls are cacheable by their URL. +If a request is made with the same cache key (URL by default) but with an HTTP method other than GET, the cached request is then cleared. -If you would like to disable the GET request cache, set the `requestCacheEnabled` property to `false`. +If you would like to disable the GET request cache, set the `requestCacheEnabled` property to `false`. You might want to do this if your API is not actually cacheable or your data changes over time. ```js title="requestCacheEnabled.js" class MoviesAPI extends RESTDataSource { From 67681112ba3e15b23d9ac49d7abdf02de66396f5 Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Fri, 19 Aug 2022 14:42:32 -0700 Subject: [PATCH 15/17] Rebase from main --- .changeset/lemon-goats-happen.md | 5 ----- CHANGELOG.md | 2 ++ 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 .changeset/lemon-goats-happen.md diff --git a/.changeset/lemon-goats-happen.md b/.changeset/lemon-goats-happen.md deleted file mode 100644 index f21d3a536fb..00000000000 --- a/.changeset/lemon-goats-happen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"apollo-datasource-rest": patch ---- - -Add option to disable RESTDatasource GET cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 16335c65314..b7b1905755d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The version headers in this history reflect the versions of Apollo Server itself ## vNEXT +- [apollo-datasource-rest] Add option to disable GET cache [PR #6650](https://github.com/apollographql/apollo-server/pull/6650) + ## v3.10.1 - ⚠️ **SECURITY**: The default landing page contained HTML to display a sample `curl` command which is made visible if the full landing page bundle could not be fetched from Apollo's CDN. The server's URL is directly interpolated into this command inside the browser from `window.location.href`. On some older browsers such as IE11, this value is not URI-encoded. On such browsers, opening a malicious URL pointing at an Apollo Router could cause execution of attacker-controlled JavaScript. In this release, the fallback page does not display a `curl` command. More details are available at the [security advisory](https://github.com/apollographql/apollo-server/security/advisories/GHSA-2fvv-qxrq-7jq6). From a1f3f5a73bd1f125cb9d6c48d62523782f2d540c Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Fri, 19 Aug 2022 14:47:17 -0700 Subject: [PATCH 16/17] Update /docs with same README info --- docs/source/data/data-sources.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/data/data-sources.mdx b/docs/source/data/data-sources.mdx index a046ada9942..0a07abceb2d 100644 --- a/docs/source/data/data-sources.mdx +++ b/docs/source/data/data-sources.mdx @@ -153,10 +153,10 @@ class MoviesAPI extends RESTDataSource { ``` ##### `requestCacheEnabled` -By default, `RESTDataSource` caches all outgoing GET **requests** in a separate memoized cache from the regular response cache. It does this to prevent duplicate calls that might happen in rapid succession. -If a request is made with the same cache key (URL by default) with an HTTP method other than GET, the cached request is then cleared. +By default, `RESTDataSource` caches all outgoing GET **requests** in a separate memoized cache from the regular response cache. It makes the assumption that all responses from HTTP GET calls are cacheable by their URL. +If a request is made with the same cache key (URL by default) but with an HTTP method other than GET, the cached request is then cleared. -If you would like to disable the GET request cache, set the `requestCacheEnabled` property to `false`. +If you would like to disable the GET request cache, set the `requestCacheEnabled` property to `false`. You might want to do this if your API is not actually cacheable or your data changes over time. ```js title="requestCacheEnabled.js" class MoviesAPI extends RESTDataSource { From 0134a13b5b62b87d454662c4189c2523b2985110 Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Fri, 19 Aug 2022 14:48:49 -0700 Subject: [PATCH 17/17] Run prettier --- .../src/__tests__/RESTDataSource.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts b/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts index 10fbec9321a..d9053b0b31d 100644 --- a/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts +++ b/packages/apollo-datasource-rest/src/__tests__/RESTDataSource.test.ts @@ -806,7 +806,7 @@ describe('RESTDataSource', () => { describe('http cache', () => { it('allows setting cache options for each request', async () => { - const dataSource = new class extends RESTDataSource { + const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com'; override requestCacheEnabled = false; @@ -820,7 +820,7 @@ describe('RESTDataSource', () => { ttl: 1000000, }; } - }(); + })(); dataSource.httpCache = httpCache; @@ -835,7 +835,7 @@ describe('RESTDataSource', () => { }); it('allows setting a short TTL for the cache', async () => { - const dataSource = new class extends RESTDataSource { + const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com'; override requestCacheEnabled = false; @@ -849,7 +849,7 @@ describe('RESTDataSource', () => { ttl: 1, }; } - }(); + })(); dataSource.httpCache = httpCache;