|
| 1 | +RFC: Fragment Arguments |
| 2 | +------- |
| 3 | + |
| 4 | +# Problem: Variable Modularity |
| 5 | + |
| 6 | +GraphQL fragments are designed to allow a client's data requirements to compose. |
| 7 | +Two different screens can use the same underlying UI component. |
| 8 | +If that component has a corresponding fragment, then each of those screens can include exactly the data required by having each query spread the child component's fragment. |
| 9 | + |
| 10 | +This modularity begins to break down for variables. As an example, let's imagine a FriendsList component that shows a variable number of friends. We would have a fragment for that component like so: |
| 11 | +``` |
| 12 | +fragment FriendsList on User { |
| 13 | + friends(first: $nFriends) { |
| 14 | + name |
| 15 | + } |
| 16 | +} |
| 17 | +``` |
| 18 | + |
| 19 | +In one use, we might want to show some screen-supplied number of friends, and in another the top 10. For example: |
| 20 | +``` |
| 21 | +fragment AnySizedFriendsList on User { |
| 22 | + name |
| 23 | + ...FriendsList |
| 24 | +} |
| 25 | +
|
| 26 | +fragment TopFriendsUserProfile on User { |
| 27 | + name |
| 28 | + profile_picture { uri } |
| 29 | + ...FriendsList |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +Even though every usage of `TopFriendsUserProfile` should be setting `$nFriends` to `10`, the only way to enforce that is by manually walking all callers of `TopFriendsUserProfile`, recursively, until you arrive at the operation definition and verify the variable is defined like `$nFriends: Int = 10`. |
| 34 | + |
| 35 | +This causes a few major usability problems: |
| 36 | +- If I ever want to change the number of items `TopFriendsUserProfile` includes, I now need to update every *operation* that includes it. This could be dozens or hundreds of individual locations. |
| 37 | +- Even if the component for `TopFriendsUserProfile` is only able to display 10 friends, in most clients at runtime the user can override the default value, enabling a mismatch between the data required and the data asked for. |
| 38 | + |
| 39 | +# Existing Solution: Relay's `@arguments`/`@argumentDefinitions` |
| 40 | + |
| 41 | +Relay has a solution for this problem by using a custom, non-spec-compliant pair of directives, [`@arguments`/`@argumentDefinitions`](https://relay.dev/docs/api-reference/graphql-and-directives/#arguments). |
| 42 | + |
| 43 | +These directives live only in user-facing GraphQL definitions, and are compiled away prior to making a server request. |
| 44 | + |
| 45 | +Following the above example, if we were using Relay we'd be able to write: |
| 46 | +``` |
| 47 | +fragment FriendsList on User @argumentDefinitions(nFriends: {type: "Int!"}) { |
| 48 | + friends(first: $nFriends) { |
| 49 | + name |
| 50 | + } |
| 51 | +} |
| 52 | +
|
| 53 | +fragment AnySizedFriendsList on User { |
| 54 | + name |
| 55 | + ...FriendsList @arguments(nFriends: $operationProvidedFriendCount) |
| 56 | +} |
| 57 | +
|
| 58 | +fragment TopFriendsUserProfile on User { |
| 59 | + name |
| 60 | + profile_picture { uri } |
| 61 | + ...FriendsList @arguments(nFriends: 10) |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +Before sending a query to the server, Relay compiles away these directives so the server, when running an operation using `TopFriendsUserProfile`, sees: |
| 66 | +``` |
| 67 | +fragment FriendsList on User { |
| 68 | + friends(first: 10) { |
| 69 | + name |
| 70 | + } |
| 71 | +} |
| 72 | +
|
| 73 | +fragment TopFriendsUserProfile on User { |
| 74 | + name |
| 75 | + profile_picture { uri } |
| 76 | + ...FriendsList |
| 77 | +} |
| 78 | +``` |
| 79 | +The exact mechanics of how Relay rewrites the user-written operation based on `@arguments` supplied is not the focus of this RFC. |
| 80 | + |
| 81 | +However, even to enable this client-compile-time operation transformation, Relay had to introduce *non-compliant directives*: each argument to `@arguments` changes based on the fragment the directive is applied to. While syntactically valid, this fails the [Argument Names validation](https://spec.graphql.org/draft/#sec-Argument-Names). |
| 82 | + |
| 83 | +Additionally, the `@argumentDefinitions` directive gets very verbose and unsafe, using strings to represent variable type declarations. |
| 84 | + |
| 85 | +Relay has supported `@arguments` in its current form since [v2.0](https://github.com/facebook/relay/releases/tag/v2.0.0), released in January 2019. There's now a large body of evidence that allowing fragments to define arguments that can be passed into fragment spreads is a signficant usability improvement, and valuable to the wider GraphQL community. However, if we are to introduce this notion more broadly, we should make sure the ergonomics of it conform to users' expectations. |
| 86 | + |
| 87 | +# Proposal: Introduce Fragment Argument Definitions and Fragment Spread Arguments to client-only GraphQL |
| 88 | + |
| 89 | +Relay's `@arguments`/`@argumentDefinitions` concepts provide value, and can be applied against GraphQL written for existing GraphQL servers so long as the pre-server compiler transforms the concept away. However, client-focused tooling, like Prettier and GraphiQL, should be able to recognize and work with these new concepts. |
| 90 | + |
| 91 | +## New Fragment Variable Definition syntax |
| 92 | + |
| 93 | +For the `@argumentDefinitions` concept, we can allow fragments to share the same syntax as operation level definitions. Going back to the previous example, this would look like: |
| 94 | +``` |
| 95 | +fragment FriendsList($nFriends: Int!) on User { |
| 96 | + friends(first: $nFriends) { |
| 97 | + name |
| 98 | + } |
| 99 | +} |
| 100 | +``` |
| 101 | +This has the advantage of already being supported in `graphql-js`'s AST under an experimental flag, while also |
| 102 | + |
| 103 | +## New Fragment Spread Argument syntax |
| 104 | + |
| 105 | +For the `@arguments` concept, we can allow fragment spreads to share the same syntax as field and directive arguments. |
| 106 | +``` |
| 107 | +fragment AnySizedFriendsList on User { |
| 108 | + name |
| 109 | + ...FriendsList(nFriends: $operationProvidedFriendCount) |
| 110 | +} |
| 111 | +
|
| 112 | +fragment TopFriendsUserProfile on User { |
| 113 | + name |
| 114 | + profile_picture { uri } |
| 115 | + ...FriendsList(nFriends: 10) |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +## New Validation Rule: Fragment Argument Definitions Used in Fragment |
| 120 | + |
| 121 | +The whole point of fragment defined arguments is to make fragments more composable. Therefore, fragments should only be defining variables that are explicitly used under that fragment. |
| 122 | + |
| 123 | +Under this rule, |
| 124 | +``` |
| 125 | +fragment Foo($x: Int) on User { |
| 126 | + name |
| 127 | +} |
| 128 | +``` |
| 129 | +would be invalid. |
| 130 | + |
| 131 | +Additionally, under the strictest interpretation of the rule, |
| 132 | +``` |
| 133 | +fragment Foo($x: Int!) on User { |
| 134 | + ...Bar |
| 135 | +} |
| 136 | +
|
| 137 | +fragment Bar { |
| 138 | + number(x: $x) |
| 139 | +} |
| 140 | +``` |
| 141 | +would also be invalid: even though `$x` is used underneath Foo, it is used outside of Foo's explicit definition. |
| 142 | + |
| 143 | +However, this would be valid: |
| 144 | +``` |
| 145 | +fragment Foo($x: Int!) on User { |
| 146 | + ...Bar(x: $x) |
| 147 | +} |
| 148 | +
|
| 149 | +fragment Bar($x: Int) { |
| 150 | + number(x: $x) |
| 151 | +} |
| 152 | +``` |
| 153 | + |
| 154 | +### Considerations: how strict should this rule be? |
| 155 | + |
| 156 | +As an initial RFC, I'd advocate for encouraging the *strictest* version of this rule possible: any argument defined on a fragment must be explicitly used by that same fragment. It would be easy to relax the rule later, but very difficult to do the reverse. |
| 157 | + |
| 158 | +It's clearly more composable if, when changing a child fragment, you don't need to worry about modifying argument definitions on parent fragments. |
| 159 | + |
| 160 | + |
| 161 | +## New Validation Rule: Required Fragment Arguments are Provided |
| 162 | + |
| 163 | +Make the [Required Arguments](https://spec.graphql.org/draft/#sec-Required-Arguments) validation's first two bullets: |
| 164 | + |
| 165 | +- For each Field, Fragment Spread or Directive in the document. |
| 166 | +- Let *arguments* be the set of argument definitions of that Field, Fragment or Directive. |
| 167 | + |
| 168 | +With this rule, the below example is invalid, even if the argument `User.number(x:)` is nullable in the schema. |
| 169 | +``` |
| 170 | +fragment Foo on User { |
| 171 | + ...Bar |
| 172 | +} |
| 173 | +
|
| 174 | +fragment Bar($x: Int!) on User { |
| 175 | + number(x: $x) |
| 176 | +} |
| 177 | +``` |
| 178 | + |
| 179 | + |
| 180 | +## New Validation Rule: Fragment Argument Uniqueness |
| 181 | + |
| 182 | +If the client pre-server compiler rewrites an operation, it's possible to end up with a selection set that violates [Field Selection Merging](https://spec.graphql.org/draft/#sec-Field-Selection-Merging) validation. Additionally, we have no mechanism on servers today to handle the same fragment having different variable values depending on that fragment's location in an operation. |
| 183 | + |
| 184 | +Therefore, any Fragment Spread for the same Fragment in an Operation must have non-conflicting argument values passed in. |
| 185 | + |
| 186 | +As an example, this is invalid: |
| 187 | +``` |
| 188 | +query { |
| 189 | + user { |
| 190 | + best_friend { |
| 191 | + ...UserProfile(imageSize: 100) |
| 192 | + } |
| 193 | + ...UserProfile(imageSize: 200) |
| 194 | + } |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +Note: today Relay's compiler handles this ambiguity. In an extreme simplification, this is done by producing two unique versions of `UserProfile`, where in `UserProfile_0` `$imageSize` is replaced with `100`, and in `UserProfile_1` `$imageSize` is replaced with `200`. However, there exist client implementations that are unable to have multiple applications of the same fragment within a single operation (the clients I work on cannot use Relay's trick). |
| 199 | + |
| 200 | +# Implementation |
| 201 | + |
| 202 | +Any client that implements this RFC would have a pre-server compilation step, which transforms all fragment defined variable usages to: |
| 203 | +- The passed-in value from a parent fragment spread |
| 204 | +- Or if no value is passed in, the default value for the fragment argument definition, |
| 205 | +- Or if no default value is defined and no value is passed in, null. |
0 commit comments