Skip to content

Commit

Permalink
Add updateQuery and updateFragment methods to ApolloCache (#8382)
Browse files Browse the repository at this point in the history
Co-authored-by: Ben Newman <ben@apollographql.com>
  • Loading branch information
wassim-k and benjamn authored Aug 24, 2021
1 parent 02405fc commit 82c71dc
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 5 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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. <br/>
[@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)):
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
{
"name": "apollo-client",
"path": "./temp/bundlesize.min.cjs",
"maxSize": "24.7 kB"
"maxSize": "24.75 kB"
}
],
"engines": {
Expand Down
163 changes: 159 additions & 4 deletions src/cache/core/__tests__/cache.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -73,16 +73,16 @@ describe('abstract cache', () => {
const test = new TestCache();
test.read = jest.fn();

test.readQuery({query});
test.readQuery({ query });
expect(test.read).toBeCalled();
});

it('defaults optimistic to false', () => {
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);
});
});

Expand Down Expand Up @@ -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<string, any> = { 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<string, any> = { 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');
});
});
});
});
22 changes: 22 additions & 0 deletions src/cache/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,26 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
result: data,
}));
}

public updateQuery<TData = any, TVariables = any>(
options: Cache.UpdateQueryOptions<TData, TVariables>,
update: (data: TData | null) => TData | null | void,
): TData | null {
const value = this.readQuery<TData, TVariables>(options);
const data = update(value);
if (data === void 0 || data === null) return value;
this.writeQuery<TData, TVariables>({ ...options, data });
return data;
}

public updateFragment<TData = any, TVariables = any>(
options: Cache.UpdateFragmentOptions<TData, TVariables>,
update: (data: TData | null) => TData | null | void,
): TData | null {
const value = this.readFragment<TData, TVariables>(options);
const data = update(value);
if (data === void 0 || data === null) return value;
this.writeFragment<TData, TVariables>({ ...options, data });
return data;
}
}
2 changes: 2 additions & 0 deletions src/cache/core/types/Cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
12 changes: 12 additions & 0 deletions src/cache/core/types/DataProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ export namespace DataProxy {
export interface WriteFragmentOptions<TData, TVariables>
extends Fragment<TVariables, TData>, WriteOptions<TData> {}

export interface UpdateQueryOptions<TData, TVariables>
extends Omit<(
ReadQueryOptions<TData, TVariables> &
WriteQueryOptions<TData, TVariables>
), 'data'> {}

export interface UpdateFragmentOptions<TData, TVariables>
extends Omit<(
ReadFragmentOptions<TData, TVariables> &
WriteFragmentOptions<TData, TVariables>
), 'data'> {}

export type DiffResult<T> = {
result?: T;
complete?: boolean;
Expand Down

0 comments on commit 82c71dc

Please sign in to comment.