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

Allow custom merge functions (including merge:true and merge:false) in type policies. #7070

Merged
merged 10 commits into from
Sep 25, 2020
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
- Support inheritance of type and field policies, according to `possibleTypes`. <br/>
[@benjamn](https://github.com/benjamn) in [#7065](https://github.com/apollographql/apollo-client/pull/7065)

- Allow configuring custom `merge` functions, including the `merge: true` and `merge: false` shorthands, in type policies as well as field policies. <br/>
[@benjamn](https://github.com/benjamn) in [#7070](https://github.com/apollographql/apollo-client/pull/7070)

- Shallow-merge `options.variables` when combining existing or default options with newly-provided options, so new variables do not completely overwrite existing variables. <br/>
[@amannn](https://github.com/amannn) in [#6927](https://github.com/apollographql/apollo-client/pull/6927)

Expand Down
27 changes: 27 additions & 0 deletions docs/source/caching/cache-field-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,33 @@ const cache = new InMemoryCache({

In summary, the `Book.author` policy above allows the cache to safely merge the `author` objects of any two `Book` objects that have the same identity.

#### Configuring `merge` functions for types rather than fields

Beginning with Apollo Client 3.3, you can avoid having to configure `merge` functions for lots of different fields that might hold an `Author` object, and instead put the `merge` configuration in the `Author` type policy:

```ts{13}
const cache = new InMemoryCache({
typePolicies: {
Book: {
fields: {
// No longer necessary!
// author: {
// merge: true,
// },
},
},

Author: {
merge: true,
},
},
});
```

These configurations have the same behavior, but putting the `merge: true` in the `Author` type policy is shorter and easier to maintain, especially when `Author` objects could appear in lots of different fields besides `Book.author`.

Remember that mergeable objects will only be merged with existing objects occupying the same field of the same parent object, and only when the `__typename` of the objects is the same. If you really need to work around these rules, you can write a custom `merge` function to do whatever you want, but `merge: true` follows these rules.

### Merging arrays of non-normalized objects

Once you're comfortable with the ideas and recommendations from the previous section, consider what happens when a `Book` can have multiple authors:
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
{
"name": "apollo-client",
"path": "./dist/apollo-client.cjs.min.js",
"maxSize": "24.9 kB"
"maxSize": "25 kB"
}
],
"peerDependencies": {
Expand All @@ -77,7 +77,7 @@
"fast-json-stable-stringify": "^2.0.0",
"graphql-tag": "^2.11.0",
"hoist-non-react-statics": "^3.3.2",
"optimism": "^0.12.1",
"optimism": "^0.12.2",
"prop-types": "^15.7.2",
"symbol-observable": "^2.0.0",
"terser": "^5.2.0",
Expand Down
253 changes: 202 additions & 51 deletions src/cache/inmemory/__tests__/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3703,32 +3703,7 @@ describe("type policies", function () {
Author: {
keyFields: false,
fields: {
books: {
merge(existing: any[], incoming: any[], {
isReference,
}) {
const merged = existing ? existing.slice(0) : [];
const seen = new Set<string>();
if (existing) {
existing.forEach(book => {
if (isReference(book)) {
seen.add(book.__ref);
}
});
}
incoming.forEach(book => {
if (isReference(book)) {
if (!seen.has(book.__ref)) {
merged.push(book);
seen.add(book.__ref);
}
} else {
merged.push(book);
}
});
return merged;
},
},
books: booksMergePolicy(),
},
},
},
Expand All @@ -3737,6 +3712,35 @@ describe("type policies", function () {
testForceMerges(cache);
});

function booksMergePolicy(): FieldPolicy<any[]> {
return {
merge(existing, incoming, {
isReference,
}) {
const merged = existing ? existing.slice(0) : [];
const seen = new Set<string>();
if (existing) {
existing.forEach(book => {
if (isReference(book)) {
seen.add(book.__ref);
}
});
}
incoming.forEach(book => {
if (isReference(book)) {
if (!seen.has(book.__ref)) {
merged.push(book);
seen.add(book.__ref);
}
} else {
merged.push(book);
}
});
return merged;
},
};
}

function testForceMerges(cache: InMemoryCache) {
const queryWithAuthorName = gql`
query {
Expand Down Expand Up @@ -3808,6 +3812,7 @@ describe("type policies", function () {
books: [{
__typename: "Book",
isbn: "1250758009",
title: "The Topeka School",
}],
},
},
Expand Down Expand Up @@ -3917,7 +3922,7 @@ describe("type policies", function () {
}

// Same as previous test, except with merge:true for Book.author.
it("can force merging with merge: true", function () {
it("can force merging with merge:true field policy", function () {
const cache = new InMemoryCache({
typePolicies: {
Book: {
Expand All @@ -3932,38 +3937,184 @@ describe("type policies", function () {
Author: {
keyFields: false,
fields: {
books: {
merge(existing: any[], incoming: any[], {
isReference,
}) {
const merged = existing ? existing.slice(0) : [];
const seen = new Set<string>();
if (existing) {
existing.forEach(book => {
if (isReference(book)) {
seen.add(book.__ref);
}
});
}
incoming.forEach(book => {
if (isReference(book)) {
if (!seen.has(book.__ref)) {
merged.push(book);
seen.add(book.__ref);
}
} else {
merged.push(book);
}
});
return merged;
books: booksMergePolicy(),
},
},
},
});

testForceMerges(cache);
});

// Same as previous test, except configuring merge:true for the Author
// type instead of for the Book.author field.
it("can force merging with merge:true type policy", function () {
const cache = new InMemoryCache({
typePolicies: {
Book: {
keyFields: ["isbn"],
},

Author: {
keyFields: false,
merge: true,
fields: {
books: booksMergePolicy(),
},
},
},
});

testForceMerges(cache);
});

it("can force merging with inherited merge:true field policy", function () {
const cache = new InMemoryCache({
typePolicies: {
Authored: {
fields: {
author: {
merge: true,
},
},
},

Book: {
keyFields: ["isbn"],
},

Author: {
keyFields: false,
fields: {
books: booksMergePolicy(),
},
},
},

possibleTypes: {
Authored: ["Book", "Destruction"],
},
});

testForceMerges(cache);
});

it("can force merging with inherited merge:true type policy", function () {
const cache = new InMemoryCache({
typePolicies: {
Book: {
keyFields: ["isbn"],
},

Author: {
fields: {
books: booksMergePolicy(),
},
},

Person: {
keyFields: false,
merge: true,
},
},

possibleTypes: {
Person: ["Author"],
},
});

testForceMerges(cache);
});

function checkAuthor<TData>(data: TData, canBeUndefined = false) {
if (data || !canBeUndefined) {
expect(data).toBeTruthy();
expect(typeof data).toBe("object");
expect((data as any).__typename).toBe("Author");
}
return data;
}

it("can force merging with inherited type policy merge function", function () {
let personMergeCount = 0;

const cache = new InMemoryCache({
typePolicies: {
Book: {
keyFields: ["isbn"],
},

Author: {
fields: {
books: booksMergePolicy(),
},
},

Person: {
keyFields: false,

merge(existing, incoming) {
checkAuthor(existing, true);
checkAuthor(incoming);
++personMergeCount;
return { ...existing, ...incoming };
},
},
},

possibleTypes: {
Person: ["Author"],
},
});

testForceMerges(cache);

expect(personMergeCount).toBe(3);
});

it("can force merging with inherited field merge function", function () {
let authorMergeCount = 0;

const cache = new InMemoryCache({
typePolicies: {
Book: {
keyFields: ["isbn"],
},

Authored: {
fields: {
author: {
merge(existing, incoming) {
checkAuthor(existing, true);
checkAuthor(incoming);
++authorMergeCount;
return { ...existing, ...incoming };
},
},
},
},


Author: {
fields: {
books: booksMergePolicy(),
},
},

Person: {
keyFields: false,
},
},

possibleTypes: {
Authored: ["Destiny", "Book"],
Person: ["Author"],
},
});

testForceMerges(cache);

expect(authorMergeCount).toBe(3);
});
});

Expand Down
Loading