Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements for cache.modify: details.storage and details.INVALIDATE #6991

Merged
merged 10 commits into from Sep 8, 2020
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -27,6 +27,12 @@
- Throw if `writeFragment` cannot identify `options.data` when no `options.id` provided. <br/>
[@jcreighton](https://github.com/jcreighton) in [#6859](https://github.com/apollographql/apollo-client/pull/6859)

- Provide `options.storage` object to `cache.modify` functions, as provided to `read` and `merge` functions. <br/>
[@benjamn](https://github.com/benjamn) in [#6991](https://github.com/apollographql/apollo-client/pull/6991)

- Allow `cache.modify` functions to return `details.INVALIDATE` (similar to `details.DELETE`) to invalidate the current field, causing affected queries to rerun, even if the field's value is unchanged. <br/>
[@benjamn](https://github.com/benjamn) in [#6991](https://github.com/apollographql/apollo-client/pull/6991)

## Apollo Client 3.1.4

## Bug Fixes
Expand Down
1 change: 1 addition & 0 deletions docs/source/api/cache/InMemoryCache.mdx
Expand Up @@ -207,6 +207,7 @@ The first parameter of a modifier function is the current value of the field bei
| `canRead` | `CanReadFunction` | Returns `true` for non-normalized `StoreObjects` and non-dangling `Reference`s, indicating that `readField(name, objOrRef)` has a chance of working. Useful for filtering out dangling references from lists. |
| `isReference` | `boolean` | Utility to check if an object is a `{ __ref }` object. |
| `DELETE` | `any` | Sentinel object that can be returned from a modifier function to delete the field being modified. |
| `INVALIDATE` | `any` | Sentinel object that can be returned from a modifier function to invalidate the field, causing affected queries to rerun, without changing or deleting the field value. |

`Modifier` functions should return the value that is to be written into the cache for the field being modified, or a `DELETE` sentinel to remove the field.

Expand Down
30 changes: 30 additions & 0 deletions docs/source/caching/cache-interaction.md
Expand Up @@ -316,6 +316,36 @@ cache.modify({
});
```

### Example: Invalidating fields within a cached object

Normally, changing or deleting a field's value also _invalidates_ the field, causing watched queries to be reread if they previously consumed the field.

Using `cache.modify`, it's also possible to invalidate the field without changing or deleting its value, by returning the `INVALIDATE` sentinel:

```js
cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs, { INVALIDATE }) {
return INVALIDATE;
},
},
});
```

If you need to invalidate all fields within the given object, you can pass a modifier function as the value of the `fields` option:

```js
cache.modify({
id: cache.identify(myPost),
fields(fieldValue, details) {
return details.INVALIDATE;
},
});
```

When using this form of `cache.modify`, you can determine the individual field names using `details.fieldName`. This technique works for any modifier function, not just those that return `INVALIDATE`.

## Obtaining an object's custom ID

If a type in your cache uses a [custom identifier](./cache-configuration/#customizing-identifier-generation-by-type) (or even if it doesn't), you can use the `cache.identify` method to obtain the identifier for an object of that type. This method takes an object and computes its ID based on both its `__typename` and its identifier field(s). This means you don't have to keep track of which fields make up each type's identifier.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -53,7 +53,7 @@
{
"name": "apollo-client",
"path": "./dist/apollo-client.cjs.min.js",
"maxSize": "24.5 kB"
"maxSize": "24.7 kB"
}
],
"peerDependencies": {
Expand Down
4 changes: 4 additions & 0 deletions src/cache/core/types/common.ts
Expand Up @@ -7,6 +7,8 @@ import {
isReference,
} from '../../../utilities';

import { StorageType } from '../../inmemory/policies';

// The Readonly<T> type only really works for object types, since it marks
// all of the object's properties as readonly, but there are many cases when
// a generic type parameter like TExisting might be a string or some other
Expand Down Expand Up @@ -55,12 +57,14 @@ export type CanReadFunction = (value: StoreValue) => boolean;

export type Modifier<T> = (value: T, details: {
DELETE: any;
INVALIDATE: any;
fieldName: string;
storeFieldName: string;
readField: ReadFieldFunction;
canRead: CanReadFunction;
isReference: typeof isReference;
toReference: ToReferenceFunction;
storage: StorageType;
}) => T;

export type Modifiers = {
Expand Down
23 changes: 23 additions & 0 deletions src/cache/inmemory/__tests__/__snapshots__/cache.ts.snap
Expand Up @@ -228,6 +228,29 @@ Object {
}
`;

exports[`InMemoryCache#modify should allow invalidation using details.INVALIDATE 1`] = `
Object {
"Author:{\\"name\\":\\"Maria Dahvana Headley\\"}": Object {
"__typename": "Author",
"name": "Maria Dahvana Headley",
},
"Book:{\\"isbn\\":\\"0374110034\\"}": Object {
"__typename": "Book",
"author": Object {
"__ref": "Author:{\\"name\\":\\"Maria Dahvana Headley\\"}",
},
"isbn": "0374110034",
"title": "Beowulf: A New Translation",
},
"ROOT_QUERY": Object {
"__typename": "Query",
"currentlyReading": Object {
"__ref": "Book:{\\"isbn\\":\\"0374110034\\"}",
},
},
}
`;

exports[`TypedDocumentNode<Data, Variables> should determine Data and Variables types of {write,read}{Query,Fragment} 1`] = `
Object {
"Author:{\\"name\\":\\"John C. Mitchell\\"}": Object {
Expand Down
22 changes: 22 additions & 0 deletions src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap
Expand Up @@ -765,3 +765,25 @@ Object {
},
}
`;

exports[`type policies field policies read, merge, and modify functions can access options.storage 1`] = `
Object {
"ROOT_QUERY": Object {
"__typename": "Query",
"mergeModify": 11,
"mergeRead": 1,
"mergeReadModify": 101,
},
}
`;

exports[`type policies field policies read, merge, and modify functions can access options.storage 2`] = `
Object {
"ROOT_QUERY": Object {
"__typename": "Query",
"mergeModify": 11,
"mergeRead": 1,
"mergeReadModify": 101,
},
}
`;
98 changes: 98 additions & 0 deletions src/cache/inmemory/__tests__/cache.ts
Expand Up @@ -1645,6 +1645,104 @@ describe("InMemoryCache#modify", () => {
expect(resultAfterModify).toEqual({ a: 1, b: -1, c: 0 });
});

it("should allow invalidation using details.INVALIDATE", () => {
const cache = new InMemoryCache({
typePolicies: {
Book: {
keyFields: ["isbn"],
},
Author: {
keyFields: ["name"],
},
},
});

const query: TypedDocumentNode<{
currentlyReading: {
title: string;
isbn: string;
author: {
name: string;
},
},
}> = gql`
query {
currentlyReading {
title
isbn
author {
name
}
}
}
`;

const currentlyReading = {
__typename: "Book",
isbn: "0374110034",
title: "Beowulf: A New Translation",
author: {
__typename: "Author",
name: "Maria Dahvana Headley",
},
};

cache.writeQuery({
query,
data: {
currentlyReading,
}
});

function read() {
return cache.readQuery({ query })!;
}

const initialResult = read();

expect(cache.extract()).toMatchSnapshot();

expect(cache.modify({
id: cache.identify({
__typename: "Author",
name: "Maria Dahvana Headley",
}),
fields: {
name(_, { INVALIDATE }) {
return INVALIDATE;
},
},
})).toBe(false); // Nothing actually modified.

const resultAfterAuthorInvalidation = read();
expect(resultAfterAuthorInvalidation).not.toBe(initialResult);
expect(resultAfterAuthorInvalidation).toEqual(initialResult);

expect(cache.modify({
id: cache.identify({
__typename: "Book",
isbn: "0374110034",
}),
// Invalidate all fields of the Book entity.
fields(_, { INVALIDATE }) {
return INVALIDATE;
},
})).toBe(false); // Nothing actually modified.

const resultAfterBookInvalidation = read();
expect(resultAfterBookInvalidation).not.toBe(resultAfterAuthorInvalidation);
expect(resultAfterBookInvalidation).toEqual(resultAfterAuthorInvalidation);
expect(resultAfterBookInvalidation.currentlyReading.author).toEqual({
__typename: "Author",
name: "Maria Dahvana Headley",
});
expect(
resultAfterBookInvalidation.currentlyReading.author
).toBe(
resultAfterAuthorInvalidation.currentlyReading.author
);
});

it("should allow deletion using details.DELETE", () => {
const cache = new InMemoryCache({
typePolicies: {
Expand Down