diff --git a/.github/algorithm-format-check.mjs b/.github/algorithm-format-check.mjs index 592d721de..702c79411 100644 --- a/.github/algorithm-format-check.mjs +++ b/.github/algorithm-format-check.mjs @@ -2,6 +2,9 @@ import { readFile, readdir } from "node:fs/promises"; const SPEC_DIR = new URL("../spec", import.meta.url).pathname; +/** @see {@link https://spec-md.com/#sec-Value-Literals} */ +const valueLiteralsRegexp = /\{((?:[^{}]|(?:\{[^{}]*\}))+)\}/g; + process.exitCode = 0; const filenames = await readdir(SPEC_DIR); for (const filename of filenames) { @@ -72,6 +75,33 @@ for (const filename of filenames) { console.log(); process.exitCode = 1; } + + const stepWithoutValueLiterals = step.replace( + valueLiteralsRegexp, + "" + ); + if (stepWithoutValueLiterals.match(/\b[A-Z][A-Za-z0-9]+\(/)) { + console.log( + `Bad formatting of '${algorithmName}' step (algorithm call should be wrapped in braces: \`{MyAlgorithm(a, b, c)}\`) in '${filename}':` + ); + console.dir(step); + console.log(); + process.exitCode = 1; + } + + const valueLiterals = step.matchAll(valueLiteralsRegexp, ""); + for (const lit of valueLiterals) { + const inner = lit[1]; + if (inner.includes("{")) { + console.log( + `Bad formatting of '${algorithmName}' step (algorithm call should not contain braces: \`${lit}\`) in '${filename}':` + ); + console.dir(step); + console.log(); + process.exitCode = 1; + } + } + const trimmedInnerLine = step.replace(/\s+/g, " "); if ( trimmedInnerLine.match( diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12638150e..6d1b284e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,8 +6,9 @@ contributions. Contributions that do not change the interpretation of the spec but instead improve legibility, fix editorial errors, clear up ambiguity and improve -examples are encouraged and are often merged by a spec editor with little -process. +examples are encouraged. These "editorial changes" will normally be given the +["✏ Editorial" label](https://github.com/graphql/graphql-spec/issues?q=sort%3Aupdated-desc+is%3Aopen+label%3A%22%E2%9C%8F%EF%B8%8F+Editorial%22) +and are often merged by a spec editor with little process. However, contributions that _do_ meaningfully change the interpretation of the spec must follow an RFC (Request For Comments) process led by a _champion_ diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 7ee0e2915..cb4557983 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -82,3 +82,25 @@ MyAlgorithm(argOne, argTwo): - Let {something} be {true}. - Return {something}. ``` + +## Definitions + +For important terms, use +[Spec Markdown definition paragraphs](https://spec-md.com/#sec-Definition-Paragraph). + +Definition paragraphs start with `::` and add the matching italicized term to +the [specification index](https://spec.graphql.org/draft/#index), making it easy +to reference them. + +## Tone of voice + +The GraphQL specification is a reference document and should use neutral and +descriptive tone of voice. + +**Favor the present tense** + +The present tense is usually clearer and shorter: + +✅ Present: The client then sends a request to the server. + +❌ Future: The client will then send a request to the server. diff --git a/spec/Section 2 -- Language.md b/spec/Section 2 -- Language.md index 76b5fadcb..c2c601fa2 100644 --- a/spec/Section 2 -- Language.md +++ b/spec/Section 2 -- Language.md @@ -1253,7 +1253,7 @@ Type : Name - Let {name} be the string value of {Name}. - Let {type} be the type defined in the Schema named {name}. -- {type} must not be {null}. +- {type} must exist. - Return {type}. Type : [ Type ] diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 04e4fa450..8f3c5cb3f 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -351,7 +351,7 @@ IsInputType(type): - If {type} is a List type or Non-Null type: - Let {unwrappedType} be the unwrapped type of {type}. - - Return IsInputType({unwrappedType}). + - Return {IsInputType(unwrappedType)}. - If {type} is a Scalar, Enum, or Input Object type: - Return {true}. - Return {false}. @@ -360,7 +360,7 @@ IsOutputType(type): - If {type} is a List type or Non-Null type: - Let {unwrappedType} be the unwrapped type of {type}. - - Return IsOutputType({unwrappedType}). + - Return {IsOutputType(unwrappedType)}. - If {type} is a Scalar, Object, Interface, Union, or Enum type: - Return {true}. - Return {false}. @@ -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,15 @@ 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 can only include directives available on the +schema; thus operations including `@defer` or `@stream` directives can only be +executed by a GraphQL service that supports them. + GraphQL implementations that support the type system definition language should provide the `@specifiedBy` directive if representing custom scalar definitions. @@ -2164,3 +2173,117 @@ to the relevant IETF specification. ```graphql example scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") ``` + +### @defer + +```graphql +directive @defer( + if: Boolean! = true + label: String +) 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, it may return an _incremental stream_ +consisting of an _initial response payload_ containing all non-deferred data, +followed by one or more _subsequent response payload_ including the 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 in the _incremental stream_ and associate + it with the corresponding defer directive. If provided, the GraphQL service + must include this label in the corresponding pending object within the + _incremental stream_. The `label` argument must be unique across all `@defer` + and `@stream` directives in the document. + +### @stream + +```graphql +directive @stream( + initialCount: Int! = 0 + if: Boolean! = true + label: String +) on FIELD +``` + +The `@stream` directive may be provided for a field whose type incorporates a +`List` type modifier. The directive enables returning a partial list initially, +followed by additional items in subsequent payloads. If the field type +incorporates multiple `List` type modifiers, only the outermost list is +streamed. + +Note: The mechanism through which items are streamed is implementation-defined +and may use technologies such as asynchronous iterators. + +The `@include` and `@skip` directives take precedence over `@stream`. + +```graphql example +query myQuery($shouldStream: Boolean! = true) { + user { + friends(first: 10) + @stream(label: "friendsStream", initialCount: 5, if: $shouldStream) { + name + } + } +} +``` + +#### @stream Arguments + +- `initialCount: Int! = 0` - The number of list items to include initially when + completing the parent selection set. If omitted, defaults to `0`. A field + error will be raised if the value of this argument is less than `0`. When the + size of the list is greater than or equal to the value of `initialCount`, the + GraphQL service _must_ initially include at least as many list items as the + value of `initialCount` (see related note below). +- `if: Boolean! = true` - When `true`, field _should_ be streamed (see related + note below). When `false`, the field must behave as if the `@stream` directive + is not present—it must not be streamed and all of the list items must be + included. Defaults to `true` when omitted. +- `label: String` - An optional string literal (variables are disallowed) used + by GraphQL clients to identify data in the _incremental stream_ and associate + it with the corresponding stream directive. If provided, the GraphQL service + must include this label in the corresponding pending object within the + _incremental stream_. The `label` argument must be unique across all `@defer` + and `@stream` directives in the document. + +Note: The +[Defer And Stream Directive Labels Are Unique](#sec-Defer-And-Stream-Directive-Labels-Are-Unique) +validation rule ensures uniqueness of the values passed to `label` on both the +`@defer` and `@stream` directives. Variables are disallowed in the `label` +because their values may not be known during validation. + +Note: The ability to defer and/or stream data 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 individual +`@defer` and/or `@stream` directives. This also applies to the `initialCount` +argument on the `@stream` directive. Clients must be able to process a streamed +field result that contains more initial list items than what was specified in +the `initialCount` argument. diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 75af96ffd..44bc9dbba 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -420,7 +420,7 @@ FieldsInSetCanMerge(set): - Let {fieldsForName} be the set of selections with a given response name in {set} including visiting fragments and inline fragments. -- Given each pair of members {fieldA} and {fieldB} in {fieldsForName}: +- Given each pair of distinct members {fieldA} and {fieldB} in {fieldsForName}: - {SameResponseShape(fieldA, fieldB)} must be true. - If the parent types of {fieldA} and {fieldB} are equal or if either is not an Object Type: @@ -452,7 +452,8 @@ SameResponseShape(fieldA, fieldB): selection set of {fieldB}. - Let {fieldsForName} be the set of selections with a given response name in {mergedSet} including visiting fragments and inline fragments. -- Given each pair of members {subfieldA} and {subfieldB} in {fieldsForName}: +- Given each pair of distinct members {subfieldA} and {subfieldB} in + {fieldsForName}: - If {SameResponseShape(subfieldA, subfieldB)} is {false}, return {false}. - Return {true}. diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 8be4928ae..187c75a39 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -165,11 +165,11 @@ ExecuteMutation(mutation, schema, variableValues, initialValue): ### Subscription If the operation is a subscription, the result is an _event stream_ called the -"Response Stream" where each event in the event stream is the result of -executing the operation for each new event on an underlying "Source Stream". +_response stream_ where each event in the event stream is the result of +executing the operation for each new event on an underlying _source stream_. Executing a subscription operation creates a persistent function on the service -that maps an underlying Source Stream to a returned Response Stream. +that maps an underlying _source stream_ to a returned _response stream_. Subscribe(subscription, schema, variableValues, initialValue): @@ -257,9 +257,9 @@ service details should be chosen by the implementing service. #### Source Stream -A Source Stream is an _event stream_ representing a sequence of root values, -each of which will trigger a GraphQL execution. Like field value resolution, the -logic to create a Source Stream is application-specific. +:: A _source stream_ is an _event stream_ representing a sequence of root +values, each of which will trigger a GraphQL execution. Like field value +resolution, the logic to create a _source stream_ is application-specific. CreateSourceEventStream(subscription, schema, variableValues, initialValue): @@ -294,7 +294,7 @@ operation type. #### Response Stream -Each event from the underlying Source Stream triggers execution of the +Each event from the underlying _source stream_ triggers execution of the subscription _selection set_ using that event's value as the {initialValue}. MapSourceToResponseEvent(sourceStream, subscription, schema, variableValues): @@ -339,7 +339,7 @@ Note: The {ExecuteSubscriptionEvent()} algorithm is intentionally similar to #### Unsubscribe -Unsubscribe cancels the Response Stream when a client no longer wishes to +Unsubscribe cancels the _response stream_ when a client no longer wishes to receive payloads for a subscription. This in turn also cancels the Source Stream, which is a good opportunity to clean up any other resources used by the subscription. @@ -355,8 +355,7 @@ type need to be known, as well as whether it must be executed serially, or may be executed in parallel. First, the selection set is turned into a grouped field set; then, each -represented field in the grouped field set produces an entry into a response -map. +represented field in the grouped field set produces an entry into a result map. ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): @@ -596,7 +595,7 @@ directives may be applied in either order since they apply commutatively. ## Executing Fields Each field requested in the grouped field set that is defined on the selected -objectType will result in an entry in the response map. Field execution first +objectType will result in an entry in the result map. Field execution first coerces any provided argument values, then resolves a value for the field, and finally completes that value either by recursively executing another selection set or coercing a scalar value. diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 8da86c586..daca5bb1f 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 GraphQL request returns either a _response_ or a _response stream_. + +### Response + +:: A GraphQL request returns a _response_ when the GraphQL operation is a query +or mutation. A _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 @@ -35,6 +40,11 @@ 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 response during debugging. +### Response Stream + +:: A GraphQL request returns a _response stream_ when the GraphQL operation is a +subscription. A _response stream_ must be a stream of _response_. + ### Data The `data` entry in the response will be the result of the execution of the