From cf3c2a45ccfff2802e49d59f8ab5e11ef30e204c Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 20 Aug 2020 16:50:13 -0400 Subject: [PATCH 1/2] Provide default empty StoreObject for root IDs like ROOT_QUERY. This change means the existence of root objects like ROOT_QUERY and ROOT_MUTATION will no longer depend on whether other queries/mutations have previously written data into the cache. This matters because (until just recently) cache.read would return null for a completely missing ROOT_QUERY object, but throw missing field errors if ROOT_QUERY existed but did not have the requested fields. In other words, this commit hides the difference between the absence of ROOT_QUERY and its incompleteness, so unrelated cache writes can no longer unexpectedly influence the null-returning vs. exception-throwing behavior of cache.read. As an additional motivation, with AC3 field policies, it's possible now to define synthetic root query fields that have a chance of returning useful values even if the cache is completely empty. Providing a default empty object for root IDs like ROOT_QUERY is important to let those read functions be called, instead of prematurely returning null from cache.read when ROOT_QUERY does not exist. Note: this PR builds on PR #7098. --- src/cache/inmemory/__tests__/readFromStore.ts | 52 +++++++++++++++++++ src/cache/inmemory/entityStore.ts | 14 ++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/cache/inmemory/__tests__/readFromStore.ts b/src/cache/inmemory/__tests__/readFromStore.ts index e44a44ee675..6e879fed769 100644 --- a/src/cache/inmemory/__tests__/readFromStore.ts +++ b/src/cache/inmemory/__tests__/readFromStore.ts @@ -1065,6 +1065,58 @@ describe('reading from the store', () => { }); }); + it("read functions for root query fields work with empty cache", () => { + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + uuid() { + return "8d573b9c-cfcf-4e3e-98dd-14d255af577e"; + }, + null() { + return null; + }, + } + }, + }, + }); + + expect(cache.readQuery({ + query: gql`query { uuid null }`, + })).toEqual({ + uuid: "8d573b9c-cfcf-4e3e-98dd-14d255af577e", + null: null, + }); + + expect(cache.extract()).toEqual({}); + + expect(cache.readFragment({ + id: "ROOT_QUERY", + fragment: gql` + fragment UUIDFragment on Query { + null + uuid + } + `, + })).toEqual({ + uuid: "8d573b9c-cfcf-4e3e-98dd-14d255af577e", + null: null, + }); + + expect(cache.extract()).toEqual({}); + + expect(cache.readFragment({ + id: "does not exist", + fragment: gql` + fragment F on Never { + whatever + } + `, + })).toBe(null); + + expect(cache.extract()).toEqual({}); + }); + it("custom read functions can map/filter dangling references", () => { const cache = new InMemoryCache({ typePolicies: { diff --git a/src/cache/inmemory/entityStore.ts b/src/cache/inmemory/entityStore.ts index b98a226092a..e95c1118516 100644 --- a/src/cache/inmemory/entityStore.ts +++ b/src/cache/inmemory/entityStore.ts @@ -79,8 +79,18 @@ export abstract class EntityStore implements NormalizedCache { // should not rely on this dependency, since the contents could change // without the object being added or removed. if (dependOnExistence) this.group.depend(dataId, "__exists"); - return hasOwn.call(this.data, dataId) ? this.data[dataId] : - this instanceof Layer ? this.parent.lookup(dataId, dependOnExistence) : void 0; + + if (hasOwn.call(this.data, dataId)) { + return this.data[dataId]; + } + + if (this instanceof Layer) { + return this.parent.lookup(dataId, dependOnExistence); + } + + if (this.policies.rootTypenamesById[dataId]) { + return Object.create(null); + } } public merge(dataId: string, incoming: StoreObject): void { From 3da7a0295e37176dfabaefa3e7ea011c28a2ea89 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 30 Sep 2020 17:11:21 -0400 Subject: [PATCH 2/2] Mention PR #7100 in CHANGELOG.md. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d84ef03bae..dc0651571f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ - In addition to the `result.data` property, `useQuery` and `useLazyQuery` will now provide a `result.previousData` property, which can be useful when a network request is pending and `result.data` is undefined, since `result.previousData` can be rendered instead of rendering an empty/loading state.
[@hwillson](https://github.com/hwillson) in [#7082](https://github.com/apollographql/apollo-client/pull/7082) +- Provide default empty cache object for root IDs like `ROOT_QUERY`, to avoid differences in behavior before/after `ROOT_QUERY` data has been written into `InMemoryCache`.
+ [@benjamn](https://github.com/benjamn) in [#7100](https://github.com/apollographql/apollo-client/pull/7100) + ## Apollo Client 3.2.1 ## Bug Fixes