From 8719bdfa22a7cd1191829fe74a3a9041368ad18b Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 7 Nov 2024 13:09:51 +0100 Subject: [PATCH 01/14] Adds Empty Object Type Validation Rule (#49) Co-authored-by: PascalSenn --- spec/Section 4 -- Composition.md | 80 ++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 35d3b72..6b6232c 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -18,4 +18,84 @@ run in sequence to produce the composite execution schema. ### Post Merge Validation +#### Empty Merged Object Type + +**Error Code** + +`EMPTY_MERGED_OBJECT_TYPE` + +**Severity** ERROR + +**Formal Specification** + +- Let {types} be the set of all object types across all source schemas +- For each {type} in {types}: + - {IsObjectTypeEmpty(type)} must be false. + +IsObjectTypeEmpty(type): + +- If {type} has `@inaccessible` directive +- return false +- Let {fields} be a set of all fields in {type} +- For each {field} in {fields}: + - If {IsExposed(field)} is true + - return false +- return true + +**Explanatory Text** + +For object types defined across multiple source schemas, the merged object type +is the superset of all fields defined in these source schemas. However, any +field marked with `@inaccessible` in any source schema is hidden and not +included in the merged object type. An object type with no fields, after +considering `@inaccessible` annotations, is considered empty and invalid. + +In the following example, the merged object type `ObjectType1` is valid. It +includes all fields from both source schemas, with `field2` being hidden due to +the `@inaccessible` directive in one of the source schemas: + +```graphql +type ObjectType1 { + field1: String + field2: Int @inaccessible +} + +type ObjectType1 { + field2: Int + field3: Boolean +} +``` + +If the `@inaccessible` directive is applied to an object type itself, the entire +merged object type is excluded from the composite execution schema, and it is +not required to contain any fields. + +```graphql +type ObjectType1 @inaccessible { + field1: String + field2: Int +} + +type ObjectType1 { + field3: Boolean +} +``` + +This counter-example demonstrates an invalid merged object type. In this case, +`ObjectType1` is defined in two source schemas, but all fields are marked as +`@inaccessible` in at least one of the source schemas, resulting in an empty +merged object type: + +```graphql counter-example +type ObjectType1 { + field1: String @inaccessible + field2: Boolean +} + +type ObjectType1 { + field1: String + field2: Boolean @inaccessible +} +``` + ## Validate Satisfiability From a350b1935d92c5e36febcaf9febc10c0070bd171 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Thu, 7 Nov 2024 13:28:55 +0100 Subject: [PATCH 02/14] Adds Output Field Types Mergable to Composition (#38) Co-authored-by: Michael Staib --- spec/Section 4 -- Composition.md | 101 +++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 6b6232c..1ea1a52 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -14,6 +14,107 @@ run in sequence to produce the composite execution schema. ### Pre Merge Validation +#### Output Field Types Mergeable + +**Error Code** + +OUTPUT_FIELD_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 name {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 name {fieldName} from all + {types}. + - {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} + - {SameOutputTypeShape(typeA, typeB)} must be true. + +**Explanatory Text** + +Fields on objects or interfaces 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. + +```graphql example +type User { + birthdate: String +} + +type User { + birthdate: String +} +``` + +Fields with different nullability are mergeable, resulting in a merged field +with a nullable type. + +```graphql example +type User { + birthdate: String! +} + +type User { + birthdate: String +} +``` + +```graphql example +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. + +```graphql counter-example +type User { + birthdate: String! +} + +type User { + birthdate: DateTime! +} +``` + +```graphql counter-example +type User { + tags: [Tag] +} + +type Tag { + value: String +} + +type User { + tags: [Tag] +} + +scalar Tag +``` + ### Merge ### Post Merge Validation From 6754b118dd3e14155591e462dfb49592030373f7 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Thu, 14 Nov 2024 11:21:05 +0100 Subject: [PATCH 03/14] Adds validation rule for external (#60) --- spec/Section 4 -- Composition.md | 500 ++++++++++++++++++++++++++++++- 1 file changed, 499 insertions(+), 1 deletion(-) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 1ea1a52..1a9bca9 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -115,6 +115,504 @@ type User { scalar Tag ``` +#### Disallowed Inaccessible Elements + +**Error Code** + +`DISALLOWED_INACCESSIBLE` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {type} be the set of all types from all source schemas. +- For each {type} in {types}: + - If {type} is a built-in scalar type or introspection type: + - {IsAccessible(type)} must be true. + - For each {field} in {type}: + - {IsAccessible(field)} must be true. + - For each {argument} in {field}: + - {IsAccessible(argument)} must be true. +- For each {directive} in {directives}: + - If {directive} is a built-in directive: + - {IsAccessible(directive)} must be true. + - For each {argument} in {directive}: + - {IsAccessible(argument)} must be true. + +**Explanatory Text** + +This rule ensures that certain essential elements of a GraphQL schema, +particularly built-in scalars, directives and introspection types, cannot be +marked as `@inaccessible`. These types are fundamental to GraphQL's. Making +these elements inaccessible would break core GraphQL functionalities. + +Here, the `String` type is not marked as `@inaccessible`, which adheres to the +rule: + +```graphql example +type Product { + price: Float + name: String +} +``` + +In this example, the `String` scalar is marked as `@inaccessible`. This violates +the rule because `String` is a required built-in type that cannot be +inaccessible: + +```graphql counter-example +scalar String @inaccessible + +type Product { + price: Float + name: String +} +``` + +In this example, the introspection type `__Type` is marked as `@inaccessible`. +This violates the rule because introspection types must remain accessible for +GraphQL introspection queries to work. + +```graphql counter-example +type __Type @inaccessible { + kind: __TypeKind! + name: String + fields(includeDeprecated: Boolean = false): [__Field!] +} +``` + +#### External Argument Default Mismatch + +**Error Code** + +`EXTERNAL_ARGUMENT_DEFAULT_MISMATCH` + +**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 name {typeName} from all source + schemas. + - Let {fieldNames} be the set of all field names from all types in {types}. + - For each {fieldName} in {fieldNames} + - Let {fields} be the set of all fields with the name {fieldName} from all + types in {types}. + - Let {externalFields} be the set of all fields in {fields} that are marked + with `@external`. + - If {externalFields} is not empty + - Let {argumentNames} be the set of all argument names from all fields in + {fields}. + - For each {argumentName} in {argumentNames} + - Let {arguments} be the set of all arguments with the name + {argumentName} from all fields in {fields}. + - Let {defaultValue} be the first default value found in {arguments}. + - Let {externalArguments} be the set of all arguments with the name + {argumentName} from all fields in {externalFields}. + - For each {externalArgument} in {externalArguments} + - The default value of {externalArgument} must be equal to + {defaultValue}. + +**Explanatory Text** + +This rule ensures that arguments on fields marked as `@external` have default +values compatible with the corresponding arguments on fields from other source +schemas where the field is defined (non-`@external`). Since `@external` fields +represent fields that are resolved by other source schemas, their arguments and +defaults must match to maintain consistent behavior across different source +schemas. + +Here, the `name` field on `Product` is defined in one source schema and marked +as `@external` in another. The argument `language` has the same default value in +both source schemas, satisfying the rule: + +```graphql example +# Subgraph A +type Product { + name(language: String = "en"): String +} + +# Subgraph B +type Product { + name(language: String = "en") @external: String +} +``` + +Here, the `name` field on `Product` is defined in one source schema and marked +as `@external` in another. The argument `language` has different default values +in the two source schemas, violating the rule: + +```graphql counter-example +# Subgraph A +type Product { + name(language: String = "en"): String +} + +# Subgraph B +type Product { + name(language: String = "de") @external: String +} +``` + +In the following counter example, the `name` field on `Product` is defined in +one source schema and marked as `@external` in another. The argument `language` +has a default value in the source schema where the field is defined, but it does +not have a default value in the source schema where the field is marked as +`@external`, violating the rule: + +```graphql counter-example +# Subgraph A +type Product { + name(language: String = "en"): String +} + +# Subgraph B +type Product { + name(language: String): String @external +} +``` + +#### External Argument Missing + +**Error Code** + +`EXTERNAL_ARGUMENT_MISSING` + +**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 name {typeName} from all source + schemas. + - Let {fieldNames} be the set of all field names from all types in {types}. + - For each {fieldName} in {fieldNames} + - Let {fields} be the set of all fields with the name {fieldName} from all + types in {types}. + - Let {externalFields} be the set of all fields in {fields} that are marked + with `@external`. + - Let {nonExternalFields} be the set of all fields in {fields} that are not + marked with `@external`. + - If {externalFields} is not empty + - Let {argumentNames} be the set of all argument names from all fields in + {nonExternalFields} + - For each {argumentName} in {argumentNames}: + - For each {externalField} in {externalFields} + - {argumentName} must be present in the arguments of {externalField}. + +**Explanatory Text** + +This rule ensures that fields marked with `@external` have all the necessary +arguments that exist on the corresponding field definitions in other source +schemas. Each argument defined on the base field (the field definition in the +source source schema) must be present on the `@external` field in other source +schemas. If an argument is missing on an `@external` field, the field cannot be +resolved correctly, which is an inconsistency. + +In this example, the `language` argument is present on both the `@external` +field in source schema B and the base field in source schema A, satisfying the +rule: + +```graphql example +# Subgraph A +type Product { + name(language: String): String +} + +# Subgraph B +type Product { + name(language: String): String @external +} +``` + +Here, the `@external` field in source schema B is missing the `language` +argument that is present in the base field definition in source schema A, +violating the rule: + +```graphql counter-example +# Subgraph A +type Product { + name(language: String): String +} + +# Subgraph B +type Product { + name: String @external +} +``` + +#### External Argument Type Mismatch + +**Error Code** + +`EXTERNAL_ARGUMENT_TYPE_MISMATCH` + +**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 name {typeName} from all source + schemas. + - Let {fieldNames} be the set of all field names from all types in {types}. + - For each {fieldName} in {fieldNames} + - Let {fields} be the set of all fields with the name {fieldName} from all + types in {types}. + - Let {externalFields} be the set of all fields in {fields} that are marked + with `@external`. + - Let {nonExternalFields} be the set of all fields in {fields} that are not + marked with `@external`. + - If {externalFields} is not empty + - Let {argumentNames} be the set of all argument names from all fields in + {nonExternalFields} + - For each {argumentName} in {argumentNames}: + - For each {externalField} in {externalFields} + - Let {externalArgument} be the argument with the name {argumentName} + from {externalField}. + - {externalArgument} must strictly equal all arguments with the name + {argumentName} from {nonExternalFields}. + +**Explanatory Text** + +This rule ensures that arguments on fields marked as `@external` have types +compatible with the corresponding arguments on the fields defined in other +source schemas. The arguments must have the exact same type signature, including +nullability and list nesting. + +Here, the `@external` field's `language` argument has the same type (`Language`) +as the base field, satisfying the rule: + +```graphql example +# Subgraph A +type Product { + name(language: Language): String +} + +# Subgraph B +type Product { + name(language: Language): String +} +``` + +In this example, the `@external` field's `language` argument type does not match +the base field's `language` argument type (`Language` vs. `String`), violating +the rule: + +```graphql example +# Subgraph A +type Product { + name(language: Language): String +} + +# Subgraph B +type Product { + name(language: String): String +} +``` + +#### External Missing on Base + +**Error Code** + +`EXTERNAL_MISSING_ON_BASE` + +**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 name {typeName} from all source + schemas. + - Let {fieldNames} be the set of all field names from all types in {types}. + - For each {fieldName} in {fieldNames} + - Let {fields} be the set of all fields with the name {fieldName} from all + types in {types}. + - Let {externalFields} be the set of all fields in {fields} that are marked + with `@external`. + - Let {nonExternalFields} be the set of all fields in {fields} that are not + marked with `@external`. + - If {externalFields} is not empty + - {nonExternalFields} must not be empty. + +**Explanatory Text** + +This rule ensures that any field marked as `@external` in a source schema is +actually defined (non-`@external`) in at least one other source schema. The +`@external` directive is used to indicate that the field is not usually resolved +by the source schema it is declared in, implying it should be resolvable by at +least one other source schema. + +Here, the `name` field on `Product` is defined in source schema A and marked as +`@external` in source schema B, which is valid because there is a base +definition in source schema A: + +```graphql example +# Subgraph A +type Product { + id: ID + name: String +} + +# Subgraph B +type Product { + id: ID + name: String @external +} +``` + +In this example, the `name` field on `Product` is marked as `@external` in +source schema B but has no non-`@external` declaration in any other source +schema, violating the rule: + +```graphql counter-example +# Subgraph A +type Product { + id: ID +} + +# Subgraph B +type Product { + id: ID + name: String @external +} +``` + +#### External Type Mismatch + +**Error Code** + +`EXTERNAL_TYPE_MISMATCH` + +**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 name {typeName} from all source + schemas. + - Let {fieldNames} be the set of all field names from all types in {types}. + - For each {fieldName} in {fieldNames} + - Let {fields} be the set of all fields with the name {fieldName} from all + types in {types}. + - Let {externalFields} be the set of all fields in {fields} that are marked + with `@external`. + - Let {nonExternalFields} be the set of all fields in {fields} that are not + marked with `@external`. + - For each {externalField} in {externalFields} + - The type of {externalField} must strictly equal all types of + {nonExternalFields}. + +**Explanatory Text** + +This rule ensures that a field marked as `@external` has a return type +compatible with the corresponding field defined in other source schemas. Fields +with the same name must represent the same data type to maintain schema +consistency + +Here, the `@external` field `name` has the same return type (`String`) as the +base field definition, satisfying the rule: + +```graphql example +# Subgraph A +type Product { + name: String +} + +# Subgraph B +type Product { + name: String @external +} +``` + +In this example, the `@external` field `name` has a return type of `ProductName` +that doesn't match the base field's return type `String`, violating the rule: + +```graphql counter-example +# Subgraph A +type Product { + name: String +} + +# Subgraph B +type Product { + name: ProductName @external +} +``` + +#### External Unused + +**Error Code** + +`EXTERNAL_UNUSED` + +**Severity** + +ERROR + +**Formal Specification** + +- For each {schema} in all source schemas + - Let {types} be the set of all composite types (object, interface) in + {schema}. + - For each {type} in {types}: + - Let {fields} be the set of fields for {type}. + - For each {field} in {fields}: + - If {field} is marked with `@external`: + - Let {referencingFields} be the set of fields in {schema} that + reference {type}. + - {referencingFields} must contain at least one field that references + {field} in `@provides` + +**Explanatory Text** + +This rule ensures that every field marked as `@external` in a source schema is +actually used by that source schema in a `@provides` directive. + +**Examples** + +In this example, the `name` field is marked with `@external` and is used by the +`@provides` directive, satisfying the rule: + +```graphql example +# Subgraph A +type Product { + id: ID + name: String @external +} + +type Query { + productByName(name: String): Product @provides(fields: "name") +} +``` + +In this example, the `name` field is marked with `@external` but is not used by +the `@provides` directive, violating the rule: + +```graphql counter-example +# Subgraph A +type Product { + title: String @external + author: Author +} +``` + ### Merge ### Post Merge Validation @@ -139,7 +637,7 @@ IsObjectTypeEmpty(type): - return false - Let {fields} be a set of all fields in {type} - For each {field} in {fields}: - - If {IsExposed(field)} is true + - If {IsAccessible(field)} is true - return false - return true From f56a36606ce8bac3346e1fc212b53b2b3f5ef7d0 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 14 Nov 2024 14:47:18 +0200 Subject: [PATCH 04/14] Adds Enum Type Default Value Uses Inaccessible Value Rule (#47) --- spec/Section 4 -- Composition.md | 108 +++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 1a9bca9..09e5e4b 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -14,6 +14,114 @@ run in sequence to produce the composite execution schema. ### Pre Merge Validation +#### Enum Type Default Value Uses Inaccessible Value + +**Error Code** + +`ENUM_TYPE_DEFAULT_VALUE_INACCESSIBLE` + +**Formal Specification** + +- {ValidateArgumentDefaultValues()} must be true. +- {ValidateInputFieldDefaultValues()} must be true. + +ValidateArgumentDefaultValues(): + +- Let {arguments} be all arguments of fields and directives across all source + schemas +- 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 +- return true + +ValidateInputFieldDefaultValues(): + +- Let {inputFields} be all input fields across all source schemas +- For each {inputField} in {inputFields}: + - Let {type} be the type of {inputField} + - 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 +- return true + +ValidateDefaultValue(defaultValue): + +- If {defaultValue} is a ListValue: + - For each {valueNode} in {defaultValue}: + - If {ValidateDefaultValue(valueNode)} is false + - return false +- 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 + - 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 +- return true + +**Explanatory Text** + +This rule ensures that inaccessible enum values are not exposed in the composed +schema through default values. Output field arguments, input fields, and +directive arguments must only use enum values as their default value when not +annotated with the `@inaccessible` directive. + +In this example the `FOO` value in the `Enum1` enum is not marked with +`@inaccessible`, hence it does not violate the rule. + +```graphql +type Query { + field(type: Enum1 = FOO): [Baz!]! +} + +enum Enum1 { + FOO + BAR +} +``` + +The following example violates this rule because the default value for the field +`field` in type `Input1` references an enum value (`FOO`) that is marked as +`@inaccessible`. + +```graphql counter-example +type Query { + field(arg: Enum1 = FOO): [Baz!]! +} + +input Input1 { + field: Enum1 = FOO +} + +directive @directive1(arg: Enum1 = FOO) on FIELD_DEFINITION + +enum Enum1 { + FOO @inaccessible + BAR +} +``` + +```graphql counter-example +type Query { + field(arg: Input1 = { field2: "ERROR" }): [Baz!]! +} + +directive @directive1(arg: Input1 = { field2: "ERROR" }) on FIELD_DEFINITION + +input Input1 { + field1: String + field2: String @inaccessible +} +``` + #### Output Field Types Mergeable **Error Code** From 0a2799c621189d1e27a7c840de8949963e9a4531 Mon Sep 17 00:00:00 2001 From: Glen Date: Tue, 17 Dec 2024 14:45:50 +0200 Subject: [PATCH 05/14] Fixed formatting of Appendix A (#63) --- spec/Appendix A -- Field Selection.md | 119 +++++++++++++++----------- 1 file changed, 70 insertions(+), 49 deletions(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index e001a7b..3b8993b 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -339,20 +339,30 @@ Is equivalent to the {Name} defined in the ### Path -Path :: - < TypeName > . PathSegment - PathSegment +Path :: -PathSegment :: - FieldName - FieldName . PathSegment - FieldName < TypeName > . -PathSegment +- < TypeName > . PathSegment +- PathSegment -FieldName :: - Name +PathSegment :: -TypeName :: - Name +- FieldName +- FieldName . PathSegment +- FieldName < TypeName > . PathSegment + +FieldName :: + +- Name + +TypeName :: + +- Name The {Path} literal is a string used to select a single output value from the _return type_ by specifying a path to that value. This path is defined as a sequence of field names, each separated by a period (`.`) to create segments. -```example +```graphql example book.title ``` @@ -367,14 +377,18 @@ after the field name if the field is not defined on an interface. In the following example, the path `mediaById.isbn` specifies that `mediaById` returns a `Book`, and the `isbn` field is selected from that `Book`. -```example +```graphql example mediaById.isbn ``` ### SelectedValue -SelectedValue :: - Path - SelectedObjectValue - Path . SelectedObjectValue - -SelectedValue | SelectedValue +SelectedValue :: + +- Path +- SelectedObjectValue +- Path . SelectedObjectValue +- SelectedValue | SelectedValue A {SelectedValue} is defined as either a {Path} or a {SelectedObjectValue} @@ -386,7 +400,7 @@ paths based on type, a {Path} can include multiple paths separated by a pipe In the following example, the value could be `title` when referring to a `Book` and `movieTitle` when referring to a `Movie`. -```example +```graphql example mediaById.title | mediaById.movieTitle ``` @@ -394,19 +408,23 @@ The `|` operator can be used to match multiple possible {SelectedValue}. This operator is applied when mapping an abstract output type to a `@oneOf` input type. -```example +```graphql example { movieId: .id } | { productId: .id } ``` -```example +```graphql example { nested: { movieId: .id } | { productId: .id }} ``` ### SelectedObjectValue -SelectedObjectValue :: - { SelectedObjectField+ } +SelectedObjectValue :: -SelectedObjectField :: - Name: SelectedValue +- { SelectedObjectField+ } + +SelectedObjectField :: + +- Name: SelectedValue {SelectedObjectValue} are unordered lists of keyed input values wrapped in curly-braces `{}`. It has to be used when the expected input type is an object @@ -450,7 +468,9 @@ type Product { ### SelectedListValue -SelectedListValue :: - [ SelectedValue ] +SelectedListValue :: + +- [ SelectedValue ] A {SelectedListValue} is an ordered list of {SelectedValue} wrapped in square brackets `[]`. It is used to express semantic equivalence between an argument @@ -628,9 +648,9 @@ type context. **Formal Specification** - For each {segment} in the {Path}: -- If the {segment} is a field - - Let {fieldName} be the field name in the current {segment}. - - {fieldName} must be defined on the current type in scope. + - If the {segment} is a field + - Let {fieldName} be the field name in the current {segment}. + - {fieldName} must be defined on the current type in scope. **Explanatory Text** @@ -669,11 +689,11 @@ selected field is a leaf node. **Formal Specification** - For each {segment} in the {Path}: -- Let {selectedType} be the unwrapped type of the current {segment}. -- If {selectedType} is a scalar or enum: - - There must not be any further segments in {Path}. -- If {selectedType} is an object, interface, or union: - - There must be another segment in {Path}. + - Let {selectedType} be the unwrapped type of the current {segment}. + - If {selectedType} is a scalar or enum: + - There must not be any further segments in {Path}. + - If {selectedType} is an object, interface, or union: + - There must be another segment in {Path}. **Explanatory Text** @@ -716,12 +736,12 @@ the current context. **Formal Specification** - For each {segment} in a {Path}: -- If {segment} is a type reference: - - Let {type} be the type referenced in the {segment}. - - Let {parentType} be the type of the parent of the {segment}. - - Let {applicableTypes} be the intersection of {GetPossibleTypes(type)} and - {GetPossibleTypes(parentType)}. - - {applicableTypes} must not be empty. + - If {segment} is a type reference: + - Let {type} be the type referenced in the {segment}. + - Let {parentType} be the type of the parent of the {segment}. + - Let {applicableTypes} be the intersection of {GetPossibleTypes(type)} and + {GetPossibleTypes(parentType)}. + - {applicableTypes} must not be empty. GetPossibleTypes(type): @@ -740,8 +760,8 @@ logically apply within the parent type. **Formal Specification** - For each SelectedValue {value}: -- Let {type} be the type expected in the position {value} is found. -- {value} must be coercible to {type}. + - Let {type} be the type expected in the position {value} is found. + - {value} must be coercible to {type}. **Explanatory Text** @@ -780,10 +800,10 @@ type Store { **Formal Specification** - For each Selected Object Field {field} in the document: -- Let {fieldName} be the Name of {field}. -- Let {fieldDefinition} be the field definition provided by the parent selected - object type named {fieldName}. -- {fieldDefinition} must exist. + - Let {fieldName} be the Name of {field}. + - Let {fieldDefinition} be the field definition provided by the parent + selected object type named {fieldName}. + - {fieldDefinition} must exist. **Explanatory Text** @@ -822,10 +842,11 @@ type Store { **Formal Specification** - For each selected object value {selectedObject}: -- For every {field} in {selectedObject}: - - Let {name} be the Name of {field}. - - Let {fields} be all Selected Object Fields named {name} in {selectedObject}. - - {fields} must be the set containing only {field}. + - For every {field} in {selectedObject}: + - Let {name} be the Name of {field}. + - Let {fields} be all Selected Object Fields named {name} in + {selectedObject}. + - {fields} must be the set containing only {field}. **Explanatory Text** @@ -850,16 +871,16 @@ type Store { **Formal Specification** - For each Selected Object: -- Let {fields} be the fields provided by that Selected Object. -- Let {fieldDefinitions} be the set of input object field definitions of that - Selected Object. -- For each {fieldDefinition} in {fieldDefinitions}: - - Let {type} be the expected type of {fieldDefinition}. - - Let {defaultValue} be the default value of {fieldDefinition}. - - If {type} is Non-Null and {defaultValue} does not exist: - - Let {fieldName} be the name of {fieldDefinition}. - - Let {field} be the input object field in {fields} named {fieldName}. - - {field} must exist. + - Let {fields} be the fields provided by that Selected Object. + - Let {fieldDefinitions} be the set of input object field definitions of that + Selected Object. + - For each {fieldDefinition} in {fieldDefinitions}: + - Let {type} be the expected type of {fieldDefinition}. + - Let {defaultValue} be the default value of {fieldDefinition}. + - If {type} is Non-Null and {defaultValue} does not exist: + - Let {fieldName} be the name of {fieldDefinition}. + - Let {field} be the input object field in {fields} named {fieldName}. + - {field} must exist. **Explanatory Text** From e416179a7995255813a471a037922ab0a1798a9f Mon Sep 17 00:00:00 2001 From: Glen Date: Tue, 17 Dec 2024 15:01:29 +0200 Subject: [PATCH 06/14] Change `SameOutputTypeShape` to `SameTypeShape` (#64) --- 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 09e5e4b..4a18fd3 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -149,7 +149,7 @@ 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} - - {SameOutputTypeShape(typeA, typeB)} must be true. + - {SameTypeShape(typeA, typeB)} must be true. **Explanatory Text** From 5cb364f7ae807ced8e82992dab5d1cba8ceea51b Mon Sep 17 00:00:00 2001 From: Glen Date: Tue, 17 Dec 2024 15:02:29 +0200 Subject: [PATCH 07/14] Editorial Changes (#62) --- spec/Section 2 -- Source Schema.md | 22 ++++++++++++---------- spec/Section 4 -- Composition.md | 12 ++++++------ spec/Spec.md | 15 ++++++++------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/spec/Section 2 -- Source Schema.md b/spec/Section 2 -- Source Schema.md index 5bbc683..50656e1 100644 --- a/spec/Section 2 -- Source Schema.md +++ b/spec/Section 2 -- Source Schema.md @@ -261,7 +261,8 @@ lookup fields that can resolve entities by different keys. ```graphql example extend type Query { person( - by: PersonByInput @is(field: "{ id } | { addressId: address.id } { name }") + by: PersonByInput + @is(field: "{ id } | { addressId: address.id } | { name }") ): Person } @@ -298,7 +299,7 @@ type Product { } ``` -The upper example would translate to the following in the _composite schema_. +The above example would translate to the following in the _composite schema_. ```graphql example type Product { @@ -309,7 +310,7 @@ type Product { This can also be done by using input types. The selection path map specifies which data is required and needs to be resolved from other source schemas. If -the input type is only used to express a requirements it is removed from the +the input type is only used to express requirements it is removed from the composite schema. ```graphql example @@ -384,8 +385,8 @@ type Product @key(fields: "id") @key(fields: "key") { ``` While multiple keys define separate ways to reference the same entity based on -different sets of fields, a composite key allows to uniquely identify an entity -by using a combination of multiple fields. +different sets of fields, a composite key allows for uniquely identifying an +entity by using a combination of multiple fields. ```graphql example type Product @key(fields: "id sku") { @@ -416,8 +417,8 @@ field to an object type. This prevents source schemas from inadvertently defining similarly named fields that are semantically not the same. Fields have to be explicitly marked as `@shareable` to allow multiple source -schemas to define it, and ensures that the step of allowing a field to be served -from multiple source schemas is an explicit, coordinated decision. +schemas to define it, and this ensures that the step of allowing a field to be +served from multiple source schemas is an explicit, coordinated decision. If multiple source schemas define the same field, these are assumed to be semantically equivalent, and the executor is free to choose between them as it @@ -433,7 +434,8 @@ directive @provides(fields: SelectionSet!) on FIELD_DEFINITION The `@provides` directive is an optimization hint specifying child fields that can be resolved locally at the given source schema through a particular query -path. This allows for a variation of overlapping field to improve data fetching. +path. This allows for a variation of overlapping fields to improve data +fetching. **Arguments:** @@ -446,7 +448,7 @@ directive @external on OBJECT_DEFINITION | INTERFACE_DEFINITION | FIELD_DEFINITI ``` The `@external` directive is used in combination with the `@provides` directive -and specifies data that is not owned ba a particular source schema. +and specifies data that is not owned by a particular source schema. ### @override @@ -454,5 +456,5 @@ and specifies data that is not owned ba a particular source schema. directive @override(from: String!) on FIELD_DEFINITION ``` -The `@override` directive allows to migrate fields from one source schema to +The `@override` directive allows for migrating fields from one source schema to another. diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 4a18fd3..f9e4cdf 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -4,11 +4,11 @@ The schema composition describes the process of merging multiple source schemas into a single GraphQL schema, known as the _composite execution schema_, which is a valid GraphQL schema annotated with execution directives. This composite execution schema is the output of the schema composition process. The schema -composition process is divided into four major algorithms: **Validate Source -Schema**, **Merge Source Schema**, and **Validate Satisfiability**, which are +composition process is divided into three main steps: **Validate Source +Schemas**, **Merge Source Schemas**, and **Validate Satisfiability**, which are run in sequence to produce the composite execution schema. -## Validate Source Schema +## Validate Source Schemas ## Merge Source Schemas @@ -126,7 +126,7 @@ input Input1 { **Error Code** -OUTPUT_FIELD_TYPES_NOT_MERGEABLE +`OUTPUT_FIELD_TYPES_NOT_MERGEABLE` **Severity** @@ -253,8 +253,8 @@ ERROR This rule ensures that certain essential elements of a GraphQL schema, particularly built-in scalars, directives and introspection types, cannot be -marked as `@inaccessible`. These types are fundamental to GraphQL's. Making -these elements inaccessible would break core GraphQL functionalities. +marked as `@inaccessible`. These types are fundamental to GraphQL. Making these +elements inaccessible would break core GraphQL functionality. Here, the `String` type is not marked as `@inaccessible`, which adheres to the rule: diff --git a/spec/Spec.md b/spec/Spec.md index d138d78..146d4d4 100644 --- a/spec/Spec.md +++ b/spec/Spec.md @@ -2,7 +2,7 @@ **Introduction** -The GraphQL Composite Schemas introduces a comprehensive specification for +The GraphQL Composite Schemas Spec introduces a comprehensive specification for creating distributed GraphQL systems that seamlessly merges multiple GraphQL schemas. This specification describes the process of composing a federated GraphQL schema and outlines algorithms for executing GraphQL queries on the @@ -41,9 +41,10 @@ POSSIBILITY OF SUCH DAMAGE. **Conformance** -A conforming implementation of GraphQL over HTTP must fulfill all normative -requirements. Conformance requirements are described in this document via both -descriptive assertions and key words with clearly defined meanings. +A conforming implementation of the GraphQL Composite Schemas Spec must fulfill +all normative requirements. Conformance requirements are described in this +document via both descriptive assertions and key words with clearly defined +meanings. The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in the normative portions of @@ -52,9 +53,9 @@ this document are to be interpreted as described in in lowercase and still retain their meaning unless explicitly declared as non-normative. -A conforming implementation of GraphQL over HTTP may provide additional -functionality, but must not where explicitly disallowed or would otherwise -result in non-conformance. +A conforming implementation of the GraphQL Composite Schemas Spec may provide +additional functionality, but must not where explicitly disallowed or would +otherwise result in non-conformance. **Non-Normative Portions** From bf0ae3b4e81d84581790ee584f427173c5093be4 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 27 Dec 2024 10:35:44 +0100 Subject: [PATCH 08/14] Added provides spec text (#81) --- spec/Section 2 -- Source Schema.md | 119 +++++++++++++++++++++++++++-- 1 file changed, 114 insertions(+), 5 deletions(-) diff --git a/spec/Section 2 -- Source Schema.md b/spec/Section 2 -- Source Schema.md index 50656e1..000f608 100644 --- a/spec/Section 2 -- Source Schema.md +++ b/spec/Section 2 -- Source Schema.md @@ -432,14 +432,123 @@ Note: Key fields are always considered sharable. directive @provides(fields: SelectionSet!) on FIELD_DEFINITION ``` -The `@provides` directive is an optimization hint specifying child fields that -can be resolved locally at the given source schema through a particular query -path. This allows for a variation of overlapping fields to improve data -fetching. +The `@provides` directive indicates that a field can provide certain subfields +of its return type from the same source schema, without requiring an additional +resolution step elsewhere. + +```graphql example +type Review { + id: ID! + body: String! + author: User @provides(fields: "email") +} + +type User @key(fields: "id") { + id: ID! + email: String! @external + name: String! +} + +type Query { + reviews: [Review!] + users: [User!] +} +``` + +When a field annotated with `@provides` returns an object, interface or union +type that may also be contributed by other source schemas, this directive +declares which of that type’s subfields the current source schema can resolve +directly. + +```graphql example +{ + reviews { + body + user { + name + email + } + } +} +``` + +If a client tries to fetch the same subfield (`User.email`) through a different +path (e.g., users query field), the source schema will not be able to resolve it +and will throw an error. + +```graphql counter-example +{ + users { + # The source schema does NOT provide email in this context, + # and this field will fail at runtime. + email + } +} +``` + +The `@provides` directive may reference multiple fields or nested fields: + +```graphql example +type Review { + id: ID! + product: Product @provides(fields: "sku variation { size }") +} + +type Product @key(fields: "sku variation { id }") { + sku: String! @external + variation: ProductVariation! + name: String! +} + +type ProductVariation { + id: String! + size: String! @external +} +``` + +When a field annotated with the provides directive has an abstract return type +the fields syntax can leverage inline fragments to express fields that can be +resolved locally. + +```graphql example +type Review { + id: ID! + # The @provides directive tells us that this source schema can supply different + # fields depending on which concrete type of Product is returned. + product: Product + @provides( + fields: """ + ... on Book { author } + ... on Clothing { size } + """ + ) +} + +interface Product @key(fields: "id") { + id: ID! +} + +type Book implements Product { + id: ID! + title: String! + author: String! @external +} + +type Clothing implements Product { + id: ID! + name: String! + size: String! @external +} + +type Query { + reviews: [Review!]! +} +``` **Arguments:** -- `fields`: Represents a selection set syntax. +- `fields`: Represents a selection set syntax describing the subfields of the + returned type that can be provided by the current source schema. ### @external From 1f90a5dfbc7721a64a184719a5e983742294a35b Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 27 Dec 2024 11:14:10 +0100 Subject: [PATCH 09/14] Added `@external` directive spec text (#82) --- spec/Section 2 -- Source Schema.md | 49 ++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/spec/Section 2 -- Source Schema.md b/spec/Section 2 -- Source Schema.md index 000f608..0a147ce 100644 --- a/spec/Section 2 -- Source Schema.md +++ b/spec/Section 2 -- Source Schema.md @@ -553,11 +553,54 @@ type Query { ### @external ```graphql -directive @external on OBJECT_DEFINITION | INTERFACE_DEFINITION | FIELD_DEFINITION +directive @external on FIELD_DEFINITION ``` -The `@external` directive is used in combination with the `@provides` directive -and specifies data that is not owned by a particular source schema. +The @external directive indicates that a field is recognized by the current +source schema but is not directly contributed (resolved) by it. Instead, this +schema references the field for specific composition purposes. + +**Entity Keys** + +When combined with one or more `@key` directives, an external field can serve as +an entity identifier (or part of a composite identifier). + +```graphql example +type Query { + productBySku(sku: String!): Product @lookup + productByUpc(upc: String!): Product @lookup +} + +type Product @key(fields: "sku") @key(fields: "upc") { + sku: String! @external + upc: String! @external + name: String +} +``` + +**Field Resolution** + +When another field in the same source schema uses `@provides` to declare that it +can resolve certain external fields in a single data-fetching step. + +```graphql example +type Review { + id: ID! + text: String + author: User @provides(fields: "email") +} + +extend type User { + id: ID! + email: String! @external +} +``` + +When a field is marked `@external`, the composition process understands that the +field is provided by another source schema. The current source schema references +it only for entity identification (via `@key`) or for providing a field through +`@provides`. If no such usage exists, the presence of an `@external` field +produces a composition error. ### @override From 1efa3c9062e4c8512c12213741f8b7e113603a99 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 27 Dec 2024 11:14:40 +0100 Subject: [PATCH 10/14] Fixed provides example query (#83) --- spec/Section 2 -- Source Schema.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Section 2 -- Source Schema.md b/spec/Section 2 -- Source Schema.md index 0a147ce..6569d43 100644 --- a/spec/Section 2 -- Source Schema.md +++ b/spec/Section 2 -- Source Schema.md @@ -464,7 +464,7 @@ directly. { reviews { body - user { + author { name email } From 878444fbbee713ce262dcf57bc3d6597f4045615 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 27 Dec 2024 12:15:59 +0200 Subject: [PATCH 11/14] Change "Subgraph" to "Source schema" in Schema Composition document (#77) --- spec/Section 4 -- Composition.md | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index f9e4cdf..d49a6a4 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -340,12 +340,12 @@ as `@external` in another. The argument `language` has the same default value in both source schemas, satisfying the rule: ```graphql example -# Subgraph A +# Source schema A type Product { name(language: String = "en"): String } -# Subgraph B +# Source schema B type Product { name(language: String = "en") @external: String } @@ -356,12 +356,12 @@ as `@external` in another. The argument `language` has different default values in the two source schemas, violating the rule: ```graphql counter-example -# Subgraph A +# Source schema A type Product { name(language: String = "en"): String } -# Subgraph B +# Source schema B type Product { name(language: String = "de") @external: String } @@ -374,12 +374,12 @@ not have a default value in the source schema where the field is marked as `@external`, violating the rule: ```graphql counter-example -# Subgraph A +# Source schema A type Product { name(language: String = "en"): String } -# Subgraph B +# Source schema B type Product { name(language: String): String @external } @@ -430,12 +430,12 @@ field in source schema B and the base field in source schema A, satisfying the rule: ```graphql example -# Subgraph A +# Source schema A type Product { name(language: String): String } -# Subgraph B +# Source schema B type Product { name(language: String): String @external } @@ -446,12 +446,12 @@ argument that is present in the base field definition in source schema A, violating the rule: ```graphql counter-example -# Subgraph A +# Source schema A type Product { name(language: String): String } -# Subgraph B +# Source schema B type Product { name: String @external } @@ -502,12 +502,12 @@ Here, the `@external` field's `language` argument has the same type (`Language`) as the base field, satisfying the rule: ```graphql example -# Subgraph A +# Source schema A type Product { name(language: Language): String } -# Subgraph B +# Source schema B type Product { name(language: Language): String } @@ -518,12 +518,12 @@ the base field's `language` argument type (`Language` vs. `String`), violating the rule: ```graphql example -# Subgraph A +# Source schema A type Product { name(language: Language): String } -# Subgraph B +# Source schema B type Product { name(language: String): String } @@ -569,13 +569,13 @@ Here, the `name` field on `Product` is defined in source schema A and marked as definition in source schema A: ```graphql example -# Subgraph A +# Source schema A type Product { id: ID name: String } -# Subgraph B +# Source schema B type Product { id: ID name: String @external @@ -587,12 +587,12 @@ source schema B but has no non-`@external` declaration in any other source schema, violating the rule: ```graphql counter-example -# Subgraph A +# Source schema A type Product { id: ID } -# Subgraph B +# Source schema B type Product { id: ID name: String @external @@ -638,12 +638,12 @@ Here, the `@external` field `name` has the same return type (`String`) as the base field definition, satisfying the rule: ```graphql example -# Subgraph A +# Source schema A type Product { name: String } -# Subgraph B +# Source schema B type Product { name: String @external } @@ -653,12 +653,12 @@ In this example, the `@external` field `name` has a return type of `ProductName` that doesn't match the base field's return type `String`, violating the rule: ```graphql counter-example -# Subgraph A +# Source schema A type Product { name: String } -# Subgraph B +# Source schema B type Product { name: ProductName @external } @@ -699,7 +699,7 @@ In this example, the `name` field is marked with `@external` and is used by the `@provides` directive, satisfying the rule: ```graphql example -# Subgraph A +# Source schema A type Product { id: ID name: String @external @@ -714,7 +714,7 @@ In this example, the `name` field is marked with `@external` but is not used by the `@provides` directive, violating the rule: ```graphql counter-example -# Subgraph A +# Source schema A type Product { title: String @external author: Author From feb539c32fb05e0b75775dcc87ff0e35a2bb4d40 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 27 Dec 2024 12:16:22 +0200 Subject: [PATCH 12/14] Remove IsAccessible check on directive (#67) --- spec/Section 4 -- Composition.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index d49a6a4..76f5f14 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -245,16 +245,15 @@ ERROR - {IsAccessible(argument)} must be true. - For each {directive} in {directives}: - If {directive} is a built-in directive: - - {IsAccessible(directive)} must be true. - For each {argument} in {directive}: - {IsAccessible(argument)} must be true. **Explanatory Text** This rule ensures that certain essential elements of a GraphQL schema, -particularly built-in scalars, directives and introspection types, cannot be -marked as `@inaccessible`. These types are fundamental to GraphQL. Making these -elements inaccessible would break core GraphQL functionality. +particularly built-in scalars, directive arguments, and introspection types, +cannot be marked as `@inaccessible`. These types are fundamental to GraphQL. +Making these elements inaccessible would break core GraphQL functionality. Here, the `String` type is not marked as `@inaccessible`, which adheres to the rule: From c4e1c26bd03f39e2a3b83de3278f813056d6d2d7 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 27 Dec 2024 12:16:35 +0200 Subject: [PATCH 13/14] Remove unused variable `{type}` in `ValidateInputFieldDefaultValues` (#71) --- spec/Section 4 -- Composition.md | 1 - spec/temp.md | 1 - 2 files changed, 2 deletions(-) diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 76f5f14..f259a7f 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -40,7 +40,6 @@ ValidateInputFieldDefaultValues(): - Let {inputFields} be all input fields across all source schemas - For each {inputField} in {inputFields}: - - Let {type} be the type of {inputField} - If {IsExposed(inputField)} is true and {inputField} has a default value: - Let {defaultValue} be the default value of {inputField} - If {ValidateDefaultValue(defaultValue)} is false diff --git a/spec/temp.md b/spec/temp.md index 9220f23..676272b 100644 --- a/spec/temp.md +++ b/spec/temp.md @@ -1224,7 +1224,6 @@ ValidateInputFieldDefaultValues(): - Let {inputFields} be all input fields of across all subgraphs - For each {inputField} in {inputFields}: - - Let {type} be the type of {inputField} - If {IsExposed(inputField)} is true and {inputField} has a default value: - Let {defaultValue} be the default value of {inputField} - if ValidateDefaultValue(defaultValue) is false From a5ee88f9977b5aeeafa57de3ed1d28c1fe3ab61b Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 27 Dec 2024 15:26:25 +0100 Subject: [PATCH 14/14] Added override directive spec text. (#84) --- spec/Section 2 -- Source Schema.md | 75 +++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/spec/Section 2 -- Source Schema.md b/spec/Section 2 -- Source Schema.md index 6569d43..d63133f 100644 --- a/spec/Section 2 -- Source Schema.md +++ b/spec/Section 2 -- Source Schema.md @@ -608,5 +608,76 @@ produces a composition error. directive @override(from: String!) on FIELD_DEFINITION ``` -The `@override` directive allows for migrating fields from one source schema to -another. +The `@override` directive is used to migrate a field from one source schema to +another. When a field in the local schema is annotated with +`@override(from: "Catalog")`, it signals that the local schema overrides the +field previously contributed by the `Catalog` source schema. As a result, the +composite schema will source this field from the local schema, rather than from +the original source schema. + +The following example shows how a field can be migrated from the `Catalog` +schema to the new `Payments` schema. By using `@override`, a field can be moved +to a new schema without requiring any change to the original `Catalog` schema. + +```graphql example +# The original "Catalog" schema: +type Product @key(fields: "id") { + id: ID! + name: String! + price: Float! +} + +# The new "Payments" schema: +extend type Product @key(fields: "id") { + id: ID! @external + price: Float! @override(from: "Catalog") + tax: Float! +} +``` + +Fields that are annotated can themselves be migrated. + +```graphql example +# The original "Catalog" schema: +type Product @key(fields: "id") { + id: ID! + name: String! + price: Float! +} + +# The new "Payments" schema: +extend type Product @key(fields: "id") { + id: ID! @external + price: Float! @override(from: "Catalog") + tax: Float! +} + +# The new "Pricing" schema: +extend type Product @key(fields: "id") { + id: ID! @external + price: Float! @override(from: "Payments") + tax: Float! +} +``` + +If the composition detects cyclic overrides it must throw a composition error. + +```graphql example +# The original "Catalog" schema: +type Product @key(fields: "id") { + id: ID! + name: String! + price: Float! @override(from: "Pricing") +} + +# The new "Payments" schema: +extend type Product @key(fields: "id") { + id: ID! @external + price: Float! @override(from: "Catalog") + tax: Float! +} +``` + +**Arguments:** + +- `from`: The name of the source schema that originally provided this field.