Skip to content

Commit

Permalink
Fix type policy inheritance involving fuzzy possibleTypes (#10633)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamn authored Mar 9, 2023
1 parent 7df51ee commit 90a06ee
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/odd-students-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@apollo/client': patch
---

Fix type policy inheritance involving fuzzy `possibleTypes`
2 changes: 1 addition & 1 deletion config/bundlesize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
176 changes: 176 additions & 0 deletions src/cache/inmemory/__tests__/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Coworker, "uid"> {
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({
Expand Down
32 changes: 27 additions & 5 deletions src/cache/inmemory/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 90a06ee

Please sign in to comment.