Replies: 5 comments 13 replies
-
Concern: Variable Shadowing and ScopeScope: Local ✅In the current implementation, fragment argument values are scoped to the fragment that has the variable definition. This means you can only use a variable with the name of a fragment variable definition inside that same fragment (not within a parent, child, or sibling fragment). From all the discussions so far, this seems generally correct. Alternatives are:
Shadowed Variables: Allowed❓In the current implementation, we allow shadowing both global variables and parent-fragment-defined variables. So this is legal: query Q($a: Int) {
x(a: $a)
...B
}
fragment B($a: String) on Query {
stringOrNull(a: $a)
...C(a: $a)
}
fragment C($a: String = "Red") on Query {
requiredString(a: $a)
} Within We could disallow this class of shadowing, but I'm currently for allowing shadowing, for a few reasons:
|
Beta Was this translation helpful? Give feedback.
-
Concern: Variable Definition or Argument Definition?In the current PRs I re-use Relay currently defines them as Given the syntax is identical to Operation level Variable Definition, I've preserved Variable Definition at the definition site, Variable for usage of the variable within the fragment, and Argument for when a fragment spread is setting a locally scoped variable to some value. I've tried implementations and Spec edits with both Argument Definition (creating a new ArgumentDefinition AST kind), and the current state seems reasonable to me, but this concern may be worth further discussion. Feel free to discuss here and also add inline comments on the graphql-spec PR if there are any confusing bits you can point to! |
Beta Was this translation helpful? Give feedback.
-
Concern: Algorithm Options for ExecutionI've explored two different algorithms that are isomorphic in result. Option 1: Passing In ScopeThe first is to have true scoped variables, where we consider there to be an "operation" scope and "fragment local" scope. Whenever we encounter a fragment spread, we update the scope by replacing the set of parent local variable values with the new local variable values. So instead of function definitions like:
we have something like:
This extra argument needs to be passed into all functions that checks Option 2: Replacing Fragment Selection Set ASTThis is functionally equivalent, but instead of keeping track of our current scope, whenever we encounter a fragment spread we figure out what the fragment-scoped variable values are by copying and visiting the copied fragment's selection set: whenever we find a variable usage defined by the fragment, we replace it with the value of the argument passed in, the default value if there is no passed in argument, or This substitution function is pretty simple to describe even as a spec algorithm: SubstituteFragmentArguments(fragment, arguments):
- Let {variablesToSubstitute} be initialized to an empty map.
- For each {variableDefinition} in {fragment}:
- Let {variableName} be the name of {variableDefinition}.
- If {variableName} is a key in {arguments}:
- Let {argumentValue} be the value of {variableName} in {arguments}.
- Add {argumentValue} to {variablesToSubstitute} at key {variableName}.
- Otherwise if {variableDefinition} has a default value {defaultValue}:
- Add {defaultValue} to {variablesToSubstitute} at key {variableName}.
- Otherwise:
- Set the key {variableName} in {variableToSubstitute} to a value indicating
the variable is unset.
- Let {substitutedFragment} be a copy of {fragment} where:
- For each {variable} in the selection set of {fragment}:
- Let {variableUsageName} be the name of {variable}.
- If {variableUsageName} is in {variablesToSubstitute}:
- Replace {variable} with the value of {variableUsageName} in
{variablesToSubstitute}.
- Return {substitutedFragment}. Note we need to explicitly replace unset variables with some impossible-to-set-at-the-operation variable, so that they act the same as variables defined on the variable that are never set. If we just don't replace them, then we run into the bug where operation scoped variables can be used at execution for fragment-defined variable usages. Likewise, if we replace these instances with Current choice: Option 2 (Replacement)I like the mental model of the passed-in scope algorithm. However, if we consider changes to our existing algorithms, the second option is a much cleaner change. We end up only needing to update the execution algorithm in one location: when we encounter fragment spreads. With the scoped variables option, we need to update every part of our algorithms that check or coerce variable values to include the concept of first checking the local scope, then the operation scope. This affects basically every function under Execution, and we either need to add an extra argument on all of these functions for locally scoped variables, or we need to modify the type of variableValues. Potential Additional OptionsWe could make it so that only variables with default values can be unset arguments. I actually prefer this mental model, but decided against it as this would mean understanding the difference in argument behavior between fields (where an unset argument becomes null unless there's a default value) vs. fragments (where it now is invalid to have an unset nullable argument without default value). This option would eliminate the need for |
Beta Was this translation helpful? Give feedback.
-
Concern: Fields Can Merge Validation ComplexityThis was brought up by @andimarek in the Jan 2023 APAC WG. Is the addition of fragment variables an attack vector for algorithmic complexity and explosion? The Fields Can Merge validation is already very expensive: does this PR increase the cost? BeliefI believe the answer is generally no, or at least: the cost is no worse than creating N independent fragments, where N is the unique combination of arguments passed in. Note, this is how Relay handles fragment arguments today (they compile each fragment spread with unique arguments into a new fragment with a unique name). Given the way to "solve" for parameterized fragments today is by having uniquely-named fragments, the complexity seems, on the surface, to be isomorphic to today's field merging validation complexity. The "cost" seems no different from adding fields with aliases and different arguments: whenever we do encounter two fragment spreads at the same level with the same name but different arguments, we are able to stop the validation then and there, rather than traversing deeper into the fragment. TODO: Come up with degenerate examples, test implementationsThere likely is a class of degenerate usage. The question we'll need to answer is if this degeneracy is worse than the degenerate field merging case. If it's not, I do not think the degeneracy should block adoption of fragment arguments. If it is worse, we may need to figure out algorithmic shortcuts (possibly updates to validation) that would reduce the attack vector. |
Beta Was this translation helpful? Give feedback.
-
Concern: Ecosystem supportIn order for Relay users (an JavaScript users generally) to be able to adopt this syntax, there are a number of ecosystem updates that will need to be pushed through:
None of these individually are complicated updates, but due to the dependency chains they may take time to roll out, especially if we don't have someone actively driving the work. |
Beta Was this translation helpful? Give feedback.
-
We now have PRs for Fragment Arguments in
graphql-js
: graphql/graphql-js#3835 andgraphql-spec
: graphql/graphql-spec#1010This discussion will include threads for all open questions and TODOs, and should be the source of truth for discussion about what is left to do or consider, while the PRs should be focused on discussion around the reference and spec implementations.
Beta Was this translation helpful? Give feedback.
All reactions