diff --git a/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap index f4d2f946cb6..a4b15795298 100644 --- a/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap +++ b/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap @@ -71,6 +71,64 @@ Object { } `; +exports[`writing to the store root type policy merge is called before cache deep merge 1`] = ` +Array [ + Array [ + Object { + "existing": Object { + "__ref": "Person:123", + }, + "incoming": Object { + "__typename": "Person", + "age": 28, + "id": 123, + "name": "Gaston", + "status": "ACTIVE", + "updatedAt": 100000, + }, + "times": 1, + }, + ], + Array [ + Object { + "existing": undefined, + "incoming": Object { + "__ref": "Person:123", + }, + "times": 2, + }, + ], + Array [ + Object { + "existing": Object { + "__ref": "Person:123", + }, + "incoming": Object { + "__typename": "Person", + "id": 123, + "status": "DISABLED", + "updatedAt": 50, + }, + "times": 3, + }, + ], + Array [ + Object { + "existing": Object { + "__ref": "Person:123", + }, + "incoming": Object { + "__typename": "Person", + "id": 123, + "status": "PENDING", + "updatedAt": 100001, + }, + "times": 4, + }, + ], +] +`; + exports[`writing to the store should not keep reference when type of mixed inlined field changes to non-inlined field 1`] = ` [MockFunction] { "calls": Array [ diff --git a/src/cache/inmemory/__tests__/writeToStore.ts b/src/cache/inmemory/__tests__/writeToStore.ts index 9e236c7ccfc..b870256366f 100644 --- a/src/cache/inmemory/__tests__/writeToStore.ts +++ b/src/cache/inmemory/__tests__/writeToStore.ts @@ -2937,6 +2937,157 @@ describe('writing to the store', () => { }); }); + it("root type policy merge is called before cache deep merge", () => { + const personMergeMock = jest.fn() + + let times = 0 + const cache = new InMemoryCache({ + typePolicies: { + Person: { + merge(existing, incoming, tools) { + times++ + + personMergeMock({ + times, + existing, + incoming, + }) + + if (tools.isReference(existing) && !tools.isReference(incoming)) { + const cachedData = tools.cache.data.lookup(existing.__ref) + const existingUpdatedAt = cachedData?.["updatedAt"] + const incomingUpdatedAt = incoming?.["updatedAt"] + if ( + typeof existingUpdatedAt === "number" && + typeof incomingUpdatedAt === "number" && + existingUpdatedAt > incomingUpdatedAt + ) { + return existing + } + } + + return tools.mergeObjects(existing, incoming) + }, + }, + }, + }) + + expect(times).toEqual(0) + + const query = gql` + query Person($offset: Int, $limit: Int) { + person { + id + name + age + status + updatedAt + } + } + ` + + expect(times).toEqual(0) + + cache.writeQuery({ + query, + data: { + person: { + __typename: "Person", + id: 123, + name: "Gaston", + age: 28, + status: "ACTIVE", + updatedAt: 100000, + }, + }, + variables: {}, + }) + + expect(times).toEqual(2) // TODO: ideally this should only be called once here + + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + person: { + __ref: "Person:123", + }, + }, + "Person:123": { + __typename: "Person", + id: 123, + name: "Gaston", + age: 28, + status: "ACTIVE", + updatedAt: 100000, + }, + }) + + cache.writeQuery({ + query, + data: { + person: { + __typename: "Person", + id: 123, + status: "DISABLED", + updatedAt: 50, + }, + }, + variables: {}, + }) + + expect(times).toEqual(3) + + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + person: { + __ref: "Person:123", + }, + }, + "Person:123": { + __typename: "Person", + id: 123, + name: "Gaston", + age: 28, + status: "ACTIVE", + updatedAt: 100000, + }, + }) + + cache.writeQuery({ + query, + data: { + person: { + __typename: "Person", + id: 123, + status: "PENDING", + updatedAt: 100001, + }, + }, + variables: {}, + }) + + expect(personMergeMock.mock.calls).toMatchSnapshot() + expect(times).toEqual(4) + + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + person: { + __ref: "Person:123", + }, + }, + "Person:123": { + __typename: "Person", + id: 123, + name: "Gaston", + age: 28, + status: "PENDING", + updatedAt: 100001, + }, + }) + }) + describe("StoreWriter", () => { const writer = new StoreWriter(new InMemoryCache()); diff --git a/src/cache/inmemory/entityStore.ts b/src/cache/inmemory/entityStore.ts index f5fbf610b43..d7404da70e0 100644 --- a/src/cache/inmemory/entityStore.ts +++ b/src/cache/inmemory/entityStore.ts @@ -75,7 +75,7 @@ export abstract class EntityStore implements NormalizedCache { } } - protected lookup(dataId: string, dependOnExistence?: boolean): StoreObject | undefined { + public lookup(dataId: string, dependOnExistence?: boolean): StoreObject | undefined { // The has method (above) calls lookup with dependOnExistence = true, so // that it can later be invalidated when we add or remove a StoreObject for // this dataId. Any consumer who cares about the contents of the StoreObject diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index 6ce2a71d677..873e2db933b 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -32,7 +32,7 @@ type BroadcastOptions = Pick< > export class InMemoryCache extends ApolloCache { - private data: EntityStore; + public data: EntityStore; private optimisticData: EntityStore; protected config: InMemoryCacheConfig;