Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix type policy inheritance involving fuzzy possibleTypes #10633

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"],
},
},
});
Comment on lines +631 to +653
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this may still look like a lot of boilerplate, the key point is that we don't need any mention of subtypes like CoworkerEntity or ManagerEntity in this configuration code.


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" },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm honored to make it into a test 😆

{ __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" },
],
},
});
jerelmiller marked this conversation as resolved.
Show resolved Hide resolved

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