From abafb767fab86a71b2875f986f53af346fa0e70a Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Fri, 1 Nov 2024 11:40:43 -0400 Subject: [PATCH 1/8] Spec edits for incremental delivery, Section 3 & 7 only --- cspell.yml | 1 + spec/Section 3 -- Type System.md | 106 +++++++- spec/Section 7 -- Response.md | 402 ++++++++++++++++++++++++++++++- 3 files changed, 496 insertions(+), 13 deletions(-) diff --git a/cspell.yml b/cspell.yml index 0ea1def96..ec8eaabc2 100644 --- a/cspell.yml +++ b/cspell.yml @@ -4,6 +4,7 @@ ignoreRegExpList: - /[a-z]{2,}'s/ words: # Terms of art + - deprioritization - endianness - interoperation - monospace diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 4f9356f0c..d394121c3 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -794,8 +794,9 @@ 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 temporarily skipped 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 @@ -1942,6 +1943,11 @@ by a validator, executor, or client tool such as a code generator. GraphQL implementations should provide the `@skip` and `@include` directives. +GraphQL implementations are not required to implement the `@defer` and `@stream` +directives. If either or both of these directives are implemented, they must be +implemented according to this 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 the schema. @@ -2162,3 +2168,99 @@ 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 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 is +delivered in a subsequent response. `@include` and `@skip` take precedence over +`@defer`. + +```graphql example +query myQuery($shouldDefer: Boolean) { + 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 will not be deferred and data will + be included in the initial response. Defaults to `true` when omitted. +- `label: String` - May be used by GraphQL clients to identify the data from + responses and associate it with the corresponding defer directive. If + provided, the GraphQL service must add it to the corresponding pending object + in the response. `label` must be unique label across all `@defer` and + `@stream` directives in a document. `label` must not be provided as a + variable. + +### @stream + +```graphql +directive @stream( + label: String + if: Boolean! = true + initialCount: Int = 0 +) on FIELD +``` + +The `@stream` directive may be provided for a field of `List` type so that the +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 precedence over `@stream`. + +```graphql example +query myQuery($shouldStream: Boolean) { + user { + friends(first: 10) { + nodes @stream(label: "friendsStream", initialCount: 5, if: $shouldStream) + } + } +} +``` + +#### @stream Arguments + +- `if: Boolean! = true` - When `true`, field _should_ be streamed (see related + note below). When `false`, the field will not be streamed and all list items + will be included in the initial response. Defaults to `true` when omitted. +- `label: String` - May be used by GraphQL clients to identify the data from + responses and associate it with the corresponding stream directive. If + provided, the GraphQL service must add it to the corresponding pending object + in the response. `label` must be unique label across all `@defer` and + `@stream` directives in a document. `label` must not be provided as a + variable. +- `initialCount: Int` - The number of list items the service should return as + part of the initial response. If omitted, defaults to `0`. A field error will + be raised if the value of this argument is less than `0`. + +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 bd9448293..1efe807ad 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -10,7 +10,12 @@ 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. +A response to a GraphQL request must be a map or a stream of incrementally +delivered results. The response will be a stream of incrementally delivered +results when the GraphQL service has deferred or streamed data as a result of +the `@defer` or `@stream` directives. When the response of the GraphQL operation +contains incrementally delivered results, the first value will be an initial +payload, followed by one or more subsequent payloads. 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 @@ -22,14 +27,31 @@ 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 response of the GraphQL operation contains incrementally delivered +results, both the initial payload and all subsequent payloads 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. This entry must not be present for GraphQL +operations that return a single response map. + +When the response of the GraphQL operation contains incrementally delivered +results, both the initial payload and any subsequent payloads 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 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. +restrictions on its contents. When the response of the GraphQL operation is a +response stream, the initial payload and any subsequent payloads may contain an +entry with the key `extensions`, also reserved for implementers to extend the +protocol however they see fit. Additionally, implementers may send subsequent +payloads containing only `hasNext` and `extensions` entries. 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. +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 @@ -48,6 +70,10 @@ present in the result. 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 contains incrementally delivered +results, `data` may only be present in the initial payload. `data` must not be +present in any subsequent payloads. + ### Errors The `errors` entry in the response is a non-empty list of errors raised during @@ -107,14 +133,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 +264,366 @@ 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 contains incrementally delivered results, +this field may appear on both the initial and subsequent payloads. 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 payload, or one of the +following payloads. + +**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 payload 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 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 +payload, or one of the Incremental Results in a prior payload. + +### Incremental + +The `incremental` entry in the response is a non-empty list of Incremental +Results. If the response of the GraphQL operation contains incrementally +delivered results, this field may appear on both the initial and subsequent +values. 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 bubbled 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 bubble 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 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 `@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 Record'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 an `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 `subPath`. + +If any field errors were raised during the execution of the results in `data` +and these errors bubbled 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 bubble 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 contains incrementally delivered +results, this field may appear on both the initial and subsequent payloads. 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 payload. + +**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. + +### Examples + +#### 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 response stream might look like: + +Payload 1, 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 payloads 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 payloads. + +```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 +} +``` + +Payload 2, 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 +} +``` + +Payload 3, contains the final stream payload. 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 +} +``` + +Payload 4, contains no incremental data. {hasNext} set to {false} indicates the +end of the response stream. This response is sent when the underlying iterator +of the `films` field closes. + +```json example +{ + "hasNext": false +} +``` + +### Examples + +#### 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 response stream might look like: + +Payload 1, 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 payload 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 +payloads. + +```json example +{ + "data": { + "person": { + "firstName": "Luke" + } + }, + "pending": [ + { "id": "0", "path": ["person"], "label": "homeWorldDefer" }, + { "id": "1", "path": ["person"], "label": "nameAndWorld" } + ], + "hasNext": true +} +``` + +Payload 2, 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 +} +``` + +Payload 3, contains the remaining data from the `NameAndHomeWorldFragment`. +`lastName` is the only remaining field that has not been delivered in a previous +payload. + +```json example +{ + "incremental": [ + { + "id": "1", + "data": { "lastName": "Skywalker" }] + } + ], + "completed": [{"id": "1"}], + "hasNext": false +} +``` + ## Serialization Format GraphQL does not require a specific serialization format. However, clients From 03106561cc33e427563e97426cfba19dd5602f5d Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Fri, 8 Nov 2024 11:18:40 -0500 Subject: [PATCH 2/8] Move examples to Appendix --- spec/Appendix C -- Examples.md | 191 ++++++++++++++++++++++++++++++++ spec/GraphQL.md | 2 + spec/Section 7 -- Response.md | 192 --------------------------------- 3 files changed, 193 insertions(+), 192 deletions(-) create mode 100644 spec/Appendix C -- Examples.md diff --git a/spec/Appendix C -- Examples.md b/spec/Appendix C -- Examples.md new file mode 100644 index 000000000..4b58fde48 --- /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 response stream might look like: + +Payload 1, 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 payloads 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 payloads. + +```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 +} +``` + +Payload 2, 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 +} +``` + +Payload 3, contains the final stream payload. 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 +} +``` + +Payload 4, contains no incremental data. {hasNext} set to {false} indicates the +end of the response 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 response stream might look like: + +Payload 1, 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 payload 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 +payloads. + +```json example +{ + "data": { + "person": { + "firstName": "Luke" + } + }, + "pending": [ + { "id": "0", "path": ["person"], "label": "homeWorldDefer" }, + { "id": "1", "path": ["person"], "label": "nameAndWorld" } + ], + "hasNext": true +} +``` + +Payload 2, 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 +} +``` + +Payload 3, contains the remaining data from the `NameAndHomeWorldFragment`. +`lastName` is the only remaining field that has not been delivered in a previous +payload. + +```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 7 -- Response.md b/spec/Section 7 -- Response.md index 1efe807ad..932139de3 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -432,198 +432,6 @@ 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. -### Examples - -#### 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 response stream might look like: - -Payload 1, 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 payloads 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 payloads. - -```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 -} -``` - -Payload 2, 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 -} -``` - -Payload 3, contains the final stream payload. 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 -} -``` - -Payload 4, contains no incremental data. {hasNext} set to {false} indicates the -end of the response stream. This response is sent when the underlying iterator -of the `films` field closes. - -```json example -{ - "hasNext": false -} -``` - -### Examples - -#### 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 response stream might look like: - -Payload 1, 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 payload 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 -payloads. - -```json example -{ - "data": { - "person": { - "firstName": "Luke" - } - }, - "pending": [ - { "id": "0", "path": ["person"], "label": "homeWorldDefer" }, - { "id": "1", "path": ["person"], "label": "nameAndWorld" } - ], - "hasNext": true -} -``` - -Payload 2, 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 -} -``` - -Payload 3, contains the remaining data from the `NameAndHomeWorldFragment`. -`lastName` is the only remaining field that has not been delivered in a previous -payload. - -```json example -{ - "incremental": [ - { - "id": "1", - "data": { "lastName": "Skywalker" }] - } - ], - "completed": [{"id": "1"}], - "hasNext": false -} -``` - ## Serialization Format GraphQL does not require a specific serialization format. However, clients From fcf898f71f28c44231746f4907095b91dc46c603 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Fri, 8 Nov 2024 11:48:03 -0500 Subject: [PATCH 3/8] PR feedback --- spec/Section 3 -- Type System.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index d394121c3..e55ca9da4 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1943,11 +1943,6 @@ by a validator, executor, or client tool such as a code generator. GraphQL implementations should provide the `@skip` and `@include` directives. -GraphQL implementations are not required to implement the `@defer` and `@stream` -directives. If either or both of these directives are implemented, they must be -implemented according to this 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 the schema. @@ -2171,6 +2166,14 @@ scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") ### @defer +GraphQL implementations are not required to implement the `@defer` and `@stream` +directives. If either or both of these directives are implemented, they must be +implemented according to this specification. GraphQL implementations that do not +support these directives must not make them available via introspection. The +[Directives Are Defined](#sec-Directives-Are-Defined) validation rule will +prevent GraphQL Operations containing the `@defer` or `@stream` directive from +being executed by a GraphQL service that does not implement these directives. + ```graphql directive @defer( label: String @@ -2182,9 +2185,8 @@ 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 is -delivered in a subsequent response. `@include` and `@skip` take precedence over -`@defer`. +deferred data is delivered in subsequent responses. `@include` and `@skip` take +precedence over `@defer`. ```graphql example query myQuery($shouldDefer: Boolean) { @@ -2226,7 +2228,10 @@ directive @stream( The `@stream` directive may be provided for a field of `List` type so that the 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 precedence over `@stream`. +responses. `@include` and `@skip` take precedence over `@stream`. The +[Stream Directives Are Used On List Fields](#sec-Stream-Directives-Are-Used-On-List-Fields) +validation rule is used to prevent the `@stream` directive from being applied to +a field that is not a `List` type. ```graphql example query myQuery($shouldStream: Boolean) { From ffb8cad0367a80155e5538ab3994445692935ca9 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 21 Nov 2024 12:37:41 -0500 Subject: [PATCH 4/8] defer/stream is optional for GraphQL services --- spec/Section 3 -- Type System.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index e55ca9da4..290172abb 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1947,6 +1947,14 @@ GraphQL implementations that support the type system definition language must provide the `@deprecated` directive if representing deprecated portions of the schema. +GraphQL services are not required to implement the `@defer` and `@stream` +directives. If either or both of these directives are implemented, they must be +implemented according to this specification. GraphQL services that do not +support these directives must not make them available via introspection. The +[Directives Are Defined](#sec-Directives-Are-Defined) validation rule will +prevent GraphQL Operations containing the `@defer` or `@stream` directive from +being executed by a GraphQL service that does not implement these directives. + GraphQL implementations that support the type system definition language should provide the `@specifiedBy` directive if representing custom scalar definitions. @@ -2166,14 +2174,6 @@ scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") ### @defer -GraphQL implementations are not required to implement the `@defer` and `@stream` -directives. If either or both of these directives are implemented, they must be -implemented according to this specification. GraphQL implementations that do not -support these directives must not make them available via introspection. The -[Directives Are Defined](#sec-Directives-Are-Defined) validation rule will -prevent GraphQL Operations containing the `@defer` or `@stream` directive from -being executed by a GraphQL service that does not implement these directives. - ```graphql directive @defer( label: String From c7c3ee169ad4c370a99b5bf7e2ebe2eea5d41e60 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Mon, 2 Dec 2024 15:51:58 -0500 Subject: [PATCH 5/8] PR feedback --- spec/Section 3 -- Type System.md | 77 +++++++++++++++++--------------- spec/Section 7 -- Response.md | 22 ++++----- 2 files changed, 53 insertions(+), 46 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 290172abb..e46b91849 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1947,13 +1947,13 @@ GraphQL implementations that support the type system definition language must provide the `@deprecated` directive if representing deprecated portions of the schema. -GraphQL services are not required to implement the `@defer` and `@stream` -directives. If either or both of these directives are implemented, they must be -implemented according to this specification. GraphQL services that do not -support these directives must not make them available via introspection. The -[Directives Are Defined](#sec-Directives-Are-Defined) validation rule will -prevent GraphQL Operations containing the `@defer` or `@stream` directive from -being executed by a GraphQL service that does not implement these directives. +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. @@ -2181,15 +2181,16 @@ directive @defer( ) 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 -deferred data is delivered in subsequent responses. `@include` and `@skip` take -precedence over `@defer`. +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 response may consist of multiple +payloads: the initial payload containing all non-deferred data, while subsequent +payloads include deferred data. + +The `@include` and `@skip` directives take precedence over `@defer`. ```graphql example -query myQuery($shouldDefer: Boolean) { +query myQuery($shouldDefer: Boolean! = true) { user { name ...someFragment @defer(label: "someLabel", if: $shouldDefer) @@ -2208,12 +2209,12 @@ fragment someFragment on User { - `if: Boolean! = true` - When `true`, fragment _should_ be deferred (see related note below). When `false`, fragment will not be deferred and data will be included in the initial response. Defaults to `true` when omitted. -- `label: String` - May be used by GraphQL clients to identify the data from - responses and associate it with the corresponding defer directive. If - provided, the GraphQL service must add it to the corresponding pending object - in the response. `label` must be unique label across all `@defer` and - `@stream` directives in a document. `label` must not be provided as a - variable. +- `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 @@ -2225,19 +2226,25 @@ directive @stream( ) on FIELD ``` -The `@stream` directive may be provided for a field of `List` type so that the -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 precedence over `@stream`. The -[Stream Directives Are Used On List Fields](#sec-Stream-Directives-Are-Used-On-List-Fields) -validation rule is used to prevent the `@stream` directive from being applied to -a field that is not a `List` type. +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 in the initial payload, +and additional list items in subsequent payloads. + +The `@include` and `@skip` directives take precedence over `@stream`. + +Note: The [Directives Are Defined](#sec-Directives-Are-Defined) validation rule +ensures that GraphQL Operations containing the `@stream` directive cannot be +executed by a GraphQL service that does not support this directive. ```graphql example -query myQuery($shouldStream: Boolean) { +query myQuery($shouldStream: Boolean! = true) { user { friends(first: 10) { - nodes @stream(label: "friendsStream", initialCount: 5, if: $shouldStream) + nodes + @stream(label: "friendsStream", initialCount: 5, if: $shouldStream) { + name + } } } } @@ -2248,12 +2255,12 @@ query myQuery($shouldStream: Boolean) { - `if: Boolean! = true` - When `true`, field _should_ be streamed (see related note below). When `false`, the field will not be streamed and all list items will be included in the initial response. Defaults to `true` when omitted. -- `label: String` - May be used by GraphQL clients to identify the data from - responses and associate it with the corresponding stream directive. If - provided, the GraphQL service must add it to the corresponding pending object - in the response. `label` must be unique label across all `@defer` and - `@stream` directives in a document. `label` must not be provided as a - variable. +- `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 response. 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 as part of the initial response. If omitted, defaults to `0`. A field error will be raised if the value of this argument is less than `0`. diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 932139de3..ed77a9ef1 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -11,10 +11,10 @@ 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 or a stream of incrementally -delivered results. The response will be a stream of incrementally delivered -results when the GraphQL service has deferred or streamed data as a result of +delivered payloads. The response will be a stream of incrementally delivered +payloads when the GraphQL service has deferred or streamed data as a result of the `@defer` or `@stream` directives. When the response of the GraphQL operation -contains incrementally delivered results, the first value will be an initial +contains incrementally delivered payloads, the first value will be an initial payload, followed by one or more subsequent payloads. If the request raised any errors, the response map must contain an entry with @@ -28,14 +28,14 @@ request 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 contains incrementally delivered -results, both the initial payload and all subsequent payloads must contain an +payloads, both the initial payload and all subsequent payloads 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. This entry must not be present for GraphQL operations that return a single response map. When the response of the GraphQL operation contains incrementally delivered -results, both the initial payload and any subsequent payloads may contain +payloads, both the initial payload and any subsequent payloads 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. @@ -71,7 +71,7 @@ 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 contains incrementally delivered -results, `data` may only be present in the initial payload. `data` must not be +payloads, `data` may only be present in the initial payload. `data` must not be present in any subsequent payloads. ### Errors @@ -185,9 +185,9 @@ The response might look like: ``` If the field which experienced an error was declared as `Non-Null`, the `null` -result will bubble up 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. +result will propagate to the next nullable parent 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 @@ -280,7 +280,7 @@ 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 contains incrementally delivered results, +the response of the GraphQL operation contains incrementally delivered payloads, this field may appear on both the initial and subsequent payloads. If present, the `pending` entry must contain at least one Pending Result. @@ -321,7 +321,7 @@ payload, or one of the Incremental Results in a prior payload. The `incremental` entry in the response is a non-empty list of Incremental Results. If the response of the GraphQL operation contains incrementally -delivered results, this field may appear on both the initial and subsequent +delivered payloads, this field may appear on both the initial and subsequent values. If present, the `incremental` entry must contain at least one Incremental Result. From 2934a5920528410f5d94b2ea30dd46d49c399104 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Mon, 2 Dec 2024 15:58:56 -0500 Subject: [PATCH 6/8] more PR feedback --- spec/Section 3 -- Type System.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index e46b91849..64d16aca9 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2207,10 +2207,10 @@ fragment someFragment on User { #### @defer Arguments - `if: Boolean! = true` - When `true`, fragment _should_ be deferred (see - related note below). When `false`, fragment will not be deferred and data will - be included in the initial response. Defaults to `true` when omitted. + related note below). When `false`, fragment will 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 + by GraphQL clients to identify data from payloads 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 @@ -2228,8 +2228,8 @@ directive @stream( 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 in the initial payload, -and additional list items in subsequent payloads. +such as asynchronous iterators to provide a partial list initially, and +additional list items in subsequent payloads. The `@include` and `@skip` directives take precedence over `@stream`. @@ -2254,16 +2254,16 @@ query myQuery($shouldStream: Boolean! = true) { - `if: Boolean! = true` - When `true`, field _should_ be streamed (see related note below). When `false`, the field will not be streamed and all list items - will be included in the initial response. Defaults to `true` when omitted. + will 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 + by GraphQL clients to identify data from payloads and associate it with the corresponding stream 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. -- `initialCount: Int` - The number of list items the service should return as - part of the initial response. If omitted, defaults to `0`. A field error will - be raised if the value of this argument is less than `0`. +- `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 From 5d724165323fdc2296d36978bd3c7c4018756699 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 3 Dec 2024 09:39:13 -0500 Subject: [PATCH 7/8] more PR feedback --- spec/Section 3 -- Type System.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 64d16aca9..0a663ddc4 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -794,9 +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 or temporarily skipped via -`@defer`. 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 From 03adfb9ef4dea9eb81d6a57882cc08a851f60836 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 3 Dec 2024 09:45:19 -0500 Subject: [PATCH 8/8] bubble -> propagate --- spec/Section 7 -- Response.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index ed77a9ef1..674f3fe26 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -185,9 +185,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 parent 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. +result will bubble up 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 @@ -360,13 +360,13 @@ 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 bubbled to a path higher than the Incremental List Result's +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 bubble to a path higher than the Incremental List +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. @@ -396,13 +396,13 @@ Incremental Object Result must point to the Pending Result that results in the shortest `subPath`. If any field errors were raised during the execution of the results in `data` -and these errors bubbled 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 +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 bubble to a path higher than the Incremental Object +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.