diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index cf0cd42b7..9d6360366 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2126,10 +2126,7 @@ scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") ### @defer ```graphql -directive @defer( - label: String - if: Boolean! = true -) on FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @defer(if: Boolean! = true) on FRAGMENT_SPREAD | INLINE_FRAGMENT ``` The `@defer` directive may be provided for fragment spreads and inline fragments @@ -2144,7 +2141,7 @@ delivered in a subsequent response. `@include` and `@skip` take precedence over query myQuery($shouldDefer: Boolean) { user { name - ...someFragment @defer(label: "someLabel", if: $shouldDefer) + ...someFragment @defer(if: $shouldDefer) } } fragment someFragment on User { @@ -2161,20 +2158,11 @@ fragment someFragment on User { [related note](#note-088b7)). When `false`, fragment will not be deferred and data will be included in the initial response. Defaults to `true` when omitted. -- `label: String` - May be used by GraphQL clients to identify the data from - responses and associate it with the corresponding defer directive. If - provided, the GraphQL service must add it to the corresponding payload. - `label` must be unique label across all `@defer` and `@stream` directives in a - document. `label` must not be provided as a variable. ### @stream ```graphql -directive @stream( - label: String - if: Boolean! = true - initialCount: Int = 0 -) on FIELD +directive @stream(if: Boolean! = true, initialCount: Int = 0) on FIELD ``` The `@stream` directive may be provided for a field of `List` type so that the @@ -2186,7 +2174,7 @@ responses. `@include` and `@skip` take precedence over `@stream`. query myQuery($shouldStream: Boolean) { user { friends(first: 10) { - nodes @stream(label: "friendsStream", initialCount: 5, if: $shouldStream) + nodes @stream(initialCount: 5, if: $shouldStream) } } } @@ -2198,11 +2186,6 @@ query myQuery($shouldStream: Boolean) { [related note](#note-088b7)). When `false`, the field will not be streamed and all list items will be included in the initial response. Defaults to `true` when omitted. -- `label: String` - May be used by GraphQL clients to identify the data from - responses and associate it with the corresponding stream directive. If - provided, the GraphQL service must add it to the corresponding payload. - `label` must be unique label across all `@defer` and `@stream` directives in a - document. `label` must not be provided as a variable. - `initialCount: Int` - The number of list items the service should return as part of the initial response. If omitted, defaults to `0`. A field error will be raised if the value of this argument is less than `0`. diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 1e8fbd6d7..678ef379d 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -474,7 +474,7 @@ unambiguous. Therefore any two field selections which might both be encountered for the same object are only valid if they are equivalent. During execution, the simultaneous execution of fields with the same response -name is accomplished by {MergeSelectionSets()} and {CollectFields()}. +name is accomplished by {CollectFields()}. For simple hand-written GraphQL, this rule is obviously a clear developer error, however nested fragments can make this difficult to detect manually. @@ -1605,71 +1605,6 @@ subscription sub { } ``` -### Defer And Stream Directive Labels Are Unique - -** Formal Specification ** - -- Let {labelValues} be an empty set. -- For every {directive} in the document: - - Let {directiveName} be the name of {directive}. - - If {directiveName} is "defer" or "stream": - - For every {argument} in {directive}: - - Let {argumentName} be the name of {argument}. - - Let {argumentValue} be the value passed to {argument}. - - If {argumentName} is "label": - - {argumentValue} must not be a variable. - - {argumentValue} must not be present in {labelValues}. - - Append {argumentValue} to {labelValues}. - -**Explanatory Text** - -The `@defer` and `@stream` directives each accept an argument "label". This -label may be used by GraphQL clients to uniquely identify response payloads. If -a label is passed, it must not be a variable and it must be unique within all -other `@defer` and `@stream` directives in the document. - -For example the following document is valid: - -```graphql example -{ - dog { - ...fragmentOne - ...fragmentTwo @defer(label: "dogDefer") - } - pets @stream(label: "petStream") { - name - } -} - -fragment fragmentOne on Dog { - name -} - -fragment fragmentTwo on Dog { - owner { - name - } -} -``` - -For example, the following document will not pass validation because the same -label is used in different `@defer` and `@stream` directives.: - -```raw graphql counter-example -{ - dog { - ...fragmentOne @defer(label: "MyLabel") - } - pets @stream(label: "MyLabel") { - name - } -} - -fragment fragmentOne on Dog { - name -} -``` - ### Stream Directives Are Used On List Fields **Formal Specification** diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index c84b86b5e..deff3f335 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -133,14 +133,23 @@ An initial value may be provided when executing a query operation. ExecuteQuery(query, schema, variableValues, initialValue): - Let {subsequentPayloads} be an empty list. +- Initialize {branches} to the empty set. +- Initialize {streams} to the empty set. - Let {queryType} be the root Query type in {schema}. - Assert: {queryType} is an Object type. - Let {selectionSet} be the top level Selection Set in {query}. -- Let {data} be the result of running {ExecuteSelectionSet(selectionSet, - queryType, initialValue, variableValues, subsequentPayloads)} _normally_ - (allowing parallelization). +- Let {groupedFieldSet} and {newDeferDepth} be the result of + {CollectRootFields(queryType, selectionSet, variableValues)}. +- Let {data} be the result of running {ExecuteGroupedFieldSet(groupedFieldSet, + queryType, initialValue, variableValues, subsequentPayloads, branches, + streams)} _normally_ (allowing parallelization). - Let {errors} be the list of all _field error_ raised while executing the selection set. +- If {newDeferDepth} is defined and {ShouldBranch(branches, groupedFieldSet, + path)} is {true}: + - Call {ExecuteDeferredFragment(objectType, objectValue, groupedFieldSet, + path, newDeferDepth, variableValues, asyncRecord, subsequentPayloads, + branches, streams)} - If {subsequentPayloads} is empty: - Return an unordered map containing {data} and {errors}. - If {subsequentPayloads} is not empty: @@ -168,13 +177,23 @@ mutations ensures against race conditions during these side-effects. ExecuteMutation(mutation, schema, variableValues, initialValue): - Let {subsequentPayloads} be an empty list. +- Initialize {branches} to the empty set. +- Initialize {streams} to the empty set. - Let {mutationType} be the root Mutation type in {schema}. - Assert: {mutationType} is an Object type. - Let {selectionSet} be the top level Selection Set in {mutation}. -- Let {data} be the result of running {ExecuteSelectionSet(selectionSet, - mutationType, initialValue, variableValues, subsequentPayloads)} _serially_. +- Let {groupedFieldSet} and {newDeferDepth} be the result of + {CollectRootFields(queryType, selectionSet, variableValues)}. +- Let {data} be the result of running {ExecuteGroupedFieldSet(groupedFieldSet, + mutationType, initialValue, variableValues, subsequentPayloads, branches, + streams)} _serially_. - Let {errors} be the list of all _field error_ raised while executing the selection set. +- If {newDeferDepth} is defined and {ShouldBranch(branches, groupedFieldSet, + path)} is {true}: + - Call {ExecuteDeferredFragment(objectType, objectValue, + deferredGroupFieldSet, path, newDeferDepth, variableValues, asyncRecord, + subsequentPayloads, branches, streams)} - If {subsequentPayloads} is empty: - Return an unordered map containing {data} and {errors}. - If {subsequentPayloads} is not empty: @@ -286,13 +305,13 @@ CreateSourceEventStream(subscription, schema, variableValues, initialValue): - Let {subscriptionType} be the root Subscription type in {schema}. - Assert: {subscriptionType} is an Object type. - Let {selectionSet} be the top level Selection Set in {subscription}. -- Let {groupedFieldSet} be the result of {CollectFields(subscriptionType, +- Let {groupedFieldSet} be the result of {CollectRootFields(subscriptionType, selectionSet, variableValues)}. - If {groupedFieldSet} does not have exactly one entry, raise a _request error_. -- Let {fields} be the value of the first entry in {groupedFieldSet}. -- Let {fieldName} be the name of the first entry in {fields}. Note: This value - is unaffected if an alias is used. -- Let {field} be the first entry in {fields}. +- Let {taggedField} be the value of the first entry in {groupedFieldSet}. +- Let {field} be the corresponding entry within {taggedField}. +- Let {fieldName} be the name of {field}. Note: This value is unaffected if an + alias is used. - Let {argumentValues} be the result of {CoerceArgumentValues(subscriptionType, field, variableValues)} - Let {fieldStream} be the result of running @@ -332,7 +351,9 @@ ExecuteSubscriptionEvent(subscription, schema, variableValues, initialValue): - Let {subscriptionType} be the root Subscription type in {schema}. - Assert: {subscriptionType} is an Object type. - Let {selectionSet} be the top level Selection Set in {subscription}. -- Let {data} be the result of running {ExecuteSelectionSet(selectionSet, +- Let {groupedFieldSet} be the result of {CollectRootFields(subscriptionType, + selectionSet, variableValues)}. +- Let {data} be the result of running {ExecuteGroupedFieldSet(groupedFieldSet, subscriptionType, initialValue, variableValues)} _normally_ (allowing parallelization). - Let {errors} be the list of all _field error_ raised while executing the @@ -400,37 +421,35 @@ YieldSubsequentPayloads(initialResponse, subsequentPayloads): {true}. - Yield {subsequentResponse} -## Executing Selection Sets +## Executing Grouped Field Sets -To execute a selection set, the object value being evaluated and the object type -need to be known, as well as whether it must be executed serially, or may be -executed in parallel. +To execute a grouped field set, the object value being evaluated and the object +type need to be known, as well as whether it must be executed serially, or may +be executed in parallel. -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. +Each represented field in the grouped field set produces an entry into a +response map. -ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues, path, -subsequentPayloads, asyncRecord): +ExecuteGroupedFieldSet(groupedFieldSet, objectType, objectValue, variableValues, +path, subsequentPayloads, branches, streams, asyncRecord): -- If {path} is not provided, initialize it to an empty list. +- If {path} is not provided, initialize it to an empty list, unique for this + execution. - If {subsequentPayloads} is not provided, initialize it to the empty set. -- Let {groupedFieldSet} and {deferredGroupedFieldsList} be the result of - {CollectFields(objectType, selectionSet, variableValues)}. - 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 - is unaffected if an alias is used. +- For each {groupedFieldSet} as {responseKey} and {fieldGroup}: + - Let {taggedField} be the value of the first entry of {fieldGroup}. + - Let {field} be the corresponding entry within {taggedField}. + - Let {fieldName} be the name of {field}. Note: This value is unaffected if an + alias is used. - 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, path, subsequentPayloads, asyncRecord)}. - - Set {responseValue} as the value for {responseKey} in {resultMap}. -- For each {deferredGroupFieldSet} and {label} in {deferredGroupedFieldsList} - - Call {ExecuteDeferredFragment(label, objectType, objectValue, - deferredGroupFieldSet, path, variableValues, asyncRecord, - subsequentPayloads)} + - If {ShouldExecute(fieldGroup, asyncRecord)} is {true}: + - Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, + fieldGroup, variableValues, path, subsequentPayloads, branches, streams, + asyncRecord)}. + - Set {responseValue} as the value for {responseKey} in {resultMap}. - Return {resultMap}. Note: {resultMap} is ordered by which fields appear first in the operation. This @@ -611,19 +630,37 @@ are no longer required to execute serially. Execution of the deferred or streamed sections of the subsection may be executed in parallel, as defined in {ExecuteStreamField} and {ExecuteDeferredFragment}. +#### Tagged Field Node + +A Tagged Field Record is a structure containing: + +- {field}: the underlying field from an operation. +- {depth}: the depth of the field. Root fields have a depth of 0, their + subfields have a depth of 1, and so on. The depth is used by the + {CollectFields} algorithm to assign a {deferDepth}. +- {deferDepth}: the depth of the closest enclosing fragment with a defer + directive, or undefined if the field is not deferred. + ### Field Collection Before execution, the selection set is converted to a grouped field set by calling {CollectFields()}. Each entry in the grouped field set is a list of -fields that share a response key (the alias if defined, otherwise the field -name). This ensures all fields with the same response key (including those in -referenced fragments) are executed at the same time. A deferred selection set's -fields will not be included in the grouped field set. Rather, a record -representing the deferred fragment and additional context will be stored in a -list. The executor revisits and resumes execution for the list of deferred -fragment records after the initial execution is initiated. This deferred -execution would ‘re-execute’ fields with the same response key that were present -in the grouped field set. +tagged field records that share a response key (the alias if defined, otherwise +the field name). This ensures all fields with the same response key (including +those in referenced fragments) are executed at the same time. + +A deferred selection set's fields will not be included in the initial response. +Rather, the executor revisits and resumes execution for each deferred fragment, +merging deferred fragments at the same depth. This deferred execution may +‘re-execute’ and ‘re-send’ fields with the same response key that were already +executed and/or sent. + +Note: The specification allows services to determine that it is more performant +to not ‘re-execute’ fields that sent within the initial payload, and even to +track and not ‘re-send’ any values that have already been sent. Therefore, +GraphQL clients _must_ be able to process incremental responses that are missing +values previously sent. Incremental responses should therefore be interpreted as +‘patches’ applied to the previously sent values. As an example, collecting the fields of this selection set would collect two instances of the field `a` and one of field `b`: @@ -648,13 +685,10 @@ 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, -deferredGroupedFieldsList): +CollectFields(objectType, selectionSet, variableValues, visitedFragments, depth, +deferDepth): -- If {visitedFragments} is not provided, initialize it to the empty set. -- Initialize {groupedFields} to an empty ordered map of lists. -- If {deferredGroupedFieldsList} is not provided, initialize it to an empty - list. +- Initialize {groupedFieldSet} to an empty ordered map of lists. - For each {selection} in {selectionSet}: - If {selection} provides the directive `@skip`, let {skipDirective} be that directive. @@ -667,11 +701,13 @@ deferredGroupedFieldsList): in {variableValues} with the value {true}, continue with the next {selection} in {selectionSet}. - If {selection} is a {Field}: + - Let {taggedField} be an empty tagged field record created from + {selection}, {depth}, and {deferDepth}. - Let {responseKey} be the response key of {selection} (the alias if defined, otherwise the field name). - - Let {groupForResponseKey} be the list in {groupedFields} for + - Let {groupForResponseKey} be the list in {groupedFieldSet} for {responseKey}; if no such list exists, create it as an empty list. - - Append {selection} to the {groupForResponseKey}. + - Append {taggedField} to the {groupForResponseKey}. - If {selection} is a {FragmentSpread}: - Let {fragmentSpreadName} be the name of {selection}. - If {fragmentSpreadName} provides the directive `@defer` and its {if} @@ -691,23 +727,21 @@ deferredGroupedFieldsList): - 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}. - If {deferDirective} is defined: - - Let {label} be the value or the variable to {deferDirective}'s {label} - argument. - - Let {deferredGroupedFields} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments, deferredGroupedFieldsList)}. - - Append a record containing {label} and {deferredGroupedFields} to - {deferredGroupedFieldsList}. - - Continue with the next {selection} in {selectionSet}. - - Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments, deferredGroupedFieldsList)}. + - Let {newDeferDepth} be depth. + - Let {fragmentDeferDepth} be {depth}. + - Otherwise: + - Let {fragmentDeferDepth} be {depthDepth}. + - Let {fragmentSelectionSet} be the top-level selection set of {fragment}. + - Let {fragmentGroupedFieldSet} and {fragmentNewDeferDepth} be the result of + calling {CollectFields(objectType, fragmentSelectionSet, variableValues, + visitedFragments, depth, fragmentDeferDepth)}. + - If {fragmentNewDeferDepth} is defined: + - Let {newDeferDepth} be depth. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. - - Let {groupForResponseKey} be the list in {groupedFields} for + - Let {groupForResponseKey} be the list in {groupedFieldSet} for {responseKey}; if no such list exists, create it as an empty list. - Append all items in {fragmentGroup} to {groupForResponseKey}. - If {selection} is an {InlineFragment}: @@ -723,24 +757,22 @@ deferredGroupedFieldsList): - If this execution is for a subscription operation, raise a _field error_. - If {deferDirective} is defined: - - Let {label} be the value or the variable to {deferDirective}'s {label} - argument. - - Let {deferredGroupedFields} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments, deferredGroupedFieldsList)}. - - Append a record containing {label} and {deferredGroupedFields} to - {deferredGroupedFieldsList}. - - Continue with the next {selection} in {selectionSet}. - - Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments, deferredGroupedFieldsList)}. + - Let {newDeferDepth} be depth. + - Let {fragmentDeferDepth} be {depth}. + - Otherwise: + - Let {fragmentDeferDepth} be {depthDepth}. + - Let {fragmentGroupedFieldSet} and {fragmentNewDeferDepth} be the result of + calling {CollectFields(objectType, fragmentSelectionSet, variableValues, + visitedFragments, depth, fragmentDeferDepth)}. + - If {fragmentNewDeferDepth} is defined: + - Let {newDeferDepth} be depth. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. - - Let {groupForResponseKey} be the list in {groupedFields} for + - Let {groupForResponseKey} be the list in {groupedFieldSet} for {responseKey}; if no such list exists, create it as an empty list. - Append all items in {fragmentGroup} to {groupForResponseKey}. -- Return {groupedFields}, {deferredGroupedFieldsList} and {visitedFragments}. +- Return {groupedFieldSet}, {newDeferDepth}, and {visitedFragments}. Note: The steps in {CollectFields()} evaluating the `@skip` and `@include` directives may be applied in either order since they apply commutatively. @@ -762,9 +794,11 @@ DoesFragmentTypeApply(objectType, fragmentType): An Async Payload Record is either a Deferred Fragment Record or a Stream Record. All Async Payload Records are structures containing: -- {label}: value derived from the corresponding `@defer` or `@stream` directive. - {path}: a list of field names and indices from root to the location of the corresponding `@defer` or `@stream` directive. +- {deferDepth}: a number corresponding to the depth along the path of any + enclosing fragment containing the defer directive, or undefined, if this + payload is a Stream Record not contained by a defer directive. - {iterator}: The underlying iterator if created from a `@stream` directive. - {isCompletedIterator}: a boolean indicating the payload record was generated from an iterator that has completed. @@ -774,22 +808,27 @@ All Async Payload Records are structures containing: #### Execute Deferred Fragment -ExecuteDeferredFragment(label, objectType, objectValue, groupedFieldSet, path, -variableValues, parentRecord, subsequentPayloads): +ExecuteDeferredFragment(objectType, objectValue, groupedFieldSet, path, +deferDepth, variableValues, parentRecord, subsequentPayloads, branches, +streams): -- Let {deferRecord} be an async payload record created from {label} and {path}. +- Let {deferRecord} be an async payload record created from {path} and + {deferDepth}. - Initialize {errors} on {deferRecord} to an empty list. - Let {dataExecution} be the asynchronous future value of: - Let {payload} be an unordered map. - 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 is unaffected if an alias is used. + - For each {groupedFieldSet} as {responseKey} and {fieldGroup}: + - Let {taggedField} be the value of the first entry of {fieldGroup}. + - Let {field} be the corresponding entry within {taggedField}. + - Let {fieldName} be the name of {field}. Note: This value is unaffected if + an alias is used. - 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, path, subsequentPayloads, asyncRecord)}. + fieldGroup, variableValues, path, subsequentPayloads, asyncRecord, + branches, streams)}. - Set {responseValue} as the value for {responseKey} in {resultMap}. - Append any encountered field errors to {errors}. - If {parentRecord} is defined: @@ -801,13 +840,50 @@ variableValues, parentRecord, subsequentPayloads): - Add an entry to {payload} named `data` with the value {null}. - Otherwise: - Add an entry to {payload} named `data` with the value {resultMap}. - - If {label} is defined: - - Add an entry to {payload} named `label` with the value {label}. - Add an entry to {payload} named `path` with the value {path}. - Return {payload}. - Set {dataExecution} on {deferredFragmentRecord}. - Append {deferRecord} to {subsequentPayloads}. +### Root Field Collection + +Root field collection processes the operation's top-level selection set: + +CollectRootFields(rootType, operationSelectionSet, variableValues): + +- Initialize {visitedFragments} to the empty set. +- Let {groupedFieldSet} and {newDeferDepth} be the result of calling + {CollectFields(rootType, operationSelectionSet, variableValues, + visitedFragments)}. +- Return {groupedFieldSet} and {newDeferDepth}. + +### Object Subfield Collection + +Object subfield collection processes a field's sub-selection sets: + +CollectSubfields(objectType, fieldGroup, variableValues): + +- Initialize {visitedFragments} to the empty set. +- Initialize {groupedSubfieldSet} to an empty ordered map of lists. +- For each {taggedField} in {fieldGroup}: + - Let {field} be the corresponding entry within {taggedField}. + - Let {fieldName} be the name of {field}. Note: This value is unaffected if an + alias is used. + - Let {fieldSelectionSet} be the selection set of {field}. + - If {fieldSelectionSet} is null or empty, continue to the next field. + - Let {fieldGroupedFieldSet} and {fieldNewDeferDepth} be the result of calling + {CollectFields(objectType, fragmentSelectionSet, variableValues, + visitedFragments)}. + - For each {fieldGroup} in {fieldGroupedFieldSet}: + - Let {responseKey} be the response key shared by all fields in + {fragmentGroup}. + - Let {groupForResponseKey} be the list in {groupedFieldSet} for + {responseKey}; if no such list exists, create it as an empty list. + - Append all items in {fieldGroup} to {groupForResponseKey}. + - If {fieldNewDeferDepth} is defined: + - Let {deferDepth} be {fieldNewDeferDepth}. +- Return {groupedSubfieldSet} and {deferDepth}. + ## Executing Fields Each field requested in the grouped field set that is defined on the selected @@ -816,18 +892,21 @@ 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, path, -subsequentPayloads, asyncRecord): +ExecuteField(objectType, objectValue, fieldType, fieldGroup, variableValues, +path, subsequentPayloads, branches, streams, asyncRecord): -- Let {field} be the first entry in {fields}. -- Let {fieldName} be the field name of {field}. -- Append {fieldName} to {path}. +- Let {taggedField} be the value of the first entry in {fieldGroup}. +- Let {field} be the corresponding entry within {taggedField}. +- Let {fieldName} be the name of {field}. +- Let {fieldPath} be a unique list for for this execution equal to {path} with + {fieldName} appended. - Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, variableValues)} - Let {resolvedValue} be {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. -- Let {result} be the result of calling {CompleteValue(fieldType, fields, - resolvedValue, variableValues, path, subsequentPayloads, asyncRecord)}. +- Let {result} be the result of calling {CompleteValue(fieldType, fieldGroup, + resolvedValue, variableValues, fieldPath, subsequentPayloads, branches, + streams, asyncRecord)}. - Return {result}. ### Coercing Field Arguments @@ -930,13 +1009,16 @@ yielded items satisfies `initialCount` specified on the `@stream` directive. #### Execute Stream Field -ExecuteStreamField(label, iterator, index, fields, innerType, path, -parentRecord, variableValues, subsequentPayloads): +ExecuteStreamField(iterator, index, fieldGroup, innerType, path, deferDepth, +parentRecord, variableValues, subsequentPayloads, branches, streams): -- Let {streamRecord} be an async payload record created from {label}, {path}, - and {iterator}. +- If {parentRecord} is defined: + - Let {deferDepth} be equal to the corresponding entry on {asyncRecord}. +- Let {streamRecord} be an async payload record created from {path}, + {deferDepth}, and {iterator}. - Initialize {errors} on {streamRecord} to an empty list. -- Let {itemPath} be {path} with {index} appended. +- Let {itemPath} be a unique list for this execution equal to {path} with + {index} appended. - Let {dataExecution} be the asynchronous future value of: - Wait for the next item from {iterator}. - If an item is not retrieved because {iterator} has completed: @@ -948,12 +1030,13 @@ parentRecord, variableValues, subsequentPayloads): - Add an entry to {payload} named `items` with the value {null}. - Otherwise: - Let {item} be the item retrieved from {iterator}. - - Let {data} be the result of calling {CompleteValue(innerType, fields, - item, variableValues, itemPath, subsequentPayloads, parentRecord)}. + - Let {data} be the result of calling {CompleteValue(innerType, fieldGroup, + item, variableValues, itemPath, subsequentPayloads, branches, streams, + parentRecord)}. - Append any encountered field errors to {errors}. - Increment {index}. - - Call {ExecuteStreamField(label, iterator, index, fields, innerType, path, - streamRecord, variableValues, subsequentPayloads)}. + - Call {ExecuteStreamField(iterator, index, fieldGroup, innerType, path, + streamRecord, variableValues, subsequentPayloads, branches, streams)}. - If a field error was raised, causing a {null} to be propagated to {data}, and {innerType} is a Non-Nullable type: - Add an entry to {payload} named `items` with the value {null}. @@ -962,8 +1045,6 @@ parentRecord, variableValues, subsequentPayloads): {data}. - If {errors} is not empty: - Add an entry to {payload} named `errors` with the value {errors}. - - If {label} is defined: - - Add an entry to {payload} named `label` with the value {label}. - Add an entry to {payload} named `path` with the value {itemPath}. - If {parentRecord} is defined: - Wait for the result of {dataExecution} on {parentRecord}. @@ -971,20 +1052,21 @@ parentRecord, variableValues, subsequentPayloads): - Set {dataExecution} on {streamRecord}. - Append {streamRecord} to {subsequentPayloads}. -CompleteValue(fieldType, fields, result, variableValues, path, -subsequentPayloads, asyncRecord): +CompleteValue(fieldType, fieldGroup, result, variableValues, path, +subsequentPayloads, asyncRecord, branches, streams): - 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, path)}. + fieldGroup, result, variableValues, path)}. - If {completedResult} is {null}, raise a _field error_. - Return {completedResult}. - If {result} is {null} (or another internal value similar to {null} such as {undefined}), return {null}. - If {fieldType} is a List type: - If {result} is not a collection of values, raise a _field error_. - - Let {field} be the first entry in {fields}. + - Let {taggedField} be the value of the first entry in {groupedFieldSet}. + - Let {field} be the corresponding entry within {taggedField}. - Let {innerType} be the inner type of {fieldType}. - If {field} provides the directive `@stream` and its {if} argument is not {false} and is not a variable in {variableValues} with the value {false} and @@ -995,25 +1077,25 @@ subsequentPayloads, asyncRecord): - Let {initialCount} be the value or variable provided to {streamDirective}'s {initialCount} argument. - If {initialCount} is less than zero, raise a _field error_. - - Let {label} be the value or variable provided to {streamDirective}'s - {label} argument. - Let {iterator} be an iterator for {result}. - Let {items} be an empty list. - Let {index} be zero. - While {result} is not closed: - If {streamDirective} is defined and {index} is greater than or equal to {initialCount}: - - Call {ExecuteStreamField(label, iterator, index, fields, innerType, - path, asyncRecord, subsequentPayloads)}. + - If {ShouldStream(streams, fieldGroup)}: + - Call {ExecuteStreamField(iterator, index, fieldGroup, innerType, path, + asyncRecord, subsequentPayloads, branches, streams)}. - Return {items}. - Otherwise: - Wait for the next item from {result} via the {iterator}. - If an item is not retrieved because of an error, raise a _field error_. - Let {resultItem} be the item retrieved from {result}. - - Let {itemPath} be {path} with {index} appended. + - Let {itemPath} be a unique list for this execution equal to {path} with + {index} appended. - Let {resolvedItem} be the result of calling {CompleteValue(innerType, - fields, resultItem, variableValues, itemPath, subsequentPayloads, - asyncRecord)}. + fieldGroup, resultItem, variableValues, itemPath, subsequentPayloads, + branches, streams, asyncRecord)}. - Append {resolvedItem} to {items}. - Increment {index}. - Return {items}. @@ -1024,10 +1106,41 @@ subsequentPayloads, asyncRecord): - Let {objectType} be {fieldType}. - Otherwise if {fieldType} is an Interface or Union type. - 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, path, subsequentPayloads, asyncRecord)} - _normally_ (allowing for parallelization). + - Let {groupedSubfieldSet} and {newDeferDepth} be the result of calling + {CollectSubfields(objectType, fieldGroup, variableValues)}. + - Let {resultMap} be the result of evaluating + {ExecuteGroupedFieldSet(groupedSubfieldSet, deferredGroupedSubfieldsList + objectType, result, variableValues, path, subsequentPayloads, branches, + streams, asyncRecord)} _normally_ (allowing for parallelization). + - If {newDeferDepth} is defined and {ShouldBranch(branches, groupedFieldSet, + path)} is {true}: + - Call {ExecuteDeferredFragment(objectType, objectValue, groupedFieldSet, + path, newDeferDepth, variableValues, asyncRecord, subsequentPayloads, + branches, streams)}, + - Return {resultMap}. + +ShouldBranch(branches, path): + +- If {branches} contains {path}, return {false}. +- Add {path} to {branches}. +- Return {true}. + +ShouldStream(streams, path): + +- If {streams} contains {path}, return {false}. +- Add {path} to {streams}. +- Return {true}. + +ShouldExecute(fieldGroup, asyncPayloadRecord): + +- Let {hasDepth} equal {false}. +- If {asyncPayloadRecord} is defined: + - Let {deferDepth} be the corresponding entry on {asyncPayloadRecord}. +- For each {taggedField} in {fieldGroup}: + - Let {fieldDeferDepth} be the entry for {deferDepth} on {taggedField}. + - If {fieldDeferDepth} is equal to {deferDepth}: + - Set {hasDepth} equal to {true}. +- Return {hasDepth}. **Coercing Results** @@ -1090,17 +1203,9 @@ sub-selections. } ``` -After resolving the value for `me`, the selection sets are merged together so -`firstName` and `lastName` can be resolved for one value. - -MergeSelectionSets(fields): - -- Let {selectionSet} be an empty list. -- For each {field} in {fields}: - - Let {fieldSelectionSet} be the selection set of {field}. - - If {fieldSelectionSet} is null or empty, continue to the next field. - - Append all selections in {fieldSelectionSet} to {selectionSet}. -- Return {selectionSet}. +After resolving the value for `me`, the selection sets are merged together by +calling {CollectSubfields()} so `firstName` and `lastName` can be resolved for +one value. ### Handling Field Errors @@ -1151,10 +1256,10 @@ error: ```graphql example { birthday { - ... @defer(label: "monthDefer") { + ... @defer { month } - ... @defer(label: "yearDefer") { + ... @defer { year } } @@ -1170,16 +1275,14 @@ Response 1, the initial response is sent: } ``` -Response 2, the defer payload for label "monthDefer" is sent. The {data} entry -has been set to {null}, as this {null} as propagated as high as the error -boundary will allow. +Response 2, a defer payload is sent. The {data} entry has been set to {null}, as +this {null} as propagated as high as the error boundary will allow. ```json example { "incremental": [ { "path": ["birthday"], - "label": "monthDefer", "data": null } ], @@ -1187,22 +1290,6 @@ boundary will allow. } ``` -Response 3, the defer payload for label "yearDefer" is sent. The data in this -payload is unaffected by the previous null error. - -```json example -{ - "incremental": [ - { - "path": ["birthday"], - "label": "yearDefer", - "data": { "year": "2022" } - } - ], - "hasNext": false -} -``` - If the `stream` directive is present on a list field with a Non-Nullable inner type, and a field error has caused a {null} to propagate to the list item, the {null} should not propagate any further, and the associated Stream Payload's diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index db50408fa..f6ece8f6d 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -26,12 +26,10 @@ When the response of the GraphQL operation is a response stream, the first value will be the initial response. All subsequent values may contain an `incremental` entry, containing a list of Defer or Stream payloads. -The `label` and `path` entries on Defer and Stream payloads are used by clients -to identify the `@defer` or `@stream` directive from the GraphQL operation that -triggered this response to be included in an `incremental` entry on a value -returned by the response stream. When a label is provided, the combination of -these two entries will be unique across all Defer and Stream payloads returned -in the response stream. +The `path` entries on Defer and Stream payloads are used by clients to identify +the `@defer` or `@stream` directive from the GraphQL operation that triggered +this response to be included in an `incremental` entry on a value returned by +the response stream. If the response of the GraphQL operation is a response stream, each response map must contain an entry with key `hasNext`. The value of this entry is `true` for @@ -275,9 +273,9 @@ For example, a query containing both defer and stream: ```graphql example query { person(id: "cGVvcGxlOjE=") { - ...HomeWorldFragment @defer(label: "homeWorldDefer") + ...HomeWorldFragment @defer name - films @stream(initialCount: 1, label: "filmsStream") { + films @stream(initialCount: 1) { title } } @@ -312,12 +310,10 @@ Response 2, contains the defer payload and the first stream payload. { "incremental": [ { - "label": "homeWorldDefer", "path": ["person"], "data": { "homeWorld": { "name": "Tatooine" } } }, { - "label": "filmsStream", "path": ["person", "films", 1], "items": [{ "title": "The Empire Strikes Back" }] } @@ -335,7 +331,6 @@ would be the final response. { "incremental": [ { - "label": "filmsStream", "path": ["person", "films", 2], "items": [{ "title": "Return of the Jedi" }] } @@ -359,7 +354,7 @@ iterator of the `films` field closes. A stream payload is a map that may appear as an item in the `incremental` entry of a response. A stream payload is the result of an associated `@stream` directive in the operation. A stream payload must contain `items` and `path` -entries and may contain `label`, `errors`, and `extensions` entries. +entries and may contain `errors` and `extensions` entries. ##### Items @@ -374,7 +369,7 @@ than the list field with the associated `@stream` directive. A defer payload is a map that may appear as an item in the `incremental` entry of a response. A defer payload is the result of an associated `@defer` directive in the operation. A defer payload must contain `data` and `path` entries and may -contain `label`, `errors`, and `extensions` entries. +contain `errors` and `extensions` entries. ##### Data @@ -411,14 +406,6 @@ of the field containing the associated `@defer` directive. When the `path` field is present on an "Error result", it indicates the response field which experienced the error. -#### Label - -Stream and Defer payloads may contain a string field `label`. This `label` is -the same label passed to the `@defer` or `@stream` directive associated with the -response. This allows clients to identify which `@defer` or `@stream` directive -is associated with this value. `label` will not be present if the corresponding -`@defer` or `@stream` directive is not passed a `label` argument. - ## Serialization Format GraphQL does not require a specific serialization format. However, clients