Skip to content

Commit ecf3de1

Browse files
prevent merging into supertype field policies (#12750)
Co-authored-by: Jerel Miller <[email protected]> Co-authored-by: jerelmiller <[email protected]>
1 parent 0bcd2f4 commit ecf3de1

File tree

4 files changed

+119
-29
lines changed

4 files changed

+119
-29
lines changed

.changeset/great-suns-cover.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Prevent field policies from overwriting/merging into supertype field policies.

.size-limits.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"dist/apollo-client.min.cjs": 42661,
3-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 34714
2+
"dist/apollo-client.min.cjs": 42669,
3+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 34723
44
}

src/cache/inmemory/__tests__/policies.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,81 @@ describe("type policies", function () {
661661
).toBe('DeathAdder:{"tagId":"LethalAbacus666"}');
662662
});
663663

664+
it("specific field policies will not override supertype policies", () => {
665+
const cache = new InMemoryCache({
666+
typePolicies: {
667+
Named: {
668+
fields: Object.freeze({
669+
name: Object.freeze({
670+
read(existing: any) {
671+
return existing + " (Named)";
672+
},
673+
}),
674+
}),
675+
},
676+
Person: {
677+
fields: {
678+
name: {
679+
read(existing) {
680+
return existing + " (Person)";
681+
},
682+
},
683+
},
684+
},
685+
},
686+
possibleTypes: {
687+
Named: ["Person", "Pet"],
688+
},
689+
});
690+
const query = gql`
691+
{
692+
me {
693+
__typename
694+
id
695+
name
696+
pets {
697+
__typename
698+
id
699+
name
700+
}
701+
}
702+
}
703+
`;
704+
cache.writeQuery({
705+
query,
706+
data: {
707+
me: {
708+
__typename: "Person",
709+
id: 23,
710+
name: "John Doe",
711+
pets: [
712+
{
713+
__typename: "Pet",
714+
id: 1,
715+
name: "Fido",
716+
},
717+
{
718+
__typename: "Pet",
719+
id: 2,
720+
name: "Whiskers",
721+
},
722+
],
723+
},
724+
},
725+
});
726+
expect(cache.readQuery({ query })).toEqual({
727+
me: {
728+
__typename: "Person",
729+
id: 23,
730+
name: "John Doe (Person)",
731+
pets: [
732+
{ __typename: "Pet", id: 1, name: "Fido (Named)" },
733+
{ __typename: "Pet", id: 2, name: "Whiskers (Named)" },
734+
],
735+
},
736+
});
737+
});
738+
664739
it("typePolicies can be inherited from supertypes with fuzzy possibleTypes", () => {
665740
const cache = new InMemoryCache({
666741
possibleTypes: {

src/cache/inmemory/policies.ts

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -281,17 +281,20 @@ export type PossibleTypesMap = {
281281
[supertype: string]: string[];
282282
};
283283

284+
type InternalFieldPolicy = {
285+
typename: string;
286+
keyFn?: KeyArgsFunction;
287+
read?: FieldReadFunction<any>;
288+
merge?: FieldMergeFunction<any>;
289+
};
290+
284291
export class Policies {
285292
private typePolicies: {
286293
[__typename: string]: {
287294
keyFn?: KeyFieldsFunction;
288295
merge?: FieldMergeFunction<any>;
289296
fields: {
290-
[fieldName: string]: {
291-
keyFn?: KeyArgsFunction;
292-
read?: FieldReadFunction<any>;
293-
merge?: FieldMergeFunction<any>;
294-
};
297+
[fieldName: string]: InternalFieldPolicy;
295298
};
296299
};
297300
} = Object.create(null);
@@ -440,7 +443,11 @@ export class Policies {
440443
});
441444
}
442445

443-
private updateTypePolicy(typename: string, incoming: TypePolicy) {
446+
private updateTypePolicy(
447+
typename: string,
448+
incoming: TypePolicy,
449+
existingFieldPolicies: Record<string, InternalFieldPolicy>
450+
) {
444451
const existing = this.getTypePolicy(typename);
445452
const { keyFields, fields } = incoming;
446453

@@ -476,7 +483,17 @@ export class Policies {
476483

477484
if (fields) {
478485
Object.keys(fields).forEach((fieldName) => {
479-
const existing = this.getFieldPolicy(typename, fieldName, true)!;
486+
let existing = existingFieldPolicies[fieldName] as
487+
| InternalFieldPolicy
488+
| undefined;
489+
// Field policy inheritance is atomic/shallow: you can't inherit a
490+
// field policy and then override just its read function, since read
491+
// and merge functions often need to cooperate, so changing only one
492+
// of them would be a recipe for inconsistency.
493+
// So here we avoid merging an inherited field policy with an updated one.
494+
if (!existing || existing?.typename !== typename) {
495+
existing = existingFieldPolicies[fieldName] = { typename };
496+
}
480497
const incoming = fields[fieldName];
481498

482499
if (typeof incoming === "function") {
@@ -623,7 +640,11 @@ export class Policies {
623640
// Merge the pending policies into this.typePolicies, in the order they
624641
// were originally passed to addTypePolicy.
625642
inbox.splice(0).forEach((policy) => {
626-
this.updateTypePolicy(typename, policy);
643+
this.updateTypePolicy(
644+
typename,
645+
policy,
646+
this.typePolicies[typename].fields
647+
);
627648
});
628649
}
629650

@@ -632,21 +653,10 @@ export class Policies {
632653

633654
private getFieldPolicy(
634655
typename: string | undefined,
635-
fieldName: string,
636-
createIfMissing: boolean
637-
):
638-
| {
639-
keyFn?: KeyArgsFunction;
640-
read?: FieldReadFunction<any>;
641-
merge?: FieldMergeFunction<any>;
642-
}
643-
| undefined {
656+
fieldName: string
657+
): InternalFieldPolicy | undefined {
644658
if (typename) {
645-
const fieldPolicies = this.getTypePolicy(typename).fields;
646-
return (
647-
fieldPolicies[fieldName] ||
648-
(createIfMissing && (fieldPolicies[fieldName] = Object.create(null)))
649-
);
659+
return this.getTypePolicy(typename).fields[fieldName];
650660
}
651661
}
652662

@@ -760,13 +770,13 @@ export class Policies {
760770
}
761771

762772
public hasKeyArgs(typename: string | undefined, fieldName: string) {
763-
const policy = this.getFieldPolicy(typename, fieldName, false);
773+
const policy = this.getFieldPolicy(typename, fieldName);
764774
return !!(policy && policy.keyFn);
765775
}
766776

767777
public getStoreFieldName(fieldSpec: FieldSpecifier): string {
768778
const { typename, fieldName } = fieldSpec;
769-
const policy = this.getFieldPolicy(typename, fieldName, false);
779+
const policy = this.getFieldPolicy(typename, fieldName);
770780
let storeFieldName: Exclude<ReturnType<KeyArgsFunction>, KeySpecifier>;
771781

772782
let keyFn = policy && policy.keyFn;
@@ -835,7 +845,7 @@ export class Policies {
835845
objectOrReference,
836846
storeFieldName
837847
);
838-
const policy = this.getFieldPolicy(options.typename, fieldName, false);
848+
const policy = this.getFieldPolicy(options.typename, fieldName);
839849
const read = policy && policy.read;
840850

841851
if (read) {
@@ -866,7 +876,7 @@ export class Policies {
866876
typename: string | undefined,
867877
fieldName: string
868878
): FieldReadFunction | undefined {
869-
const policy = this.getFieldPolicy(typename, fieldName, false);
879+
const policy = this.getFieldPolicy(typename, fieldName);
870880
return policy && policy.read;
871881
}
872882

@@ -878,7 +888,7 @@ export class Policies {
878888
let policy:
879889
| Policies["typePolicies"][string]
880890
| Policies["typePolicies"][string]["fields"][string]
881-
| undefined = this.getFieldPolicy(parentTypename, fieldName, false);
891+
| undefined = this.getFieldPolicy(parentTypename, fieldName);
882892
let merge = policy && policy.merge;
883893
if (!merge && childTypename) {
884894
policy = this.getTypePolicy(childTypename);

0 commit comments

Comments
 (0)