From 3ef45205068fee1490ad25b85bdf916e3c7c1859 Mon Sep 17 00:00:00 2001 From: aiova Date: Thu, 31 Oct 2024 14:53:01 +0200 Subject: [PATCH 01/27] add multiple caching options --- package.json | 2 +- src/HTTPCache.ts | 298 +++++++++++++++++++++++------------------- src/RESTDataSource.ts | 40 +++--- 3 files changed, 186 insertions(+), 154 deletions(-) diff --git a/package.json b/package.json index 566abdef..48de20d6 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@apollo/datasource-rest", + "name": "@apollo/datasource-rest-storage", "description": "REST DataSource for Apollo Server v4", "version": "6.3.0", "author": "Apollo ", diff --git a/src/HTTPCache.ts b/src/HTTPCache.ts index fda4a568..2080947b 100644 --- a/src/HTTPCache.ts +++ b/src/HTTPCache.ts @@ -17,6 +17,12 @@ import type { ValueOrPromise, } from './RESTDataSource'; +interface PolicyCacheEntry { + policy: any; + ttlOverride?: number; + body: string; +} + // We want to use a couple internal properties of CachePolicy. (We could get // `_url` and `_status` off of the serialized CachePolicyObject, but `age()` is // just missing from `@types/http-cache-semantics` for now.) So we just cast to @@ -30,6 +36,7 @@ interface SneakyCachePolicy extends CachePolicy { interface ResponseWithCacheWritePromise { response: FetcherResponse; cacheWritePromise?: Promise; + parsedBody?: any; } export class HTTPCache { @@ -75,15 +82,26 @@ export class HTTPCache { return { response: await this.httpFetch(urlString, requestOpts) }; } - const entry = - requestOpts.skipCache !== true - ? await this.keyValueCache.get(cacheKey) - : undefined; - if (!entry) { - // There's nothing in our cache. Fetch the URL and save it to the cache if - // we're allowed. + let cacheOptions = cache?.cacheOptions; + if (typeof cacheOptions === 'function') { const response = await this.httpFetch(urlString, requestOpts); - + cacheOptions = await cacheOptions(urlString, response, requestOpts); + if (cacheOptions?.cacheStrategy === 'object') { + const parsedBody = await response.json(); + // Store in cache if ttl is provided + if (cacheOptions?.ttl) { + const cacheWritePromise = this.keyValueCache.set( + cacheKey, + parsedBody, + cacheOptions as CO + ).catch(error => { + console.error('Error writing to cache:', error); + }); + return { response, parsedBody, cacheWritePromise }; + } + return { response, parsedBody }; + } + // Handle policy-based caching const policy = new CachePolicy( policyRequestFrom(urlString, requestOpts), policyResponseFrom(response), @@ -96,16 +114,94 @@ export class HTTPCache { requestOpts, policy, cacheKey, - cache?.cacheOptions, + cacheOptions, ); } - const { policy: policyRaw, ttlOverride, body } = JSON.parse(entry); + // Determine caching strategy + const cacheStrategy = cacheOptions?.cacheStrategy ?? 'default'; + + if (cacheStrategy === 'object') { + return this.handleDirectCache(urlString, requestOpts, cacheKey, cacheOptions); + } + + return this.handlePolicyCache( + urlString, + requestOpts, + cacheKey, + cacheOptions, + cache?.httpCacheSemanticsCachePolicyOptions + ); + } + + private async handleDirectCache( + urlString: string, + requestOpts: RequestOptions, + cacheKey: string, + cacheOptions?: CacheOptions, + ): Promise { + if (requestOpts.skipCache) { + const response = await this.httpFetch(urlString, requestOpts); + const parsedBody = await response.json(); + return { response, parsedBody }; + } + + const cachedValue = await this.keyValueCache.get(cacheKey); + if (cachedValue !== undefined) { + return { + response: new NodeFetchResponse(undefined, { status: 200 }), + parsedBody: cachedValue, + }; + } + + const response = await this.httpFetch(urlString, requestOpts); + const parsedBody = await response.json(); + + if (cacheOptions?.ttl) { + const cacheWritePromise = this.keyValueCache.set( + cacheKey, + parsedBody, + cacheOptions as CO + ).catch(error => { + console.error('Error writing to cache:', error); + }); + return { response, parsedBody, cacheWritePromise }; + } + + return { response, parsedBody }; + } + + private async handlePolicyCache( + urlString: string, + requestOpts: RequestOptions, + cacheKey: string, + cacheOptions?: CO, + httpCacheSemanticsCachePolicyOptions?: HttpCacheSemanticsOptions, + ): Promise { + const entry = requestOpts.skipCache !== true + ? await this.keyValueCache.get(cacheKey) + : undefined; + + if (!entry) { + const response = await this.httpFetch(urlString, requestOpts); + const policy = new CachePolicy( + policyRequestFrom(urlString, requestOpts), + policyResponseFrom(response), + httpCacheSemanticsCachePolicyOptions, + ) as SneakyCachePolicy; + + return this.storeResponseAndReturnClone( + urlString, + response, + requestOpts, + policy, + cacheKey, + cacheOptions, + ); + } + const { policy: policyRaw, ttlOverride, body } = JSON.parse(entry) as PolicyCacheEntry; const policy = CachePolicy.fromObject(policyRaw) as SneakyCachePolicy; - // Remove url from the policy, because otherwise it would never match a - // request with a custom cache key (ie, we want users to be able to tell us - // that two requests should be treated as the same even if the URL differs). const urlFromPolicy = policy._url; policy._url = undefined; @@ -116,10 +212,6 @@ export class HTTPCache { policyRequestFrom(urlString, requestOpts), )) ) { - // Either the cache entry was created with an explicit TTL override (ie, - // `ttl` returned from `cacheOptionsFor`) and we're within that TTL, or - // the cache entry was not created with an explicit TTL override and the - // header-based cache policy says we can safely use the cached response. const headers = policy.responseHeaders(); return { response: new NodeFetchResponse(body, { @@ -128,57 +220,42 @@ export class HTTPCache { headers: cachePolicyHeadersToNodeFetchHeadersInit(headers), }), }; - } else { - // We aren't sure that we're allowed to use the cached response, so we are - // going to actually do a fetch. However, we may have one extra trick up - // our sleeve. If the cached response contained an `etag` or - // `last-modified` header, then we can add an appropriate `if-none-match` - // or `if-modified-since` header to the request. If what we're fetching - // hasn't changed, then the server can return a small 304 response instead - // of a large 200, and we can use the body from our cache. This logic is - // implemented inside `policy.revalidationHeaders`; we support it by - // setting a larger KeyValueCache TTL for responses with these headers - // (see `canBeRevalidated`). (If the cached response doesn't have those - // headers, we'll just end up fetching the normal request here.) - // - // Note that even if we end up able to reuse the cached body here, we - // still re-write to the cache, because we might need to update the TTL or - // other aspects of the cache policy based on the headers we got back. - const revalidationHeaders = policy.revalidationHeaders( - policyRequestFrom(urlString, requestOpts), - ); - const revalidationRequest = { - ...requestOpts, - headers: cachePolicyHeadersToFetcherHeadersInit(revalidationHeaders), - }; - const revalidationResponse = await this.httpFetch( - urlString, - revalidationRequest, - ); + } - const { policy: revalidatedPolicy, modified } = policy.revalidatedPolicy( - policyRequestFrom(urlString, revalidationRequest), - policyResponseFrom(revalidationResponse), - ) as unknown as { policy: SneakyCachePolicy; modified: boolean }; + const revalidationHeaders = policy.revalidationHeaders( + policyRequestFrom(urlString, requestOpts), + ); + const revalidationRequest = { + ...requestOpts, + headers: cachePolicyHeadersToFetcherHeadersInit(revalidationHeaders), + }; + const revalidationResponse = await this.httpFetch( + urlString, + revalidationRequest, + ); - return this.storeResponseAndReturnClone( - urlString, - new NodeFetchResponse( - modified ? await revalidationResponse.text() : body, - { - url: revalidatedPolicy._url, - status: revalidatedPolicy._status, - headers: cachePolicyHeadersToNodeFetchHeadersInit( - revalidatedPolicy.responseHeaders(), - ), - }, - ), - requestOpts, - revalidatedPolicy, - cacheKey, - cache?.cacheOptions, - ); - } + const { policy: revalidatedPolicy, modified } = policy.revalidatedPolicy( + policyRequestFrom(urlString, revalidationRequest), + policyResponseFrom(revalidationResponse), + ) as unknown as { policy: SneakyCachePolicy; modified: boolean }; + + return this.storeResponseAndReturnClone( + urlString, + new NodeFetchResponse( + modified ? await revalidationResponse.text() : body, + { + url: revalidatedPolicy._url, + status: revalidatedPolicy._status, + headers: cachePolicyHeadersToNodeFetchHeadersInit( + revalidatedPolicy.responseHeaders(), + ), + }, + ), + requestOpts, + revalidatedPolicy, + cacheKey, + cacheOptions, + ); } private async storeResponseAndReturnClone( @@ -187,99 +264,44 @@ export class HTTPCache { request: RequestOptions, policy: SneakyCachePolicy, cacheKey: string, - cacheOptions?: - | CO - | (( - url: string, - response: FetcherResponse, - request: RequestOptions, - ) => ValueOrPromise), + cacheOptions?: CO, ): Promise { if (typeof cacheOptions === 'function') { + // @ts-ignore cacheOptions = await cacheOptions(url, response, request); } - let ttlOverride = cacheOptions?.ttl; + const ttlOverride = cacheOptions?.ttl; if ( - // With a TTL override, only cache successful responses but otherwise ignore method and response headers !(ttlOverride && policy._status >= 200 && policy._status <= 299) && - // Without an override, we only cache GET requests and respect standard HTTP cache semantics !(request.method === 'GET' && policy.storable()) ) { return { response }; } - let ttl = - ttlOverride === undefined - ? Math.round(policy.timeToLive() / 1000) - : ttlOverride; + const ttl = ttlOverride ?? Math.round(policy.timeToLive() / 1000); if (ttl <= 0) return { response }; - // If a response can be revalidated, we don't want to remove it from the - // cache right after it expires. (See the comment above the call to - // `revalidationHeaders` for details.) We may be able to use better - // heuristics here, but for now we'll take the max-age times 2. - if (canBeRevalidated(response)) { - ttl *= 2; - } - - // Clone the response and return it. In the background, read the original - // response and write it to the cache. The caller is responsible for - // `await`ing or `catch`ing `cacheWritePromise`. (By default, RESTDataSource - // `catch`es it with `console.log`.) - // - // When you clone a response, you're generally expected (at least by - // node-fetch: https://github.com/node-fetch/node-fetch/issues/151) to read - // both bodies in parallel; if you only read one of them and ignore the - // other, the one you're reading might start blocking once the second one's - // buffer fills. We don't think this is a real problem here: we do - // immediately read from the one we're writing to the cache, and if the - // caller doesn't bother to read its response, the only real downside is - // that we won't ever write to the cache, which seems maybe OK for an - // "ignored" body. (It could perhaps lead to a memory leak, but the answer - // there is to make sure your parseBody override does consume the response.) const returnedResponse = response.clone(); - return { - response: returnedResponse, - cacheWritePromise: this.readResponseAndWriteToCache({ - response, - policy, - cacheOptions, - ttl, - ttlOverride, - cacheKey, - }), - }; - } - - private async readResponseAndWriteToCache({ - response, - policy, - cacheOptions, - ttl, - ttlOverride, - cacheKey, - }: { - response: FetcherResponse; - policy: CachePolicy; - cacheOptions?: CO; - ttl: number | null | undefined; - ttlOverride: number | undefined; - cacheKey: string; - }): Promise { const body = await response.text(); - const entry = JSON.stringify({ + + const entry: PolicyCacheEntry = { policy: policy.toObject(), ttlOverride, body, - }); + }; + + const cacheWritePromise = this.keyValueCache + .set(cacheKey, JSON.stringify(entry), { + ...cacheOptions, + ttl: canBeRevalidated(response) ? ttl * 2 : ttl, + } as CO) + .catch(error => { + console.error('Error writing to cache:', error); + }); - // Set the value into the cache, and forward all the set cache option into the setter function - await this.keyValueCache.set(cacheKey, entry, { - ...cacheOptions, - ttl, - } as CO); + return { response: returnedResponse, cacheWritePromise }; } } diff --git a/src/RESTDataSource.ts b/src/RESTDataSource.ts index e87a6197..6ce9a2d1 100644 --- a/src/RESTDataSource.ts +++ b/src/RESTDataSource.ts @@ -136,6 +136,11 @@ export interface CacheOptions { * cached. */ ttl?: number; + /** + * default - the default cache strategy that will stringify/parse the response + * object - will cache the response in the original format + */ + cacheStrategy?: 'default' | 'object'; } const NODE_ENV = process.env.NODE_ENV; @@ -323,17 +328,23 @@ export abstract class RESTDataSource { 'requestDeduplication' >, requestDeduplicationResult: RequestDeduplicationResult, + cacheOptions: CacheOptions ): DataSourceFetchResult { return { ...dataSourceFetchResult, requestDeduplication: requestDeduplicationResult, - parsedBody: this.cloneParsedBody(dataSourceFetchResult.parsedBody), + parsedBody: this.cloneParsedBody(dataSourceFetchResult.parsedBody, cacheOptions), }; } - protected cloneParsedBody(parsedBody: TResult) { + protected cloneParsedBody(parsedBody: TResult, cacheOptions: CacheOptions) { // consider using `structuredClone()` when we drop support for Node 16 - return cloneDeep(parsedBody); + const cacheStrategy = cacheOptions?.cacheStrategy + if(cacheStrategy === "object") { + return (Array.isArray(parsedBody) ? [...parsedBody] : {...parsedBody}) as TResult + } else { + return cloneDeep(parsedBody); + } } protected shouldJSONSerializeBody( @@ -536,7 +547,7 @@ export abstract class RESTDataSource { ? outgoingRequest.cacheOptions : this.cacheOptionsFor?.bind(this); try { - const { response, cacheWritePromise } = await this.httpCache.fetch( + const result = await this.httpCache.fetch( url, outgoingRequest, { @@ -547,24 +558,20 @@ export abstract class RESTDataSource { }, ); - if (cacheWritePromise) { - this.catchCacheWritePromiseErrors(cacheWritePromise); - } - - const parsedBody = await this.parseBody(response); + const parsedBody = result.parsedBody ?? await this.parseBody(result.response); await this.throwIfResponseIsError({ url, request: outgoingRequest, - response, + response: result.response, parsedBody, }); return { parsedBody: parsedBody as any as TResult, - response, + response: result.response, httpCache: { - cacheWritePromise, + cacheWritePromise: result.cacheWritePromise, }, }; } catch (error) { @@ -574,6 +581,9 @@ export abstract class RESTDataSource { }); }; + const cacheOptions = outgoingRequest.cacheOptions + ? outgoingRequest.cacheOptions + : this.cacheOptionsFor?.bind(this); // Cache GET requests based on the calculated cache key // Disabling the request cache does not disable the response cache const policy = this.requestDeduplicationPolicyFor(url, outgoingRequest); @@ -589,7 +599,7 @@ export abstract class RESTDataSource { this.cloneDataSourceFetchResult(result, { policy, deduplicatedAgainstPreviousRequest: true, - }), + }, cacheOptions as CacheOptions), ); const thisRequestPromise = performRequest(); @@ -609,8 +619,8 @@ export abstract class RESTDataSource { // haven't quite bothered yet. return this.cloneDataSourceFetchResult(await thisRequestPromise, { policy, - deduplicatedAgainstPreviousRequest: false, - }); + deduplicatedAgainstPreviousRequest: false + }, cacheOptions as CacheOptions); } finally { if (policy.policy === 'deduplicate-during-request-lifetime') { this.deduplicationPromises.delete(policy.deduplicationKey); From d9cb70704876a1479ff526c36308e16544e69e98 Mon Sep 17 00:00:00 2001 From: aiova Date: Thu, 31 Oct 2024 15:04:38 +0200 Subject: [PATCH 02/27] add CI/CD pipeline --- .github/workflows/release-pr.yml | 15 ++++++--------- package.json | 3 ++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index ffa09a26..6f4223ff 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -1,10 +1,8 @@ name: Release - on: push: branches: - - main - + - feature/aiova/storage-mode jobs: release: name: Release @@ -15,19 +13,18 @@ jobs: with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - - name: Setup Node.js 16.x uses: actions/setup-node@v4 with: node-version: 16.x - - name: Install Dependencies run: npm i - - - name: Create Release Pull Request / NPM Publish + - name: Create Release uses: changesets/action@v1 with: - publish: npm run changeset-publish + version: npm run changeset-version + commit: "chore: version packages" + title: "chore: version packages" + createGithubReleases: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/package.json b/package.json index 48de20d6..2850e052 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "watch": "tsc --build --watch", "lint": "eslint src/**/*.ts", "changeset-publish": "changeset publish", - "changeset-check": "changeset status --verbose --since=origin/main" + "changeset-check": "changeset status --verbose --since=origin/main", + "changeset-version": "changeset version" }, "devDependencies": { "@apollo/server": "4.11.0", From 714b10bf6b039aa39dd0d287100c4f3cea0bcd47 Mon Sep 17 00:00:00 2001 From: aiova Date: Thu, 31 Oct 2024 15:07:32 +0200 Subject: [PATCH 03/27] add changeset --- .changeset/tough-rice-sort.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tough-rice-sort.md diff --git a/.changeset/tough-rice-sort.md b/.changeset/tough-rice-sort.md new file mode 100644 index 00000000..cfa9ea3f --- /dev/null +++ b/.changeset/tough-rice-sort.md @@ -0,0 +1,5 @@ +--- +'@apollo/datasource-rest-storage': patch +--- + +Add multiple cache strategies From 6bd5bb4e12d100b95f2b64b1af2a811d2cbdf3e5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 31 Oct 2024 13:07:59 +0000 Subject: [PATCH 04/27] chore: version packages --- .changeset/tough-rice-sort.md | 5 ----- CHANGELOG.md | 6 ++++++ package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/tough-rice-sort.md diff --git a/.changeset/tough-rice-sort.md b/.changeset/tough-rice-sort.md deleted file mode 100644 index cfa9ea3f..00000000 --- a/.changeset/tough-rice-sort.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@apollo/datasource-rest-storage': patch ---- - -Add multiple cache strategies diff --git a/CHANGELOG.md b/CHANGELOG.md index e9aa61af..dbdbb6a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/datasource-rest +## 6.3.1 + +### Patch Changes + +- [`714b10b`](https://github.com/apollographql/datasource-rest/commit/714b10bf6b039aa39dd0d287100c4f3cea0bcd47) Thanks [@adr-iova](https://github.com/adr-iova)! - Add multiple cache strategies + ## 6.3.0 ### Minor Changes diff --git a/package.json b/package.json index 2850e052..7201138f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@apollo/datasource-rest-storage", "description": "REST DataSource for Apollo Server v4", - "version": "6.3.0", + "version": "6.3.1", "author": "Apollo ", "license": "MIT", "repository": { From d613f2bdd9d260da60c9d4e3ea88259b6e9c3060 Mon Sep 17 00:00:00 2001 From: aiova Date: Thu, 31 Oct 2024 15:19:04 +0200 Subject: [PATCH 05/27] change release flow --- .github/workflows/release-pr.yml | 37 +++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 6f4223ff..6e4b5921 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -11,7 +11,6 @@ jobs: - name: Checkout Repo uses: actions/checkout@v4 with: - # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - name: Setup Node.js 16.x uses: actions/setup-node@v4 @@ -19,12 +18,34 @@ jobs: node-version: 16.x - name: Install Dependencies run: npm i - - name: Create Release - uses: changesets/action@v1 - with: - version: npm run changeset-version - commit: "chore: version packages" - title: "chore: version packages" - createGithubReleases: true + - name: Setup Git + run: | + git config user.name "GitHub Actions" + git config user.email "github-actions@github.com" + - name: Version and Create Tag + id: version + run: | + # Run changeset version + npx changeset version + + # Get new version from package.json + NEW_VERSION=$(node -p "require('./package.json').version") + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + + # Commit changes + git add . + git commit -m "chore: release version $NEW_VERSION" + + # Create and push tag + git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION" + git push origin "v$NEW_VERSION" + git push origin feature/aiova/storage-mode + - name: Create GitHub Release + uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.version.outputs.new_version }} + release_name: Release v${{ steps.version.outputs.new_version }} + draft: false + prerelease: false From 05dd97c8034d494d779b1279c71f46e2d9c9d84d Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 31 Oct 2024 13:19:40 +0000 Subject: [PATCH 06/27] chore: release version 6.3.1 --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ccd8341..ae81590d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@apollo/datasource-rest", - "version": "6.3.0", + "name": "@apollo/datasource-rest-storage", + "version": "6.3.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "@apollo/datasource-rest", - "version": "6.3.0", + "name": "@apollo/datasource-rest-storage", + "version": "6.3.1", "license": "MIT", "dependencies": { "@apollo/utils.fetcher": "^3.0.0", From 425b70b337f8e3b3519e56b499c4476d9bf5bac8 Mon Sep 17 00:00:00 2001 From: aiova Date: Thu, 31 Oct 2024 15:21:23 +0200 Subject: [PATCH 07/27] change release branch to main --- .github/workflows/release-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 6e4b5921..20bdb59c 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -2,7 +2,7 @@ name: Release on: push: branches: - - feature/aiova/storage-mode + - main jobs: release: name: Release From 13998ee8bfc92758e942ab4d513d25aedcd2a0bb Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 1 Nov 2024 16:11:19 +0200 Subject: [PATCH 08/27] fix body already used for big responses --- src/HTTPCache.ts | 310 ++++++++++++++++++++++-------------------- src/RESTDataSource.ts | 22 +-- 2 files changed, 175 insertions(+), 157 deletions(-) diff --git a/src/HTTPCache.ts b/src/HTTPCache.ts index 2080947b..f36631a6 100644 --- a/src/HTTPCache.ts +++ b/src/HTTPCache.ts @@ -17,12 +17,6 @@ import type { ValueOrPromise, } from './RESTDataSource'; -interface PolicyCacheEntry { - policy: any; - ttlOverride?: number; - body: string; -} - // We want to use a couple internal properties of CachePolicy. (We could get // `_url` and `_status` off of the serialized CachePolicyObject, but `age()` is // just missing from `@types/http-cache-semantics` for now.) So we just cast to @@ -83,111 +77,53 @@ export class HTTPCache { } let cacheOptions = cache?.cacheOptions; - if (typeof cacheOptions === 'function') { - const response = await this.httpFetch(urlString, requestOpts); - cacheOptions = await cacheOptions(urlString, response, requestOpts); - if (cacheOptions?.cacheStrategy === 'object') { + const cacheStrategy = (cacheOptions as CacheOptions)?.cacheStrategy ?? 'default'; + + if (cacheStrategy === 'object') { + if (requestOpts.skipCache) { + const response = await this.httpFetch(urlString, requestOpts); const parsedBody = await response.json(); - // Store in cache if ttl is provided - if (cacheOptions?.ttl) { - const cacheWritePromise = this.keyValueCache.set( - cacheKey, - parsedBody, - cacheOptions as CO - ).catch(error => { - console.error('Error writing to cache:', error); - }); - return { response, parsedBody, cacheWritePromise }; - } return { response, parsedBody }; } - // Handle policy-based caching - const policy = new CachePolicy( - policyRequestFrom(urlString, requestOpts), - policyResponseFrom(response), - cache?.httpCacheSemanticsCachePolicyOptions, - ) as SneakyCachePolicy; - - return this.storeResponseAndReturnClone( - urlString, - response, - requestOpts, - policy, - cacheKey, - cacheOptions, - ); - } - - // Determine caching strategy - const cacheStrategy = cacheOptions?.cacheStrategy ?? 'default'; - - if (cacheStrategy === 'object') { - return this.handleDirectCache(urlString, requestOpts, cacheKey, cacheOptions); - } - return this.handlePolicyCache( - urlString, - requestOpts, - cacheKey, - cacheOptions, - cache?.httpCacheSemanticsCachePolicyOptions - ); - } + const cachedValue = await this.keyValueCache.get(cacheKey); + if (cachedValue !== undefined) { + return { + response: new NodeFetchResponse(undefined, { status: 200 }), + parsedBody: cachedValue, + }; + } - private async handleDirectCache( - urlString: string, - requestOpts: RequestOptions, - cacheKey: string, - cacheOptions?: CacheOptions, - ): Promise { - if (requestOpts.skipCache) { const response = await this.httpFetch(urlString, requestOpts); const parsedBody = await response.json(); - return { response, parsedBody }; - } - const cachedValue = await this.keyValueCache.get(cacheKey); - if (cachedValue !== undefined) { - return { - response: new NodeFetchResponse(undefined, { status: 200 }), - parsedBody: cachedValue, - }; - } - - const response = await this.httpFetch(urlString, requestOpts); - const parsedBody = await response.json(); + if ((cacheOptions as CacheOptions)?.ttl) { + const cacheWritePromise = this.keyValueCache.set( + cacheKey, + parsedBody, + cacheOptions as CO + ).catch(error => { + console.error('Error writing to cache:', error); + }); + return { response, parsedBody, cacheWritePromise }; + } - if (cacheOptions?.ttl) { - const cacheWritePromise = this.keyValueCache.set( - cacheKey, - parsedBody, - cacheOptions as CO - ).catch(error => { - console.error('Error writing to cache:', error); - }); - return { response, parsedBody, cacheWritePromise }; + return { response, parsedBody }; } - return { response, parsedBody }; - } - - private async handlePolicyCache( - urlString: string, - requestOpts: RequestOptions, - cacheKey: string, - cacheOptions?: CO, - httpCacheSemanticsCachePolicyOptions?: HttpCacheSemanticsOptions, - ): Promise { - const entry = requestOpts.skipCache !== true - ? await this.keyValueCache.get(cacheKey) - : undefined; - + const entry = + requestOpts.skipCache !== true + ? await this.keyValueCache.get(cacheKey) + : undefined; if (!entry) { + // There's nothing in our cache. Fetch the URL and save it to the cache if + // we're allowed. const response = await this.httpFetch(urlString, requestOpts); + const policy = new CachePolicy( policyRequestFrom(urlString, requestOpts), policyResponseFrom(response), - httpCacheSemanticsCachePolicyOptions, + cache?.httpCacheSemanticsCachePolicyOptions, ) as SneakyCachePolicy; return this.storeResponseAndReturnClone( @@ -196,12 +132,16 @@ export class HTTPCache { requestOpts, policy, cacheKey, - cacheOptions, + cache?.cacheOptions, ); } - const { policy: policyRaw, ttlOverride, body } = JSON.parse(entry) as PolicyCacheEntry; + const { policy: policyRaw, ttlOverride, body } = JSON.parse(entry); + const policy = CachePolicy.fromObject(policyRaw) as SneakyCachePolicy; + // Remove url from the policy, because otherwise it would never match a + // request with a custom cache key (ie, we want users to be able to tell us + // that two requests should be treated as the same even if the URL differs). const urlFromPolicy = policy._url; policy._url = undefined; @@ -212,6 +152,10 @@ export class HTTPCache { policyRequestFrom(urlString, requestOpts), )) ) { + // Either the cache entry was created with an explicit TTL override (ie, + // `ttl` returned from `cacheOptionsFor`) and we're within that TTL, or + // the cache entry was not created with an explicit TTL override and the + // header-based cache policy says we can safely use the cached response. const headers = policy.responseHeaders(); return { response: new NodeFetchResponse(body, { @@ -220,42 +164,57 @@ export class HTTPCache { headers: cachePolicyHeadersToNodeFetchHeadersInit(headers), }), }; - } + } else { + // We aren't sure that we're allowed to use the cached response, so we are + // going to actually do a fetch. However, we may have one extra trick up + // our sleeve. If the cached response contained an `etag` or + // `last-modified` header, then we can add an appropriate `if-none-match` + // or `if-modified-since` header to the request. If what we're fetching + // hasn't changed, then the server can return a small 304 response instead + // of a large 200, and we can use the body from our cache. This logic is + // implemented inside `policy.revalidationHeaders`; we support it by + // setting a larger KeyValueCache TTL for responses with these headers + // (see `canBeRevalidated`). (If the cached response doesn't have those + // headers, we'll just end up fetching the normal request here.) + // + // Note that even if we end up able to reuse the cached body here, we + // still re-write to the cache, because we might need to update the TTL or + // other aspects of the cache policy based on the headers we got back. + const revalidationHeaders = policy.revalidationHeaders( + policyRequestFrom(urlString, requestOpts), + ); + const revalidationRequest = { + ...requestOpts, + headers: cachePolicyHeadersToFetcherHeadersInit(revalidationHeaders), + }; + const revalidationResponse = await this.httpFetch( + urlString, + revalidationRequest, + ); - const revalidationHeaders = policy.revalidationHeaders( - policyRequestFrom(urlString, requestOpts), - ); - const revalidationRequest = { - ...requestOpts, - headers: cachePolicyHeadersToFetcherHeadersInit(revalidationHeaders), - }; - const revalidationResponse = await this.httpFetch( - urlString, - revalidationRequest, - ); + const { policy: revalidatedPolicy, modified } = policy.revalidatedPolicy( + policyRequestFrom(urlString, revalidationRequest), + policyResponseFrom(revalidationResponse), + ) as unknown as { policy: SneakyCachePolicy; modified: boolean }; - const { policy: revalidatedPolicy, modified } = policy.revalidatedPolicy( - policyRequestFrom(urlString, revalidationRequest), - policyResponseFrom(revalidationResponse), - ) as unknown as { policy: SneakyCachePolicy; modified: boolean }; - - return this.storeResponseAndReturnClone( - urlString, - new NodeFetchResponse( - modified ? await revalidationResponse.text() : body, - { - url: revalidatedPolicy._url, - status: revalidatedPolicy._status, - headers: cachePolicyHeadersToNodeFetchHeadersInit( - revalidatedPolicy.responseHeaders(), - ), - }, - ), - requestOpts, - revalidatedPolicy, - cacheKey, - cacheOptions, - ); + return this.storeResponseAndReturnClone( + urlString, + new NodeFetchResponse( + modified ? await revalidationResponse.text() : body, + { + url: revalidatedPolicy._url, + status: revalidatedPolicy._status, + headers: cachePolicyHeadersToNodeFetchHeadersInit( + revalidatedPolicy.responseHeaders(), + ), + }, + ), + requestOpts, + revalidatedPolicy, + cacheKey, + cache?.cacheOptions, + ); + } } private async storeResponseAndReturnClone( @@ -264,44 +223,99 @@ export class HTTPCache { request: RequestOptions, policy: SneakyCachePolicy, cacheKey: string, - cacheOptions?: CO, + cacheOptions?: + | CO + | (( + url: string, + response: FetcherResponse, + request: RequestOptions, + ) => ValueOrPromise), ): Promise { if (typeof cacheOptions === 'function') { - // @ts-ignore cacheOptions = await cacheOptions(url, response, request); } - const ttlOverride = cacheOptions?.ttl; + let ttlOverride = cacheOptions?.ttl; if ( + // With a TTL override, only cache successful responses but otherwise ignore method and response headers !(ttlOverride && policy._status >= 200 && policy._status <= 299) && + // Without an override, we only cache GET requests and respect standard HTTP cache semantics !(request.method === 'GET' && policy.storable()) ) { return { response }; } - const ttl = ttlOverride ?? Math.round(policy.timeToLive() / 1000); + let ttl = + ttlOverride === undefined + ? Math.round(policy.timeToLive() / 1000) + : ttlOverride; if (ttl <= 0) return { response }; + // If a response can be revalidated, we don't want to remove it from the + // cache right after it expires. (See the comment above the call to + // `revalidationHeaders` for details.) We may be able to use better + // heuristics here, but for now we'll take the max-age times 2. + if (canBeRevalidated(response)) { + ttl *= 2; + } + + // Clone the response and return it. In the background, read the original + // response and write it to the cache. The caller is responsible for + // `await`ing or `catch`ing `cacheWritePromise`. (By default, RESTDataSource + // `catch`es it with `console.log`.) + // + // When you clone a response, you're generally expected (at least by + // node-fetch: https://github.com/node-fetch/node-fetch/issues/151) to read + // both bodies in parallel; if you only read one of them and ignore the + // other, the one you're reading might start blocking once the second one's + // buffer fills. We don't think this is a real problem here: we do + // immediately read from the one we're writing to the cache, and if the + // caller doesn't bother to read its response, the only real downside is + // that we won't ever write to the cache, which seems maybe OK for an + // "ignored" body. (It could perhaps lead to a memory leak, but the answer + // there is to make sure your parseBody override does consume the response.) const returnedResponse = response.clone(); - const body = await response.text(); + return { + response: returnedResponse, + cacheWritePromise: this.readResponseAndWriteToCache({ + response, + policy, + cacheOptions, + ttl, + ttlOverride, + cacheKey, + }), + }; + } - const entry: PolicyCacheEntry = { + private async readResponseAndWriteToCache({ + response, + policy, + cacheOptions, + ttl, + ttlOverride, + cacheKey, + }: { + response: FetcherResponse; + policy: CachePolicy; + cacheOptions?: CO; + ttl: number | null | undefined; + ttlOverride: number | undefined; + cacheKey: string; + }): Promise { + const body = await response.text(); + const entry = JSON.stringify({ policy: policy.toObject(), ttlOverride, body, - }; - - const cacheWritePromise = this.keyValueCache - .set(cacheKey, JSON.stringify(entry), { - ...cacheOptions, - ttl: canBeRevalidated(response) ? ttl * 2 : ttl, - } as CO) - .catch(error => { - console.error('Error writing to cache:', error); - }); + }); - return { response: returnedResponse, cacheWritePromise }; + // Set the value into the cache, and forward all the set cache option into the setter function + await this.keyValueCache.set(cacheKey, entry, { + ...cacheOptions, + ttl, + } as CO); } } diff --git a/src/RESTDataSource.ts b/src/RESTDataSource.ts index 6ce9a2d1..70a26250 100644 --- a/src/RESTDataSource.ts +++ b/src/RESTDataSource.ts @@ -338,7 +338,6 @@ export abstract class RESTDataSource { } protected cloneParsedBody(parsedBody: TResult, cacheOptions: CacheOptions) { - // consider using `structuredClone()` when we drop support for Node 16 const cacheStrategy = cacheOptions?.cacheStrategy if(cacheStrategy === "object") { return (Array.isArray(parsedBody) ? [...parsedBody] : {...parsedBody}) as TResult @@ -547,7 +546,7 @@ export abstract class RESTDataSource { ? outgoingRequest.cacheOptions : this.cacheOptionsFor?.bind(this); try { - const result = await this.httpCache.fetch( + const { response, cacheWritePromise, parsedBody } = await this.httpCache.fetch( url, outgoingRequest, { @@ -558,20 +557,25 @@ export abstract class RESTDataSource { }, ); - const parsedBody = result.parsedBody ?? await this.parseBody(result.response); + if (cacheWritePromise) { + this.catchCacheWritePromiseErrors(cacheWritePromise); + } + + const _parsedBody = parsedBody ?? await this.parseBody(response); await this.throwIfResponseIsError({ url, request: outgoingRequest, - response: result.response, - parsedBody, + response, + // @ts-ignore + _parsedBody, }); return { - parsedBody: parsedBody as any as TResult, - response: result.response, + parsedBody: _parsedBody as any as TResult, + response, httpCache: { - cacheWritePromise: result.cacheWritePromise, + cacheWritePromise, }, }; } catch (error) { @@ -619,7 +623,7 @@ export abstract class RESTDataSource { // haven't quite bothered yet. return this.cloneDataSourceFetchResult(await thisRequestPromise, { policy, - deduplicatedAgainstPreviousRequest: false + deduplicatedAgainstPreviousRequest: false, }, cacheOptions as CacheOptions); } finally { if (policy.policy === 'deduplicate-during-request-lifetime') { From 9c37ba20c45688d21d634003896c524707ecc5f9 Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 1 Nov 2024 16:14:46 +0200 Subject: [PATCH 09/27] release --- .changeset/green-jars-suffer.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/green-jars-suffer.md diff --git a/.changeset/green-jars-suffer.md b/.changeset/green-jars-suffer.md new file mode 100644 index 00000000..cab1d201 --- /dev/null +++ b/.changeset/green-jars-suffer.md @@ -0,0 +1,5 @@ +--- +'@apollo/datasource-rest-storage': patch +--- + +fix body already used for big responses From 2fb7cdbeed1a42a6046e44b35805bcc30d5da127 Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 1 Nov 2024 17:05:54 +0200 Subject: [PATCH 10/27] test --- .github/workflows/release-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 20bdb59c..6e4b5921 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -2,7 +2,7 @@ name: Release on: push: branches: - - main + - feature/aiova/storage-mode jobs: release: name: Release From 97546e81679d5a3cb0db0ae3f97a4eaf464c2c1d Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 1 Nov 2024 17:13:51 +0200 Subject: [PATCH 11/27] test --- .github/workflows/release-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 6e4b5921..20bdb59c 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -2,7 +2,7 @@ name: Release on: push: branches: - - feature/aiova/storage-mode + - main jobs: release: name: Release From ba4a1eeef7cabfe9fc6d333e48410ffdd562e2d0 Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 1 Nov 2024 17:24:35 +0200 Subject: [PATCH 12/27] test --- .github/workflows/release-pr.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 20bdb59c..5a92f06b 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -2,7 +2,12 @@ name: Release on: push: branches: - - main + - feature/aiova/storage-mode + +permissions: + contents: write + pull-requests: write + jobs: release: name: Release From b95476eba33651bbc9b7e7ef28653e5e97be7c88 Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 1 Nov 2024 17:25:23 +0200 Subject: [PATCH 13/27] test --- .github/workflows/release-pr.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 5a92f06b..5e3d3489 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -29,6 +29,8 @@ jobs: git config user.email "github-actions@github.com" - name: Version and Create Tag id: version + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Run changeset version npx changeset version From 6c09be665e6fecb4ed81bf5bd07bf280a34fd0b0 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 1 Nov 2024 15:25:52 +0000 Subject: [PATCH 14/27] chore: release version 6.3.2 --- .changeset/green-jars-suffer.md | 5 ----- CHANGELOG.md | 6 ++++++ package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/green-jars-suffer.md diff --git a/.changeset/green-jars-suffer.md b/.changeset/green-jars-suffer.md deleted file mode 100644 index cab1d201..00000000 --- a/.changeset/green-jars-suffer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@apollo/datasource-rest-storage': patch ---- - -fix body already used for big responses diff --git a/CHANGELOG.md b/CHANGELOG.md index dbdbb6a5..35d678fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/datasource-rest +## 6.3.2 + +### Patch Changes + +- [`9c37ba2`](https://github.com/apollographql/datasource-rest/commit/9c37ba20c45688d21d634003896c524707ecc5f9) Thanks [@adr-iova](https://github.com/adr-iova)! - fix body already used for big responses + ## 6.3.1 ### Patch Changes diff --git a/package.json b/package.json index 7201138f..db9f3fd9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@apollo/datasource-rest-storage", "description": "REST DataSource for Apollo Server v4", - "version": "6.3.1", + "version": "6.3.2", "author": "Apollo ", "license": "MIT", "repository": { From 951b28ec8cd2ca9f7e65e51b48f56462c17d91a4 Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 1 Nov 2024 17:26:30 +0200 Subject: [PATCH 15/27] test --- .github/workflows/release-pr.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 5e3d3489..d028a04b 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -3,11 +3,6 @@ on: push: branches: - feature/aiova/storage-mode - -permissions: - contents: write - pull-requests: write - jobs: release: name: Release From 9b4ed278111e031c3ee86672f0ecf2586b891590 Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 1 Nov 2024 17:57:05 +0200 Subject: [PATCH 16/27] changes for ci/cd --- .changeset/olive-seals-behave.md | 5 +++++ .github/workflows/release-pr.yml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/olive-seals-behave.md diff --git a/.changeset/olive-seals-behave.md b/.changeset/olive-seals-behave.md new file mode 100644 index 00000000..de448589 --- /dev/null +++ b/.changeset/olive-seals-behave.md @@ -0,0 +1,5 @@ +--- +'@apollo/datasource-rest-storage': patch +--- + +changes for CI/CD diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index d028a04b..9178c1ff 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -2,7 +2,7 @@ name: Release on: push: branches: - - feature/aiova/storage-mode + - master jobs: release: name: Release From 6759ba4a7a1608ee9a4f78e99db7b0caaae35031 Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 1 Nov 2024 17:58:24 +0200 Subject: [PATCH 17/27] changes for ci/cd --- .github/workflows/release-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 9178c1ff..d48ef6f3 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -2,7 +2,7 @@ name: Release on: push: branches: - - master + - main jobs: release: name: Release From fa3d858573e42827b4c3444c7c0bc4e3607a18b1 Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 1 Nov 2024 18:01:28 +0200 Subject: [PATCH 18/27] push back to main --- .github/workflows/release-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index d48ef6f3..7f6896dd 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -41,7 +41,7 @@ jobs: # Create and push tag git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION" git push origin "v$NEW_VERSION" - git push origin feature/aiova/storage-mode + git push origin main - name: Create GitHub Release uses: actions/create-release@v1 env: From f3978bff791e084a242262eafc6c111f2755674f Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 1 Nov 2024 18:13:50 +0200 Subject: [PATCH 19/27] fix version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index db9f3fd9..a9b0ec3b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@apollo/datasource-rest-storage", "description": "REST DataSource for Apollo Server v4", - "version": "6.3.2", + "version": "6.3.3", "author": "Apollo ", "license": "MIT", "repository": { From 4e38bf9557a9fe049e6cc12cc8fe61e596b0c509 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 1 Nov 2024 16:14:47 +0000 Subject: [PATCH 20/27] chore: release version 6.3.4 --- .changeset/olive-seals-behave.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/olive-seals-behave.md diff --git a/.changeset/olive-seals-behave.md b/.changeset/olive-seals-behave.md deleted file mode 100644 index de448589..00000000 --- a/.changeset/olive-seals-behave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@apollo/datasource-rest-storage': patch ---- - -changes for CI/CD diff --git a/CHANGELOG.md b/CHANGELOG.md index 35d678fa..70c0b070 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/datasource-rest +## 6.3.4 + +### Patch Changes + +- [`9b4ed27`](https://github.com/apollographql/datasource-rest/commit/9b4ed278111e031c3ee86672f0ecf2586b891590) Thanks [@adr-iova](https://github.com/adr-iova)! - changes for CI/CD + ## 6.3.2 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index ae81590d..99e98f9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/datasource-rest-storage", - "version": "6.3.1", + "version": "6.3.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@apollo/datasource-rest-storage", - "version": "6.3.1", + "version": "6.3.3", "license": "MIT", "dependencies": { "@apollo/utils.fetcher": "^3.0.0", diff --git a/package.json b/package.json index a9b0ec3b..3837eade 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@apollo/datasource-rest-storage", "description": "REST DataSource for Apollo Server v4", - "version": "6.3.3", + "version": "6.3.4", "author": "Apollo ", "license": "MIT", "repository": { From 0f1750788d66d594888fed8d9cfe2af9356ef3ad Mon Sep 17 00:00:00 2001 From: aiova Date: Tue, 14 Jan 2025 20:29:47 +0200 Subject: [PATCH 21/27] fix: extend cache type to string and object --- src/HTTPCache.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/HTTPCache.ts b/src/HTTPCache.ts index f36631a6..4bd437a8 100644 --- a/src/HTTPCache.ts +++ b/src/HTTPCache.ts @@ -34,7 +34,7 @@ interface ResponseWithCacheWritePromise { } export class HTTPCache { - private keyValueCache: KeyValueCache; + private keyValueCache: KeyValueCache; private httpFetch: Fetcher; constructor( @@ -136,7 +136,7 @@ export class HTTPCache { ); } - const { policy: policyRaw, ttlOverride, body } = JSON.parse(entry); + const { policy: policyRaw, ttlOverride, body } = typeof entry === "object" ? entry : JSON.parse(entry); const policy = CachePolicy.fromObject(policyRaw) as SneakyCachePolicy; // Remove url from the policy, because otherwise it would never match a From baf9a32e00da0a12d57915e7bb62991a90bb5b11 Mon Sep 17 00:00:00 2001 From: aiova Date: Tue, 14 Jan 2025 20:48:03 +0200 Subject: [PATCH 22/27] fix: extend cache type to string and object --- .changeset/shy-poets-try.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shy-poets-try.md diff --git a/.changeset/shy-poets-try.md b/.changeset/shy-poets-try.md new file mode 100644 index 00000000..0e61a4fd --- /dev/null +++ b/.changeset/shy-poets-try.md @@ -0,0 +1,5 @@ +--- +'@apollo/datasource-rest-storage': minor +--- + +Handle objects and strings From 61657989b33111c85ddd5fda22a6cbfe22a72623 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 14 Jan 2025 18:49:07 +0000 Subject: [PATCH 23/27] chore: release version 6.4.0 --- .changeset/shy-poets-try.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/shy-poets-try.md diff --git a/.changeset/shy-poets-try.md b/.changeset/shy-poets-try.md deleted file mode 100644 index 0e61a4fd..00000000 --- a/.changeset/shy-poets-try.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@apollo/datasource-rest-storage': minor ---- - -Handle objects and strings diff --git a/CHANGELOG.md b/CHANGELOG.md index 70c0b070..457b8fac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/datasource-rest +## 6.4.0 + +### Minor Changes + +- [`baf9a32`](https://github.com/apollographql/datasource-rest/commit/baf9a32e00da0a12d57915e7bb62991a90bb5b11) Thanks [@adr-iova](https://github.com/adr-iova)! - Handle objects and strings + ## 6.3.4 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 99e98f9c..39e9660d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/datasource-rest-storage", - "version": "6.3.3", + "version": "6.3.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@apollo/datasource-rest-storage", - "version": "6.3.3", + "version": "6.3.4", "license": "MIT", "dependencies": { "@apollo/utils.fetcher": "^3.0.0", diff --git a/package.json b/package.json index 3837eade..d98886e2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@apollo/datasource-rest-storage", "description": "REST DataSource for Apollo Server v4", - "version": "6.3.4", + "version": "6.4.0", "author": "Apollo ", "license": "MIT", "repository": { From 2f0e04e433556f0c5533e0699b1da2291fa93927 Mon Sep 17 00:00:00 2001 From: aiova Date: Tue, 14 Jan 2025 21:29:50 +0200 Subject: [PATCH 24/27] fix: extend cache type to string and object --- .changeset/pink-pants-film.md | 5 +++++ src/HTTPCache.ts | 2 +- src/RESTDataSource.ts | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/pink-pants-film.md diff --git a/.changeset/pink-pants-film.md b/.changeset/pink-pants-film.md new file mode 100644 index 00000000..8b18af8a --- /dev/null +++ b/.changeset/pink-pants-film.md @@ -0,0 +1,5 @@ +--- +'@apollo/datasource-rest-storage': minor +--- + +add object to cache type diff --git a/src/HTTPCache.ts b/src/HTTPCache.ts index 4bd437a8..64f991f2 100644 --- a/src/HTTPCache.ts +++ b/src/HTTPCache.ts @@ -38,7 +38,7 @@ export class HTTPCache { private httpFetch: Fetcher; constructor( - keyValueCache: KeyValueCache = new InMemoryLRUCache(), + keyValueCache: KeyValueCache = new InMemoryLRUCache(), httpFetch: Fetcher = nodeFetch, ) { this.keyValueCache = new PrefixingKeyValueCache( diff --git a/src/RESTDataSource.ts b/src/RESTDataSource.ts index 70a26250..a2fd25b9 100644 --- a/src/RESTDataSource.ts +++ b/src/RESTDataSource.ts @@ -3,7 +3,7 @@ import type { FetcherRequestInit, FetcherResponse, } from '@apollo/utils.fetcher'; -import type { KeyValueCache } from '@apollo/utils.keyvaluecache'; +import type { KeyValueCache, KeyValueCacheSetOptions } from '@apollo/utils.keyvaluecache'; import type { Logger } from '@apollo/utils.logger'; import type { WithRequired } from '@apollo/utils.withrequired'; import { GraphQLError } from 'graphql'; @@ -146,7 +146,7 @@ export interface CacheOptions { const NODE_ENV = process.env.NODE_ENV; export interface DataSourceConfig { - cache?: KeyValueCache; + cache?: KeyValueCache; fetch?: Fetcher; logger?: Logger; } From d40bc5324ad834c498bcd15ceb8e5c748c9b6255 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 14 Jan 2025 19:31:00 +0000 Subject: [PATCH 25/27] chore: release version 6.5.0 --- .changeset/pink-pants-film.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/pink-pants-film.md diff --git a/.changeset/pink-pants-film.md b/.changeset/pink-pants-film.md deleted file mode 100644 index 8b18af8a..00000000 --- a/.changeset/pink-pants-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@apollo/datasource-rest-storage': minor ---- - -add object to cache type diff --git a/CHANGELOG.md b/CHANGELOG.md index 457b8fac..5455f49d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/datasource-rest +## 6.5.0 + +### Minor Changes + +- [`2f0e04e`](https://github.com/apollographql/datasource-rest/commit/2f0e04e433556f0c5533e0699b1da2291fa93927) Thanks [@adr-iova](https://github.com/adr-iova)! - add object to cache type + ## 6.4.0 ### Minor Changes diff --git a/package-lock.json b/package-lock.json index 39e9660d..13981daf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/datasource-rest-storage", - "version": "6.3.4", + "version": "6.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@apollo/datasource-rest-storage", - "version": "6.3.4", + "version": "6.4.0", "license": "MIT", "dependencies": { "@apollo/utils.fetcher": "^3.0.0", diff --git a/package.json b/package.json index d98886e2..9c16449d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@apollo/datasource-rest-storage", "description": "REST DataSource for Apollo Server v4", - "version": "6.4.0", + "version": "6.5.0", "author": "Apollo ", "license": "MIT", "repository": { From b6de2cc5f0e0d9006f87c28f54cbf885ed30e6bf Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 20 Jun 2025 17:19:30 +0300 Subject: [PATCH 26/27] fix: cache only successful responses --- src/HTTPCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HTTPCache.ts b/src/HTTPCache.ts index 64f991f2..cd35f23c 100644 --- a/src/HTTPCache.ts +++ b/src/HTTPCache.ts @@ -97,7 +97,7 @@ export class HTTPCache { const response = await this.httpFetch(urlString, requestOpts); const parsedBody = await response.json(); - if ((cacheOptions as CacheOptions)?.ttl) { + if ((cacheOptions as CacheOptions)?.ttl && response.status >= 200 && response.status <= 299) { const cacheWritePromise = this.keyValueCache.set( cacheKey, parsedBody, From f5fee95877e73a0e3c6fc3af243f65cccf8d860f Mon Sep 17 00:00:00 2001 From: aiova Date: Fri, 20 Jun 2025 17:37:06 +0300 Subject: [PATCH 27/27] fix: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c16449d..12edfdd9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@apollo/datasource-rest-storage", "description": "REST DataSource for Apollo Server v4", - "version": "6.5.0", + "version": "6.5.1", "author": "Apollo ", "license": "MIT", "repository": {