diff --git a/spec/Appendix B -- Grammar Summary.md b/spec/Appendix B -- Grammar Summary.md index 2291ee35f..446e9c04d 100644 --- a/spec/Appendix B -- Grammar Summary.md +++ b/spec/Appendix B -- Grammar Summary.md @@ -168,15 +168,20 @@ 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 FragmentArgumentsDefinition? +TypeCondition Directives? SelectionSet FragmentName : Name but not `on` +FragmentArgumentsDefinition : ( FragmentArgumentDefinition+ ) + +FragmentArgumentDefinition : Description? Variable : Type DefaultValue? +Directives[Const]? + TypeCondition : on NamedType Value[Const] : @@ -396,6 +401,7 @@ ExecutableDirectiveLocation : one of - `FRAGMENT_SPREAD` - `INLINE_FRAGMENT` - `VARIABLE_DEFINITION` +- `FRAGMENT_ARGUMENT_DEFINITION` TypeSystemDirectiveLocation : one of diff --git a/spec/Section 2 -- Language.md b/spec/Section 2 -- Language.md index 1aca650a8..270510314 100644 --- a/spec/Section 2 -- Language.md +++ b/spec/Section 2 -- Language.md @@ -516,10 +516,10 @@ which returns the result: ## Fragments -FragmentSpread : ... FragmentName Directives? +FragmentSpread : ... FragmentName Arguments? Directives? -FragmentDefinition : fragment FragmentName TypeCondition Directives? -SelectionSet +FragmentDefinition : fragment FragmentName FragmentArgumentsDefinition? +TypeCondition Directives? SelectionSet FragmentName : Name but not `on` @@ -1209,13 +1209,76 @@ 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 with a given operation, so a variable used within a fragment must either +be declared in any top-level operation that transitively consumes that fragment, +or by that same fragment as a fragment argument. If a variable is referenced in +a fragment that does not define it as an argument and is included by an +operation that does not define that variable, that operation is invalid (see [All Variable Uses Defined](#sec-All-Variable-Uses-Defined)). +## Fragment Arguments + +FragmentArgumentsDefinition : ( FragmentArgumentDefinition+ ) + +FragmentArgumentDefinition : Description? Variable : Type DefaultValue? +Directives[Const]? + +Fragments may define locally scoped arguments, which can be used in locations +that accept 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 withFragmentArguments { + 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 argument is scoped to the fragment that defines it. Fragment +arguments are allowed to shadow operation 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 +argument `size`. + ## Type References Type : diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index cce64b857..bc47d9f32 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1876,6 +1876,7 @@ ExecutableDirectiveLocation : one of - `FRAGMENT_SPREAD` - `INLINE_FRAGMENT` - `VARIABLE_DEFINITION` +- `FRAGMENT_ARGUMENT_DEFINITION` TypeSystemDirectiveLocation : one of diff --git a/spec/Section 4 -- Introspection.md b/spec/Section 4 -- Introspection.md index 3054a9f6c..522b5928a 100644 --- a/spec/Section 4 -- Introspection.md +++ b/spec/Section 4 -- Introspection.md @@ -206,6 +206,7 @@ enum __DirectiveLocation { FRAGMENT_SPREAD INLINE_FRAGMENT VARIABLE_DEFINITION + FRAGMENT_ARGUMENT_DEFINITION SCHEMA SCALAR OBJECT @@ -475,6 +476,7 @@ supported. All possible locations are listed in the `__DirectiveLocation` enum: - {"FRAGMENT_SPREAD"} - {"INLINE_FRAGMENT"} - {"VARIABLE_DEFINITION"} +- {"FRAGMENT_ARGUMENT_DEFINITION"} - {"SCHEMA"} - {"SCALAR"} - {"OBJECT"} diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index dceec126b..24e850875 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -418,8 +418,14 @@ fragment directFieldSelectionOnUnion on CatOrDog { FieldsInSetCanMerge(set): +- Let {visitedSelections} be the selections in {set} including visiting + fragments and inline fragments an applying any supplied fragment arguments. +- Let {spreadsForName} be the set of fragment spreads with a given name in + {visitedSelections}. +- Given each pair of members {spreadA} and {spreadB} in {spreadsForName}: + - {spreadA} and {spreadB} must have identical sets of arguments. - Let {fieldsForName} be the set of selections with a given response name in - {set} including visiting fragments and inline fragments. + {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 @@ -570,6 +576,50 @@ fragment conflictingDifferingResponses on Pet { } ``` +Fragment arguments can also cause fields to fail to merge. + +While the following is valid: + +```graphql example +fragment commandFragment($command: DogCommand!) on Dog { + doesKnowCommand(dogCommand: $command) +} + +fragment potentiallyConflictingArguments( + $commandOne: DogCommand! + $commandTwo: DogCommand! +) on Dog { + ...commandFragment(command: $commandOne) + ...commandFragment(command: $commandTwo) +} + +fragment safeFragmentArguments on Dog { + ...potentiallyConflictingArguments(commandOne: SIT, commandTwo: SIT) +} +``` + +it is only valid because `safeFragmentArguments` uses +`potentiallyConflictingArguments` with the same value for `commandOne` and +`commandTwo`. Therefore `commandFragment` resolves `doesKnowCommand`'s +`dogCommand:` arg to `SIT` in both cases. + +However, by changing the argument values: + +```graphql counter-example +fragment conflictingFragmentArguments on Dog { + ...potentiallyConflictingArguments(commandOne: SIT, commandTwo: DOWN) +} +``` + +the response will have two conflicting versions of the `doesKnowCommand` +fragment that cannot merge. + +If two fragment spreads with the same name 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. + ### Leaf Field Selections **Formal Specification** @@ -647,8 +697,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 @@ -657,7 +707,7 @@ 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}. + field, fragment definition or directive definition named {argumentName}. - {argumentDefinition} must exist. **Explanatory Text** @@ -675,9 +725,18 @@ fragment argOnRequiredArg on Dog { fragment argOnOptional on Dog { isHouseTrained(atOtherHomes: true) @include(if: true) } + +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 `DogCommand`. +The following is invalid since `command` is not defined on +`Dog.doesKnowCommand`. ```graphql counter-example fragment invalidArgName on Dog { @@ -685,6 +744,15 @@ 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) +} +``` + and this is also invalid as `unless` is not defined on `@include`. ```graphql counter-example @@ -727,9 +795,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** @@ -741,10 +809,11 @@ invalid. #### Required Arguments -- For each Field or Directive in the document: - - Let {arguments} be the arguments provided by the Field or Directive. - - Let {argumentDefinitions} be the set of argument definitions of that Field - or Directive. +- For each Field, Fragment Spread or Directive in the document: + - Let {arguments} be the arguments provided by the Field, Fragment Spread or + Directive. + - Let {argumentDefinitions} be the set of argument definitions of that Field, + Fragment Spread or Directive. - For each {argumentDefinition} in {argumentDefinitions}: - Let {type} be the expected type of {argumentDefinition}. - Let {defaultValue} be the default value of {argumentDefinition}. @@ -1776,7 +1845,7 @@ 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. **Explanatory Text** @@ -1828,6 +1897,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. + +Likewise, it would be invalid if the variable was shadowed by a fragment +argument: + +```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. @@ -1853,6 +1945,40 @@ fragment isHouseTrainedFragment on Dog { This document is not valid because {queryWithExtraVar} defines an extraneous variable. +### All Fragment Arguments Used + +**Formal Specification** + +- For every {fragment} in the document: + - Let {arguments} be the arguments defined by that {fragment}. + - Each {argument} in {arguments} must be used at least once in the fragment's + scope. + +**Explanatory Text** + +All arguments defined by a fragment must be used in that same fragment. Because +fragment arguments are scoped to the fragment they are defined on, if the +fragment does not use the argument, then the argument is superfluous. + +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 because even though `fragmentArgUnused` is spread with +the argument `atOtherHomes`, and even though `$atOtherHomes` is defined as an +operation variable, there is never a variable `$atOtherHomes` used within the +scope of `fragmentArgUnused`. + ### All Variable Usages Are Allowed **Formal Specification** @@ -1861,8 +1987,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 an argument of + {variableName}: + - Let {variableDefinition} be the {ArgumentDefinition} 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 28862ea89..ee97c5cb2 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -332,10 +332,11 @@ First, the selection set is turned into a grouped field set; then, each represented field in the grouped field set produces an entry into a response map. -ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): +ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues, +argumentValues): - Let {groupedFieldSet} be the result of {CollectFields(objectType, - selectionSet, variableValues)}. + selectionSet, variableValues, argumentValues)}. - Initialize {resultMap} to an empty ordered map. - For each {groupedFieldSet} as {responseKey} and {fields}: - Let {fieldName} be the name of the first entry in {fields}. Note: This value @@ -344,7 +345,7 @@ ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): {objectType}. - If {fieldType} is defined: - Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, - fields, variableValues)}. + fields, variableValues, argumentValues)}. - Set {responseValue} as the value for {responseKey} in {resultMap}. - Return {resultMap}. @@ -490,7 +491,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, argumentValues, +visitedFragments): - If {visitedFragments} is not provided, initialize it to the empty set. - Initialize {groupedFields} to an empty ordered map of lists. @@ -513,20 +515,24 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): - Append {selection} 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}. + - Let {spreadArgumentValues} be the result of calling + {ArgumentsFromSpread(selection, fragment, variableValues, argumentValues)} + - Let {fragmentSpreadKey} be a unique key of {fragmentSpreadName} and + {spreadArgumentValues}. + - If {fragmentSpreadKey} is in {visitedFragments}, continue with the next + {selection} in {selectionSet}. + - Add {fragmentSpreadKey} 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 {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments)}. + spreadArgumentValues, visitedFragments)}. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. @@ -541,7 +547,7 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): - Let {fragmentSelectionSet} be the top-level selection set of {selection}. - Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments)}. + argumentValues, visitedFragments)}. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. @@ -550,6 +556,14 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): - Append all items in {fragmentGroup} to {groupForResponseKey}. - Return {groupedFields}. +ArgumentsFromSpread(fragmentSpread, fragment, variableValues, +parentArgumentValues): + +- Let {argumentDefinitions} be the arguments defined on {fragment} +- Let {spreadArguments} be the arguments set on {fragmentSpread} +- Return the result of {CoerceArgumentValues(argumentDefinitions, + spreadArguments, variableValues, parentArgumentValues)} + DoesFragmentTypeApply(objectType, fragmentType): - If {fragmentType} is an Object Type: @@ -573,16 +587,17 @@ 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, +fragmentArgumentValues): - 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, fragmentArgumentValues)} - Let {resolvedValue} be {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. - Return the result of {CompleteValue(fieldType, fields, resolvedValue, - variableValues)}. + variableValues, fragmentArgumentValues)}. ### Coercing Field Arguments @@ -593,13 +608,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, +fragmentArgumentValues): -- 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, fragmentArgumentValues)} + +CoerceArgumentValues(argumentDefinitions, argumentValues, variableValues, +fragmentArgumentValues): + - For each {argumentDefinition} in {argumentDefinitions}: - Let {argumentName} be the name of {argumentDefinition}. - Let {argumentType} be the expected type of {argumentDefinition}. @@ -610,10 +631,15 @@ CoerceArgumentValues(objectType, field, variableValues): {argumentName}. - If {argumentValue} is a {Variable}: - Let {variableName} be the name of {argumentValue}. - - Let {hasValue} be {true} if {variableValues} provides a value for the name - {variableName}. - - Let {value} be the value provided in {variableValues} for the name - {variableName}. + - If {fragmentArgumentValues} provides a value for the name {variableName}: + - Let {hasValue} be {true}. + - Let {value} be the value provided in {fragmentArgumentValues} for the + name {variableName}. + - Otherwise if {variableValues} provides a value for the name + {variableName}: + - Let {hasValue} be {true}. + - Let {value} be the value provided in {variableValues} for the name + {variableName}. - Otherwise, let {value} be {argumentValue}. - If {hasValue} is not {true} and {defaultValue} exists (including {null}): - Add an entry to {coercedValues} named {argumentName} with the value @@ -669,12 +695,12 @@ After resolving the value for a field, it is completed by ensuring it adheres to the expected return type. If the return type is another Object type, then the field execution process continues recursively. -CompleteValue(fieldType, fields, result, variableValues): +CompleteValue(fieldType, fields, result, variableValues, argumentValues): - If the {fieldType} is a Non-Null type: - Let {innerType} be the inner type of {fieldType}. - Let {completedResult} be the result of calling {CompleteValue(innerType, - fields, result, variableValues)}. + fields, result, variableValues, argumentValues)}. - If {completedResult} is {null}, raise a _field error_. - Return {completedResult}. - If {result} is {null} (or another internal value similar to {null} such as @@ -683,8 +709,8 @@ CompleteValue(fieldType, fields, result, variableValues): - If {result} is not a collection of values, raise a _field error_. - Let {innerType} be the inner type of {fieldType}. - Return a list where each list item is the result of calling - {CompleteValue(innerType, fields, resultItem, variableValues)}, where - {resultItem} is each item in {result}. + {CompleteValue(innerType, fields, resultItem, variableValues, + argumentValues)}, where {resultItem} is each item in {result}. - If {fieldType} is a Scalar or Enum type: - Return the result of {CoerceResult(fieldType, result)}. - If {fieldType} is an Object, Interface, or Union type: @@ -694,8 +720,8 @@ CompleteValue(fieldType, fields, result, variableValues): - Let {objectType} be {ResolveAbstractType(fieldType, result)}. - Let {subSelectionSet} be the result of calling {MergeSelectionSets(fields)}. - Return the result of evaluating {ExecuteSelectionSet(subSelectionSet, - objectType, result, variableValues)} _normally_ (allowing for - parallelization). + objectType, result, variableValues, argumentValues)} _normally_ (allowing + for parallelization). **Coercing Results**