From 065d3175ad7e16af6ce21f6cbe16750cc945356b Mon Sep 17 00:00:00 2001 From: Ben Newman <ben@apollographql.com> Date: Sat, 22 Aug 2020 15:30:22 -0400 Subject: [PATCH 1/7] Use getFragmentFromSelection helper in executeSelectionSet. --- src/cache/inmemory/readFromStore.ts | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts index 85824c1ef28..1abafb40f30 100644 --- a/src/cache/inmemory/readFromStore.ts +++ b/src/cache/inmemory/readFromStore.ts @@ -1,8 +1,6 @@ import { DocumentNode, FieldNode, - FragmentDefinitionNode, - InlineFragmentNode, SelectionSetNode, } from 'graphql'; import { wrap, OptimisticWrapperFunction } from 'optimism'; @@ -10,7 +8,6 @@ import { invariant, InvariantError } from 'ts-invariant'; import { isField, - isInlineFragment, resultKeyNameFromField, Reference, isReference, @@ -26,6 +23,7 @@ import { getQueryDefinition, maybeDeepFreeze, mergeDeepArray, + getFragmentFromSelection, } from '../../utilities'; import { Cache } from '../core/types/Cache'; import { @@ -201,7 +199,7 @@ export class StoreReader { }; } - const { fragmentMap, variables, policies, store } = context; + const { variables, policies, store } = context; const objectsToMerge: { [key: string]: any }[] = []; const finalResult: ExecResult = { result: null }; const typename = store.getFieldValue<string>(objectOrReference, "__typename"); @@ -313,19 +311,12 @@ export class StoreReader { invariant(context.path.pop() === resultName); } else { - let fragment: InlineFragmentNode | FragmentDefinitionNode; - - if (isInlineFragment(selection)) { - fragment = selection; - } else { - // This is a named fragment - invariant( - fragment = fragmentMap[selection.name.value], - `No fragment named ${selection.name.value}`, - ); - } + const fragment = getFragmentFromSelection( + selection, + context.fragmentMap, + ); - if (policies.fragmentMatches(fragment, typename)) { + if (fragment && policies.fragmentMatches(fragment, typename)) { fragment.selectionSet.selections.forEach(workSet.add, workSet); } } From 990a488ec0242131fd4957eadfd5f95b7c29898a Mon Sep 17 00:00:00 2001 From: Ben Newman <ben@apollographql.com> Date: Fri, 21 Aug 2020 14:33:07 -0400 Subject: [PATCH 2/7] Invert storage of subtype-supertype relationships in Policies class. It's more convenient to configure possibleTypes as a map from supertypes to arrays of subtypes, since that's how a schema introspection query reports them. However, we can perform policies.fragmentMatches checks much more efficiently if we invert that structure internally, using a map from subtypes to sets of possible supertypes. When a fragment with type condition S is tested against an object with __typename T, we now search upwards from T through its supertypes until we find S (fragment matches), or the search terminates (matching fails). We can (and did) achieve the same results by starting from S and searching downward for T, but the branching factors tend to be larger in that direction, so the search tends to take longer. --- src/cache/inmemory/policies.ts | 65 +++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index c32ede8c92a..4ddf39da6a1 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -227,7 +227,6 @@ export class Policies { private typePolicies: { [__typename: string]: { keyFn?: KeyFieldsFunction; - subtypes?: Set<string>; fields?: { [fieldName: string]: { keyFn?: KeyArgsFunction; @@ -238,6 +237,12 @@ export class Policies { }; } = Object.create(null); + // Map from subtype names to sets of supertype names. Note that this + // representation inverts the structure of possibleTypes (whose keys are + // supertypes and whose values are arrays of subtypes) because it tends + // to be much more efficient to search upwards than downwards. + private supertypeMap = new Map<string, Set<string>>(); + public readonly cache: InMemoryCache; public readonly rootIdsByTypename: Record<string, string> = Object.create(null); @@ -407,8 +412,9 @@ export class Policies { public addPossibleTypes(possibleTypes: PossibleTypesMap) { (this.usingPossibleTypes as boolean) = true; Object.keys(possibleTypes).forEach(supertype => { - const subtypeSet = this.getSubtypeSet(supertype, true); - possibleTypes[supertype].forEach(subtypeSet!.add, subtypeSet); + possibleTypes[supertype].forEach(subtype => { + this.getSupertypeSet(subtype, true)!.add(supertype); + }); }); } @@ -422,17 +428,6 @@ export class Policies { } } - private getSubtypeSet( - supertype: string, - createIfMissing: boolean, - ): Set<string> | undefined { - const policy = this.getTypePolicy(supertype, createIfMissing); - if (policy) { - return policy.subtypes || ( - createIfMissing ? policy.subtypes = new Set<string>() : void 0); - } - } - private getFieldPolicy( typename: string | undefined, fieldName: string, @@ -453,6 +448,17 @@ export class Policies { } } + private getSupertypeSet( + subtype: string, + createIfMissing: boolean, + ): Set<string> | undefined { + let supertypeSet = this.supertypeMap.get(subtype); + if (!supertypeSet && createIfMissing) { + this.supertypeMap.set(subtype, supertypeSet = new Set<string>()); + } + return supertypeSet; + } + public fragmentMatches( fragment: InlineFragmentNode | FragmentDefinitionNode, typename: string | undefined, @@ -464,23 +470,34 @@ export class Policies { if (!typename) return false; const supertype = fragment.typeCondition.name.value; + // Common case: fragment type condition and __typename are the same. if (typename === supertype) return true; if (this.usingPossibleTypes) { - const workQueue = [this.getSubtypeSet(supertype, false)]; + const typenameSupertypeSet = this.getSupertypeSet(typename, true)!; + const workQueue = [typenameSupertypeSet]; + const maybeEnqueue = (subtype: string) => { + const supertypeSet = this.getSupertypeSet(subtype, false); + if (supertypeSet && workQueue.indexOf(supertypeSet) < 0) { + workQueue.push(supertypeSet); + } + }; + // It's important to keep evaluating workQueue.length each time through // the loop, because the queue can grow while we're iterating over it. for (let i = 0; i < workQueue.length; ++i) { - const subtypes = workQueue[i]; - if (subtypes) { - if (subtypes.has(typename)) return true; - subtypes.forEach(subtype => { - const subsubtypes = this.getSubtypeSet(subtype, false); - if (subsubtypes && workQueue.indexOf(subsubtypes) < 0) { - workQueue.push(subsubtypes); - } - }); + const supertypeSet = workQueue[i]; + + if (supertypeSet.has(supertype)) { + // Record positive results for faster future lookup. + // Unfortunately, we cannot safely cache negative results, + // because new possibleTypes data could always be added to the + // Policies class. + typenameSupertypeSet.add(supertype); + return true; } + + supertypeSet.forEach(maybeEnqueue); } } From 1c54969c831fe6a437c8d8ded2b641723bc27ed8 Mon Sep 17 00:00:00 2001 From: Ben Newman <ben@apollographql.com> Date: Sat, 22 Aug 2020 14:23:58 -0400 Subject: [PATCH 3/7] Bring back heuristic fragment matching, with a twist. A full explanation of these changes can be found in PR #6901. --- src/cache/inmemory/helpers.ts | 35 +++++++++++++-- src/cache/inmemory/policies.ts | 68 ++++++++++++++++++++++++++++++ src/cache/inmemory/writeToStore.ts | 21 ++++++++- 3 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/cache/inmemory/helpers.ts b/src/cache/inmemory/helpers.ts index 9cdd09ae12c..47d9ee28e5c 100644 --- a/src/cache/inmemory/helpers.ts +++ b/src/cache/inmemory/helpers.ts @@ -1,4 +1,4 @@ -import { FieldNode } from 'graphql'; +import { FieldNode, SelectionSetNode } from 'graphql'; import { NormalizedCache } from './types'; import { @@ -9,6 +9,8 @@ import { isField, DeepMerger, ReconcilerFunction, + resultKeyNameFromField, + shouldInclude, } from '../../utilities'; export const hasOwn = Object.prototype.hasOwnProperty; @@ -22,12 +24,39 @@ export function getTypenameFromStoreObject( : objectOrReference && objectOrReference.__typename; } -const FieldNamePattern = /^[_A-Za-z0-9]+/; +export const TypeOrFieldNameRegExp = /^[_a-z][_0-9a-z]*/i; + export function fieldNameFromStoreName(storeFieldName: string): string { - const match = storeFieldName.match(FieldNamePattern); + const match = storeFieldName.match(TypeOrFieldNameRegExp); return match ? match[0] : storeFieldName; } +export function selectionSetMatchesResult( + selectionSet: SelectionSetNode, + result: Record<string, any>, + variables?: Record<string, any>, +): boolean { + if (result && typeof result === "object") { + return Array.isArray(result) + ? result.every(item => selectionSetMatchesResult(selectionSet, item, variables)) + : selectionSet.selections.every(field => { + if (isField(field) && shouldInclude(field, variables)) { + const key = resultKeyNameFromField(field); + return hasOwn.call(result, key) && + (!field.selectionSet || + selectionSetMatchesResult(field.selectionSet, result[key], variables)); + } + // If the selection has been skipped with @skip(true) or + // @include(false), it should not count against the matching. If + // the selection is not a field, it must be a fragment (inline or + // named). We will determine if selectionSetMatchesResult for that + // fragment when we get to it, so for now we return true. + return true; + }); + } + return false; +} + // Invoking merge functions needs to happen after processSelectionSet has // finished, but requires information that is more readily available // during processSelectionSet, so processSelectionSet embeds special diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index 4ddf39da6a1..133ddb5246e 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -29,6 +29,8 @@ import { FieldValueToBeMerged, isFieldValueToBeMerged, storeValueIsStoreObject, + selectionSetMatchesResult, + TypeOrFieldNameRegExp, } from './helpers'; import { cacheSlot } from './reactiveVars'; import { InMemoryCache } from './inMemoryCache'; @@ -243,6 +245,12 @@ export class Policies { // to be much more efficient to search upwards than downwards. private supertypeMap = new Map<string, Set<string>>(); + // Any fuzzy subtypes specified by possibleTypes will be converted to + // RegExp objects and recorded here. Every key of this map can also be + // found in supertypeMap. In many cases this Map will be empty, which + // means no fuzzy subtype checking will happen in fragmentMatches. + private fuzzySubtypes = new Map<string, RegExp>(); + public readonly cache: InMemoryCache; public readonly rootIdsByTypename: Record<string, string> = Object.create(null); @@ -414,6 +422,11 @@ export class Policies { Object.keys(possibleTypes).forEach(supertype => { possibleTypes[supertype].forEach(subtype => { this.getSupertypeSet(subtype, true)!.add(supertype); + const match = subtype.match(TypeOrFieldNameRegExp); + if (!match || match[0] !== subtype) { + // TODO Don't interpret just any invalid typename as a RegExp. + this.fuzzySubtypes.set(subtype, new RegExp(subtype)); + } }); }); } @@ -462,6 +475,8 @@ export class Policies { public fragmentMatches( fragment: InlineFragmentNode | FragmentDefinitionNode, typename: string | undefined, + result?: Record<string, any>, + variables?: Record<string, any>, ): boolean { if (!fragment.typeCondition) return true; @@ -483,12 +498,40 @@ export class Policies { } }; + // We need to check fuzzy subtypes only if we encountered fuzzy + // subtype strings in addPossibleTypes, and only while writing to + // the cache, since that's when selectionSetMatchesResult gives a + // strong signal of fragment matching. The StoreReader class calls + // policies.fragmentMatches without passing a result object, so + // needToCheckFuzzySubtypes is always false while reading. + let needToCheckFuzzySubtypes = !!(result && this.fuzzySubtypes.size); + + // We don't need to check selectionSetMatchesResult for known + // supertype/subtype relationships specified in possibleTypes, but + // we will need to enforce that verification once we begin checking + // fuzzy subtypes. + let mustVerifyResult = false; + // It's important to keep evaluating workQueue.length each time through // the loop, because the queue can grow while we're iterating over it. for (let i = 0; i < workQueue.length; ++i) { const supertypeSet = workQueue[i]; if (supertypeSet.has(supertype)) { + if (result && mustVerifyResult) { + // Since this verification doesn't depend on any for-loop + // variables, we could technically hoist it outside the loop, + // but the verification can be expensive, so we postpone it + // until the last possible moment (here). + if (selectionSetMatchesResult(fragment.selectionSet, result, variables)) { + invariant.warn(`Inferring subtype ${typename} of supertype ${supertype}`); + } else { + // Since the result of the verification isn't going to + // change the next time we do it, we can go ahead and + // terminate the search at this point. + return false; + } + } // Record positive results for faster future lookup. // Unfortunately, we cannot safely cache negative results, // because new possibleTypes data could always be added to the @@ -498,6 +541,31 @@ export class Policies { } supertypeSet.forEach(maybeEnqueue); + + // We start checking fuzzy subtypes only after we've exhausted all + // non-fuzzy subtypes. + const isFinalIteration = i === workQueue.length - 1; + if (isFinalIteration && needToCheckFuzzySubtypes) { + // Check fuzzy subtypes at most once (that is, don't run the + // this.fuzzySubtypes.forEach loop more than once). + needToCheckFuzzySubtypes = false; + + // Now that we're checking fuzzy subtypes, enforce heuristic + // fragment/result matching before returning true if/when we + // find a matching supertype. + mustVerifyResult = true; + + // If we find any fuzzy subtypes that match typename, extend the + // workQueue to search through the supertypes of those fuzzy + // subtypes. Otherwise the for-loop will terminate and we'll + // return false below. + this.fuzzySubtypes.forEach((regExp, fuzzyString) => { + const match = typename.match(regExp); + if (match && match[0] === typename) { + maybeEnqueue(fuzzyString); + } + }); + } } } diff --git a/src/cache/inmemory/writeToStore.ts b/src/cache/inmemory/writeToStore.ts index 25feb11be28..1d05ba8e3f2 100644 --- a/src/cache/inmemory/writeToStore.ts +++ b/src/cache/inmemory/writeToStore.ts @@ -245,7 +245,26 @@ export class StoreWriter { context.fragmentMap, ); - if (fragment && policies.fragmentMatches(fragment, typename)) { + if (fragment && + // By passing result and context.variables, we enable + // policies.fragmentMatches to bend the rules when typename is + // not a known subtype of the fragment type condition, but the + // result object contains all the keys requested by the + // fragment, which strongly suggests the fragment probably + // matched. This fuzzy matching behavior must be enabled by + // including a regular expression string (such as ".*" or + // "Prefix.*" or ".*Suffix") in the possibleTypes array for + // specific supertypes; otherwise, all matching remains exact. + // Fuzzy matches are remembered by the Policies object and + // later used when reading from the cache. Since there is no + // incoming result object to check when reading, reading does + // not involve the same fuzzy inference, so the StoreReader + // class calls policies.fragmentMatches without passing result + // or context.variables. The flexibility of fuzzy matching + // allows existing clients to accommodate previously unknown + // __typename strings produced by server/schema changes, which + // would otherwise be breaking changes. + policies.fragmentMatches(fragment, typename, result, context.variables)) { fragment.selectionSet.selections.forEach(workSet.add, workSet); } } From 92a04db4e7d06c05ecfe326c4443386a1a1bf1dd Mon Sep 17 00:00:00 2001 From: Ben Newman <ben@apollographql.com> Date: Sat, 22 Aug 2020 14:53:25 -0400 Subject: [PATCH 4/7] Return early when inexact fragment type condition is unknown. --- src/cache/inmemory/policies.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index 133ddb5246e..f2018e53e4f 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -420,6 +420,11 @@ export class Policies { public addPossibleTypes(possibleTypes: PossibleTypesMap) { (this.usingPossibleTypes as boolean) = true; Object.keys(possibleTypes).forEach(supertype => { + // Make sure all types have an entry in this.supertypeMap, even if + // their supertype set is empty, so we can return false immediately + // from policies.fragmentMatches for unknown supertypes. + this.getSupertypeSet(supertype, true); + possibleTypes[supertype].forEach(subtype => { this.getSupertypeSet(subtype, true)!.add(supertype); const match = subtype.match(TypeOrFieldNameRegExp); @@ -488,12 +493,15 @@ export class Policies { // Common case: fragment type condition and __typename are the same. if (typename === supertype) return true; - if (this.usingPossibleTypes) { + if (this.usingPossibleTypes && + this.supertypeMap.has(supertype)) { const typenameSupertypeSet = this.getSupertypeSet(typename, true)!; const workQueue = [typenameSupertypeSet]; const maybeEnqueue = (subtype: string) => { const supertypeSet = this.getSupertypeSet(subtype, false); - if (supertypeSet && workQueue.indexOf(supertypeSet) < 0) { + if (supertypeSet && + supertypeSet.size && + workQueue.indexOf(supertypeSet) < 0) { workQueue.push(supertypeSet); } }; From fcbc86f45ccbf6f3dedd6d31beb5e682f4e896d7 Mon Sep 17 00:00:00 2001 From: Ben Newman <ben@apollographql.com> Date: Wed, 26 Aug 2020 11:45:01 -0400 Subject: [PATCH 5/7] Check selectionSetMatchesResult before checking fuzzy subtypes. --- src/cache/inmemory/policies.ts | 54 +++++++++++++--------------------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index f2018e53e4f..d36974b0845 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -513,12 +513,7 @@ export class Policies { // policies.fragmentMatches without passing a result object, so // needToCheckFuzzySubtypes is always false while reading. let needToCheckFuzzySubtypes = !!(result && this.fuzzySubtypes.size); - - // We don't need to check selectionSetMatchesResult for known - // supertype/subtype relationships specified in possibleTypes, but - // we will need to enforce that verification once we begin checking - // fuzzy subtypes. - let mustVerifyResult = false; + let checkingFuzzySubtypes = false; // It's important to keep evaluating workQueue.length each time through // the loop, because the queue can grow while we're iterating over it. @@ -526,42 +521,35 @@ export class Policies { const supertypeSet = workQueue[i]; if (supertypeSet.has(supertype)) { - if (result && mustVerifyResult) { - // Since this verification doesn't depend on any for-loop - // variables, we could technically hoist it outside the loop, - // but the verification can be expensive, so we postpone it - // until the last possible moment (here). - if (selectionSetMatchesResult(fragment.selectionSet, result, variables)) { + if (!typenameSupertypeSet.has(supertype)) { + if (checkingFuzzySubtypes) { invariant.warn(`Inferring subtype ${typename} of supertype ${supertype}`); - } else { - // Since the result of the verification isn't going to - // change the next time we do it, we can go ahead and - // terminate the search at this point. - return false; } + // Record positive results for faster future lookup. + // Unfortunately, we cannot safely cache negative results, + // because new possibleTypes data could always be added to the + // Policies class. + typenameSupertypeSet.add(supertype); } - // Record positive results for faster future lookup. - // Unfortunately, we cannot safely cache negative results, - // because new possibleTypes data could always be added to the - // Policies class. - typenameSupertypeSet.add(supertype); return true; } supertypeSet.forEach(maybeEnqueue); - // We start checking fuzzy subtypes only after we've exhausted all - // non-fuzzy subtypes. - const isFinalIteration = i === workQueue.length - 1; - if (isFinalIteration && needToCheckFuzzySubtypes) { - // Check fuzzy subtypes at most once (that is, don't run the - // this.fuzzySubtypes.forEach loop more than once). + if (needToCheckFuzzySubtypes && + // Start checking fuzzy subtypes only after exhausting all + // non-fuzzy subtypes (after the final iteration of the loop). + i === workQueue.length - 1 && + // We could wait to compare fragment.selectionSet to result + // after we verify the supertype, but this check is often less + // expensive than that search, and we will have to do the + // comparison anyway whenever we find a potential match. + selectionSetMatchesResult(fragment.selectionSet, result!, variables)) { + // We don't always need to check fuzzy subtypes (if no result + // was provided, or !this.fuzzySubtypes.size), but, when we do, + // we only want to check them once. needToCheckFuzzySubtypes = false; - - // Now that we're checking fuzzy subtypes, enforce heuristic - // fragment/result matching before returning true if/when we - // find a matching supertype. - mustVerifyResult = true; + checkingFuzzySubtypes = true; // If we find any fuzzy subtypes that match typename, extend the // workQueue to search through the supertypes of those fuzzy From ae4a6f5148448dad549d62b234c9a24c4d97b010 Mon Sep 17 00:00:00 2001 From: Ben Newman <ben@apollographql.com> Date: Thu, 10 Sep 2020 11:33:04 -0400 Subject: [PATCH 6/7] Add more tests of possibleTypes and fragmentMatches. --- .../__snapshots__/fragmentMatcher.ts.snap | 29 ++ .../inmemory/__tests__/fragmentMatcher.ts | 338 ++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 src/cache/inmemory/__tests__/__snapshots__/fragmentMatcher.ts.snap diff --git a/src/cache/inmemory/__tests__/__snapshots__/fragmentMatcher.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/fragmentMatcher.ts.snap new file mode 100644 index 00000000000..0b0c04b9abf --- /dev/null +++ b/src/cache/inmemory/__tests__/__snapshots__/fragmentMatcher.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`policies.fragmentMatches can infer fuzzy subtypes heuristically 1`] = ` +Object { + "ROOT_QUERY": Object { + "__typename": "Query", + "objects": Array [ + Object { + "__typename": "E", + "c": "ce", + }, + Object { + "__typename": "F", + "c": "cf", + }, + Object { + "__typename": "G", + "c": "cg", + }, + Object { + "__typename": "TooLong", + }, + Object { + "__typename": "H", + }, + ], + }, +} +`; diff --git a/src/cache/inmemory/__tests__/fragmentMatcher.ts b/src/cache/inmemory/__tests__/fragmentMatcher.ts index 279a42ea902..d4062f64d97 100644 --- a/src/cache/inmemory/__tests__/fragmentMatcher.ts +++ b/src/cache/inmemory/__tests__/fragmentMatcher.ts @@ -1,6 +1,8 @@ import gql from 'graphql-tag'; import { InMemoryCache } from '../inMemoryCache'; +import { visit, FragmentDefinitionNode } from 'graphql'; +import { hasOwn } from '../helpers'; describe('fragment matching', () => { it('can match exact types with or without possibleTypes', () => { @@ -222,4 +224,340 @@ describe('fragment matching', () => { cache.writeQuery({ query, data }); expect(cache.readQuery({ query })).toEqual(data); }); + +}); + +describe("policies.fragmentMatches", () => { + const warnings: any[] = []; + const { warn } = console; + + beforeEach(() => { + warnings.length = 0; + console.warn = function (message: any) { + warnings.push(message); + }; + }); + + afterEach(() => { + console.warn = warn; + }); + + it("can infer fuzzy subtypes heuristically", () => { + const cache = new InMemoryCache({ + possibleTypes: { + A: ["B", "C"], + B: ["D"], + C: ["[E-Z]"], + }, + }); + + const fragments = gql` + fragment FragA on A { a } + fragment FragB on B { b } + fragment FragC on C { c } + fragment FragD on D { d } + fragment FragE on E { e } + fragment FragF on F { f } + `; + + function checkTypes( + expected: Record<string, Record<string, boolean>>, + ) { + const checked = new Set<FragmentDefinitionNode>(); + + visit(fragments, { + FragmentDefinition(frag) { + function check(typename: string, result: boolean) { + if (result !== cache.policies.fragmentMatches(frag, typename)) { + fail(`fragment ${ + frag.name.value + } should${result ? "" : " not"} have matched typename ${typename}`); + } + } + + const supertype = frag.typeCondition.name.value; + expect("ABCDEF".split("")).toContain(supertype); + + if (hasOwn.call(expected, supertype)) { + Object.keys(expected[supertype]).forEach(subtype => { + check(subtype, expected[supertype][subtype]); + }); + + checked.add(frag); + } + }, + }); + + return checked; + } + + expect(checkTypes({ + A: { + A: true, + B: true, + C: true, + D: true, + E: false, + F: false, + G: false, + }, + B: { + A: false, + B: true, + C: false, + D: true, + E: false, + F: false, + G: false, + }, + C: { + A: false, + B: false, + C: true, + D: false, + E: false, + F: false, + G: false, + }, + D: { + A: false, + B: false, + C: false, + D: true, + E: false, + F: false, + G: false, + }, + E: { + A: false, + B: false, + C: false, + D: false, + E: true, + F: false, + G: false, + }, + F: { + A: false, + B: false, + C: false, + D: false, + E: false, + F: true, + G: false, + }, + G: { + A: false, + B: false, + C: false, + D: false, + E: false, + F: false, + G: true, + }, + }).size).toBe("ABCDEF".length); + + cache.writeQuery({ + query: gql` + query { + objects { + ...FragC + } + } + ${fragments} + `, + data: { + objects: [ + { __typename: "E", c: "ce" }, + { __typename: "F", c: "cf" }, + { __typename: "G", c: "cg" }, + // The /[E-Z]/ subtype pattern specified for the C supertype + // must match the entire subtype string. + { __typename: "TooLong", c: "nope" }, + // The H typename matches the regular expression for C, but it + // does not pass the heuristic test of having all the fields + // expected if FragC matched. + { __typename: "H", h: "not c" }, + ], + }, + }); + + expect(warnings).toEqual([ + "Inferring subtype E of supertype C", + "Inferring subtype F of supertype C", + "Inferring subtype G of supertype C", + // Note that TooLong is not inferred here. + ]); + + expect(checkTypes({ + A: { + A: true, + B: true, + C: true, + D: true, + E: true, + F: true, + G: true, + H: false, + }, + B: { + A: false, + B: true, + C: false, + D: true, + E: false, + F: false, + G: false, + H: false, + }, + C: { + A: false, + B: false, + C: true, + D: false, + E: true, + F: true, + G: true, + H: false, + }, + D: { + A: false, + B: false, + C: false, + D: true, + E: false, + F: false, + G: false, + H: false, + }, + E: { + A: false, + B: false, + C: false, + D: false, + E: true, + F: false, + G: false, + H: false, + }, + F: { + A: false, + B: false, + C: false, + D: false, + E: false, + F: true, + G: false, + H: false, + }, + G: { + A: false, + B: false, + C: false, + D: false, + E: false, + F: true, + G: true, + H: false, + }, + }).size).toBe("ABCDEF".length); + + expect(cache.extract()).toMatchSnapshot(); + + // Now add the TooLong subtype of C explicitly. + cache.policies.addPossibleTypes({ + C: ["TooLong"], + }); + + expect(checkTypes({ + A: { + A: true, + B: true, + C: true, + D: true, + E: true, + F: true, + G: true, + TooLong: true, + H: false, + }, + B: { + A: false, + B: true, + C: false, + D: true, + E: false, + F: false, + G: false, + TooLong: false, + H: false, + }, + C: { + A: false, + B: false, + C: true, + D: false, + E: true, + F: true, + G: true, + TooLong: true, + H: false, + }, + D: { + A: false, + B: false, + C: false, + D: true, + E: false, + F: false, + G: false, + TooLong: false, + H: false, + }, + E: { + A: false, + B: false, + C: false, + D: false, + E: true, + F: false, + G: false, + TooLong: false, + H: false, + }, + F: { + A: false, + B: false, + C: false, + D: false, + E: false, + F: true, + G: false, + TooLong: false, + H: false, + }, + G: { + A: false, + B: false, + C: false, + D: false, + E: false, + F: true, + G: true, + TooLong: false, + H: false, + }, + H: { + A: false, + B: false, + C: false, + D: false, + E: false, + F: false, + G: false, + TooLong: false, + H: true, + }, + }).size).toBe("ABCDEF".length); + }); }); From 02b0ac84e9c7b81cfcc37b82d625ebf6699984a1 Mon Sep 17 00:00:00 2001 From: Ben Newman <ben@apollographql.com> Date: Thu, 10 Sep 2020 14:06:24 -0400 Subject: [PATCH 7/7] Mention PR #6901 in CHANGELOG.md. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd3aff8bee..28e7a425e1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ - Substantially improve type inference for data and variables types using `TypedDocumentNode<Data, Variables>` type instead of `DocumentNode`. <br/> [@dotansimha](https://github.com/dotansimha) in [#6720](https://github.com/apollographql/apollo-client/pull/6720) +- Bring back an improved form of heuristic fragment matching, by allowing `possibleTypes` to specify subtype regular expression strings, which count as matches if the written result object has all the fields expected for the fragment. <br/> + [@benjamn](https://github.com/benjamn) in [#6901](https://github.com/apollographql/apollo-client/pull/6901) + - Allow `options.nextFetchPolicy` to be a function that takes the current `FetchPolicy` and returns a new (or the same) `FetchPolicy`, making `nextFetchPolicy` more suitable for global use in `defaultOptions.watchQuery`. <br/> [@benjamn](https://github.com/benjamn) in [#6893](https://github.com/apollographql/apollo-client/pull/6893)