__fulfilled meta field
At a glance​
- Identifier: #879
- Stage: RFC0: Strawman
- Champion: @mjmahone
- PR: __fulfilled meta field
- Related:
- FragmentModularity (Fragment Modularity)
Timeline​
- Added to 2021-09-02 WG agenda
- Mentioned in 2021-09-02 WG notes
- Added to 2021-08-05 WG agenda
- Spec PR created on 2021-07-23 by mjmahone
- Commit pushed: __fulfilled meta field on 2021-07-23 by @mjmahone
Proposal: Add a
__fulfilled(label: String): Boolean!
meta-fieldThis 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 valuetrue
:__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 optionallabel
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 returnnull
ifUserProfilePic
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'slabel
. Even that seems a bit buggy: what ifUserWithName
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 likeFoo__fulfilled: __fulfilled(label: "Foo")
, and see if the response containsFoo_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 bothUser
andBusiness
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 fromfragment 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 aUser
orBusiness
- 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 functionid(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 paradigmsI am open to other suggestions!
Alternative 1: Just use
__typename
​Using
__typename
is an option. However, because__typename
is of typeString!
, 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 theasUserProfilePic
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.