Skip to content

Simplify the spec to only include the ! nullability designator #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: clientControlledNullability
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions spec/Appendix B -- Grammar Summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Token ::
- FloatValue
- StringValue

Punctuator :: one of ! ? $ & ( ) ... : = @ [ ] { | }
Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | }

Name ::

Expand Down Expand Up @@ -170,7 +170,6 @@ ListNullability : `[` Nullability? `]`
NullabilityDesignator :

- `!`
- `?`

Arguments[Const] : ( Argument[?Const]+ )

Expand Down
69 changes: 24 additions & 45 deletions spec/Section 2 -- Language.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ characters are permitted between the characters defining a {FloatValue}.

### Punctuators

Punctuator :: one of ! ? $ & ( ) ... : = @ [ ] { | }
Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | }

GraphQL documents include punctuation in order to describe structure. GraphQL is
a data description language and not a programming language, therefore GraphQL
Expand Down Expand Up @@ -527,25 +527,19 @@ ListNullability : `[` Nullability? `]`
NullabilityDesignator :

- `!`
- `?`

Fields can have their nullability designated with either a `!` to indicate that
a field should be `Non-Nullable` or a `?` to indicate that a field should be
`Nullable`. These designators override the nullability set on a field by the
schema for the operation where they're being used. In addition to being
`Non-Nullable`, if a field marked with `!` resolves to `null`, it propagates to
the nearest parent field marked with a `?` or to `data` if there is not a parent
marked with a `?`. An error is added to the `errors` array identical to if the
field had been `Non-Nullable` in the schema.
Fields can have their nullability designated with a `!` to indicate that a field
should be `Non-Nullable`. These designators override the nullability set on a
field by the schema for the operation where they're being used. If a field
marked with `!` resolves to `null`, it behaves as if the field had been
`Non-Nullable` in the schema.

In this example, we can indicate that a `user`'s `name` that could possibly be
`null`, should not be `null` and that `null` propagation should halt at the
`user` field. We can use `?` to create null propagation boundary. `user` will be
treated as `Nullable` for this operation:
`null`, should not be `null`:

```graphql example
{
user(id: 4)? {
user(id: 4) {
id
name!
}
Expand Down Expand Up @@ -583,62 +577,47 @@ marked `Non-Nullable` in the schema:
}
```

If `!` is used on a field and it is not paired with `?` on a parent, then `null`
will propagate all the way to the `data` response field.
Nullability designators can also be applied to list elements like so.

```graphql example
{
user(id: 4) {
id
name!
petsNames[!]
}
}
```

Response:
In the above example, the query author is saying that each individual pet name
should be `Non-Nullable`. The same syntax can be applied to multidimensional
lists.

```json example
```graphql example
{
"data": null,
"errors": [
{
"locations": [{ "column": 13, "line": 4 }],
"message": "Cannot return null for non-nullable field User.name.",
"path": ["user", "name"]
}
]
threeDimensionalMatrix[[[]!]]!
}
```

Nullability designators can also be applied to list elements like so.
Any field without a nullability designator will inherit its nullability from the
schema definition. When designating nullability for list fields, query authors
can either use the designator `!` to designate the nullability of the entire
field, or they can use the list element nullability syntax displayed above. The
number of dimensions indicated by list element nullability syntax cannot exceed
the number of dimensions of the field. Anything else results in a query
validation error. Both are valid examples:

```graphql example
{
user(id: 4)? {
id
petsNames[!]?
}
threeDimensionalMatrix[[[!]]]
}
```

In the above example, the query author is saying that each individual pet name
should be `Non-Nullable`, but the list as a whole should be `Nullable`. The same
syntax can be applied to multidimensional lists.

```graphql example
{
threeDimensionalMatrix[[[?]!]]!
threeDimensionalMatrix[[[!]!]!]!
}
```

Any field without a nullability designator will inherit its nullability from the
schema definition. When designating nullability for list fields, query authors
can either use a single designator (`!` or `?`) to designate the nullability of
the entire field, or they can use the list element nullability syntax displayed
above. The number of dimensions indicated by list element nullability syntax is
required to match the number of dimensions of the field. Anything else results
in a query validation error.

## Fragments

FragmentSpread : ... FragmentName Directives?
Expand Down
15 changes: 8 additions & 7 deletions spec/Section 5 -- Validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -592,18 +592,19 @@ fragment conflictingDifferingResponses on Pet {
- If {designatorDepth} is 0
- return true
- Let {typeDepth} be the number of list dimensions in {fieldType}
- If {typeDepth} equals {designatorDepth} or {designatorDepth} equals 0 return
true
- Otherwise return false
- If {designatorDepth} exceeds {typeDepth} return false
- Otherwise return true

**Explanatory Text**

List fields can be marked with nullability designators that look like `[?]!` to
List fields can be marked with nullability designators that look like `[!]!` to
indicate the nullability of the list's elements and the nullability of the list
itself. For multi-dimensional lists, the designator would look something like
`[[[!]?]]!`. If any `ListNullability` operators are used then the number of
dimensions of the designator are required to match the number of dimensions of
the field's type. If the two do not match then a validation error is thrown.
`[[[!]]]!`. If any `ListNullability` operators are used then the number of
dimensions of the designator are required to be less than or equal to the number
of dimensions of the field's type. If the number of dimensions of the designator
exceed the number of dimensions of the field's type, then a validation error is
thrown.

### Leaf Field Selections

Expand Down
101 changes: 23 additions & 78 deletions spec/Section 6 -- Execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,91 +564,35 @@ 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. `ccnPropagationPairs` is an unordered map where
the keys are paths of required fields, and values are paths of the nearest
optional parent to those required fields. `currentPropagationPath` starts as an
empty path to indicate that `null` propagation should continue until it hits
`data` if there is no optional field.
set or coercing a scalar value.

ExecuteField(objectType, objectValue, fieldType, fields, variableValues,
currentPropagationPath, ccnPropagationPairs):
ExecuteField(objectType, objectValue, fieldType, fields, variableValues):

- Let {field} be the first entry in {fields}.
- Let {fieldName} be the field name of {field}.
- Let {requiredStatus} be the required status of {field}.
- Let {newPropagationPath} be {path} if {requiredStatus} is optional, otherwise
let {newPropagationPath} be {currentPropagationPath}
- If {requiredStatus} is optional:
- Let {newPropagationPath} be {path}
- If {requiredStatus} is required:
- Set {path} to {newPropagationPath} in {ccnPropagationPairs}
- Let {nullabilityAssertionNode} be the nullability assertion node of
{fieldName}.
- Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field,
variableValues)}
variableValues)}.
- Let {resolvedValue} be {ResolveFieldValue(objectType, objectValue, fieldName,
argumentValues)}.
- Let {modifiedFieldType} be {ApplyRequiredStatus(fieldType, requiredStatus)}.
- Return the result of {CompleteValue(modifiedFieldType, fields, resolvedValue,
variableValues, newPropagationPath, ccnPropagationPairs)}.
- Let {returnType} be {SimpleTypeTransfer(fieldType, nullabilityAssertionNode)}.
- Return the result of {CompleteValue(fieldType, fields, resolvedValue,
variableValues)}.

## Accounting For Client Controlled Nullability Designators

A field can have its nullability status set either in its service's schema, or a
nullability designator (`!` or `?`) can override it for the duration of an
execution. In order to determine a field's true nullability, both are taken into
account and a final type is produced. A field marked with a `!` is called a
"required field" and a field marked with a `?` is called an optional field.

ApplyRequiredStatus(type, requiredStatus):

- If there is no {requiredStatus}:
- return {type}
- If {requiredStatus} is not a list:
- If {requiredStatus} is required:
- return a `Non-Null` version of {type}
- If {requiredStatus} is optional:
- return a nullable version of {type}
- Create a {stack} initially containing {type}.
- As long as the top of {stack} is a list:
- Let {currentType} be the top item of {stack}.
- Push the {elementType} of {currentType} to the {stack}.
- If {requiredStatus} exists:
- Start visiting {node}s in {requiredStatus} and building up a
{resultingType}:
- For each {node} that is a RequiredDesignator:
- If {resultingType} exists:
- Let {nullableResult} be the nullable type of {resultingType}.
- Set {resultingType} to the Non-Nullable type of {nullableResult}.
- Continue onto the next node.
- Pop the top of {stack} and let {nextType} be the result.
- Let {nullableType} be the nullable type of {nextType}.
- Set {resultingType} to the Non-Nullable type of {nullableType}.
- Continue onto the next node.
- For each {node} that is a OptionalDesignator:
- If {resultingType} exists:
- Set {resultingType} to the nullableType type of {resultingType}.
- Continue onto the next node.
- Pop the top of {stack} and let {nextType} be the result.
- Set {resultingType} to the nullable type of {resultingType}
- Continue onto the next node.
- For each {node} that is a ListNullabilityDesignator:
- Pop the top of {stack} and let {listType} be the result
- If the nullable type of {listType} is not a list
- Pop the top of {stack} and set {listType} to the result
- If {listType} does not exist:
- Raise a field error because {requiredStatus} had more list dimensions
than {outputType} and is invalid.
- If {resultingType} exist:
- If {listType} is Non-Nullable:
- Set {resultingType} to a Non-Nullable list where the element is
{resultingType}.
- Otherwise:
- Set {resultingType} to a list where the element is {resultingType}.
- Continue onto the next node.
- Set {resultingType} to {listType}
- If {stack} is not empty:
- Raise a field error because {requiredStatus} had fewer list dimensions than
{outputType} and is invalid.
- Return {resultingType}.
nullability designator (`!`) can override it for the duration of an execution.
In order to determine a field's true nullability, both are taken into account
and a final type is produced. A field marked with a `!` is called a "required
field".

SimpleTypeTransfer(fieldType, nullabilityAssertionNode):

- If {nullabilityAssertionNode} is a {Non-Null NullabilityDesignator}
- Return the Non-Nullable type of {fieldType}.
- Return {fieldType}.

### Coercing Field Arguments

Expand Down Expand Up @@ -748,8 +692,12 @@ CompleteValue(fieldType, fields, result, variableValues):
- If {fieldType} is a List type:
- If {result} is not a collection of values, raise a field error.
- Let {innerType} be the inner type of {fieldType}.
- Let {nullabilityAssertionNode} be the nullability assertion node of
{innerType}.
- Let {returnType} be {SimpleTypeTransfer(innerType,
nullabilityAssertionNode)}.
- Return a list where each list item is the result of calling
{CompleteValue(innerType, fields, resultItem, variableValues)}, where
{CompleteValue(returnType, fields, resultItem, variableValues)}, where
{resultItem} is each item in {result}.
- If {fieldType} is a Scalar or Enum type:
- Return the result of {CoerceResult(fieldType, result)}.
Expand Down Expand Up @@ -866,9 +814,6 @@ 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 its parent field.

If a required field resolves to {null}, propagation instead happens until an
optional field is found.

If a `List` type wraps a `Non-Null` type, and one of the elements of that list
resolves to {null}, then the entire list must resolve to {null}. If the `List`
type is also wrapped in a `Non-Null`, the field error continues to propagate
Expand Down
8 changes: 3 additions & 5 deletions spec/Section 7 -- Response.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,9 @@ The response might look like:
```

If the field which experienced an error was declared as `Non-Null`, the `null`
result will propagate to the next nullable field. If it was marked with a
required designator, then it will propagate to the nearest optional parent field
instead. In either case, the `path` for the error should include the full path
to the result field where the error was raised, even if that field is not
present in the response.
result will propagate to the next nullable field. In that case, the `path` for
the error should include the full path to the result field where the error was
raised, even if that field is not present in the response.

For example, if the `name` field from above had declared a `Non-Null` return
type in the schema, the result would look different but the error reported would
Expand Down