-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
RFC: Fragment Arguments (parameterized fragments) for GraphQL #865
Conversation
@mjmahone some general questions:
thanks |
Yes, but that's already true for clients like Relay today. This spec proposal would have the actual spec basically accepting that current reality.
I would like to, but there's a lot higher of a bar to this. Especially in terms of backwards compatibility: this spec RFC provides a way for a client hitting any current-spec-compliant server implementation to use these new idioms, without waiting for a server upgrade. Basically, I anticipate more changes in this space, including changes to server execution behavior, but I'd be loathe to block a client from using the new paradigm until after we have the server-side figured out. Importantly: this paradigm is widely used within FB inside Relay, but the ergonomics of
In short, tooling. GraphiQL and Prettier's GraphQL plugin both rely on the spec's interpretation of "what GraphQL is". In some sense, that eliminates our ability to iterate on "future GraphQL": if we need both the server and client to agree on any new "spec features", even when the client could "transpile" to previous-spec-GraphQL (as people call the process of JS-new to JS-old compilation), we end up in a chicken-and-egg situation for introducing new paradigms. New paradigms, without tooling support, can't get adopted. An alternative to introducing this in the spec would be to introduce it purely in the tooling repos: this is certainly possible, but if It seems better IMO to put an "optional extension" into the spec "if we allowed fragment arguments, this is the syntax we'd use. For clients using the new syntax to stay compatible with the current server spec, here's the limitations we need to add via extra validation rules". |
@mjmahone In the long run, we need to switch My proposal is to merge it as RFC and implement parsing and validation in |
I actually think the executor implementation is not that difficult: graphql/graphql-js#3152. I'll iterate on the spec changes required once we have some form of consensus that this experimental implementation is OK to iterate with. |
I think it would be awesome if we could add this without the rule "Fragment Argument Uniqueness". I don't see a logical reason from a spec point of view why this rule needs to exist. It might be more complex to implement on the server or client, but not having this rule in place would have huge benefits IMO: With the rule in place, a user of a fragment has to have knowledge of the entire query tree to know if the fragment can be used in a certain context. Without the rule, all dependencies can be explicitly defined throughout the entire query tree, and a component that uses a fragment can be used any number of times with any variables in any context. As an idea, we could add a rule that all the variables that are used within a fragment have to be defined on the fragment definition. Valid example query Query($large: Int!, $small: Int!) {
user {
...ProfileImage(size: $large)
friends {
...ProfileImage(size: $small)
}
}
}
fragment ProfileImage($size: Int!) on User {
imageUrl(size: $size)
} Invalid example fragment ProfileImage on User {
imageUrl(size: $size)
} Benefits of this approach:
To ensure backward compatibility, we can detect if there are variables passed to fragments in the query definition. If there are fragment variables passed, the query is executed with the new strict logic, otherwise, the current validation rules apply. |
@ivome: I agree that making fragments truly modular and composable is desirable. There's a lot that can be done, but what you've proposed won't by itself solve the issue. If the goal is to make fragments fully modular, I think you'd do well to work through as many counterexamples as you can for your proposal. How would you prevent this?
In short: GraphQL is already not modular with respect to fragments. This proposal is not trying to solve all modularity problems, but to make it possible to solve some of them (primarily, allowing fragments to be used without an operation needing to know about that fragment's variables directly). There are probably a series of modifications we could make to the language to get GraphQL to be truly modular, but that's outside this RFC's scope. |
This particular case would be prevented by the rules defined for field selection merging: https://spec.graphql.org/June2018/#sec-Field-Selection-Merging
I agree, GraphQL is not 100% modular and it would require major changes to make it that way. What I'm proposing here is related to a similar point that @leebyron raised in his latest comment on your PR graphql/graphql-js#3152:
If we allow variable names and types to be overridden in fragments, we remove the dependency between fragment and query variable definitions, and could also allow fragments to be used multiple times in different parts of the query with different variables (what I described above). |
@ivome unfortunately, I know of at least two GraphQL implementations that could be made to support this more-restricted version of the spec change, but could not, without GraphQL response-format changes, be made to support the looser version you're suggesting. While I recognize that's an implementation detail, I think it would be a bit of a tragedy if the first version of this new syntax could have enabled clients to be spec-compliant in full support of fragment variables, but because we didn't include an extra validation rule, cannot be. Basically: I'd rather allow GraphQL implementations to support more than the spec by ignoring certain validation rules, than prevent existing spec-compliant implementations from supporting a future spec because we didn't include enough validation rules. Once we're at the point of fragment variables existing, in a very restricted format, in the spec, it becomes easier to provide a pathway for those other implementations to iterate and drop some validation rules that we no longer want to require. |
✅ Deploy Preview for graphql-spec-draft ready!
To edit notification comments on pull requests, go to your Netlify site settings. |
referenced in a fragment and is included by an operation that does not define | ||
that variable, that operation is invalid (see | ||
Variables can be used within fragments. Operation defined variables have global | ||
scope with a given operation, so a variable used within a fragment must either |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Operation-defined variables ... ... within the operation
spec/Section 2 -- Language.md
Outdated
Directives[Const]? | ||
|
||
Fragments may define locally scoped arguments, which can be used in locations | ||
that accept variables. This allows fragments to be re-used while enabling the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reused (no dash)
spec/Section 2 -- Language.md
Outdated
} | ||
``` | ||
|
||
In this case, the `user` will have a larger `profilePic` than those found in the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure but I don't think you need comma after 'In this case'
spec/Section 2 -- Language.md
Outdated
|
||
The profilePic for `user` will be determined by the variables set by the | ||
operation, while `secondUser` will always have a profilePic of size 10. In this | ||
case, the fragment `variableProfilePic` uses the operation defined variable, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
operation-defined
spec/Section 2 -- Language.md
Outdated
operation, while `secondUser` will always have a profilePic of size 10. In this | ||
case, the fragment `variableProfilePic` uses the operation defined variable, | ||
while `dynamicProfilePic` uses the value passed in via the fragment spread's | ||
argument `size:`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you need colon after size?
|
||
FragmentArgumentDefinition : Description? Variable : Type DefaultValue? | ||
Directives[Const]? | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we introduce FragmentArgumentDefinition instead of reusing VariableDefinition just because we want to add Description as well? How about simply adding Description to Variable definition - let's discuss it. If Description makes sense here then it makes sense in Operation variable definition (even more I think)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried creating an implementation where we reuse VariableDefinition, and it turned out to be more clunky than this. Fragment argument definitions need to be described everywhere as being argument definitions, not as defining a variable. It causes confusion when referring to a fragment argument's variable definition, IMO. Furthermore, in the actual implementation in graphql-js, the AST refers to arguments
in the same way that a field or directive has arguments. When those arguments are defined via variable definitions, you end up over-visiting the VariableDefinition values and needing to check the context of those definitions more than if you have a unique AST value for fragment arguments.
If I could, I would have reused InputValueDefinition: logically, fragment argument definitions produce the same kind of thing in the language as field argument definitions, directive argument definitions, and input object field definitions. Ideally, I would have had FragmentArgumentDefinition : $ InputValueDefinition
. This doesn't work, though, as InputValueDefinition
is defined as InputValueDefinition : Description? Name : Type DefaultValue? Directives[Const]?
.
I could be swayed that it makes sense to reuse VariableDefinition, so long as we don't need to update the text to describe them as anything other than argument definitions in the same way that field arguments are argument definitions in the Spec description. However, another advantage to separating the two is that we probably do not want a directive that is allowed on VARIABLE_DEFINITION to automatically be allowed on FRAGMENT_ARGUMENT_DEFINITION and vice-versa.
On the Description front, agree it probably makes sense to add Description to Operation VariableDefinition, but even so I think FragmentArgumentDefinition makes sense to separate as it is defining an input as opposed to VariableDefinition, which defines a globally available value.
spec/Section 5 -- Validation.md
Outdated
``` | ||
|
||
the response will have two conflicting versions of the `doesKnowCommand` that | ||
cannot merge. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in what sense 'cannot merge'? I can write implementation that merges them. The fields have different values I guess. Would be better 'should not merge'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the same sense as above,
However, the field responses must be shapes which can be merged
spec/Section 5 -- Validation.md
Outdated
One strategy to resolving field conflicts caused by duplicated fragment spreads | ||
is to short-circuit when two fragment spreads with the same name are found to be | ||
merging with different argument values. In this case, validation could short | ||
circuit when `commandFragment(command: SIT)` is merged with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again 'short circuit' but now without dash.
spec/Section 5 -- Validation.md
Outdated
``` | ||
|
||
the following is invalid since `command` is not defined on `DogCommand`. | ||
the following is invalid since `command:` is not defined on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
command:
-> command
argument
spec/Section 5 -- Validation.md
Outdated
|
||
```graphql counter-example | ||
fragment invalidArgName on Dog { | ||
doesKnowCommand(command: CLEAN_UP_HOUSE) | ||
} | ||
``` | ||
|
||
and this is also invalid as `$dogCommand` is not defined on fragment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$dogCommand
-> dogCommand
argument
spec/Section 5 -- Validation.md
Outdated
``` | ||
|
||
the following is invalid since `command` is not defined on `DogCommand`. | ||
the following is invalid since `command:` is not defined on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Capital T at sentence start
spec/Section 5 -- Validation.md
Outdated
@@ -1828,6 +1891,29 @@ fragment isHouseTrainedWithoutVariableFragment on Dog { | |||
} | |||
``` | |||
|
|||
Fragment arguments can shadow operation variables: fragments that use an | |||
argument are not using the operation defined variable of the same name. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
operation-defined (dash)
spec/Section 5 -- Validation.md
Outdated
|
||
because | ||
{$atOtherHomes} is only referenced in a fragment that defines it as a | ||
locally scoped argument, the operation defined {$atOtherHomes} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
operation-defined
spec/Section 5 -- Validation.md
Outdated
**Explanatory Text** | ||
|
||
All arguments defined by a fragment must be used in that same fragment. Because | ||
fragment arguments are scoped to the fragment they're defined on, if the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
they're - bad style to use these speech shortcuts in documents, change to 'they are'
spec/Section 5 -- Validation.md
Outdated
|
||
All arguments defined by a fragment must be used in that same fragment. Because | ||
fragment arguments are scoped to the fragment they're defined on, if the | ||
fragment does not contain a variable with the same name as the argument, then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe simply 'fragment does not use the argument' ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably. I'll change it but we're not explaining the "local scope" well (as a variable being used by an operation includes any recursive fragments, but in this case use means excluding recursive fragments).
spec/Section 5 -- Validation.md
Outdated
fragment does not contain a variable with the same name as the argument, then | ||
the argument is superfluous. | ||
|
||
For example the following is invalid: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add comma after 'For example'
https://linguaholic.com/linguablog/comma-after-for-example/
} | ||
``` | ||
|
||
This document is invalid because even though `fragmentArgUnused` is spread with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'is A spread' (currently 'spread' sounds like a verb)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Under "Fragment Spread is Possible", spread
is used as a verb:
Fragments are declared on a type and will only apply when the runtime object type matches the type condition. They also are spread within the context of a parent type.
I think we use spread
as a verb explicitly: spreading is the action that you take when applying a fragment spread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good, with few comments
spec/Section 5 -- Validation.md
Outdated
cannot merge. | ||
|
||
One strategy to resolving field conflicts caused by duplicated fragment spreads | ||
is to short-circuit when two fragment spreads with the same name are found to be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure what short-circuit means
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to update the algorithm to explicitly fail if two spreads with the same name have different arguments, rather than using a "short circuit" (stop early) option.
I'm really excited for this to make it across the finish line! Great work @mjmahone! I have read all of the discussions above and in the graphql-js PR regarding the fragment argument uniqueness rule, and I agree with @mjmahone that allowing multiple fragment spreads of the same fragment with different arguments on the same object, would require response format changes and a lot more work to implement. But I'm not clear why are aren't allowing them on different objects? This would be problematic: query Query($large: Int!, $small: Int!) {
user {
...ProfileImage(size: $large)
...ProfileImage(size: $small)
}
}
fragment ProfileImage($size: Int!) on User {
imageUrl(size: $size)
} But, unless I have missed something, it looks like this example, given by @ivome, would also be invalid: query Query($large: Int!, $small: Int!) {
user {
...ProfileImage(size: $large)
friends {
...ProfileImage(size: $small)
}
}
} I'm not sure why that is the case. Fulfilling the fragment on different objects should not cause any issues that I have been able to think of so far. The spec already discusses the concept of different objects not needed fields to merge. This should not require the response format to need any changes. I don't have any reason to believe this makes supporting this new feature in other libraries significantly harder. @mjmahone The example you give in this comment would be invalid, because there would be a field merging collision in https://spec.graphql.org/June2018/#sec-Field-Selection-Merging, as @ivome stated. As long as field merging is still valid, I don't see why this should be disallowed. This operation should still be invalid if the fragment is defined in a way that would cause a field merging violation: query Query($large: Int!, $small: Int!) {
user {
...ProfileImages(size: $large)
friends {
...ProfileImages(size: $small)
}
}
fragment ProfileImages($size: Int!) on User {
imageUrl(size: $size)
friends {
imageUrl(size: $size)
}
} This would be invalid because when completing field merging the Am I mistaken, and this does work the way I'm hoping, or are there other counterexamples that make this problematic? |
Ah @AnthonyMDev the spec/graphql-JS changes as they exist TODAY would allow merging fragment spreads with different arguments at different levels of the response. It reuses the field merging logic. My comment from 2021 is outdated! How you want it to work is also how I want it to work. But thanks for the example: I will add a test for the example where the field merging needs to fail despite spreads being on different objects. |
That's great news! Thanks for all of this. Can't wait to be able to start using this in Apollo! |
rfcs/FragmentArguments.md
Outdated
If we were writing the language from scratch, I'd advocate for making _all_ | ||
argument definitions without a default value to be required, regardless of their | ||
nullability. If you want to make a nullable argument optional, you do so by | ||
adding a `= null` to its definition. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't work really well either. If you do
fragment Bar($x: Int = null) { number(x: $x) }
then what would $x
be in ...Bar
? Would it be null
or undefined
? To be consistent with other default values, I'd expect it to be null
but then there's no way to pass undefined
(no value).
Not sure what a good solution would be. Maybe add a undefined
keyword and allow a subset of union types...
fragment Bar($x: Int | undefined)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think allowing undefined variables was a mistake :)
However, the reason this version is labelled as an alternative is because it would try to fix that mistake in one place (fragment arguments) and not elsewhere (operation variables, field/directive arguments, input objects). At the very least we'd want to unify with either operation variables or (field/directive arguments and input objects). Since fixing the mistake of undefined inputs is outside the scope of this feature, I'm opting to keep the same behavior we already have (namely, unset gets resolved to either the fragment defined argument default value, or, if there is no default and the variable is nullable, resolves to null or the field defined argument default value).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think allowing undefined variables was a mistake :)
Gotcha 👍 I think I agree. I cannot think of a use case where a undefined variable or field argument is used (but would be interested to learn about them if any).
For the sake of consistency, I'd rather keep the same behaviour as operation variables though. It took me enough time to understand how it was working that I don't want to learn something new for fragments 😅
3a458c3
to
e736f78
Compare
Pulled RFC doc into graphql/graphql-wg#1229 (moved to graphql-wg where RFCs now live). Note CLA issue was just due to missing an email in my github account. Check should pass on next upload. |
Closing in favor of #1010, as this PR carries a lot of historical baggage that new reviewers don't necessarily need, and has caused confusion. |
This is a proposal to bring Relay-style Fragment Arguments into the Spec as an optional, client-only language feature.
Overview
We would allow clients to write GraphQL like:
which would, prior to sending to the server, be transformed into two unique queries that current spec-compliant servers would understand, such that it behaved like the queries were written like:
and
The exact mechanics of how the query gets rewritten could be different per client, but the behavior should be identical. This RFC seeks to describe the new syntax, as well as adding additional validation for clients that choose to support fragment arguments.
Background
Relay has seen a lot of usage of their non-spec-compliant
@arguments
and@argumentDefinitions
directives, but they're both cumbersome to use and fail basic Spec validation (directives with arguments used but never defined). They accomplish this by pre-compiling any GraphQL definitions using@arguments
and@argumentDefinitions
to transform the document such that, by the time it's sent to the server, it is spec compliant. However, Relay's user-facing directives both do not conform to the spec and also provide a pretty cumbersome, somewhat unintuitive user-facing design.This proposal is to allow clients that pre-compile their GraphQL prior to sending it to the server (such as Relay) to have a built-in syntax for passing argument values into fragment spreads for fragments with well-defined argument definitions.
Champion: @mjmahone