diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/Logging/LogEntryCodes.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/Logging/LogEntryCodes.cs index 12b557762de..7eda8a2d23f 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/Logging/LogEntryCodes.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/Logging/LogEntryCodes.cs @@ -70,5 +70,5 @@ public static class LogEntryCodes public const string RootSubscriptionUsed = "ROOT_SUBSCRIPTION_USED"; public const string SpecifiedByUrlMismatch = "SPECIFIED_BY_URL_MISMATCH"; public const string TypeKindMismatch = "TYPE_KIND_MISMATCH"; - public const string Unsatisfiable = "UNSATISFIABLE"; + public const string UnsatisfiableQueryPath = "UNSATISFIABLE_QUERY_PATH"; } diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs index ef914b839ff..e704399af17 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs @@ -238,7 +238,7 @@ private void VisitOutputField( _log.Write( LogEntryBuilder.New() .SetMessage(error.ToString()) - .SetCode(LogEntryCodes.Unsatisfiable) + .SetCode(LogEntryCodes.UnsatisfiableQueryPath) .SetSeverity(LogSeverity.Error) .SetExtension("error", error) .Build()); @@ -297,7 +297,7 @@ private void VisitNodeField( _log.Write( LogEntryBuilder.New() .SetMessage(error.ToString()) - .SetCode(LogEntryCodes.Unsatisfiable) + .SetCode(LogEntryCodes.UnsatisfiableQueryPath) .SetSeverity(LogSeverity.Error) .SetExtension("error", error) .Build()); diff --git a/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs b/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs index 159b3e7fcbc..9d1cc3b54e7 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs @@ -62,6 +62,7 @@ type User { // assert Assert.True(result.IsFailure); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot( """ Unable to access the field 'User.membershipStatus' on path 'A:Query.profileById -> A:Profile.user'. @@ -119,6 +120,7 @@ type Product { // assert Assert.True(result.IsFailure); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot( """ Unable to access the field 'Product.sku' on path 'B:Query.productById'. @@ -179,6 +181,7 @@ type Product { // assert Assert.True(result.IsFailure); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot( """ Unable to access the field 'Product.sku' on path 'A:Query.productById'. @@ -319,6 +322,7 @@ type Address @shareable { // assert Assert.True(result.IsFailure); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot( """ Unable to access the field 'Address.country' on path 'A:Query.userById -> A:User.address
'. @@ -375,6 +379,7 @@ type Category @key(fields: "id") { // assert Assert.True(result.IsFailure); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot( """ Unable to access the field 'Category.name' on path 'B:Query.categoryById'. @@ -1470,6 +1475,7 @@ type Product { // assert Assert.True(result.IsFailure); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot( """ Unable to access the field 'Product.sku' on path 'A:Query.productById'. @@ -1632,6 +1638,7 @@ interface Animal { // assert Assert.False(result.IsSuccess); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)) .MatchInlineSnapshot( """ @@ -1980,6 +1987,7 @@ type Dog { // assert Assert.False(result.IsSuccess); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)) .MatchInlineSnapshot( """ @@ -2032,6 +2040,7 @@ type Product { // assert Assert.True(result.IsFailure); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot( """ Unable to access the field 'Product.title' on path 'A:Query.productById'. @@ -2120,6 +2129,7 @@ type Section { // assert Assert.True(result.IsFailure); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot( """ Unable to access the field 'Product.title' on path 'A:Query.productById'. @@ -2300,6 +2310,7 @@ type Viewer { // assert Assert.True(result.IsFailure); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot( """ Unable to access the field 'Viewer.lastName' on path 'A:Query.viewer'. @@ -2360,6 +2371,7 @@ type Viewer { // assert Assert.True(result.IsFailure); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot( """ Unable to access the field 'Viewer.fullName' on path 'A:Query.viewer'. @@ -2529,6 +2541,7 @@ type Section { // assert Assert.True(result.IsFailure); + Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code)); string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot( """ Unable to access the field 'Category.name' on path 'A:Query.productById -> B:Product.category'. diff --git a/website/src/docs/fusion/v16/composition.md b/website/src/docs/fusion/v16/composition.md index f986e58913d..77f745a8d87 100644 --- a/website/src/docs/fusion/v16/composition.md +++ b/website/src/docs/fusion/v16/composition.md @@ -71,7 +71,7 @@ Validates the merged schema as a whole. The rules here treat the composed schema ### 8. Validate Satisfiability -Performs reachability analysis. Starting from the root types, the pipeline walks every reachable field in the merged schema and confirms it can be resolved by at least one subgraph given the available `@lookup` and `@key` paths. If a field is reachable from a query but no subgraph can produce it, satisfiability fails with `UNSATISFIABLE`. This is the last line of defense against shapes that look valid statically but cannot actually be served at runtime. +Performs reachability analysis. Starting from the root types, the pipeline walks every reachable field in the merged schema and confirms it can be resolved by at least one subgraph given the available `@lookup` and `@key` paths. If a field is reachable from a query but no subgraph can produce it, satisfiability fails with `UNSATISFIABLE_QUERY_PATH`. This is the last line of defense against shapes that look valid statically but cannot actually be served at runtime. ## Common Scenarios @@ -180,7 +180,7 @@ The placeholders `{0}`, `{1}`, etc. in the message column are replaced with the | [ROOT_QUERY_USED](https://graphql.github.io/composite-schemas-spec/draft/#sec-Root-Query-Used) | The root query type in schema '{0}' must be named 'Query'. | Fusion requires the root query type to use the canonical name `Query`. Rename the type in the named source schema. A type named `Query` must not exist unless it is the root query type. See [Getting Started](/docs/fusion/v16/getting-started) for the expected schema layout. | | [ROOT_SUBSCRIPTION_USED](https://graphql.github.io/composite-schemas-spec/draft/#sec-Root-Subscription-Used) | The root subscription type in schema '{0}' must be named 'Subscription'. | Fusion requires the root subscription type to use the canonical name `Subscription`. Rename the type in the named source schema. A type named `Subscription` must not exist unless it is the root subscription type. | | [TYPE_KIND_MISMATCH](https://graphql.github.io/composite-schemas-spec/draft/#sec-Type-Kind-Mismatch) | The type '{0}' has a different kind in schema '{1}' ({2}) than it does in schema '{3}' ({4}). | The same type name was used for different kinds (for example, an object in one source schema and an interface in another). Decide which kind is correct and update the other source schema, or rename one of the types so they no longer collide. | -| `UNSATISFIABLE` | (Message varies. Includes the unreachable field, the path the validator tried, and the lookups it considered.) | Some reachable field cannot be resolved through the available `@lookup` and `@key` paths. See [Diagnosing UNSATISFIABLE Errors](#diagnosing-unsatisfiable-errors) for how to read the message and fix the underlying gap. | +| [UNSATISFIABLE_QUERY_PATH](https://graphql.github.io/composite-schemas-spec/draft/#sec-Unsatisfiable-Query-Path) | (Message varies. Includes the unreachable field, the path the validator tried, and the lookups it considered.) | Some reachable field cannot be resolved through the available `@lookup` and `@key` paths. See [Diagnosing UNSATISFIABLE_QUERY_PATH Errors](#diagnosing-unsatisfiable_query_path-errors) for how to read the message and fix the underlying gap. | ### Warnings @@ -189,9 +189,9 @@ The placeholders `{0}`, `{1}`, etc. in the message column are replaced with the | [LOOKUP_RETURNS_NON_NULLABLE_TYPE](https://graphql.github.io/composite-schemas-spec/draft/#sec-Lookup-Returns-Non-Nullable-Type) | The lookup field '{0}' in schema '{1}' should return a nullable type. | Lookups should return nullable entities so the gateway can represent missing keys without throwing. Change the return type of the lookup field to nullable. See [Entities and Lookups](/docs/fusion/v16/entities-and-lookups). | | [SPECIFIED_BY_URL_MISMATCH](https://graphql.github.io/composite-schemas-spec/draft/#sec-SpecifiedBy-URL-Mismatch) | The scalar type '{0}' has a different specified-by URL in schema '{1}' ({2}) than it does in schema '{3}' ({4}). | The same scalar declares conflicting `@specifiedBy(url: ...)` URLs in different source schemas. Align the URL across all source schemas that define the scalar so the composed schema points to a single specification. | -### Diagnosing UNSATISFIABLE Errors +### Diagnosing UNSATISFIABLE_QUERY_PATH Errors -Most composition errors point at a single source schema or definition. `UNSATISFIABLE` is different. It is raised by the satisfiability validator (phase 8) and tells you that the merged schema as a whole has at least one reachable field that no source schema can serve through the available `@lookup` and `@key` paths. +Most composition errors point at a single source schema or definition. `UNSATISFIABLE_QUERY_PATH` is different. It is raised by the satisfiability validator (phase 8) and tells you that the merged schema as a whole has at least one reachable field that no source schema can serve through the available `@lookup` and `@key` paths. Because the validator weighs every option for reaching a field across every source schema, the diagnostic carries a tree of nested errors that explain why each candidate option failed. Reading that tree from the bottom up is the fastest way to find the gap. @@ -231,7 +231,7 @@ Set the option via the CLI flag during composition: nitro fusion compose --include-satisfiability-paths ``` -Paths add noise to short outputs and pay off on any non-trivial graph. Turn them on whenever an `UNSATISFIABLE` error is hard to triage. +Paths add noise to short outputs and pay off on any non-trivial graph. Turn them on whenever an `UNSATISFIABLE_QUERY_PATH` error is hard to triage. #### Common causes and fixes @@ -240,7 +240,7 @@ Paths add noise to short outputs and pay off on any non-trivial graph. Turn them - **`@require` reaches for a field that is not on the path.** A `@require(field: "...")` references parent fields that the validator cannot resolve given the path it took. Either expose the required field on the parent type along that path, or rewrite the requirement. - **`Node` interface without a node lookup.** A type implements `Node` but no source schema exposes a `node(id: ID!): Node` lookup that returns it. Add one in any source schema that owns the entity. -When you fix the root cause, both the top-level `UNSATISFIABLE` error and its nested children disappear together. +When you fix the root cause, both the top-level `UNSATISFIABLE_QUERY_PATH` error and its nested children disappear together. #### Temporarily ignoring a non-accessible field diff --git a/website/src/docs/fusion/v16/migration/migrate-from-15-to-16.md b/website/src/docs/fusion/v16/migration/migrate-from-15-to-16.md index cabe5038521..ffbb4041fb0 100644 --- a/website/src/docs/fusion/v16/migration/migrate-from-15-to-16.md +++ b/website/src/docs/fusion/v16/migration/migrate-from-15-to-16.md @@ -29,7 +29,7 @@ Validating Fusion configuration of API 'QXBpCmcwMTlkMmIzMGUzNGY3YzQ2OTBjNTgxOTNk ❌ [ERR] Unable to access the field 'Review.productVariant'. Unable to transition between schemas 'REVIEWS' and 'PRODUCTS' for access to field 'PRODUCTS:Review.productVariant'. - No lookups found for type 'Review' in schema 'PRODUCTS'. (UNSATISFIABLE) + No lookups found for type 'Review' in schema 'PRODUCTS'. (UNSATISFIABLE_QUERY_PATH) Satisfiability validation failed. --> @@ -410,7 +410,7 @@ public static class Query } ``` -Missing `@lookup` annotations will usually manifest as `UNSATISFIABLE` errors in the composition. See [Entities and lookups](/docs/fusion/v16/entities-and-lookups) for details. +Missing `@lookup` annotations will usually manifest as `UNSATISFIABLE_QUERY_PATH` errors in the composition. See [Entities and lookups](/docs/fusion/v16/entities-and-lookups) for details. If your graph has overlapping fields, i.e. multiple subgraphs providing the same field, you now also have to explicitly mark those fields as shareable from both sides.