diff --git a/spec/Appendix C -- Examples.md b/spec/Appendix C -- Examples.md new file mode 100644 index 000000000..6558dea98 --- /dev/null +++ b/spec/Appendix C -- Examples.md @@ -0,0 +1,191 @@ +# C. Appendix: Examples + +## Incremental Delivery Examples + +### Example 1 - A query containing both defer and stream + +```graphql example +query { + person(id: "cGVvcGxlOjE=") { + ...HomeWorldFragment @defer(label: "homeWorldDefer") + name + films @stream(initialCount: 1, label: "filmsStream") { + title + } + } +} +fragment HomeWorldFragment on Person { + homeWorld { + name + } +} +``` + +The incremental stream might look like: + +The initial response does not contain any deferred or streamed results in the +`data` entry. The initial response contains a `hasNext` entry, indicating that +subsequent responses will be delivered. There are two Pending Responses +indicating that results for both the `@defer` and `@stream` in the query will be +delivered in the subsequent responses. + +```json example +{ + "data": { + "person": { + "name": "Luke Skywalker", + "films": [{ "title": "A New Hope" }] + } + }, + "pending": [ + { "id": "0", "path": ["person"], "label": "homeWorldDefer" }, + { "id": "1", "path": ["person", "films"], "label": "filmsStream" } + ], + "hasNext": true +} +``` + +Subsequent response 1, contains the deferred data and the first streamed list +item. There is one Completed Result, indicating that the deferred data has been +completely delivered. + +```json example +{ + "incremental": [ + { + "id": "0", + "data": { "homeWorld": { "name": "Tatooine" } } + }, + { + "id": "1", + "items": [{ "title": "The Empire Strikes Back" }] + } + ], + "completed": [ + {"id": "0"} + ] + "hasNext": true +} +``` + +Subsequent response 2, contains the final stream results. In this example, the +underlying iterator does not close synchronously so {hasNext} is set to {true}. +If this iterator did close synchronously, {hasNext} would be set to {false} and +this would be the final response. + +```json example +{ + "incremental": [ + { + "id": "1", + "items": [{ "title": "Return of the Jedi" }] + } + ], + "hasNext": true +} +``` + +Subsequent response 3, contains no incremental data. {hasNext} set to {false} +indicates the end of the incremental stream. This response is sent when the +underlying iterator of the `films` field closes. + +```json example +{ + "hasNext": false +} +``` + +### Example 2 - A query containing overlapping defers + +```graphql example +query { + person(id: "cGVvcGxlOjE=") { + ...HomeWorldFragment @defer(label: "homeWorldDefer") + ...NameAndHomeWorldFragment @defer(label: "nameAndWorld") + firstName + } +} +fragment HomeWorldFragment on Person { + homeWorld { + name + terrain + } +} + +fragment NameAndHomeWorldFragment on Person { + firstName + lastName + homeWorld { + name + } +} +``` + +The incremental stream might look like: + +The initial response contains the results of the `firstName` field. Even though +it is also present in the `HomeWorldFragment`, it must be returned in the +initial response because it is also defined outside of any fragments with the +`@defer` directive. Additionally, There are two Pending Responses indicating +that results for both `@defer`s in the query will be delivered in the subsequent +responses. + +```json example +{ + "data": { + "person": { + "firstName": "Luke" + } + }, + "pending": [ + { "id": "0", "path": ["person"], "label": "homeWorldDefer" }, + { "id": "1", "path": ["person"], "label": "nameAndWorld" } + ], + "hasNext": true +} +``` + +Subsequent response 1, contains the deferred data from `HomeWorldFragment`. +There is one Completed Result, indicating that `HomeWorldFragment` has been +completely delivered. Because the `homeWorld` field is present in two separate +`@defer`s, it is separated into its own Incremental Result. + +The second Incremental Result contains the data for the `terrain` field. This +incremental result contains a `subPath` property to indicate to clients that the +path of this result can be determined by concatenating the path from the Pending +Result with id `"0"` and this `subPath` entry. + +```json example +{ + "incremental": [ + { + "id": "0", + "data": { "homeWorld": { "name": "Tatooine" } } + }, + { + "id": "0", + "subPath": ["homeWorld"], + "data": { "terrain": "desert" } + } + ], + "completed": [{ "id": "0" }], + "hasNext": true +} +``` + +Subsequent response 2, contains the remaining data from the +`NameAndHomeWorldFragment`. `lastName` is the only remaining field that has not +been delivered in a previous response. + +```json example +{ + "incremental": [ + { + "id": "1", + "data": { "lastName": "Skywalker" } + } + ], + "completed": [{ "id": "1" }], + "hasNext": false +} +``` diff --git a/spec/GraphQL.md b/spec/GraphQL.md index fad6bcdbe..45c9c24eb 100644 --- a/spec/GraphQL.md +++ b/spec/GraphQL.md @@ -139,3 +139,5 @@ Note: This is an example of a non-normative note. # [Appendix: Notation Conventions](Appendix%20A%20--%20Notation%20Conventions.md) # [Appendix: Grammar Summary](Appendix%20B%20--%20Grammar%20Summary.md) + +# [Appendix: Examples](Appendix%20C%20--%20Examples.md) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 04e4fa450..258e37532 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -794,8 +794,8 @@ And will yield the subset of each object type queried: When querying an Object, the resulting mapping of fields are conceptually ordered in the same order in which they were encountered during execution, excluding fragments for which the type does not apply and fields or fragments -that are skipped via `@skip` or `@include` directives. This ordering is -correctly produced when using the {CollectFields()} algorithm. +that are skipped via `@skip` or `@include` directives or postponed via `@defer`. +This ordering is correctly produced when using the {CollectFields()} algorithm. Response serialization formats capable of representing ordered maps should maintain this ordering. Serialization formats which can only represent unordered @@ -1948,6 +1948,14 @@ GraphQL implementations that support the type system definition language must provide the `@deprecated` directive if representing deprecated portions of the schema. +GraphQL implementations may provide the `@defer` and/or `@stream` directives. If +either or both of these directives are provided, they must conform to the +requirements defined in this specification. + +Note: The [Directives Are Defined](#sec-Directives-Are-Defined) validation rule +ensures that GraphQL Operations containing the `@defer` or `@stream` directives +cannot be executed by a GraphQL service that does not support them. + GraphQL implementations that support the type system definition language should provide the `@specifiedBy` directive if representing custom scalar definitions. @@ -2164,3 +2172,104 @@ to the relevant IETF specification. ```graphql example scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") ``` + +### @defer + +```graphql +directive @defer( + label: String + if: Boolean! = true +) on FRAGMENT_SPREAD | INLINE_FRAGMENT +``` + +The `@defer` directive may be provided on a fragment spread or inline fragment +to indicate that execution of the related selection set should be deferred. When +a request includes the `@defer` directive, the result may consist of multiple +responses: the initial response containing all non-deferred data, while +subsequent responses include deferred data. + +The `@include` and `@skip` directives take precedence over `@defer`. + +```graphql example +query myQuery($shouldDefer: Boolean! = true) { + user { + name + ...someFragment @defer(label: "someLabel", if: $shouldDefer) + } +} +fragment someFragment on User { + id + profile_picture { + uri + } +} +``` + +#### @defer Arguments + +- `if: Boolean! = true` - When `true`, fragment _should_ be deferred (see + related note below). When `false`, fragment must not be deferred. Defaults to + `true` when omitted. +- `label: String` - An optional string literal (variables are disallowed) used + by GraphQL clients to identify data from responses and associate it with the + corresponding defer directive. If provided, the GraphQL service must include + this label in the corresponding pending object within the response. The + `label` argument must be unique across all `@defer` and `@stream` directives + in the document. + +### @stream + +```graphql +directive @stream( + label: String + if: Boolean! = true + initialCount: Int = 0 +) on FIELD +``` + +The `@stream` directive may be provided for a field whose type incorporates a +`List` type modifier; the directive enables the backend to leverage technology +such as asynchronous iterators to provide a partial list initially, and +additional list items in subsequent responses. + +The `@include` and `@skip` directives take precedence over `@stream`. + +```graphql example +query myQuery($shouldStream: Boolean! = true) { + user { + friends(first: 10) { + nodes + @stream(label: "friendsStream", initialCount: 5, if: $shouldStream) { + name + } + } + } +} +``` + +#### @stream Arguments + +- `if: Boolean! = true` - When `true`, field _should_ be streamed (see related + note below). When `false`, the field must not be streamed and all list items + must be initially included. Defaults to `true` when omitted. +- `label: String` - An optional string literal (variables are disallowed) used + by GraphQL clients to identify data from responses and associate it with the + corresponding stream directive. If provided, the GraphQL service must include + this label in the corresponding pending object within the result. The `label` + argument must be unique across all `@defer` and `@stream` directives in the + document. +- `initialCount: Int` - The number of list items the service should return + initially. If omitted, defaults to `0`. A field error will be raised if the + value of this argument is less than `0`. + +Note: The ability to defer and/or stream parts of a response can have a +potentially significant impact on application performance. Developers generally +need clear, predictable control over their application's performance. It is +highly recommended that GraphQL services honor the `@defer` and `@stream` +directives on each execution. However, the specification allows advanced use +cases where the service can determine that it is more performant to not defer +and/or stream. Therefore, GraphQL clients _must_ be able to process a response +that ignores the `@defer` and/or `@stream` directives. This also applies to the +`initialCount` argument on the `@stream` directive. Clients _must_ be able to +process a streamed response that contains a different number of initial list +items than what was specified in the `initialCount` argument. diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 59c6bbb80..203411327 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -10,31 +10,74 @@ the case that any _field error_ was raised on a field and was replaced with ## Response Format -A response to a GraphQL request must be a map. +The result of a GraphQL request must be either a single initial response or an +incremental stream. The response will be an incremental stream when the GraphQL +service has deferred or streamed data as a result of the `@defer` or `@stream` +directives. When the result of the GraphQL operation is an incremental stream, +the first value will be an initial response, followed by one or more subsequent +responses. + +### Initial Response + +An initial response must be a map. If the request raised 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 request completed without raising any errors, this entry must not be present. -If the request included execution, the response map must contain an entry with -key `data`. The value of this entry is described in the "Data" section. If the -request failed before execution, due to a syntax error, missing information, or -validation error, this entry must not be present. +If the request included execution, the initial response map must contain an +entry with key `data`. The value of this entry is described in the "Data" +section. If the request failed before execution, due to a syntax error, missing +information, or validation error, this entry must not be present. + +When the result of the GraphQL operation is an incremental stream, the initial +response must contain an entry with key `hasNext`. The value of this entry must +be {true}. This entry must not be present for GraphQL operations that result in +a single initial response. -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 implementers to -extend the protocol however they see fit, and hence there are no additional -restrictions on its contents. +When the result of the GraphQL operation is an incremental stream, the initial +response may contain entries with the keys `pending`, `incremental`, and/or +`completed`. The value of these entries are described in the "Pending", +"Incremental", and "Completed" sections below. + +The initial 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 +implementers to extend the protocol however they see fit, and hence there are no +additional restrictions on its contents. To ensure future changes to the protocol do not break existing services and -clients, the top level response map must not contain any entries other than the -three described above. +clients, the initial response map must not contain any entries other than the +entries described above. -Note: When `errors` is present in the response, it may be helpful for it to -appear first when serialized to make it more clear when errors are present in a +Note: When `errors` is present in a response, it may be helpful for it to appear +first when serialized to make it more clear when errors are present in a response during debugging. +### Subsequent Response + +When the result of the GraphQL operation is an incremental stream, the first +value will be an initial response, followed by one or more subsequent responses. +A subsequent response must be a map. + +Each subsequent response must contain an entry with key `hasNext`. The value of +this entry must be {true} for all but the last response in the stream. The value +of this entry must be {false} for the last response of the stream. + +Each subsequent response may contain entries with the keys `pending`, +`incremental`, and/or `completed`. The value of these entries are described in +the "Pending", "Incremental", and "Completed" sections below. + +The subsequent 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 +implementers to extend the protocol however they see fit, and hence there are no +additional restrictions on its contents. Implementers may send subsequent +responses containing only `hasNext` and `extensions` entries. + +To ensure future changes to the protocol do not break existing services and +clients, the initial response map must not contain any entries other than the +entries described above. + ### Data The `data` entry in the response will be the result of the execution of the @@ -48,6 +91,10 @@ present in the response. If an error was raised during the execution that prevented a valid response, the `data` entry in the response should be `null`. +When the response of the GraphQL operation is an incremental stream, `data` may +only be present in the initial response. `data` must not be present in any +subsequent responses. + ### Errors The `errors` entry in the response is a non-empty list of errors raised during @@ -107,14 +154,8 @@ 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. - -If present, this field must 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 must be strings, and path segments that represent list -indices must be 0-indexed integers. If the error happens in an aliased field, -the path to the error must use the aliased name, since it represents a path in -the response, not in the request. +`null` result is intentional or caused by a runtime error. The value of this +field is described in the [Path](#sec-Path) section. For example, if fetching one of the friends' names fails in the following operation: @@ -244,6 +285,173 @@ 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 request. + +When the `path` field is present on an "Error result", it indicates the response +field which experienced the error. + +### Pending + +The `pending` entry in the response is a non-empty list of Pending Results. If +the response of the GraphQL operation is an incremental stream, this field may +appear on both the initial and subsequent responses. If present, the `pending` +entry must contain at least one Pending Result. + +Each Pending Result corresponds to either a `@defer` or `@stream` directive +located at a specific path in the response data. The Pending Result is used to +communicate that the GraphQL service has chosen to incrementally deliver the +data associated with this `@defer` or `@stream` directive and clients should +expect the associated data in either the current response, or one of the +following responses. + +**Pending Result Format** + +Every Pending Result must contain an entry with the key `id` with a string +value. This `id` should be used by clients to correlate Pending Results with +Completed Results. The `id` value must be unique for the entire response stream. +There must not be any other Pending Result in any response that contains the +same `id`. + +Every Pending Result must contain an entry with the key `path`. When the Pending +Result is associated with a `@stream` directive, it indicates the response list +field that is not known to be complete. Clients should expect the GraphQL +Service to incrementally deliver the remainder of indicated list field. When the +Pending Result is associated with a `@defer` directive, it indicates that the +response fields contained in the deferred fragment are not known to be complete. +Clients should expect the GraphQL Service to incrementally deliver the remainder +of the fields contained in the deferred fragment. + +If the associated `@defer` or `@stream` directive contains a `label` argument, +the Pending Result must contain an entry `label` with the value of this +argument. + +If a Pending Result is not returned for a `@defer` or `@stream` directive, +clients must assume that the GraphQL service chose not to incrementally deliver +this data, and the data can be found either in the `data` entry in the initial +response, or one of the Incremental Results in a prior subsequent response. + +### Incremental + +The `incremental` entry in the response is a non-empty list of Incremental +Results. If the response of the GraphQL operation is an incremental stream, this +field may appear on both the initial and subsequent responses. If present, the +`incremental` entry must contain at least one Incremental Result. + +The Incremental Result is used to deliver data that the GraphQL service has +chosen to incrementally deliver. An Incremental Result may be ether an +Incremental List Result or an Incremental Object Result. + +An Incremental List Result is used to deliver additional list items for a list +field with a `@stream` directive. + +An Incremental Object Result is used to deliver additional response fields that +were contained in one or more fragments with a `@defer` directive. + +**Incremental Result Format** + +Every Incremental Result must contain an entry with the key `id` with a string +value. This `id` must match the `id` that was returned in a prior Pending +Result. + +Additionally, Incremental List Results and Incremental Object Results have +further requirements. + +**Incremental List Result Format** + +An Incremental List Result's `id` entry must match the `id` that was returned in +a prior Pending Result. This Pending Result must be associated with a `@stream` +directive. + +The Incremental List Result's path can be determined using the prior Pending +Result with the same `id` as this Incremental Result. The Incremental List +Result's path is the same as the Pending Result's `path`. + +Every Incremental List Result must contain an `items` entry. The `items` entry +must contain a list of additional list items for the response field at the +Incremental List Result's `path`. This output will be a list of the same type of +the response field at this path. + +If any field errors were raised during the execution of the results in `items` +and these errors propagate to a path higher than the Incremental List Result's +path, The Incremental List Result is considered failed and should not be +included in the response stream. The errors that caused this failure will be +included in a Completed Result. + +If any field errors were raised during the execution of the results in `items` +and these errors did not propagate to a path higher than the Incremental List +Result's path, the Incremental List Result must contain an entry with key +`errors` containing these field errors. The value of this entry is described in +the "Errors" section. + +**Incremental Object Result Format** + +An Incremental Object Result's `id` entry must match the `id` that was returned +in a prior Pending Result. This Pending Result must be associated with a +`@defer` directive. + +The Incremental Object Result's path can be determined using the prior Pending +Result with the same `id` as this Incremental Result. The Incremental Object +Result may contain a `subPath` entry. If the `subPath` entry is present, The +Incremental Object Result's path can be determined by concatenating the Pending +Result's `path` with this `subPath`. If no `subPath` entry is present, the path +is the same as the Pending Result's `path`. + +Every Incremental Object Result must contain a `data` entry. The `data` entry +must contain a map of additional response fields. The `data` entry in an +Incremental Object Result will be of the type of a particular field in the +GraphQL result. The Incremental Object Result's path will contain the path +segments of the field this data is associated with. + +An Incremental Object Result's data may contain response fields that were +contained in more than one deferred fragments. In that case, the `id` of the +Incremental Object Result must point to the Pending Result that results in the +shortest path. + +If any field errors were raised during the execution of the results in `data` +and these errors propagated to a path higher than the Incremental Object +Result's path, The Incremental Object Result is considered failed and should not +be included in the response stream. The errors that caused this failure will be +included in a Completed Result. + +If any field errors were raised during the execution of the results in `data` +and these errors did not propagate to a path higher than the Incremental Object +Result's path, the Incremental Object Result must contain an entry with key +`errors` containing these field errors. The value of this entry is described in +the "Errors" section. + +### Completed + +The `completed` entry in the response is a non-empty list of Completed Results. +If the response of the GraphQL operation is an incremental stream, this field +may appear on both the initial and subsequent responses. If present, the +`completed` entry must contain at least one Completed Result. + +Each Completed Result corresponds to a prior Pending Result. The Completed +Result is used to communicate that the GraphQL service has completed the +incremental delivery of the data associated with the corresponding Pending +Result. The associated data must have been completed in the current response. + +**Completed Result Format** + +Every Completed Result must contain an entry with the key `id` with a string +value. The `id` entry must match the `id` that was returned in a prior Pending +Result. + +A Completed Result may contain an `errors` entry. When the `errors` entry is +present, it informs clients that the delivery of the data associated with the +corresponding Pending Result has failed, due to an error bubbling to a path +higher than the Incremental Data Result's path. The `errors` entry must contain +these field errors. The value of this entry is described in the "Errors" +section. + ## Serialization Format GraphQL does not require a specific serialization format. However, clients