diff --git a/CHANGELOG.md b/CHANGELOG.md index dd32e939442..8870c4553b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Apollo Client 3.5.0 (not yet released) +### Improvements + +- Add `updateQuery` and `updateFragment` methods to `ApolloCache`, simplifying common `readQuery`/`writeQuery` cache update patterns.
+ [@wassim-k](/~https://github.com/wassim-k) in [#8382](/~https://github.com/apollographql/apollo-client/pull/8382) + ### React Refactoring #### Bug Fixes (due to [@brainkim](/~https://github.com/brainkim) in [#8596](/~https://github.com/apollographql/apollo-client/pull/8596)): diff --git a/package.json b/package.json index 54e4c20b02d..928fae6e0a1 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ { "name": "apollo-client", "path": "./temp/bundlesize.min.cjs", - "maxSize": "24.7 kB" + "maxSize": "24.75 kB" } ], "engines": { diff --git a/src/cache/core/__tests__/cache.ts b/src/cache/core/__tests__/cache.ts index f1417add255..baa090c1af4 100644 --- a/src/cache/core/__tests__/cache.ts +++ b/src/cache/core/__tests__/cache.ts @@ -1,5 +1,5 @@ import gql from 'graphql-tag'; -import { ApolloCache } from '../cache'; +import { ApolloCache } from '../cache'; import { Cache, DataProxy } from '../..'; import { Reference } from '../../../utilities/graphql/storeUtils'; @@ -73,7 +73,7 @@ describe('abstract cache', () => { const test = new TestCache(); test.read = jest.fn(); - test.readQuery({query}); + test.readQuery({ query }); expect(test.read).toBeCalled(); }); @@ -81,8 +81,8 @@ describe('abstract cache', () => { const test = new TestCache(); test.read = ({ optimistic }) => optimistic as any; - expect(test.readQuery({query})).toBe(false); - expect(test.readQuery({query}, true)).toBe(true); + expect(test.readQuery({ query })).toBe(false); + expect(test.readQuery({ query }, true)).toBe(true); }); }); @@ -151,4 +151,159 @@ describe('abstract cache', () => { expect(test.write).toBeCalled(); }); }); + + describe('updateQuery', () => { + it('runs the readQuery & writeQuery methods', () => { + const test = new TestCache(); + test.readQuery = jest.fn(); + test.writeQuery = jest.fn(); + + test.updateQuery({ query }, data => 'foo'); + + expect(test.readQuery).toBeCalled(); + expect(test.writeQuery).toBeCalled(); + }); + + it('does not call writeQuery method if data is null', () => { + const test = new TestCache(); + test.readQuery = jest.fn(); + test.writeQuery = jest.fn(); + + test.updateQuery({ query }, data => null); + + expect(test.readQuery).toBeCalled(); + expect(test.writeQuery).not.toBeCalled(); + }); + + it('does not call writeQuery method if data is undefined', () => { + const test = new TestCache(); + test.readQuery = jest.fn(); + test.writeQuery = jest.fn(); + + test.updateQuery({ query }, data => { return; }); + + expect(test.readQuery).toBeCalled(); + expect(test.writeQuery).not.toBeCalled(); + }); + + it('calls the readQuery & writeQuery methods with the options object', () => { + const test = new TestCache(); + const options: Cache.UpdateQueryOptions = { query, broadcast: true, variables: { test: 1 }, optimistic: true, returnPartialData: true }; + test.readQuery = jest.fn(); + test.writeQuery = jest.fn(); + + test.updateQuery(options, data => 'foo'); + + expect(test.readQuery).toBeCalledWith( + expect.objectContaining(options) + ); + + expect(test.writeQuery).toBeCalledWith( + expect.objectContaining({ ...options, data: 'foo' }) + ); + }); + + it('returns current value in memory if no update was made', () => { + const test = new TestCache(); + test.readQuery = jest.fn().mockReturnValue('foo'); + expect(test.updateQuery({ query }, data => null)).toBe('foo'); + }); + + it('returns the updated value in memory if an update was made', () => { + const test = new TestCache(); + let currentValue = 'foo'; + test.readQuery = jest.fn().mockImplementation(() => currentValue); + test.writeQuery = jest.fn().mockImplementation(({ data }) => currentValue = data); + expect(test.updateQuery({ query }, data => 'bar')).toBe('bar'); + }); + + it('calls update function with the current value in memory', () => { + const test = new TestCache(); + test.readQuery = jest.fn().mockReturnValue('foo'); + test.updateQuery({ query }, data => { + expect(data).toBe('foo'); + }); + }); + }); + + describe('updateFragment', () => { + const fragmentId = 'frag'; + const fragment = gql` + fragment a on b { + name + } + `; + + it('runs the readFragment & writeFragment methods', () => { + const test = new TestCache(); + test.readFragment = jest.fn(); + test.writeFragment = jest.fn(); + + test.updateFragment({ id: fragmentId, fragment }, data => 'foo'); + + expect(test.readFragment).toBeCalled(); + expect(test.writeFragment).toBeCalled(); + }); + + it('does not call writeFragment method if data is null', () => { + const test = new TestCache(); + test.readFragment = jest.fn(); + test.writeFragment = jest.fn(); + + test.updateFragment({ id: fragmentId, fragment }, data => null); + + expect(test.readFragment).toBeCalled(); + expect(test.writeFragment).not.toBeCalled(); + }); + + it('does not call writeFragment method if data is undefined', () => { + const test = new TestCache(); + test.readFragment = jest.fn(); + test.writeFragment = jest.fn(); + + test.updateFragment({ id: fragmentId, fragment }, data => { return; }); + + expect(test.readFragment).toBeCalled(); + expect(test.writeFragment).not.toBeCalled(); + }); + + it('calls the readFragment & writeFragment methods with the options object', () => { + const test = new TestCache(); + const options: Cache.UpdateFragmentOptions = { id: fragmentId, fragment, fragmentName: 'a', broadcast: true, variables: { test: 1 }, optimistic: true, returnPartialData: true }; + test.readFragment = jest.fn(); + test.writeFragment = jest.fn(); + + test.updateFragment(options, data => 'foo'); + + expect(test.readFragment).toBeCalledWith( + expect.objectContaining(options) + ); + + expect(test.writeFragment).toBeCalledWith( + expect.objectContaining({ ...options, data: 'foo' }) + ); + }); + + it('returns current value in memory if no update was made', () => { + const test = new TestCache(); + test.readFragment = jest.fn().mockReturnValue('foo'); + expect(test.updateFragment({ id: fragmentId, fragment }, data => { return; })).toBe('foo'); + }); + + it('returns the updated value in memory if an update was made', () => { + const test = new TestCache(); + let currentValue = 'foo'; + test.readFragment = jest.fn().mockImplementation(() => currentValue); + test.writeFragment = jest.fn().mockImplementation(({ data }) => currentValue = data); + expect(test.updateFragment({ id: fragmentId, fragment }, data => 'bar')).toBe('bar'); + }); + + it('calls update function with the current value in memory', () => { + const test = new TestCache(); + test.readFragment = jest.fn().mockReturnValue('foo'); + test.updateFragment({ id: fragmentId, fragment }, data => { + expect(data).toBe('foo'); + }); + }); + }); }); diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index db5c63879a8..f15a142f8f8 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -166,4 +166,26 @@ export abstract class ApolloCache implements DataProxy { result: data, })); } + + public updateQuery( + options: Cache.UpdateQueryOptions, + update: (data: TData | null) => TData | null | void, + ): TData | null { + const value = this.readQuery(options); + const data = update(value); + if (data === void 0 || data === null) return value; + this.writeQuery({ ...options, data }); + return data; + } + + public updateFragment( + options: Cache.UpdateFragmentOptions, + update: (data: TData | null) => TData | null | void, + ): TData | null { + const value = this.readFragment(options); + const data = update(value); + if (data === void 0 || data === null) return value; + this.writeFragment({ ...options, data }); + return data; + } } diff --git a/src/cache/core/types/Cache.ts b/src/cache/core/types/Cache.ts index 98a21afd4f5..fe9b9d83bbb 100644 --- a/src/cache/core/types/Cache.ts +++ b/src/cache/core/types/Cache.ts @@ -92,5 +92,7 @@ export namespace Cache { export import ReadFragmentOptions = DataProxy.ReadFragmentOptions; export import WriteQueryOptions = DataProxy.WriteQueryOptions; export import WriteFragmentOptions = DataProxy.WriteFragmentOptions; + export import UpdateQueryOptions = DataProxy.UpdateQueryOptions; + export import UpdateFragmentOptions = DataProxy.UpdateFragmentOptions; export import Fragment = DataProxy.Fragment; } diff --git a/src/cache/core/types/DataProxy.ts b/src/cache/core/types/DataProxy.ts index 8e04d01a5df..1793149688c 100644 --- a/src/cache/core/types/DataProxy.ts +++ b/src/cache/core/types/DataProxy.ts @@ -118,6 +118,18 @@ export namespace DataProxy { export interface WriteFragmentOptions extends Fragment, WriteOptions {} + export interface UpdateQueryOptions + extends Omit<( + ReadQueryOptions & + WriteQueryOptions + ), 'data'> {} + + export interface UpdateFragmentOptions + extends Omit<( + ReadFragmentOptions & + WriteFragmentOptions + ), 'data'> {} + export type DiffResult = { result?: T; complete?: boolean;