diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs index fd9da00d0de..cdb3a1dc2b7 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs @@ -136,7 +136,7 @@ public static List GetPossibleFusionLookupDirectivesById( public static void RemoveUnreferencedDefinitions( this MutableSchemaDefinition schema, - ImmutableSortedSet sourceSchemas) + IReadOnlySet preservedTypeNames) { var touchedDefinitions = new HashSet(); var backlog = new Stack(); @@ -161,8 +161,6 @@ public static void RemoveUnreferencedDefinitions( backlog.Push(schema.SubscriptionType); } - var preservedTypeNames = GetPreservedTypeNames(sourceSchemas); - foreach (var typeName in preservedTypeNames) { if (schema.Types.TryGetType(typeName, out var inputType)) @@ -260,7 +258,8 @@ private static List GetFusionLookupDirectives( /// Returns a list of type names for types that must be preserved in the merged schema /// even if they are not directly referenced. /// - private static HashSet GetPreservedTypeNames(ImmutableSortedSet sourceSchemas) + public static HashSet GetPreservedTypeNames( + ImmutableSortedSet sourceSchemas) { var preservedTypeNames = new HashSet(); diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs index 1de9513be6b..16c9a451208 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs @@ -78,6 +78,18 @@ public CompositionResult Compose() return enrichmentResult.Errors; } + // Prune unreachable definitions from each source schema before validation, so types + // stripped by @excludeByTag (or otherwise unreferenced) are not validated or merged. + if (_schemaComposerOptions.Merger.RemoveUnreferencedDefinitions) + { + var preservedTypeNames = MutableSchemaDefinitionExtensions.GetPreservedTypeNames(schemas); + + foreach (var schema in schemas) + { + schema.RemoveUnreferencedDefinitions(preservedTypeNames); + } + } + // Validate Source Schemas var validationResult = new SourceSchemaValidator(schemas, s_sourceSchemaRules, _log).Validate(); diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/SourceSchemaMerger.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/SourceSchemaMerger.cs index 0d89e1e78d0..aa149d250e3 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/SourceSchemaMerger.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/SourceSchemaMerger.cs @@ -107,7 +107,8 @@ public CompositionResult Merge() // Remove unreferenced definitions. if (_options.RemoveUnreferencedDefinitions) { - mergedSchema.RemoveUnreferencedDefinitions(_schemas); + mergedSchema.RemoveUnreferencedDefinitions( + MutableSchemaDefinitionExtensions.GetPreservedTypeNames(_schemas)); } // Add Fusion definitions. diff --git a/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SchemaComposerTests.cs b/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SchemaComposerTests.cs index 480d3573f14..62dbd365be7 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SchemaComposerTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SchemaComposerTests.cs @@ -41,6 +41,76 @@ type Product { entry.Message); } + [Fact] + public void Compose_OrphanedTypeAfterTagExclusion_DoesNotProduceShareableError() + { + // arrange + // Schema A's Product is only reachable via Mutation. When Mutation is removed by + // tag exclusion, Product becomes orphaned. Without pruning, Product.name would + // collide with Schema B's Product.name and trigger an InvalidFieldSharing error. + var schemaComposer = new SchemaComposer( + [ + new SourceSchemaText( + "A", + """ + type Query { + book(id: ID!): Book @lookup + } + + type Mutation @tag(name: "internal") { + createProduct(name: String!): Product + } + + type Book { + id: ID! + title: String! + } + + type Product { + id: ID! + name: String! + } + + directive @tag(name: String!) repeatable on OBJECT + """), + new SourceSchemaText( + "B", + """ + type Query { + productById(id: ID!): Product @lookup + } + + type Product { + id: ID! + name: String! + } + """) + ], + new SchemaComposerOptions + { + Merger = { AddFusionDefinitions = false }, + SourceSchemas = + { + ["A"] = new SourceSchemaOptions + { + Preprocessor = new SourceSchemaPreprocessorOptions + { + ExcludeByTag = ["internal"] + } + } + } + }, + new CompositionLog()); + + // act + var result = schemaComposer.Compose(); + + // assert + Assert.True(result.IsSuccess); + Assert.False(result.Value.Types.ContainsName("Mutation")); + Assert.True(result.Value.Types.ContainsName("Product")); + } + [Fact] public void Compose_WithExtensions_AppliesExtensions() {