From 90a06eeeb5a50eb172f5c6211693ea051897d8f3 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 9 Mar 2023 14:04:47 -0500 Subject: [PATCH] Fix type policy inheritance involving fuzzy `possibleTypes` (#10633) --- .changeset/odd-students-crash.md | 5 + config/bundlesize.ts | 2 +- src/cache/inmemory/__tests__/policies.ts | 176 +++++++++++++++++++++++ src/cache/inmemory/policies.ts | 32 ++++- 4 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 .changeset/odd-students-crash.md diff --git a/.changeset/odd-students-crash.md b/.changeset/odd-students-crash.md new file mode 100644 index 00000000000..ede83e9c182 --- /dev/null +++ b/.changeset/odd-students-crash.md @@ -0,0 +1,5 @@ +--- +'@apollo/client': patch +--- + +Fix type policy inheritance involving fuzzy `possibleTypes` diff --git a/config/bundlesize.ts b/config/bundlesize.ts index df0e4599b11..c65cce731d9 100644 --- a/config/bundlesize.ts +++ b/config/bundlesize.ts @@ -3,7 +3,7 @@ import { join } from "path"; import { gzipSync } from "zlib"; import bytes from "bytes"; -const gzipBundleByteLengthLimit = bytes("33.97KB"); +const gzipBundleByteLengthLimit = bytes("34.1KB"); const minFile = join("dist", "apollo-client.min.cjs"); const minPath = join(__dirname, "..", minFile); const gzipByteLen = gzipSync(readFileSync(minPath)).byteLength; diff --git a/src/cache/inmemory/__tests__/policies.ts b/src/cache/inmemory/__tests__/policies.ts index 843d7ce8542..11ac14dcb16 100644 --- a/src/cache/inmemory/__tests__/policies.ts +++ b/src/cache/inmemory/__tests__/policies.ts @@ -627,6 +627,182 @@ describe("type policies", function () { })).toBe('DeathAdder:{"tagId":"LethalAbacus666"}'); }); + it("typePolicies can be inherited from supertypes with fuzzy possibleTypes", () => { + const cache = new InMemoryCache({ + possibleTypes: { + EntitySupertype: [".*Entity"], + }, + typePolicies: { + Query: { + fields: { + coworkers: { + merge(existing, incoming) { + return existing ? existing.concat(incoming) : incoming; + }, + }, + }, + }, + + // The point of this test is to ensure keyFields: ["uid"] can be + // registered for all __typename strings matching the RegExp /.*Entity/, + // without manually enumerating all of them. + EntitySupertype: { + keyFields: ["uid"], + }, + }, + }); + + type Coworker = { + __typename: "CoworkerEntity" | "ManagerEntity"; + uid: string; + name: string; + } + + const query: TypedDocumentNode<{ + coworkers: Coworker[]; + }> = gql` + query { + coworkers { + uid + name + } + } + `; + + cache.writeQuery({ + query, + data: { + coworkers: [ + { __typename: "CoworkerEntity", uid: "qwer", name: "Alessia" }, + { __typename: "CoworkerEntity", uid: "asdf", name: "Jerel" }, + { __typename: "CoworkerEntity", uid: "zxcv", name: "Lenz" }, + { __typename: "ManagerEntity", uid: "uiop", name: "Jeff" }, + ], + }, + }); + + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + coworkers: [ + { __ref: 'CoworkerEntity:{"uid":"qwer"}' }, + { __ref: 'CoworkerEntity:{"uid":"asdf"}' }, + { __ref: 'CoworkerEntity:{"uid":"zxcv"}' }, + { __ref: 'ManagerEntity:{"uid":"uiop"}' }, + ], + }, + 'CoworkerEntity:{"uid":"qwer"}': { + __typename: "CoworkerEntity", + uid: "qwer", + name: "Alessia", + }, + 'CoworkerEntity:{"uid":"asdf"}': { + __typename: "CoworkerEntity", + uid: "asdf", + name: "Jerel", + }, + 'CoworkerEntity:{"uid":"zxcv"}': { + __typename: "CoworkerEntity", + uid: "zxcv", + name: "Lenz", + }, + 'ManagerEntity:{"uid":"uiop"}': { + __typename: "ManagerEntity", + uid: "uiop", + name: "Jeff", + }, + }); + + interface CoworkerWithAlias extends Omit { + idAlias: string; + } + + const queryWithAlias: TypedDocumentNode<{ + coworkers: CoworkerWithAlias[]; + }> = gql` + query { + coworkers { + idAlias: uid + name + } + } + `; + + expect(cache.readQuery({ query: queryWithAlias })).toEqual({ + coworkers: [ + { __typename: "CoworkerEntity", idAlias: "qwer", name: "Alessia" }, + { __typename: "CoworkerEntity", idAlias: "asdf", name: "Jerel" }, + { __typename: "CoworkerEntity", idAlias: "zxcv", name: "Lenz" }, + { __typename: "ManagerEntity", idAlias: "uiop", name: "Jeff" }, + ], + }); + + cache.writeQuery({ + query: queryWithAlias, + data: { + coworkers: [ + { __typename: "CoworkerEntity", idAlias: "hjkl", name: "Martijn" }, + { __typename: "ManagerEntity", idAlias: "vbnm", name: "Hugh" }, + ], + }, + }); + + expect(cache.readQuery({ query })).toEqual({ + coworkers: [ + { __typename: "CoworkerEntity", uid: "qwer", name: "Alessia" }, + { __typename: "CoworkerEntity", uid: "asdf", name: "Jerel" }, + { __typename: "CoworkerEntity", uid: "zxcv", name: "Lenz" }, + { __typename: "ManagerEntity", uid: "uiop", name: "Jeff" }, + { __typename: "CoworkerEntity", uid: "hjkl", name: "Martijn" }, + { __typename: "ManagerEntity", uid: "vbnm", name: "Hugh" }, + ], + }); + + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + coworkers: [ + { __ref: 'CoworkerEntity:{"uid":"qwer"}' }, + { __ref: 'CoworkerEntity:{"uid":"asdf"}' }, + { __ref: 'CoworkerEntity:{"uid":"zxcv"}' }, + { __ref: 'ManagerEntity:{"uid":"uiop"}' }, + { __ref: 'CoworkerEntity:{"uid":"hjkl"}' }, + { __ref: 'ManagerEntity:{"uid":"vbnm"}' }, + ], + }, + 'CoworkerEntity:{"uid":"qwer"}': { + __typename: "CoworkerEntity", + uid: "qwer", + name: "Alessia", + }, + 'CoworkerEntity:{"uid":"asdf"}': { + __typename: "CoworkerEntity", + uid: "asdf", + name: "Jerel", + }, + 'CoworkerEntity:{"uid":"zxcv"}': { + __typename: "CoworkerEntity", + uid: "zxcv", + name: "Lenz", + }, + 'ManagerEntity:{"uid":"uiop"}': { + __typename: "ManagerEntity", + uid: "uiop", + name: "Jeff", + }, + 'CoworkerEntity:{"uid":"hjkl"}': { + __typename: "CoworkerEntity", + uid: "hjkl", + name: "Martijn", + }, + 'ManagerEntity:{"uid":"vbnm"}': { + __typename: "ManagerEntity", + uid: "vbnm", + name: "Hugh", + }, + }); + }); + describe("field policies", function () { it(`can filter arguments using keyArgs`, function () { const cache = new InMemoryCache({ diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index 5f7b76d3285..121ef24ffd8 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -565,11 +565,33 @@ export class Policies { // and merge functions often need to cooperate, so changing only one // of them would be a recipe for inconsistency. // - // Once the TypePolicy for typename has been accessed, its - // properties can still be updated directly using addTypePolicies, - // but future changes to supertype policies will not be reflected in - // this policy, because this code runs at most once per typename. - const supertypes = this.supertypeMap.get(typename); + // Once the TypePolicy for typename has been accessed, its properties can + // still be updated directly using addTypePolicies, but future changes to + // inherited supertype policies will not be reflected in this subtype + // policy, because this code runs at most once per typename. + let supertypes = this.supertypeMap.get(typename); + if (!supertypes && this.fuzzySubtypes.size) { + // To make the inheritance logic work for unknown typename strings that + // may have fuzzy supertypes, we give this typename an empty supertype + // set and then populate it with any fuzzy supertypes that match. + supertypes = this.getSupertypeSet(typename, true)!; + // This only works for typenames that are directly matched by a fuzzy + // supertype. What if there is an intermediate chain of supertypes? + // While possible, that situation can only be solved effectively by + // specifying the intermediate relationships via possibleTypes, manually + // and in a non-fuzzy way. + this.fuzzySubtypes.forEach((regExp, fuzzy) => { + if (regExp.test(typename)) { + // The fuzzy parameter is just the original string version of regExp + // (not a valid __typename string), but we can look up the + // associated supertype(s) in this.supertypeMap. + const fuzzySupertypes = this.supertypeMap.get(fuzzy); + if (fuzzySupertypes) { + fuzzySupertypes.forEach(supertype => supertypes!.add(supertype)); + } + } + }); + } if (supertypes && supertypes.size) { supertypes.forEach(supertype => { const { fields, ...rest } = this.getTypePolicy(supertype);