Skip to content

Commit

Permalink
Add modifyQuery & modifyFragment to ApolloCache
Browse files Browse the repository at this point in the history
  • Loading branch information
wassim-k authored and benjamn committed Aug 24, 2021
1 parent ed9f5cb commit a4d4574
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 4 deletions.
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('modifyQuery', () => {
it('runs the readQuery & writeQuery methods', () => {
const test = new TestCache();
test.readQuery = jest.fn();
test.writeQuery = jest.fn();

test.modifyQuery({ 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.modifyQuery({ 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.modifyQuery({ 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.ModifyQueryOptions<string, any> = { query, broadcast: true, variables: { test: 1 }, optimistic: true, returnPartialData: true };
test.readQuery = jest.fn();
test.writeQuery = jest.fn();

test.modifyQuery(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.modifyQuery({ 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.modifyQuery({ query }, data => 'bar')).toBe('bar');
});

it('calls modify function with the current value in memory', () => {
const test = new TestCache();
test.readQuery = jest.fn().mockReturnValue('foo');
test.modifyQuery({ query }, data => {
expect(data).toBe('foo');
});
});
});

describe('modifyFragment', () => {
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.modifyFragment({ 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.modifyFragment({ 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.modifyFragment({ 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.ModifyFragmentOptions<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.modifyFragment(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.modifyFragment({ 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.modifyFragment({ id: fragmentId, fragment }, data => 'bar')).toBe('bar');
});

it('calls modify function with the current value in memory', () => {
const test = new TestCache();
test.readFragment = jest.fn().mockReturnValue('foo');
test.modifyFragment({ 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 modifyQuery<TData = any, TVariables = any>(
options: Cache.ModifyQueryOptions<TData, TVariables>,
modifyFn: (data: TData | null) => TData | null | void
): TData | null {
const value = this.readQuery<TData, TVariables>(options);
const data = modifyFn(value);
if (data === undefined || data === null) return value;
this.writeQuery<TData, TVariables>({ ...options, data });
return data;
}

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

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

export interface ModifyFragmentOptions<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 a4d4574

Please sign in to comment.