Skip to content

RFC: __fulfilled meta field #879

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

mjmahone
Copy link
Contributor

@mjmahone mjmahone commented Jul 23, 2021

Proposal: Add a __fulfilled(label: String): Boolean! meta-field

This is currently a Stage 0: Strawman proposal, but I'm including a PR with edits and a graphql-js implementation such that this can get knocked down or advance to Stage 1 quickly. @mjmahone is currently championing this proposal.

Implemented in graphql-js PR 3216.

Details: Add the below after 4.1: Type Introspection

GraphQL supports the ability to introspect into whether any given selection was included in a response. When a selection is included, all of that selection's fields will be explicitly set, except those not executed due to a directive such as @include or @skip. Selection introspection is accomplished via the meta-field __fulfilled(label: String): Boolean! on any Object, Interface, or Union. It always returns the value true: __fulfilled will be set if and only if the selection containing it is included in the response.

For example, given the operation:

query Q($foo: Boolean!) {
  ... @include(if: $foo) {
    included: __fulfilled(label: "user.included")
  }
  ... @skip(if: $foo) {
    skipped: __fulfilled(label: "user.skipped")
  }
}

The response should be either:

{
  "included": true
}

or

{
  "skipped": true
}

This meta-field is often used to clarify response interaction: as an example, the __fulfilled field can be used as a way to tell the consumer whether a specific fragment was included in the response. The optional label argument may be used to differentiate between __fulfilled selections.

As a meta-field, __fulfilled is implicit and does not appear in the fields list in any defined type.

Note: __fulfilled may not be included as a root field in a subscription operation.

End Spec Addition: Discussion continues below

Why?

I've discussed in the past about how Facebook is moving from product developers interacting with Response-shaped models that use inheritance such that any field in a selection set and all of its children are available. Instead, we try to use composition such that there is one model a product can interact with per selection set, and models need to explicitly convert (via a special conversion method) from parent to child selection sets.

For example, in combination with the @defer specification, when we have GraphQL that looks like:

fragment UserWithName on User {
  name
  ...UserProfilePic @defer
}

fragment UserProfilePic on User {
  name
  profilePicture { 
    uri
  }
}

An example of well-typed fragment models for the user to interact with, in pseudo-typescript, might look like this:

type UserWithName = {
  name: string,
  asUserProfilePic(): UserProfilePic | null,
};

type UserProfilePic = {
  name: string,
  profilePicture: {
      uri: string,
  },
};

In this case, asUserProfilePic() should return null if UserProfilePic has not yet been fulfilled in a response payload. Unfortunately, to determine that, we'd need to reach outside our local fragment, and check the response's label. Even that seems a bit buggy: what if UserWithName is included in multiple locations in the operation's tree?

The cleanest solution I've thought through is having a well-defined boolean field inserted, at the location we care about, that will be true if and only if the selection set we care about is included in the response. Note, this solution applies not just to @defer, but also to existing directives like @include and @skip, and could even enable simpler handling of type resolution. All a client needs to do to check whether a selection set was included, is to add something like Foo__fulfilled: __fulfilled(label: "Foo"), and see if the response contains Foo_fulfilled: true.

Client Specific Considerations

On most of our clients today (including Relay), we parse data from the server into a Graph format: we merge every instance of a single unique record (usually as determined by a primary key's value like id), taking all fields requested across an entire operation (or many operations), and merge them into a locally consistent graph.

However, product developers interact with this local graph as though it is shaped like the GraphQL selection sets described by their operation and fragment definitions. Meaning even though the server response might look like:

{
  "actor": {
    "__typename": "OpenSourceContributor",
    "name": "Matt",
    "org": "GraphQL"
  }
}

there may be a variety of fragments, with drastically different shapes, that describe how specific components use that underlying response:

fragment A on Viewer {
  actor {
    __typename
    name
    ...B @include(if: $use_b)
    ... on Business @include(if: $use_business) {
      org
    }
  }
}

fragment B on User {
  org
}

Just by looking at the structure of the response, I don't know whether my component using A should have access to actor.org: if both User and Business are interfaces, how can we tell whether this is the User's org or the Business' org? Or both?

Today, in order to determine whether org is from fragment B or ... on Business, we have clients that do one of:

  • Include a known type hierarchy in the client, and
    • Check the boolean values for $use_b and $use_business sent with the original request
    • Check whether the concrete type OpenSourceContributor is a User or Business
    • NOTE: this is prone to dropping fields, as future concrete types are not known to implement any interfaces by the client.
  • Use the __typename-aliasing hack, and re-route aliased typenames to a client-only field for our local Graph.

What to call this boolean meta-field?

I am open to bikeshedding here. Some alternatives:

  • __exists: the field is for figuring out whether a selection set exists, so this feels natural
  • __id: it's almost a "selection set identity" function. This is probably not a good name, as it will clash with lots of other implementations where __id means "identifier" rather than the "identity function id(x: X): X".
  • isFulfilled: this makes it clear we're using the field to answer a question. The downside is it's a camelCase'd meta-field, where many implementations use a snake_case style guide. I would greatly prefer a name that works well under either camelCase or snake_case paradigms

I am open to other suggestions!

Alternative 1: Just use __typename

Using __typename is an option. However, because __typename is of type String!, it's a complicating option: the response is bloated with unnecessary information, and any infra that relies on this field needs to translate the response as a string to a boolean value, based on whether the key exists rather than the actual __typename value.

Concretely: it's much less error-prone to generate models that, under the hood, do boolean checking on boolean-returned values, rather than requiring either a field-existence check, or a string-exists-and-is-not-empty check (depending on client language).

Furthermore, if we are using a normalized, canonical store for our response data, as Relay and Apollo and other clients do, we need a special fulfilled field handler just to make sure we don't accidentally write is_Foo_fulfilled to the canonical __typename, and ask whether __typename exists. Note, this is usually how we attempt to solve the problem today, and it has lead to actual bugs, hence this PR.

With the __fulfilled(label:) argument, the canonical version of the field can be used to differentiate unique selection sets, allowing the asUserProfilePic implementation from the Why? section to become:

  asUserProfilePic(): UserProfilePic | null {
    if (_normalizedStoreRecord.getBoolean('__fulfilled(label:"UserProfilePic")')) {
      return new UserProfilePic(_normalizedStoreRecord);
    }
    return null;
  }

Alternative 2: Don't use a meta-field, if you want this ability explicitly add the field to each type in your schema

We could do this, but then we can't use this field on Unions. Also, if a field exists on every type, it should probably be a meta-field.

Other Alternatives

This is a straw man, so I'd be very interested in how other people solve this problem, and whether the above proposal would make their implementations better or be completely useless or harmful.

@mjmahone mjmahone added the 💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md) label Jul 26, 2021
@mjmahone mjmahone marked this pull request as ready for review July 26, 2021 15:47
@andimarek
Copy link
Contributor

Some early feedback here @mjmahone, could be very well I am missing something. Please correct me where I got something wrong.

My concern regarding this PR is that it seems to elevate fragments into something that execution engines need to preserve.

This would be a big change to GraphQL with huge (potential breaking) impact.
As a real life example: in Atlassian in our GraphQL Gateway we are resolving all fragments before starting to execute the query and then the query is actually forwarded to other services for the actual execution. The forwarded query doesn't resemble the original query at all and all original fragments are gone.

The logic behind this "resolving all fragments" is based on the merged field validation: it basically merges all fields together and resolves all type conditions into Objects so that you only have a tree of fields. Here are a bunch of tests which make this hopefully clearer.

My gut feeling is that any improvements should come from __typename. That is basically all what counts at execution time (at least currently).

In general I am also not sure relying on the specific selections sets/fragments is great. Looking at your example above regarding the org

fragment A on Viewer {
  actor {
    __typename
    name
    ...B @include(if: $use_b)
    ... on Business @include(if: $use_business) {
      org
    }
  }
}

fragment B on User {
  org
}

You ask where org comes from: is it a Business Org or a User org. I am wondering: if this information is important for the client, why not explicitly ask for it in the query, for example by aliasing it?

fragment A on Viewer {
  actor {
    __typename
    name
    ...B @include(if: $use_b)
    ... on Business @include(if: $use_business) {
      businessOrg: org
    }
  }
}

fragment B on User {
  userOrg: org
}

@benjie
Copy link
Member

benjie commented Sep 8, 2021

My concern regarding this PR is that it seems to elevate fragments into something that execution engines need to preserve.

@andimarek I don't think this is correct; even after deep merging of the fragments either the field exists in the selection set or it doesn't - it doesn't actually matter that it was inside a particular fragment since it's just a field that always returns true. This is one of the beauties of Matt's proposal - it works with the existing field merging mechanics and is a very very small change to GraphQL implementations. Technically you could implement it with aliases of __typename and it would work in all spec-compliant GraphQL services today.

@andimarek
Copy link
Contributor

My concern regarding this PR is that it seems to elevate fragments into something that execution engines need to preserve.

@andimarek I don't think this is correct; even after deep merging of the fragments either the field exists in the selection set or it doesn't - it doesn't actually matter that it was inside a particular fragment since it's just a field that always returns true. This is one of the beauties of Matt's proposal - it works with the existing field merging mechanics and is a very very small change to GraphQL implementations. Technically you could implement it with aliases of __typename and it would work in all spec-compliant GraphQL services today.

I understood it as the label Argument allows you to uniquely identify the selection set, which would mean you need preserve the selection set. But maybe this is not correct.

@benjie
Copy link
Member

benjie commented Sep 8, 2021

No, it's just an argument to work around another way that Relay behaves 😉 Specifically Relay's cache kind of ignores aliases so a: myField and b: myField both get stored into the Relay cache as if you just requested myField with no alias (the field name and arguments are the important things, argues Relay). As such using __typename with aliases wouldn't work (since they'd all be merged together in the store and treated as a single value), so __fulfilled(alias: String!) guarantees that the __fulfilled references won't be merged since the arguments differ, but also helps people who are not using Relay to ensure that they don't accidentally use the same aliases for different __fulfilled instances since alias: __fulfilled(label: "frag1") and alias: __fulfilled(label: "frag2") cannot merge (same applies without aliases - and thus encourages users to use aliases).

DISCLAIMER: this is based on my recollections of the WG meetings and not an actual understanding of how Relay works, so it may not be accurate or at least might not represent the current public version of Relay 🤷‍♂️

@andimarek
Copy link
Contributor

Thanks for the explanation @benjie

@AnthonyMDev
Copy link

I am a huge, huge supporter of getting this proposal merged as well!

Specifically Relay's cache kind of ignores aliases so a: myField and b: myField both get stored into the Relay cache as if you just requested myField with no alias (the field name and arguments are the important things, argues Relay).

FWIW, Apollo's normalized cache works the same way. Aliases are stripped before storing in the cache.

My only comment here is that the label argument seems to be a little bit clunky. I understand the reason for its usage, as @benjie explains.

__fulfilled(alias: String!) guarantees that the __fulfilled references won't be merged since the arguments differ, but also helps people who are not using Relay to ensure that they don't accidentally use the same aliases for different __fulfilled instances since alias: __fulfilled(label: "frag1") and alias: __fulfilled(label: "frag2") cannot merge (same applies without aliases - and thus encourages users to use aliases).

But it seems redundant to require both the alias and the label argument. I don't love that you need to put both the alias and the argument (Foo_fulfilled: __fulfilled(label: "Foo"). As a workaround if you are adding this to your own types, it makes sense, but I wonder if it's better to handle this more elegantly if we are making it a standard part of the spec.

The simple proposal I'd make is that the __fulfilled field has a non-optional alias argument that we handle as a special case. The argument would be treated as if the field had an alias of $(alias)_fulfilled for all purposes.

The downside here is that this would have to be handled specifically as a new edge case by all implementations of course. While the current proposal really just works with the way GraphQL works today. But if it's being added to the spec as a standard meta-data field, it might not be too bad to make that trade off. The implementation of this case would be a pretty trivial addition for most tools.

It's also new domain specific knowledge of some magic that just sort of happens under the hood and is a bit opaque, so I understand the arguments against this. But I'd argue the label argument is kind of opaque itself. Until reading @benjie's explanation, I was very confused about why that argument even existed.

Additionally, if that argument is optional and it's not supplied, normalized caches are going to treat all __fulfilled fields as one field and we are going to lose fidelity. So it would still need to be handled as a special case by clients with normalized caches. We would need to make the argument required AND require it to be unique.

Perhaps there is a better solution for identifying the scope of the __fulfilled field? But I suspect any solution is going to require this meta-data field to be handled as a special case in some layer (server execution, client caching, response serialization, etc).

@benjie
Copy link
Member

benjie commented Jan 14, 2022

Thanks for your input @AnthonyMDev, it's good to get an insight into Apollo's usage as it is one of the widest used clients! By the way, in case you've not been keeping up with the GraphQL Working Group discussions, there's a lot of discussions ongoing at the moment about the place of fragments within GraphQL that cascade into lots of topics. I suspect the WG will be looking for a holistic solution that addresses as many concerns as possible in the most elegant way we can; this RFC may or may not be part of that solution, we need to explore the problem space more before we can figure it out I think - if this is of interest then have a look through the recent WG notes/videos and get involved in the relevant discussions 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants