diff --git a/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap index 296372a0cd1..ec9b573d8df 100644 --- a/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap +++ b/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap @@ -1211,6 +1211,86 @@ Object { } `; +exports[`type policies field policies custom finalize functions are called if merge function returns undefined 1`] = ` +Object { + "ROOT_QUERY": Object { + "__typename": "Query", + "todoList": Object { + "__ref": "ToDoList:{}", + }, + }, + "Task:{\\"taskID\\":1}": Object { + "__typename": "Task", + "taskID": 1, + "text": "task #1", + }, + "Task:{\\"taskID\\":2}": Object { + "__typename": "Task", + "taskID": 2, + "text": "task #2", + }, + "ToDoList:{}": Object { + "__typename": "ToDoList", + "tasks": Array [ + Object { + "__ref": "Task:{\\"taskID\\":1}", + }, + Object { + "__ref": "Task:{\\"taskID\\":2}", + }, + ], + }, +} +`; + +exports[`type policies field policies custom finalize functions are called if merge function returns undefined 2`] = ` +Object { + "ROOT_QUERY": Object { + "__typename": "Query", + "todoList": Object { + "__ref": "ToDoList:{}", + }, + }, + "Task:{\\"taskID\\":1}": Object { + "__typename": "Task", + "taskID": 1, + "text": "task #1", + }, + "Task:{\\"taskID\\":2}": Object { + "__typename": "Task", + "taskID": 2, + "text": "task #2", + }, + "Task:{\\"taskID\\":3}": Object { + "__typename": "Task", + "taskID": 3, + "text": "task #3", + }, + "Task:{\\"taskID\\":4}": Object { + "__typename": "Task", + "taskID": 4, + "text": "task #4", + }, + "ToDoList:{}": Object { + "__typename": "ToDoList", + "tasks": Array [ + Object { + "__ref": "Task:{\\"taskID\\":1}", + }, + Object { + "__ref": "Task:{\\"taskID\\":2}", + }, + Object { + "__ref": "Task:{\\"taskID\\":3}", + }, + Object { + "__ref": "Task:{\\"taskID\\":4}", + }, + ], + }, +} +`; + exports[`type policies field policies read, merge, and modify functions can access options.storage 1`] = ` Object { "ROOT_QUERY": Object { diff --git a/src/cache/inmemory/__tests__/policies.ts b/src/cache/inmemory/__tests__/policies.ts index 9d6f739b599..69916c4447c 100644 --- a/src/cache/inmemory/__tests__/policies.ts +++ b/src/cache/inmemory/__tests__/policies.ts @@ -1824,6 +1824,104 @@ describe("type policies", function () { }, }); }); + + itAsync("are called if merge function returns undefined", resolve => { + const cache = new InMemoryCache({ + typePolicies: { + ToDoList: { + keyFields: [], + fields: { + tasks: { + keyArgs: false, + + merge(existing: number[] | undefined, incoming: number[], { args }) { + if (args && args.deleteOnMerge) return; + return existing ? [ + ...existing, + ...incoming, + ] : incoming; + }, + + finalize(existing) { + expect(existing).toEqual([ + { __ref: 'Task:{"taskID":1}' }, + { __ref: 'Task:{"taskID":2}' }, + { __ref: 'Task:{"taskID":3}' }, + { __ref: 'Task:{"taskID":4}' }, + ]); + // Finish the test (success). + resolve(); + }, + }, + }, + }, + + Task: { + keyFields: ["taskID"], + }, + }, + }); + + const query = gql` + query { + todoList { + tasks { + taskID + text + } + } + } + `; + + cache.writeQuery({ + query, + data: { + todoList: { + __typename: "ToDoList", + tasks: [ + { __typename: "Task", taskID: 1, text: "task #1" }, + { __typename: "Task", taskID: 2, text: "task #2" }, + ], + }, + }, + }); + + expect(cache.extract()).toMatchSnapshot(); + + cache.writeQuery({ + query, + data: { + todoList: { + __typename: "ToDoList", + tasks: [ + { __typename: "Task", taskID: 3, text: "task #3" }, + { __typename: "Task", taskID: 4, text: "task #4" }, + ], + }, + }, + }); + + expect(cache.extract()).toMatchSnapshot(); + + cache.writeQuery({ + query: gql` + query { + todoList { + tasks(deleteOnMerge: true) { + taskID + text + } + } + } + `, + data: { + todoList: { + __typename: "ToDoList", + tasks: [], + }, + }, + }); + }); }); it("merge functions can deduplicate items using readField", function () { diff --git a/src/cache/inmemory/entityStore.ts b/src/cache/inmemory/entityStore.ts index 7c8ae0e1b3d..cb4fd90bf0d 100644 --- a/src/cache/inmemory/entityStore.ts +++ b/src/cache/inmemory/entityStore.ts @@ -233,7 +233,19 @@ export abstract class EntityStore implements NormalizedCache { // restrict our attention to the incoming fields, since those are the // top-level fields that might have changed. Object.keys(incoming).forEach(storeFieldName => { - walk(existing[storeFieldName], incoming[storeFieldName]); + const eChild = existing[storeFieldName]; + const iChild = incoming[storeFieldName]; + + walk(eChild, iChild); + + if (iChild === void 0) { + this.policies.finalizeField( + existing.__typename, + existing, + storeFieldName, + context, + ); + } }); } } @@ -608,12 +620,19 @@ class CacheGroup { } } + // This WeakMap maps every non-normalized object reference contained by the + // store to the path of that object within the enclosing entity object. This + // information is collected by the assignPaths method after every store.merge, + // so store.data should never contain any un-pathed objects. As a reminder, + // these object references are handled immutably from here on, so the objects + // should not move around in a way that invalidates these paths. This path + // information is useful in the getStorage method, below. private paths = new WeakMap(); + public assignPaths(dataId: string, merged: StoreObject) { const paths = this.paths; const path: (string | number)[] = [dataId]; - // TODO function assign(this: void, obj: StoreValue) { if (Array.isArray(obj)) { obj.forEach(handleChild);