From d193f58704995aa1bc0ab46426f7a616e3afe8df Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Thu, 20 Oct 2016 20:55:59 -0700 Subject: [PATCH 1/5] Clarify execution section. This adds algorithms to the section on execution and reorders content to better follow the flow of execution. Note that no additional semantics are being introduced in this PR. This is simply algorithmic clarification of the execution process. --- spec/Section 3 -- Type System.md | 27 +- spec/Section 6 -- Execution.md | 443 +++++++++++++++++++++---------- 2 files changed, 327 insertions(+), 143 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 87497ea63..f1c186506 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -775,13 +775,26 @@ An input object is never a valid result. **Input Coercion** -The input to an input object should be an unordered map, otherwise an error -should be thrown. The result of the coercion is an unordered map, with an -entry for each input field, whose key is the name of the input field. -The value of an entry in the coerced map is the result of input coercing the -value of the entry in the input with the same key; if the input does not have a -corresponding entry, the value is the result of coercing null. The input -coercion above should be performed according to the input coercion rules of the +The value for an input object should be an input object literal or an unordered +map, otherwise an error should be thrown. This unordered map should not contain +any entries with names not defined by a field of this input object type, +otherwise an error should be thrown. + +If any non-nullable fields defined by the input object do not have corresponding +entries in the original value, were provided a variable for which a value was +not provided, or for which the value {null} was provided, an error should +be thrown. + +The result of coercion is an environment-specific unordered map defining slots +for each field of the input object type. + +For each field of the input object type, if the original value has an entry with +the same name, and the value at that entry is a literal value or a variable +which was provided a runtime value, an entry is added to the result with the +name of the field. + +The value of that entry in the result is the outcome of input coercing the +original entry value according to the input coercion rules of the type declared by the input field. #### Input Object type validation diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 8be79e5d9..124170d7d 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -1,19 +1,50 @@ # Execution -This section describes how GraphQL generates a response from a request. +GraphQL generates a response from a request via execution. +A request for execution consists of a few pieces of information: -## Evaluating requests +* The schema to use, typically solely provided by the GraphQL service. +* A Document containing GraphQL Operations and Fragments to execute. +* Optionally: The name of the Operation in the Document to execute. +* Optionally: Values for Variables defined by the Operation. +* Optionally: An initial value corresponding to the root type being executed. -To evaluate a request, the executor must have a parsed `Document` (as defined +Given this information, the result of {ExecuteRequest()} produces the response, +to be formatted according to the Reponse section below. + + +## Executing Requests + +To execute a request, the executor must have a parsed `Document` (as defined in the “Query Language” part of this spec) and a selected operation name to run if the document defines multiple operations. +ExecuteRequest(schema, document, operationName, variableValues, initialValue): + + * Let {operation} be the result of {GetOperation(document, operationName)}. + * Let {coercedVariableValues} be the result of {CoerceVariableValues(schema, operation, variableValues)}. + * If {operation} is a query operation: + * Return {ExecuteQuery(operation, schema, coercedVariableValues, initialValue)}. + * Otherwise if {operation} is a mutation operation: + * Return {ExecuteMutation(operation, schema, coercedVariableValues, initialValue)}. + The executor should find the `Operation` in the `Document` with the given operation name. If no such operation exists, the executor should throw an -error. If the operation is found, then the result of evaluating the request -should be the result of evaluating the operation according to the “Evaluating -operations” section. +error. If the operation is found, then the result of executing the request +should be the result of executing the operation according to the "Executing +Operations” section. + +GetOperation(document, operationName): + + * If {operationName} is not {null}: + * Let {operation} be the Operation named {operationName} in {document}. + * If {operation} was not found, produce a query error. + * Return {operation}. + * Otherwise if there is only one Operation in {document}: + * Return that Operation. + * Otherwise: + * Produce a query error requiring a non-null {operationName}. ## Validation of operation @@ -42,44 +73,106 @@ of variable's declared type. If a query error is encountered during input coercion of variable values, then the operation fails without execution. +If any variable defined as non-null is not provided, or is provided the value +{null}, then the operation fails without execution. -## Evaluating operations +CoerceVariableValues(schema, operation, variableValues) + +## Executing Operations The type system, as described in the “Type System” part of the spec, must provide a “Query Root” and a “Mutation Root” object. +If the operation is a query, the result of the operation is the result of +executing the query’s top level selection set on the “Query Root” object. + +An initial value can be optionally provided when executing a query. + +ExecuteQuery(query, schema, variableValues, initialValue): + + * 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)} + *normally* (allowing parallelization). + * Let {errors} be any *field errors* produced while executing the + selection set. + * Return an unordered map containing {data} and {errors}. + If the operation is a mutation, the result of the operation is the result of -evaluating the mutation’s top level selection set on the “Mutation Root” -object. This selection set should be evaluated serially. +executing the mutation’s top level selection set on the “Mutation Root” +object. This selection set should be executed serially. -If the operation is a query, the result of the operation is the result of -evaluating the query’s top level selection set on the “Query Root” object. +It is expected that the top level fields in a mutation operation perform +side-effects on the underlying data system. Serial execution of the provided +mutations ensures against race conditions during these side-effects. + +ExecuteMutation(mutation, schema, variableValues, initialValue): + + * Let {variableValues} be the set of variable values to be used by any + field argument value coercion. + * 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)} + *serially*. + * Let {errors} be any *field errors* produced while executing the + selection set. + * Return an unordered map containing {data} and {errors}. + + +## Executing Selection 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. -## Evaluating selection sets +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. -To evaluate a selection set, the executor needs to know the object on which it -is evaluating the set and whether it is being evaluated serially. +ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): -If the selection set is being evaluated on the `null` object, then the result -of evaluating the selection set is `null`. + * Initialize {visitedFragments} to be the empty set. + * Let {groupedFieldSet} be the result of + {CollectFields(objectType, selectionSet, visitedFragments, variableValues)}. + * Initialize {resultMap} to an empty ordered map. + * For each {groupedFields} in {groupedFieldSet}: + * Let {entryTuple} be {GetFieldEntry(objectType, objectValue, groupedFields, variableValues)}. + * If {entryTuple} is not {null}: + * Let {responseKey} and {responseValue} be the values of {entryTuple}. + * Set {responseValue} as the value for {responseKey} in {resultMap}. + * Return {resultMap}. -Otherwise, the selection set is turned into a grouped field set; each entry in -the grouped field set is a list of fields that share a responseKey. +Note: {responseMap} is ordered by which fields appear first in the query. This +is explained in greater detail in the Response section below. -The selection set is converted to a grouped field set by calling -`CollectFields`, initializing `visitedFragments` to an empty list. +Note: Normally, each call to {GetFieldEntry()} in the algorithm above is +performed in parallel. However there are conditions in which each call must be +done in serial, such as for mutations. This is explain in more detail in the +sections below. -CollectFields(objectType, selectionSet, visitedFragments): +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. + +This ensures all fields with the same response key (alias or field name) +included via referenced fragments are executed at the same time. + +CollectFields(objectType, selectionSet, visitedFragments, variableValues): * Initialize {groupedFields} to an empty ordered list of lists. - * For each {selection} in {selectionSet}; + * For each {selection} in {selectionSet}: * If {selection} provides the directive `@skip`, let {skipDirective} be that directive. - * If {skipDirective}'s {if} argument is {true}, continue with the - next {selection} in {selectionSet}. + * If {skipDirective}'s {if} argument is {true} or is a variable with a + {true} value in {variableValues}, continue with the next + {selection} in {selectionSet}. * If {selection} provides the directive `@include`, let {includeDirective} be that directive. - * If {includeDirective}'s {if} argument is {false}, continue with the - next {selection} in {selectionSet}. + * If {includeDirective}'s {if} argument is {false} or is a variable with + *no* {true} value in {variableValues}, continue with the next + {selection} in {selectionSet}. * If {selection} is a {Field}: * Let {responseKey} be the response key of {selection}. * Let {groupForResponseKey} be the list in {groupedFields} for @@ -95,7 +188,7 @@ CollectFields(objectType, selectionSet, visitedFragments): * If no such {fragment} exists, continue with the next {selection} in {selectionSet}. * Let {fragmentType} be the type condition on {fragment}. - * If {doesFragmentTypeApply(objectType, fragmentType)} is false, continue + * 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 @@ -107,7 +200,7 @@ CollectFields(objectType, selectionSet, visitedFragments): * Append all items in {fragmentGroup} to {groupForResponseKey}. * If {selection} is an {InlineFragment}: * Let {fragmentType} be the type condition on {selection}. - * If {fragmentType} is not {null} and {doesFragmentTypeApply(objectType, fragmentType)} is false, continue + * If {fragmentType} is not {null} and {DoesFragmentTypeApply(objectType, fragmentType)} is false, continue with the next {selection} in {selectionSet}. * Let {fragmentSelectionSet} be the top-level selection set of {selection}. * Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, visitedFragments)}. @@ -118,7 +211,7 @@ CollectFields(objectType, selectionSet, visitedFragments): * Append all items in {fragmentGroup} to {groupForResponseKey}. * Return {groupedFields}. -doesFragmentTypeApply(objectType, fragmentType): +DoesFragmentTypeApply(objectType, fragmentType): * If {fragmentType} is an Object Type: * if {objectType} and {fragmentType} are the same type, return {true}, otherwise return {false}. @@ -127,113 +220,27 @@ doesFragmentTypeApply(objectType, fragmentType): * If {fragmentType} is a Union: * if {objectType} is a possible type of {fragmentType}, return {true} otherwise return {false}. -The result of evaluating the selection set is the result of evaluating the +The result of executing the selection set is the result of executing the corresponding grouped field set. The corresponding grouped field set should be -evaluated serially if the selection set is being evaluated serially, otherwise -it should be evaluated normally. - +executed serially if the selection set is being executed serially, otherwise +it should be executed normally. -## Evaluating a grouped field set - -The result of evaluating a grouped field set will be an ordered map. For each +The result of executing a grouped field set will be an ordered map. For each item in the grouped field set, an entry is added to the resulting ordered map, where the key is the response key shared by all fields for that entry, and the -value is the result of evaluating those fields. - - -### Field entries - -Each item in the grouped field set can potentially create an entry in the -result map. That entry in the result map is the result of calling -`GetFieldEntry` on the corresponding item in the grouped field set. -`GetFieldEntry` can return `null`, which indicates that there should be no -entry in the result map for this item. Note that this is distinct from -returning an entry with a string key and a null value, which indicates that an -entry in the result should be added for that key, and its value should be null. - -`GetFieldEntry` assumes the existence of two functions that are not defined in -this section of the spec. It is expected that the type system provides these -methods: - - * `ResolveFieldOnObject`, which takes an object type, a field, and an object, - and returns the result of resolving that field on the object. - * `GetFieldTypeFromObjectType`, which takes an object type and a field, and - returns that field's type on the object type, or `null` if the field is not - valid on the object type. - -GetFieldEntry(objectType, object, fields): - - * Let {firstField} be the first entry in the ordered list {fields}. Note that - {fields} is never empty, as the entry in the grouped field set would not - exist if there were no fields. - * Let {responseKey} be the response key of {firstField}. - * Let {fieldType} be the result of calling - {GetFieldTypeFromObjectType(objectType, firstField)}. - * If {fieldType} is {null}, return {null}, indicating that no entry exists in - the result map. - * Let {resolvedObject} be {ResolveFieldOnObject(objectType, object, fieldEntry)}. - * If {resolvedObject} is {null}, return {tuple(responseKey, null)}, - indicating that an entry exists in the result map whose value is `null`. - * Let {subSelectionSet} be the result of calling {MergeSelectionSets(fields)}. - * Let {responseValue} be the result of calling {CompleteValue(fieldType, resolvedObject, subSelectionSet)}. - * Return {tuple(responseKey, responseValue)}. - -GetFieldTypeFromObjectType(objectType, firstField): - * Call the method provided by the type system for determining the field type - on a given object type. - -ResolveFieldOnObject(objectType, object, firstField): - * Call the method provided by the type system for determining the resolution - of a field on a given object. - -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}. - -CompleteValue(fieldType, result, subSelectionSet): - * 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, result, subSelectionSet)}. - * If {completedResult} is {null}, throw a field error. - * Return {completedResult}. - * If {result} is {null} or a value similar to {null} such as {undefined} or - {NaN}, return {null}. - * If {fieldType} is a List type: - * If {result} is not a collection of values, throw a field error. - * Let {innerType} be the inner type of {fieldType}. - * Return a list where each item is the result of calling - {CompleteValue(innerType, resultItem, subSelectionSet)}, where {resultItem} is each item - in {result}. - * If {fieldType} is a Scalar or Enum type: - * Return the result of "coercing" {result}, ensuring it is a legal value of - {fieldType}, otherwise {null}. - * If {fieldType} is an Object, Interface, or Union type: - * If {fieldType} is an Object type. - * Let {objectType} be {fieldType}. - * Otherwise if {fieldType} is an Interface or Union type. - * Let {objectType} be ResolveAbstractType({fieldType}, {result}). - * Return the result of evaluating {subSelectionSet} on {objectType} normally. - -ResolveAbstractType(abstractType, objectValue): - * Return the result of calling the internal method provided by the type - system for determining the Object type of {abstractType} given the - value {objectValue}. +value is the result of executing those fields. -### Normal evaluation +### Normal Execution -When evaluating a grouped field set without a serial execution order requirement, +When executing a grouped field set without a serial execution order requirement, the executor can determine the entries in the result map in whatever order it chooses. Because the resolution of fields other than top-level mutation fields is always side effect–free and idempotent, the execution order must not -affect the result, and hence the server has the freedom to evaluate the field +affect the result, and hence the server has the freedom to execute the field entries in whatever order it deems optimal. -For example, given the following grouped field set to be evaluated normally: +For example, given the following grouped field set to be executed normally: ```graphql { @@ -250,18 +257,18 @@ A valid GraphQL executor can resolve the four fields in whatever order it chose. -### Serial execution +### Serial Execution Observe that based on the above sections, the only time an executor will run in serial execution order is on the top level selection set of a mutation operation and on its corresponding grouped field set. -When evaluating a grouped field set serially, the executor must consider each entry +When executing a grouped field set serially, the executor must consider each entry from the grouped field set in the order provided in the grouped field set. It must determine the corresponding entry in the result map for each item to completion before it continues on to the next item in the grouped field set: -For example, given the following selection set to be evaluated serially: +For example, given the following selection set to be executed serially: ```graphql { @@ -276,10 +283,10 @@ For example, given the following selection set to be evaluated serially: The executor must, in serial: - - Run `getFieldEntry` for `changeBirthday`, which during `CompleteValue` will - evaluate the `{ month }` sub-selection set normally. - - Run `getFieldEntry` for `changeAddress`, which during `CompleteValue` will - evaluate the `{ street }` sub-selection set normally. + - Run {GetFieldEntry()} for `changeBirthday`, which during {CompleteValue()} + will execute the `{ month }` sub-selection set normally. + - Run {GetFieldEntry()} for `changeAddress`, which during {CompleteValue()} + will execute the `{ street }` sub-selection set normally. As an illustrative example, let's assume we have a mutation field `changeTheNumber` that returns an object containing one field, @@ -299,14 +306,14 @@ As an illustrative example, let's assume we have a mutation field } ``` -The executor will evaluate the following serially: +The executor will execute the following serially: - Resolve the `changeTheNumber(newNumber: 1)` field - - Evaluate the `{ theNumber }` sub-selection set of `first` normally + - Execute the `{ theNumber }` sub-selection set of `first` normally - Resolve the `changeTheNumber(newNumber: 3)` field - - Evaluate the `{ theNumber }` sub-selection set of `second` normally + - Execute the `{ theNumber }` sub-selection set of `second` normally - Resolve the `changeTheNumber(newNumber: 2)` field - - Evaluate the `{ theNumber }` sub-selection set of `third` normally + - Execute the `{ theNumber }` sub-selection set of `third` normally A correct executor must generate the following result for that selection set: @@ -325,6 +332,134 @@ A correct executor must generate the following result for that selection set: ``` +## Executing Fields + +Each item in the grouped field set can potentially create an entry in the +result map. That entry in the result map is the result of calling +{GetFieldEntry()} on the corresponding item in the grouped field set. +{GetFieldEntry()} can return {null}, which indicates that there should be no +entry in the result map for this item. Note that this is distinct from +returning an entry with a string key and a {null} value, which indicates that +an entry in the result should be added for that key, and its value should +be {null}. + +GetFieldEntry(objectType, objectValue, fields, variableValues): + + * Let {firstField} be the first entry in the ordered list {fields}. Note that + {fields} is never empty, as the entry in the grouped field set would not + exist if there were no fields. + * Let {responseKey} be the response key of {firstField}. Note: If an alias was + provided, it is used as the response key. + * Let {fieldName} be the name of {firstField}. Note: This value is unaffected + if an alias is provided. + * Let {fieldType} be the result of calling + {GetFieldTypeFromObjectType(objectType, fieldName)}. + * If {fieldType} is {null}, return {null}, indicating that no entry exists in + the result map. + * Let {resolvedValue} be {ResolveFieldValue(objectType, firstField, objectValue, variableValues)}. + * If {resolvedValue} is {null}, return {tuple(responseKey, null)}, + indicating that an entry exists in the result map whose value is `null`. + * Let {subSelectionSet} be the result of calling {MergeSelectionSets(fields)}. + * Let {responseValue} be the result of calling {CompleteValue(fieldType, resolvedValue, subSelectionSet, variableValues)}. + * Return {tuple(responseKey, responseValue)}. + +Every Object type is defined as a set of fields, each of which provides a return +type. {GetFieldTypeFromObjectType()} produces this type for a named field. + +GetFieldTypeFromObjectType(objectType, fieldName): + * Return the field type defined by {objectType} with the name {fieldName}. + +While nearly all of GraphQL execution can be described generically, ultimately +the internal system exposing the GraphQL interface must provide values. +This is exposed via {ResolveFieldValue}, which produces a value for a given +field on a type for a real value. + +As an example, this might accept the {objectType} `Person`, and the {fieldName} +{"soulMate"} and the {object} value representing John Lennon. It would be +expected to yield the value representing Yoko Ono. + +Note: While described here in immediate procedural steps, it's common for this +operation to be asynchronous by relying on reading an underlying database or +networked service to produce a value. This necessitates the rest of a GraphQL +executor to handle an asynchronous execution flow. + +ResolveFieldValue(objectType, field, object, variableValues): + * Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, variableValues)} + * Let {fieldName} be the name of {field}. + * Call the internal function provided by {objectType} for determining the + resolved value of a field named {fieldName} on a given {object} + provided {argumentValues}. + +When more than one fields of the same name are executed in parallel, their +selection sets are merged together to produce a single result. + +An example query illustrating parallel fields with the same name: + +```graphql +{ + me { + firstName + } + me { + lastName + } +} +``` + +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 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, result, subSelectionSet, variableValues): + * 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, result, subSelectionSet)}. + * If {completedResult} is {null}, throw a field error. + * Return {completedResult}. + * If {result} is {null} (or another internal value similar to {null} such as + {undefined} or {NaN}), return {null}. + * If {fieldType} is a List type: + * If {result} is not a collection of values, throw a field error. + * Let {innerType} be the inner type of {fieldType}. + * Return a list where each item is the result of calling + {CompleteValue(innerType, resultItem, subSelectionSet)}, where + {resultItem} is each item in {result}. + * If {fieldType} is a Scalar or Enum type: + * Return the result of "coercing" {result}, ensuring it is a legal value of + {fieldType}, otherwise {null}. + * If {fieldType} is an Object, Interface, or Union type: + * If {fieldType} is an Object type. + * Let {objectType} be {fieldType}. + * Otherwise if {fieldType} is an Interface or Union type. + * Let {objectType} be ResolveAbstractType({fieldType}, {result}). + * Return the result of evaluating ExecuteSelectionSet(subSelectionSet, objectType, result, variableValues) *normally* (allowing for parallelization). + +When completing a field with an abstract return type, that is an Interface or +Union return type, first the abstract type must be resolved to a relevant Object +type. This determination is made by the internal system using whatever +means appropriate. + +Note: A common method of determining the Object type for an {objectValue} in +object-oriented environments, such as Java or C#, is to use the class name of +the {objectValue}. + +ResolveAbstractType(abstractType, objectValue): + * Return the result of calling the internal method provided by the type + system for determining the Object type of {abstractType} given the + value {objectValue}. + ### Nullability If the result of resolving a field is `null` (either because the function to @@ -339,7 +474,7 @@ If the field resolve function returned `null`, the resulting field error must be added to the `"errors"` list in the response. -### Error handling +### Error Handling If an error occurs when resolving a field, it should be treated as though the field returned `null`, and an error must be added to the `"errors"` list @@ -350,3 +485,39 @@ cannot be `null` the error is propagated to be dealt with by the parent field. If all fields from the root of the request to the source of the error return `Non-Null` types, then the `"data"` entry in the response should be `null`. + + +### Coercing Field Arguments + +Fields may include arguments which are provided to the underlying runtime in +order to correctly produce a value. These arguments are defined by the field in +the type system to have a specific input type: Scalars, Enum, Input Object, or +List or Non-Null wrapped variations of these three. + +At each argument position in a query may be a literal value or a variable to be +provided at runtime. + +CoerceArgumentValues(objectType, field, variableValues) + * 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}. + * Let {coercedArgumentValues} be an empty Map. + * For each {argumentDefinitions} as {argumentName} and {argumentType}: + * If no value was provided in {argumentValues} for the name {argumentName}: + * Continue to the next argument definition. + * Let {value} be the value provided in {argumentValues} for the name {argumentName}. + * If {value} is a Variable: + * If a value exists in {variableValues} for the Variable {value}: + * Add an entry to {coercedArgumentValues} named {argName} with the + value of the Variable {value} found in {variableValues}. + * Otherwise: + * Let {coercedValue} be the result of coercing {value} according to the + input coercion rules of {argType}. + * Add an entry to {coercedArgumentValues} named {argName} with the + value {coercedValue}. + * Return {coercedArgumentValues}. + +Note: Variable values are not coerced because they are expected to be coerced +based on the type of the variable, and valid queries must only allow usage of +variables of appropriate types. From 237a9cf9f32a8de8382e6c35dc012947dfbdcecc Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Fri, 21 Oct 2016 17:15:28 -0700 Subject: [PATCH 2/5] Follow up improvements thanks to @jjergus feedback --- spec/Section 3 -- Type System.md | 24 ++++++- spec/Section 6 -- Execution.md | 104 ++++++++++++++++++------------- 2 files changed, 83 insertions(+), 45 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index f1c186506..37df99e9e 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -786,7 +786,8 @@ not provided, or for which the value {null} was provided, an error should be thrown. The result of coercion is an environment-specific unordered map defining slots -for each field of the input object type. +for each field both defined by the input object type and provided by the +original value. For each field of the input object type, if the original value has an entry with the same name, and the value at that entry is a literal value or a variable @@ -797,6 +798,27 @@ The value of that entry in the result is the outcome of input coercing the original entry value according to the input coercion rules of the type declared by the input field. +Following are examples of Input Object coercion for the type: + +```graphql +input ExampleInputObject { + a: String + b: Int! +} +``` + +Original Value | Variables | Coerced Value +------------------------------------------------------------------------------- +`{ a: "abc", b: 123 }` | `{}` | `{ a: "abc", b: 123 }` +`{ a: 123, b: "123" }` | `{}` | `{ a: "123", b: 123 }` +`{ a: "abc" }` | `{}` | Error: Missing required field {b} +`{ b: $var }` | `{ var: 123 }` | `{ b: 123 }` +`{ b: $var }` | `{ var: null }` | Error: {b} must be non-null. +`{ b: $var }` | `{}` | Error: {b} must be non-null. +`{ b: $var }` | `{}` | Error: {b} must be non-null. +`{ a: $var, b: 1 }` | `{ var: null }` | `{ a: null, b: 1 }` +`{ a: $var, b: 1 }` | `{}` | `{ b: 1 }` + #### Input Object type validation 1. An Input Object type must define one or more fields. diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 124170d7d..c40098e03 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -11,14 +11,36 @@ A request for execution consists of a few pieces of information: * Optionally: An initial value corresponding to the root type being executed. Given this information, the result of {ExecuteRequest()} produces the response, -to be formatted according to the Reponse section below. +to be formatted according to the Response section below. + + +## Validating Requests + +As explained in the Validation section, only requests which pass all validation +rules should be executed. If validation errors are known, they should be +reported in the list of "errors" in the response and the request must fail +without execution. + +Typically validation is performed in the context of a request immediately +before execution, however a GraphQL service may execute a request without +immediately validating it if that exact same request is known to have been +validated before. A GraphQL service should only execute requests which *at some +point* were known to be free of any validation errors, and have since +not changed. + +For example: the request may be validated during development, provided it does +not later change, or a service may validate a request once and memoize the +result to avoid validating the same request again in the future. ## Executing Requests To execute a request, the executor must have a parsed `Document` (as defined in the “Query Language” part of this spec) and a selected operation name to -run if the document defines multiple operations. +run if the document defines multiple operations, otherwise the document is +expected to only contain a single operation. The result of the request is +determined by the result of executing this operation according to the "Executing +Operations” section below. ExecuteRequest(schema, document, operationName, variableValues, initialValue): @@ -29,12 +51,6 @@ ExecuteRequest(schema, document, operationName, variableValues, initialValue): * Otherwise if {operation} is a mutation operation: * Return {ExecuteMutation(operation, schema, coercedVariableValues, initialValue)}. -The executor should find the `Operation` in the `Document` with the given -operation name. If no such operation exists, the executor should throw an -error. If the operation is found, then the result of executing the request -should be the result of executing the operation according to the "Executing -Operations” section. - GetOperation(document, operationName): * If {operationName} is not {null}: @@ -47,24 +63,6 @@ GetOperation(document, operationName): * Produce a query error requiring a non-null {operationName}. -## Validation of operation - -As explained in the Validation section, only requests which pass all validation -rules should be executed. If validation errors are known, they should be -reported in the list of "errors" in the response and the operation must fail -without execution. - -Typically validation is performed in the context of a request immediately -before execution, however a GraphQL service may execute a request without -explicitly validating it if that exact same request is known to have been -validated before. For example: the request may be validated during development, -provided it does not later change, or a service may validate a request once and -memoize the result to avoid validating the same request again in the future. - -A GraphQL service should only execute requests which *at some point* were -known to be free of any validation errors, and have not changed since. - - ## Coercing Variable Values If the operation has defined any variables, then the values for @@ -73,10 +71,28 @@ of variable's declared type. If a query error is encountered during input coercion of variable values, then the operation fails without execution. -If any variable defined as non-null is not provided, or is provided the value -{null}, then the operation fails without execution. +CoerceVariableValues(schema, operation, variableValues): + + * Let {coercedValues} be an empty unordered Map. + * Let {variables} be the variables defined by {operation}. + * For each {variables} as {variableName} and {variableType}: + * If no value was provided in {variableValues} for the name {variableName}: + * If {variableType} is a Non-Nullable type, throw a query error. + * Continue to the next variable. + * Let {value} be the value provided in {variableValues} for the name {variableName}. + * If {value} cannot be coerced according to the + input coercion rules of {variableType}, throw a query error. + * Let {coercedValue} be the result of coercing {value} according to the + input coercion rules of {variableType}. + * Add an entry to {coercedValues} named {variableName} with the value {coercedValue}. + * Return {coercedValues}. + +Note: This algorithm is very similar to {CoerceArgumentValues()}, however is +less forgiving of non-coerceable values. + +Note: If any variable defined as non-null is not provided, or is provided the +value {null}, then the operation fails without execution. -CoerceVariableValues(schema, operation, variableValues) ## Executing Operations @@ -110,8 +126,6 @@ mutations ensures against race conditions during these side-effects. ExecuteMutation(mutation, schema, variableValues, initialValue): - * Let {variableValues} be the set of variable values to be used by any - field argument value coercion. * 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}. @@ -135,9 +149,8 @@ response map. ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): - * Initialize {visitedFragments} to be the empty set. * Let {groupedFieldSet} be the result of - {CollectFields(objectType, selectionSet, visitedFragments, variableValues)}. + {CollectFields(objectType, selectionSet, variableValues)}. * Initialize {resultMap} to an empty ordered map. * For each {groupedFields} in {groupedFieldSet}: * Let {entryTuple} be {GetFieldEntry(objectType, objectValue, groupedFields, variableValues)}. @@ -151,7 +164,7 @@ is explained in greater detail in the Response section below. Note: Normally, each call to {GetFieldEntry()} in the algorithm above is performed in parallel. However there are conditions in which each call must be -done in serial, such as for mutations. This is explain in more detail in the +done in serial, such as for mutations. This is explained in more detail in the sections below. Before execution, the selection set is converted to a grouped field set by @@ -161,8 +174,9 @@ fields that share a response key. This ensures all fields with the same response key (alias or field name) included via referenced fragments are executed at the same time. -CollectFields(objectType, selectionSet, visitedFragments, variableValues): +CollectFields(objectType, selectionSet, variableValues, visitedFragments): + * If {visitedFragments} if not provided, initialize it to the empty set. * Initialize {groupedFields} to an empty ordered list of lists. * For each {selection} in {selectionSet}: * If {selection} provides the directive `@skip`, let {skipDirective} be that directive. @@ -203,7 +217,7 @@ CollectFields(objectType, selectionSet, visitedFragments, variableValues): * If {fragmentType} is not {null} and {DoesFragmentTypeApply(objectType, fragmentType)} is false, continue with the next {selection} in {selectionSet}. * Let {fragmentSelectionSet} be the top-level selection set of {selection}. - * Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, visitedFragments)}. + * Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, visitedFragments)}. * 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 @@ -502,22 +516,24 @@ CoerceArgumentValues(objectType, field, variableValues) * Let {fieldName} be the name of {field}. * Let {argumentDefinitions} be the arguments defined by {objectType} for the field named {fieldName}. - * Let {coercedArgumentValues} be an empty Map. + * Let {coercedValues} be an empty unordered Map. * For each {argumentDefinitions} as {argumentName} and {argumentType}: * If no value was provided in {argumentValues} for the name {argumentName}: - * Continue to the next argument definition. + * If {argumentType} is a Non-Nullable type, throw a field error. + * Otherwise, continue to the next argument definition. * Let {value} be the value provided in {argumentValues} for the name {argumentName}. * If {value} is a Variable: * If a value exists in {variableValues} for the Variable {value}: - * Add an entry to {coercedArgumentValues} named {argName} with the + * Add an entry to {coercedValues} named {argName} with the value of the Variable {value} found in {variableValues}. - * Otherwise: + * Otherwise if {value} can be coerced according to the input coercion rules + of {argType}: * Let {coercedValue} be the result of coercing {value} according to the input coercion rules of {argType}. - * Add an entry to {coercedArgumentValues} named {argName} with the + * Add an entry to {coercedValues} named {argName} with the value {coercedValue}. - * Return {coercedArgumentValues}. + * Return {coercedValues}. Note: Variable values are not coerced because they are expected to be coerced -based on the type of the variable, and valid queries must only allow usage of -variables of appropriate types. +before executing the request in {CoerceVariableValues()}, and valid queries must +only allow usage of variables of appropriate types. From 7352f04302f08609c9615aeba1cb96868f00eebe Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Fri, 21 Oct 2016 18:46:47 -0700 Subject: [PATCH 3/5] Another pass at further improvements to describing these operations. Included @jjergus's suggestion of getting rid of the tuple based response keying. --- spec/Section 6 -- Execution.md | 528 ++++++++++++++++----------------- spec/Section 7 -- Response.md | 6 +- 2 files changed, 264 insertions(+), 270 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index c40098e03..6b023b3f8 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -14,25 +14,6 @@ Given this information, the result of {ExecuteRequest()} produces the response, to be formatted according to the Response section below. -## Validating Requests - -As explained in the Validation section, only requests which pass all validation -rules should be executed. If validation errors are known, they should be -reported in the list of "errors" in the response and the request must fail -without execution. - -Typically validation is performed in the context of a request immediately -before execution, however a GraphQL service may execute a request without -immediately validating it if that exact same request is known to have been -validated before. A GraphQL service should only execute requests which *at some -point* were known to be free of any validation errors, and have since -not changed. - -For example: the request may be validated during development, provided it does -not later change, or a service may validate a request once and memoize the -result to avoid validating the same request again in the future. - - ## Executing Requests To execute a request, the executor must have a parsed `Document` (as defined @@ -53,17 +34,36 @@ ExecuteRequest(schema, document, operationName, variableValues, initialValue): GetOperation(document, operationName): - * If {operationName} is not {null}: + * If {operationName} is {null}: + * If {document} contains exactly one operation. + * Return the Operation contained in the {document}. + * Otherwise produce a query error requiring {operationName}. + * Otherwise: * Let {operation} be the Operation named {operationName} in {document}. * If {operation} was not found, produce a query error. * Return {operation}. - * Otherwise if there is only one Operation in {document}: - * Return that Operation. - * Otherwise: - * Produce a query error requiring a non-null {operationName}. -## Coercing Variable Values +### Validating Requests + +As explained in the Validation section, only requests which pass all validation +rules should be executed. If validation errors are known, they should be +reported in the list of "errors" in the response and the request must fail +without execution. + +Typically validation is performed in the context of a request immediately +before execution, however a GraphQL service may execute a request without +immediately validating it if that exact same request is known to have been +validated before. A GraphQL service should only execute requests which *at some +point* were known to be free of any validation errors, and have since +not changed. + +For example: the request may be validated during development, provided it does +not later change, or a service may validate a request once and memoize the +result to avoid validating the same request again in the future. + + +### Coercing Variable Values If the operation has defined any variables, then the values for those variables need to be coerced using the input coercion rules @@ -74,11 +74,13 @@ execution. CoerceVariableValues(schema, operation, variableValues): * Let {coercedValues} be an empty unordered Map. - * Let {variables} be the variables defined by {operation}. - * For each {variables} as {variableName} and {variableType}: + * Let {variableDefinitions} be the variables defined by {operation}. + * For each {variableDefinition} in {variableDefinitions}: + * Let {variableName} be the name of {variableDefinition}. + * Let {variableType} be the expected type of {variableDefinition}. * If no value was provided in {variableValues} for the name {variableName}: * If {variableType} is a Non-Nullable type, throw a query error. - * Continue to the next variable. + * Otherwise, continue to the next variable definition. * Let {value} be the value provided in {variableValues} for the name {variableName}. * If {value} cannot be coerced according to the input coercion rules of {variableType}, throw a query error. @@ -90,19 +92,17 @@ CoerceVariableValues(schema, operation, variableValues): Note: This algorithm is very similar to {CoerceArgumentValues()}, however is less forgiving of non-coerceable values. -Note: If any variable defined as non-null is not provided, or is provided the -value {null}, then the operation fails without execution. - ## Executing Operations -The type system, as described in the “Type System” part of the spec, must -provide a “Query Root” and a “Mutation Root” object. +The type system, as described in the “Type System” section of the spec, must +provide a query root object type. If mutations are supported, it must also +provide a mutation root object type. If the operation is a query, the result of the operation is the result of -executing the query’s top level selection set on the “Query Root” object. +executing the query’s top level selection set with the query root object type. -An initial value can be optionally provided when executing a query. +An initial value may be provided when executing a query. ExecuteQuery(query, schema, variableValues, initialValue): @@ -117,8 +117,8 @@ ExecuteQuery(query, schema, variableValues, initialValue): * Return an unordered map containing {data} and {errors}. If the operation is a mutation, the result of the operation is the result of -executing the mutation’s top level selection set on the “Mutation Root” -object. This selection set should be executed serially. +executing the mutation’s top level selection set on the mutation root +object type. This selection set should be executed serially. It is expected that the top level fields in a mutation operation perform side-effects on the underlying data system. Serial execution of the provided @@ -152,107 +152,27 @@ ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): * Let {groupedFieldSet} be the result of {CollectFields(objectType, selectionSet, variableValues)}. * Initialize {resultMap} to an empty ordered map. - * For each {groupedFields} in {groupedFieldSet}: - * Let {entryTuple} be {GetFieldEntry(objectType, objectValue, groupedFields, variableValues)}. - * If {entryTuple} is not {null}: - * Let {responseKey} and {responseValue} be the values of {entryTuple}. - * Set {responseValue} as the value for {responseKey} in {resultMap}. + * 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 {fieldType} be the return type defined for the field {fieldName} of {objectType}. + * If {fieldType} is {null}: + * Continue to the next iteration of {groupedFieldSet}. + * Let {responseValue} be {ExecuteField(objectType, objectValue, fields, fieldType, variableValues)}. + * Set {responseValue} as the value for {responseKey} in {resultMap}. * Return {resultMap}. Note: {responseMap} is ordered by which fields appear first in the query. This -is explained in greater detail in the Response section below. - -Note: Normally, each call to {GetFieldEntry()} in the algorithm above is -performed in parallel. However there are conditions in which each call must be -done in serial, such as for mutations. This is explained in more detail in the -sections below. - -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. - -This ensures all fields with the same response key (alias or field name) -included via referenced fragments are executed at the same time. - -CollectFields(objectType, selectionSet, variableValues, visitedFragments): - - * If {visitedFragments} if not provided, initialize it to the empty set. - * Initialize {groupedFields} to an empty ordered list of lists. - * For each {selection} in {selectionSet}: - * If {selection} provides the directive `@skip`, let {skipDirective} be that directive. - * If {skipDirective}'s {if} argument is {true} or is a variable with a - {true} value in {variableValues}, continue with the next - {selection} in {selectionSet}. - * If {selection} provides the directive `@include`, let {includeDirective} be that directive. - * If {includeDirective}'s {if} argument is {false} or is a variable with - *no* {true} value in {variableValues}, continue with the next - {selection} in {selectionSet}. - * If {selection} is a {Field}: - * Let {responseKey} be the response key of {selection}. - * 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}. - * 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 {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, visitedFragments)}. - * 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 - {responseKey}; if no such list exists, create it as an empty list. - * Append all items in {fragmentGroup} to {groupForResponseKey}. - * If {selection} is an {InlineFragment}: - * Let {fragmentType} be the type condition on {selection}. - * If {fragmentType} is not {null} and {DoesFragmentTypeApply(objectType, fragmentType)} is false, continue - with the next {selection} in {selectionSet}. - * Let {fragmentSelectionSet} be the top-level selection set of {selection}. - * Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, visitedFragments)}. - * 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 - {responseKey}; if no such list exists, create it as an empty list. - * Append all items in {fragmentGroup} to {groupForResponseKey}. - * Return {groupedFields}. +is explained in greater detail in the Field Collection section below. -DoesFragmentTypeApply(objectType, fragmentType): - * If {fragmentType} is an Object Type: - * if {objectType} and {fragmentType} are the same type, return {true}, otherwise return {false}. - * If {fragmentType} is an Interface Type: - * if {objectType} is an implementation of {fragmentType}, return {true} otherwise return {false}. - * If {fragmentType} is a Union: - * if {objectType} is a possible type of {fragmentType}, return {true} otherwise return {false}. - -The result of executing the selection set is the result of executing the -corresponding grouped field set. The corresponding grouped field set should be -executed serially if the selection set is being executed serially, otherwise -it should be executed normally. - -The result of executing a grouped field set will be an ordered map. For each -item in the grouped field set, an entry is added to the resulting ordered map, -where the key is the response key shared by all fields for that entry, and the -value is the result of executing those fields. +### Normal and Serial Execution - -### Normal Execution - -When executing a grouped field set without a serial execution order requirement, -the executor can determine the entries in the result map in whatever order it -chooses. Because the resolution of fields other than top-level mutation fields -is always side effect–free and idempotent, the execution order must not -affect the result, and hence the server has the freedom to execute the field -entries in whatever order it deems optimal. +Normally the executor can execute the entries in a grouped field set in whatever +order it chooses (often in parallel). Because the resolution of fields other +than top-level mutation fields must always be side effect-free and idempotent, +the execution order must not affect the result, and hence the server has the +freedom to execute the field entries in whatever order it deems optimal. For example, given the following grouped field set to be executed normally: @@ -268,14 +188,11 @@ For example, given the following grouped field set to be executed normally: ``` A valid GraphQL executor can resolve the four fields in whatever order it -chose. - - -### Serial Execution +chose (however of course `birthday` must be resolved before `month`, and +`address` before `street`). -Observe that based on the above sections, the only time an executor will run in -serial execution order is on the top level selection set of a mutation -operation and on its corresponding grouped field set. +When executing a mutation, the selections in the top most selection set will be +executed in serial order. When executing a grouped field set serially, the executor must consider each entry from the grouped field set in the order provided in the grouped field set. It must @@ -297,9 +214,9 @@ For example, given the following selection set to be executed serially: The executor must, in serial: - - Run {GetFieldEntry()} for `changeBirthday`, which during {CompleteValue()} + - Run {ExecuteField()} for `changeBirthday`, which during {CompleteValue()} will execute the `{ month }` sub-selection set normally. - - Run {GetFieldEntry()} for `changeAddress`, which during {CompleteValue()} + - Run {ExecuteField()} for `changeAddress`, which during {CompleteValue()} will execute the `{ street }` sub-selection set normally. As an illustrative example, let's assume we have a mutation field @@ -346,100 +263,182 @@ A correct executor must generate the following result for that selection set: ``` +### 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. This ensures all fields with the same response +key (alias or field name) included via referenced fragments are executed at the +same time. + +As an example, collecting the fields of this selection set would collect two +instances of the field `a` and one of field `b`: + +```graphql +{ + a { + subfield1 + } + ...ExampleFragment +} + +fragment ExampleFragment on Query { + a { + subfield2 + } + b +} +``` + +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): + + * If {visitedFragments} if not provided, initialize it to the empty set. + * Initialize {groupedFields} to an empty ordered map of lists. + * For each {selection} in {selectionSet}: + * 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}. + * 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}. + * If {selection} is a {Field}: + * Let {responseKey} be the response key of {selection}. + * 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}. + * 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 {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, visitedFragments)}. + * 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 + {responseKey}; if no such list exists, create it as an empty list. + * Append all items in {fragmentGroup} to {groupForResponseKey}. + * If {selection} is an {InlineFragment}: + * Let {fragmentType} be the type condition on {selection}. + * If {fragmentType} is not {null} and {DoesFragmentTypeApply(objectType, fragmentType)} is false, continue + with the next {selection} in {selectionSet}. + * Let {fragmentSelectionSet} be the top-level selection set of {selection}. + * Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, visitedFragments)}. + * 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 + {responseKey}; if no such list exists, create it as an empty list. + * Append all items in {fragmentGroup} to {groupForResponseKey}. + * Return {groupedFields}. + +DoesFragmentTypeApply(objectType, fragmentType): + + * If {fragmentType} is an Object Type: + * if {objectType} and {fragmentType} are the same type, return {true}, otherwise return {false}. + * If {fragmentType} is an Interface Type: + * if {objectType} is an implementation of {fragmentType}, return {true} otherwise return {false}. + * If {fragmentType} is a Union: + * if {objectType} is a possible type of {fragmentType}, return {true} otherwise return {false}. + + ## Executing Fields -Each item in the grouped field set can potentially create an entry in the -result map. That entry in the result map is the result of calling -{GetFieldEntry()} on the corresponding item in the grouped field set. -{GetFieldEntry()} can return {null}, which indicates that there should be no -entry in the result map for this item. Note that this is distinct from -returning an entry with a string key and a {null} value, which indicates that -an entry in the result should be added for that key, and its value should -be {null}. - -GetFieldEntry(objectType, objectValue, fields, variableValues): - - * Let {firstField} be the first entry in the ordered list {fields}. Note that - {fields} is never empty, as the entry in the grouped field set would not - exist if there were no fields. - * Let {responseKey} be the response key of {firstField}. Note: If an alias was - provided, it is used as the response key. - * Let {fieldName} be the name of {firstField}. Note: This value is unaffected - if an alias is provided. - * Let {fieldType} be the result of calling - {GetFieldTypeFromObjectType(objectType, fieldName)}. - * If {fieldType} is {null}, return {null}, indicating that no entry exists in - the result map. - * Let {resolvedValue} be {ResolveFieldValue(objectType, firstField, objectValue, variableValues)}. - * If {resolvedValue} is {null}, return {tuple(responseKey, null)}, - indicating that an entry exists in the result map whose value is `null`. - * Let {subSelectionSet} be the result of calling {MergeSelectionSets(fields)}. - * Let {responseValue} be the result of calling {CompleteValue(fieldType, resolvedValue, subSelectionSet, variableValues)}. - * Return {tuple(responseKey, responseValue)}. - -Every Object type is defined as a set of fields, each of which provides a return -type. {GetFieldTypeFromObjectType()} produces this type for a named field. - -GetFieldTypeFromObjectType(objectType, fieldName): - * Return the field type defined by {objectType} with the name {fieldName}. +Each field requested in the grouped field set that is defined on the selected +objectType will result in an entry in the response map. Field execution first +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): + * Let {field} be the first entry in {fields}. + * Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, variableValues)} + * Let {resolvedValue} be {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. + * Return the result of {CompleteValue(fieldType, fields, resolvedValue, variableValues)}. + + +### Coercing Field Arguments + +Fields may include arguments which are provided to the underlying runtime in +order to correctly produce a value. These arguments are defined by the field in +the type system to have a specific input type: Scalars, Enum, Input Object, or +List or Non-Null wrapped variations of these three. + +At each argument position in a query may be a literal value or a variable to be +provided at runtime. + +CoerceArgumentValues(objectType, field, variableValues): + * 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}. + * Let {coercedValues} be an empty unordered Map. + * For each {argumentDefinitions} as {argumentName} and {argumentType}: + * If no value was provided in {argumentValues} for the name {argumentName}: + * If {argumentType} is a Non-Nullable type, throw a field error. + * Otherwise, continue to the next argument definition. + * Let {value} be the value provided in {argumentValues} for the name {argumentName}. + * If {value} is a Variable: + * If a value exists in {variableValues} for the Variable {value}: + * Add an entry to {coercedValues} named {argName} with the + value of the Variable {value} found in {variableValues}. + * Otherwise if {value} can be coerced according to the input coercion rules + of {argType}: + * Let {coercedValue} be the result of coercing {value} according to the + input coercion rules of {argType}. + * Add an entry to {coercedValues} named {argName} with the + value {coercedValue}. + * Return {coercedValues}. + +Note: Variable values are not coerced because they are expected to be coerced +before executing the operation in {CoerceVariableValues()}, and valid queries +must only allow usage of variables of appropriate types. + + +### Value Resolution While nearly all of GraphQL execution can be described generically, ultimately the internal system exposing the GraphQL interface must provide values. This is exposed via {ResolveFieldValue}, which produces a value for a given field on a type for a real value. -As an example, this might accept the {objectType} `Person`, and the {fieldName} -{"soulMate"} and the {object} value representing John Lennon. It would be +As an example, this might accept the {objectType} `Person`, the {field} +{"soulMate"}, and the {objectValue} representing John Lennon. It would be expected to yield the value representing Yoko Ono. -Note: While described here in immediate procedural steps, it's common for this -operation to be asynchronous by relying on reading an underlying database or -networked service to produce a value. This necessitates the rest of a GraphQL -executor to handle an asynchronous execution flow. - -ResolveFieldValue(objectType, field, object, variableValues): - * Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, variableValues)} - * Let {fieldName} be the name of {field}. - * Call the internal function provided by {objectType} for determining the - resolved value of a field named {fieldName} on a given {object} - provided {argumentValues}. - -When more than one fields of the same name are executed in parallel, their -selection sets are merged together to produce a single result. - -An example query illustrating parallel fields with the same name: +ResolveFieldValue(objectType, objectValue, fieldName, argumentValues): + * Let {resolver} be the internal function provided by {objectType} for + determining the resolved value of a field named {fieldName}. + * Return the result of calling {resolver}, providing {objectValue} and {argumentValues}. -```graphql -{ - me { - firstName - } - me { - lastName - } -} -``` +Note: It is common for {resolver} to be asynchronous due to relying on reading +an underlying database or networked service to produce a value. This +necessitates the rest of a GraphQL executor to handle an asynchronous +execution flow. -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}. +### Value Completion 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, result, subSelectionSet, variableValues): +CompleteValue(fieldType, fields, result, variableValues): * 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, result, subSelectionSet)}. + {CompleteValue(innerType, fields, result, variableValues)}. * If {completedResult} is {null}, throw a field error. * Return {completedResult}. * If {result} is {null} (or another internal value similar to {null} such as @@ -447,8 +446,8 @@ CompleteValue(fieldType, result, subSelectionSet, variableValues): * If {fieldType} is a List type: * If {result} is not a collection of values, throw a field error. * Let {innerType} be the inner type of {fieldType}. - * Return a list where each item is the result of calling - {CompleteValue(innerType, resultItem, subSelectionSet)}, where + * Return a list where each list item is the result of calling + {CompleteValue(innerType, fields, resultItem, variableValues)}, where {resultItem} is each item in {result}. * If {fieldType} is a Scalar or Enum type: * Return the result of "coercing" {result}, ensuring it is a legal value of @@ -458,8 +457,11 @@ CompleteValue(fieldType, result, subSelectionSet, variableValues): * 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) *normally* (allowing for parallelization). +**Resolving Abstract Types** + When completing a field with an abstract return type, that is an Interface or Union return type, first the abstract type must be resolved to a relevant Object type. This determination is made by the internal system using whatever @@ -474,66 +476,58 @@ ResolveAbstractType(abstractType, objectValue): system for determining the Object type of {abstractType} given the value {objectValue}. -### Nullability - -If the result of resolving a field is `null` (either because the function to -resolve the field returned `null` or because an error occurred), and that -field is of a `Non-Null` type, then a field error is thrown. - -If the field was `null` because of an error which has already been added to the -`"errors"` list in the response, the `"errors"` list must not be -further affected. +**Merging Selection Sets** -If the field resolve function returned `null`, the resulting field error must be -added to the `"errors"` list in the response. +When more than one fields of the same name are executed in parallel, their +selection sets are merged together when completing the value in order to +continue execution of the sub-selection sets. +An example query illustrating parallel fields with the same name with +sub-selections. -### Error Handling +```graphql +{ + me { + firstName + } + me { + lastName + } +} +``` -If an error occurs when resolving a field, it should be treated as though -the field returned `null`, and an error must be added to the `"errors"` list -in the response. +After resolving the value for `me`, the selection sets are merged together so +`firstName` and `lastName` can be resolved for one value. -However, if the type of that field is of a `Non-Null` type, since the field -cannot be `null` the error is propagated to be dealt with by the parent field. +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}. -If all fields from the root of the request to the source of the error return -`Non-Null` types, then the `"data"` entry in the response should be `null`. +### Errors and Non-Nullability -### Coercing Field Arguments +If an error is thrown while resolving a field, it should be treated as though +the field returned {null}, and an error must be added to the {"errors"} list +in the response. -Fields may include arguments which are provided to the underlying runtime in -order to correctly produce a value. These arguments are defined by the field in -the type system to have a specific input type: Scalars, Enum, Input Object, or -List or Non-Null wrapped variations of these three. +If the result of resolving a field is {null} (either because the function to +resolve the field returned `null` or because an error occurred), and that +field is of a `Non-Null` type, then a field error is thrown. The +error must be added to the {"errors"} list in the response. -At each argument position in a query may be a literal value or a variable to be -provided at runtime. +If the field returns {null} because of an error which has already been added to +the {"errors"} list in the response, the {"errors"} list must not be +further affected. That is, only one error should be added to the errors list per +field. -CoerceArgumentValues(objectType, field, variableValues) - * 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}. - * Let {coercedValues} be an empty unordered Map. - * For each {argumentDefinitions} as {argumentName} and {argumentType}: - * If no value was provided in {argumentValues} for the name {argumentName}: - * If {argumentType} is a Non-Nullable type, throw a field error. - * Otherwise, continue to the next argument definition. - * Let {value} be the value provided in {argumentValues} for the name {argumentName}. - * If {value} is a Variable: - * If a value exists in {variableValues} for the Variable {value}: - * Add an entry to {coercedValues} named {argName} with the - value of the Variable {value} found in {variableValues}. - * Otherwise if {value} can be coerced according to the input coercion rules - of {argType}: - * Let {coercedValue} be the result of coercing {value} according to the - input coercion rules of {argType}. - * Add an entry to {coercedValues} named {argName} with the - value {coercedValue}. - * Return {coercedValues}. +Since `Non-Null` type fields cannot be {null}, field errors are propagated to be +handled by the parent field. If the parent field may be {null} then it resolves +to {null}, otherwise if it is a `Non-Null` type, the field error is further +propagated to it's parent field. -Note: Variable values are not coerced because they are expected to be coerced -before executing the request in {CoerceVariableValues()}, and valid queries must -only allow usage of variables of appropriate types. +If all fields from the root of the request to the source of the error return +`Non-Null` types, then the {"data"} entry in the response should be {null}. diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index d28fcf7f4..3c38df493 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -22,9 +22,9 @@ representations of the following four primitives: * Null Serialization formats which can represent an ordered map should preserve the -order of requested fields as defined by query execution. Serialization formats -which can only represent unordered maps should retain this order -grammatically (such as JSON). +order of requested fields as defined by {CollectFields()} in the Execution +section. Serialization formats which can only represent unordered maps should +retain this order grammatically (such as JSON). Producing a response where fields are represented in the same order in which they appear in the request improves human readability during debugging and From 6147aef5c9a22345aa02caff2f0046c124f8180f Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Fri, 21 Oct 2016 19:04:27 -0700 Subject: [PATCH 4/5] Note about the purpose of initial value --- spec/Section 6 -- Execution.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 6b023b3f8..ea5418ef2 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -7,8 +7,11 @@ A request for execution consists of a few pieces of information: * The schema to use, typically solely provided by the GraphQL service. * A Document containing GraphQL Operations and Fragments to execute. * Optionally: The name of the Operation in the Document to execute. -* Optionally: Values for Variables defined by the Operation. -* Optionally: An initial value corresponding to the root type being executed. +* Optionally: Values for any Variables defined by the Operation. +* An initial value corresponding to the root type being executed. + Conceptually, an initial value represents the "universe" of data available via + a GraphQL Service. It is common for a GraphQL Service to always use the same + initial value for every request. Given this information, the result of {ExecuteRequest()} produces the response, to be formatted according to the Response section below. From a82434f8952fb645e94ad7df4d43ed0d86edddab Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Mon, 24 Oct 2016 11:46:55 -0700 Subject: [PATCH 5/5] Add default value rules, further error throwing spots --- spec/Section 6 -- Execution.md | 59 ++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index ea5418ef2..1c214629a 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -81,19 +81,23 @@ CoerceVariableValues(schema, operation, variableValues): * For each {variableDefinition} in {variableDefinitions}: * Let {variableName} be the name of {variableDefinition}. * Let {variableType} be the expected type of {variableDefinition}. - * If no value was provided in {variableValues} for the name {variableName}: - * If {variableType} is a Non-Nullable type, throw a query error. - * Otherwise, continue to the next variable definition. + * Let {defaultValue} be the default value for {variableDefinition}. * Let {value} be the value provided in {variableValues} for the name {variableName}. - * If {value} cannot be coerced according to the - input coercion rules of {variableType}, throw a query error. + * If {value} does not exist (was not provided in {variableValues}): + * If {defaultValue} exists (including {null}): + * Add an entry to {coercedValues} named {variableName} with the + value {defaultValue}. + * Otherwise if {variableType} is a Non-Nullable type, throw a query error. + * Otherwise, continue to the next variable definition. + * Otherwise, if {value} cannot be coerced according to the input coercion + rules of {variableType}, throw a query error. * Let {coercedValue} be the result of coercing {value} according to the input coercion rules of {variableType}. - * Add an entry to {coercedValues} named {variableName} with the value {coercedValue}. + * Add an entry to {coercedValues} named {variableName} with the + value {coercedValue}. * Return {coercedValues}. -Note: This algorithm is very similar to {CoerceArgumentValues()}, however is -less forgiving of non-coerceable values. +Note: This algorithm is very similar to {CoerceArgumentValues()}. ## Executing Operations @@ -382,26 +386,39 @@ At each argument position in a query may be a literal value or a variable to be provided at runtime. CoerceArgumentValues(objectType, field, variableValues): + * 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}. - * Let {coercedValues} be an empty unordered Map. - * For each {argumentDefinitions} as {argumentName} and {argumentType}: - * If no value was provided in {argumentValues} for the name {argumentName}: - * If {argumentType} is a Non-Nullable type, throw a field error. - * Otherwise, continue to the next argument definition. + * For each {argumentDefinition} in {argumentDefinitions}: + * Let {argumentName} be the name of {argumentDefinition}. + * Let {argumentType} be the expected type of {argumentDefinition}. + * Let {defaultValue} be the default value for {argumentDefinition}. * Let {value} be the value provided in {argumentValues} for the name {argumentName}. * If {value} is a Variable: - * If a value exists in {variableValues} for the Variable {value}: + * Let {variableName} be the name of Variable {value}. + * Let {variableValue} be the value provided in {variableValues} for the name {variableName}. + * If {variableValue} exists (including {null}): * Add an entry to {coercedValues} named {argName} with the - value of the Variable {value} found in {variableValues}. - * Otherwise if {value} can be coerced according to the input coercion rules - of {argType}: - * Let {coercedValue} be the result of coercing {value} according to the - input coercion rules of {argType}. - * Add an entry to {coercedValues} named {argName} with the - value {coercedValue}. + value {variableValue}. + * Otherwise, if {defaultValue} exists (including {null}): + * Add an entry to {coercedValues} named {argName} with the + value {defaultValue}. + * Otherwise, if {argumentType} is a Non-Nullable type, throw a field error. + * Otherwise, continue to the next argument definition. + * Otherwise, if {value} does not exist (was not provided in {argumentValues}: + * If {defaultValue} exists (including {null}): + * Add an entry to {coercedValues} named {argName} with the + value {defaultValue}. + * Otherwise, if {argumentType} is a Non-Nullable type, throw a field error. + * Otherwise, continue to the next argument definition. + * Otherwise, if {value} cannot be coerced according to the input coercion + rules of {argType}, throw a field error. + * Let {coercedValue} be the result of coercing {value} according to the + input coercion rules of {argType}. + * Add an entry to {coercedValues} named {argName} with the + value {coercedValue}. * Return {coercedValues}. Note: Variable values are not coerced because they are expected to be coerced