Skip to content

Commit

Permalink
Merge pull request #1 from robrichard/updates
Browse files Browse the repository at this point in the history
hasNext, initialCount, and updates to Response section
  • Loading branch information
robrichard authored Jul 2, 2020
2 parents 00a6556 + 1806812 commit eab9204
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 39 deletions.
9 changes: 5 additions & 4 deletions rfcs/DeferStream.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Each subsequent payload will be an object with the following properties
* `label`: The string that was passed to the label argument of the `@defer` or `@stream` directive that corresponds to this results.
* `data`: The data that is being delivered incrementally.
* `path`: a list of keys (with plural indexes) from the root of the response to the insertion point that informs the client how to patch a subsequent delta payload into the original payload.
* `isFinal`: A boolean that is present and `false` when there are more payloads that will be sent for this operation.
* `hasNext`: A boolean that is present and `true` when there are more payloads that will be sent for this operation. The last payload in a multi payload response should return `hasNext: false`. `hasNext` is not required for single-payload responses to preserve backwards compatibility.
* `errors`: An array that will be present and contain any field errors that are produced while executing the deferred or streamed selection set.
* `extensions`: For implementors to extend the protocol

Expand Down Expand Up @@ -104,30 +104,31 @@ fragment GroupAdminFragment {
// payload 1
{
data: {id: 1},
isFinal: false
hasNext: true
}
// payload 2
{
label: "friendStream"
path: [“viewer”, “friends”, 1],
data: {id: 4},
isFinal: false
hasNext: true
}
// payload 3
{
label: "friendStream"
path: [“viewer”, “friends”, 2],
data: {id: 5},
isFinal: false
hasNext: true
}
// payload 4
{
label: "groupAdminDefer",
path: [“viewer”],
data: {managed_groups: [{id: 1, id: 2}]}
hasNext: false
}
```

Expand Down
19 changes: 12 additions & 7 deletions spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
Expand Up @@ -1753,7 +1753,12 @@ A GraphQL schema describes directives which are used to annotate various parts
of a GraphQL document as an indicator that they should be evaluated differently
by a validator, executor, or client tool such as a code generator.

GraphQL implementations should provide the `@skip`, `@include`, `@defer` and `@stream` directives.
GraphQL implementations should provide the `@skip` and `@include` directives.

GraphQL implementations are not required to implement the `@defer` and `@stream`
directives. If they are implemented, they must be implemented according to the
specification. GraphQL implementations that do not support these directives must
not make them available via introspection.

GraphQL implementations that support the type system definition language must
provide the `@deprecated` directive if representing deprecated portions of
Expand Down Expand Up @@ -1924,14 +1929,14 @@ type ExampleType {

### @defer
```graphql
directive @defer(label: String!, if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @defer(label: String, if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT
```
The `@defer` directive may be provided for fragment spreads and inline fragments to
inform the executor to delay the execution of the current fragment to indicate
deprioritization of the current fragment. A query with `@defer` directive will cause
the request to potentially return multiple responses, where non-deferred data is
delivered in the initial response and data deferred delivered in a subsequent response.
`@include` and `@skip` take presedence over `@defer`.
delivered in the initial response and data deferred is delivered in a subsequent
response. `@include` and `@skip` take presedence over `@defer`.

```graphql example
query myQuery($shouldDefer: Boolean) {
Expand All @@ -1950,17 +1955,17 @@ fragment someFragment on User {

### @stream
```graphql
directive @stream(label: String!, initial_count: Int!, if: Boolean) on FIELD
directive @stream(label: String, initialCount: Int!, if: Boolean) on FIELD
```
The `@stream` directive may be provided for a field of `List` type so that the
backend can leverage technology such asynchronous iterators to provide a partial
backend can leverage technology such as asynchronous iterators to provide a partial
list in the initial response, and additional list items in subsequent responses.
`@include` and `@skip` take presedence over `@stream`.
```graphql example
query myQuery($shouldDefer: Boolean) {
user {
friends(first: 10) {
nodes @stream(label: "friendsStream", initial_count: 5)
nodes @stream(label: "friendsStream", initialCount: 5)
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions spec/Section 5 -- Validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ FieldsInSetCanMerge(set):
{set} including visiting fragments and inline fragments.
* Given each pair of members {fieldA} and {fieldB} in {fieldsForName}:
* {SameResponseShape(fieldA, fieldB)} must be true.
* {SameStreamDirective(fieldA, fieldB)} must be true.
* If the parent types of {fieldA} and {fieldB} are equal or if either is not
an Object Type:
* {fieldA} and {fieldB} must have identical field names.
Expand Down Expand Up @@ -444,6 +445,16 @@ SameResponseShape(fieldA, fieldB):
* If {SameResponseShape(subfieldA, subfieldB)} is false, return false.
* Return true.

SameStreamDirective(fieldA, fieldB):

* If neither {fieldA} nor {fieldB} has a directive named `stream`.
* Return true.
* If both {fieldA} and {fieldB} have a directive named `stream`.
* Let {streamA} be the directive named `stream` on {fieldA}.
* Let {streamB} be the directive named `stream` on {fieldB}.
* If {streamA} and {streamB} have identical sets of arguments, return true.
* Return false.

**Explanatory Text**

If multiple field selections with the same response names are encountered
Expand Down
42 changes: 24 additions & 18 deletions spec/Section 6 -- Execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ request is determined by the result of executing this operation according to the
"Executing Operations” section below.

ExecuteRequest(schema, document, operationName, variableValues, initialValue):
Note: the execution assumes implementing language support coroutines.
Note: the execution assumes implementing language supports coroutines.
Alternatively, the socket can provide a write buffer pointer to allow {ExecuteRequest()}
to directly write payloads into the buffer.
* Let {operation} be the result of {GetOperation(document, operationName)}.
Expand Down Expand Up @@ -141,6 +141,10 @@ ExecuteQuery(query, schema, variableValues, initialValue):
* For each {payload} in {subsequentPayloads}:
* If {payload} is a Deferred Fragment Record:
* Yield the value from calling {ResolveDeferredFragmentRecord(payload, variableValues, subsequentPayloads)}.
* If {payload} is not the final payload in {subsequentPayloads}
* Add an entry to {payload} named `hasNext` with the value {true}.
* If {payload} is the final payload in {subsequentPayloads}
* Add an entry to {payload} named `hasNext` with the value {false}.
* If {payload} is a Stream Record:
* Yield ResolveStreamRecord(payload, variableValues, subsequentPayloads).

Expand Down Expand Up @@ -333,7 +337,7 @@ response map.
ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues, subsequentPayloads, parentPath):

* If {subsequentPayloads} is not provided, initialize it to the empty set.
* If {parentPath} is not provided, initialize it to an emtpy list.
* If {parentPath} is not provided, initialize it to an empty list.
* Let {groupedFieldSet} be the result of
{CollectFields(objectType, objectValue, selectionSet, variableValues, subsequentPayloads, parentPath)}.
* Initialize {resultMap} to an empty ordered map.
Expand Down Expand Up @@ -464,11 +468,13 @@ Before execution, the selection set is converted to a grouped field set by
calling {CollectFields()}. Each entry in the grouped field set is a list of
fields that share a response key (the alias if defined, otherwise the field
name). This ensures all fields with the same response key included via
referenced fragments are executed at the same time. A deferred seclection set's
fields will not be included in the grouped field set. Rather, a record
representing the deferred fragment and addition context will be stored in a
list. The executor revisits and resume execution for the list of deferred
fragment records after the initial execution finishes.
referenced fragments are executed at the same time. A deferred selection
set's fields will not be included in the grouped field set. Rather, a record
representing the deferred fragment and additional context will be stored in a
list. The executor revisits and resumes execution for the list of deferred
fragment records after the initial execution is initiated. This deferred
execution would ‘re-execute’ fields with the same response key that were
present in the grouped field set.


As an example, collecting the fields of this selection set would collect two
Expand Down Expand Up @@ -607,7 +613,7 @@ ExecuteField(objectType, objectValue, fieldType, fields, variableValues, subsequ
* Let {fieldName} be the field name of {field}.
* Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, variableValues)}
* If {field} provides the directive `@stream`, let {streamDirective} be that directive.
* Let {initialCount} be the value or variable provided to {streamDirective}'s {initial_count} argument.
* Let {initialCount} be the value or variable provided to {streamDirective}'s {initialCount} argument.
* Let {resolvedValue} be {ResolvedFieldGenerator(objectType, objectValue, fieldName, argumentValues, initialCount)}.
* Let {result} be the result of calling {CompleteValue(fieldType, fields, resolvedValue, variableValues, subsequentPayloads, parentPath)}.
* Append {fieldName} to the {path} field of every {subsequentPayloads}.
Expand Down Expand Up @@ -682,16 +688,16 @@ This is exposed via {ResolveFieldValue}, which produces a value for a given
field on a type for a real value. In addition, {ResolveFieldGenerator} will be
exposed to produce an iterator for a field with `List` return type.
The internal system may optionally define a generator function. In the case
where the generator is not defined, the GraphQL executor provide a default generator.
For example, a trivial generator that yield the entire list upon the first iteration.
where the generator is not defined, the GraphQL executor provides a default generator.
For example, a trivial generator that yields the entire list upon the first iteration.

As an example, a {ResolveFieldValue} 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.

A {ResolveFieldGenerator} might accept the {objectType} `MusicBand`, the {field}
{"members"}, and the {objectValue} representing Beatles. It would be expected to yield
a iterator of values representing, John Lennon, Paul, McCartney, Ringo Starr and
a iterator of values representing, John Lennon, Paul McCartney, Ringo Starr and
George Harrison.

ResolveFieldValue(objectType, objectValue, fieldName, argumentValues):
Expand All @@ -701,7 +707,7 @@ ResolveFieldValue(objectType, objectValue, fieldName, argumentValues):

ResolveFieldGenerator(objectType, objectValue, fieldName, argumentValues, initialCount):
* If {objectType} provide an internal function {generatorResolver} for
generating partitially resolved valueof a list field named {fieldName}:
generating partitially resolved value of a list field named {fieldName}:
* Let {generatorResolver} be the internal function.
* Return the iterator from calling {generatorResolver}, providing
{objectValue}, {argumentValues} and {initialCount}.
Expand All @@ -712,7 +718,7 @@ 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. In addition, a commom implementation of {generator} is to leverage
asynchronos iterators or asynchronos generators provided by many programing languages.
asynchronous iterators or asynchronous generators provided by many programming languages.

### Value Completion

Expand All @@ -721,7 +727,7 @@ to the expected return type. If the return type is another Object type, then
the field execution process continues recursively. In the case where a value
returned for a list type field is an iterator due to `@stream` specified on the
field, value completition iterates over the iterator until the number of items
yield by the iterator satisfies `initial_count` specified on the `@stream` directive.
yield by the iterator satisfies `initialCount` specified on the `@stream` directive.
Unresolved items in the iterator will be stored in a stream record which the executor
resumes to execute after the initial execution finishes.

Expand Down Expand Up @@ -765,11 +771,11 @@ CompleteValue(fieldType, fields, result, variableValues, subsequentPayloads, par
* 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 a iterator:
* Let {field} be thte first entry in {fields}.
* If {result} is an iterator:
* Let {field} be the first entry in {fields}.
* Let {innerType} be the inner type of {fieldType}.
* Let {streamDirective} be the `@stream` directived provided on {field}.
* Let {initialCount} be the value or variable provided to {streamDirective}'s {initial_count} argument.
* Let {initialCount} be the value or variable provided to {streamDirective}'s {initialCount} argument.
* Let {label} be the value or variable provided to {streamDirective}'s {label} argument.
* Let {resolvedItems} be an empty list
* For each {members} in {result}:
Expand All @@ -780,7 +786,7 @@ CompleteValue(fieldType, fields, result, variableValues, subsequentPayloads, par
* Let {streamRecord} be the result of calling {CreateStreamRecord(label, initialCount, result, remainingItems, initialCount, fields, innerType, parentPath)}
* Append {streamRecord} to {subsequentPayloads}.
* Let {result} be {initialItems}.
* Exit For each loop.
* Exit for each loop.
* 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 list item is the result of calling
Expand Down
61 changes: 51 additions & 10 deletions spec/Section 7 -- Response.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ request.
A response may contain both a partial response as well as encountered errors in
the case that a field error occurred on a field which was replaced with {null}.

A response to a GraphQL operation must be a map or an event stream of maps. The
value of this map is described in the "Response Format" section.

## Response Format

A response to a GraphQL operation must be a map.

If the operation encountered any errors, the response map must contain an
entry with key `errors`. The value of this entry is described in the "Errors"
section. If the operation completed without encountering any errors, this entry
Expand All @@ -23,6 +23,24 @@ with key `data`. The value of this entry is described in the "Data" section. If
the operation failed before execution, due to a syntax error, missing
information, or validation error, this entry must not be present.

When the response of the GraphQL operation is an event stream, the first value
will be the initial response. All subsequent values may contain `label` and
`path` entries. These two entries are used by clients to identify the the
`@defer` or `@stream` directive from the GraphQL operation that triggered this
value to be returned by the event stream. The combination of these two entries
must be unique across all values returned by the event stream.

If the response of the GraphQL operation is an event stream, each response map
must contain an entry with key `hasNext`. The value of this entry is `true` for
all but the last response in the stream. The value of this entry is `false` for
the last response of the stream. This entry is not required for GraphQL
operations that return a single response map.

The GraphQL server may determine there are no more values in the event stream
after a previous value with `hasNext`: `true` has been emitted. In this case
the last value in the event stream should be a map without `data`, `label`,
and `path` entries, and a `hasNext` entry with a value of `false`.

The response map may also contain an entry with key `extensions`. This entry,
if set, must have a map as its value. This entry is reserved for implementors
to extend the protocol however they see fit, and hence there are no additional
Expand All @@ -43,6 +61,11 @@ requested operation. If the operation was a query, this output will be an
object of the schema's query root type; if the operation was a mutation, this
output will be an object of the schema's mutation root type.

If the result of the operation is an event stream, the `data` entry in
subsequent values will be an object of the type of a particular field in the
GraphQL result. The adjacent `path` field will contain the path segments of
the field this data is associated with.

If an error was encountered before execution begins, the `data` entry should
not be present in the result.

Expand Down Expand Up @@ -82,14 +105,8 @@ associated syntax element.
If an error can be associated to a particular field in the GraphQL result, it
must contain an entry with the key `path` that details the path of the
response field which experienced the error. This allows clients to identify
whether a `null` result is intentional or caused by a runtime error.

This field should be a list of path segments starting at the root of the
response and ending with the field associated with the error. Path segments
that represent fields should be strings, and path segments that
represent list indices should be 0-indexed integers. If the error happens
in an aliased field, the path to the error should use the aliased name, since
it represents a path in the response, not in the query.
whether a `null` result is intentional or caused by a runtime error. The value
of this field is described in the "Path" section.

For example, if fetching one of the friends' names fails in the following
query:
Expand Down Expand Up @@ -220,6 +237,30 @@ still discouraged.
```


## Path

A `path` field allows for the association to a particular field in a GraphQL
result. This field should be a list of path segments starting at the root of the
response and ending with the field to be associated with. Path segments
that represent fields should be strings, and path segments that
represent list indices should be 0-indexed integers. If the path is associated to
an aliased field, the path should use the aliased name, since it represents a path in the response, not in the query.

When the `path` field is present on a GraphQL response, it indicates that the
`data` field is not the root query or mutation result, but is rather associated to
a particular field in the root result.

When the `path` field is present on an "Error result", it indicates the response field which experienced the error.

## Label

If the response of the GraphQL operation is an event stream, subsequent values
may contain a string field `label`. This `label` is the same label passed to
the `@defer` or `@stream` directive that triggered this value. This allows
clients to identify which `@defer` or `@stream` directive is associated with
this value. `label` will not be present if the corresponding `@defer` or
`@stream` directive is not passed a `label` argument.

## Serialization Format

GraphQL does not require a specific serialization format. However, clients
Expand Down

0 comments on commit eab9204

Please sign in to comment.