diff --git a/spec/Appendix B -- Grammar Summary.md b/spec/Appendix B -- Grammar Summary.md index a0ab1494f..1308c24ec 100644 --- a/spec/Appendix B -- Grammar Summary.md +++ b/spec/Appendix B -- Grammar Summary.md @@ -233,6 +233,7 @@ TypeDefinition : - ObjectTypeDefinition - InterfaceTypeDefinition - UnionTypeDefinition + - TaggedTypeDefinition - EnumTypeDefinition - InputObjectTypeDefinition @@ -241,6 +242,7 @@ TypeExtension : - ObjectTypeExtension - InterfaceTypeExtension - UnionTypeExtension + - TaggedTypeExtension - EnumTypeExtension - InputObjectTypeExtension @@ -285,6 +287,20 @@ UnionTypeExtension : - extend union Name Directives[Const]? UnionMemberTypes - extend union Name Directives[Const] +TaggedTypeDefinition : Description? tagged TaggedTypeVariant Name Directives[Const]? TaggedMemberFieldsDefinition? + +TaggedTypeVariant : + - input + - output + +TaggedMemberFieldsDefinition : { TaggedMemberFieldDefinition+ } + +TaggedMemberFieldDefinition : Description? Name : Type Directives[Const]? + +TaggedTypeExtension : + - extend tagged TaggedTypeVariant Name Directives[Const]? TaggedMemberFieldsDefinition + - extend tagged TaggedTypeVariant Name Directives[Const] + EnumTypeDefinition : Description? enum Name Directives[Const]? EnumValuesDefinition? EnumValuesDefinition : { EnumValueDefinition+ } @@ -331,6 +347,8 @@ TypeSystemDirectiveLocation : one of `ARGUMENT_DEFINITION` `INTERFACE` `UNION` + `TAGGED` + `TAGGED_MEMBER_FIELD_DEFINITION` `ENUM` `ENUM_VALUE` `INPUT_OBJECT` diff --git a/spec/Section 2 -- Language.md b/spec/Section 2 -- Language.md index a4667adca..0e82f01a6 100644 --- a/spec/Section 2 -- Language.md +++ b/spec/Section 2 -- Language.md @@ -607,7 +607,7 @@ can be used in the context of querying a `User`. Fragments cannot be specified on any input value (scalar, enumeration, or input object). -Fragments can be specified on object types, interfaces, and unions. +Fragments can be specified on object types, interfaces, unions, and tagged types. Selections within fragments only return values when the concrete type of the object it is operating on matches the type of the fragment. diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index b19d78537..65e61615a 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -240,6 +240,7 @@ TypeDefinition : - ObjectTypeDefinition - InterfaceTypeDefinition - UnionTypeDefinition + - TaggedTypeDefinition - EnumTypeDefinition - InputObjectTypeDefinition @@ -266,6 +267,11 @@ A `Union` defines a list of possible types; similar to interfaces, whenever the type system claims a union will be returned, one of the possible types will be returned. +A `Tagged` defines a set of possible member fields, where each member field has +a name and references another type in the system. Whenever the type system +references a tagged type, exactly one of these possible member fields must be +present and all others must be omitted. + Finally, oftentimes it is useful to provide complex structs as inputs to GraphQL field arguments or variables; the `Input Object` type allows the schema to define exactly what data is expected. @@ -297,7 +303,8 @@ like Scalar and Enum types, can be used as both input types and output types; other kinds of types can only be used in one or the other. Input Object types 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. +types depending on how the wrapped type may be used. There are two variants of +tagged types - one for use as an input, and one for use as an output. IsInputType(type) : * If {type} is a List type or Non-Null type: @@ -305,6 +312,8 @@ IsInputType(type) : * Return IsInputType({unwrappedType}) * If {type} is a Scalar, Enum, or Input Object type: * Return {true} + * If {type} is an input Tagged type: + * Return {true} * Return {false} IsOutputType(type) : @@ -313,6 +322,8 @@ IsOutputType(type) : * Return IsOutputType({unwrappedType}) * If {type} is a Scalar, Object, Interface, Union, or Enum type: * Return {true} + * If {type} is an output Tagged type: + * Return {true} * Return {false} @@ -323,6 +334,7 @@ TypeExtension : - ObjectTypeExtension - InterfaceTypeExtension - UnionTypeExtension + - TaggedTypeExtension - EnumTypeExtension - InputObjectTypeExtension @@ -645,9 +657,9 @@ Must only yield exactly that subset: } ``` -A field of an Object type may be a Scalar, Enum, another Object type, -an Interface, or a Union. Additionally, it may be any wrapping type whose -underlying base type is one of those five. +A field of an Object type may be a Scalar, Enum, another Object type, Tagged +type, an Interface, or a Union. Additionally, it may be any wrapping type +whose underlying base type is one of those six. For example, the `Person` type might include a `relationship`: @@ -794,8 +806,8 @@ Produces the ordered result: **Result Coercion** -Determining the result of coercing an object is the heart of the GraphQL -executor, so this is covered in that section of the spec. +Determining the result of coercing an object or tagged type is the heart of the +GraphQL executor, so this is covered in that section of the spec. **Input Coercion** @@ -914,8 +926,9 @@ May yield the result: } ``` -The type of an object field argument must be an input type (any type except an -Object, Interface, or Union type). +The type of an object field argument must be an input type (any type for which +{IsInputType(type)} returns {true}, which includes Object, Interface, Union and +input Tagged types). ### Field Deprecation @@ -988,9 +1001,10 @@ GraphQL interfaces represent a list of named fields and their arguments. GraphQL objects and interfaces can then implement these interfaces which requires that the implementing type will define all fields defined by those interfaces. -Fields on a GraphQL interface have the same rules as fields on a GraphQL object; -their type can be Scalar, Object, Enum, Interface, or Union, or any wrapping -type whose base type is one of those five. +Fields on a GraphQL interface have the same rules as fields on a GraphQL +object; their type can be any for which {IsOutputType(type)} returns {true}: +Scalar, Object, Enum, Interface, Union, output Tagged types, or any wrapping +types of those six. For example, an interface `NamedEntity` may describe a required field and types such as `Person` or `Business` may then implement this interface to guarantee @@ -1319,8 +1333,8 @@ Union types have the potential to be invalid if incorrectly defined. 1. A Union type must include one or more unique member types. 2. The member types of a Union type must all be Object base types; - Scalar, Interface and Union types must not be member types of a Union. - Similarly, wrapping types must not be member types of a Union. + Scalar, Interface, Union and Tagged types must not be member types of a + Union. Similarly, wrapping types must not be member types of a Union. ### Union Extensions @@ -1340,14 +1354,295 @@ Union type extensions have the potential to be invalid if incorrectly defined. 1. The named type must already be defined and must be a Union type. 2. The member types of a Union type extension must all be Object base types; - Scalar, Interface and Union types must not be member types of a Union. - Similarly, wrapping types must not be member types of a Union. + Scalar, Interface, Union and Tagged types must not be member types of a + Union. Similarly, wrapping types must not be member types of a Union. 3. All member types of a Union type extension must be unique. 4. All member types of a Union type extension must not already be a member of the original Union type. 5. Any non-repeatable directives provided must not already apply to the original Union type. + +## Tagged Types + +TaggedTypeDefinition : Description? tagged TaggedTypeVariant Name Directives[Const]? TaggedMemberFieldsDefinition? + +TaggedTypeVariant : + - input + - output + +TaggedTypeExtension : + - extend tagged TaggedTypeVariant Name Directives[Const]? TaggedMemberFieldsDefinition + - extend tagged TaggedTypeVariant Name Directives[Const] + +TaggedMemberFieldsDefinition : { TaggedMemberFieldDefinition+ } + +TaggedMemberFieldDefinition : Description? Name : Type Directives[Const]? + +Tagged types represent a list of possible named member fields, exactly one of +which must be present. This resulting member field must yield a value of a +specific type. Every tagged type must be suitable for input or for output, but +not both; this input/output suitability is referred to as the variant. + +Output Tagged types, like Objects, should be serialized as ordered maps: the +queried member field names (or aliases) that are present are the keys and the +result of evaluating the member field is the value, ordered by the order in +which they appear in the query. Note that the resulting map for a Tagged types +may be an object with more than one key even though they guarantee that exactly +one member field is present, this is because aliases may be used, and +`__typename` may also be queried (and will return the name of the tagged type). + +All member fields defined within a Tagged type must not have a name which +begins with {"__"} (two underscores), as this is used exclusively by GraphQL's +introspection system. + +For example, a type `PackQuantity` could be described as: + +```graphql example +tagged output PackQuantity { + volumeInMetresCubed: Float! + numberOfBoxes: Int! + description: String! +} +``` + +In this case we're representing exactly one of the following: + +- an object containing a single key `volumeInMetresCubed` with an {Float} value +- an object containing a single key `numberOfBoxes` with a {Int} value +- an object containing a single key `description` with an {String} value + +The `PackQuantity` Tagged type above was declared as an `output`, but it could +have been declared as an `instead` instead since it only references types that +are suitable for both input and output. This is not true of all tagged types. +Output tagged types may only contain member fields whose type is valid for +output. Input tagged types may only contain member fields whose type is valid +for input. + +A member field of a Tagged type may be of any valid GraphQL type, including +Scalar, Enum, Input Object type, Object type, an Interface, a Union, another +Tagged type, or any wrapping type (e.g. list, non-null, or any combination +thereof) whose underlying base type is one of those seven. + +Selecting all the member fields of our `PackQuantity` type: + +```graphql example +{ + volumeInMetresCubed + numberOfBoxes + description +} +``` + +Could yield one of the following results: + +- `{ "volumeInMetresCubed": 3.8 }` +- `{ "numberOfBoxes": 27 }` +- `{ "description": "4 large boxes, 7 medium boxes, and 16 small boxes" }` + +Valid queries must supply a nested field set for a field that returns a Tagged +type, so for this schema: + +```graphql example +type Query { + packQuantity: PackQuantity +} +``` + +This query is not valid: + +```graphql counter-example +{ + packQuantity +} +``` + +However, this query is valid: + +```graphql example +{ + packQuantity { + numberOfBoxes + } +} +``` + +And may yield one of the following results: + +- `{ "numberOfBoxes": 27 }` +- `{}` + +**Field Ordering** + +Like with Object types, when querying an output Tagged type, the resulting +mapping of fields are conceptually ordered in the same order in which they were +encountered during query execution, excluding fragments for which the type does +not apply and fields or fragments that are skipped via `@skip` or `@include` +directives, and fields which are neither the single matched member field nor +the `__typename` introspection field. This ordering is correctly produced when +using the {CollectFields()} algorithm. + +**Result Coercion** + +Determining the result of coercing an object or output tagged type is the heart +of the GraphQL executor, so this is covered in that section of the spec. + +Note: only output Tagged types may be used in output. + +**Input Coercion** + +Only input Tagged types may be used as input. + +The value for an input Tagged type should be an input object literal or an +unordered map supplied by a variable, otherwise a query error must be thrown. +In either case, the input object literal or unordered map must not contain any +entries with names not defined by a member field of this Tagged type, otherwise +an error must be thrown. The input object literal or unordered map must contain +exactly one member field, otherwise an error must be thrown. + +The result of coercion is an unordered map with an entry for exactly one member +field both defined by the tagged type and present in the input. The resulting +map is constructed with the following rules: + +* If more than one field is defined in the input, an error must be thrown. + +* For the single field defined in the input, if the value is {null} and the + field's type is a non-null type, an error must be thrown. + +* If a literal value is provided for an input object field, an entry in the + coerced unordered map is given the result of coercing that value according + to the input coercion rules for the type of the tagged member field for that + field. + +* If a variable is provided for an input object field, the runtime value of that + variable must be used. If the runtime value is {null} and the tagged member + field type is non-null, a field error must be thrown. If no runtime value is + provided, the variable definition's default value should be used. If the + variable definition does not provide a default value, the field should be + treated as if it was not specified. + +* No entry should be added to the unordered map for any other member fields + defined on the Tagged type. + +Following are examples of input coercion for an input Tagged type with a +`String` member field `a` and a required (non-null) `Int!` member field `b`: + +```graphql example +tagged input ExampleInputTagged { + a: String + b: Int! +} +``` + +Literal Value | Variables | Coerced Value +------------------------ | ----------------------- | --------------------------- +`{ a: "abc", b: 123 }` | `{}` | Error: Exactly one key must be specified +`{ a: null, b: 123 }` | `{}` | Error: Exactly one key must be specified +`{ b: 123 }` | `{}` | `{ b: 123 }` +`{ a: $var, b: 123 }` | `{ var: null }` | Error: Exactly one key must be specified +`{ a: $var, b: 123 }` | `{}` | `{ b: 123 }` +`{ b: $var }` | `{ var: 123 }` | `{ b: 123 }` +`$var` | `{ var: { b: 123 } }` | `{ b: 123 }` +`"abc123"` | `{}` | Error: Incorrect value +`$var` | `{ var: "abc123" } }` | Error: Incorrect value +`{ a: "abc", b: "123" }` | `{}` | Error: Exactly one key must be specified +`{ b: "123" }` | `{}` | Error: Incorrect value for member field {b} +`{ a: "abc" }` | `{}` | `{ a: "abc" }` +`{ b: $var }` | `{}` | Error: No keys were specified +`$var` | `{ var: { a: "abc" } }` | `{ a: "abc" }` +`{ a: "abc", b: null }` | `{}` | Error: Exactly one key must be specified +`{ b: $var }` | `{ var: null }` | Error: {b} must be non-null. +`{ b: 123, c: "xyz" }` | `{}` | Error: Exactly one key must be specified + +**Type Validation** + +Tagged types have the potential to be invalid if incorrectly defined. This set +of rules must be adhered to by every Tagged type in a GraphQL schema. + +1. A Tagged type must define one or more member fields. +2. A Tagged type must be either an input tagged type or an output tagged type + (but not both). +3. For each member field of a Tagged type: + 1. The member field must have a unique name within that Tagged type; no two + member fields may share the same name. + 2. The member field must not have a name which begins with the characters + {"__"} (two underscores). + 3. If the Tagged type is an input Tagged type, {IsInputType(memberType)} + must be {true}. + 4. If the Tagged type is an output Tagged type, {IsOutputType(memberType)} + must be {true}. + +### Member Field Deprecation + +Member fields in a Tagged type may be marked as deprecated as deemed necessary +by the application. It is still legal to query for these member fields on +outputs or supply these member fields in inputs (to ensure existing clients are +not broken by the change), but the member fields should be appropriately +treated in documentation and tooling. + +When using the type system definition language, `@deprecated` directives are +used to indicate that a field is deprecated: + +```graphql example +tagged output ExampleType { + oldField: String! @deprecated +} +``` + +Note: due to the nature of tagged types, it is valid to mark a member field of +a tagged type deprecated even when that member field is non-nullable. + +### Tagged Extensions + +TaggedTypeExtension : + - extend tagged TaggedTypeVariant Name Directives[Const]? TaggedMemberFieldsDefinition + - extend tagged TaggedTypeVariant Name Directives[Const] + +Tagged type extensions are used to represent a Tagged type which has been +extended from some original Tagged type. For example, this might be used to +represent a GraphQL service which is itself an extension of another GraphQL +service. + +In this example, an additional option `startsWith` is added to a `StringFilter` +input Tagged type: + +```graphql example +extend tagged input StringFilter { + startsWith: String! +} +``` + +Tagged type extensions may choose not to add additional member fields, instead +only adding directives. + +In this example, a directive is added to a `StringFilter` type without adding +member fields: + +```graphql example +extend tagged input StringFilter @addedDirective +``` + +**Type Validation** + +Tagged type extensions have the potential to be invalid if incorrectly defined. + +1. The named type must already be defined and must be a Tagged type. +2. The variant of the extension must match the variant of the existing Tagged + type (they must both be input, or both be output). +3. The member fields of a Tagged type extension must have unique names; no two + member fields may share the same name. +4. The member fields of a Tagged type extension must not have names beginning + with {"__"}. +5. Any member fields of a Tagged type extension must not be already defined on + the original Tagged type. +6. If the Tagged type is an input Tagged type then all member fields of a + Tagged type extension must have {IsInputType(memberType)} equal to {true}. +6. If the Tagged type is an output Tagged type then all member fields of a + Tagged type extension must have {IsOutputType(memberType)} equal to {true}. +7. Any non-repeatable directives provided must not already apply to the + original Tagged type. + + ## Enums EnumTypeDefinition : Description? enum Name Directives[Const]? EnumValuesDefinition? @@ -1428,9 +1723,10 @@ InputFieldsDefinition : { InputValueDefinition+ } Fields may accept arguments to configure their behavior. These inputs are often scalars or enums, but they sometimes need to represent more complex values. -A GraphQL Input Object defines a set of input fields; the input fields are either -scalars, enums, or other input objects. This allows arguments to accept -arbitrarily complex structs. +A GraphQL Input Object defines a set of input fields; the input fields are +either scalars, enums, other input objects, or tagged types for which +{IsInputType(type)} returns {true}. This allows arguments to accept arbitrarily +complex structs. In this example, an Input Object called `Point2D` describes `x` and `y` inputs: @@ -1743,6 +2039,8 @@ TypeSystemDirectiveLocation : one of `ARGUMENT_DEFINITION` `INTERFACE` `UNION` + `TAGGED` + `TAGGED_MEMBER_FIELD_DEFINITION` `ENUM` `ENUM_VALUE` `INPUT_OBJECT` @@ -1901,12 +2199,13 @@ must *not* be queried if either the `@skip` condition is true *or* the ```graphql directive @deprecated( reason: String = "No longer supported" -) on FIELD_DEFINITION | ENUM_VALUE +) on FIELD_DEFINITION | ENUM_VALUE | TAGGED_MEMBER_FIELD_DEFINITION ``` The `@deprecated` directive is used within the type system definition language to indicate deprecated portions of a GraphQL service's schema, such as -deprecated fields on a type or deprecated enum values. +deprecated fields on a type, deprecated enum values or deprecated tagged member +fields. Deprecations include a reason for why it is deprecated, which is formatted using Markdown syntax (as specified by [CommonMark](https://commonmark.org/)). diff --git a/spec/Section 4 -- Introspection.md b/spec/Section 4 -- Introspection.md index 5859a5a39..1514cf543 100644 --- a/spec/Section 4 -- Introspection.md +++ b/spec/Section 4 -- Introspection.md @@ -91,7 +91,8 @@ warnings. GraphQL supports type name introspection at any point within a query by the meta-field `__typename: String!` when querying against any Object, Interface, -or Union. It returns the name of the object type currently being queried. +Union or Tagged types. It returns the name of the concrete type currently being +queried, which will be an Object or output Tagged type. This is most often used when querying against Interface or Union types to identify which actual type of the possible types has been returned. @@ -139,6 +140,9 @@ type __Type { # should be non-null for INTERFACE and UNION only, always null for the others possibleTypes: [__Type!] + # should be non-null for TAGGED only, must be null for the others + memberFields(includeDeprecated: Boolean = false): [__TaggedMemberField!] + # should be non-null for ENUM only, must be null for the others enumValues(includeDeprecated: Boolean = false): [__EnumValue!] @@ -147,6 +151,12 @@ type __Type { # should be non-null for NON_NULL and LIST only, must be null for the others ofType: __Type + + # should return true for any type for which {IsInputType(type)} returns true. + isInputType: Boolean + + # should return true for any type for which {IsOutputType(type)} returns true. + isOutputType: Boolean } type __Field { @@ -165,6 +175,14 @@ type __InputValue { defaultValue: String } +type __TaggedMemberField { + name: String! + description: String + type: __Type! + isDeprecated: Boolean! + deprecationReason: String +} + type __EnumValue { name: String! description: String @@ -177,6 +195,7 @@ enum __TypeKind { OBJECT INTERFACE UNION + TAGGED ENUM INPUT_OBJECT LIST @@ -206,6 +225,8 @@ enum __DirectiveLocation { ARGUMENT_DEFINITION INTERFACE UNION + TAGGED + TAGGED_MEMBER_FIELD_DEFINITION ENUM ENUM_VALUE INPUT_OBJECT @@ -216,8 +237,8 @@ enum __DirectiveLocation { ### The __Type Type -`__Type` is at the core of the type introspection system. -It represents scalars, interfaces, object types, unions, enums, input objects types in the system. +`__Type` is at the core of the type introspection system. It represents all +named types in the system. `__Type` also represents type modifiers, which are used to modify a type that it refers to (`ofType: __Type`). This is how we represent lists, @@ -242,6 +263,8 @@ Fields * `kind` must return `__TypeKind.SCALAR`. * `name` must return a String. * `description` may return a String or {null}. +* `isInputType` should return {true}. +* `isOutputType` should return {true}. * All other fields must return {null}. @@ -259,6 +282,8 @@ Fields * Accepts the argument `includeDeprecated` which defaults to {false}. If {true}, deprecated fields are also returned. * `interfaces`: The set of interfaces that an object implements. +* `isInputType` should return {false}. +* `isOutputType` should return {true}. * All other fields must return {null}. @@ -275,6 +300,8 @@ Fields * `description` may return a String or {null}. * `possibleTypes` returns the list of types that can be represented within this union. They must be object types. +* `isInputType` should return {false}. +* `isOutputType` should return {true}. * All other fields must return {null}. @@ -296,6 +323,29 @@ Fields * `interfaces`: The set of interfaces that this interface implements. * `possibleTypes` returns the list of types that implement this interface. They must be object types. +* `isInputType` should return {false}. +* `isOutputType` should return {true}. +* All other fields must return {null}. + + +#### Tagged + +Tagged types are an type where exactly one member out of a list of potential +members must be present. The possible members of a tagged type are modeled as +fields - a name and associated type - and are explicitly listed out in +`memberFields`. No modification of a type is necessary to use it as the member +field type of a tagged type. + +Fields + +* `kind` must return `__TypeKind.TAGGED`. +* `name` must return a String. +* `description` may return a String or {null}. +* `memberFields`: The set of member fields query-able on this type. + * Accepts the argument `includeDeprecated` which defaults to {false}. If + {true}, deprecated member fields are also returned. +* `isInputType` should return {IsInputType(type)}. +* `isOutputType` should return {IsOutputType(type)}. * All other fields must return {null}. @@ -312,6 +362,8 @@ Fields must have unique names. * Accepts the argument `includeDeprecated` which defaults to {false}. If {true}, deprecated enum values are also returned. +* `isInputType` should return {true}. +* `isOutputType` should return {true}. * All other fields must return {null}. @@ -335,6 +387,8 @@ Fields * `name` must return a String. * `description` may return a String or {null}. * `inputFields`: a list of `InputValue`. +* `isInputType` should return {true}. +* `isOutputType` should return {false}. * All other fields must return {null}. @@ -348,6 +402,8 @@ Fields * `kind` must return `__TypeKind.LIST`. * `ofType`: Any type. +* `isInputType` should return {IsInputType(type)}. +* `isOutputType` should return {IsOutputType(type)}. * All other fields must return {null}. @@ -361,6 +417,8 @@ required inputs for arguments and input object fields. * `kind` must return `__TypeKind.NON_NULL`. * `ofType`: Any type except Non-null. +* `isInputType` should return {IsInputType(type)}. +* `isOutputType` should return {IsOutputType(type)}. * All other fields must return {null}. @@ -396,6 +454,20 @@ Fields default value used by this input value in the condition a value is not provided at runtime. If this input value has no default value, returns {null}. +### The __TaggedMemberField Type + +The `__TaggedMemberField` type represents `memberFields` of a tagged type. + +Fields + +* `name` must return a String +* `description` may return a String or {null} +* `type` must return a `__Type` that represents the type this member field expects. +* `isDeprecated` returns {true} if this member field should no longer be used, + otherwise {false}. +* `deprecationReason` optionally provides a reason why this member field is deprecated. + + ### The __EnumValue Type The `__EnumValue` type represents one of possible values of an enum. diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 317e88de6..56600bd76 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -81,6 +81,19 @@ type Cat implements Pet { union CatOrDog = Cat | Dog union DogOrHuman = Dog | Human union HumanOrAlien = Human | Alien + +tagged output Being { + cat: Cat + dog: Dog + human: Human + alien: Alien +} + +tagged output Earthling { + cat: Cat + dog: Dog + human: Human +} ``` @@ -324,7 +337,12 @@ must provide the operation name as described in {GetOperation()}. ## Fields -### Field Selections on Objects, Interfaces, and Unions Types +### Field Selections + +Field selections must exist on Object, Interface, Union and Tagged types. + +Note: Tagged types define member fields which are queried with field +selections. **Formal Specification** @@ -397,7 +415,6 @@ fragment directFieldSelectionOnUnion on CatOrDog { } ``` - ### Field Selection Merging **Formal Specification** @@ -568,7 +585,7 @@ fragment conflictingDifferingResponses on Pet { * Let {selectionType} be the result type of {selection} * If {selectionType} is a scalar or enum: * The subselection set of that selection must be empty -* If {selectionType} is an interface, union, or object +* If {selectionType} is an interface, union, object or output tagged type * The subselection set of that selection must NOT BE empty **Explanatory Text** @@ -594,9 +611,9 @@ fragment scalarSelectionsNotAllowedOnInt on Dog { } ``` -Conversely the leaf field selections of GraphQL queries -must be of type scalar or enum. Leaf selections on objects, interfaces, -and unions without subfields are disallowed. +Conversely the leaf field selections of GraphQL queries must be of type scalar +or enum. Leaf selections on objects, interfaces, unions and tagged types +without subfields are disallowed. Let's assume the following additions to the query root type of the schema: @@ -605,6 +622,7 @@ extend type Query { human: Human pet: Pet catOrDog: CatOrDog + being: Being } ``` @@ -622,6 +640,10 @@ query directQueryOnInterfaceWithoutSubFields { query directQueryOnUnionWithoutSubFields { catOrDog } + +query directQueryOnTaggedWithoutSubFields { + being +} ``` @@ -895,14 +917,14 @@ fragment inlineNotExistingType on Dog { **Formal Specification** * For each {fragment} defined in the document. -* The target type of fragment must have kind {UNION}, {INTERFACE}, or - {OBJECT}. +* The target type of fragment must have kind {UNION}, {INTERFACE}, + {OBJECT} or {TAGGED}. **Explanatory Text** -Fragments can only be declared on unions, interfaces, and objects. They are -invalid on scalars. They can only be applied on non-leaf fields. This rule -applies to both inline and named fragments. +Fragments can only be declared on unions, interfaces, objects and tagged types. +They are invalid on scalars. They can only be applied on non-leaf fields. This +rule applies to both inline and named fragments. The following fragment declarations are valid: @@ -920,6 +942,12 @@ fragment fragOnUnion on CatOrDog { name } } + +fragment fragOnTagged on Being { + dog { + name + } +} ``` and the following are invalid: @@ -1094,7 +1122,7 @@ fragment ownerFragment on Human { GetPossibleTypes(type): - * If {type} is an object type, return a set containing {type} + * If {type} is an object type or tagged type, return a set containing {type} * If {type} is an interface type, return the set of types implementing {type} * If {type} is a union type, return the set of possible types of {type} @@ -1134,6 +1162,43 @@ fragment catInDogFragmentInvalid on Dog { ``` +##### Tagged Spreads In Tagged Scope + +In the scope of a tagged type, the only valid tagged type +fragment spread is one that applies to the same type that +is in scope. + +For example + +```graphql example +fragment beingFragment on Being { + ... on Being { + dog { + barkVolume + } + } +} +``` + +and the following is invalid + +```graphql counter-example +fragment beingFragment on Being { + ... on Earthling { + dog { + barkVolume + } + } +} +``` + +This counter-example may be surprising since Being is covariant to Earthling +(Being contains all the member fields Earthling contains), however allowing +this spread to be valid would inhibit schema evolution - we'd have to ensure +that Being always remained covariant to Earthling, preventing us from adding +member fields to Earthling alone without adding them to Being. + + ##### Abstract Spreads in Object Scope In scope of an object type, unions or interface spreads can be used @@ -1572,7 +1637,7 @@ fragment HouseTrainedFragment on Query { **Explanatory Text** Variables can only be input types. Objects, unions, and interfaces cannot be -used as inputs. +used as inputs, neither can non-input tagged types. For these examples, consider the following typesystem additions: diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 4247e2e8e..ff76a28f3 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -320,10 +320,20 @@ 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. +GetTaggedMemberFieldName(objectType, objectValue): + + * If {objectType} is not a Tagged type, return {null}. + * Let {fields} be the fields of {objectValue} that are member names of {objectType}. + * If the length of {fields} is not 1: + * Throw a field error. + * Let {memberName} be the first entry in {fields} + * Return {memberName}. + ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): + * Let {taggedMemberFieldName} be {GetTaggedMemberFieldName}(objectType, objectValue)}. * Let {groupedFieldSet} be the result of - {CollectFields(objectType, selectionSet, variableValues)}. + {CollectFields(objectType, selectionSet, variableValues, taggedMemberFieldName)}. * Initialize {resultMap} to an empty ordered map. * For each {groupedFieldSet} as {responseKey} and {fields}: * Let {fieldName} be the name of the first entry in {fields}. @@ -477,8 +487,9 @@ 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, taggedMemberFieldName, visitedFragments): + * If {taggedMemberFieldName} is not provided, initialize it to {null}. * If {visitedFragments} is not provided, initialize it to the empty set. * Initialize {groupedFields} to an empty ordered map of lists. * For each {selection} in {selectionSet}: @@ -489,6 +500,10 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): * 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}. * If {selection} is a {Field}: + * Let {fieldName} be the field name. + * If {taggedMemberFieldName} is not null: + * If {fieldName} is not {taggedMemberFieldName} and {fieldName} is not an introspection field (beginning with the characters {"__"} (two underscores)): + * Continue with the next {selection} in {selectionSet}. * 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. @@ -507,7 +522,7 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): with the next {selection} in {selectionSet}. * Let {fragmentSelectionSet} be the top-level selection set of {fragment}. * Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, visitedFragments)}. + {CollectFields(objectType, fragmentSelectionSet, variableValues, taggedMemberFieldName, visitedFragments)}. * For each {fragmentGroup} in {fragmentGroupedFieldSet}: * Let {responseKey} be the response key shared by all fields in {fragmentGroup}. * Let {groupForResponseKey} be the list in {groupedFields} for @@ -518,7 +533,7 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): * If {fragmentType} is not {null} and {DoesFragmentTypeApply(objectType, 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 {CollectFields(objectType, fragmentSelectionSet, variableValues, visitedFragments)}. + * Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, taggedMemberFieldName, visitedFragments)}. * For each {fragmentGroup} in {fragmentGroupedFieldSet}: * Let {responseKey} be the response key shared by all fields in {fragmentGroup}. * Let {groupForResponseKey} be the list in {groupedFields} for @@ -530,6 +545,8 @@ DoesFragmentTypeApply(objectType, fragmentType): * If {fragmentType} is an Object Type: * if {objectType} and {fragmentType} are the same type, return {true}, otherwise return {false}. + * If {fragmentType} is a Tagged Type: + * 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} otherwise return {false}. * If {fragmentType} is a Union: @@ -636,8 +653,8 @@ execution flow. ### Value Completion After resolving the value for a field, it is completed by ensuring it adheres -to the expected return type. If the return type is another Object type, then -the field execution process continues recursively. +to the expected return type. If the return type is another Object or Tagged +type, then the field execution process continues recursively. CompleteValue(fieldType, fields, result, variableValues): * If the {fieldType} is a Non-Null type: @@ -657,8 +674,8 @@ CompleteValue(fieldType, fields, result, variableValues): * If {fieldType} is a Scalar or Enum type: * Return the result of "coercing" {result}, ensuring it is a legal value of {fieldType}, otherwise {null}. - * If {fieldType} is an Object, Interface, or Union type: - * If {fieldType} is an Object type. + * If {fieldType} is an Object, Interface, Union or Tagged type: + * If {fieldType} is an Object type or Tagged type. * Let {objectType} be {fieldType}. * Otherwise if {fieldType} is an Interface or Union type. * Let {objectType} be {ResolveAbstractType(fieldType, result)}.