diff --git a/.github/algorithm-format-check.mjs b/.github/algorithm-format-check.mjs new file mode 100644 index 000000000..13a31ece9 --- /dev/null +++ b/.github/algorithm-format-check.mjs @@ -0,0 +1,114 @@ +import { readFile, readdir } from "node:fs/promises"; + +const SPEC_DIR = new URL("../spec", import.meta.url).pathname; + +process.exitCode = 0; +const filenames = await readdir(SPEC_DIR); +for (const filename of filenames) { + if (!filename.endsWith(".md")) { + continue; + } + const markdown = await readFile(`${SPEC_DIR}/${filename}`, "utf8"); + + /** + * Not strictly 'lines' since we try and group indented things together as if + * they were one line. Close enough though. + */ + const lines = markdown.split(/\n(?=[\S\n]|\s*(?:-|[0-9]+\.) )/); + + for (let i = 0, l = lines.length; i < l; i++) { + const line = lines[i]; + + // Check algorithm is consistently formatted + { + // Is it an algorithm definition? + const matches = line.match(/^([a-z0-9A-Z]+)(\s*)\(([^)]*)\)(\s*):(\s*)$/); + if (matches) { + const [, algorithmName, ns1, _args, ns2, ns3] = matches; + if (ns1 || ns2 || ns3) { + console.log( + `Bad whitespace in definition of ${algorithmName} in '${filename}':` + ); + console.log(line); + console.log(); + process.exitCode = 1; + } + if (lines[i + 1] !== "") { + console.log( + `No empty space after algorithm ${algorithmName} header in '${filename}'` + ); + console.log(); + process.exitCode = 1; + } + for (let j = i + 2; j < l; j++) { + const step = lines[j]; + if (!step.match(/^\s*(-|[0-9]+\.) /)) { + if (step !== "") { + console.log( + `Bad algorithm ${algorithmName} step in '${filename}':` + ); + console.log(step); + console.log(); + process.exitCode = 1; + } + break; + } + if (!step.match(/[.:]$/)) { + console.log( + `Bad formatting for '${algorithmName}' step (does not end in '.' or ':') in '${filename}':` + ); + console.log(step); + console.log(); + process.exitCode = 1; + } + if (step.match(/^\s*(-|[0-9]\.)\s+[a-z]/)) { + console.log( + `Bad formatting of '${algorithmName}' step (should start with a capital) in '${filename}':` + ); + console.log(step); + console.log(); + process.exitCode = 1; + } + const trimmedInnerLine = step.replace(/\s+/g, " "); + if ( + trimmedInnerLine.match( + /(?:[rR]eturn|is (?:not )?)(true|false|null)\b/ + ) && + !trimmedInnerLine.match(/null or empty/) + ) { + console.log( + `Potential bad formatting of '${algorithmName}' step (true/false/null should be wrapped in curly braces, e.g. '{true}') in '${filename}':` + ); + console.log(step); + console.log(); + process.exitCode = 1; + } + } + } + } + + // Check `- ...:` step is followed by an indent + { + const matches = line.match(/^(\s*)- .*:\s*$/); + if (matches) { + const indent = matches[1]; + const nextLine = lines[i + 1]; + if (!nextLine.startsWith(`${indent} `)) { + console.log( + `Lacking indent in '${filename}' following ':' character:` + ); + console.dir(line); + console.dir(nextLine); + console.log(); + // TODO: process.exitCode = 1; + } + } + } + } +} + +if (process.exitCode === 0) { + console.log(`Everything looks okay!`); +} else { + console.log(`Please resolve the errors detailed above.`); +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be594a4bd..f56e99d2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: - uses: actions/setup-node@v3 - run: npm ci - run: npm run test:format + - run: npm run test:algorithm-format test-build: runs-on: ubuntu-latest steps: diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 8514811e0..7ee0e2915 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -55,3 +55,30 @@ hyphens) should be capitalized, with the following exceptions: All elements in hyphenated words follow the same rules, e.g. headings may contain `Non-Null`, `Context-Free`, `Built-in` (`in` is a preposition, so is not capitalized). + +## Algorithms + +A named algorithm definition starts with the name of the algorithm in +`PascalCase`, an open parenthesis, a comma-and-space separated list of +arguments, a close parenthesis and then a colon. It is followed by a blank +newline and a list of steps in the algorithm which may be numbered or bulleted. + +Each step in an algorithm should either end in a colon (`:`) with an indented +step on the next line, or a fullstop (`.`). (A step after a step ending in a +full stop may or may not be indented, use your discretion.) + +Indentation in algorithms is significant. + +Every step in an algorithm should start with a capital letter. + +``` +MyAlgorithm(argOne, argTwo): + +- Let {something} be {true}. +- For each {arg} in {argOne}: + - If {arg} is greater than {argTwo}: + - Let {something} be {false}. + - Otherwise if {arg} is less than {argTwo}: + - Let {something} be {true}. +- Return {something}. +``` diff --git a/package.json b/package.json index 7a4dd650b..0d05daa05 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test:spelling": "cspell \"spec/**/*.md\" README.md", "format": "prettier --write \"**/*.{md,yml,yaml,json}\"", "test:format": "prettier --check \"**/*.{md,yml,yaml,json}\" || npm run suggest:format", + "test:algorithm-format": "node .github/algorithm-format-check.mjs", "suggest:format": "echo \"\nTo resolve this, run: $(tput bold)npm run format$(tput sgr0)\" && exit 1", "build": "./build.sh", "test:build": "spec-md --metadata spec/metadata.json spec/GraphQL.md > /dev/null", diff --git a/spec/Appendix B -- Grammar Summary.md b/spec/Appendix B -- Grammar Summary.md index 2291ee35f..22c30704b 100644 --- a/spec/Appendix B -- Grammar Summary.md +++ b/spec/Appendix B -- Grammar Summary.md @@ -100,7 +100,7 @@ StringValue :: - `""` [lookahead != `"`] - `"` StringCharacter+ `"` -- `"""` BlockStringCharacter\* `"""` +- BlockString StringCharacter :: @@ -121,6 +121,8 @@ HexDigit :: one of EscapedCharacter :: one of `"` `\` `/` `b` `f` `n` `r` `t` +BlockString :: `"""` BlockStringCharacter\* `"""` + BlockStringCharacter :: - SourceCharacter but not `"""` or `\"""` @@ -168,12 +170,12 @@ Arguments[Const] : ( Argument[?Const]+ ) Argument[Const] : Name : Value[?Const] -FragmentSpread : ... FragmentName Directives? +FragmentSpread : ... FragmentName Arguments? Directives? InlineFragment : ... TypeCondition? Directives? SelectionSet -FragmentDefinition : fragment FragmentName TypeCondition Directives? -SelectionSet +FragmentDefinition : fragment FragmentName VariablesDefinition? TypeCondition +Directives? SelectionSet FragmentName : Name but not `on` diff --git a/spec/Section 2 -- Language.md b/spec/Section 2 -- Language.md index 1aca650a8..29bb564aa 100644 --- a/spec/Section 2 -- Language.md +++ b/spec/Section 2 -- Language.md @@ -516,10 +516,10 @@ which returns the result: ## Fragments -FragmentSpread : ... FragmentName Directives? +FragmentSpread : ... FragmentName Arguments? Directives? -FragmentDefinition : fragment FragmentName TypeCondition Directives? -SelectionSet +FragmentDefinition : fragment FragmentName VariablesDefinition? TypeCondition +Directives? SelectionSet FragmentName : Name but not `on` @@ -806,7 +806,7 @@ StringValue :: - `""` [lookahead != `"`] - `"` StringCharacter+ `"` -- `"""` BlockStringCharacter\* `"""` +- BlockString StringCharacter :: @@ -827,6 +827,8 @@ HexDigit :: one of EscapedCharacter :: one of `"` `\` `/` `b` `f` `n` `r` `t` +BlockString :: `"""` BlockStringCharacter\* `"""` + BlockStringCharacter :: - SourceCharacter but not `"""` or `\"""` @@ -1007,7 +1009,11 @@ StringCharacter :: `\` EscapedCharacter | {`r`} | U+000D | carriage return | | {`t`} | U+0009 | horizontal tab | -StringValue :: `"""` BlockStringCharacter\* `"""` +StringValue :: BlockString + +- Return the _Unicode text_ by evaluating the {BlockString}. + +BlockString :: `"""` BlockStringCharacter\* `"""` - Let {rawValue} be the _Unicode text_ by concatenating the evaluation of all {BlockStringCharacter} (which may be an empty sequence). @@ -1026,7 +1032,7 @@ BlockStringValue(rawValue): - Let {lines} be the result of splitting {rawValue} by {LineTerminator}. - Let {commonIndent} be {null}. - For each {line} in {lines}: - - If {line} is the first item in {lines}, continue to the next line. + - If {line} is the first item in {lines}, continue to the next {line}. - Let {length} be the number of characters in {line}. - Let {indent} be the number of leading consecutive {WhiteSpace} characters in {line}. @@ -1111,10 +1117,10 @@ ListValue : [ ] ListValue : [ Value+ ] - Let {inputList} be a new empty list value. -- For each {Value+} +- For each {Value+}: - Let {value} be the result of evaluating {Value}. - Append {value} to {inputList}. -- Return {inputList} +- Return {inputList}. ### Input Object Values @@ -1158,11 +1164,11 @@ ObjectValue : { } ObjectValue : { ObjectField+ } - Let {inputObject} be a new input object value with no fields. -- For each {field} in {ObjectField+} +- For each {field} in {ObjectField+}: - Let {name} be {Name} in {field}. - Let {value} be the result of evaluating {Value} in {field}. - Add a field to {inputObject} of name {name} containing value {value}. -- Return {inputObject} +- Return {inputObject}. ## Variables @@ -1209,13 +1215,72 @@ size `60`: **Variable Use Within Fragments** -Variables can be used within fragments. Variables have global scope with a given -operation, so a variable used within a fragment must be declared in any -top-level operation that transitively consumes that fragment. If a variable is -referenced in a fragment and is included by an operation that does not define -that variable, that operation is invalid (see +Variables can be used within fragments. Operation-defined variables have global +scope within a given operation. Fragment-defined variables have local scope +within the fragment definition in which they are defined. A variable used within +a fragment must either be declared in each top-level operation that transitively +consumes that fragment, or by that same fragment as a fragment variable +definition. If a variable referenced in a fragment is included by an operation +where neither the fragment nor the operation defines that variable, that +operation is invalid (see [All Variable Uses Defined](#sec-All-Variable-Uses-Defined)). +## Fragment Variable Definitions + +Fragments may define locally scoped variables. This allows fragments to be +reused while enabling the caller to specify the fragment's behavior. + +For example, the profile picture may need to be a different size depending on +the parent context: + +```graphql example +query userAndFriends { + user(id: 4) { + ...dynamicProfilePic(size: 100) + friends(first: 10) { + id + name + ...dynamicProfilePic + } + } +} + +fragment dynamicProfilePic($size: Int! = 50) on User { + profilePic(size: $size) +} +``` + +In this case the `user` will have a larger `profilePic` than those found in the +list of `friends`. + +A fragment-defined variable is scoped to the fragment that defines it. +Fragment-defined variables are allowed to shadow operation-defined variables. + +```graphql example +query withShadowedVariables($size: Int!) { + user(id: 4) { + ...variableProfilePic + } + secondUser: user(id: 5) { + ...dynamicProfilePic(size: 10) + } +} + +fragment variableProfilePic on User { + ...dynamicProfilePic(size: $size) +} + +fragment dynamicProfilePic($size: Int!) on User { + profilePic(size: $size) +} +``` + +The profilePic for `user` will be determined by the variables set by the +operation, while `secondUser` will always have a `profilePic` of size `10`. In +this case, the fragment `variableProfilePic` uses the operation-defined +variable, while `dynamicProfilePic` uses the value passed in via the fragment +spread's `size` argument. + ## Type References Type : @@ -1241,22 +1306,22 @@ input type. Type : Name -- Let {name} be the string value of {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} -- Return {type} +- {type} must not be {null}. +- Return {type}. Type : [ Type ] -- Let {itemType} be the result of evaluating {Type} +- Let {itemType} be the result of evaluating {Type}. - Let {type} be a List type where {itemType} is the contained type. -- Return {type} +- Return {type}. Type : Type ! -- Let {nullableType} be the result of evaluating {Type} +- Let {nullableType} be the result of evaluating {Type}. - Let {type} be a Non-Null type where {nullableType} is the contained type. -- Return {type} +- Return {type}. ## Directives diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 7c116bf81..497ad3ab2 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -122,7 +122,7 @@ RootOperationTypeDefinition : OperationType : NamedType A GraphQL service's collective type system capabilities are referred to as that service's "schema". A schema is defined in terms of the types and directives it -supports as well as the root operation types for each kind of operation: query, +supports as well as the _root operation type_ for each kind of operation: query, mutation, and subscription; this determines the place in the type system where those operations begin. @@ -141,24 +141,24 @@ introspection system. ### Root Operation Types -A schema defines the initial root operation type for each kind of operation it -supports: query, mutation, and subscription; this determines the place in the +:: A schema defines the initial _root operation type_ for each kind of operation +it supports: query, mutation, and subscription; this determines the place in the type system where those operations begin. -The {`query`} root operation type must be provided and must be an Object type. +The {`query`} _root operation type_ must be provided and must be an Object type. -The {`mutation`} root operation type is optional; if it is not provided, the +The {`mutation`} _root operation type_ is optional; if it is not provided, the service does not support mutations. If it is provided, it must be an Object type. -Similarly, the {`subscription`} root operation type is also optional; if it is +Similarly, the {`subscription`} _root operation type_ is also optional; if it is not provided, the service does not support subscriptions. If it is provided, it must be an Object type. The {`query`}, {`mutation`}, and {`subscription`} root types must all be different types if provided. -The fields on the {`query`} root operation type indicate what fields are +The fields on the {`query`} _root operation type_ indicate what fields are available at the top level of a GraphQL query operation. For example, this example operation: @@ -169,7 +169,8 @@ query { } ``` -is only valid when the {`query`} root operation type has a field named "myName": +is only valid when the {`query`} _root operation type_ has a field named +"myName": ```graphql example type Query { @@ -177,8 +178,8 @@ type Query { } ``` -Similarly, the following mutation is only valid if the {`mutation`} root -operation type has a field named "setName". +Similarly, the following mutation is only valid if the {`mutation`} _root +operation type_ has a field named "setName". ```graphql example mutation { @@ -191,8 +192,8 @@ mutation { When using the type system definition language, a document must include at most one {`schema`} definition. -In this example, a GraphQL schema is defined with both query and mutation root -operation types: +In this example, a GraphQL schema is defined with both a query and mutation +_root operation type_: ```graphql example schema { @@ -211,18 +212,22 @@ type MyMutationRootType { **Default Root Operation Type Names** -While any type can be the root operation type for a GraphQL operation, the type -system definition language can omit the schema definition when the {`query`}, -{`mutation`}, and {`subscription`} root types are named {"Query"}, {"Mutation"}, -and {"Subscription"} respectively. +:: The _default root type name_ for each {`query`}, {`mutation`}, and +{`subscription`} _root operation type_ are {"Query"}, {"Mutation"}, and +{"Subscription"} respectively. + +The type system definition language can omit the schema definition when each +_root operation type_ uses its respective _default root type name_ and no other +type uses any _default root type name_. Likewise, when representing a GraphQL schema using the type system definition -language, a schema definition should be omitted if it only uses the default root -operation type names. +language, a schema definition should be omitted if each _root operation type_ +uses its respective _default root type name_ and no other type uses any _default +root type name_. This example describes a valid complete GraphQL schema, despite not explicitly including a {`schema`} definition. The {"Query"} type is presumed to be the -{`query`} root operation type of the schema. +{`query`} _root operation type_ of the schema. ```graphql example type Query { @@ -230,6 +235,30 @@ type Query { } ``` +This example describes a valid GraphQL schema without a {`mutation`} _root +operation type_, even though it contains a type named {"Mutation"}. The schema +definition must be included, otherwise the {"Mutation"} type would be +incorrectly presumed to be the {`mutation`} _root operation type_ of the schema. + +```graphql example +schema { + query: Query +} + +type Query { + latestVirus: Virus +} + +type Virus { + name: String + mutations: [Mutation] +} + +type Mutation { + name: String +} +``` + ### Schema Extension SchemaExtension : @@ -318,23 +347,23 @@ can only be used as input types. Object, Interface, and Union types can only be used as output types. Lists and Non-Null types may be used as input types or output types depending on how the wrapped type may be used. -IsInputType(type) : +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} + - Return {true}. +- Return {false}. -IsOutputType(type) : +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} + - Return {true}. +- Return {false}. ### Type Extensions @@ -405,12 +434,17 @@ conform to its described rules. ```graphql example scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") scalar URL @specifiedBy(url: "https://tools.ietf.org/html/rfc3986") +scalar DateTime + @specifiedBy(url: "https://scalars.graphql.org/andimarek/date-time") ``` Custom *scalar specification URL*s should provide a single, stable format to avoid ambiguity. If the linked specification is in flux, the service should link to a fixed version rather than to a resource which might change. +Note: Some community-maintained custom scalar specifications are hosted at +[scalars.graphql.org](https://scalars.graphql.org/). + Custom *scalar specification URL*s should not be changed once defined. Doing so would likely disrupt tooling or could introduce breaking changes within the linked specification's contents. @@ -419,7 +453,9 @@ Built-in scalar types must not provide a _scalar specification URL_ as they are specified by this document. Note: Custom scalars should also summarize the specified format and provide -examples in their description. +examples in their description; see the GraphQL scalars +[implementation guide](https://scalars.graphql.org/implementation-guide) for +more guidance. **Result Coercion and Serialization** @@ -878,10 +914,12 @@ of rules must be adhered to by every Object type in a GraphQL schema. 4. For each argument of the field: 1. The argument must not have a name which begins with the characters {"\_\_"} (two underscores). - 2. The argument must accept a type where {IsInputType(argumentType)} + 2. The argument must have a unique name within that field; no two + arguments may share the same name. + 3. The argument must accept a type where {IsInputType(argumentType)} returns {true}. - 3. If argument type is Non-Null and a default value is not defined: - - The `@deprecated` directive must not be applied to this argument. + 4. If argument type is Non-Null and a default value is not defined: + 1. The `@deprecated` directive must not be applied to this argument. 3. An object type may declare that it implements one or more unique interfaces. 4. An object type must be a super-set of all interfaces it implements: 1. Let this object type be {objectType}. @@ -1228,7 +1266,9 @@ Interface types have the potential to be invalid if incorrectly defined. 4. For each argument of the field: 1. The argument must not have a name which begins with the characters {"\_\_"} (two underscores). - 2. The argument must accept a type where {IsInputType(argumentType)} + 2. The argument must have a unique name within that field; no two + arguments may share the same name. + 3. The argument must accept a type where {IsInputType(argumentType)} returns {true}. 3. An interface type may declare that it implements one or more unique interfaces, but may not implement itself. @@ -1591,7 +1631,7 @@ The value for an input object should be an input object literal or an unordered map supplied by a variable, otherwise a _request error_ must be raised. In either case, the input object literal or unordered map must not contain any entries with names not defined by a field of this input object type, otherwise a -response error must be raised. +request error must be raised. The result of coercion is an unordered map with an entry for each field both defined by the input object type and for which a value exists. The resulting map @@ -1659,7 +1699,7 @@ input ExampleInputObject { 3. The input field must accept a type where {IsInputType(inputFieldType)} returns {true}. 4. If input field type is Non-Null and a default value is not defined: - - The `@deprecated` directive must not be applied to this input field. + 1. The `@deprecated` directive must not be applied to this input field. 3. If an Input Object references itself either directly or through referenced Input Objects, at least one of the fields in the chain of references must be either a nullable or a List type. @@ -1756,8 +1796,9 @@ to denote a field that uses a Non-Null type like this: `name: String!`. **Nullable vs. Optional** Fields are _always_ optional within the context of a selection set, a field may -be omitted and the selection set is still valid. However fields that return -Non-Null types will never return the value {null} if queried. +be omitted and the selection set is still valid (so long as the selection set +does not become empty). However fields that return Non-Null types will never +return the value {null} if queried. Inputs (such as field arguments), are always optional by default. However a non-null input type is required. In addition to not accepting the value {null}, @@ -2004,7 +2045,9 @@ repeatable directives. 4. For each argument of the directive: 1. The argument must not have a name which begins with the characters {"\_\_"} (two underscores). - 2. The argument must accept a type where {IsInputType(argumentType)} returns + 2. The argument must have a unique name within that directive; no two + arguments may share the same name. + 3. The argument must accept a type where {IsInputType(argumentType)} returns {true}. ### @skip @@ -2110,6 +2153,9 @@ behavior of [custom scalar types](#sec-Scalars.Custom-Scalars). The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types. +Note: Details on implementing a GraphQL scalar specification can be found in the +[scalars.graphql.org implementation guide](https://scalars.graphql.org/implementation-guide). + In this example, a custom scalar type for `UUID` is defined with a URL pointing to the relevant IETF specification. diff --git a/spec/Section 4 -- Introspection.md b/spec/Section 4 -- Introspection.md index 3054a9f6c..6fb31b820 100644 --- a/spec/Section 4 -- Introspection.md +++ b/spec/Section 4 -- Introspection.md @@ -414,8 +414,8 @@ The `__Field` type represents each field in an Object or Interface type. Fields\: -- `name` must return a String -- `description` may return a String or {null} +- `name` must return a String. +- `description` may return a String or {null}. - `args` returns a List of `__InputValue` representing the arguments this field accepts. - Accepts the argument `includeDeprecated` which defaults to {false}. If @@ -433,8 +433,8 @@ The `__InputValue` type represents field and directive arguments as well as the Fields\: -- `name` must return a String -- `description` may return a String or {null} +- `name` must return a String. +- `description` may return a String or {null}. - `type` must return a `__Type` that represents the type this input value expects. - `defaultValue` may return a String encoding (using the GraphQL language) of @@ -451,8 +451,8 @@ The `__EnumValue` type represents one of possible values of an enum. Fields\: -- `name` must return a String -- `description` may return a String or {null} +- `name` must return a String. +- `description` may return a String or {null}. - `isDeprecated` returns {true} if this enum value should no longer be used, otherwise {false}. - `deprecationReason` optionally provides a reason why this enum value is @@ -489,8 +489,8 @@ supported. All possible locations are listed in the `__DirectiveLocation` enum: Fields\: -- `name` must return a String -- `description` may return a String or {null} +- `name` must return a String. +- `description` may return a String or {null}. - `locations` returns a List of `__DirectiveLocation` representing the valid locations this directive may be placed. - `args` returns a List of `__InputValue` representing the arguments this diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index dceec126b..9bb7f0077 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -418,8 +418,15 @@ fragment directFieldSelectionOnUnion on CatOrDog { FieldsInSetCanMerge(set): -- Let {fieldsForName} be the set of selections with a given response name in - {set} including visiting fragments and inline fragments. +- Let {visitedSelections} be the selections in {set} including visiting + fields, fragment-spreads and inline fragments. +- Let {spreadsForName} be the set of fragment spreads with a given name in + {visitedSelections}. +- For each {spreadsForName} as {name} and {spreads}: + - Each entry in {spreads} must have identical sets of arguments to each other + entry in {spreads}. +- Let {fieldsForName} be the set of field selections with a given response name + in {visitedSelections}. - Given each pair of 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 @@ -435,25 +442,25 @@ SameResponseShape(fieldA, fieldB): - Let {typeA} be the return type of {fieldA}. - Let {typeB} be the return type of {fieldB}. - If {typeA} or {typeB} is Non-Null: - - If {typeA} or {typeB} is nullable, return false. + - If {typeA} or {typeB} is nullable, return {false}. - Let {typeA} be the nullable type of {typeA}. - Let {typeB} be the nullable type of {typeB}. - If {typeA} or {typeB} is List: - - If {typeA} or {typeB} is not List, return false. + - If {typeA} or {typeB} is not List, return {false}. - Let {typeA} be the item type of {typeA}. - Let {typeB} be the item type of {typeB}. - Repeat from step 3. - If {typeA} or {typeB} is Scalar or Enum: - - If {typeA} and {typeB} are the same type return true, otherwise return - false. + - If {typeA} and {typeB} are the same type return {true}, otherwise return + {false}. - Assert: {typeA} and {typeB} are both composite types. - Let {mergedSet} be the result of adding the selection set of {fieldA} and the 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}: - - If {SameResponseShape(subfieldA, subfieldB)} is false, return false. -- Return true. + - If {SameResponseShape(subfieldA, subfieldB)} is {false}, return {false}. +- Return {true}. **Explanatory Text** @@ -570,6 +577,35 @@ fragment conflictingDifferingResponses on Pet { } ``` +Fragment spread arguments can also cause fields to fail to merge. + +```graphql example +fragment commandFragment($command: DogCommand!) on Dog { + doesKnowCommand(dogCommand: $command) +} + +fragment potentiallyConflictingArguments( + $commandOne: DogCommand! +) on Dog { + ...commandFragment(command: $commandOne) + ...commandFragment(command: $commandTwo) +} + +fragment safeFragmentArguments on Dog { + ...potentiallyConflictingArguments(commandOne: SIT, commandTwo: DOWN) +} +``` + +If two fragment spreads with the same name supply different argument values, +their fields will not be able to merge. In this case, validation fails because +the fragment spread `...commandFragment(command: SIT)` and +`...commandFragment(command: DOWN)` are part of the visited selections that will +be merged. + +If both of these spreads would have `$commandOne` or `$commandTwo` as the +argument-value, it would be allowed as we can be sure that we'd resolve +identical fields. + ### Leaf Field Selections **Formal Specification** @@ -647,8 +683,8 @@ query directQueryOnObjectWithSubFields { ## Arguments -Arguments are provided to both fields and directives. The following validation -rules apply in both cases. +Arguments are provided to fields, fragment spreads and directives. The following +validation rules apply in each case. ### Argument Names @@ -656,8 +692,10 @@ rules apply in both cases. - For each {argument} in the document: - Let {argumentName} be the Name of {argument}. - - Let {argumentDefinition} be the argument definition provided by the parent - field or definition named {argumentName}. + - If the parent is a field or directive: + - Let {argumentDefinition} be the argument or variable definition named + {argumentName} provided by the parent field definition, directive definition + or fragment definition. - {argumentDefinition} must exist. **Explanatory Text** @@ -677,7 +715,22 @@ fragment argOnOptional on Dog { } ``` -the following is invalid since `command` is not defined on `DogCommand`. +The above is also applicable to fragment-definitions and fragment-spreads, each +variable must be defined by the fragment-definition before it can be inserted as +an argument by the fragment-spread. + +```graphql example +fragment withFragmentArg($command: DogCommand) on Dog { + doesKnowCommand(dogCommand: $command) +} + +fragment usesFragmentArg on Dog { + ...withFragmentArg(command: DOWN) +} +``` + +The following is invalid since `command` is not defined on +`Dog.doesKnowCommand`. ```graphql counter-example fragment invalidArgName on Dog { @@ -685,6 +738,19 @@ fragment invalidArgName on Dog { } ``` +and this is also invalid as the argument `dogCommand` is not defined on fragment +`withFragmentArg`. + +```graphql counter-example +fragment invalidFragmentArgName on Dog { + ...withFragmentArg(dogCommand: SIT) +} + +fragment withFragmentArg($command: DogCommand) on Dog { + doesKnowCommand(dogCommand: $command) +} +``` + and this is also invalid as `unless` is not defined on `@include`. ```graphql counter-example @@ -727,9 +793,9 @@ fragment multipleArgsReverseOrder on Arguments { ### Argument Uniqueness -Fields and directives treat arguments as a mapping of argument name to value. -More than one argument with the same name in an argument set is ambiguous and -invalid. +Fields, fragment spreads and directives treat arguments as a mapping of argument +name to value. More than one argument with the same name in an argument set is +ambiguous and invalid. **Formal Specification** @@ -739,12 +805,13 @@ invalid. which contains {argument}. - {arguments} must be the set containing only {argument}. -#### Required Arguments +### Required Arguments -- For each Field or Directive in the document: - - Let {arguments} be the arguments provided by the Field or Directive. +- For each Field, Fragment Spread or Directive in the document: + - Let {arguments} be the arguments provided by the Field, Directive or + Fragment Spread. - Let {argumentDefinitions} be the set of argument definitions of that Field - or Directive. + or Directive, or the variable definitions of that Fragment. - For each {argumentDefinition} in {argumentDefinitions}: - Let {type} be the expected type of {argumentDefinition}. - Let {defaultValue} be the default value of {argumentDefinition}. @@ -1014,7 +1081,7 @@ is a validation error if the target of a spread is not defined. - For each {fragmentDefinition} in the document: - Let {visited} be the empty set. - - {DetectFragmentCycles(fragmentDefinition, visited)} + - {DetectFragmentCycles(fragmentDefinition, visited)}. DetectFragmentCycles(fragmentDefinition, visited): @@ -1023,7 +1090,7 @@ DetectFragmentCycles(fragmentDefinition, visited): - {visited} must not contain {spread}. - Let {nextVisited} be the set including {spread} and members of {visited}. - Let {nextFragmentDefinition} be the target of {spread}. - - {DetectFragmentCycles(nextFragmentDefinition, nextVisited)} + - {DetectFragmentCycles(nextFragmentDefinition, nextVisited)}. **Explanatory Text** @@ -1523,18 +1590,19 @@ query ($foo: Boolean = true, $bar: Boolean = false) { **Formal Specification** -- For every {operation} in the document: - - For every {variable} defined on {operation}: +- For every {operation} and {fragment} in the document: + - Let {operationOrFragment} be that {operation} or {fragment}. + - For every {variable} defined on {operationOrFragment}: - Let {variableName} be the name of {variable}. - Let {variables} be the set of all variables named {variableName} on - {operation}. + {operationOrFragment}. - {variables} must be a set of one. **Explanatory Text** -If any operation defines more than one variable with the same name, it is -ambiguous and invalid. It is invalid even if the type of the duplicate variable -is the same. +If any operation or fragment defines more than one variable with the same name, +it is ambiguous and invalid. It is invalid even if the type of the duplicate +variable is the same. ```graphql counter-example query houseTrainedQuery($atOtherHomes: Boolean, $atOtherHomes: Boolean) { @@ -1563,12 +1631,42 @@ fragment HouseTrainedFragment on Query { } ``` +Likewise, it is valid for a fragment to define a variable with a name that is +also defined on an operation: + +```graphql example +query C($atOtherHomes: Boolean) { + ...HouseTrainedFragment + aDog: dog { + ...HouseTrainedDog + } +} + +fragment HouseTrainedFragment on Query { + dog { + isHouseTrained(atOtherHomes: $atOtherHomes) + } +} + +fragment HouseTrainedDog($atOtherHomes: Boolean) on Dog { + isHouseTrained(atOtherHomes: $atOtherHomes) +} +``` + +Fragment-defined variables are scoped locally to the fragment that defines them, +and override any operation-defined variable values, so there is never ambiguity +about which value to use. In this case, the value of the argument `atOtherHomes` +within `HouseTrainedFragment` will be the operation-set value, and within +`HouseTrainedDog` will default to being unset (unless a default-value applies), +as the argument is not set by the fragment spread in the query `C`. + ### Variables Are Input Types **Formal Specification** -- For every {operation} in a {document}: - - For every {variable} on each {operation}: +- For every {operation} and {fragment} in a {document}: + - Let {operationOrFragment} be that {operation} or {fragment}. + - For every {variable} defined on {operationOrFragment}: - Let {variableType} be the type of {variable}. - {IsInputType(variableType)} must be {true}. @@ -1636,13 +1734,14 @@ query takesCatOrDog($catOrDog: CatOrDog) { transitively. - For each {fragment} in {fragments}: - For each {variableUsage} in scope of {fragment}, variable must be in - {operation}'s variable list. + {fragment}'s or {operation}'s variable list. **Explanatory Text** -Variables are scoped on a per-operation basis. That means that any variable used -within the context of an operation must be defined at the top level of that -operation +Operation-defined Variables are scoped on a per-operation basis, while +Fragment-defined Variables are scoped locally to the fragment. That means that +any variable used within the context of an operation must either be defined at +the top level of that operation or on the fragment that uses that variable. For example: @@ -1669,9 +1768,10 @@ query variableIsNotDefined { ${atOtherHomes} is not defined by the operation. Fragments complicate this rule. Any fragment transitively included by an -operation has access to the variables defined by that operation. Fragments can -appear within multiple operations and therefore variable usages must correspond -to variable definitions in all of those operations. +operation has access to the variables defined by that operation and defined on +the fragment. Fragments can appear within multiple operations and therefore +variable usages not defined on the fragment must correspond to variable +definitions in all of those operations. For example the following is valid: @@ -1768,7 +1868,7 @@ This is because {houseTrainedQueryTwoNotDefined} does not define a variable ${atOtherHomes} but that variable is used by {isHouseTrainedFragment} which is included in that operation. -### All Variables Used +### All Operation Variables Used **Formal Specification** @@ -1776,7 +1876,7 @@ included in that operation. - Let {variables} be the variables defined by that {operation}. - Each {variable} in {variables} must be used at least once in either the operation scope itself or any fragment transitively referenced by that - operation. + operation, excluding fragments that define the same name as an argument. **Explanatory Text** @@ -1828,6 +1928,29 @@ fragment isHouseTrainedWithoutVariableFragment on Dog { } ``` +Fragment arguments can shadow operation variables: fragments that use an +argument are not using the operation-defined variable of the same name. + +Likewise, it would be invalid if the variable was shadowed by a fragment +argument: + +```graphql counter-example +query variableNotUsedWithinFragment($atOtherHomes: Boolean) { + dog { + ...shadowedVariableFragment + } +} + +fragment shadowedVariableFragment($atOtherHomes: Boolean) on Dog { + isHouseTrained(atOtherHomes: $atOtherHomes) +} +``` + +because +{$atOtherHomes} is only referenced in a fragment that defines it as a +locally scoped argument, the operation-defined {$atOtherHomes} +variable is never used. + All operations in a document must use all of their variables. As a result, the following document does not validate. @@ -1853,6 +1976,39 @@ fragment isHouseTrainedFragment on Dog { This document is not valid because {queryWithExtraVar} defines an extraneous variable. +### All Fragment Variables Used + +**Formal Specification** + +- For every {fragment} in the document: + - Let {variables} be the variables defined by that {fragment}. + - Each {variable} in {variables} must be used at least once in the fragment's + scope. + +**Explanatory Text** + +All variables defined by a fragment must be used in that same fragment. Because +fragment-defined variables are scoped to the fragment they are defined on, if +the fragment does not use the variable, then the variable definition is +superfluous. + +For example, the following is invalid: + +```graphql counter-example +query queryWithFragmentArgUnused($atOtherHomes: Boolean) { + dog { + ...fragmentArgUnused(atOtherHomes: $atOtherHomes) + } +} + +fragment fragmentArgUnused($atOtherHomes: Boolean) on Dog { + isHouseTrained +} +``` + +This document is invalid: fragment `fragmentArgUnused` defines a fragment +variable `$atOtherHomes`, but this variable is not used within this fragment. + ### All Variable Usages Are Allowed **Formal Specification** @@ -1861,8 +2017,12 @@ variable. - Let {variableUsages} be all usages transitively included in the {operation}. - For each {variableUsage} in {variableUsages}: - Let {variableName} be the name of {variableUsage}. - - Let {variableDefinition} be the {VariableDefinition} named {variableName} - defined within {operation}. + - If the usage is within a {fragment} that defines a {variableDefinition} + for {variableName}: + - Let {variableDefinition} be the {VariableDefinition} named + {variableName} defined within {fragment}. + - Otherwise, let {variableDefinition} be the {VariableDefinition} named + {variableName} defined within {operation}. - {IsVariableUsageAllowed(variableDefinition, variableUsage)} must be {true}. diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 28862ea89..ed50317bf 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -96,7 +96,7 @@ CoerceVariableValues(schema, operation, variableValues): {defaultValue}. - Otherwise if {variableType} is a Non-Nullable type, and either {hasValue} is not {true} or {value} is {null}, raise a _request error_. - - Otherwise if {hasValue} is true: + - Otherwise if {hasValue} is {true}: - If {value} is {null}: - Add an entry to {coercedValues} named {variableName} with the value {null}. @@ -173,7 +173,8 @@ Subscribe(subscription, schema, variableValues, initialValue): - Let {sourceStream} be the result of running {CreateSourceEventStream(subscription, schema, variableValues, initialValue)}. - Let {responseStream} be the result of running - {MapSourceToResponseEvent(sourceStream, subscription, schema, variableValues)} + {MapSourceToResponseEvent(sourceStream, subscription, schema, + variableValues)}. - Return {responseStream}. Note: In a large-scale subscription system, the {Subscribe()} and @@ -224,9 +225,8 @@ must receive no more events from that event stream. **Supporting Subscriptions at Scale** -Supporting subscriptions is a significant change for any GraphQL service. Query -and mutation operations are stateless, allowing scaling via cloning of GraphQL -service instances. Subscriptions, by contrast, are stateful and require +Query and mutation operations are stateless, allowing scaling via cloning of +GraphQL service instances. Subscriptions, by contrast, are stateful and require maintaining the GraphQL document, variables, and other context over the lifetime of the subscription. @@ -264,7 +264,7 @@ CreateSourceEventStream(subscription, schema, variableValues, initialValue): is unaffected if an alias is used. - Let {field} be the first entry in {fields}. - Let {argumentValues} be the result of {CoerceArgumentValues(subscriptionType, - field, variableValues)} + field, variableValues)}. - Let {fieldStream} be the result of running {ResolveFieldEventStream(subscriptionType, initialValue, fieldName, argumentValues)}. @@ -320,7 +320,7 @@ the subscription. Unsubscribe(responseStream): -- Cancel {responseStream} +- Cancel {responseStream}. ## Executing Selection Sets @@ -340,11 +340,13 @@ ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): - For each {groupedFieldSet} as {responseKey} and {fields}: - Let {fieldName} be the name of the first entry in {fields}. Note: This value is unaffected if an alias is used. + - Let {fragmentVariableValues} be the fragment-variables value of the first + entry in {fields}. - Let {fieldType} be the return type defined for the field {fieldName} of {objectType}. - If {fieldType} is defined: - Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, - fields, variableValues)}. + fields, variableValues, fragmentVariableValues)}. - Set {responseValue} as the value for {responseKey} in {resultMap}. - Return {resultMap}. @@ -490,7 +492,8 @@ The depth-first-search order of the field groups produced by {CollectFields()} is maintained through execution, ensuring that fields appear in the executed response in a stable and predictable order. -CollectFields(objectType, selectionSet, variableValues, visitedFragments): +CollectFields(objectType, selectionSet, variableValues, visitedFragments, +localVariableValues): - If {visitedFragments} is not provided, initialize it to the empty set. - Initialize {groupedFields} to an empty ordered map of lists. @@ -498,32 +501,34 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): - If {selection} provides the directive `@skip`, let {skipDirective} be that directive. - If {skipDirective}'s {if} argument is {true} or is a variable in - {variableValues} with the value {true}, continue with the next {selection} - in {selectionSet}. + {localVariableValues} or {variableValues} with the value {true}, continue + with the next {selection} in {selectionSet}. - If {selection} provides the directive `@include`, let {includeDirective} be that directive. - If {includeDirective}'s {if} argument is not {true} and is not a variable - in {variableValues} with the value {true}, continue with the next - {selection} in {selectionSet}. + in {localVariableValues} or {variableValues} with the value {true}, + continue with the next {selection} in {selectionSet}. - If {selection} is a {Field}: - Let {responseKey} be the response key of {selection} (the alias if defined, otherwise the field name). - Let {groupForResponseKey} be the list in {groupedFields} for {responseKey}; if no such list exists, create it as an empty list. - - Append {selection} to the {groupForResponseKey}. + - Append {selection} and {localVariableValues} to the {groupForResponseKey}. - If {selection} is a {FragmentSpread}: - Let {fragmentSpreadName} be the name of {selection}. - - If {fragmentSpreadName} is in {visitedFragments}, continue with the next - {selection} in {selectionSet}. - - Add {fragmentSpreadName} to {visitedFragments}. - Let {fragment} be the Fragment in the current Document whose name is {fragmentSpreadName}. - If no such {fragment} exists, continue with the next {selection} in {selectionSet}. + - If {fragmentSpreadName} is in {visitedFragments}, continue with the next + {selection} in {selectionSet}. + - Add {fragmentSpreadName} to {visitedFragments}. - Let {fragmentType} be the type condition on {fragment}. - - If {DoesFragmentTypeApply(objectType, fragmentType)} is false, continue + - If {DoesFragmentTypeApply(objectType, fragmentType)} is {false}, continue with the next {selection} in {selectionSet}. - - Let {fragmentSelectionSet} be the top-level selection set of {fragment}. + - Let {localVariableValues} be the result of calling + {getArgumentValuesFromSpread(selection, fragmentDefinition, + variableValues, localVariableValues)}. - Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, visitedFragments)}. @@ -536,7 +541,7 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): - If {selection} is an {InlineFragment}: - Let {fragmentType} be the type condition on {selection}. - If {fragmentType} is not {null} and {DoesFragmentTypeApply(objectType, - fragmentType)} is false, continue with the next {selection} in + fragmentType)} is {false}, continue with the next {selection} in {selectionSet}. - Let {fragmentSelectionSet} be the top-level selection set of {selection}. - Let {fragmentGroupedFieldSet} be the result of calling @@ -553,15 +558,38 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): DoesFragmentTypeApply(objectType, fragmentType): - If {fragmentType} is an Object Type: - - if {objectType} and {fragmentType} are the same type, return {true}, + - If {objectType} and {fragmentType} are the same type, return {true}, otherwise return {false}. - If {fragmentType} is an Interface Type: - - if {objectType} is an implementation of {fragmentType}, return {true} + - If {objectType} is an implementation of {fragmentType}, return {true} otherwise return {false}. - If {fragmentType} is a Union: - - if {objectType} is a possible type of {fragmentType}, return {true} + - If {objectType} is a possible type of {fragmentType}, return {true} otherwise return {false}. +getArgumentValuesFromSpread(fragmentSpread, fragmentDefinition, variableValues, +fragmentArgumentValues): + +- Let {coercedValues} be an empty unordered Map. +- For each {variableDefinition} in {fragmentDefinition}: + - Let {variableName} be the name of {variableDefinition}. + - Let {variableType} be the type of {variableDefinition}. + - Let {defaultValue} be the default value for {variableDefinition}. + - Let {argumentNode} be the node provided in the fragment-spread for + {variableName} + - If {argumentNode} isn't present or is null + - If {defaultValue} exists + - Add an entry to {coercedValues} named {argumentName} with the value + {defaultValue}. + - If {variableType} is non-nullable raise a field-error + - Let {hasValue} be {true} if {fragmentArgumentValues} or {variableValues} + provides a value for the name {variableName}. + - If {variableType} is non-nullable and {hasValue} is {false} raise a + field-error + - Add an entry to {coercedValues} named {argumentName} with the value found in + {variableValues} or {fragmentArgumentValues}. +- Return {coercedValues}. + Note: The steps in {CollectFields()} evaluating the `@skip` and `@include` directives may be applied in either order since they apply commutatively. @@ -573,12 +601,13 @@ 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. -ExecuteField(objectType, objectValue, fieldType, fields, variableValues): +ExecuteField(objectType, objectValue, fieldType, fields, variableValues, +fragmentVariableValues): - Let {field} be the first entry in {fields}. - Let {fieldName} be the field name of {field}. -- Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, - variableValues)} +- Let {argumentValues} be the result of {CoerceFieldArgumentValues(objectType, + field, variableValues, fragmentVariableValues)} - Let {resolvedValue} be {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. - Return the result of {CompleteValue(fieldType, fields, resolvedValue, @@ -593,13 +622,19 @@ the type system to have a specific input type. At each argument position in an operation may be a literal {Value}, or a {Variable} to be provided at runtime. -CoerceArgumentValues(objectType, field, variableValues): +CoerceFieldArgumentValues(objectType, field, variableValues, +fragmentVariableValues): -- Let {coercedValues} be an empty unordered Map. - Let {argumentValues} be the argument values provided in {field}. - Let {fieldName} be the name of {field}. - Let {argumentDefinitions} be the arguments defined by {objectType} for the field named {fieldName}. +- Return {CoerceArgumentValues(argumentDefinitions, argumentValues, + variableValues, fragmentVariableValues)} + +CoerceArgumentValues(argumentDefinitions, argumentValues, variableValues, +fragmentVariableValues): + - For each {argumentDefinition} in {argumentDefinitions}: - Let {argumentName} be the name of {argumentDefinition}. - Let {argumentType} be the expected type of {argumentDefinition}. @@ -610,6 +645,10 @@ CoerceArgumentValues(objectType, field, variableValues): {argumentName}. - If {argumentValue} is a {Variable}: - Let {variableName} be the name of {argumentValue}. + - Let {hasValue} be {true} if {fragmentVariableValues} provides a value for + the name {variableName}. + - Let {value} be the value provided in {fragmentVariableValues} for the name + {variableName}. - Let {hasValue} be {true} if {variableValues} provides a value for the name {variableName}. - Let {value} be the value provided in {variableValues} for the name @@ -620,7 +659,7 @@ CoerceArgumentValues(objectType, field, variableValues): {defaultValue}. - Otherwise if {argumentType} is a Non-Nullable type, and either {hasValue} is not {true} or {value} is {null}, raise a _field error_. - - Otherwise if {hasValue} is true: + - Otherwise if {hasValue} is {true}: - If {value} is {null}: - Add an entry to {coercedValues} named {argumentName} with the value {null}.