diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index ffa09a26..7f6896dd 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -1,10 +1,8 @@ name: Release - on: push: branches: - main - jobs: release: name: Release @@ -13,21 +11,43 @@ 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 with: node-version: 16.x - - name: Install Dependencies run: npm i - - - name: Create Release Pull Request / NPM Publish - uses: changesets/action@v1 - with: - publish: npm run changeset-publish + - 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 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + 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 main + - name: Create GitHub Release + uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + with: + tag_name: v${{ steps.version.outputs.new_version }} + release_name: Release v${{ steps.version.outputs.new_version }} + draft: false + prerelease: false diff --git a/CHANGELOG.md b/CHANGELOG.md index e9aa61af..5455f49d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # @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 + +- [`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 + +- [`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 + +- [`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 + +- [`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-lock.json b/package-lock.json index 6ccd8341..13981daf 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.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "@apollo/datasource-rest", - "version": "6.3.0", + "name": "@apollo/datasource-rest-storage", + "version": "6.4.0", "license": "MIT", "dependencies": { "@apollo/utils.fetcher": "^3.0.0", diff --git a/package.json b/package.json index 566abdef..12edfdd9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@apollo/datasource-rest", + "name": "@apollo/datasource-rest-storage", "description": "REST DataSource for Apollo Server v4", - "version": "6.3.0", + "version": "6.5.1", "author": "Apollo ", "license": "MIT", "repository": { @@ -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", diff --git a/src/HTTPCache.ts b/src/HTTPCache.ts index fda4a568..cd35f23c 100644 --- a/src/HTTPCache.ts +++ b/src/HTTPCache.ts @@ -30,14 +30,15 @@ interface SneakyCachePolicy extends CachePolicy { interface ResponseWithCacheWritePromise { response: FetcherResponse; cacheWritePromise?: Promise; + parsedBody?: any; } export class HTTPCache { - private keyValueCache: KeyValueCache; + private keyValueCache: KeyValueCache; private httpFetch: Fetcher; constructor( - keyValueCache: KeyValueCache = new InMemoryLRUCache(), + keyValueCache: KeyValueCache = new InMemoryLRUCache(), httpFetch: Fetcher = nodeFetch, ) { this.keyValueCache = new PrefixingKeyValueCache( @@ -75,6 +76,41 @@ export class HTTPCache { return { response: await this.httpFetch(urlString, requestOpts) }; } + let cacheOptions = cache?.cacheOptions; + 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(); + 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 && response.status >= 200 && response.status <= 299) { + 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 }; + } + const entry = requestOpts.skipCache !== true ? await this.keyValueCache.get(cacheKey) @@ -100,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 diff --git a/src/RESTDataSource.ts b/src/RESTDataSource.ts index e87a6197..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'; @@ -136,12 +136,17 @@ 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; export interface DataSourceConfig { - cache?: KeyValueCache; + cache?: KeyValueCache; fetch?: Fetcher; logger?: Logger; } @@ -323,17 +328,22 @@ 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) { - // consider using `structuredClone()` when we drop support for Node 16 - return cloneDeep(parsedBody); + protected cloneParsedBody(parsedBody: TResult, cacheOptions: CacheOptions) { + const cacheStrategy = cacheOptions?.cacheStrategy + if(cacheStrategy === "object") { + return (Array.isArray(parsedBody) ? [...parsedBody] : {...parsedBody}) as TResult + } else { + return cloneDeep(parsedBody); + } } protected shouldJSONSerializeBody( @@ -536,7 +546,7 @@ export abstract class RESTDataSource { ? outgoingRequest.cacheOptions : this.cacheOptionsFor?.bind(this); try { - const { response, cacheWritePromise } = await this.httpCache.fetch( + const { response, cacheWritePromise, parsedBody } = await this.httpCache.fetch( url, outgoingRequest, { @@ -551,17 +561,18 @@ export abstract class RESTDataSource { this.catchCacheWritePromiseErrors(cacheWritePromise); } - const parsedBody = await this.parseBody(response); + const _parsedBody = parsedBody ?? await this.parseBody(response); await this.throwIfResponseIsError({ url, request: outgoingRequest, response, - parsedBody, + // @ts-ignore + _parsedBody, }); return { - parsedBody: parsedBody as any as TResult, + parsedBody: _parsedBody as any as TResult, response, httpCache: { cacheWritePromise, @@ -574,6 +585,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 +603,7 @@ export abstract class RESTDataSource { this.cloneDataSourceFetchResult(result, { policy, deduplicatedAgainstPreviousRequest: true, - }), + }, cacheOptions as CacheOptions), ); const thisRequestPromise = performRequest(); @@ -610,7 +624,7 @@ export abstract class RESTDataSource { return this.cloneDataSourceFetchResult(await thisRequestPromise, { policy, deduplicatedAgainstPreviousRequest: false, - }); + }, cacheOptions as CacheOptions); } finally { if (policy.policy === 'deduplicate-during-request-lifetime') { this.deduplicationPromises.delete(policy.deduplicationKey);