From e4f8774dff69330a0ca53e9a22e9041e41839b75 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Thu, 14 Nov 2024 00:22:17 +0100 Subject: [PATCH 1/5] Adds federation error code coerage docs --- rfcs/apollo-federation-error-codes.md | 136 ++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 rfcs/apollo-federation-error-codes.md diff --git a/rfcs/apollo-federation-error-codes.md b/rfcs/apollo-federation-error-codes.md new file mode 100644 index 0000000..3b09cb1 --- /dev/null +++ b/rfcs/apollo-federation-error-codes.md @@ -0,0 +1,136 @@ +# Validation Rule from Federation + +## Implemented + +| Status | Error | Description | +| ------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ✅ | DISALLOWED_INACCESSIBLE | An element is marked as @inaccessible that is not allowed to be @inaccessible. | +| ✅ | EMPTY_MERGED_ENUM_TYPE | An enum type has no value common to all the subgraphs that define the type. Merging that type would result in an invalid empty enum type. | +| ✅ | EMPTY_MERGED_INPUT_TYPE | An input object type has no field common to all the subgraphs that define the type. Merging that type would result in an invalid empty input object type. | +| ✅ | EXTERNAL_ARGUMENT_DEFAULT_MISMATCH | An @external field declares an argument with a default that is incompatible with the corresponding argument in the declaration(s) of that field in other subgraphs. | +| ✅ | EXTERNAL_ARGUMENT_MISSING | An @external field is missing some arguments present in the declaration(s) of that field in other subgraphs. | +| ✅ | EXTERNAL_ARGUMENT_TYPE_MISMATCH | An @external field declares an argument with a type that is incompatible with the corresponding argument in the declaration(s) of that field in other subgraphs. | +| ✅ | EXTERNAL_MISSING_ON_BASE | A field is marked as @external in a subgraph but with no non-external declaration in any other subgraph. | +| ✅ | EXTERNAL_TYPE_MISMATCH | An @external field has a type that is incompatible with the declaration(s) of that field in other subgraphs. | +| ✅ | EXTERNAL_UNUSED | An @external field is not being used by any instance of @key, @requires, @provides or to satisfy an interface implementation. | +| ✅ | FIELD_ARGUMENT_DEFAULT_MISMATCH | An argument (of a field/directive) has a default value that is incompatible with that of other declarations of that same argument in other subgraphs. | +| ✅ | FIELD_ARGUMENT_TYPE_MISMATCH | An argument (of a field/directive) has a type that is incompatible with that of other declarations of that same argument in other subgraphs. | +| ✅ | FIELD_TYPE_MISMATCH | A field has a type that is incompatible with other declarations of that field in other subgraphs. | +| ✅ | INPUT_FIELD_DEFAULT_MISMATCH | An input field has a default value that is incompatible with other declarations of that field in other subgraphs. | + +## Covered by other rule + +| Status | Error | Description | +| ------ | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ◉ | DEFAULT_VALUE_USES_INACCESSIBLE | An element is marked as @inaccessible but is used in the default value of an element visible in the API schema. | +| ◉ | ENUM_VALUE_MISMATCH | An input object type has no field common to all the subgraphs that define the type. Merging that type would result in an invalid empty input object type. | +| ◉ | REFERENCE_INACCESSIBLE | An element is marked as @inaccessible but is referenced by an element visible in the API schema. | +| ◉ | REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH | An argument of a field or directive definition is mandatory in some subgraphs, but the argument is not defined in all the subgraphs that define the field or directive definition. | +| ◉ | REQUIRED_INACCESSIBLE | An element is marked as @inaccessible but is required by an element visible in the API schema. | +| ◉ | REQUIRED_INPUT_FIELD_MISSING_IN_SOME_SUBGRAPH | A field of an input object type is mandatory in some subgraphs, but the field is not defined in all the subgraphs that define the input object type. | + +## Missing + +| Status | Error | Description | +| ------ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 📋 | IMPLEMENTED_BY_INACCESSIBLE | An element is marked as @inaccessible but implements an element visible in the API schema. | +| 📋 | INTERFACE_FIELD_NO_IMPLEM | After subgraph merging, an implementation is missing a field of one of the interfaces it implements (which can happen for valid subgraphs). | +| 📋 | INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE | A subgraph has a @key on an interface type, but that subgraph does not define an implementation (in the supergraph) of that interface. | +| 📋 | INTERFACE_KEY_NOT_ON_IMPLEMENTATION | A @key is defined on an interface type, but is not defined (or is not resolvable) on at least one of the interface implementations. | +| 📋 | INVALID_FIELD_SHARING | A field that is non-shareable in at least one subgraph is resolved by multiple subgraphs. | +| 📋 | INVALID_SHAREABLE_USAGE | The @shareable Federation directive is used in an invalid way. | +| 📋 | KEY_DIRECTIVE_IN_FIELDS_ARG | The fields argument of a @key directive includes some directive applications. This is not supported. | +| 📋 | KEY_FIELDS_HAS_ARGS | The fields argument of a @key directive includes a field defined with arguments (which is not currently supported). | +| 📋 | KEY_FIELDS_SELECT_INVALID_TYPE | The fields argument of @key directive includes a field whose type is a list, interface, or union type. Fields of these types cannot be part of a @key. | +| 📋 | KEY_INVALID_FIELDS | The fields argument of a @key directive is invalid (it has invalid syntax, includes unknown fields, ...). | +| 📋 | NO_QUERIES | None of the composed subgraphs expose any query. | +| 📋 | ONLY_INACCESSIBLE_CHILDREN | A type visible in the API schema has only @inaccessible children. | +| 📋 | PROVIDES_DIRECTIVE_IN_FIELDS_ARG | The fields argument of a @provides directive includes some directive applications. This is not supported. | +| 📋 | PROVIDES_FIELDS_HAS_ARGS | The fields argument of a @provides directive includes a field defined with arguments (which is not currently supported). | +| 📋 | PROVIDES_FIELDS_MISSING_EXTERNAL | The fields argument of a @provides directive includes a field that is not marked as @external. | +| 📋 | QUERY_ROOT_TYPE_INACCESSIBLE | An element is marked as @inaccessible but is the query root type, which must be visible in the API schema. | +| 📋 | REQUIRES_DIRECTIVE_IN_FIELDS_ARG | The fields argument of a @requires directive includes some directive applications. This is not supported. | +| 📋 | REQUIRES_INVALID_FIELDS_TYPE | The value passed to the fields argument of a @requires directive is not a string. | +| 📋 | REQUIRES_INVALID_FIELDS | The fields argument of a @requires directive is invalid (it has invalid syntax, includes unknown fields, ...). | +| 📋 | ROOT_MUTATION_USED | A subgraph's schema defines a type with the name mutation, while also specifying a different type name as the root query object. This is not allowed. | +| 📋 | ROOT_QUERY_USED | A subgraph's schema defines a type with the name query, while also specifying a different type name as the root query object. This is not allowed. | +| 📋 | ROOT_SUBSCRIPTION_USED | A subgraph's schema defines a type with the name subscription, while also specifying a different type name as the root query object. This is not allowed. | +| 📋 | SATISFIABILITY_ERROR | Subgraphs can be merged, but the resulting supergraph API would have queries that cannot be satisfied by those subgraphs. | +| 📋 | SHAREABLE_HAS_MISMATCHED_RUNTIME_TYPES | A shareable field return type has mismatched possible runtime types in the subgraphs in which the field is declared. As shared fields must resolve the same way in all subgraphs, this is almost surely a mistake. | +| 📋 | TYPE_DEFINITION_INVALID | A built-in or Federation type has an invalid definition in the schema. | +| 📋 | TYPE_KIND_MISMATCH | A type has the same name in different subgraphs, but a different kind. For instance, one definition is an object type but another is an interface.Replaces VALUE_TYPE_KIND_MISMATCH, EXTENSION_OF_WRONG_KIND, ENUM_MISMATCH_TYPE. | +| 📋 | PROVIDES_INVALID_FIELDS | The fields argument of a @provides directive is invalid (it has invalid syntax, includes unknown fields, ...). | +| 📋 | INVALID_GRAPHQL | A schema is invalid GraphQL: it violates one of the rules of the specification. | + +## Later + +| Status | Error | Description | +| ------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| ⏭️ | DIRECTIVE_COMPOSITION_ERROR | Error when composing custom directives. | +| ⏭️ | DIRECTIVE_DEFINITION_INVALID | A built-in or Federation directive has an invalid definition in the schema. | +| ⏭️ | DOWNSTREAM_SERVICE_ERROR | Indicates an error in a subgraph service query during query execution in a | +| ⏭️ | EXTENSION_WITH_NO_BASE | A subgraph is attempting to extend a type that is not originally defined in any known subgraph. | +| ⏭️ | KEY_UNSUPPORTED_ON_INTERFACE | A @key directive is used on an interface, which is only supported when @linking to Federation v2.3 or later. | +| ⏭️ | OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE | The @override directive cannot be used on external fields, nor to override fields with either @external, @provides, or @requires. | +| ⏭️ | OVERRIDE_FROM_SELF_ERROR | Field with @override directive has "from" location that references its own subgraph. | +| ⏭️ | OVERRIDE_LABEL_INVALID | The @override directive label argument must match the pattern `/^[a-zA-Z]a-zA-Z0-9\*-:./]\*$/ or /^percent((d{1,2}(.d{1,8})I 100))$/` | +| ⏭️ | OVERRIDE_ON_INTERFACE | The @override directive cannot be used on the fields of an interface type. | +| ⏭️ | OVERRIDE_SOURCE_HAS_OVERRIDE | Field which is overridden to another subgraph is also marked @override. | + +## Open Questions + +| Status | Error | Description | +| ------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| ❓ | EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE | The @external directive collides with other directives in some situations. | +| ❓ | INTERFACE_OBJECT_USAGE_ERROR | Error in the usage of the @interfaceObject directive. | +| ❓ | INVALID_FEDERATION_SUPERGRAPH | Indicates that a schema provided for an Apollo Federation supergraph is not a valid supergraph schema. | +| ❓ | KEY_INVALID_FIELDS_TYPE | The value passed to the fields argument of a @key directive is not a string. | +| ❓ | MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL | In a subgraph, a field is both marked @external and has a merged directive applied to it. | +| ❓ | PROVIDES_INVALID_FIELDS_TYPE | The value passed to the fields argument of a @provides directive is not a string. | +| ❓ | PROVIDES_ON_NON_OBJECT_FIELD | A @provides directive is used to mark a field whose base type is not an object type. | +| ❓ | PROVIDES_UNSUPPORTED_ON_INTERFACE | A @provides directive is used on an interface, which is not (yet) supported. | + +## Not needed in composite schemas + +| Status | Error | Description | +| ------ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 🔴 | EXTERNAL_ON_INTERFACE | The field of an interface type is marked with @external: as external is about marking field not resolved by the subgraph and as interface field are not resolved (only implementations of those fields are), an "external" interface field is nonsensical | +| 🔴 | INVALID_LINK_DIRECTIVE_USAGE | An application of the @link directive is invalid/does not respect the specification. | +| 🔴 | INVALID_LINK_IDENTIFIER | A URL/version for a @link feature is invalid/does not respect the specification. | +| 🔴 | INVALID_SUBGRAPH_NAME | A subgraph name is invalid. (Subgraph names cannot be a single underscore (\*)). | +| 🔴 | LINK_IMPORT_NAME_MISMATCH | The import name for a merged directive (as declared by the relevant @link(import:) argument) is inconsistent between subgraphs. | +| 🔴 | REQUIRES_FIELDS_MISSING_EXTERNAL | The fields argument of a @requires directive includes a field that is not marked as @external. | +| 🔴 | REQUIRES_UNSUPPORTED_ON_INTERFACE | A @requires directive is used on an interface, which is not (yet) supported. | +| 🔴 | TYPE_WITH_ONLY_UNUSED_EXTERNAL | A Federation 1 schema has a composite type comprised only of unused external fields. Note that this error can only be raised for Federation 1 schema as Federation 2 schema do not allow unused external fields (and errors with code EXTERNAL_UNUSED will be raised in that case). But when Federation 1 schema are automatically migrated to Federation 2 ones, unused external fields are automatically removed, and in rare case this can leave a type empty. If that happens, an error with this code will be raised. | +| 🔴 | UNKNOWN_FEDERATION_LINK_VERSION | The version of Federation in a @link directive on the schema is unknown. | +| 🔴 | UNKNOWN_LINK_VERSION | The version of @link set on the schema is unknown. | +| 🔴 | UNSUPPORTED_FEATURE | Indicates an error due to feature currently unsupported by Federation. | +| 🔴 | UNSUPPORTED_LINKED_FEATURE | Indicates that a feature used in a @link is either unsupported or is used with unsupported options. | + +# Removed codes ( in Apollo Federation) + +The following error codes have been removed and are no longer generated by the +most recent version of the @apollo/gateway library: + +| Status | Error | Description | +| ------ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 🔴 | DUPLICATE_ENUM_DEFINITION | As duplicate enum definitions is invalid GraphQL, this will now be an error with code INVALID_GRAPHQL. | +| 🔴 | DUPLICATE_ENUM_VALUE | As duplicate enum values are invalid in GraphQL, this will now be an error with code INVALID_GRAPHQL. | +| 🔴 | DUPLICATE_SCALAR_DEFINITION | As duplicate scalar definitions are invalid in GraphQL, this will now be an error with code INVALID_GRAPHQL. | +| 🔴 | ENUM_MISMATCH | Subgraph definitions for an enum are now merged by composition. | +| 🔴 | EXTERNAL_USED_ON_BASE | As there is no type ownership anymore, there is also no particular limitation as to where a field can be external. | +| 🔴 | INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH | This error was thrown by a validation introduced to avoid running into a known runtime bug. Since Federation v2.3, the underlying runtime bug has been addressed and the validation/limitation was no longer necessary and has been removed. | +| 🔴 | KEY_FIELDS_MISSING_EXTERNAL | Using @external for key fields is now discouraged, unless the field is truly meant to be external. | +| 🔴 | KEY_FIELDS_MISSING_ON_BASE | Keys can now use any field from any other subgraph. | +| 🔴 | KEY_MISSING_ON_BASE | Each subgraph is now free to declare a key only if it needs it. | +| 🔴 | KEY_NOT_SPECIFIED | Each subgraph can declare a key independently of any other subgraph. | +| 🔴 | MULTIPLE_KEYS_ON_EXTENSION | Every subgraph can have multiple keys, as necessary. | +| 🔴 | NON_REPEATABLE_DIRECTIVE_ARGUMENTS_MISMATCH | Since Federation v2.1.0, the case this error used to cover is now a warning (with code INCONSISTENT_NON_REPEATABLE_DIRECTIVE_ARGUMENTS) instead of an error. | +| 🔴 | PROVIDES_FIELDS_SELECT_INVALID_TYPE | @provides can now be used on fields of interface, union, and list types. | +| 🔴 | PROVIDES_NOT_ON_ENTITY | @provides can now be used on any type. | +| 🔴 | REQUIRES_FIELDS_HAS_ARGS | Since Federation v2.1.1, using fields with arguments in a @requires is fully supported. | +| 🔴 | REQUIRES_FIELDS_MISSING_ON_BASE | Fields in @requires can now be from any subgraph. | +| 🔴 | REQUIRES_USED_ON_BASE | As there is no type ownership anymore, there is also no particular limitation as to which subgraph can use a @requires. | +| 🔴 | RESERVED_FIELD_USED | This error was previously not correctly enforced: the service and entities, if present, were overridden; this is still the case. | +| 🔴 | VALUE_TYPE_NO_ENTITY | There is no strong difference between entity and value types in the model (they are just usage patterns), and a type can have keys in one subgraph but not another. | +| 🔴 | VALUE_TYPE_UNION_TYPES_MISMATCH | Subgraph definitions for a union are now merged by composition. | From 5f9ee4da85b904f987abbdbf2a00caa450227dba Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Tue, 17 Dec 2024 13:59:00 +0100 Subject: [PATCH 2/5] feat: add more rules --- rfcs/apollo-federation-error-codes.md | 19 +- spec/Section 4 -- Composition.md | 773 ++++++++++++++++++++++++++ 2 files changed, 782 insertions(+), 10 deletions(-) diff --git a/rfcs/apollo-federation-error-codes.md b/rfcs/apollo-federation-error-codes.md index 3b09cb1..0a67bb3 100644 --- a/rfcs/apollo-federation-error-codes.md +++ b/rfcs/apollo-federation-error-codes.md @@ -17,6 +17,15 @@ | ✅ | FIELD_ARGUMENT_TYPE_MISMATCH | An argument (of a field/directive) has a type that is incompatible with that of other declarations of that same argument in other subgraphs. | | ✅ | FIELD_TYPE_MISMATCH | A field has a type that is incompatible with other declarations of that field in other subgraphs. | | ✅ | INPUT_FIELD_DEFAULT_MISMATCH | An input field has a default value that is incompatible with other declarations of that field in other subgraphs. | +| ✅ | NO_QUERIES | None of the composed subgraphs expose any query. | +| ✅ | ROOT_MUTATION_USED | A subgraph's schema defines a type with the name mutation, while also specifying a different type name as the root query object. This is not allowed. | +| ✅ | ROOT_QUERY_USED | A subgraph's schema defines a type with the name query, while also specifying a different type name as the root query object. This is not allowed. | +| ✅ | ROOT_SUBSCRIPTION_USED | A subgraph's schema defines a type with the name subscription, while also specifying a different type name as the root query object. This is not allowed. | +| ✅ | KEY_DIRECTIVE_IN_FIELDS_ARG | The fields argument of a @key directive includes some directive applications. This is not supported. | +| ✅ | KEY_FIELDS_HAS_ARGS | The fields argument of a @key directive includes a field defined with arguments (which is not currently supported). | +| ✅ | KEY_FIELDS_SELECT_INVALID_TYPE | The fields argument of @key directive includes a field whose type is a list, interface, or union type. Fields of these types cannot be part of a @key. | +| ✅ | KEY_INVALID_FIELDS | The fields argument of a @key directive is invalid (it has invalid syntax, includes unknown fields, ...). | +| ✅ | PROVIDES_DIRECTIVE_IN_FIELDS_ARG | The fields argument of a @provides directive includes some directive applications. This is not supported. | ## Covered by other rule @@ -39,22 +48,12 @@ | 📋 | INTERFACE_KEY_NOT_ON_IMPLEMENTATION | A @key is defined on an interface type, but is not defined (or is not resolvable) on at least one of the interface implementations. | | 📋 | INVALID_FIELD_SHARING | A field that is non-shareable in at least one subgraph is resolved by multiple subgraphs. | | 📋 | INVALID_SHAREABLE_USAGE | The @shareable Federation directive is used in an invalid way. | -| 📋 | KEY_DIRECTIVE_IN_FIELDS_ARG | The fields argument of a @key directive includes some directive applications. This is not supported. | -| 📋 | KEY_FIELDS_HAS_ARGS | The fields argument of a @key directive includes a field defined with arguments (which is not currently supported). | -| 📋 | KEY_FIELDS_SELECT_INVALID_TYPE | The fields argument of @key directive includes a field whose type is a list, interface, or union type. Fields of these types cannot be part of a @key. | -| 📋 | KEY_INVALID_FIELDS | The fields argument of a @key directive is invalid (it has invalid syntax, includes unknown fields, ...). | -| 📋 | NO_QUERIES | None of the composed subgraphs expose any query. | | 📋 | ONLY_INACCESSIBLE_CHILDREN | A type visible in the API schema has only @inaccessible children. | -| 📋 | PROVIDES_DIRECTIVE_IN_FIELDS_ARG | The fields argument of a @provides directive includes some directive applications. This is not supported. | -| 📋 | PROVIDES_FIELDS_HAS_ARGS | The fields argument of a @provides directive includes a field defined with arguments (which is not currently supported). | | 📋 | PROVIDES_FIELDS_MISSING_EXTERNAL | The fields argument of a @provides directive includes a field that is not marked as @external. | | 📋 | QUERY_ROOT_TYPE_INACCESSIBLE | An element is marked as @inaccessible but is the query root type, which must be visible in the API schema. | | 📋 | REQUIRES_DIRECTIVE_IN_FIELDS_ARG | The fields argument of a @requires directive includes some directive applications. This is not supported. | | 📋 | REQUIRES_INVALID_FIELDS_TYPE | The value passed to the fields argument of a @requires directive is not a string. | | 📋 | REQUIRES_INVALID_FIELDS | The fields argument of a @requires directive is invalid (it has invalid syntax, includes unknown fields, ...). | -| 📋 | ROOT_MUTATION_USED | A subgraph's schema defines a type with the name mutation, while also specifying a different type name as the root query object. This is not allowed. | -| 📋 | ROOT_QUERY_USED | A subgraph's schema defines a type with the name query, while also specifying a different type name as the root query object. This is not allowed. | -| 📋 | ROOT_SUBSCRIPTION_USED | A subgraph's schema defines a type with the name subscription, while also specifying a different type name as the root query object. This is not allowed. | | 📋 | SATISFIABILITY_ERROR | Subgraphs can be merged, but the resulting supergraph API would have queries that cannot be satisfied by those subgraphs. | | 📋 | SHAREABLE_HAS_MISMATCHED_RUNTIME_TYPES | A shareable field return type has mismatched possible runtime types in the subgraphs in which the field is declared. As shared fields must resolve the same way in all subgraphs, this is almost surely a mistake. | | 📋 | TYPE_DEFINITION_INVALID | A built-in or Federation type has an invalid definition in the schema. | diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 1ea1a52..f8cb6ed 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -115,6 +115,656 @@ type User { scalar Tag ``` +### Root Mutation Used + +**Error Code** + +`ROOT_MUTATION_USED` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas. +- For each {schema} in {schemas}: + - Let {rootMutation} be the root mutation type defined in {schema}, if it + exists. + - Let {namedMutationType} be the type with the name `Mutation` in {schema}, if + it exists. + - If {rootMutation} is defined: + - {rootMutation} must be named `Mutation`. + - Otherwise, {namedMutationType} must not be defined. + +**Explanatory Text** + +This rule enforces that, for any subgraph schema, if a root mutation type is +defined, it must be named `Mutation`. Defining a root mutation type with a name +other than `Mutation` or using a differently named type alongside a type +explicitly named `Mutation` creates inconsistencies in schema design and +violates the composite schema specification. + +Valid Example: + +```graphql example +schema { + mutation: Mutation +} + +type Mutation { + createProduct(name: String): Product +} + +type Product { + id: ID! + name: String +} +``` + +The following counter-example violates the rule because `RootMutation` is used +as the root mutation type, but a type named `Mutation` is also defined. + +```graphql counter-example +schema { + mutation: RootMutation +} + +type RootMutation { + createProduct(name: String): Product +} + +type Mutation { + deprecatedField: String +} +``` + +### Root Query Used + +**Error Code** + +`ROOT_QUERY_USED` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas. +- For each {schema} in {schemas}: + - Let {rootQuery} be the root mutation type defined in {schema}, if it exists. + - Let {namedQueryType} be the type with the name `Query` in {schema}, if it + exists. + - If {rootQuery} is defined: + - {rootQuery} must be named `Query`. + - Otherwise, {namedQueryType} must not be defined. + +**Explanatory Text** + +This rule enforces that the root query type in any subgraph schema must be named +`Query`. Defining a root query type with a name other than `Query` or using a +differently named type alongside a type explicitly named `Query` creates +inconsistencies in schema design and violates the composite schema +specification. + +**Examples** + +Valid Example: + +```graphql example +schema { + query: Query +} + +type Query { + product(id: ID!): Product +} + +type Product { + id: ID! + name: String +} +``` + +The following counter-example violates the rule because `RootQuery` is used as +the root query type, but a type named `Query` is also defined. + +```graphql counter-example +# Subgraph A +schema { + query: RootQuery +} + +type RootQuery { + product(id: ID!): Product +} + +type Query { + deprecatedField: String +} +``` + +### Root Subscription Used + +**Error Code** + +`ROOT_SUBSCRIPTION_USED` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas. +- For each {schema} in {schemas}: + - Let {rootSubscription} be the root mutation type defined in {schema}, if it + exists. + - Let {namedSubscriptionType} be the type with the name `Subscription` in + {schema}, if it exists. + - If {rootSubscription} is defined: + - {rootSubscription} must be named `Subscription`. + - Otherwise, {namedSubscriptionType} must not be defined. + +**Explanatory Text** + +This rule enforces that, for any subgraph schema, if a root subscription type is +defined, it must be named `Subscription`. Defining a root subscription type with +a name other than `Subscription` or using a differently named type alongside a +type explicitly named `Subscription` creates inconsistencies in schema design +and violates the composite schema specification. + +**Examples** + +Valid Example: + +```graphql example +# Subgraph A +schema { + subscription: Subscription +} + +type Subscription { + productCreated: Product +} + +type Product { + id: ID! + name: String +} +``` + +The following counter-example violates the rule because `RootSubscription` is +used as the root subscription type, but a type named `Subscription` is also +defined. + +```graphql counter-example +# Subgraph A +schema { + subscription: RootSubscription +} + +type RootSubscription { + productCreated: Product +} + +type Subscription { + deprecatedField: String +} +``` + +### Key Fields Select Invalid Type + +**Error Code** + +`KEY_FIELDS_SELECT_INVALID_TYPE` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be the set of all source schemas. + - Let {types} be the set of all object types that are annotated with the + `@key` directive in {schema}. + - For each {type} in {types}: + - Let {keyFields} be the set of fields referenced by the `fields` argument + of the `@key` directive on {type}. + - For each {field} in {keyFields}: + - Let {fieldType} be the type of {field}. + - {fieldType} must not be a `List`, `Interface`, or `Union` type. + +**Explanatory Text** + +The `@key` directive is used to define the set of fields that uniquely identify +an entity. These fields must reference scalars or object types to ensure a valid +and consistent representation of the entity across subgraphs. Fields of types +`List`, `Interface`, or `Union` cannot be part of a `@key` because they do not +have a well-defined unique value. + +**Examples** + +In this valid example, the `Product` type has a valid `@key` directive +referencing the scalar field `sku`. + +```graphql example +type Product @key(fields: "sku") { + sku: String! + name: String +} +``` + +In the following counter-example, the `Product` type has an invalid `@key` +directive referencing a field (`featuredItem`) whose type is an interface, +violating the rule. + +```graphql counter-example +type Product @key(fields: "featuredItem { id }") { + featuredItem: Node! + sku: String! +} + +interface Node { + id: ID! +} +``` + +In this following counter example, the `@key` directive references a field +(`tags`) of type `List`, which is also not allowed. + +```graphql counter-example +type Product @key(fields: "tags") { + tags: [String!]! + sku: String! +} +``` + +In this counter example, the `@key` directive references a field +(`relatedItems`) of type `Union`, which violates the rule. + +```graphql counter-example +type Product @key(fields: "relatedItems") { + relatedItems: Related! + sku: String! +} + +union Related = Product | Service + +type Service { + id: ID! +} +``` + +### Key Directive in Fields Argument + +**Error Code** + +`KEY_DIRECTIVE_IN_FIELDS_ARG` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be the set of all source schemas. + - Let {types} be the set of all object types that are annotated with the + `@key` directive in {schema}. + - For each {type} in {types}: + - Let {fields} be the string value of the `fields` argument of the `@key` + directive on {type}. + - For each {selection} in {fields}: + - {selection} must not contain a directive application. + +**Explanatory Text** + +The `@key` directive specifies the set of fields used to uniquely identify an +entity. The `fields` argument must consist of a valid GraphQL selection set that +does not include any directive applications. Directives in the `fields` argument +are not supported. + +**Examples** + +In this example, the `fields` argument of the `@key` directive does not include +any directive applications, satisfying the rule. + +```graphql example +directive @lowercase on FIELD_DEFINITION + +type User @key(fields: "id name") { + id: ID! + name: String +} +``` + +In this counter-example, the `fields` argument of the `@key` directive includes +a directive application `@lowercase`, which is not allowed. + +```graphql counter-example +directive @lowercase on FIELD_DEFINITION + +type User @key(fields: "id name @lowercase") { + id: ID! + name: String +} +``` + +In this example, the `fields` argument includes a directive application +`@lowercase` nested inside the selection set, which is also invalid. + +```graphql counter-example +directive @lowercase on FIELD_DEFINITION + +type User @key(fields: "id name { firstName @lowercase }") { + id: ID! + name: FullName +} + +type FullName { + firstName: String + lastName: String +} +``` + +### Key Fields Has Arguments + +**Error Code** + +`KEY_FIELDS_HAS_ARGS` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be the set of all source schemas. + - Let {types} be the set of all object types that are annotated with the + `@key` directive in {schema}. + - For each {type} in {types}: + - Let {keyFields} be the set of fields referenced by the `fields` argument + of the `@key` directive on {type}. + - For each {field} in {keyFields}: + - HasKeyFieldsArguments(field) must be true. + +HasKeyFieldsArguments(field): + +- If {field} has arguments: + - return false +- If {field} has a selection set: + - Let {subFields} be the set of all fields in the selection set of {field}. + - For each {subField} in {subFields}: + - HasKeyFieldsArguments(subField) must be true. +- return true + +**Explanatory Text** + +The `@key` directive is used to define the set of fields that uniquely identify +an entity. These fields must not include any field that is defined with +arguments, as arguments introduce variability that prevents consistent and valid +entity resolution across subgraphs. Fields included in the `fields` argument of +the `@key` directive must be static and consistently resolvable. + +**Examples** + +In this example, the `User` type has a valid `@key` directive that references +the argument free fields `id` and `name`. + +```graphql example +type User @key(fields: "id name") { + id: ID! + name: String + tags: [String] +} +``` + +In this counter-example, the `@key` directive references a field (`tags`) that +is defined with arguments (`limit`), which is not allowed. + +```graphql counter-example +type User @key(fields: "id tags") { + id: ID! + tags(limit: Int = 10): [String] +} +``` + +### Key Invalid Fields + +**Error Code** + +`KEY_INVALID_FIELDS` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be the set of all source schemas. + - Let {types} be the set of all object types that are annotated with the + `@key` directive in {schema}. + - For each {type} in {types}: + - Let {fields} be the set of string values of the `fields` argument of the + `@key` directive on {type}. + - For each {field} in {fields}: + - IsValidKeyField(field, type) must be true. + +IsValidKeyField(field, type): + +- If {field} is not defined on {type}: + - return false +- If {field} has a selection set: + - Let {subType} be the return type of {field}. + - Let {subFields} be the set of all fields in the selection set of {field}. + - For each {subField} in {subFields}: + - IsValidKeyField(subField, subType) must be true. +- return true + +**Explanatory Text** + +The `@key` directive specifies the set of fields used to uniquely identify an +entity. The `fields` argument must be valid and meet the following conditions: + +1. It must have valid GraphQL syntax. +2. It must reference fields that are defined on the annotated type. + +Violations of these conditions result in an invalid schema composition, as the +entity key cannot be properly resolved. + +**Examples** + +In this valid example, the `fields` argument of the `@key` directive is properly +defined with valid syntax and references existing fields. + +```graphql example +type Product @key(fields: "sku featuredItem { id }") { + sku: String! + featuredItem: Node! +} + +interface Node { + id: ID! +} +``` + +In this counter-example, the `fields` argument of the `@key` directive has +invalid syntax because it is missing a closing brace. + +```graphql counter-example +type Product @key(fields: "featuredItem { id") { + featuredItem: Node! + sku: String! +} + +interface Node { + id: ID! +} +``` + +In this counter-example, the `fields` argument of the `@key` directive +references a field `id`, which does not exist on the `Product` type. + +```graphql counter-example +type Product @key(fields: "id") { + sku: String! +} +``` + +### Provides Directive in Fields Argument + +**Error Code** + +`PROVIDES_DIRECTIVE_IN_FIELDS_ARG` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be the set of all source schemas. + - Let {fieldsWithProvides} be the set of all fields annotated with the + `@provides` directive in {schema}. + - For each {field} in {fieldsWithProvides}: + - Let {fields} be the selected fields of the `fields` argument of the + `@provides` directive on {field}. + - For each {selection} in {fields}: + - {HasProvidesDirective(selection)} must be false + +HasProvidesDirective(selection): + +- If {selection} has a directive application: + - return true +- If {selection} has a selection set: + - Let {subSelections} be the selections in {selection} + - For each {subSelection} in {subSelections}: + - If {HasProvidesDirective(subSelection)} is true + - return true + +**Explanatory Text** + +The `@provides` directive is used to specify the set of fields on an object type +that a resolver provides for the parent type. The `fields` argument must consist +of a valid GraphQL selection set without any directive applications, as +directives within the `fields` argument are not supported. + +**Examples** + +In this example, the `fields` argument of the `@provides` directive does not +have any directive applications, satisfying the rule. + +```graphql example +directive @lowercase on FIELD_DEFINITION + +type User @key(fields: "id name") { + id: ID! + name: String + profile: Profile @provides(fields: "name") +} + +type Profile { + id: ID! + name: String +} +``` + +In this counter-example, the `fields` argument of the `@provides` directive has +a directive application `@lowercase`, which is not allowed. + +```graphql counter-example +directive @lowercase on FIELD_DEFINITION + +type User @key(fields: "id name") { + id: ID! + name: String + profile: Profile @provides(fields: "name @lowercase") +} + +type Profile { + id: ID! + name: String +} +``` + +### Provides Fields Has Arguments + +**Error Code** + +`PROVIDES_FIELDS_HAS_ARGS` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be the set of all source schemas. + - Let {fieldsWithProvides} be the set of all fields annotated with the + `@provides` directive in {schema}. + - For each {field} in {fieldsWithProvides}: + - Let {selections} be the field selections of the `fields` argument of the + `@provides` directive on {field}. + - Let {type} be the return type of {field} + - For each {selection} in {selections}: + - {ProvidesHasArguments(selection, type)} must be false + +ProvidesHasArguments(selection, type): + +- Let {field} be the field of {type} selected by {selection} +- If {field} has arguments: + - return true +- If {selection} has a selection set: + - Let {subSelections} be the selections in {selection} + - Let {subType} be the return type of {field} + - For each {subSelection} in {subSelections}: + - If {ProvidesHasArguments(subField, subSelection)} is true + - return true + +**Explanatory Text** + +The `@provides` directive specifies fields that a resolver provides for the +parent type. The `fields` argument must reference fields that do not have +arguments, as fields with arguments introduce variability that is incompatible +with the consistent behavior expected of `@provides`. + +**Examples** + +```graphql example +type User @key(fields: "id") { + id: ID! + tags: [String] +} + +type Article @key(fields: "id") { + id: ID! + author: User! @provides(fields: "tags") +} +``` + +This violates the rule because the `tags` field referenced in the `fields` +argument of the `@provides` directive is defined with arguments +(`limit: UserType = ADMIN`). + +```graphql counter-example +type User @key(fields: "id") { + id: ID! + tags(limit: UserType = ADMIN): [String] +} + +enum UserType { + REGULAR + ADMIN +} + +type Article @key(fields: "id") { + id: ID! + author: User! @provides(fields: "tags") +} +``` + + ### Merge ### Post Merge Validation @@ -199,4 +849,127 @@ type ObjectType1 { } ``` +#### No Queries + +**Error Code** + +`NO_QUERIES` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {fields} be the set of all fields in the `Query` type of the merged + schema. +- {HasPublicField(fields)} must be true. + +HasPublicField(fields): + +- For each {field} in {fields}: + - If {IsExposed(field)} is true + - return true +- return false + +**Explanatory Text** + +This rule ensures that the composed schema includes at least one accessible +field on the root `Query` type. + +In GraphQL, the `Query` type is essential as it defines the entry points for +read operations. If none of the composed subgraphs expose any query fields, the +composed schema would lack a root query, making it a invalid GraphQL schema. + +**Examples** + +In this example, at least one subgraph provides accessible query fields, +satisfying the rule. + +```graphql +# Subgraph A +type Query { + product(id: ID!): Product +} + +type Product { + id: ID! +} +``` + +```graphql +type Query { + review(id: ID!): Review +} + +# Subgraph B +type Review { + id: ID! + content: String + rating: Int +} +``` + +Even if some query fields are marked as `@inaccessible`, as long as there is at +least one accessible query field in the composed schema, the rule is satisfied. + +In this case, Subgraph A exposes an internal query field `internalData` marked +with `@inaccessible`, making it hidden in the composed schema. However, Subgraph +B provides an accessible `product` query field. Therefore, the composed schema +has at least one accessible query field, adhering to the rule. + +```graphql +# Subgraph A +type Query { + internalData: InternalData @inaccessible +} + +type InternalData { + secret: String +} +``` + +```graphql +# Subgraph B +type Query { + product(id: ID!): Product +} + +type Product { + id: ID! + name: String +} +``` + +If all query fields in all subgraphs are marked as `@inaccessible`, the composed +schema will lack accessible query fields, violating the rule. + +In the following counter-example, both subgraphs have query fields, but all are +marked as `@inaccessible`. + +This means there are no accessible query fields in the composed schema, +triggering the `NO_QUERIES` error. + +```graphql +# Subgraph A +type Query { + internalData: InternalData @inaccessible +} + +type InternalData { + secret: String +} +``` + +```graphql +# Subgraph B +type Query { + adminStats: AdminStats @inaccessible +} + +type AdminStats { + userCount: Int +} +``` + ## Validate Satisfiability From 1a39f2e9f91c694650d4b0641ffe7dcb7e2c745b Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Fri, 27 Dec 2024 12:49:51 +0100 Subject: [PATCH 3/5] adds: IMPLEMENTED_BY_INACCESSIBLE --- rfcs/apollo-federation-error-codes.md | 2 +- spec/Section 4 -- Composition.md | 78 +++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/rfcs/apollo-federation-error-codes.md b/rfcs/apollo-federation-error-codes.md index 0a67bb3..ebb633b 100644 --- a/rfcs/apollo-federation-error-codes.md +++ b/rfcs/apollo-federation-error-codes.md @@ -26,6 +26,7 @@ | ✅ | KEY_FIELDS_SELECT_INVALID_TYPE | The fields argument of @key directive includes a field whose type is a list, interface, or union type. Fields of these types cannot be part of a @key. | | ✅ | KEY_INVALID_FIELDS | The fields argument of a @key directive is invalid (it has invalid syntax, includes unknown fields, ...). | | ✅ | PROVIDES_DIRECTIVE_IN_FIELDS_ARG | The fields argument of a @provides directive includes some directive applications. This is not supported. | +| ✅ | IMPLEMENTED_BY_INACCESSIBLE | An element is marked as @inaccessible but implements an element visible in the API schema. | ## Covered by other rule @@ -42,7 +43,6 @@ | Status | Error | Description | | ------ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 📋 | IMPLEMENTED_BY_INACCESSIBLE | An element is marked as @inaccessible but implements an element visible in the API schema. | | 📋 | INTERFACE_FIELD_NO_IMPLEM | After subgraph merging, an implementation is missing a field of one of the interfaces it implements (which can happen for valid subgraphs). | | 📋 | INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE | A subgraph has a @key on an interface type, but that subgraph does not define an implementation (in the supergraph) of that interface. | | 📋 | INTERFACE_KEY_NOT_ON_IMPLEMENTATION | A @key is defined on an interface type, but is not defined (or is not resolvable) on at least one of the interface implementations. | diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index f8cb6ed..5710b93 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -972,4 +972,82 @@ type AdminStats { } ``` +### Implemented by Inaccessible + +**Error Code** + +`IMPLEMENTED_BY_INACCESSIBLE` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be the merged composite execution schema. +- Let {types} be the set of all object types in {schema}. +- For each {type} in {types}: + - If {type} is not marked with `@inaccessible`: + - Let {implementedInterfaces} be the set of all interfaces implemented by {type}. + - For each {field} in {type}'s fields: + - If {field} is marked with `@inaccessible`: + - For each {implementedInterface} in {implementedInterfaces}: + - Let {interfaceField} be the field on {implementedInterface} that has the same name as {field} + - If {interfaceField} exists: + - {IsExposed(interfaceField)} must be false + +**Explanatory Text** + +This rule ensures that inaccessible fields (`@inaccessible`) on an object type are not exposed through an interface. +An object type that implements an interface must provide public access to each field defined by the interface. +If a field on an object type is marked as `@inaccessible` but implements an interface field that is visible in the composed schema, this creates a contradiction: the interface contract requires that field to be accessible, yet the object type implementation hides it. + +This rule prevents inconsistencies in the composed schema, ensuring that every interface field visible in the composed schema is also publicly visible on all types implementing that interface. + +**Examples** + +In the following example, `User.id` is accessible and implements `Node.id` which is also accessible, no error occurs. + + +```graphql +# The interface field `id` is visible and provided by `User` without @inaccessible. +interface Node { + id: ID! +} + +type User implements Node { + id: ID! + name: String +} +``` + +Since `Auditable` and its field `lastAudit` are `@inaccessible`, the `Order.lastAudit` field is allowed to be `@inaccessible` because it does not implement any visible interface field in the composed schema. + +```graphql +# The entire interface is @inaccessible, thus its fields are not publicly visible. +interface Auditable @inaccessible { + lastAudit: DateTime! +} + +type Order implements Auditable { + lastAudit: DateTime! @inaccessible + orderNumber: String +} +``` + +In this example, `Node.id` is visible in the public schema (no `@inaccessible`), but `User.id` is marked `@inaccessible`. +This violates the interface contract because `User` claims to implement `Node`, yet does not expose the `id` field to the public schema. + +```graphql counter-example +interface Node { + id: ID! +} + +type User implements Node { + id: ID! @inaccessible + name: String +} +``` + + ## Validate Satisfiability From 30ca4c026e9c31cea1f18ac8ae087833196ea1ec Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sat, 28 Dec 2024 02:02:34 +0100 Subject: [PATCH 4/5] add more rules --- rfcs/apollo-federation-error-codes.md | 24 +- spec/Section 4 -- Composition.md | 1490 ++++++++++++++++++++++--- 2 files changed, 1334 insertions(+), 180 deletions(-) diff --git a/rfcs/apollo-federation-error-codes.md b/rfcs/apollo-federation-error-codes.md index ebb633b..d4cf6ab 100644 --- a/rfcs/apollo-federation-error-codes.md +++ b/rfcs/apollo-federation-error-codes.md @@ -27,6 +27,18 @@ | ✅ | KEY_INVALID_FIELDS | The fields argument of a @key directive is invalid (it has invalid syntax, includes unknown fields, ...). | | ✅ | PROVIDES_DIRECTIVE_IN_FIELDS_ARG | The fields argument of a @provides directive includes some directive applications. This is not supported. | | ✅ | IMPLEMENTED_BY_INACCESSIBLE | An element is marked as @inaccessible but implements an element visible in the API schema. | +| ✅ | INTERFACE_FIELD_NO_IMPLEM | After subgraph merging, an implementation is missing a field of one of the interfaces it implements (which can happen for valid subgraphs). | +| ✅ | INVALID_FIELD_SHARING | A field that is non-shareable in at least one subgraph is resolved by multiple subgraphs. | +| ✅ | INVALID_SHAREABLE_USAGE | The @shareable Federation directive is used in an invalid way. | +| ✅ | ONLY_INACCESSIBLE_CHILDREN | A type visible in the API schema has only @inaccessible children. | +| ✅ | PROVIDES_FIELDS_MISSING_EXTERNAL | The fields argument of a @provides directive includes a field that is not marked as @external. | +| ✅ | QUERY_ROOT_TYPE_INACCESSIBLE | An element is marked as @inaccessible but is the query root type, which must be visible in the API schema. | +| ✅ | REQUIRES_DIRECTIVE_IN_FIELDS_ARG | The fields argument of a @requires directive includes some directive applications. This is not supported. | +| ✅ | REQUIRES_INVALID_FIELDS_TYPE | The value passed to the fields argument of a @requires directive is not a string. | +| ✅ | REQUIRES_INVALID_FIELDS | The fields argument of a @requires directive is invalid (it has invalid syntax, includes unknown fields, ...). **Composte schema also has REQUIRES_INVALID_SYNTAX** | +| ✅ | TYPE_DEFINITION_INVALID | A built-in or Federation type has an invalid definition in the schema. | +| ✅ | TYPE_KIND_MISMATCH | A type has the same name in different subgraphs, but a different kind. For instance, one definition is an object type but another is an interface.Replaces VALUE_TYPE_KIND_MISMATCH, EXTENSION_OF_WRONG_KIND, ENUM_MISMATCH_TYPE. | +| ✅ | PROVIDES_INVALID_FIELDS | The fields argument of a @provides directive is invalid (it has invalid syntax, includes unknown fields, ...). | ## Covered by other rule @@ -43,22 +55,10 @@ | Status | Error | Description | | ------ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 📋 | INTERFACE_FIELD_NO_IMPLEM | After subgraph merging, an implementation is missing a field of one of the interfaces it implements (which can happen for valid subgraphs). | | 📋 | INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE | A subgraph has a @key on an interface type, but that subgraph does not define an implementation (in the supergraph) of that interface. | | 📋 | INTERFACE_KEY_NOT_ON_IMPLEMENTATION | A @key is defined on an interface type, but is not defined (or is not resolvable) on at least one of the interface implementations. | -| 📋 | INVALID_FIELD_SHARING | A field that is non-shareable in at least one subgraph is resolved by multiple subgraphs. | -| 📋 | INVALID_SHAREABLE_USAGE | The @shareable Federation directive is used in an invalid way. | -| 📋 | ONLY_INACCESSIBLE_CHILDREN | A type visible in the API schema has only @inaccessible children. | -| 📋 | PROVIDES_FIELDS_MISSING_EXTERNAL | The fields argument of a @provides directive includes a field that is not marked as @external. | -| 📋 | QUERY_ROOT_TYPE_INACCESSIBLE | An element is marked as @inaccessible but is the query root type, which must be visible in the API schema. | -| 📋 | REQUIRES_DIRECTIVE_IN_FIELDS_ARG | The fields argument of a @requires directive includes some directive applications. This is not supported. | -| 📋 | REQUIRES_INVALID_FIELDS_TYPE | The value passed to the fields argument of a @requires directive is not a string. | -| 📋 | REQUIRES_INVALID_FIELDS | The fields argument of a @requires directive is invalid (it has invalid syntax, includes unknown fields, ...). | | 📋 | SATISFIABILITY_ERROR | Subgraphs can be merged, but the resulting supergraph API would have queries that cannot be satisfied by those subgraphs. | | 📋 | SHAREABLE_HAS_MISMATCHED_RUNTIME_TYPES | A shareable field return type has mismatched possible runtime types in the subgraphs in which the field is declared. As shared fields must resolve the same way in all subgraphs, this is almost surely a mistake. | -| 📋 | TYPE_DEFINITION_INVALID | A built-in or Federation type has an invalid definition in the schema. | -| 📋 | TYPE_KIND_MISMATCH | A type has the same name in different subgraphs, but a different kind. For instance, one definition is an object type but another is an interface.Replaces VALUE_TYPE_KIND_MISMATCH, EXTENSION_OF_WRONG_KIND, ENUM_MISMATCH_TYPE. | -| 📋 | PROVIDES_INVALID_FIELDS | The fields argument of a @provides directive is invalid (it has invalid syntax, includes unknown fields, ...). | | 📋 | INVALID_GRAPHQL | A schema is invalid GraphQL: it violates one of the rules of the specification. | ## Later diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index 5710b93..b0d96ea 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -764,96 +764,84 @@ type Article @key(fields: "id") { } ``` - -### Merge - -### Post Merge Validation - -#### Empty Merged Object Type +### Provides Fields Missing External **Error Code** -`EMPTY_MERGED_OBJECT_TYPE` - -**Severity** ERROR +`PROVIDES_FIELDS_MISSING_EXTERNAL` -**Formal Specification** +**Severity** -- Let {types} be the set of all object types across all source schemas -- For each {type} in {types}: - - {IsObjectTypeEmpty(type)} must be false. +ERROR -IsObjectTypeEmpty(type): +**Formal Specification** -- 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 +- Let {schemas} be the set of all source schemas. +- For each {schema} in {schemas} + - Let {objectTypes} be the set of all object types in {schema}. + - For each {objectType} in {objectTypes}: + - Let {providingFields} be the set of fields on {objectType} annotated with + `@provides`. + - For each {field} in {providingFields}: + - Let {referencedFields} be the set of fields referenced by the `fields` + argument of the `@provides` directive on {field}. + - For each {referencedField} in {referencedFields}: + - If {referencedField} is **not** marked as `@external` + - Produce a `PROVIDES_FIELDS_MISSING_EXTERNAL` error. **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: +The `@provides` directive indicates that an object type field will supply +additional fields belonging to the return type in this execution specific path. +Any field listed in the `@provides(fields: ...)` argument must therefore be +_external_ in the local schema, meaning that the local schema itself does +**not** provide it. -```graphql -type ObjectType1 { - field1: String - field2: Int @inaccessible -} +This rule disallows selecting non-external fields in a `@provides` selection +set. If a field is already provided by the same schema in all execution paths, +there is no need to `@provide`. -type ObjectType1 { - field2: Int - field3: Boolean -} -``` +**Examples** -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. +Here, the `Order` type from this schema is providing fields on `User` through +`@provides`. The `name` field of `User` is **not** defined in this schema; it is +declared with `@external` indicating that the `name` field comes from elsewhere. +Thus, referencing `name` under `@provides(fields: "name")` is valid. -```graphql -type ObjectType1 @inaccessible { - field1: String - field2: Int +```graphql example +type Order { + id: ID! + customer: User @provides(fields: "name") } -type ObjectType1 { - field3: Boolean +type User @key(fields: "id") { + id: ID! + name: String @external } ``` -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: +In this counter-example, `User.address` is **not** marked as `@external` in the +same schema that applies `@provides`. This means the schema already provides the +`address` field in all possible paths, so using `@provides(fields: "address")` +is invalid. ```graphql counter-example -type ObjectType1 { - field1: String @inaccessible - field2: Boolean +type User { + id: ID! + address: String } -type ObjectType1 { - field1: String - field2: Boolean @inaccessible +type Order { + id: ID! + buyer: User @provides(fields: "address") } ``` -#### No Queries +### Query Root Type Inaccessible **Error Code** -`NO_QUERIES` +`QUERY_ROOT_TYPE_INACCESSIBLE` **Severity** @@ -861,122 +849,198 @@ ERROR **Formal Specification** -- Let {fields} be the set of all fields in the `Query` type of the merged - schema. -- {HasPublicField(fields)} must be true. - -HasPublicField(fields): - -- For each {field} in {fields}: - - If {IsExposed(field)} is true - - return true -- return false +- Let {schemas} be the set of all source schemas. +- For each {schema} in {schemas}: + - Let {queryType} be the query operation type defined in {schema}. + - If {queryType} is annotated with `@inaccessible`: + - Produce a `QUERY_ROOT_TYPE_INACCESSIBLE` error. **Explanatory Text** -This rule ensures that the composed schema includes at least one accessible -field on the root `Query` type. - -In GraphQL, the `Query` type is essential as it defines the entry points for -read operations. If none of the composed subgraphs expose any query fields, the -composed schema would lack a root query, making it a invalid GraphQL schema. +Every source schema that contributes to the final composite schema must expose a +public (accessible) root query type. Marking the root query type as +`@inaccessible` makes it invisible to the gateway, defeating its purpose as the +primary entry point for queries and lookups. **Examples** -In this example, at least one subgraph provides accessible query fields, -satisfying the rule. +In this example, no `@inaccessible` annotation is applied to the query root, so +the rule is satisfied. + +```graphql example +extend schema { + query: Query +} -```graphql -# Subgraph A type Query { - product(id: ID!): Product + allBooks: [Book] } -type Product { +type Book { id: ID! + title: String } ``` -```graphql -type Query { - review(id: ID!): Review +Since the schema marks the query root type as `@inaccessible`, the rule is +violated. `QUERY_ROOT_TYPE_INACCESSIBLE` is raised because a schema’s root query +type cannot be hidden from consumers. + +```graphql counter-example +extend schema { + query: Query } -# Subgraph B -type Review { +type Query @inaccessible { + allBooks: [Book] +} + +type Book { id: ID! - content: String - rating: Int + title: String } ``` -Even if some query fields are marked as `@inaccessible`, as long as there is at -least one accessible query field in the composed schema, the rule is satisfied. +### Requires Directive in Fields Argument -In this case, Subgraph A exposes an internal query field `internalData` marked -with `@inaccessible`, making it hidden in the composed schema. However, Subgraph -B provides an accessible `product` query field. Therefore, the composed schema -has at least one accessible query field, adhering to the rule. +**Error Code** -```graphql -# Subgraph A -type Query { - internalData: InternalData @inaccessible +`REQUIRES_DIRECTIVE_IN_FIELDS_ARG` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas. +- For each {schema} in {schemas}: + - Let {compositeTypes} be the set of all composite types in {schema}. + - For each {composite} in {compositeTypes}: + - Let {fields} be the set of fields on {composite} + - Let {arguments} be the set of all arguments on {fields} + - For each {argument} in {arguments}: + - If {argument} is **not** marked with `@requires`: + - Continue + - Let {fieldsArg} be the value of the `fields` argument of the `@requires` + directive on {argument}. + - If {fieldsArg} contains a directive application: + - Produce a `REQUIRES_DIRECTIVE_IN_FIELDS_ARG` error. + +**Explanatory Text** + +The `@requires` directive is used to specify fields on the same type that an +argument depends on in order to resolve the annotated field. +When using `@requires(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 `REQUIRES_DIRECTIVE_IN_FIELDS_ARG` error. + +**Examples** + +In this valid usage, the `@requires` directive’s `fields` argument references +`name` without any directive applications, avoiding the error. + +```graphql example +type User @key(fields: "id name") { + id: ID! + profile(name: String! @requires(fields: "name")): Profile } -type InternalData { - secret: String +type Profile { + id: ID! + name: String } ``` -```graphql -# Subgraph B -type Query { - product(id: ID!): Product +Because the `@requires` selection (`name @lowercase`) includes a directive +application (`@lowercase`), this violates the rule and triggers a +`REQUIRES_DIRECTIVE_IN_FIELDS_ARG` error. + +```graphql counter-example +type User @key(fields: "id name") { + id: ID! + name: String + profile(name: String! @requires(fields: "name @lowercase")): Profile } -type Product { +type Profile { id: ID! name: String } ``` -If all query fields in all subgraphs are marked as `@inaccessible`, the composed -schema will lack accessible query fields, violating the rule. +### Requires Invalid Fields Type -In the following counter-example, both subgraphs have query fields, but all are -marked as `@inaccessible`. +**Error Code** -This means there are no accessible query fields in the composed schema, -triggering the `NO_QUERIES` error. +`REQUIRES_INVALID_FIELDS_TYPE` -```graphql -# Subgraph A -type Query { - internalData: InternalData @inaccessible +**Severity** + +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas. +- For each {schema} in {schemas}: + - Let {compositeTypes} be the set of all composite types in {schema}. + - For each {composite} in {compositeTypes}: + - Let {fields} be the set of fields on {composite}. + - Let {arguments} be the set of all arguments on {fields}. + - For each {argument} in {arguments}: + - If {argument} is **not** annotated with `@requires`: + - Continue + - Let {fieldsArg} be the value of the `fields` argument of the `@requires` + directive on {argument}. + - If {fieldsArg} is **not** a string: + - Produce a `REQUIRES_INVALID_FIELDS_TYPE` error. + +**Explanatory Text** + +When using the `@requires` directive, the `fields` argument must always be a +string that defines a (potentially nested) selection set of fields from the same +type. If the `fields` argument is provided as a type other than a string (such +as an integer, boolean, or enum), the directive usage is invalid and will cause +schema composition to fail. + +**Examples** + +In the following example, the `@requires` directive’s `fields` argument is a +valid string and satisfies the rule. + +```graphql example +type User @key(fields: "id") { + id: ID! + profile(name: String! @requires(fields: "name")): Profile } -type InternalData { - secret: String +type Profile { + id: ID! + name: String } ``` -```graphql -# Subgraph B -type Query { - adminStats: AdminStats @inaccessible +Since `fields` is set to `123` (an integer) instead of a string, this violates +the rule and triggers a `REQUIRES_INVALID_FIELDS_TYPE` error. + +```graphql counter-example +type User @key(fields: "id") { + id: ID! + profile(name: String! @requires(fields: 123)): Profile } -type AdminStats { - userCount: Int +type Profile { + id: ID! + name: String } ``` -### Implemented by Inaccessible +### Requires Invalid Syntax **Error Code** -`IMPLEMENTED_BY_INACCESSIBLE` +`REQUIRES_INVALID_SYNTAX` **Severity** @@ -984,70 +1048,1160 @@ ERROR **Formal Specification** -- Let {schema} be the merged composite execution schema. -- Let {types} be the set of all object types in {schema}. -- For each {type} in {types}: - - If {type} is not marked with `@inaccessible`: - - Let {implementedInterfaces} be the set of all interfaces implemented by {type}. - - For each {field} in {type}'s fields: - - If {field} is marked with `@inaccessible`: - - For each {implementedInterface} in {implementedInterfaces}: - - Let {interfaceField} be the field on {implementedInterface} that has the same name as {field} - - If {interfaceField} exists: - - {IsExposed(interfaceField)} must be false - -**Explanatory Text** +- Let {schemas} be the set of all source schemas. +- For each {schema} in {schemas} + - Let {compositeTypes} be the set of all composite types in {schema}. + - For each {composite} in {compositeTypes}: + - Let {fields} be the set of fields on {composite}. + - Let {arguments} be the set of all arguments on {fields}. + - For each {argument} in {arguments}: + - If {argument} is **not** annotated with `@requires`: + - Continue + - Let {fieldsArg} be the string value of the `fields` argument of the + `@requires` directive on {argument}. + - {fieldsArg} must be be parsable as a valid selection map -This rule ensures that inaccessible fields (`@inaccessible`) on an object type are not exposed through an interface. -An object type that implements an interface must provide public access to each field defined by the interface. -If a field on an object type is marked as `@inaccessible` but implements an interface field that is visible in the composed schema, this creates a contradiction: the interface contract requires that field to be accessible, yet the object type implementation hides it. +**Explanatory Text** -This rule prevents inconsistencies in the composed schema, ensuring that every interface field visible in the composed schema is also publicly visible on all types implementing that interface. +The `@requires` directive’s `fields` argument must be syntactically valid +GraphQL. If the selection map string is malformed (e.g., missing closing braces, +unbalanced quotes, invalid tokens), then the schema cannot be composed +correctly. In such cases, the error `REQUIRES_INVALID_SYNTAX` is raised. **Examples** -In the following example, `User.id` is accessible and implements `Node.id` which is also accessible, no error occurs. +In the following example, the `@requires` directive’s `fields` argument is a +valid selection map and satisfies the rule. - -```graphql -# The interface field `id` is visible and provided by `User` without @inaccessible. -interface Node { +```graphql example +type User @key(fields: "id") { id: ID! + profile(name: String! @requires(fields: "name")): Profile } -type User implements Node { +type Profile { id: ID! name: String } ``` -Since `Auditable` and its field `lastAudit` are `@inaccessible`, the `Order.lastAudit` field is allowed to be `@inaccessible` because it does not implement any visible interface field in the composed schema. - -```graphql -# The entire interface is @inaccessible, thus its fields are not publicly visible. -interface Auditable @inaccessible { - lastAudit: DateTime! -} - -type Order implements Auditable { - lastAudit: DateTime! @inaccessible - orderNumber: String -} -``` +In the following counter-example, the `@requires` directive’s `fields` argument +has invalid syntax because it is missing a closing brace. -In this example, `Node.id` is visible in the public schema (no `@inaccessible`), but `User.id` is marked `@inaccessible`. -This violates the interface contract because `User` claims to implement `Node`, yet does not expose the `id` field to the public schema. +This violates the rule and triggers a `REQUIRES_INVALID_FIELDS` error. ```graphql counter-example -interface Node { +type Book { id: ID! + title(lang: String! @requires(fields: "author { name ")): String } -type User implements Node { - id: ID! @inaccessible +type Author { name: String } ``` +### Type Definition Invalid + +**Error Code** + +`TYPE_DEFINITION_INVALID` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be one of the source schemas. +- Let {types} be the set of built-in types (for example, `FieldSelectionMap`) + defined by the composition specification. +- For each {type} in {types}: + - {type} must strictly equal the built-in type defined by the composition + specification. + +**Explanatory Text** + +Certain types are reserved in composite schema specification for specific +purposes and must adhere to the specification’s definitions. For example, +`FieldSelectionMap` is a built-in scalar that represents a selection of fields +as a string. Redefining these built-in types with a different kind (e.g., an +input object, enum, union, or object type) is disallowed and makes the +composition invalid. + +This rule ensures that built-in types maintain their expected shapes and +semantics so the composed schema can correctly interpret them. + +**Examples** + +In the following counter-example, `FieldSelectionMap` is declared as an `input` +type instead of the required `scalar`. This leads to a `TYPE_DEFINITION_INVALID` +error because the defined scalar `FieldSelectionMap` is being overridden by an +incompatible definition. + +```graphql counter-example +directive @require(field: FieldSelectionMap!) on ARGUMENT_DEFINITION + +input FieldSelectionMap { + fields: [String!]! +} +``` + +### Type Kind Mismatch + +**Error Code** +`TYPE_KIND_MISMATCH` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas. +- For each type name {typeName} defined in at least one of these schemas: + - Let {types} be the set of all types named {typeName} across all source + schemas. + - Let {typeKinds} be the set of + [type kinds](https://spec.graphql.org/October2021/#sec-Type-Kinds) in + {types} + - {typeKinds} must contain exactly one element. + +**Explanatory Text** + +Each named type must represent the **same** kind of GraphQL type across all +source schemas. For instance, a type named `User` must consistently be an object +type, or consistently be an interface, and so forth. If one schema defines +`User` as an object type, while another schema declares `User` as an interface +(or input object, union, etc.), the schema composition process cannot merge +these definitions coherently. + +This rule ensures semantic consistency: a single type name cannot serve +multiple, incompatible purposes in the final composed schema. + +**Examples** + +All schemas agree that `User` is an object type: + +```graphql +# Schema A +type User { + id: ID! + name: String +} + +# Schema B +type User { + id: ID! + email: String +} + +# Schema C +type User { + id: ID! + joinedAt: String +} +``` + +In the following counter-example, `User` is defined as an object type in one of +the schemas and as an interface in another. This violates the rule and results + +```graphql +# Schema A: `User` is an object type +type User { + id: ID! + name: String +} + +# Schema B: `User` is an interface +extend interface User { + id: ID! + friends: [User!]! +} + +# Schema C: `User` is an input object +extend input User { + id: ID! +} +``` + +### Provides Invalid Syntax + +**Error Code** +`PROVIDES_INVALID_SYNTAX` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas. +- For each {schema} in {schemas} + - Let {fieldsWithProvides} be the set of all fields annotated with the + `@provides` directive in {schema}. + - For each {field} in {fieldsWithProvides}: + - Let {fieldsArg} be the string value of the `fields` argument of the + `@provides` directive on {field}. + - {fieldsArg} must be a valid selection set string + +**Explanatory Text** + +The `@provides` directive’s `fields` argument must be a syntactically valid +selection set string, as if you were selecting fields in a GraphQL query. If the +selection set is malformed (e.g., missing braces, unbalanced quotes, or invalid +tokens), the schema composition fails with a `PROVIDES_INVALID_SYNTAX` error. + +**Examples** + +Here, the `@provides` directive’s `fields` argument is a valid selection set: + +```graphql example +type User @key(fields: "id") { + id: ID! + address: Address @provides(fields: "street city") +} + +type Address { + street: String + city: String +} +``` + +In this counter-example, the `fields` argument is missing a closing brace. It +cannot be parsed as a valid GraphQL selection set, triggering a +`PROVIDES_INVALID_SYNTAX` error. + +```graphql counter-example +type User @key(fields: "id") { + id: ID! + address: Address @provides(fields: "{ street city ") +} +``` + +### Merge + +### 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 +} +``` + +#### No Queries + +**Error Code** + +`NO_QUERIES` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {fields} be the set of all fields in the `Query` type of the merged + schema. +- {HasPublicField(fields)} must be true. + +HasPublicField(fields): + +- For each {field} in {fields}: + - If {IsExposed(field)} is true + - return true +- return false + +**Explanatory Text** + +This rule ensures that the composed schema includes at least one accessible +field on the root `Query` type. + +In GraphQL, the `Query` type is essential as it defines the entry points for +read operations. If none of the composed subgraphs expose any query fields, the +composed schema would lack a root query, making it a invalid GraphQL schema. + +**Examples** + +In this example, at least one subgraph provides accessible query fields, +satisfying the rule. + +```graphql +# Subgraph A +type Query { + product(id: ID!): Product +} + +type Product { + id: ID! +} +``` + +```graphql +type Query { + review(id: ID!): Review +} + +# Subgraph B +type Review { + id: ID! + content: String + rating: Int +} +``` + +Even if some query fields are marked as `@inaccessible`, as long as there is at +least one accessible query field in the composed schema, the rule is satisfied. + +In this case, Subgraph A exposes an internal query field `internalData` marked +with `@inaccessible`, making it hidden in the composed schema. However, Subgraph +B provides an accessible `product` query field. Therefore, the composed schema +has at least one accessible query field, adhering to the rule. + +```graphql +# Subgraph A +type Query { + internalData: InternalData @inaccessible +} + +type InternalData { + secret: String +} +``` + +```graphql +# Subgraph B +type Query { + product(id: ID!): Product +} + +type Product { + id: ID! + name: String +} +``` + +If all query fields in all subgraphs are marked as `@inaccessible`, the composed +schema will lack accessible query fields, violating the rule. + +In the following counter-example, both subgraphs have query fields, but all are +marked as `@inaccessible`. + +This means there are no accessible query fields in the composed schema, +triggering the `NO_QUERIES` error. + +```graphql +# Subgraph A +type Query { + internalData: InternalData @inaccessible +} + +type InternalData { + secret: String +} +``` + +```graphql +# Subgraph B +type Query { + adminStats: AdminStats @inaccessible +} + +type AdminStats { + userCount: Int +} +``` + +### Implemented by Inaccessible + +**Error Code** + +`IMPLEMENTED_BY_INACCESSIBLE` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be the merged composite execution schema. +- Let {types} be the set of all object types in {schema}. +- For each {type} in {types}: + - If {type} is not marked with `@inaccessible`: + - Let {implementedInterfaces} be the set of all interfaces implemented by + {type}. + - For each {field} in {type}'s fields: + - If {field} is marked with `@inaccessible`: + - For each {implementedInterface} in {implementedInterfaces}: + - Let {interfaceField} be the field on {implementedInterface} that has + the same name as {field} + - If {interfaceField} exists: + - {IsExposed(interfaceField)} must be false + +**Explanatory Text** + +This rule ensures that inaccessible fields (`@inaccessible`) on an object type +are not exposed through an interface. An object type that implements an +interface must provide public access to each field defined by the interface. If +a field on an object type is marked as `@inaccessible` but implements an +interface field that is visible in the composed schema, this creates a +contradiction: the interface contract requires that field to be accessible, yet +the object type implementation hides it. + +This rule prevents inconsistencies in the composed schema, ensuring that every +interface field visible in the composed schema is also publicly visible on all +types implementing that interface. + +**Examples** + +In the following example, `User.id` is accessible and implements `Node.id` which +is also accessible, no error occurs. + +```graphql +# The interface field `id` is visible and provided by `User` without @inaccessible. +interface Node { + id: ID! +} + +type User implements Node { + id: ID! + name: String +} +``` + +Since `Auditable` and its field `lastAudit` are `@inaccessible`, the +`Order.lastAudit` field is allowed to be `@inaccessible` because it does not +implement any visible interface field in the composed schema. + +```graphql +# The entire interface is @inaccessible, thus its fields are not publicly visible. +interface Auditable @inaccessible { + lastAudit: DateTime! +} + +type Order implements Auditable { + lastAudit: DateTime! @inaccessible + orderNumber: String +} +``` + +In this example, `Node.id` is visible in the public schema (no `@inaccessible`), +but `User.id` is marked `@inaccessible`. This violates the interface contract +because `User` claims to implement `Node`, yet does not expose the `id` field to +the public schema. + +```graphql counter-example +interface Node { + id: ID! +} + +type User implements Node { + id: ID! @inaccessible + name: String +} +``` + +### Interface Field No Implementation + +**Error Code** + +`INTERFACE_FIELD_NO_IMPLEM` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be the merged composite execution schema. +- Let {objectTypes} be the set of all object types defined in {schema}. +- For each {objectType} in {objectTypes}: + - Let {interfaces} be the set of interface types that {objectType} implements. + - For each {interface} in {interfaces}: + - Let {interfaceFields} be the set of fields defined on {interface} that are + visible in the merged schema. + - For each {field} in {interfaceFields}: + - If {field} is not present on {objectType}: + - Produce an `INTERFACE_FIELD_NO_IMPLEM` error. + +**Explanatory Text** + +In GraphQL, any object type that implements an interface must provide a field +definition for every field declared by that interface. If an object type fails +to implement a particular field required by one of its interfaces, the composite +schema becomes invalid because the resulting schema breaks the contract defined +by that interface. + +This rule checks that object types merged from different sources correctly +implement all interface fields. In scenarios where a schema defines an interface +field, but the implementing object type in another schema omits that field, an +error is raised. + +**Examples** + +In this valid example, the `User` interface has three fields: `id`, `name`, and +`email`. Both the `RegisteredUser` and `GuestUser` types implement all three +fields, satisfying the interface contract. + +```graphql example +# Schema A +interface User { + id: ID! + name: String! + email: String +} + +type RegisteredUser implements User { + id: ID! + name: String! + email: String + lastLogin: DateTime +} + +# Schema B +interface User { + id: ID! + name: String! + email: String +} + +type GuestUser implements User { + id: ID! + name: String! + email: String + temporaryCartId: String +} +``` + +In this counter-example, the `User` interface is defined with three fields, but +the `GuestUser` type omits one of them (`email`), causing an +`INTERFACE_FIELD_NO_IMPLEM` error. + +Although `GuestUser` implements `User`, it does not provide the `email` field. +Since the merged schema sees that the interface `User` has `email` but +`GuestUser` does not provide it, the schema composition fails with the +`INTERFACE_FIELD_NO_IMPLEM` error. + +```graphql counter-example +# Schema A +interface User { + id: ID! + name: String! + email: String +} + +type RegisteredUser implements User { + id: ID! + name: String! + email: String + lastLogin: DateTime +} + +# Schema B +interface User { + id: ID! + name: String! +} + +type GuestUser implements User { + id: ID! + name: String! + temporaryCartId: String +} +``` + +### Invalid Field Sharing + +**Error Code** + +`INVALID_FIELD_SHARING` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be the merged composite execution schema. +- Let {types} be the set of all object and interface types in {schema}. +- For each {type} in {types}: + - If {type} is the `Subscription` type: + - Let {fields} be the set of all fields in {type}. + - For each {field} in {fields}: + - If {field} is marked with `@shareable`: + - Produce an `INVALID_FIELD_SHARING` error. + - Otherwise: + - Let {fields} be the set of all fields on {type}. + - For each {field} in {fields}: + - If {field} is not part of a `@key` directive: + - Let {fieldDefinitions} be the set of all field definitions for {field} + across all source schemas excluding fields marked with `@external` or + `@override`. + - If {fieldDefinitions} has more than one element: + - {field} must be marked as `@shareable` in at least one schema. + +**Explanatory Text** + +A field in a federated GraphQL schema may be marked `@shareable`, indicating +that the same field can be resolved by multiple schemas without conflict. When a +field is **not** marked as `@shareable` (sometimes called "non-shareable"), it +cannot be provided by more than one schema. + +Field definitions marked as `@external` or `@override` are excluded when +validating whether a field is shareable. These annotations indicate specific +cases where field ownership lies with another schema or has been replaced. + +Additionally, subscription root fields cannot be shared (i.e., they are +effectively non-shareable), as subscription events from multiple schemas would +create conflicts in the composed schema. Attempting to mark a subscription field +as shareable or to define it in multiple schemas triggers the same error. + +**Examples** + +In this example, the `User` type field `fullName` is marked as shareable in both +schemas, allowing them to serve consistent data for that field without conflict. + +```graphql example +# Schema A +type User @key(fields: "id") { + id: ID! + username: String + fullName: String @shareable +} + +# Schema B +type User @key(fields: "id") { + id: ID! + fullName: String @shareable + email: String +} +``` + +In the following example, `User.fullName` is overriden in one schema and +therefore the field can be define in multiple schemas without being marked as +`@shareable`. + +```graphql example +# Schema A +type User @key(fields: "id") { + id: ID! + fullName: String @override(from": "B") +} + +# Schema B +type User @key(fields: "id") { + id: ID! + fullName: String +} +``` + +In the following example, `User.fullName` is marked as `@external` in one schema +and therefore the field can be define in the other schema without being marked +as `@shareable`. + +```graphql example +# Schema A +type User @key(fields: "id") { + id: ID! + fullName: String @external +} + +# Schema B +type User @key(fields: "id") { + id: ID! + fullName: String +} +``` + +In the following counter-example, `User.profile` is non-shareable but is defined +and resolved by two different schemas, resulting in an `INVALID_FIELD_SHARING` +error. + +```graphql counter-example +# Schema A +type User @key(fields: "id") { + id: ID! + profile: Profile +} + +type Profile { + avatarUrl: String +} + +# Schema B +type User @key(fields: "id") { + id: ID! + profile: Profile +} + +type Profile { + avatarUrl: String +} +``` + +By definition, root subscription fields cannot be shared across multiple +schemas. In this example, both schemas define a subscription field +`newOrderPlaced`: + +```graphql counter-example +# Schema A +type Subscription { + newOrderPlaced: Order @shareable +} + +type Order { + id: ID! + items: [String] +} + +# Schema B +type Subscription { + newOrderPlaced: Order @shareable +} +``` + +### Invalid Shareable Usage + +**Error Code** + +`INVALID_SHAREABLE_USAGE` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be one of the composed schemas. +- Let {types} be the set of types defined in {schema}. +- For each {type} in {types}: + - If {type} is an interface type: + - For each field definition {field} in {type}: + - If {field} is annotated with `@shareable`, produce an + `INVALID_SHAREABLE_USAGE` error. + +**Explanatory Text** + +The `@shareable` directive is intended to indicate that a field on an **object +type** can be resolved by multiple schemas without conflict. As a result, it is +only valid to use `@shareable` on fields **of object types** (or on the entire +object type itself). + +Applying `@shareable` to interface fields is disallowed and violates the valid +usage of the directive. This rule prevents schema composition errors and data +conflicts by ensuring that `@shareable` is used only in contexts where shared +field resolution is meaningful and unambiguous. + +**Examples** + +In this example, the field `orderStatus` on the `Order` object type is marked +with `@shareable`, which is allowed. It signals that this field can be served +from multiple schemas without creating a conflict. + +```graphql example +type Order { + id: ID! + orderStatus: String @shareable + total: Float +} +``` + +In this example, the `InventoryItem` interface has a field `sku` marked with +`@shareable`, which is invalid usage. Marking an interface field as shareable +leads to an `INVALID_SHAREABLE_USAGE` error. + +```graphql counter-example +interface InventoryItem { + sku: ID! @shareable + name: String +} +``` + +### Only Inaccessible Children + +**Error Code** +`ONLY_INACCESSIBLE_CHILDREN` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schema} be the composed schema. +- Let {types} be the set of all types in {schema}. +- For each {type} in {types}: + - If {IsExposed(type)} is false: + - continue + - If {type} is the query, mutation, or subscription root type: + - continue + - If {type} is an object type: + - {HasObjectTypeAccessibleChildren(type)} must be true + - If {type} is an enum type: + - {HasEnumAccessibleChildren(type)} must be true + - If {type} is an input object type: + - {HasInputObjectAccessibleChildren(type)} must be true + - If {type} is an interface type: + - {HasInterfaceAccessibleChildren(type)} must be true + - If {type} is a union type: + - {HasUnionAccessibleChildren(type)} must be true + +HasObjectTypeAccessibleChildren(type): + +- Let {fields} be the set of all fields in {type}. +- For each {field} in {fields}: + - If {field} is **not** marked with `@inaccessible` and **not** `@internal`: + - return true +- return false + +HasEnumAccessibleChildren(type): + +- Let {values} be the set of all values in {type}. +- For each {value} in {values}: + - If {value} is **not** marked with `@inaccessible`: + - return true +- return false + +HasInputObjectAccessibleChildren(type): + +- Let {fields} be the set of all fields in {type}. +- For each {field} in {fields}: + - If {value} is **not** marked with `@inaccessible`: + - return true +- return false + +HasInterfaceAccessibleChildren(type): + +- Let {fields} be the set of all fields in {type}. +- For each {field} in {fields}: + - If {field} is **not** marked with `@inaccessible`: + - return true +- return false + +HasUnionAccessibleChildren(type): + +- Let {members} be the set of all member types in {type}. +- For each {member} in {members}: + - Let {type} be the type of {member}. + - If {type} is **not** marked with `@inaccessible`: + - return true +- return false + +**Explanatory Text** + +A type that is **not** annotated with `@inaccessible` is expected to appear in +the composed schema. If, however, all of its child elements (fields in an object +or interface, values in an enum, fields in an input object or all types of a +union) are individually marked `@inaccessible` (or `@internal`), then there are +no accessible sub-parts of that type for consumers to query or reference. + +Allowing such a type to remain in the composed schema despite having no publicly +visible fields or values leads to an invalid schema. This rule enforces that a +type visible in the composed schema must have at least one accessible child. +Otherwise, it triggers an `ONLY_INACCESSIBLE_CHILDREN` error, prompting the user +to either make the entire type `@inaccessible` or expose at least one child +element. + +Additionally, the rule applies to all types except the query, mutation, and +subscription root types. + +**Examples** + +In the following example, the `Profile` type is included in the composed schema, +and `Profile.email` is **not** marked with `@inaccessible`. This satisfies the +rule, as there is at least one accessible child element. + +```graphql +type User { + id: ID! + profile: Profile +} + +type Profile { + name: String @inaccessible + email: String +} +``` + +In the following example, all fields of the `Profile` type are marked with +`@inaccessible`. But since `Profile` itself is marked with `@inaccessible`, it +is not required to have any accessible children. + +```graphql +type User { + id: ID! + profile: Profile @inaccessible +} + +type Profile @inaccessible { + name: String @inaccessible + email: String @inaccessible +} +``` + +The `Profile` type is included in the composed schema (no `@inaccessible` on the +type), but **all** of its fields are marked `@inaccessible`, triggering an +`ONLY_INACCESSIBLE_CHILDREN` error. + +```graphql counter-example +type User { + id: ID! + profile: Profile +} + +type Profile { + name: String @inaccessible + email: String @inaccessible +} +``` + +In this example, the `DeliveryStatus` enum is not annotated with +`@inaccessible`, yet all of its values are. + +Since there are no publicly visible values, an `ONLY_INACCESSIBLE_CHILDREN` +error is produced. + +```graphql counter-example +enum DeliveryStatus { + PENDING @inaccessible + SHIPPED @inaccessible + DELIVERED @inaccessible +} +``` + +### Requires Invalid Fields + +**Error Code** + +`REQUIRES_INVALID_FIELDS` + +**Severity** + +ERROR + +**Formal Specification** + +- Let {schema} be the merged composite execution schema. +- Let {compositeTypes} be the set of all composite types in {schema}. +- For each {composite} in {compositeTypes}: + - Let {fields} be the set of fields on {composite}. + - Let {arguments} be the set of all arguments on {fields}. + - For each {argument} in {arguments}: + - If {argument} is **not** annotated with `@requires`: + - Continue + - Let {fieldsArg} be the string value of the `fields` argument of the + `@requires` directive on {argument}. + - Let {parsedFieldsArg} be the parsed selection map from {fieldsArg}. + - {ValidateSelectionMap(parsedFieldsArg, parentType)} must be true. + +ValidateSelectionMap(selectionMap, parentType): + +- For each {selection} in {selectionMap}: + - Let {field} be the field selected by {selection} on {parentType}. + - If {field} is **not** defined on {parentType}: + - return false + - Let {fieldType} be the type of {field}. + - If {fieldType} is not a scalar type + - Let {subSelections} be the selections in {selection} + - If {subSelections} is empty + - return false + - If {ValidateSelectionMap(subSelections, fieldType)} is false + - return false +- return true + +**Explanatory Text** + +Even if the selection map for `@requires(fields: "…")` is syntactically valid, +its contents must also be valid within the composed schema. Fields must exist on +the parent type for them to be referenced by `@requires`. In addition, fields +requiring unknown fields break the valid usage of `@requires`, leading to a +`REQUIRES_INVALID_FIELDS` error. + +**Examples** + +In the following example, the `@requires` directive’s `fields` argument is a +valid selection set and satisfies the rule. + +```graphql example +type User @key(fields: "id") { + id: ID! + name: String! + profile(name: String! @requires(fields: "name")): Profile +} + +type Profile { + id: ID! + name: String +} +``` + +In this counter-example, the `@requires` directive does not have a valid +selection set and triggers a `REQUIRES_INVALID_FIELDS` error. + +```graphql counter-example +type Book { + id: ID! + title(lang: String! @requires(fields: "author { }")): String +} + +type Author { + name: String +} +``` + +In this counter-example, the `@requires` directive references a field +(`unknown`) that does not exist on the parent type (`Book`), causing a +`REQUIRES_INVALID_FIELDS` error. + +```graphql counter-example +type Book { + id: ID! + pages(pageSize: Int @requires(fields: "unknownField")): Int +} +``` + +### Provides Invalid Fields + +**Error Code** +`PROVIDES_INVALID_FIELDS` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schema} be the merged composite execution schema. +- Let {fieldsWithProvides} be the set of all fields annotated with the + `@provides` directive in {schema}. +- For each {field} in {fieldsWithProvides}: + - Let {fieldsArg} be the string value of the `fields` argument of the + `@provides` directive on {field}. + - Let {parsedSelectionSet} be the parsed selection set from {fieldsArg}. + - Let {returnType} be the return type of {field}. + - {ValidateSelectionSet(parsedSelectionSet, returnType)} must be true. + +ValidateSelectionSet(selectionSet, parentType): + +- For each {selection} in {selectionSet}: + - Let {selectedField} be the field named by {selection} in {parentType}. + - If {selectedField} does not exist on {parentType}: + - return false + - If {selectedField} returns a composite type then {selection} + - Let {subSelections} be the selections in {selection} + - If {subSelections} is empty + - return false + - If {ValidateSelectionSet(subSelections, fieldType)} is false + - return false +- return true + +**Explanatory Text** + +Even if the `@provides(fields: "…")` argument is well-formed syntactically, the +selected fields must actually exist on the return type of the field. Invalid +field references— e.g., selecting non-existent fields, referencing fields on the +wrong type, or incorrectly omitting required nested selections—lead to a +`PROVIDES_INVALID_FIELDS` error. + +**Examples** + +In the following example, the `@provides` directive references a valid field +(`hobbies`) on the `UserDetails` type. + +```graphql example +type User @key(fields: "id") { + id: ID! + details: UserDetails @provides(fields: "hobbies") +} + +type UserDetails { + hobbies: [String] +} +``` + +In the following counter-example, the `@provides` directive specifies a field +named `unknownField` which is not defined on `UserDetails`. This raises a +`PROVIDES_INVALID_FIELDS` error. + +```graphql counter-example +type User @key(fields: "id") { + id: ID! + details: UserDetails @provides(fields: "unknownField") +} + +type UserDetails { + hobbies: [String] +} +``` ## Validate Satisfiability From d40fa8278c459cba17b0586ecad25bf5f5c3f6b6 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 29 Dec 2024 22:11:30 +0100 Subject: [PATCH 5/5] add more rules --- rfcs/apollo-federation-error-codes.md | 28 +- spec/Section 4 -- Composition.md | 1068 ++++++++++++++++++++++--- 2 files changed, 952 insertions(+), 144 deletions(-) diff --git a/rfcs/apollo-federation-error-codes.md b/rfcs/apollo-federation-error-codes.md index d4cf6ab..39ec154 100644 --- a/rfcs/apollo-federation-error-codes.md +++ b/rfcs/apollo-federation-error-codes.md @@ -39,6 +39,16 @@ | ✅ | TYPE_DEFINITION_INVALID | A built-in or Federation type has an invalid definition in the schema. | | ✅ | TYPE_KIND_MISMATCH | A type has the same name in different subgraphs, but a different kind. For instance, one definition is an object type but another is an interface.Replaces VALUE_TYPE_KIND_MISMATCH, EXTENSION_OF_WRONG_KIND, ENUM_MISMATCH_TYPE. | | ✅ | PROVIDES_INVALID_FIELDS | The fields argument of a @provides directive is invalid (it has invalid syntax, includes unknown fields, ...). | +| ✅ | INVALID_GRAPHQL | A schema is invalid GraphQL: it violates one of the rules of the specification. | +| ✅ ️ | OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE | The @override directive cannot be used on external fields, nor to override fields with either @external, @provides, or @requires. | +| ✅ ️️ | OVERRIDE_FROM_SELF_ERROR | Field with @override directive has "from" location that references its own subgraph. | +| ✅ ️️️ | OVERRIDE_ON_INTERFACE | The @override directive cannot be used on the fields of an interface type. | +| ✅ ️️️️ | OVERRIDE_SOURCE_HAS_OVERRIDE | Field which is overridden to another subgraph is also marked @override. | +| ✅ ️️️️️ | EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE | The @external directive collides with other directives in some situations. | +| ✅ ️️️️️ ️| KEY_INVALID_FIELDS_TYPE | The value passed to the fields argument of a @key directive is not a string. | +| ✅ ️️️️️️ | PROVIDES_INVALID_FIELDS_TYPE | The value passed to the fields argument of a @provides directive is not a string. | +| ✅ ️️️️️️ | PROVIDES_ON_NON_OBJECT_FIELD | A @provides directive is used to mark a field whose base type is not an object type. | +| ✅ ️️️️️️️ | EXTERNAL_ON_INTERFACE | The field of an interface type is marked with @external: as external is about marking field not resolved by the subgraph and as interface field are not resolved (only implementations of those fields are), an "external" interface field is nonsensical | ## Covered by other rule @@ -59,41 +69,31 @@ | 📋 | INTERFACE_KEY_NOT_ON_IMPLEMENTATION | A @key is defined on an interface type, but is not defined (or is not resolvable) on at least one of the interface implementations. | | 📋 | SATISFIABILITY_ERROR | Subgraphs can be merged, but the resulting supergraph API would have queries that cannot be satisfied by those subgraphs. | | 📋 | SHAREABLE_HAS_MISMATCHED_RUNTIME_TYPES | A shareable field return type has mismatched possible runtime types in the subgraphs in which the field is declared. As shared fields must resolve the same way in all subgraphs, this is almost surely a mistake. | -| 📋 | INVALID_GRAPHQL | A schema is invalid GraphQL: it violates one of the rules of the specification. | ## Later | Status | Error | Description | | ------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| ⏭️ | OVERRIDE_LABEL_INVALID | The @override directive label argument must match the pattern `/^[a-zA-Z]a-zA-Z0-9\*-:./]\*$/ or /^percent((d{1,2}(.d{1,8})I 100))$/` | | ⏭️ | DIRECTIVE_COMPOSITION_ERROR | Error when composing custom directives. | | ⏭️ | DIRECTIVE_DEFINITION_INVALID | A built-in or Federation directive has an invalid definition in the schema. | | ⏭️ | DOWNSTREAM_SERVICE_ERROR | Indicates an error in a subgraph service query during query execution in a | | ⏭️ | EXTENSION_WITH_NO_BASE | A subgraph is attempting to extend a type that is not originally defined in any known subgraph. | | ⏭️ | KEY_UNSUPPORTED_ON_INTERFACE | A @key directive is used on an interface, which is only supported when @linking to Federation v2.3 or later. | -| ⏭️ | OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE | The @override directive cannot be used on external fields, nor to override fields with either @external, @provides, or @requires. | -| ⏭️ | OVERRIDE_FROM_SELF_ERROR | Field with @override directive has "from" location that references its own subgraph. | -| ⏭️ | OVERRIDE_LABEL_INVALID | The @override directive label argument must match the pattern `/^[a-zA-Z]a-zA-Z0-9\*-:./]\*$/ or /^percent((d{1,2}(.d{1,8})I 100))$/` | -| ⏭️ | OVERRIDE_ON_INTERFACE | The @override directive cannot be used on the fields of an interface type. | -| ⏭️ | OVERRIDE_SOURCE_HAS_OVERRIDE | Field which is overridden to another subgraph is also marked @override. | +| ⏭️ | MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL | In a subgraph, a field is both marked @external and has a merged directive applied to it. | +| ⏭️ | PROVIDES_UNSUPPORTED_ON_INTERFACE | A @provides directive is used on an interface, which is not (yet) supported. | + ## Open Questions | Status | Error | Description | | ------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| ❓ | EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE | The @external directive collides with other directives in some situations. | | ❓ | INTERFACE_OBJECT_USAGE_ERROR | Error in the usage of the @interfaceObject directive. | | ❓ | INVALID_FEDERATION_SUPERGRAPH | Indicates that a schema provided for an Apollo Federation supergraph is not a valid supergraph schema. | -| ❓ | KEY_INVALID_FIELDS_TYPE | The value passed to the fields argument of a @key directive is not a string. | -| ❓ | MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL | In a subgraph, a field is both marked @external and has a merged directive applied to it. | -| ❓ | PROVIDES_INVALID_FIELDS_TYPE | The value passed to the fields argument of a @provides directive is not a string. | -| ❓ | PROVIDES_ON_NON_OBJECT_FIELD | A @provides directive is used to mark a field whose base type is not an object type. | -| ❓ | PROVIDES_UNSUPPORTED_ON_INTERFACE | A @provides directive is used on an interface, which is not (yet) supported. | - ## Not needed in composite schemas | Status | Error | Description | | ------ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔴 | EXTERNAL_ON_INTERFACE | The field of an interface type is marked with @external: as external is about marking field not resolved by the subgraph and as interface field are not resolved (only implementations of those fields are), an "external" interface field is nonsensical | | 🔴 | INVALID_LINK_DIRECTIVE_USAGE | An application of the @link directive is invalid/does not respect the specification. | | 🔴 | INVALID_LINK_IDENTIFIER | A URL/version for a @link feature is invalid/does not respect the specification. | | 🔴 | INVALID_SUBGRAPH_NAME | A subgraph name is invalid. (Subgraph names cannot be a single underscore (\*)). | diff --git a/spec/Section 4 -- Composition.md b/spec/Section 4 -- Composition.md index b0d96ea..de962c9 100644 --- a/spec/Section 4 -- Composition.md +++ b/spec/Section 4 -- Composition.md @@ -139,13 +139,15 @@ ERROR **Explanatory Text** -This rule enforces that, for any subgraph schema, if a root mutation type is +This rule enforces that, for any source schema, if a root mutation type is defined, it must be named `Mutation`. Defining a root mutation type with a name other than `Mutation` or using a differently named type alongside a type explicitly named `Mutation` creates inconsistencies in schema design and violates the composite schema specification. -Valid Example: +**Examples** + +Valid example: ```graphql example schema { @@ -202,7 +204,7 @@ ERROR **Explanatory Text** -This rule enforces that the root query type in any subgraph schema must be named +This rule enforces that the root query type in any source schema must be named `Query`. Defining a root query type with a name other than `Query` or using a differently named type alongside a type explicitly named `Query` creates inconsistencies in schema design and violates the composite schema @@ -210,7 +212,7 @@ specification. **Examples** -Valid Example: +Valid example: ```graphql example schema { @@ -231,7 +233,6 @@ The following counter-example violates the rule because `RootQuery` is used as the root query type, but a type named `Query` is also defined. ```graphql counter-example -# Subgraph A schema { query: RootQuery } @@ -269,7 +270,7 @@ ERROR **Explanatory Text** -This rule enforces that, for any subgraph schema, if a root subscription type is +This rule enforces that, for any source schema, if a root subscription type is defined, it must be named `Subscription`. Defining a root subscription type with a name other than `Subscription` or using a differently named type alongside a type explicitly named `Subscription` creates inconsistencies in schema design @@ -277,10 +278,9 @@ and violates the composite schema specification. **Examples** -Valid Example: +Valid example: ```graphql example -# Subgraph A schema { subscription: Subscription } @@ -300,7 +300,6 @@ used as the root subscription type, but a type named `Subscription` is also defined. ```graphql counter-example -# Subgraph A schema { subscription: RootSubscription } @@ -327,20 +326,22 @@ ERROR **Formal Specification** - Let {schema} be the set of all source schemas. - - Let {types} be the set of all object types that are annotated with the - `@key` directive in {schema}. + - Let {types} be the set of all object or interface types that are annotated + with the `@key` directive in {schema}. - For each {type} in {types}: - - Let {keyFields} be the set of fields referenced by the `fields` argument - of the `@key` directive on {type}. - - For each {field} in {keyFields}: - - Let {fieldType} be the type of {field}. - - {fieldType} must not be a `List`, `Interface`, or `Union` type. + - Let {keyDirectives} be the set of all `@key` directives on {type}. + - For each {keyDirective} in {keyDirectives} + - Let {keyFields} be the set of all fields (including nested) referenced + by the `fields` argument of {keyDirective}. + - For each {field} in {keyFields}: + - Let {fieldType} be the type of {field}. + - {fieldType} must not be a `List`, `Interface`, or `Union` type. **Explanatory Text** The `@key` directive is used to define the set of fields that uniquely identify an entity. These fields must reference scalars or object types to ensure a valid -and consistent representation of the entity across subgraphs. Fields of types +and consistent representation of the entity across schemas. Fields of types `List`, `Interface`, or `Union` cannot be part of a `@key` because they do not have a well-defined unique value. @@ -371,8 +372,8 @@ interface Node { } ``` -In this following counter example, the `@key` directive references a field -(`tags`) of type `List`, which is also not allowed. +In this counter example, the `@key` directive references a field (`tags`) of +type `List`, which is also not allowed. ```graphql counter-example type Product @key(fields: "tags") { @@ -410,13 +411,13 @@ ERROR **Formal Specification** - Let {schema} be the set of all source schemas. - - Let {types} be the set of all object types that are annotated with the - `@key` directive in {schema}. + - Let {types} be the set of all object and interface types in {schema}. - For each {type} in {types}: - - Let {fields} be the string value of the `fields` argument of the `@key` - directive on {type}. - - For each {selection} in {fields}: - - {selection} must not contain a directive application. + - Let {keyDirectives} be the set of all `@key` directives on {type}. + - For each {keyDirective} in {keyDirectives}: + - Let {fields} be the string value of the `fields` argument of + {keyDirective}. + - {fields} must not contain a directive application. **Explanatory Text** @@ -431,8 +432,6 @@ In this example, the `fields` argument of the `@key` directive does not include any directive applications, satisfying the rule. ```graphql example -directive @lowercase on FIELD_DEFINITION - type User @key(fields: "id name") { id: ID! name: String @@ -504,13 +503,13 @@ HasKeyFieldsArguments(field): The `@key` directive is used to define the set of fields that uniquely identify an entity. These fields must not include any field that is defined with arguments, as arguments introduce variability that prevents consistent and valid -entity resolution across subgraphs. Fields included in the `fields` argument of +entity resolution across schemas. Fields included in the `fields` argument of the `@key` directive must be static and consistently resolvable. **Examples** In this example, the `User` type has a valid `@key` directive that references -the argument free fields `id` and `name`. +the argument-free fields `id` and `name`. ```graphql example type User @key(fields: "id name") { @@ -530,6 +529,66 @@ type User @key(fields: "id tags") { } ``` +### Key Invalid Syntax + +**Error Code** +`KEY_INVALID_SYNTAX` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas. + - Let {types} be the set of all object or interface types in each {schema}. + - For each {type} in {types}: + - Let {keyDirectives} be the set of all `@key` directives on {type}. + - For each {keyDirective} in {keyDirectives}: + - Let {fieldsArg} be the string value of the `fields` argument of + {keyDirective}. + - Attempt to parse {fieldsArg} as a valid GraphQL selection set. + - Parsing must **not** fail (e.g., missing braces, invalid tokens, + unbalanced curly braces, or other syntax errors). + +**Explanatory Text** + +Each `@key` directive must specify the fields that uniquely identify an entity +using a valid GraphQL selection set in its `fields` argument. If the `fields` +argument string is syntactically incorrect—missing closing braces, containing +invalid tokens, or otherwise malformed - it cannot be composed into a valid +schema and triggers the `KEY_INVALID_SYNTAX` error. + +**Examples** + +In this valid scenario, the `fields` argument is a correctly formed selection +set: `"sku featuredItem { id }"` is properly balanced and contains no syntax +errors. + +```graphql example +type Product @key(fields: "sku featuredItem { id }") { + sku: String! + featuredItem: Node! +} + +interface Node { + id: ID! +} +``` + +Here, the selection set `"featuredItem { id"` is missing the closing brace `}`. +It is thus invalid syntax, causing a `KEY_INVALID_SYNTAX` error. + +```graphql counter-example +type Product @key(fields: "featuredItem { id") { + featuredItem: Node! + sku: String! +} + +interface Node { + id: ID! +} +``` + ### Key Invalid Fields **Error Code** @@ -543,35 +602,36 @@ ERROR **Formal Specification** - Let {schema} be the set of all source schemas. - - Let {types} be the set of all object types that are annotated with the - `@key` directive in {schema}. + - Let {types} be the set of all object and interface types in {schema}. - For each {type} in {types}: - - Let {fields} be the set of string values of the `fields` argument of the - `@key` directive on {type}. - - For each {field} in {fields}: - - IsValidKeyField(field, type) must be true. - -IsValidKeyField(field, type): - -- If {field} is not defined on {type}: + - Let {keyDirectives} be the set of all `@key` directives on {type}. + - For each {keyDirective} in {keyDirectives}: + - Let {fieldsArg} be the string value of the `fields` argument of + {keyDirective}. + - Let {selections} be the set of fields in the selection set of + {fieldsArg}. + - For each {selection} in {selections}: + - {IsValidKeyField(selection, type)} must be true. + +IsValidKeyField(selection, type): + +- If {selection} is not defined on {type}: - return false -- If {field} has a selection set: +- If {selection} has a selection set: - Let {subType} be the return type of {field}. - Let {subFields} be the set of all fields in the selection set of {field}. - For each {subField} in {subFields}: - - IsValidKeyField(subField, subType) must be true. + - {IsValidKeyField(subField, subType)} must be true. - return true **Explanatory Text** -The `@key` directive specifies the set of fields used to uniquely identify an -entity. The `fields` argument must be valid and meet the following conditions: - -1. It must have valid GraphQL syntax. -2. It must reference fields that are defined on the annotated type. - -Violations of these conditions result in an invalid schema composition, as the -entity key cannot be properly resolved. +Even if the selection set for `@key(fields: "…")` is syntactically valid, field +reference within that selection set must also refer to **actual** fields on the +annotated type. This includes nested selections, which must appear on the +corresponding return type. If any referenced field is missing or incorrectly +named, composition fails with a `KEY_INVALID_FIELDS` error because the entity +key cannot be resolved correctly. **Examples** @@ -589,20 +649,6 @@ interface Node { } ``` -In this counter-example, the `fields` argument of the `@key` directive has -invalid syntax because it is missing a closing brace. - -```graphql counter-example -type Product @key(fields: "featuredItem { id") { - featuredItem: Node! - sku: String! -} - -interface Node { - id: ID! -} -``` - In this counter-example, the `fields` argument of the `@key` directive references a field `id`, which does not exist on the `Product` type. @@ -656,8 +702,6 @@ In this example, the `fields` argument of the `@provides` directive does not have any directive applications, satisfying the rule. ```graphql example -directive @lowercase on FIELD_DEFINITION - type User @key(fields: "id name") { id: ID! name: String @@ -901,11 +945,11 @@ type Book { } ``` -### Requires Directive in Fields Argument +### Require Directive in Fields Argument **Error Code** -`REQUIRES_DIRECTIVE_IN_FIELDS_ARG` +`REQUIRE_DIRECTIVE_IN_FIELDS_ARG` **Severity** @@ -920,31 +964,31 @@ ERROR - Let {fields} be the set of fields on {composite} - Let {arguments} be the set of all arguments on {fields} - For each {argument} in {arguments}: - - If {argument} is **not** marked with `@requires`: + - If {argument} is **not** marked with `@require`: - Continue - - Let {fieldsArg} be the value of the `fields` argument of the `@requires` + - Let {fieldsArg} be the value of the `fields` argument of the `@require` directive on {argument}. - If {fieldsArg} contains a directive application: - - Produce a `REQUIRES_DIRECTIVE_IN_FIELDS_ARG` error. + - Produce a `REQUIRE_DIRECTIVE_IN_FIELDS_ARG` error. **Explanatory Text** -The `@requires` directive is used to specify fields on the same type that an +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 `@requires(fields: "…")`, the `fields` argument must be a valid +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 `REQUIRES_DIRECTIVE_IN_FIELDS_ARG` error. +supported and triggers the `REQUIRE_DIRECTIVE_IN_FIELDS_ARG` error. **Examples** -In this valid usage, the `@requires` directive’s `fields` argument references +In this valid usage, the `@require` directive’s `fields` argument references `name` without any directive applications, avoiding the error. ```graphql example type User @key(fields: "id name") { id: ID! - profile(name: String! @requires(fields: "name")): Profile + profile(name: String! @require(fields: "name")): Profile } type Profile { @@ -953,15 +997,15 @@ type Profile { } ``` -Because the `@requires` selection (`name @lowercase`) includes a directive +Because the `@require` selection (`name @lowercase`) includes a directive application (`@lowercase`), this violates the rule and triggers a -`REQUIRES_DIRECTIVE_IN_FIELDS_ARG` error. +`REQUIRE_DIRECTIVE_IN_FIELDS_ARG` error. ```graphql counter-example type User @key(fields: "id name") { id: ID! name: String - profile(name: String! @requires(fields: "name @lowercase")): Profile + profile(name: String! @require(fields: "name @lowercase")): Profile } type Profile { @@ -970,11 +1014,11 @@ type Profile { } ``` -### Requires Invalid Fields Type +### Require Invalid Fields Type **Error Code** -`REQUIRES_INVALID_FIELDS_TYPE` +`REQUIRE_INVALID_FIELDS_TYPE` **Severity** @@ -989,16 +1033,16 @@ ERROR - Let {fields} be the set of fields on {composite}. - Let {arguments} be the set of all arguments on {fields}. - For each {argument} in {arguments}: - - If {argument} is **not** annotated with `@requires`: + - If {argument} is **not** annotated with `@require`: - Continue - - Let {fieldsArg} be the value of the `fields` argument of the `@requires` + - Let {fieldsArg} be the value of the `fields` argument of the `@require` directive on {argument}. - If {fieldsArg} is **not** a string: - - Produce a `REQUIRES_INVALID_FIELDS_TYPE` error. + - Produce a `REQUIRE_INVALID_FIELDS_TYPE` error. **Explanatory Text** -When using the `@requires` directive, the `fields` argument must always be a +When using the `@require` directive, the `fields` argument must always be a string that defines a (potentially nested) selection set of fields from the same type. If the `fields` argument is provided as a type other than a string (such as an integer, boolean, or enum), the directive usage is invalid and will cause @@ -1006,13 +1050,13 @@ schema composition to fail. **Examples** -In the following example, the `@requires` directive’s `fields` argument is a +In the following example, the `@require` directive’s `fields` argument is a valid string and satisfies the rule. ```graphql example type User @key(fields: "id") { id: ID! - profile(name: String! @requires(fields: "name")): Profile + profile(name: String! @require(fields: "name")): Profile } type Profile { @@ -1022,12 +1066,12 @@ type Profile { ``` Since `fields` is set to `123` (an integer) instead of a string, this violates -the rule and triggers a `REQUIRES_INVALID_FIELDS_TYPE` error. +the rule and triggers a `REQUIRE_INVALID_FIELDS_TYPE` error. ```graphql counter-example type User @key(fields: "id") { id: ID! - profile(name: String! @requires(fields: 123)): Profile + profile(name: String! @require(fields: 123)): Profile } type Profile { @@ -1036,11 +1080,11 @@ type Profile { } ``` -### Requires Invalid Syntax +### Require Invalid Syntax **Error Code** -`REQUIRES_INVALID_SYNTAX` +`REQUIRE_INVALID_SYNTAX` **Severity** @@ -1055,28 +1099,28 @@ ERROR - Let {fields} be the set of fields on {composite}. - Let {arguments} be the set of all arguments on {fields}. - For each {argument} in {arguments}: - - If {argument} is **not** annotated with `@requires`: + - If {argument} is **not** annotated with `@require`: - Continue - Let {fieldsArg} be the string value of the `fields` argument of the - `@requires` directive on {argument}. + `@require` directive on {argument}. - {fieldsArg} must be be parsable as a valid selection map **Explanatory Text** -The `@requires` directive’s `fields` argument must be syntactically valid +The `@require` directive’s `fields` argument must be syntactically valid GraphQL. If the selection map string is malformed (e.g., missing closing braces, unbalanced quotes, invalid tokens), then the schema cannot be composed -correctly. In such cases, the error `REQUIRES_INVALID_SYNTAX` is raised. +correctly. In such cases, the error `REQUIRE_INVALID_SYNTAX` is raised. **Examples** -In the following example, the `@requires` directive’s `fields` argument is a +In the following example, the `@require` directive’s `fields` argument is a valid selection map and satisfies the rule. ```graphql example type User @key(fields: "id") { id: ID! - profile(name: String! @requires(fields: "name")): Profile + profile(name: String! @require(fields: "name")): Profile } type Profile { @@ -1085,15 +1129,15 @@ type Profile { } ``` -In the following counter-example, the `@requires` directive’s `fields` argument +In the following counter-example, the `@require` directive’s `fields` argument has invalid syntax because it is missing a closing brace. -This violates the rule and triggers a `REQUIRES_INVALID_FIELDS` error. +This violates the rule and triggers a `REQUIRE_INVALID_FIELDS` error. ```graphql counter-example type Book { id: ID! - title(lang: String! @requires(fields: "author { name ")): String + title(lang: String! @require(fields: "author { name ")): String } type Author { @@ -1277,6 +1321,770 @@ type User @key(fields: "id") { } ``` +### Invalid GraphQL + +**Error Code** +`INVALID_GRAPHQL` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas to be composed. +- For Each {schema} in {schemas} + - {schema} must be a syntactically valid + - {schama} must be a semantically valid GraphQL schema according to the + [GraphQL specification](https://spec.graphql.org/). + +**Explanatory Text** + +Before composition, every individual source schema must be valid as per the +official GraphQL specification. Common reasons a schema may be considered +"invalid GraphQL" include: + +- **Syntax Errors**: Missing braces, invalid tokens, or misplaced punctuation. +- **Unknown Types**: Referencing types that are not defined within the schema or + imported from elsewhere. +- **Invalid Directive Usage**: Omitting required arguments to directives or + using directives in disallowed locations. +- **Invalid Default Values**: Providing default values for arguments or fields + that do not conform to the type (e.g., a default of `null` for a non-null + field, an invalid enum value, etc.). +- **Conflicting Type Definitions**: Defining or overriding a built-in type or + directive incorrectly. + +When any of these validation checks fail for a particular source schema, that +schema does not meet the baseline requirements for composition, and the +composition process cannot proceed. An `INVALID_GRAPHQL` error is raised, +prompting the schema owner to correct the GraphQL violations before retrying +composition. + +**Examples** + +In the following counter-example, the schema is invalid because the type `User` +is referenced in the `Query` type but never defined: + +```graphql counter-example +type Query { + user: User +} + +# The type "User" is never defined; this is invalid GraphQL. +``` + +In this counter-example, `"INVALID_VALUE"` is not a valid `Role`, causing +`INVALID_GRAPHQL`. + +```graphql counter-example +enum Role { + ADMIN + USER +} + +type Query { + users(role: Role = "INVALID_VALUE"): [String] +} +``` + +The GraphQL spec requires all non-null directive arguments to be supplied. The +omission of the `fields` argument in the `@provides` directive triggers +`INVALID_GRAPHQL`. + +```graphql counter-example +directive @provides(fields: String!) on FIELD_DEFINITION + +type Product { + price: Float @provides + # "fields" argument is required, but not provided. +} +``` + +### Override Collision with Another Directive + +**Error Code** +`OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas to be composed. +- For each {schema} in {schemas}: + - Let {types} be the set of all composite types in {schema}. + - For each {type} in {types}: + - Let {fields} be the set of fields on {type}. + - For each {field} in {fields}: + - If {field} is annotated with `@override`: + - {field} must **not** be annotated with `@external` + +**Explanatory Text** + +The `@override` directive designates that ownership of a field is transferred +from one source schema to another in the resulting composite schema. When such a +transfer occurs, that field **cannot** also be annotated `@external`. A field +declared as `@external` is originally defined in a **different** source schema. +Overriding a field and simultaneously claiming it is external to the local +schema is contradictory. + +In this case composition fails with an +`OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE` error. + +**Examples** + +In this scenario, `User.fullName` is defined in **Schema A** but overridden in +**Schema B**. Since `@override` is **not** combined with any of `@external` on +the same field, no collision occurs. + +```graphql example +# Source Schema A +type User { + id: ID! + fullName: String +} + +# Source Schema B +type User { + id: ID! + fullName: String @override(from: "SchemaA") +} +``` + +Here, `amount` is marked with both `@override` and `@external`. This violates +the rule because the field is simultaneously labeled as “override from another +schema” and “external” in the local schema, producing an +`OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE` error. + +```graphql counter-example +# Source Schema A +type Payment { + id: ID! + amount: Int +} + +# Source Schema B +type Payment { + id: ID! + amount: Int @override(from: "SchemaA") @external +} +``` + +### Override from Self Error + +**Error Code** +`OVERRIDE_FROM_SELF_ERROR` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas to be composed. +- For each {schema} in {schemas}: + - Let {types} be the set of all composite types in {schema}. + - For each {type} in {types}: + - Let {fields} be the set of fields on {type}. + - For each {field} in {fields}: + - If {field} is annotated with `@override`: + - Let {from} be the value of the `from` argument of the `@override` + directive on {field}. + - {from} must **not** be the same as the name of {schema}: + +**Explanatory Text** + +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`. + +**Examples** + +In the following example, **Schema B** overrides the field `amount` from +**Schema A**. The two schema names are different, so no error is raised. + +```graphql example +# Source Schema A +type Bill { + id: ID! + amount: Int +} + +# Source Schema B +type Bill { + id: ID! + amount: Int @override(from: "SchemaA") +} +``` + +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`. + +```graphql counter-example +# Source Schema A (named "SchemaA") +type Bill { + id: ID! + amount: Int @override(from: "SchemaA") +} +``` + +### Override on Interface + +**Error Code** +`OVERRIDE_ON_INTERFACE` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas to be composed. +- For each {schema} in {schemas}: + - Let {types} be the set of all interface types in {schema}. + - For each {type} in {types}: + - Let {fields} be the set of fields on {type}. + - For each {field} in {fields}: + - {field} must **not** be annotated with `@override` + +**Explanatory Text** + +The `@override` directive designates that ownership of a field is transferred +from one source schema to another. In the context of interface types, fields are +abstract—objects that implement the interface are responsible for providing the +actual fields. Consequently, it is invalid to attach `@override` directly to an +interface field. Doing so leads to an `OVERRIDE_ON_INTERFACE` error because +there is no concrete field implementation on the interface itself that can be +overridden. + +**Examples** + +In this valid example, `@override` is used on a field of an object type, +ensuring that the field definition is concrete and can be reassigned to another +schema. + +Since `@override` is **not** used on any interface fields, no error is produced. + +```graphql example +# Source Schema A +type Order { + id: ID! + amount: Int +} + +# Source Schema B +type Order { + id: ID! + amount: Int @override(from: "SchemaA") +} +``` + +In the following counter-example, `Bill.amount` is declared on an **interface** +type and annotated with `@override`. This violates the rule because the +interface field itself is not eligible for ownership transfer. The composition +fails with an `OVERRIDE_ON_INTERFACE` error. + +```graphql counter-example +# Source Schema A +interface Bill { + id: ID! + amount: Int @override(from: "SchemaB") +} +``` + +### Override Source Has Override + +**Error Code** +`OVERRIDE_SOURCE_HAS_OVERRIDE` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas to be composed. +- Let {groupedTypes} be a map grouping all object types from {schemas} by their + type name. +- For each {typeGroup} in {groupedTypes}: + - Let {types} be the set of object types in {typeGroup}. + - Let {groupedFields} be a map grouping every field across all {types} by + their field name. + - For each {fieldGroup} in {groupedFields}: + - Let {fields} be the set of field definitions in {fieldGroup}. + - Let {overrides} be the list of `@override` directives present among those + {fields}. + - If {overrides} has fewer than 2 elements: + - Continue + - Let {firstOverride} be the first directive in {overrides}. + - Let {from} be the value of the `from` argument on {firstOverride}. + - Let {sourceSchema} be the schema definining {firstOverride}. + - Let {visited} be an empty set. + - Add {sourceSchema} to {visited}. + - While {from} is not null: + - {from} must **not** be in {visited}. + - Add {from} to {visited}. + - Let {sourceField} be the field in {fields} that belongs to the schema + named {from}. + - If {sourceField} does not exist: + - Break + - If {sourceField} is **not** annotated with `@override`: + - Break + - Let {from} be the value of the `from` argument on that `@override` + directive. + - The size of {visited} must be equal to the size of {overrides}. + +**Explanatory Text** + +A field marked with `@override` signifies that its ownership is being taken over +by another schema. If multiple schemas try to override the same field, or if the +ownership chain loops back on itself, the composed schema has more than one +`@override` for a single field. This creates ambiguity about which schema +ultimately owns that field. + +Hence, **only one** `@override` may ever apply to a particular field across all +source schemas. Attempting multiple overrides, or forming any cycle of overrides +for the same field, triggers the `OVERRIDE_SOURCE_HAS_OVERRIDE` error. + +**Examples** + +In this scenario, `Bill.amount` is originally owned by **Schema A** but is +overridden in **Schema B**. No other schema further attempts to override the +same field, so the composition is valid. + +```graphql example +# Source Schema A +type Bill { + id: ID! + amount: Int +} + +# Source Schema B +type Bill { + id: ID! + amount: Int @override(from: "SchemaA") +} +``` + +Here, **Schema A** overrides `Bill.amount` from **Schema B**, while **Schema B** +also overrides the same field from **Schema A**. This circular override makes it +impossible to discern a single “owner” of the field `Bill.amount`, raising an +`OVERRIDE_SOURCE_HAS_OVERRIDE` error. + +```graphql counter-example +# Source Schema A (named "SchemaA") +type Bill { + id: ID! + amount: Int @override(from: "SchemaB") +} + +# Source Schema B (named "SchemaB") +type Bill { + id: ID! + amount: Int @override(from: "SchemaA") +} +``` + +In this case, the same field `Bill.amount` is overridden successively by A, then +B, then C. Tracing these overrides forms a cycle (A → B → C → A). This again +produces an `OVERRIDE_SOURCE_HAS_OVERRIDE` error. + +```graphql counter-example +# Source Schema A (named "A") +type Bill { + id: ID! + amount: Int @override(from: "B") +} + +# Source Schema B (named "B") +type Bill { + id: ID! + amount: Int @override(from: "C") +} + +# Source Schema C (named "C") +type Bill { + id: ID! + amount: Int @override(from: "A") +} +``` + +In the following counter-example, the field `Bill.amount` is overridden by +multiple schemas. The overrides do not form a cycle, hence there are multiple +overrides for the same field, triggering an `OVERRIDE_SOURCE_HAS_OVERRIDE` +error. + +```graphql counter-example +# Source Schema A +type Bill { + id: ID! + amount: Int @override(from: "SchemaC") +} + +# Source Schema B +type Bill { + id: ID! + amount: Int @override(from: "SchemaC") +} + +# Source Schema C +type Bill { + id: ID! + amount: Int +} +``` + +### External Collision with Another Directive + +**Error Code** +`EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas to be composed. +- For each {schema} in {schemas}: + - Let {types} be the set of all composite types in {schema}. + - For each {type} in {types}: + - Let {fields} be the set of fields on {type}. + - For each {field} in {fields}: + - If {field} is annotated with `@external`: + - For each {argument} in {field}: + - {argument} must **not** be annotated with `@require` + - {field} must **not** be annotated with `@provides` + +**Explanatory Text** + +The `@external` directive indicates that a field is **defined** in a different +source schema, and the current schema merely references it. Therefore, a field +marked with `@external` must **not** simultaneously carry directives that assume +local ownership or resolution responsibility, such as: + +- **`@provides`**: Declares that the field can supply additional nested fields + from the local schema, which conflicts with the notion of an external field + whose definition resides elsewhere. + +- **`@require`**: Specifies dependencies on other fields to resolve this field. + Since `@external` fields are not locally resolved, there is no need for + `@require`. + +- **`@override`**: Transfers ownership of the field’s definition from one schema + to another, which is incompatible with an already-external field definition. + Yet this behaviour is covered by the + `OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE` rule. + +Any combination of `@external` with either `@provides` or `@require` on the same +field results in inconsistent semantics. In such scenarios, an +`EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE` error is raised. + +**Examples** + +In this example, `method` is **only** annotated with `@external` in Schema B, +without any other directive. This usage is valid. + +```graphql example +# Source Schema A +type Payment { + id: ID! + method: String +} + +# Source Schema B +type Payment { + id: ID! + # This field is external, defined in Schema A. + method: String @external +} +``` + +In this counter-example, `description` is annotated with `@external` and also +with `@provides`. Because `@external` and `@provides` cannot co-exist on the +same field, an `EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE` error is produced. + +```graphql counter-example +# Source Schema A +type Invoice { + id: ID! + description: String +} + +# Source Schema B +type Invoice { + id: ID! + description: String @external @provides(fields: "length") +} +``` + +The following example is invalid, since `title` is marked with both `@external` +and has an argument that is annotated with `@require`. This conflict leads to an +`EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE` error. + +```graphql counter-example +# Source Schema A +type Book { + id: ID! + title: String + subtitle: String +} + +# Source Schema B +type Book { + id: ID! + title(subtitle: String @require(fields: "subtitle")) @external +} +``` + +### Key Invalid Fields Type + +**Error Code** +`KEY_INVALID_FIELDS_TYPE` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas to be composed. +- For each {schema} in {schemas}: + - Let {types} be the set of all composite types in {schema}. + - For each {type} in {types}: + - If {type} is annotated with `@key`: + - Let {fieldsArg} be the value of the `fields` argument in the `@key` + directive. + - {fieldsArg} must be a string. + +**Explanatory Text** + +The `@key` directive designates the fields used to identify a particular object +uniquely. The `fields` argument accepts a **string** that represents a selection +set (for example, `"id"`, or `"id otherField"`). If the `fields` argument is +provided as any non-string type (e.g., `Boolean`, `Int`, `Array`), the schema +fails to compose correctly because it cannot parse a valid field selection. + +**Examples** + +In this example, the `@key` directive’s `fields` argument is the string +`"id uuid"`, identifying two fields that form the object key. This usage is +valid. + +```graphql example +type User @key(fields: "id uuid") { + id: ID! + uuid: ID! + name: String +} + +type Query { + users: [User] +} +``` + +Here, the `fields` argument is provided as a boolean (`true`) instead of a +string. This violates the directive requirement and triggers a +`KEY_INVALID_FIELDS_TYPE` error. + +```graphql counter-example +type User @key(fields: true) { + id: ID +} +``` + +### Provides Invalid Fields Type + +**Error Code** +`PROVIDES_INVALID_FIELDS_TYPE` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas to be composed. +- For each {schema} in {schemas}: + - Let {types} be the set of all composite types in {schema}. + - For each {type} in {types}: + - Let {fields} be the set of fields on {type}. + - For each {field} in {fields}: + - If {field} is annotated with `@provides`: + - Let {fieldsArg} be the value of the `fields` argument on the + `@provides` directive. + - {fieldsArg} must be a string. + +**Explanatory Text** + +The `@provides` directive indicates that a field is **providing** one or more +additional fields on the returned (child) type. The `fields` argument accepts a +**string** representing a GraphQL selection set (for example, `"title author"`). +If the `fields` argument is given as a non-string type (e.g., `Boolean`, `Int`, +`Array`), the schema fails to compose because it cannot interpret a valid +selection set. + +**Examples** + +In this valid example, the `@provides` directive on `details` uses the string +`"features specifications"` to specify that both fields are provided in the +child type `ProductDetails`. + +```graphql example +type Product { + id: ID! + details: ProductDetails @provides(fields: "features specifications") +} + +type ProductDetails { + features: [String] + specifications: String +} + +type Query { + products: [Product] +} +``` + +Here, the `@provides` directive includes a numeric value (`123`) instead of a +string in its `fields` argument. This invalid usage raises a +`PROVIDES_INVALID_FIELDS_TYPE` error. + +```graphql counter-example +type Product { + id: ID! + details: ProductDetails @provides(fields: 123) +} + +type ProductDetails { + features: [String] + specifications: String +} +``` + +### Provides on Non-Composite Field + +**Error Code** +`PROVIDES_ON_NON_COMPOSITE_FIELD` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas to be composed. +- For each {schema} in {schemas}: + - Let {types} be the set of all object and interface types in {schema}. + - For each {type} in {types}: + - Let {fields} be the set of fields on {type}. + - For each {field} in {fields}: + - If {field} is annotated with `@provides`: + - Let {fieldType} be the base return type of {field} (i.e., unwrapped of + any `[ ]` or `!`). + - {fieldType} must be a interface or object type. + +**Explanatory Text** + +The `@provides` directive allows a field to “provide” additional nested fields +on the composite type it returns. If a field’s base type is not an object or +interface type (e.g., `String`, `Int`, `Boolean`, `Enum`, `Union`, or an `Input` +type), it cannot hold nested fields for `@provides` to select. Consequently, +attaching `@provides` to such a field is invalid and raises a +`PROVIDES_ON_NON_OBJECT_FIELD` error. + +**Examples** + +Here, `profile` has an **object** base type `Profile`. The `@provides` directive +can validly specify sub-fields like `settings { theme }`. + +```graphql example +type Profile { + email: String + settings: Settings +} + +type Settings { + notificationsEnabled: Boolean + theme: String +} + +type User { + id: ID! + profile: Profile @provides(fields: "settings { theme }") +} +``` + +In this counter-example, `email` has a scalar base type (`String`). Because +scalars do not expose sub-fields, attaching `@provides` to `email` triggers a +`PROVIDES_ON_NON_OBJECT_FIELD` error. + +```graphql counter-example +type User { + id: ID! + email: String @provides(fields: "length") +} +``` + +### External on Interface + +**Error Code** +`EXTERNAL_ON_INTERFACE` + +**Severity** +ERROR + +**Formal Specification** + +- Let {schemas} be the set of all source schemas to be composed. +- For each {schema} in {schemas}: + - Let {types} be the set of all composite types in {schema}. + - For each {type} in {types}: + - If {type} is an interface type: + - Let {fields} be the set of fields on {type}. + - For each {field} in {fields}: + - {field} must **not** be annotated with `@external` + +**Explanatory Text** + +The `@external` directive indicates that a field is **defined** and **resolved** +elsewhere, not in the current schema. In the case of an **interface** type, +fields are **abstract** - they do not have direct resolutions at the interface +level. Instead, each implementing object type provides the concrete field +implementations. Marking an **interface** field with `@external` is therefore +nonsensical, as there is no actual field resolution in the interface itself to +“borrow” from another schema. Such usage raises an `EXTERNAL_ON_INTERFACE` +error. + +**Examples** + +Here, the interface `Node` merely describes the field `id`. Object types `User` +and `Product` implement and resolve `id`. No `@external` usage occurs on the +interface itself, so no error is triggered. + +```graphql example +interface Node { + id: ID! +} + +type User implements Node { + id: ID! + name: String +} + +type Product implements Node { + id: ID! + price: Int +} +``` + +Since `id` is declared on an **interface** and marked with `@external`, the +composition fails with `EXTERNAL_ON_INTERFACE`. An interface does not own the +concrete field resolution, so it is invalid to mark any of its fields as +external. + +```graphql counter-example +interface Node { + id: ID! @external +} +``` + ### Merge ### Post Merge Validation @@ -1390,16 +2198,16 @@ This rule ensures that the composed schema includes at least one accessible field on the root `Query` type. In GraphQL, the `Query` type is essential as it defines the entry points for -read operations. If none of the composed subgraphs expose any query fields, the +read operations. If none of the composed schemas expose any query fields, the composed schema would lack a root query, making it a invalid GraphQL schema. **Examples** -In this example, at least one subgraph provides accessible query fields, +In this example, at least one schema provides accessible query fields, satisfying the rule. ```graphql -# Subgraph A +# Schema A type Query { product(id: ID!): Product } @@ -1414,7 +2222,7 @@ type Query { review(id: ID!): Review } -# Subgraph B +# Schema B type Review { id: ID! content: String @@ -1425,13 +2233,13 @@ type Review { Even if some query fields are marked as `@inaccessible`, as long as there is at least one accessible query field in the composed schema, the rule is satisfied. -In this case, Subgraph A exposes an internal query field `internalData` marked -with `@inaccessible`, making it hidden in the composed schema. However, Subgraph -B provides an accessible `product` query field. Therefore, the composed schema -has at least one accessible query field, adhering to the rule. +In this case, Schema A exposes an internal query field `internalData` marked +with `@inaccessible`, making it hidden in the composed schema. However, Schema B +provides an accessible `product` query field. Therefore, the composed schema has +at least one accessible query field, adhering to the rule. ```graphql -# Subgraph A +# Schema A type Query { internalData: InternalData @inaccessible } @@ -1442,7 +2250,7 @@ type InternalData { ``` ```graphql -# Subgraph B +# Schema B type Query { product(id: ID!): Product } @@ -1453,17 +2261,17 @@ type Product { } ``` -If all query fields in all subgraphs are marked as `@inaccessible`, the composed +If all query fields in all schemas are marked as `@inaccessible`, the composed schema will lack accessible query fields, violating the rule. -In the following counter-example, both subgraphs have query fields, but all are +In the following counter-example, both schemas have query fields, but all are marked as `@inaccessible`. This means there are no accessible query fields in the composed schema, triggering the `NO_QUERIES` error. ```graphql -# Subgraph A +# Schema A type Query { internalData: InternalData @inaccessible } @@ -1474,7 +2282,7 @@ type InternalData { ``` ```graphql -# Subgraph B +# Schema B type Query { adminStats: AdminStats @inaccessible } @@ -2040,11 +2848,11 @@ enum DeliveryStatus { } ``` -### Requires Invalid Fields +### Require Invalid Fields **Error Code** -`REQUIRES_INVALID_FIELDS` +`REQUIRE_INVALID_FIELDS` **Severity** @@ -2058,10 +2866,10 @@ ERROR - Let {fields} be the set of fields on {composite}. - Let {arguments} be the set of all arguments on {fields}. - For each {argument} in {arguments}: - - If {argument} is **not** annotated with `@requires`: + - If {argument} is **not** annotated with `@require`: - Continue - Let {fieldsArg} be the string value of the `fields` argument of the - `@requires` directive on {argument}. + `@require` directive on {argument}. - Let {parsedFieldsArg} be the parsed selection map from {fieldsArg}. - {ValidateSelectionMap(parsedFieldsArg, parentType)} must be true. @@ -2082,22 +2890,22 @@ ValidateSelectionMap(selectionMap, parentType): **Explanatory Text** -Even if the selection map for `@requires(fields: "…")` is syntactically valid, +Even if the selection map for `@require(fields: "…")` is syntactically valid, its contents must also be valid within the composed schema. Fields must exist on -the parent type for them to be referenced by `@requires`. In addition, fields -requiring unknown fields break the valid usage of `@requires`, leading to a -`REQUIRES_INVALID_FIELDS` error. +the parent type for them to be referenced by `@require`. In addition, fields +requiring unknown fields break the valid usage of `@require`, leading to a +`REQUIRE_INVALID_FIELDS` error. **Examples** -In the following example, the `@requires` directive’s `fields` argument is a +In the following example, the `@require` directive’s `fields` argument is a valid selection set and satisfies the rule. ```graphql example type User @key(fields: "id") { id: ID! name: String! - profile(name: String! @requires(fields: "name")): Profile + profile(name: String! @require(fields: "name")): Profile } type Profile { @@ -2106,13 +2914,13 @@ type Profile { } ``` -In this counter-example, the `@requires` directive does not have a valid -selection set and triggers a `REQUIRES_INVALID_FIELDS` error. +In this counter-example, the `@require` directive does not have a valid +selection set and triggers a `REQUIRE_INVALID_FIELDS` error. ```graphql counter-example type Book { id: ID! - title(lang: String! @requires(fields: "author { }")): String + title(lang: String! @require(fields: "author { }")): String } type Author { @@ -2120,14 +2928,14 @@ type Author { } ``` -In this counter-example, the `@requires` directive references a field -(`unknown`) that does not exist on the parent type (`Book`), causing a -`REQUIRES_INVALID_FIELDS` error. +In this counter-example, the `@require` directive references a field (`unknown`) +that does not exist on the parent type (`Book`), causing a +`REQUIRE_INVALID_FIELDS` error. ```graphql counter-example type Book { id: ID! - pages(pageSize: Int @requires(fields: "unknownField")): Int + pages(pageSize: Int @require(fields: "unknownField")): Int } ```