diff --git a/spec/Appendix B -- Grammar Summary.md b/spec/Appendix B -- Grammar Summary.md index 92f222cb3..22c30704b 100644 --- a/spec/Appendix B -- Grammar Summary.md +++ b/spec/Appendix B -- Grammar Summary.md @@ -170,12 +170,12 @@ Arguments[Const] : ( Argument[?Const]+ ) Argument[Const] : Name : Value[?Const] -FragmentSpread : ... FragmentName Directives? +FragmentSpread : ... FragmentName Arguments? Directives? InlineFragment : ... TypeCondition? Directives? SelectionSet -FragmentDefinition : fragment FragmentName TypeCondition Directives? -SelectionSet +FragmentDefinition : fragment FragmentName VariablesDefinition? TypeCondition +Directives? SelectionSet FragmentName : Name but not `on` diff --git a/spec/Section 2 -- Language.md b/spec/Section 2 -- Language.md index 2fe3a5a31..633a99d29 100644 --- a/spec/Section 2 -- Language.md +++ b/spec/Section 2 -- Language.md @@ -520,10 +520,10 @@ which returns the result: ## Fragments -FragmentSpread : ... FragmentName Directives? +FragmentSpread : ... FragmentName Arguments? Directives? -FragmentDefinition : fragment FragmentName TypeCondition Directives? -SelectionSet +FragmentDefinition : fragment FragmentName VariablesDefinition? TypeCondition +Directives? SelectionSet FragmentName : Name but not `on` @@ -1219,13 +1219,72 @@ size `60`: **Variable Use Within Fragments** -Variables can be used within fragments. Variables have global scope with a given -operation, so a variable used within a fragment must be declared in any -top-level operation that transitively consumes that fragment. If a variable is -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 within a given operation. Fragment-defined variables have local scope +within the fragment definition in which they are defined. A variable used within +a fragment must either be declared in each top-level operation that transitively +consumes that fragment, or by that same fragment as a fragment variable +definition. If a variable referenced in a fragment is included by an operation +where neither the fragment nor the operation defines that variable, that +operation is invalid (see [All Variable Uses Defined](#sec-All-Variable-Uses-Defined)). +## Fragment Variable Definitions + +Fragments may define locally scoped variables. This allows fragments to be +reused while enabling the caller to specify the fragment's behavior. + +For example, the profile picture may need to be a different size depending on +the parent context: + +```graphql example +query userAndFriends { + user(id: 4) { + ...dynamicProfilePic(size: 100) + friends(first: 10) { + id + name + ...dynamicProfilePic + } + } +} + +fragment dynamicProfilePic($size: Int! = 50) on User { + profilePic(size: $size) +} +``` + +In this case the `user` will have a larger `profilePic` than those found in the +list of `friends`. + +A fragment-defined variable is scoped to the fragment that defines it. +Fragment-defined variables are allowed to shadow operation-defined variables. + +```graphql example +query withShadowedVariables($size: Int!) { + user(id: 4) { + ...variableProfilePic + } + secondUser: user(id: 5) { + ...dynamicProfilePic(size: 10) + } +} + +fragment variableProfilePic on User { + ...dynamicProfilePic(size: $size) +} + +fragment dynamicProfilePic($size: Int!) on User { + profilePic(size: $size) +} +``` + +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, while `dynamicProfilePic` uses the value passed in via the fragment +spread's `size` argument. + ## Type References Type : diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 75af96ffd..d345cc273 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -418,8 +418,15 @@ fragment directFieldSelectionOnUnion on CatOrDog { FieldsInSetCanMerge(set): -- Let {fieldsForName} be the set of selections with a given response name in - {set} including visiting fragments and inline fragments. +- Let {visitedSelections} be the selections in {set} including visiting fields, + fragment-spreads and inline fragments. +- Let {spreadsForName} be the set of fragment spreads with a given name in + {visitedSelections}. +- For each {spreadsForName} as {name} and {spreads}: + - Each entry in {spreads} must have identical sets of arguments to each other + entry in {spreads}. +- Let {fieldsForName} be the set of field selections with a given response name + in {visitedSelections}. - Given each pair of members {fieldA} and {fieldB} in {fieldsForName}: - {SameResponseShape(fieldA, fieldB)} must be true. - If the parent types of {fieldA} and {fieldB} are equal or if either is not @@ -574,6 +581,58 @@ fragment conflictingDifferingResponses on Pet { } ``` +Fragment spread arguments can also cause fields to fail to merge. + +```graphql counter-example +fragment commandFragment($command: DogCommand!) on Dog { + doesKnowCommand(dogCommand: $command) +} + +fragment potentiallyConflictingArguments( + $commandOne: DogCommand! + $commandTwo: DogCommand! +) on Dog { + ...commandFragment(command: $commandOne) + ...commandFragment(command: $commandTwo) +} + +query { + pet { + ...potentiallyConflictingArguments(commandOne: SIT, commandTwo: DOWN) + } +} +``` + +If two fragment spreads with the same name, and hence the same selection, supply +different argument values, their fields will not be able to merge. In this case, +validation fails because the fragment spread `...commandFragment(command: SIT)` +and `...commandFragment(command: DOWN)` are part of the visited selections that +will be merged. + +If both of these spreads had used the same value for the argument value, it +would be allowed as we can be sure that we would resolve identical fields. +Spreads that use different variables that would always resolve to the same value +are also valid. For example, the following is valid: + +```graphql example +fragment commandFragment($command: DogCommand!) on Dog { + doesKnowCommand(dogCommand: $command) +} + +fragment noConflictWhenPassedOperationCommand( + $fragmentCommand: DogCommand! +) on Dog { + ...commandFragment(command: $operationCommand) + ...commandFragment(command: $fragmentCommand) +} + +query($operationCommand: DogCommand!) { + pet { + ...noConflictWhenPassedOperationCommand(fragmentCommand: $operationCommand) + } +} +``` + ### Leaf Field Selections **Formal Specification** @@ -651,8 +710,8 @@ query directQueryOnObjectWithSubFields { ## Arguments -Arguments are provided to both fields and directives. The following validation -rules apply in both cases. +Arguments are provided to fields, fragment spreads and directives. The following +validation rules apply in each case. ### Argument Names @@ -660,8 +719,9 @@ rules apply in both cases. - For each {argument} in the document: - Let {argumentName} be the Name of {argument}. - - Let {argumentDefinition} be the argument definition provided by the parent - field or definition named {argumentName}. + - Let {argumentDefinition} be the argument or variable definition named + {argumentName} provided by the parent field definition, directive definition + or fragment spread. - {argumentDefinition} must exist. **Explanatory Text** @@ -681,7 +741,22 @@ fragment argOnOptional on Dog { } ``` -the following is invalid since `command` is not defined on `DogCommand`. +The above is also applicable to fragment definitions and fragment spreads, each +variable must be defined by the fragment definition before it can be inserted as +an argument by the fragment spread. + +```graphql example +fragment withFragmentArg($command: DogCommand) on Dog { + doesKnowCommand(dogCommand: $command) +} + +fragment usesFragmentArg on Dog { + ...withFragmentArg(command: DOWN) +} +``` + +The following is invalid since `command` is not defined on +`Dog.doesKnowCommand`. ```graphql counter-example fragment invalidArgName on Dog { @@ -689,6 +764,19 @@ fragment invalidArgName on Dog { } ``` +and this is also invalid as the argument `dogCommand` is not defined on fragment +`withFragmentArg`. + +```graphql counter-example +fragment invalidFragmentArgName on Dog { + ...withFragmentArg(dogCommand: SIT) +} + +fragment withFragmentArg($command: DogCommand) on Dog { + doesKnowCommand(dogCommand: $command) +} +``` + and this is also invalid as `unless` is not defined on `@include`. ```graphql counter-example @@ -731,9 +819,9 @@ fragment multipleArgsReverseOrder on Arguments { ### Argument Uniqueness -Fields and directives treat arguments as a mapping of argument name to value. -More than one argument with the same name in an argument set is ambiguous and -invalid. +Fields, fragment spreads and directives treat arguments as a mapping of argument +name to value. More than one argument with the same name in an argument set is +ambiguous and invalid. **Formal Specification** @@ -745,10 +833,11 @@ invalid. ### Required Arguments -- For each Field or Directive in the document: - - Let {arguments} be the arguments provided by the Field or Directive. +- For each Field, Fragment Spread or Directive in the document: + - Let {arguments} be the arguments provided by the Field, Directive or + Fragment Spread. - Let {argumentDefinitions} be the set of argument definitions of that Field - or Directive. + or Directive, or the variable definitions of that Fragment. - For each {argumentDefinition} in {argumentDefinitions}: - Let {type} be the expected type of {argumentDefinition}. - Let {defaultValue} be the default value of {argumentDefinition}. @@ -1527,18 +1616,19 @@ query ($foo: Boolean = true, $bar: Boolean = false) { **Formal Specification** -- For every {operation} in the document: - - For every {variable} defined on {operation}: +- For every {operation} and {fragment} in the document: + - Let {operationOrFragment} be that {operation} or {fragment}. + - For every {variable} defined on {operationOrFragment}: - Let {variableName} be the name of {variable}. - Let {variables} be the set of all variables named {variableName} on - {operation}. + {operationOrFragment}. - {variables} must be a set of one. **Explanatory Text** -If any operation defines more than one variable with the same name, it is -ambiguous and invalid. It is invalid even if the type of the duplicate variable -is the same. +If any operation or fragment defines more than one variable with the same name, +it is ambiguous and invalid. It is invalid even if the type of the duplicate +variable is the same. ```graphql counter-example query houseTrainedQuery($atOtherHomes: Boolean, $atOtherHomes: Boolean) { @@ -1567,12 +1657,42 @@ fragment HouseTrainedFragment on Query { } ``` +Likewise, it is valid for a fragment to define a variable with a name that is +also defined on an operation: + +```graphql example +query C($atOtherHomes: Boolean) { + ...HouseTrainedFragment + aDog: dog { + ...HouseTrainedDog + } +} + +fragment HouseTrainedFragment on Query { + dog { + isHouseTrained(atOtherHomes: $atOtherHomes) + } +} + +fragment HouseTrainedDog($atOtherHomes: Boolean) on Dog { + isHouseTrained(atOtherHomes: $atOtherHomes) +} +``` + +Fragment-defined variables are scoped locally to the fragment that defines them, +and override any operation-defined variable values, so there is never ambiguity +about which value to use. In this case, the value of the argument `atOtherHomes` +within `HouseTrainedFragment` will be the operation-set value, and within +`HouseTrainedDog` will default to being unset (unless a default-value applies), +as the argument is not set by the fragment spread in the query `C`. + ### Variables Are Input Types **Formal Specification** -- For every {operation} in a {document}: - - For every {variable} on each {operation}: +- For every {operation} and {fragment} in a {document}: + - Let {operationOrFragment} be that {operation} or {fragment}. + - For every {variable} defined on {operationOrFragment}: - Let {variableType} be the type of {variable}. - {IsInputType(variableType)} must be {true}. @@ -1640,13 +1760,14 @@ query takesCatOrDog($catOrDog: CatOrDog) { transitively. - For each {fragment} in {fragments}: - For each {variableUsage} in scope of {fragment}, variable must be in - {operation}'s variable list. + either {fragment}'s or {operation}'s variable list or both. **Explanatory Text** -Variables are scoped on a per-operation basis. That means that any variable used -within the context of an operation must be defined at the top level of that -operation +Operation-defined Variables are scoped on a per-operation basis, while +Fragment-defined Variables are scoped locally to the fragment. That means that +any variable used within the context of an operation must either be defined at +the top level of that operation or on the fragment that uses that variable. For example: @@ -1673,9 +1794,10 @@ query variableIsNotDefined { ${atOtherHomes} is not defined by the operation. Fragments complicate this rule. Any fragment transitively included by an -operation has access to the variables defined by that operation. Fragments can -appear within multiple operations and therefore variable usages must correspond -to variable definitions in all of those operations. +operation has access to the variables defined by that operation and also those +defined on the fragment. Fragments can appear within multiple operations and +therefore variable usages not defined on the fragment must correspond to +variable definitions in all of those operations. For example the following is valid: @@ -1780,7 +1902,12 @@ included in that operation. - Let {variables} be the variables defined by that {operation}. - Each {variable} in {variables} must be used at least once in either the operation scope itself or any fragment transitively referenced by that - operation. + operation, excluding fragments that define the same name as an argument. +- For every {fragment} in the document: + - Let {variables} be the variables defined by that {fragment}. + - Each {variable} in {variables} must be used at least once transitively + within the fragment's selection set excluding traversal of named fragment + spreads. **Explanatory Text** @@ -1832,6 +1959,30 @@ 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. + +As such, it would be invalid if the operation defined a variable and variables +of that name were used exclusively inside fragments that define a variable with +the same name: + +```graphql counter-example +query variableNotUsedWithinFragment($atOtherHomes: Boolean) { + dog { + ...shadowedVariableFragment + } +} + +fragment shadowedVariableFragment($atOtherHomes: Boolean) on Dog { + isHouseTrained(atOtherHomes: $atOtherHomes) +} +``` + +because +{$atOtherHomes} is only referenced in a fragment that defines it as a +locally scoped argument, the operation-defined {$atOtherHomes} +variable is never used. + All operations in a document must use all of their variables. As a result, the following document does not validate. @@ -1857,6 +2008,24 @@ fragment isHouseTrainedFragment on Dog { This document is not valid because {queryWithExtraVar} defines an extraneous variable. +Fragment variables must also be used within their definitions. For example, the +following is invalid: + +```graphql counter-example +query queryWithFragmentArgUnused($atOtherHomes: Boolean) { + dog { + ...fragmentArgUnused(atOtherHomes: $atOtherHomes) + } +} + +fragment fragmentArgUnused($atOtherHomes: Boolean) on Dog { + isHouseTrained +} +``` + +This document is invalid: fragment `fragmentArgUnused` defines a fragment +variable `$atOtherHomes`, but this variable is not used within this fragment. + ### All Variable Usages Are Allowed **Formal Specification** @@ -1865,8 +2034,12 @@ variable. - Let {variableUsages} be all usages transitively included in the {operation}. - For each {variableUsage} in {variableUsages}: - Let {variableName} be the name of {variableUsage}. - - Let {variableDefinition} be the {VariableDefinition} named {variableName} - defined within {operation}. + - If the usage is within a {fragment} that defines a {variableDefinition} + for {variableName}: + - Let {variableDefinition} be the {VariableDefinition} named + {variableName} defined within {fragment}. + - Otherwise, let {variableDefinition} be the {VariableDefinition} named + {variableName} defined within {operation}. - {IsVariableUsageAllowed(variableDefinition, variableUsage)} must be {true}. diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 5b8594e30..fd3aa7caf 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -343,11 +343,13 @@ ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): - For each {groupedFieldSet} as {responseKey} and {fields}: - Let {fieldName} be the name of the first entry in {fields}. Note: This value is unaffected if an alias is used. + - Let {fragmentVariableValues} be the fragment-variables value of the first + entry in {fields}. - Let {fieldType} be the return type defined for the field {fieldName} of {objectType}. - If {fieldType} is defined: - Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, - fields, variableValues)}. + fields, variableValues, fragmentVariableValues)}. - Set {responseValue} as the value for {responseKey} in {resultMap}. - Return {resultMap}. @@ -495,7 +497,8 @@ The depth-first-search order of the field groups produced by {CollectFields()} is maintained through execution, ensuring that fields appear in the executed response in a stable and predictable order. -CollectFields(objectType, selectionSet, variableValues, visitedFragments): +CollectFields(objectType, selectionSet, variableValues, visitedFragments, +localVariableValues): - If {visitedFragments} is not provided, initialize it to the empty set. - Initialize {groupedFields} to an empty ordered map of lists. @@ -503,32 +506,34 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): - If {selection} provides the directive `@skip`, let {skipDirective} be that directive. - If {skipDirective}'s {if} argument is {true} or is a variable in - {variableValues} with the value {true}, continue with the next {selection} - in {selectionSet}. + {localVariableValues} or {variableValues} with the value {true}, continue + with the next {selection} in {selectionSet}. - If {selection} provides the directive `@include`, let {includeDirective} be that directive. - If {includeDirective}'s {if} argument is not {true} and is not a variable - in {variableValues} with the value {true}, continue with the next - {selection} in {selectionSet}. + in {localVariableValues} or {variableValues} with the value {true}, + continue with the next {selection} in {selectionSet}. - If {selection} is a {Field}: - Let {responseKey} be the response key of {selection} (the alias if defined, otherwise the field name). - Let {groupForResponseKey} be the list in {groupedFields} for {responseKey}; if no such list exists, create it as an empty list. - - Append {selection} to the {groupForResponseKey}. + - Append {selection} and {localVariableValues} to the {groupForResponseKey}. - If {selection} is a {FragmentSpread}: - Let {fragmentSpreadName} be the name of {selection}. - - If {fragmentSpreadName} is in {visitedFragments}, continue with the next - {selection} in {selectionSet}. - - Add {fragmentSpreadName} to {visitedFragments}. - Let {fragment} be the Fragment in the current Document whose name is {fragmentSpreadName}. - If no such {fragment} exists, continue with the next {selection} in {selectionSet}. + - If {fragmentSpreadName} is in {visitedFragments}, continue with the next + {selection} in {selectionSet}. + - Add {fragmentSpreadName} to {visitedFragments}. - Let {fragmentType} be the type condition on {fragment}. - If {DoesFragmentTypeApply(objectType, fragmentType)} is {false}, continue with the next {selection} in {selectionSet}. - - Let {fragmentSelectionSet} be the top-level selection set of {fragment}. + - Let {localVariableValues} be the result of calling + {getArgumentValuesFromSpread(selection, fragmentDefinition, + variableValues, localVariableValues)}. - Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, visitedFragments)}. @@ -567,6 +572,29 @@ DoesFragmentTypeApply(objectType, fragmentType): - If {objectType} is a possible type of {fragmentType}, return {true} otherwise return {false}. +getArgumentValuesFromSpread(fragmentSpread, fragmentDefinition, variableValues, +fragmentArgumentValues): + +- Let {coercedValues} be an empty unordered Map. +- For each {variableDefinition} in {fragmentDefinition}: + - Let {variableName} be the name of {variableDefinition}. + - Let {variableType} be the type of {variableDefinition}. + - Let {defaultValue} be the default value for {variableDefinition}. + - Let {argumentNode} be the node provided in the fragment-spread for + {variableName} + - If {argumentNode} isn't present or is null + - If {defaultValue} exists + - Add an entry to {coercedValues} named {argumentName} with the value + {defaultValue}. + - If {variableType} is non-nullable raise a field-error + - Let {hasValue} be {true} if {fragmentArgumentValues} or {variableValues} + provides a value for the name {variableName}. + - If {variableType} is non-nullable and {hasValue} is {false} raise a + field-error + - Add an entry to {coercedValues} named {argumentName} with the value found in + {variableValues} or {fragmentArgumentValues}. +- Return {coercedValues}. + Note: The steps in {CollectFields()} evaluating the `@skip` and `@include` directives may be applied in either order since they apply commutatively. @@ -578,12 +606,13 @@ coerces any provided argument values, then resolves a value for the field, and finally completes that value either by recursively executing another selection set or coercing a scalar value. -ExecuteField(objectType, objectValue, fieldType, fields, variableValues): +ExecuteField(objectType, objectValue, fieldType, fields, variableValues, +fragmentVariableValues): - Let {field} be the first entry in {fields}. - Let {fieldName} be the field name of {field}. -- Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, - variableValues)}. +- Let {argumentValues} be the result of {CoerceFieldArgumentValues(objectType, + field, variableValues, fragmentVariableValues)} - Let {resolvedValue} be {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. - Return the result of {CompleteValue(fieldType, fields, resolvedValue, @@ -598,13 +627,19 @@ the type system to have a specific input type. At each argument position in an operation may be a literal {Value}, or a {Variable} to be provided at runtime. -CoerceArgumentValues(objectType, field, variableValues): +CoerceFieldArgumentValues(objectType, field, variableValues, +fragmentVariableValues): -- Let {coercedValues} be an empty unordered Map. - Let {argumentValues} be the argument values provided in {field}. - Let {fieldName} be the name of {field}. - Let {argumentDefinitions} be the arguments defined by {objectType} for the field named {fieldName}. +- Return {CoerceArgumentValues(argumentDefinitions, argumentValues, + variableValues, fragmentVariableValues)} + +CoerceArgumentValues(argumentDefinitions, argumentValues, variableValues, +fragmentVariableValues): + - For each {argumentDefinition} in {argumentDefinitions}: - Let {argumentName} be the name of {argumentDefinition}. - Let {argumentType} be the expected type of {argumentDefinition}. @@ -615,6 +650,10 @@ CoerceArgumentValues(objectType, field, variableValues): {argumentName}. - If {argumentValue} is a {Variable}: - Let {variableName} be the name of {argumentValue}. + - Let {hasValue} be {true} if {fragmentVariableValues} provides a value for + the name {variableName}. + - Let {value} be the value provided in {fragmentVariableValues} for the name + {variableName}. - Let {hasValue} be {true} if {variableValues} provides a value for the name {variableName}. - Let {value} be the value provided in {variableValues} for the name