From 44c3bb9985ec670d7ed294c3d1bfcdbc4102f137 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Tue, 31 Dec 2024 15:52:29 +0100 Subject: [PATCH 1/9] Adds Input With Missing Required Fields #41 Co-authored-by: Michael Staib Co-authored-by: Glen --- spec/Section 4 -- Composition.md | 92 ++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 9 deletions(-) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 60c5473..bfdfb79 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -2740,12 +2740,11 @@ is not found, following the standard GraphQL practices for representing missing data. In a distributed system, it is likely that some entities will not be found on -other subgraphs, even when those subgraphs contribute fields to the type. -Ensuring that `@lookup` fields have nullable return types also avoids GraphQL -errors on subgraphs and prevents result erasure through non-null propagation. By -allowing null to be returned when an entity is not found, the system can -gracefully handle missing data without causing exceptions or unexpected -behavior. +other schemas, even when those schemas contribute fields to the type. Ensuring +that `@lookup` fields have nullable return types also avoids GraphQL errors on +schemas and prevents result erasure through non-null propagation. By allowing +null to be returned when an entity is not found, the system can gracefully +handle missing data without causing exceptions or unexpected behavior. Ensuring that `@lookup` fields have nullable return types allows gateways to distinguish between cases where an entity is not found (receiving null) and @@ -2951,9 +2950,9 @@ 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}. - - {InputTypesAreMerable(typeA, typeB)} must be true. + - {InputTypesAreMergeable(typeA, typeB)} must be true. -InputTypesAreMerable(typeA, typeB): +InputTypesAreMergeable(typeA, typeB): - If {typeA} is a non nullable type: - Set {typeA} to the inner type of {typeA}. @@ -2964,7 +2963,7 @@ InputTypesAreMerable(typeA, typeB): - Return false. - Let {innerTypeA} be the inner type of {typeA}. - Let {innerTypeB} be the inner type of {typeB}. - - Return {InputTypesAreMerable(innerTypeA, innerTypeB)}. + - Return {InputTypesAreMergeable(innerTypeA, innerTypeB)}. - If {typeA} is equal to {typeB} - return true - Otherwise return false. @@ -3105,6 +3104,81 @@ enum Genre { } ``` +#### Input With Missing Required Fields + +**Error Code:** + +`REQUIRED_INPUT_FIELD_MISSING_IN_SOME_SUBGRAPH` + +**Severity:** + +ERROR + +**Formal Specification:** + +- Let {typeNames} be the set of all input object types names from all source + schemas that are not declared as `@inaccessible`. +- For each {typeName} in {typeNames}: + - Let {types} be the list of all input object types from different source + schemas with the name {typeName}. + - {AreTypesConsistent(types)} must be true. + +AreTypesConsistent(inputs): + +- Let {requiredFields} be the intersection of all field names across all input + objects in {inputs} that are not marked as `@inaccessible` in any schema and + have a non-nullable type in at least one schema. +- For each {input} in {inputs}: + - For each {requiredField} in {requiredFields}: + - If {requiredField} is not in {input}: + - Return false + +**Explanatory Text:** + +Input types are merged by intersection, meaning that the merged input type will +have all fields that are present in all input types with the same name. This +rule ensures that input object types with the same name across different schemas +share a consistent set of required fields. + +**Examples** + +If all schemas define `BookFilter` with the required field `title`, the rule is +satisfied: + +```graphql +# Schema A +input BookFilter { + title: String! + author: String +} + +# Schema B +input BookFilter { + title: String! + yearPublished: Int +} +``` + +If `title` is required in one subgraph but missing in another, this violates the +rule: + +```graphql +# Schema A +input BookFilter { + title: String! + author: String +} + +# Schema B +input BookFilter { + author: String + yearPublished: Int +} +``` + +In this invalid case, `title` is mandatory in Schema A but not defined in +`Schema B`, causing inconsistency in required fields across schemas. + ### Merge During this stage, all definitions from each source schema are combined into a From 3347f1f4153098eeb4f93319fdee1fa10747302d Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Tue, 31 Dec 2024 16:03:45 +0100 Subject: [PATCH 2/9] Adds Output Field Argument Types Mergable to Composition #39 Co-authored-by: Michael Staib Co-authored-by: Glen --- spec/Section 4 -- Composition.md | 120 +++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index bfdfb79..099892d 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -3179,6 +3179,126 @@ input BookFilter { In this invalid case, `title` is mandatory in Schema A but not defined in `Schema B`, causing inconsistency in required fields across schemas. +#### Output Field Argument Types Mergeable + +**Error Code** + +`OUTPUT_FIELD_ARGUMENT_TYPES_NOT_MERGEABLE` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {typeNames} be the set of all output type names from all source schemas. +- For each {typeName} in {typeNames} + - Let {types} be the set of all types with the {typeName} from all source + schemas. + - Let {fieldNames} be the set of all field names from all {types}. + - For each {fieldName} in {fieldNames} + - Let {fields} be the set of all fields with the {fieldName} from all + {types}. + - For each {field} in {fields} + - Let {argumentNames} be the set of all argument names from all {fields}. + - For each {argumentName} in {argumentNames} + - Let {arguments} be the set of all arguments with the {argumentName} + from all {fields}. + - For each pair of {argumentA} and {argumentB} in {arguments} + - {ArgumentsAreMergeable(argumentA, argumentB)} must be true. + +ArgumentsAreMergeable(argumentA, argumentB): + +- Let {typeA} be the type of {argumentA} +- Let {typeB} be the type of {argumentB} +- {InputTypesAreMergeable(typeA, typeB)} must be true. + +**Explanatory Text** + +When multiple schemas define the same field name on the same output type (e.g., +`User.field`), these fields can be merged if their arguments are compatible. +Compatibility extends not only to the output field types themselves, but to each +argument's input type as well. The schemas must agree on each argument's name +and have compatible types, so that the composed schema can unify the definitions +into a single consistent field specification. + +_Nullability_ + +Different nullability requirements on arguments are still considered mergeable. +For example, if one schema accepts `String!` and the other accepts `String`, +these schemas can merge; the resulting argument type typically adopts the least +restrictive (nullable) version. + +_Lists_ Lists of different nullability (e.g., `[String!]` vs. `[String]!` vs. +`[String]`) remain mergeable as long as they otherwise refer to the same inner +type. Essentially, the same principle of “least restrictive” nullability merges +them successfully. + +_Incompatible Types_ + +If argument types differ on the named type itself - for example, one uses +`String` while the other uses `DateTime` - this causes an +`OUTPUT_FIELD_ARGUMENT_TYPES_NOT_MERGEABLE` error. Similarly, if one schema has +`[String]` but another has `[DateTime]`, they are incompatible. + +```graphql example +type User { + field(argument: String): String +} + +type User { + field(argument: String): String +} +``` + +Arguments that differ on nullability of an argument type are mergeable. + +```graphql example +type User { + field(argument: String!): String +} + +type User { + field(argument: String): String +} +``` + +```graphql example +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. + +```graphql counter-example +type User { + field(argument: String!): String +} + +type User { + field(argument: DateTime): String +} +``` + +```graphql counter-example +type User { + field(argument: [String]): String +} + +type User { + field(argument: [DateTime]): String +} +``` + ### Merge During this stage, all definitions from each source schema are combined into a From 57b1c13659b6cbedb1dc622a769d2b0fe2743548 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Tue, 31 Dec 2024 16:21:39 +0100 Subject: [PATCH 3/9] Adds Non Null Input fields cannot be inaccessible #40 Co-authored-by: Michael Staib Co-authored-by: Glen --- spec/Section 4 -- Composition.md | 178 +++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 099892d..92af4ae 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -5578,6 +5578,184 @@ input BookFilter { } ``` +#### Non-Null Input Fields cannot be inaccessible + +**Error Code** + +`NON_NULL_INPUT_FIELD_IS_INACCESSIBLE` + +**Formal Specification** + +- Let {fields} be the set of all fields across all input types in all source + schemas. +- For each {field} in {fields}: + - If {field} is a non-null input field: + - Let {coordinate} be the coordinate of {field}. + - {coordinate} must be in the composite schema. + +**Explanatory Text** + +When an input field is declared as non-null in any source schema, it imposes a +hard requirement: queries or mutations that reference this field _must_ provide +a value for it. If the field is then marked as `@inaccessible` or removed during +schema composition, the final schema would still implicitly demand a value for a +field that no longer exists in the composed schema, making it impossible to +fulfill the requirement. + +As a result: + +- **Nullable** (optional) fields can be hidden or removed without invalidating + the composed schema, because the user is never _required_ to supply a value + for them. +- **Non-null** (required) fields, however, must remain exposed in the composed + schema so that users can provide values for those fields. Hiding a required + input field breaks the schema contract and leads to an invalid composition. + +**Examples** + +The following is valid because the `age` field, although `@inaccessible` in one +source schema, is nullable and can be safely omitted in the final schema without +breaking any mandatory input requirement. + +```graphql example +# Schema A +input BookFilter { + author: String! + age: Int @inaccessible +} + +# Schema B +input BookFilter { + author: String! + age: Int +} + +# Composite Schema +input BookFilter { + author: String! +} +``` + +Another valid case is when a nullable input field is removed during merging: + +```graphql example +# Schema A +input BookFilter { + author: String! + age: Int +} + +# Schema B +input BookFilter { + author: String! +} + +# Composite Schema +input BookFilter { + author: String! +} +``` + +An invalid case is when a non-null input field is inaccessible: + +```graphql counter-example +# Schema A +input BookFilter { + author: String! + age: Int! +} + +# Schema B +input BookFilter { + author: String! + age: Int @inaccessible +} + +# Composite Schema +input BookFilter { + author: String! +} +``` + +Another invalid case is when a non-null input field is removed during merging: + +```graphql counter-example +# Schema A +input BookFilter { + author: String! + age: Int! +} + +# Schema B +input BookFilter { + author: String! +} + +# Composite Schema +input BookFilter { + author: String! +} +``` + +#### Input Fields cannot reference inaccessible type + +**Error Code** + +INPUT_FIELD_REFERENCES_INACCESSIBLE_TYPE + +**Formal Specification** + +- Let {fields} be the set of all fields of the input types +- For each {field} in {fields}: + - If {field} is not declared as `@inaccessible` + - Let {namedType} be the named type that {field} references + - {namedType} must not be declared as `@inaccessible` + +**Explanatory Text** + +In a composed schema, a field within an input type must only reference types +that are exposed. This requirement guarantees that public types do not reference +inaccessible structures which are intended for internal use. + +A valid case where a public input field references another public input type: + +```graphql example +input Input1 { + field1: String! + field2: Input2 +} + +input Input2 { + field3: String +} +``` + +Another valid case is where the field is not exposed in the composed schema: + +```graphql example +input Input1 { + field1: String! + field2: Input2 @inaccessible +} + +input Input2 @inaccessible { + field3: String +} +``` + +An invalid case is when an input field references an inaccessible type: + +```graphql counter-example +input Input1 { + field1: String! + field2: Input2! +} + +input Input2 @inaccessible { + field3: String +} +``` + ## Validate Satisfiability The final step confirms that the composite schema supports executable queries From 65be03d6654d23302290efb4b02d128359c8b47f Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 31 Dec 2024 20:26:33 +0200 Subject: [PATCH 4/9] Add callout that external subschema approach is not prohibited. (#19) Co-authored-by: Michael Staib Co-authored-by: Benjie --- spec/Section 1 -- Overview.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/Section 1 -- Overview.md b/spec/Section 1 -- Overview.md index b973984..e6af453 100644 --- a/spec/Section 1 -- Overview.md +++ b/spec/Section 1 -- Overview.md @@ -41,6 +41,11 @@ The GraphQL Composite Schemas specification has a number of design principles: GraphQL Composite Schemas specification prefers to be explicit about intentions and minimize reliance on inference and convention. +Note: Although the GraphQL Composite Schemas specification does not describe how +to combine arbitrary schemas, tooling may be built to transform existing or +external schemas into compliant _source schemas_. Details of building such +tooling is beyond the scope of this specification. + To enable greater interoperability between different implementations of tooling and gateways, this specification focuses on two core components: schema composition and distributed execution. From a74f8a3ef8ab84e8a8a5785c1ee5e20fe4dd1888 Mon Sep 17 00:00:00 2001 From: Glen Date: Mon, 6 Jan 2025 15:43:58 +0200 Subject: [PATCH 5/9] Update "Input Field Types Mergeable" rule specification (#108) --- spec/Section 4 -- Composition.md | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 92af4ae..dc45445 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -2950,23 +2950,7 @@ 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}. - - {InputTypesAreMergeable(typeA, typeB)} must be true. - -InputTypesAreMergeable(typeA, typeB): - -- If {typeA} is a non nullable type: - - Set {typeA} to the inner type of {typeA}. -- If {typeB} is a non nullable type: - - Set {typeB} to the inner type of {typeB}. -- If {typeA} is a list type: - - If {typeB} is not list type. - - Return false. - - Let {innerTypeA} be the inner type of {typeA}. - - Let {innerTypeB} be the inner type of {typeB}. - - Return {InputTypesAreMergeable(innerTypeA, innerTypeB)}. -- If {typeA} is equal to {typeB} - - return true -- Otherwise return false. + - {SameTypeShape(typeA, typeB)} must be true. **Explanatory Text** From f28591097933f432feb575960ad985ae0f85ebd8 Mon Sep 17 00:00:00 2001 From: Glen Date: Tue, 7 Jan 2025 11:54:58 +0200 Subject: [PATCH 6/9] Removed 2-space line breaks (#110) --- spec/Section 4 -- Composition.md | 100 ++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index dc45445..122dee6 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -1159,10 +1159,12 @@ type User @key(fields: "id tags") { #### Key Invalid Syntax -**Error Code** +**Error Code** + `KEY_INVALID_SYNTAX` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -1602,11 +1604,11 @@ ERROR **Explanatory Text** The `@require` directive is used to specify fields on the same type that an -argument depends on in order to resolve the annotated field. -When using `@require(fields: "…")`, the `fields` argument must be a valid -selection set string **without** any additional directive applications. -Applying a directive (e.g., `@lowercase`) inside this selection set is not -supported and triggers the `REQUIRE_DIRECTIVE_IN_FIELDS_ARG` error. +argument depends on in order to resolve the annotated field. When using +`@require(fields: "…")`, the `fields` argument must be a valid selection set +string **without** any additional directive applications. Applying a directive +(e.g., `@lowercase`) inside this selection set is not supported and triggers the +`REQUIRE_DIRECTIVE_IN_FIELDS_ARG` error. **Examples** @@ -1821,10 +1823,12 @@ input FieldSelectionMap { #### Type Kind Mismatch -**Error Code** +**Error Code** + `TYPE_KIND_MISMATCH` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -1898,10 +1902,12 @@ extend input User { #### Provides Invalid Syntax -**Error Code** +**Error Code** + `PROVIDES_INVALID_SYNTAX` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -1951,10 +1957,12 @@ type User @key(fields: "id") { #### Invalid GraphQL -**Error Code** +**Error Code** + `INVALID_GRAPHQL` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -2030,10 +2038,12 @@ type Product { #### Override Collision with Another Directive -**Error Code** +**Error Code** + `OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -2100,10 +2110,12 @@ type Payment { #### Override from Self Error -**Error Code** +**Error Code** + `OVERRIDE_FROM_SELF_ERROR` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -2160,10 +2172,12 @@ type Bill { #### Override on Interface -**Error Code** +**Error Code** + `OVERRIDE_ON_INTERFACE` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -2223,10 +2237,12 @@ interface Bill { #### Override Source Has Override -**Error Code** +**Error Code** + `OVERRIDE_SOURCE_HAS_OVERRIDE` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -2364,10 +2380,12 @@ type Bill { #### External Collision with Another Directive -**Error Code** +**Error Code** + `EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -2465,10 +2483,12 @@ type Book { #### Key Invalid Fields Type -**Error Code** +**Error Code** + `KEY_INVALID_FIELDS_TYPE` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -2520,10 +2540,12 @@ type User @key(fields: true) { #### Provides Invalid Fields Type -**Error Code** +**Error Code** + `PROVIDES_INVALID_FIELDS_TYPE` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -2588,10 +2610,12 @@ type ProductDetails { #### Provides on Non-Composite Field -**Error Code** +**Error Code** + `PROVIDES_ON_NON_COMPOSITE_FIELD` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -2651,10 +2675,12 @@ type User { #### External on Interface -**Error Code** +**Error Code** + `EXTERNAL_ON_INTERFACE` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -5164,10 +5190,12 @@ interface InventoryItem { #### Only Inaccessible Children -**Error Code** +**Error Code** + `ONLY_INACCESSIBLE_CHILDREN` -**Severity** +**Severity** + ERROR **Formal Specification** @@ -5406,10 +5434,12 @@ type Book { #### Provides Invalid Fields -**Error Code** +**Error Code** + `PROVIDES_INVALID_FIELDS` -**Severity** +**Severity** + ERROR **Formal Specification** From 007fa6cf226c18f1ac359ec858ba6fa6f250897d Mon Sep 17 00:00:00 2001 From: Glen Date: Tue, 7 Jan 2025 16:21:06 +0200 Subject: [PATCH 7/9] Rename "Enum Type Values Must Be The Same Across Source Schemas" rule (#111) --- spec/Section 4 -- Composition.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 122dee6..c3fd71d 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -3033,11 +3033,11 @@ input AuthorInput { } ``` -#### Enum Type Values Must Be The Same Across Source Schemas +#### Enum Values Mismatch **Error Code** -`ENUM_VALUES_MUST_BE_THE_SAME_ACROSS_SCHEMAS` +`ENUM_VALUES_MISMATCH` **Formal Specification** From cb5d41065f0e01fbb6a8f283fbb9b83430d2eff2 Mon Sep 17 00:00:00 2001 From: Glen Date: Tue, 7 Jan 2025 16:21:21 +0200 Subject: [PATCH 8/9] Rename "Override from Self Error" rule to "Override from Self" (#112) --- spec/Section 4 -- Composition.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index c3fd71d..37926c9 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -2108,11 +2108,11 @@ type Payment { } ``` -#### Override from Self Error +#### Override from Self **Error Code** -`OVERRIDE_FROM_SELF_ERROR` +`OVERRIDE_FROM_SELF` **Severity** @@ -2137,7 +2137,7 @@ When using `@override`, the `from` argument indicates the name of the source schema that originally owns the field. Overriding from the **same** schema creates a contradiction, as it implies both local and transferred ownership of the field within one schema. If the `from` value matches the local schema name, -it triggers an `OVERRIDE_FROM_SELF_ERROR`. +it triggers an `OVERRIDE_FROM_SELF` error. **Examples** @@ -2160,7 +2160,7 @@ type Bill { In the following counter-example, the local schema is also `"SchemaA"`, and the `from` argument is `"SchemaA"`. Overriding a field from the same schema is not -allowed, causing an `OVERRIDE_FROM_SELF_ERROR`. +allowed, causing an `OVERRIDE_FROM_SELF` error. ```graphql counter-example # Source Schema A (named "SchemaA") From 4e5db121c62b614e2e2fdcb66133f6cb54de539d Mon Sep 17 00:00:00 2001 From: Glen Date: Wed, 8 Jan 2025 16:10:06 +0200 Subject: [PATCH 9/9] Shorten error code for "Input With Missing Required Fields" rule (#116) --- spec/Section 4 -- Composition.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 37926c9..3730e5a 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -3118,7 +3118,7 @@ enum Genre { **Error Code:** -`REQUIRED_INPUT_FIELD_MISSING_IN_SOME_SUBGRAPH` +`INPUT_WITH_MISSING_REQUIRED_FIELDS` **Severity:**