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

Fragment matching broken when adding a new type that implements an existing interface #5750

Closed
rylanc opened this issue Jan 3, 2020 · 8 comments

Comments

@rylanc
Copy link

rylanc commented Jan 3, 2020

Context:
Imagine a scenario where the client and server code are deployed separately.

Here's the server schema:

  type Query {
    allTheStuff: [Node]
  }

  interface Node {
    id: ID!
  }

  type Rocket implements Node {
    id: ID!
    name: String
  }

  type User implements Node {
    id: ID!
    email: String!
  }

We have a client query that looks like this:

  query {
    allTheStuff {
      ...TestFragment
    }
  }

  fragment TestFragment on Node {
    id
  }

The client has been bundled with the latest fragment types, which include Rocket and User as the possible types for the Node interface.

Now, we add a third implementing type:

  type Launch implements Node {
    id: ID!
    site: String
  }

Since our client and server are deployed separately, the client's fragment types are stale.

Intended outcome:
Existing and deployed client code has access to all the fields defined in TestFragment for all objects returned by the Query.allTheStuff field.
More generally: when fragment types are stale, we'd expect to see more cache-misses but not missing data.

Actual outcome:
Existing clients do not have access to the id fields for any objects of the Launch type.
Note that this problem does not exist when using the no-cache policy, which makes the experience of using Apollo Client Cache inferior to that of not using the cache.
More generally: When fragment types are stale, using the cache provides a broken experience with missing data.

How to reproduce the issue:
I created a fork of the fullstack tutorial that illustrates this issue. It uses apollo-client v3 (I tested and confirmed the issue in v2, as well) and configures the possibleTypes for Node to only include Rocket and User. The "launches" page query does not receive any data for the Launch object returned from Query.allTheStuff.

Versions

  System:
    OS: macOS Mojave 10.14.6
  Binaries:
    Node: 12.13.0 - ~/.nvm/versions/node/v12.13.0/bin/node
    Yarn: 1.17.3 - ~/workspace/oss/fullstack-tutorial/final/client/node_modules/.bin/yarn
    npm: 6.12.0 - ~/.nvm/versions/node/v12.13.0/bin/npm
  Browsers:
    Chrome: 79.0.3945.88
    Safari: 13.0.4
  npmPackages:
    @apollo/client: ^3.0.0-beta.19 => 3.0.0-beta.19
    @apollo/react-hooks: 3.0.0 => 3.0.0
    @apollo/react-testing: 3.0.0 => 3.0.0
    apollo: ^2.16.3 => 2.17.1
    apollo-cache-inmemory: ^1.6.2 => 1.6.2
    apollo-client: ^2.6.3 => 2.6.3
    apollo-link-http: ^1.5.15 => 1.5.15
  npmGlobalPackages:
    apollo: 2.21.0

My company is moving towards using Apollo Federation with Graph Manager as the source of truth for our schema. We've been moving our schema-dependent tools and processes over to use the schema stored in Graph Manager. We're now looking at fetching the schema for the IntrospectionFragmentMatcher. We've setup a script to download the schema from GM and generating the fragment types JSON from that. Since pieces of our schema are now being deployed separately and at different times, it will be difficult for us to keep the fragment types from being stale and potentially causing client bugs.

@hwillson hwillson added this to the Release 3.0 milestone Jan 14, 2020
@hwillson hwillson self-assigned this Jan 15, 2020
@hwillson hwillson removed this from the Release 3.0 milestone Jan 15, 2020
@hwillson hwillson removed their assignment Jan 15, 2020
@benjamn benjamn self-assigned this Jan 17, 2020
@benjamn
Copy link
Member

benjamn commented Jan 17, 2020

Since this involves obtaining new information about possibleTypes from the GraphQL server at runtime, in production, and schema introspection is typically disabled in production, I think this needs dedicated server support. I'm confident we can get this working flawlessly if you're using Apollo Server, but otherwise you might need to adapt our solution to whatever server you're using.

Specifically, the server could send back any relevant extensions: { possibleTypes: { [supertype: string]: string[] }} information with every query response. That might sound like a lot of extra information, but it would be limited to just the relevant supertype-subtype relationships for the current query, given the set of all concrete __typenames in the response, and all supertypes that appear as fragment type conditions. For most queries, this would be an extremely limited set of relationships. For queries containing no fragments with type conditions, nothing would need to be sent back. This extensions.possibleTypes metadata would be safe to send to any client, even if the client isn't prepared to process it, since it can be safely ignored.

I think it still makes sense to have a way of specifying possibleTypes on the client, in case you're using a server that doesn't have this extension, but a cooperating client and server could exchange all the possibleTypes information they need in this way, in principle.

@rylanc
Copy link
Author

rylanc commented Jan 17, 2020

Wow, that sounds pretty ideal. We are using Apollo Server (for our gateway), so that solution sounds like it would work. Should I file an issue in the apollo-server repo?

@noaelad
Copy link

noaelad commented Jan 17, 2020

Second that, that sounds fantastic

@noaelad
Copy link

noaelad commented Jan 17, 2020

Would this be the equivalent of the extension based strategy of ProgressiveFragmentMatcher?
https://github.com/lucasconstantino/apollo-progressive-fragment-matcher

benjamn added a commit that referenced this issue Jan 25, 2020
@nodkz
Copy link

nodkz commented Mar 5, 2020

@benjamn it may be a stupid question – but maybe we can get rid of possibleTypes?

How I understand all these things created for readFromStore.ts & writeToStore.ts:

if (policies.fragmentMatches(fragment, typename)) {
fragment.selectionSet.selections.forEach(workSet.add, workSet);
}

if (policies.fragmentMatches(fragment, typename)) {
fragment.selectionSet.selections.forEach(workSet.add, workSet);
}

But what I really cannot understand – for what purpose exists this if (policies.fragmentMatches(fragment, typename)) check? Maybe we can read/write any fragment data to cache without any fragment check?

Anyway, the cache does not know anything about types and fragment types is some kind of duck-checking.

I will be glad if you point me for test-case or issue where I can understand the real purpose of this if-check. Tnx.

@nodkz
Copy link

nodkz commented Mar 5, 2020

No if (policies.fragmentMatches(fragment, typename)) in the code – no problem with this issue. And all stale clients (when graphql schema was extended by additional types) continue to work without breaks.

@noaelad
Copy link

noaelad commented Jun 19, 2020

@benjamn any update on this issue?

benjamn added a commit that referenced this issue Aug 21, 2020
Easily the biggest breaking change for fragment matching in Apollo Client
3.0 was the removal of the FragmentMatcher abstraction, along with its
subclasses HeuristicFragmentMatcher and IntrospectionFragmentmatcher,
which were replaced by the declarative possibleTypes configuration.

The HeuristicFragmentMatcher was so named because it attempted to perform
fragment matching without any actual knowledge of supertype/subtype
relationships in your schema, instead checking whether all the fields of a
given fragment were present in the result object, which is indeed a
relatively strong signal that the fragment probably matched.

The HeuristicFragmentMatcher could be fooled by aliases and accidental
sharing of field names between different fragments, but it was also
relatively resilient to the possibility of adding new subtypes on the
server, because heuristic matching doesn't care what the true subtypes of
a supertype are, so what's one more?

The other big drawback of the HeuristicFragmentMatcher was that it applied
the same fuzzy logic to reading from the cache, where the heuristic makes
a lot less sense. If an individual query result has all the keys you'd
expect if a certain fragment matched, that's a pretty good sign that the
fragment matched. But when you have a lot of data in your cache, from lots
of different queries, it's not as meaningful to observe that all the
fields required by the fragment are present, since they might be there
because they've been written into the cache by other queries over time,
not because the fragment actually matches the __typename of the object.

In moving to the more exact possibleTypes system, we gave up both the
benefits and the drawbacks of the HeuristicFragmentMatcher, replacing it
with something functionally similar to the IntrospectionFragmentmatcher,
but with a much simpler configuration API.

As #5750 demonstrates, the exactness of the possibleTypes system makes it
challenging for existing clients to adapt when a new subtype is added on
the server. Is there any way to make this system more flexible, like the
HeuristicFragmentMatcher, but without the drawbacks?

This PR improves the possibleTypes API by allowing "fuzzy" subtype
strings, which are interpreted as regular expressions for typenames,
rather than as actual typename strings. If a fuzzy subtype matches the
__typename of an object, fragments on supertypes of that fuzzy subtype are
allowed to match the object, provided the result also has all the keys
required by the fragment.

The key advantages of this new system compared to the
HeuristicFragmentMatcher are that (1) you can specify fuzzy subtypes for
specific supertypes (rather than applying heuristic matching for all types
by default), and (2) that it "learns" about fuzzy subtypes while writing
(where the heuristic tends to work very well), and then merely uses those
inferred supertype/subtype relationships when reading, so there is no need
to perform heuristic matching while reading. This is the "twist" mentioned
in the title of this PR. When one of these inferences happens, you'll see
a warning in the console (in development).

You do have to opt into fuzzy matching, but it can be a useful strategy
for preparing your clients for upcoming server changes, or for relaxing
the rules for a particular supertype, in cases when you know that its
subtypes change frequently on the server.
benjamn added a commit that referenced this issue Aug 22, 2020
Easily the biggest breaking change for fragment matching in Apollo Client
3.0 was the removal of the FragmentMatcher abstraction, along with its
subclasses HeuristicFragmentMatcher and IntrospectionFragmentmatcher,
which were replaced by the declarative possibleTypes configuration.

The HeuristicFragmentMatcher was so named because it attempted to perform
fragment matching without any actual knowledge of supertype/subtype
relationships in your schema, instead checking whether all the fields of a
given fragment were present in the result object, which is indeed a
relatively strong signal that the fragment probably matched.

The HeuristicFragmentMatcher could be fooled by aliases and accidental
sharing of field names between different fragments, but it was also
relatively resilient to the possibility of adding new subtypes on the
server, because heuristic matching doesn't care what the true subtypes of
a supertype are, so what's one more?

The other big drawback of the HeuristicFragmentMatcher was that it applied
the same fuzzy logic to reading from the cache, where the heuristic makes
a lot less sense. If an individual query result has all the keys you'd
expect if a certain fragment matched, that's a pretty good sign that the
fragment matched. But when you have a lot of data in your cache, from lots
of different queries, it's not as meaningful to observe that all the
fields required by the fragment are present, since they might be there
because they've been written into the cache by other queries over time,
not because the fragment actually matches the __typename of the object.

In moving to the more exact possibleTypes system, we gave up both the
benefits and the drawbacks of the HeuristicFragmentMatcher, replacing it
with something functionally similar to the IntrospectionFragmentmatcher,
but with a much simpler configuration API.

As #5750 demonstrates, the exactness of the possibleTypes system makes it
challenging for existing clients to adapt when a new subtype is added on
the server. Is there any way to make this system more flexible, like the
HeuristicFragmentMatcher, but without the drawbacks?

This PR improves the possibleTypes API by allowing "fuzzy" subtype
strings, which are interpreted as regular expressions for typenames,
rather than as actual typename strings. If a fuzzy subtype matches the
__typename of an object, fragments on supertypes of that fuzzy subtype are
allowed to match the object, provided the result also has all the keys
required by the fragment.

The key advantages of this new system compared to the
HeuristicFragmentMatcher are that (1) you can specify fuzzy subtypes for
specific supertypes (rather than applying heuristic matching for all types
by default), and (2) that it "learns" about fuzzy subtypes while writing
(where the heuristic tends to work very well), and then merely uses those
inferred supertype/subtype relationships when reading, so there is no need
to perform heuristic matching while reading. This is the "twist" mentioned
in the title of this PR. When one of these inferences happens, you'll see
a warning in the console (in development).

You do have to opt into fuzzy matching, but it can be a useful strategy
for preparing your clients for upcoming server changes, or for relaxing
the rules for a particular supertype, in cases when you know that its
subtypes change frequently on the server.
benjamn added a commit that referenced this issue Aug 25, 2020
Easily the biggest breaking change for fragment matching in Apollo Client
3.0 was the removal of the FragmentMatcher abstraction, along with its
subclasses HeuristicFragmentMatcher and IntrospectionFragmentmatcher,
which were replaced by the declarative possibleTypes configuration.

The HeuristicFragmentMatcher was so named because it attempted to perform
fragment matching without any actual knowledge of supertype/subtype
relationships in your schema, instead checking whether all the fields of a
given fragment were present in the result object, which is indeed a
relatively strong signal that the fragment probably matched.

The HeuristicFragmentMatcher could be fooled by aliases and accidental
sharing of field names between different fragments, but it was also
relatively resilient to the possibility of adding new subtypes on the
server, because heuristic matching doesn't care what the true subtypes of
a supertype are, so what's one more?

The other big drawback of the HeuristicFragmentMatcher was that it applied
the same fuzzy logic to reading from the cache, where the heuristic makes
a lot less sense. If an individual query result has all the keys you'd
expect if a certain fragment matched, that's a pretty good sign that the
fragment matched. But when you have a lot of data in your cache, from lots
of different queries, it's not as meaningful to observe that all the
fields required by the fragment are present, since they might be there
because they've been written into the cache by other queries over time,
not because the fragment actually matches the __typename of the object.

In moving to the more exact possibleTypes system, we gave up both the
benefits and the drawbacks of the HeuristicFragmentMatcher, replacing it
with something functionally similar to the IntrospectionFragmentmatcher,
but with a much simpler configuration API.

As #5750 demonstrates, the exactness of the possibleTypes system makes it
challenging for existing clients to adapt when a new subtype is added on
the server. Is there any way to make this system more flexible, like the
HeuristicFragmentMatcher, but without the drawbacks?

This PR improves the possibleTypes API by allowing "fuzzy" subtype
strings, which are interpreted as regular expressions for typenames,
rather than as actual typename strings. If a fuzzy subtype matches the
__typename of an object, fragments on supertypes of that fuzzy subtype are
allowed to match the object, provided the result also has all the keys
required by the fragment.

The key advantages of this new system compared to the
HeuristicFragmentMatcher are that (1) you can specify fuzzy subtypes for
specific supertypes (rather than applying heuristic matching for all types
by default), and (2) that it "learns" about fuzzy subtypes while writing
(where the heuristic tends to work very well), and then merely uses those
inferred supertype/subtype relationships when reading, so there is no need
to perform heuristic matching while reading. This is the "twist" mentioned
in the title of this PR. When one of these inferences happens, you'll see
a warning in the console (in development).

You do have to opt into fuzzy matching, but it can be a useful strategy
for preparing your clients for upcoming server changes, or for relaxing
the rules for a particular supertype, in cases when you know that its
subtypes change frequently on the server.
@hwillson
Copy link
Member

#6901 was merged to help with this - thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants