The GraphQL Composite Schemas spec describes a collaborative approach towards build a single schema composed from multiple source schemas by specifying the algorithms to merge different GraphQL subgraph schemas into a single composite schema.
Two source schema exposing a type with the same name form a composite type in the composite schema.
# source schema 1
type SomeType {
a: String
b: String
}
# source schema 2
type SomeType {
a: String
c: String
}
# composite schema
type SomeType {
a: String
b: String
c: String
}
The GraphQL Composite Schemas spec refers to downstream GraphQL APIs that have been designed for composition as subgraphs. These subgraphs may have additional directives specified in the composition section to specify semantics of type system members to the composition.
The result of a successful composition is a single GraphQL schema that is annotated with execution directives. This schema document represents the configuration for the distributed GraphQL executor and is called composite schema.
Entities are objects with a stable identity that endure over time. They typically represent core domain objects that act as entry points to a graph. In a distributed architecture, each subgraph can contribute different fields to the same entity, and is responsible for resolving only the fields that it contributes. In such an architecture, entities effectively act as hubs that enable transparent traversal across service boundaries.
A representation of an object’s identity is known as a key. Keys consist of one
or more fields from an object, and are defined through the @key
directive on
an object or interface type.
In a distributed architecture, it is unrealistic to expect all participating systems to agree on a common way of identifying a particular type of entity. The composite schemas spec therefore allows multiple keys to be defined for each entity type, and each subgraph defines the particular keys that it is able to support.
Lookups can also be nested if they can be reached through other lookups.
type Query {
organization(id: ID!): Organization @lookup
}
type Organization {
repository(name: String!): Repository @lookup
}
type Repository @key(fields: "id organization { id }") {
name: String!
organization: Organization
}
The arguments of a lookup field must correspond to fields specified by a @key
directive annotated on the return type of the lookup field.
type Query {
node(id: ID!): Node @lookup
}
interface Node @key(fields: "id") {
id: ID!
}
The composition of subgraphs describes the process of merging multiple subgraph
schemas into a single GraphQL schema that is annotated with execution
directives. This single GraphQL schema is the output of the Schema Composition
and represents the Gateway Configuration. The schema composition process is
divided into four algorithms, Validate
, Compose
, and Finalize
that are run
in order.
A GraphQL Composite Schemas spec composition tool ensures that the provided subgraph schemas are unambiguous and mistake-free in the context of the composed supergraph. The aim here is to guarantee that a composed supergraph will have no query errors due to an inconsistent schemas.
A request against an inconsistent supergraph is still technically executable, and will always produce a stable result as defined by the algorithms in the Execution section, however that result may be ambiguous, surprising, or unexpected relative to a request containing validation errors, so a composition tool must ensure that subgraphs involved in the supergraph allow for consistent query planning during execution.
Typically validation is performed in the context of the composition.
Error Code
F0001
Formal Specification
- Let {typesByName} be the set of all types across all subgraphs involved in the schema composition by their given type name.
- Given each pair of types {typeA} and {typeB} in {typesByName}
- {typeA} and {typeB} must have the same kind
Explanatory Text
GraphQL Composite Schemas spec considers types with the same name across subgraphs as semantically equivalent and mergeable.
Types that do not have the same kind are considered not mergeable.
type User {
id: ID!
name: String!
displayName: String!
birthdate: String!
}
type User {
id: ID!
name: String!
reviews: [Review!]
}
scalar DateTime
scalar DateTime
type User {
id: ID!
name: String!
displayName: String!
birthdate: String!
}
scalar User
enum UserKind {
A
B
C
}
scalar UserKind
Error Code
F0002
Formal Specification
- Let {fieldsByName} be a map of field lists where the key is the name of a field and the value is a list of fields from mergeable types from different subgraphs with the same name.
- for each {fields} in {fieldsByName}
- {FieldsAreMergeable(fields)} must be true.
FieldsAreMergeable(fields):
- Given each pair of members {fieldA} and {fieldB} in {fields}:
- Let {typeA} be the type of {fieldA}
- Let {typeB} be the type of {fieldB}
- {SameTypeShape(typeA, typeB)} must be true.
SameTypeShape(typeA, typeB):
- If {typeA} is Non-Null:
- If {typeB} is nullable
- Let {innerType} be the inner type of {typeA}
- return SameTypeShape({innerType}, {typeB})
- If {typeB} is nullable
- If {typeB} is Non-Null:
- If {typeA} is nullable
- Let {innerType} be the inner type of {typeB}
- return {SameTypeShape(typeA, innerType)}
- If {typeA} is nullable
- If {typeA} or {typeB} is List:
- If {typeA} or {typeB} is not List, return false.
- Let {innerTypeA} be the item type of {typeA}.
- Let {innerTypeB} be the item type of {typeB}.
- return {SameTypeShape(innerTypeA, innerTypeB)}
- If {typeA} and {typeB} are not of the same kind
- return false
- If {typeA} and {typeB} do not have the same name
- return false
Explanatory Text
Fields on mergeable objects or interfaces with that have the same name are considered semantically equivalent and mergeable when they have a mergeable field type.
Fields with the same type are mergeable.
type User {
birthdate: String
}
type User {
birthdate: String
}
Fields with different nullability are mergeable, resulting in merged field with a nullable type.
type User {
birthdate: String!
}
type User {
birthdate: String
}
type User {
tags: [String!]
}
type User {
tags: [String]!
}
type User {
tags: [String]
}
Fields are not mergeable if the named types are different in kind or name.
type User {
birthdate: String!
}
type User {
birthdate: DateTime!
}
type User {
tags: [Tag]
}
type Tag {
value: String
}
type User {
tags: [Tag]
}
scalar Tag
Error Code
F0004
Formal Specification
- Let {fieldsByName} be a map of field lists where the key is the name of a field and the value is a list of fields from mergeable types from different subgraphs with the same name.
- for each {fields} in {fieldsByName}
- if {FieldsInSetCanMerge(fields)} must be true.
FieldsAreMergeable(fields):
- Given each pair of members {fieldA} and {fieldB} in {fields}:
- {ArgumentsAremergeable(fieldA, fieldB)} must be true.
ArgumentsAremergeable(fieldA, fieldB):
- Given each pair of arguments {argumentA} and {argumentB} in {fieldA} and
{fieldB}:
- Let {typeA} be the type of {argumentA}
- Let {typeB} be the type of {argumentB}
- {SameTypeShape(typeA, typeB)} must be true.
Explanatory Text
Fields on mergeable objects or interfaces with that have the same name are considered semantically equivalent and mergeable when they have a mergeable argument types.
Fields when all matching arguments have a mergeable type.
type User {
field(argument: String): String
}
type User {
field(argument: String): String
}
Arguments that differ on nullability of an argument type are mergeable.
type User {
field(argument: String!): String
}
type User {
field(argument: String): String
}
type User {
field(argument: [String!]): String
}
type User {
field(argument: [String]!): String
}
type User {
field(argument: [String]): String
}
Arguments are not mergeable if the named types are different in kind or name.
type User {
field(argument: String!): String
}
type User {
field(argument: DateTime): String
}
type User {
field(argument: [String]): String
}
type User {
field(argument: [DateTime]): String
}
Error Code
FXXXX
Formal Specification
Explanatory Text
type User {
field(a: String): String
}
type User {
field(a: String): String
}
type User {
field(a: String): String
}
type User {
field(b: String): String
}
Error Code
F0014
Formal Specification
- Let {subgraphs} be a list of all subgraphs.
- For each {subgraph} in {subgraphs}:
- Let {arguments} be the set of all arguments in {subgraph}.
- For each {argument} in {arguments}:
- If {IsExposed(argument)} is false
- Let {type} be the type of {argument}
- {type} must not be nullable
- If {IsExposed(argument)} is false
Explanatory Text
Required arguments of fields or directives (i.e., arguments that are non-nullable) must be exposed in the composed schema. Removing a required argument from the composed schema creates a contradiction where an argument is necessary for the operation in the composed schema but is not available for external use.
The following example illustrates a valid scenario where a required argument is
not marked as @internal
:
type Query {
field1(arg1: Int!): [Item]
}
type Query {
field1(arg1: Int!): [Item]
}
In this counter example, the arg1
argument is essential for field field1
in
one of the subgraph and is marked as required, but it is also marked as
@internal
, violating the rule that required arguments cannot be internal:
type Query {
field1(arg1: Int!): [Item]
}
type Query {
field1(arg1: Int! @internal): [Item]
}
In this counter example, the arg1
argument is essential for field field1
in
one of the subgraph but does not exist in the other subgraph, violating the rule
that required arguments cannot be internal:
type Query {
field1(arg1: Int!): [Item]
}
type Query {
field1: [Item]
}
The same rule applied to directives. The following counter-example illustrate scenario where a directive argument is essential for a directive in one subgraph, but does not exist in the other subgraph:
directive @directive1(arg1: Int!) on FIELD
directive @directive1 on FIELD
Error Code
F0016
Formal Specification
- Let {subgraphs} be a list of all subgraphs.
- For each {subgraph} in {subgraphs}:
- Let {fields} be the set of all fields of the composite types in {subgraph}
- For each {field} in {fields}:
- If {IsExposed(field)} is true
- Let {namedType} be the named type that {field} references
- {IsExposed(namedType)} must be true
- If {IsExposed(field)} is true
Explanatory Text
In a composed schema, a field within a composite type must only reference types that are exposed. This requirement guarantees that public types do not reference internal structures which are intended for internal use.
Here is a valid example where a public field references another public type:
type Object1 {
field1: String!
field2: Object2
}
type Object2 {
field3: String
}
type Object1 {
field1: String!
field4: Int
}
This is another valid case where the internal type is not exposed in the composed schema:
type Object1 {
field1: String!
field2: Object2 @internal
}
type Object2 @internal {
field3: String
}
type Object1 {
field1: String!
field4: Int
}
However, it becomes invalid if a public field in one subgraph references a type
that is marked as @internal
in another subgraph:
type Object1 {
field1: String!
field2: Object2
}
type Object2 {
field3: String
}
type Object1 {
field1: String!
field2: Object2 @internal
}
type Object2 @internal {
field3: String
field5: Int
}
Error Code
F0019
Formal Specification
- Let {types} be the set of all object types across all subgraphs
- For each {type} in {types}:
- {IsObjectTypeEmpty(type)} must be false.
IsObjectTypeEmpty(type):
- Let {fields} be a set of all fields of all types with coordinate and kind {type} across all subgraphs
- For each {field} in {fields}:
- If {IsExposed(field)} is true
- return false
- If {IsExposed(field)} is true
- return true
Explanatory Text
For object types defined across multiple subgraphs, the merged object type is
the superset of all fields defined in these subgraphs. However, any field marked
with @internal
in any subgraph is hidden and not included in the merged object
type. An object type with no fields, after considering @internal
markings, is
considered empty and invalid.
In the following example, the merged object type ObjectType1
is valid. It
includes all fields from both subgraphs, with field2
being hidden due to the
@internal
directive in one of the subgraphs:
type ObjectType1 {
field1: String
field2: Int @internal
}
type ObjectType1 {
field1: String
field2: Int
field3: Boolean
}
This counter-example demonstrates an invalid merged object type. In this case,
ObjectType1
is defined in two subgraphs, but all fields are marked as
@internal
in at least one of the subgraphs, resulting in an empty merged
object type:
type ObjectType1 {
field1: String @internal
field2: Boolean
}
type ObjectType1 {
field1: String
field2: Boolean @internal
}
Error Code
F0005
Formal Specification
- Let {fieldsByName} be a map of field lists where the key is the name of a field and the value is a list of fields from mergeable input types from different subgraphs with the same name.
- For each {fields} in {fieldsByName}:
- if {InputFieldsAremergeable(fields)} must be true.
InputFieldsAremergeable(fields):
- Given each pair of members {fieldA} and {fieldB} in {fields}:
- Let {typeA} be the type of {fieldA}.
- Let {typeB} be the type of {fieldB}.
- {SameTypeShape(typeA, typeB)} must be true.
Explanatory Text
The input fields of input objects with the same name must be mergeable. This rule ensures that input objects with the same name in different subgraphs have fields that can be consistently merged without conflict.
Input fields are considered mergeable when they share the same name and have compatible types. The compatibility of types is determined by their structure (lists), excluding nullability. mergeable input fields with different nullability are considered mergeable, and the resulting merged field will be the most permissive of the two.
In this example, the field field
in Input1
has compatible types across
subgraphs, making them mergeable:
input Input1 {
field: String!
}
input Input1 {
field: String
}
Here, the field tags
in Input1
is a list type with compatible inner types,
satisfying the mergeable criteria:
input Input1 {
tags: [String!]
}
input Input1 {
tags: [String]!
}
input Input1 {
tags: [String]
}
In this counter-example, the field field
in Input1
has incompatible types
(String
and DateTime
), making them not mergeable:
input Input1 {
field: String!
}
input Input1 {
field: DateTime!
}
Here, the field tags
in Input1
is a list type with incompatible inner types
(String
and DateTime
), violating the mergeable rule:
input Input1 {
tags: [String]
}
input Input1 {
tags: [DateTime]
}
Error Code
F0006
Formal Specification
- Let {inputsByName} be a map where the key is the name of an input object type, and the value is a list of all input object types from different subgraphs with that name.
- For each {listOfInputs} in {inputsByName}:
- {InputFieldsAremergeable(listOfInputs)} must be true.
InputFieldsAremergeable(inputs):
- Let {fields} be the set of all field names of the first input object in {inputs}.
- For each {input} in {inputs}:
- Let {inputFields} be the set of all field names of {input}.
- {fields} must be equal to {inputFields}.
Explanatory Text
This rule ensures that input object types with the same name across different subgraphs have identical sets of field names. Consistency in input object fields across subgraphs is required to avoid conflicts and ambiguities in the composed schema. This rule only checks that the field names are the same, not that the field types are the same. Field types are checked by the Input Field Types mergeable rule.
When an input object is defined with differing fields across subgraphs, it can lead to issues in query execution. A field expected in one subgraph might be absent in another, leading to undefined behavior. This rule prevents such inconsistencies by enforcing that all instances of the same named input object across subgraphs have a matching set of field names.
In this example, both subgraphs define Input1
with the same field field1
,
satisfying the rule:
input Input1 {
field1: String
}
input Input1 {
field1: String
}
Here, the two definitions of Input1
have different fields (field1
and
field2
), violating the rule:
input Input1 {
field1: String
}
input Input1 {
field2: String
}
Error Code
F0010
Formal Specification
- Let {inputs} be the set of all input object types across all subgraphs
- For each {input} in {inputs}:
- If {IsExposed(input)} is true
- {IsInputObjectTypeEmpty(input)} must be false
- If {IsExposed(input)} is true
IsInputObjectTypeEmpty(input):
- Let {fields} be a set of all input fields across all subraphs with coordinate {input}
- For each {field} in {fields}:
- If {IsExposed(field)} is true
- return false
- If {IsExposed(field)} is true
Explanatory Text
When an input object type is defined in multiple subgraphs, only common fields
and fields not flagged as @internal
are included in the merged input object
type. If this process results in an input object type with no fields, the input
object type is considered empty and invalid.
In the following example, the merged input object type Input1
is valid. The
type is defined in two subgraphs:
input Input1 {
field1: String @internal
field2: Int
}
input Input1 {
field1: String
field2: Int
}
In the following example, the merged input object type Input1
is valid and
contains field1
as it exists in both subgraphs. The type is defined in two
subgraphs:
input Input1 {
field1: String
}
input Input1 {
field1: String
field2: Int
}
In the following example, the merged input object type Input1
is invalid. The
type is defined in two subgraphs, but do not have any common fields.
input Input1 {
field1: String
}
input Input1 {
field2: String
}
This counter-example shows an invalid merged input object type. The Input1
type is defined in two subgraphs, but both field1
and field2
are flagged as
@internal
in one of the subgraphs:
input Input1 {
field1: String @internal
field2: Int @internal
}
input Input1 {
field1: String
field2: Int
}
In this counter-example, the Input1
type is defined in two subgraphs, but
field1
is flagged as @internal
in one subgraph and field2
is flagged as
@internal
in the other subgraph, resulting in an empty merged input object
type:
input Input1 {
field1: String @internal
field2: Int
}
input Input1 {
field1: String
field2: Int @internal
}
In the following example, the merged input object type Input1
is invalid. The
type is defined in two subgraphs, but do not have the common field field1
is
flagged as @internal
in one of the subgraphs:
input Input1 {
field1: String @internal
}
input Input1 {
field1: String
}
Error Code
F0011
Formal Specification
- Let {inputFieldsByName} be a map where the key is the name of an input field and the value is a list of input fields from different subgraphs from the same type with the same name.
- For each {inputFields} in {inputFieldsByName}:
- Let {defaultValues} be a set containing the default values of each input field in {inputFields}.
- If the size of {defaultValues} is greater than
- {InputFieldsHaveConsistentDefaults(inputFields)} must be false.
InputFieldsHaveConsistentDefaults(inputFields):
- Given each pair of input fields {inputFieldA} and {inputFieldB} in
{inputFields}:
- If the default value of {inputFieldA} is not equal to the default value of
{inputFieldB}:
- return false
- If the default value of {inputFieldA} is not equal to the default value of
{inputFieldB}:
- return true
Explanatory Text
Input fields in different subgraphs that have the same name are required to have consistent default values. This ensures that there is no ambiguity or inconsistency when merging schemas from different subgraphs.
A mismatch in default values for input fields with the same name across different subgraphs will result in a schema composition error.
In the the following example both subgraphs have an input field field1
with
the same default value. This is valid:
input Filter {
field1: Enum1 = Value1
}
enum Enum1 {
Value1
Value2
}
input Filter {
field1: Enum1 = Value1
}
enum Enum1 {
Value1
Value2
}
In the following example both subgraphs define an input field field1
with
different default values. This is invalid:
input Filter {
field1: Int = 10
}
input Filter {
field1: Int = 20
}
Error Code
F0015
Formal Specification
- Let {subgraphs} be a list of all subgraphs.
- For each {subgraph} in {subgraphs}:
- Let {fields} be the set of all fields of the input types in {subgraph}
- For each {field} in {fields}:
- If {IsExposed(field)} is true
- Let {namedType} be the named type that {field} references
- {IsExposed(namedType)} must be true
- If {IsExposed(field)} is true
Explanatory Text
In a composed schema, a field within a input type must only reference types that are exposed. This requirement guarantees that public types do not reference internal structures which are intended for internal use.
A valid case where a public input field references another public input type:
input Input1 {
field1: String!
field2: Input2
}
input Input2 {
field3: String
}
input Input1 {
field1: String!
field2: Input2
}
input Input2 {
field3: String
}
Another valid case is where the field is not exposed in the composed schema:
input Input1 {
field1: String!
field2: Input2
}
input Input2 {
field3: String
}
input Input1 {
field1: String!
field4: Int
}
An invalid case where a public input field in one subgraph references an input
type that is marked as @internal
in another subgraph:
input Input1 {
field1: String!
field2: Input2
}
input Input2 {
field3: String
}
input Input1 {
field1: String!
field4: Int
}
input Input2 @internal {
field3: String
}
Error Code
F0012
Formal Specification
- Let {subgraphs} be a list of all subgraphs.
- For each {subgraph} in {subgraphs}:
- Let {inputs} be the set of all input object types in the {subgraph}
- For each {input} in {inputs}:
- Let {fields} be a list of fields of {input}
- For each {field} in {fields}:
- If {IsExposed(field)} is false
- Let {type} be the type of {field}
- {type} must not be nullable
- If {IsExposed(field)} is false
Explanatory Text
Input object types can be defined in multiple subgraphs. Required fields in these input object types (i.e., fields that are non-nullable) must be exposed in the composed graph. A required internal field would create a contradiction where a field is both necessary for the operation in the composed schema but also not available for external use.
This example shows a valid scenario where the required field is not marked as
@internal
:
input InputType1 {
field1: String!
}
input InputType1 {
field1: String!
}
Consider the following example where an input object type InputType1
includes
a required field field1
:
input InputType1 {
field1: String!
}
input InputType1 {
field1: String! @internal
}
In this counter-example, the field field1
is only defined in one subgraph, so
will not be included in the composed schema. Therefor this field cannot be
non-nullable:
input InputType1 {
field1: String!
field2: String!
}
input InputType1 {
field2: String!
}
Error Code
F0003
Formal Specification
- Let {enumsByName} be a map where the key is the name of an enum type, and the value is a list of all enum types from different subgraphs with that name.
- For each {listOfEnum} in {enumsByName}:
- {EnumAremergeable(listOfEnum)} must be true.
EnumAremergeable(enums):
- Let {values} be the set of all values of the first enum in {enums}
- For each {enum} in {enums}
- Let {enumValues} be the set of all values of {enum}
- {values} must be equal to {enumValues}
Explanatory Text
This rule ensures that enum types with the same name across different subgraphs in a supergraph have identical sets of values. Enums, must be consistent across subgraphs to avoid conflicts and ambiguities in the composed schema.
When an enum is defined with differing values across subgraphs, it can lead to confusion and errors in query execution. For instance, a value valid in one subgraph might be passed to another where it's unrecognized, leading to unexpected behavior or failures. This rule prevents such inconsistencies by enforcing that all instances of the same named enum across subgraphs have an exact match in their values.
In this example, both subgraphs define Enum1
with the same value BAR
,
satisfying the rule:
enum Enum1 {
BAR
}
enum Enum1 {
BAR
}
Here, the two definitions of Enum1
have different values (BAR
and Baz
),
violating the rule:
enum Enum1 {
BAR
}
enum Enum1 {
Baz
}
Error Code
F0008
Formal Specification
- {ValidateArgumentDefaultValues()} must be true.
- {ValidateInputFieldDefaultValues()} must be true.
ValidateArgumentDefaultValues():
- Let {arguments} be all arguments of fields and directives across all subgraphs
- For each {argument} in {arguments}
- If {IsExposed(argument)} is true and has a default value:
- Let {defaultValue} be the default value of {argument}
- If not ValidateDefaultValue(defaultValue)
- return false
- If {IsExposed(argument)} is true and has a default value:
- return true
ValidateInputFieldDefaultValues():
- Let {inputFields} be all input fields of across all subgraphs
- For each {inputField} in {inputFields}:
- If {IsExposed(inputField)} is true and {inputField} has a default value:
- Let {defaultValue} be the default value of {inputField}
- if ValidateDefaultValue(defaultValue) is false
- return false
- If {IsExposed(inputField)} is true and {inputField} has a default value:
- return true
ValidateDefaultValue(defaultValue):
- If {defaultValue} is a ListValue:
- For each {valueNode} in {defaultValue}:
- If {ValidateDefaultValue(valueNode)} is false
- return false
- If {ValidateDefaultValue(valueNode)} is false
- For each {valueNode} in {defaultValue}:
- If {defaultValue} is an ObjectValue:
- Let {objectFields} be a list of all fields of {defaultValue}
- Let {fields} be a list of all fields {objectFields} are referring to
- For each {field} in {fields}:
- If {IsExposed(field)} is false
- return false
- If {IsExposed(field)} is false
- For each {objectField} in {objectFields}:
- Let {value} be the value of {objectField}
- return {ValidateDefaultValue(value)}
- If {defaultValue} is an EnumValue:
- If {IsExposed(defaultValue)} is false
- return false
- If {IsExposed(defaultValue)} is false
- return true
Explanatory Text
A default value for an argument in a field must only reference enum values or a input fields that are exposed in the composed schema. This rule ensures that internal members are not exposed in the composed schema through default values.
In this example the FOO
value in the Enum1
enum is not marked with
@internal, hence it doesn't violate the rule.
type Query {
field(type: Enum1 = FOO): [Baz!]!
}
enum Enum1 {
FOO
BAR
}
This example is a violation of the rule because the default value for the field
field
in type Input1
references an enum value (FOO
) that is marked as
@internal
.
type Query {
field(type: Input1): [Baz!]!
}
input Input1 {
field: Enum1 = FOO
}
enum Enum1 {
FOO @internal
BAR
}
type Query {
field(type: Input1 = { field2: "ERROR" }): [Baz!]!
}
input Input1 {
field1: String
field2: String @internal
}
This example is a violation of the rule because the default value for the type
argument in the field
field references an enum value (BAR
) that is marked as
@internal
.
type Query {
field(type: Enum1 = BAR): [Baz!]!
}
enum Enum1 {
FOO
BAR @internal
}
Error Code
F0009
Formal Specification
- Let {enumsByName} be the set of all enums across all subgraphs involved in the schema composition grouped by their name.
- For each {enum} in {enumsByName}:
- If {IsExposed(enum)} is true
- {IsEnumEmpty(enum)} must be false
- If {IsExposed(enum)} is true
IsEnumEmpty(enum):
- Let {values} be a set of enum values for {enum} across all subgraphs
- For each {value} in {values}
- If {IsExposed(value)} is true
- return false
- If {IsExposed(value)} is true
- return true
Explanatory Text
If an enum type is defined in multiple subgraphs, only values not flagged as
@internal
in all subgraphs are included in the merged enum type. If this
process results in an enum type with no values, the enum type is considered
empty and invalid.
The following example shows a valid merged enum type. The Enum1
type is
defined in two subgraphs:
enum Enum1 {
Value1 @internal
Value2
}
enum Enum1 {
Value1
Value2
}
This counter-example shows an invalid merged enum type. The Enum1
type is
defined in two subgraphs, but the Value1
and Value2
values are flagged as
@internal
in one of the subgraphs:
enum Enum1 {
Value1 @internal
Value2 @internal
}
enum Enum1 {
Value1
Value2
}
In this counter-example, the Enum1
type is defined in two subgraphs, but the
Value1
value is flagged as @internal
in one of the subgraphs and the
Value2
value is flagged as @internal
in the other subgraph which results in
an empty merged enum type:
enum Enum1 {
Value1 @internal
Value2
}
enum Enum1 {
Value1
Value2 @internal
}
Error Code
F0018
Formal Specification
- Let {interfaces} be the set of all interface types across all subgraphs.
- For each {interface} in {interfaces}:
- If {IsExposed(interface)} is true
- {IsInterfaceTypeEmpty(interface)} must be false.
- If {IsExposed(interface)} is true
IsInterfaceTypeEmpty(interface):
- Let {fields} be a set of all fields across all subgraphs with coordinate and kind of {interface}
- For each {field} in {fields}:
- If {IsExposed(field)} is true
- return false
- If {IsExposed(field)} is true
- return true
Explanatory Text
When an interface type is defined in multiple subgraphs, only common fields and
fields not flagged as @internal
are included in the merged interface type. If
this process results in an interface type with no fields, the interface type is
considered empty and invalid.
In the following example, the merged interface type Interface1
is valid. The
type is defined in two subgraphs:
interface Interface1 {
field1: String @internal
field2: Int
}
interface Interface1 {
field1: String
field2: Int
}
In the following example, the merged interface type Interface1
is valid and
contains field1
as it exists in both subgraphs. The type is defined in two
subgraphs:
interface Interface1 {
field1: String
}
interface Interface1 {
field1: String
field2: Int
}
In the following example, the merged interface type Interface1
is invalid. The
type is defined in two subgraphs, but do not have any common fields:
interface Interface1 {
field1: String
}
interface Interface1 {
field2: String
}
This counter-example shows an invalid merged interface type. The Interface1
type is defined in two subgraphs, but both field1
and field2
are flagged as
@internal
in one of the subgraphs:
interface Interface1 {
field1: String @internal
field2: Int @internal
}
interface Interface1 {
field1: String
field2: Int
}
In this counter-example, the Interface1
type is defined in two subgraphs, but
field1
is flagged as @internal
in one subgraph and field2
is flagged as
@internal
in the other subgraph, resulting in an empty merged interface type:
interface Interface1 {
field1: String @internal
field2: Int
}
interface Interface1 {
field1: String
field2: Int @internal
}
In the following example, the merged interface type Interface1
is invalid. The
type is defined in two subgraphs, but the common field field1
is flagged as
@internal
in one of the subgraphs:
interface Interface1 {
field1: String @internal
}
interface Interface1 {
field1: String
}
Error Code
F0017
Formal Specification
- Let {subgraphs} be a list of all subgraphs.
- For each {subgraph} in {subgraphs}:
- Let {unionTypes} be the set of all union types in {subgraph}.
- For each {unionType} in {unionTypes}:
- Let {members} be the set of member types of {unionType}
- For each {member} in {members}:
- Let {type} be the type of {member}
- {IsExposed(type)} must be true
Explanatory Text
A union type must not include members where the type is marked as @internal
in
any subgraph. This rule ensures that the composed schema does not expose
internal types.
Here is a valid example where all member types of a union are public:
type Object1 {
field1: String
}
type Object2 {
field2: Int
}
union Union1 = Object1 | Object2
type Object3 {
field3: Boolean
}
union Union1 = Object3
In this counter-example, the member type Object2
is marked as @internal
in
the second subgraph, so the composed schema is invalid:
union Union1 = Object1 | Object2
type Object1 {
field1: String
}
type Object2 {
field2: Int
}
union Union1 = Object1 | Object2
type Object1 {
field1: String
}
type Object2 @internal {
field2: Int
field4: Float
}
Error Code
F0007
Formal Specification
- Let {schemas} be the set of all subgraph schemas involved in the composition
- {HasQueryRootType(schemas)} must be true
HasQueryRootType(schemas):
- for each {schema} in {schemas}
- let {queryRootType} be the query root type of {schema}
- let {fields} be the fields of {queryRootType}
- if {fields} is not empty
- return true
- return false
Explanatory Text
A schema composed from multiple subgraphs must have a query root type to be valid. The query root type is essential. This rule ensures that at least one subgraph in the composition defines a query root type that defines at least one query field.
The examples provided in the test cases demonstrate how the absence or presence of a valid query root type impacts the composition result. A composition that does not meet this requirement should result in an error with the code F0007, indicating the need for at least one query field across the included subgraphs.
type Query {
field1: String
}
type Query {
field2: String @remove
}
In the example, both field1
and field2
are marked for removal, leaving no
valid query fields:
type Query {
field1: String @remove
}
type Query @remove {
field2: String
}
In this case, the presence of @internal
on field
1 removes the field from the
composed schema, leaving no accessible query fields.
type Query {
field1: String @internal
}
type Query {
field1: String
}
If types differ only in nullability they are still considered mergeable. This algorithm determines if two types are mergeable by removing non-nullability from the types when comparing them.
SameTypeShape(typeA, typeB):
- If {typeA} is Non-Null:
- If {typeB} is nullable
- Let {innerType} be the inner type of {typeA}.
- return {SameTypeShape(innerType, typeB)}.
- If {typeB} is nullable
- If {typeB} is Non-Null:
- If {typeA} is nullable
- Let {innerType} be the inner type of {typeB}.
- return {SameTypeShape(typeA, innerType)}.
- If {typeA} is nullable
- If {typeA} or {typeB} is List:
- If {typeA} or {typeB} is not List
- return false.
- Let {innerTypeA} be the item type of {typeA}.
- Let {innerTypeB} be the item type of {typeB}.
- return {SameTypeShape(innerTypeA, innerTypeB)}.
- If {typeA} or {typeB} is not List
- If {typeA} and {typeB} are not of the same kind
- return false.
- If {typeA} and {typeB} do not have the same name
- return false.
IsExposed(member):
- Let {members} be a list of all members across all subgraphs with the same coordinate and kind as {member}
- If any {members} is marked with
@internal
- return false
- If {member} is InputField
- Let {type} be any input object type that {member} is declared on
- if {IsExposed(type)} is false
- return false
- Let {types} be the list of all types across all subgraphs with the same coordinate and kind as {type}
- If {member} is in {CommonFields(types)}
- return true
- return false
- If {member} is EnumValue
- Let {enum} be any enum that {member} is declared on
- return {IsExposed(enum)}
- If {member} is ObjectField
- Let {type} be the any type that {member} is declared on
- return {IsExposed(type)}
- If {member} is InterfaceField
- Let {type} be the any type that {member} is declared on
- If {IsExposed(type)} is false
- return false
- Let {types} be the list of all types across all subgraphs with the same coordinate and kind as {type}
- If {member} is in {CommonFields(types)}
- return true
- return false
- If {member} is Argument
- If {member} is declared on a field:
- Let {declaringField} be any field that {member} is declared on
- If {IsExposed(declaringField)} is false
- return false
- Let {declaringFields} be the list of all fields across all subgraphs with the same coordinate and kind as {declaringField}
- If {member} is in {CommonArguments(declaringFields)}
- return true
- return false
- If {member} is declared on a directive
- Let {declaringDirective} be any directive that {member} is declared on
- If {IsExposed(declaringDirective)} is false
- return false
- Let {declaringDirectives} be the list of any directives across all subgraphs with the same coordinate and kind as {declaringDirective}
- If {member} is in {CommonArguments(declaringDirectives)}
- return true
- return false
- If {member} is declared on a field:
- return true
CommonArguments(members):
- Let {commonArguments} be the set of all arguments of all {members} across all subgraphs
- For each {member} in {members}
- Let {arguments} be the set of all arguments of {member}
- Let {commonArguments} be the intersection of {commonArguments} and {arguments}
- return {commonArguments}
CommonFields(members):
- Let {commonFields} be the set of all fields of all {members} across all subgraphs
- For each {member} in {members}
- Let {fields} be the set of all fields of {member}
- Let {commonFields} be the intersection of {commonFields} and {fields}
- return {commonFields}
To compose a gateway configuration the composition tooling must have loaded at least one subgraph configuration.
ComposeConfiguration(subgraphConfigurations):
- For each {subgraphConfiguration} in {subgraphConfigurations}:
- Let {subgraphSchema} be the result of {BuildSubgraphSchema(subgraphConfiguration)}.
BuildSubgraphSchema(subgraphConfiguration):
- Todo
MergeType(typesOrTypeExtensions):
- Todo
MergeSchema(schemaA, schemaB):
- Let {mergedSchema} be an empty SchemaDefinition
- Let {queryTypeA} be the query type of {schemaA}
- Set {mergedSchema} to have query type {queryTypeA}
- Let {mutationTypeA} be the mutation type of {schemaA} or null
- Let {mutationTypeB} be the mutation type of {schemaB} or null
- If {mutationTypeA} is not null
- Set {mergedSchema} to have mutation type {mutationTypeA}
- If {mutationTypeA} is null and {mutationTypeB} is not null
- Set {mergedSchema} to have mutation type {mutationTypeB}
- Let {subscriptionTypeA} be the subscription type of {schemaA} or null
- Let {subscriptionTypeB} be the subscription type of {schemaB} or null
- If {subscriptionTypeA} is not null
- Set {mergedSchema} to have subscription type {subscriptionTypeA}
- If {subscriptionTypeA} is null and {subscriptionTypeB} is not null
- Set {mergedSchema} to have subscription type {subscriptionTypeB}
- return {mergedSchema}
This algorithm combines two object types from different subgraphs into a single
object type. It does this only if neither type is marked as @internal
,
ensuring internal types remain private.
This algorithm creates a merged output type containing all the fields from both
types, unless they are marked as @internal
. When the same field exists in both
types, it merges these fields into one.
The fields are merged by name, and the merged field is the result of calling {MergeOutputField(fieldA, fieldB)}.
MergeOutputType(typeA, typeB):
- If {typeA} or {typeB} are
@internal
- return null
- Let {outputType} be an empty OutputObjectTypeDefinition
- Let {fields} be a set of field names that are defined on either typeA or typeB
- For each {fieldName} in {fields}:
- Let {fieldA} be the field with the same name on typeA
- Let {fieldB} be the field with the same name on typeB
- If {fieldA} or {fieldB} are
@internal
- continue
- If {fieldB} is null
- Append {fieldA} to {outputType}
- continue
- If {fieldA} is null
- Append {fieldB} to {outputType}
- continue
- Let {mergedField} be the result of calling {MergeOutputField(fieldA, fieldB)}
- Append {mergedField} to {outputType}
- Set {outputType} to have the same name as {typeA}
- Set {outputType} to have {MergeInterfaceImplementation(typeA, typeB)} as its interfaces
- return {outputType}
Examples of Merging Output Object Types:
# Subgraph A
type OutputTypeA {
commonField: String
uniqueFieldA: Int
}
# Subgraph B
type OutputTypeA {
commonField: String
uniqueFieldB: Boolean
}
# Result
type OutputTypeA {
commonField: String
uniqueFieldA: Int
uniqueFieldB: Boolean
}
Merging Output Object Types with @internal
Fields:
# Subgraph A
type OutputTypeA {
commonField: String
internalField: Int @internal
}
# Subgraph B
type OutputTypeA {
commonField: String
uniqueFieldB: Boolean
}
# Result
type OutputTypeA {
commonField: String
uniqueFieldB: Boolean
}
This algorithm merges two input object types from different subgraphs. It
excludes any type marked with @internal
, ensuring internal types are not
exposed in the merged schema.
The fields of the merged input object type are the intersection of the fields of the input object types from the subgraphs. This approach ensures that the merged input object type is compatible with both subgraphs.
The fields are merged by name, and the merged field is the result of calling {MergeInputField(fieldA, fieldB)}.
MergeInputType(typeA, typeB):
- If {typeA} or {typeB} are
@internal
- return null
- Let {inputType} be an empty InputObjectTypeDefinition
- Let {commonFields} be the set of fields names that are defined on both {typeA} and {typeB}
- For each {field} in {commonFields}:
- Let {fieldA} be the field with the same name on {typeA}
- Let {fieldB} be the field with the same name on {typeB}
- If {fieldA} or {fieldB} are
@internal
- continue
- Let {mergedField} be the result of calling {MergeInputField(fieldA, fieldB)}
- Append {mergedField} to {inputType}
- Set {inputType} to have the same name as {typeA}
- return {inputType}
Merging Common Input Object Fields:
# Subgraph A
input InputTypeA {
commonField: String
uniqueFieldA: Int
}
# Subgraph B
input InputTypeA {
commonField: String
uniqueFieldB: Boolean
}
# Merged Input Object
input InputTypeA {
commonField: String
}
Merging Common Input Object Fields with @internal
Fields:
# Subgraph A
input InputTypeA {
commonField: String
internalField: Int @internal
}
# Subgraph B
input InputTypeA {
commonField: String
internalField: Int
}
# Merged Input Object
input InputTypeA {
commonField: String
}
This algorithm merges two interface types from different subgraphs, given that
neither type is marked as @internal
.
The resulting interface type is the intersection of the fields of the interface types from the subgraphs. This approach ensures that the merged interface type is compatible with object types from both subgraphs.
The fields are merged by name, and the merged field is the result of calling {MergeOutputField(fieldA, fieldB)}.
MergeInterfaceType(typeA, typeB):
- If {typeA} or {typeB} are
@internal
- return null
- Let {interfaceType} be an empty InterfaceTypeDefinition
- Let {fields} be a set of common field names that are defined in {typeA} and {typeB}
- For each {fieldName} in {fields}:
- Let {fieldA} be the field with the same name on {typeA}
- Let {fieldB} be the field with the same name on {typeB}
- If {fieldA} or {fieldB} are
@internal
- continue
- Let {mergedField} be the result of calling {MergeOutputField(fieldA, fieldB)}
- Append {mergedField} to {interfaceType}
- Set {interfaceType} to have the same name as {typeA}
- Set {interfaceType} to have {MergeInterfaceImplementation(typeA, typeB)} as its interfaces
- return {interfaceType}
Merging Common Fields:
# Subgraph A
interface InterfaceA {
commonField: String
}
# Subgraph B
interface InterfaceA {
commonField: String
uniqueField: Boolean
}
# Merged Interface
interface InterfaceA {
commonField: String
}
Merging Common Fields with @internal
Fields:
# Subgraph A
interface InterfaceA {
commonField: String
internalField: Int @internal
}
# Subgraph B
interface InterfaceA {
commonField: String
internalField: Int
}
# Merged Interface
interface InterfaceA {
commonField: String
}
This counter example shows why it is important that the gateway uses the intersection rather than the union of the fields of the interface types from the subgraphs:
# Subgraph A
interface InterfaceA {
commonField: String
uniqueFieldA: Int
}
type ObjectA implements InterfaceA {
commonField: String
uniqueFieldA: Int
}
# Subgraph B
interface InterfaceA {
commonField: String
uniqueFieldB: Boolean
}
# Merged Interface
interface InterfaceA {
commonField: String
uniqueFieldA: Int
uniqueFieldB: Boolean
}
# This object type is no longer compatible with the merged interface types as it is
# missing the `uniqueFieldB` field.
type ObjectA implements InterfaceA {
commonField: String
uniqueFieldA: Int
}
This algorithm is used to merge two union types from separate subgraphs. It
combines the members from both union types into a new union type by name, given
neither original type is marked as @internal
.
This algorithm has to combine the members of both types, because otherwise there is the possibility of invalid states during the execution. If members were removed from the union type, the subgraph could still return it as a result of a query, which would be invalid in the composed schema.
MergeUnionType(typeA, typeB):
- If {typeA} or {typeB} are
@internal
- return null
- Let {unionType} be an empty UnionTypeDefinition
- Let {types} be a set of type names that are defined on either {typeA} or {typeB}
- Set {unionType} to have the same name as {typeA}
- Set {types} to be the types of {unionType}
- return {unionType}
Merging Union Types:
# Subgraph A
union UnionTypeA = Type1 | Type2
# Subgraph B
union UnionTypeA = Type2 | Type3
# Result
union UnionTypeA = Type1 | Type2 | Type3
Merging Union Types with @internal
:
# Subgraph A
union UnionTypeA = Type1 | Type2
# Subgraph B
union UnionTypeA @internal = Type2 | Type3
# Result
null
The following counter-example shows a invalid composition. Subgraph A could
still return Type1
for the field field1
, which would lead to an execution
error:
# Subgraph A
union UnionTypeA = Type1 | Type2
type Type1 {
field1: UnionTypeA
}
# Subgraph B
union UnionTypeA = Type2
type Type1 {
field1: UnionTypeA
}
# Result
union UnionTypeA = Type2
type Type1 {
field1: UnionTypeA
}
This algorithm merges two enum types from different subgraphs into one, given if
neither is marked as @internal
. This process simply retains the enum values
from {typeA} for the merged enum type as {typeA} and {typeB} are guaranteed to
have the same values because
Values Must Be The Same Across Subgraphs
Since enum types can be used both as input and output types, they must be strictly equal in both subgraphs. This strict equality rule avoids any invalid states that could arise from using either an intersection or union of the enum values.
MergeEnumType(typeA, typeB):
- If {typeA} or {typeB} are
@internal
- return null
- Let {enumType} be an empty EnumTypeDefinition
- Let {valuesOfA} be the set of enum values defined on {typeA}
- Set {enumType} to have the same name as {typeA}
- Set {valuesOfA} to be the values of {enumType}
- return {enumType}
Examples of Merging Enum Types:
# Subgraph A
enum EnumTypeA {
VALUE1
VALUE2
}
# Subgraph B
enum EnumTypeA {
VALUE1
VALUE2
}
# Result
enum EnumTypeA {
VALUE1
VALUE2
}
The following counter-example shows an invalid composition that uses the union
merge approach. A query to subgraph B could use VALUE1
in field1
which would
lead to an execution error:
# Subgraph A
enum EnumType1 {
VALUE1
}
# Subgraph B
enum EnumType1 {
VALUE2
}
input Input1 {
field1: EnumType1
}
# Result
enum EnumType1 {
VALUE1
VALUE2
}
input Input1 {
field1: EnumType1
}
The following counter-example shows an invalid composition that uses the
intersection merge approach. A query to subgraph A could return VALUE2
in
field1
which would lead to an execution error:
# Subgraph A
enum EnumType1 {
VALUE1
}
type Type1 {
field1: EnumType1
}
# Subgraph B
enum EnumType1 {
VALUE2
}
# Result
enum EnumType1 {
VALUE1
VALUE2
}
input Type1 {
field1: EnumType1
}
This algorithm merges two input fields from the same type of different subgraphs into one. The algorithm expects that both fields have the same schema coordinate. The type of the field is the result of calling {MergeInputFieldType(typeA, typeB)}.
MergeInputField(fieldA, fieldB):
- Let {mergedField} be an empty InputFieldDefinition
- Let {typeA} be the type of fieldA
- Let {typeB} be the type of fieldB
- Let {mergedType} be the result of calling {MergeInputFieldType(typeA, typeB)}
- Set {mergedField} to have the same name as {fieldA}
- Set {mergedField} to have {mergedType} as its type
- return {mergedField}
Merging Input Fields:
# Subgraph A
input InputTypeA {
field1: String
}
# Subgraph B
input InputTypeA {
field1: String!
}
# Result
input InputTypeA {
field1: String!
}
This algorithm merges two output fields from the same type of different subgraphs into one. The algorithm expects that both fields have the same schema coordinate. The arguments of both fields are merged based on {MergeArguments(fieldA, fieldB)}. The type of the field is the result of calling {MergeOutputFieldType(typeA, typeB)}.
MergeOutputField(fieldA, fieldB):
- Let {mergedField} be an empty OutputFieldDefinition
- Let {typeA} be the type of fieldA
- Let {typeB} be the type of fieldB
- Let {mergedType} be the result of calling {MergeOutputFieldType(typeA, typeB)}
- Set {mergedField} to have the same name as {fieldA}
- Set {mergedField} to have {mergedType} as its type
- Set {mergedField} to have arguments {MergeArguments(fieldA, fieldB)}
- return {mergedField}
Merging Output Fields:
# Subgraph A
type OutputTypeA {
field1(arg1: String, arg2: String): String
}
# Subgraph B
type OutputTypeA {
field1(arg1: String, arg3: String): String!
}
# Result
type OutputTypeA {
field1(arg1: String): String!
}
This algorithm merges the arguments of two fields from the same type of different subgraphs into a combined list of arguments. The algorithm expects that both fields have the same schema coordinate.
The arguments are merged by name, and the merged argument is the result of calling {MergeArgument(argumentA, argumentB)}.
Only arguments that are defined on both fields are merged. If an argument is only defined on one field, it is not included in the merged list to ensure that the merged field is compatible with both subgraphs.
MergeArguments(fieldA, fieldB):
- Let {mergedArguments} be an empty list of ArgumentDefinitions
- Let {arguments} be the intersection of the set of argument names that are defined on {fieldA} and {fieldB}
- For each {argumentName} in {arguments}:
- Let {argumentA} be the argument with the same name on {fieldA}
- Let {argumentB} be the argument with the same name on {fieldB}
- If {argumentA} or {argumentB} are
@internal
- continue
- Let {mergedArgument} be the result of calling {MergeArgument(argumentA, argumentB)}
- Append {mergedArgument} to {mergedArguments}
- return {mergedArguments}
Merging Arguments:
# Subgraph A
type Type1 {
field1(arg1: String, arg2: String): String
}
# Subgraph B
type Type1 {
field1(arg1: String, arg3: String): String
}
# Result
type Type1 {
field1(arg1: String): String
}
The following counter-example shows an invalid composition. The field1
can be
queried on the gateway with the argument arg2
, which would lead to an
execution error on subgraph B:
# Subgraph A
type Type1 {
field1(arg1: String, arg2: String): String
}
# Subgraph B
type Type1 {
field1(arg1: String): String
}
# Result
type Type1 {
field1(arg1: String, arg2: String): String
}
This algorithm merges two arguments from the same field of different subgraphs into one. The algorithm expects that both arguments have the same schema coordinate.
The type of the argument is the result of calling {MergeInputFieldType(typeA, typeB)}.
MergeArgument(argumentA, argumentB):
- Let {mergedArgument} be an empty ArgumentDefinition
- Let {typeA} be the type of argumentA
- Let {typeB} be the type of argumentB
- Let {mergedType} be the result of calling {MergeInputFieldType(typeA, typeB)}
- Set {mergedArgument} to have the same name as {argumentA}
- Set {mergedArgument} to have {mergedType} as its type
- return {mergedArgument}
Merging Arguments:
# Subgraph A
type Type1 {
field1(arg1: String!): String
}
# Subgraph B
type Type1 {
field1(arg1: String): String
}
# Result
type Type1 {
field1(arg1: String!): String
}
This algorithm merges the interfaces implemented by two types from different
subgraphs into a combined list of interfaces. The algorithm ignores any
interface that is marked as @internal
, to avoid exposing a implementation that
does not match a interface type.
MergeInterfaceImplementation(typeA, typeB):
- Let {interfaceNames} be the set of interface names that are defined on either {typeA} or {typeB}
- Let {validInterfaceNames} be the set of all interface in the composition that
are not
@internal
- Return the intersection of {interfaceNames} and {validInterfaceNames}
Merging Interface Implementations:
# Subgraph A
interface Interface1 {
field1: String
}
type Type1 implements Interface1 {
field1: String
}
# Subgraph B
type Type1 {
field1: String
}
# Result
interface Interface1 {
field1: String
}
type Type1 implements Interface1 {
field1: String
}
Merging Interface Implementations with @internal
Interfaces:
# Subgraph A
interface Interface1 @internal {
field1: String
}
type Type1 implements Interface1 {
field1: String
}
# Subgraph B
type Type1 {
field1: String
}
# Result
type Type1 {
field1: String
}
The following counter-example shows an invalid composition. The Type1
type
implements the Interface1
interface, but the Interface1
interface is marked
as @internal
in subgraph A.
# Subgraph A
interface Interface1 @internal {
field1: String
}
type Type1 implements Interface1 {
field1: String
}
# Subgraph B
type Type1 {
field1: String
}
This algorithm merges two input types. The merge will result in the least permissive type, while still be compatible with both input types.
MergeInputFieldType(typeA, typeB):
- If {typeA} and {typeB} are the same
- return {typeA}
- If {typeA} is Non-Null and {typeB} is Non-Null
- Let {innerTypeA} be the inner type of {typeA}
- Let {innerTypeB} be the inner type of {typeB}
- Let {innerType} be {MergeOutputFieldType(innerTypeA, innerTypeB)}
- return {EnsureNonNullType(innerType)}
- If {typeA} is Non-Null and {typeB} is nullable
- Let {innerTypeA} be the inner type of {typeA}
- Let {mergedType} be {MergeOutputFieldType(innerTypeA, typeB)}
- return {EnsureNonNullType(mergedType)}
- If {typeB} is Non-Null and {typeA} is nullable
- Let {innerTypeB} be the inner type of {typeB}
- Let {mergedType} be {MergeOutputFieldType(typeA, innerTypeB)}
- return {EnsureNonNullType(mergedType)}
- If both {typeA} and {typeB} are List
- Let {itemTypeA} be the item type of {typeA}
- Let {itemTypeB} be the item type of {typeB}
- Let {innerType} be {MergeOutputFieldType(itemTypeA, itemTypeB)}
- return {ToListType(innerType)}
- raise composition error
Merging nullable and non-nullable types:
# Subgraph A
input Input1 {
field1: String
}
# Subgraph B
input Input1 {
field1: String!
}
# Result
input Input1 {
field1: String!
}
Merging nullable and non-nullable list types:
# Subgraph A
type Type1 {
field1: [String]
}
# Subgraph B
type Type1 {
field1: [String!]
}
# Subgraph C
type Type1 {
field1: [String!]!
}
# Result
type Type1 {
field1: [String!]!
}
This algorithm merges two output types. The merge will result in the most permissive type, while still being compatible with both output types. This ensures that the data contract between the GraphQL Gateway and the API consumer can be fulfilled.
MergeOutputFieldType(typeA, typeB):
- If {typeA} and {typeB} are the same
- return {typeA}
- If both {typeA} and {typeB} are Non-Null
- Let {innerTypeA} be the inner type of {typeA}
- Let {innerTypeB} be the inner type of {typeB}
- Let {innerType} be {MergeOutputFieldType(innerTypeA, innerTypeB)}
- return {EnsureNonNullType(innerType)}
- If {typeA} is Non-Null and {typeB} is nullable
- Let {innerTypeA} be the inner type of {typeA}
- Let {mergedType} be {MergeOutputFieldType(innerTypeA, typeB)}
- return {EnsureNullableType(mergedType)}
- If {typeB} is Non-Null and {typeA} is nullable
- Let {innerTypeB} be the inner type of {typeB}
- Let {mergedType} be {MergeOutputFieldType(typeA, innerTypeB)}
- return {EnsureNullableType(mergedType)}
- If both {typeA} and {typeB} are Lists:
- Let {itemTypeA} be the item type of {typeA}.
- Let {itemTypeB} be the item type of {typeB}.
- Let {innerType} be {MergeOutputFieldType(itemTypeA, itemTypeB)}
- return {ToListType(innerType)}
- raise composition error
Merging nullable and non-nullable types:
# Subgraph A
type Type1 {
field1: String
}
# Subgraph B
type Type1 {
field1: String!
}
# Result
type Type1 {
field1: String
}
Merging nullable and non-nullable list types:
# Subgraph A
type Type1 {
field1: [String!]
}
# Subgraph B
type Type1 {
field1: [String]!
}
# Result
type Type1 {
field1: [String]
}
When merging types this helper ensures that the specified {type} is a non-null type or it wraps the specified {type} as a non-null type.
EnsureNonNullType(type):
- If {type} is Non-Null
- return {type}
- Let {nonNullType} be a new Non-Null type with {type} as inner type
- return {nonNullType}
# Before
String
# After
String!
When merging types this helper ensures that the specified {type} is a nullable type or it returns the inner type of the non-null type.
EnsureNullableType(type):
- If {type} is Non-Null
- Let {innerType} be the inner type of {type}
- return {innerType}
- return {type}
# Before
String!
# After
String
This algorithm creates list type for the provided type.
ToListType(type):
- Let {listType} be a new List type with {type} as item type
- return {listType}
# Before
String
# After
[String]