diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs index 8c7f02c427a..fd9da00d0de 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs @@ -136,7 +136,7 @@ public static List GetPossibleFusionLookupDirectivesById( public static void RemoveUnreferencedDefinitions( this MutableSchemaDefinition schema, - HashSet preserveInputTypeNames) + ImmutableSortedSet sourceSchemas) { var touchedDefinitions = new HashSet(); var backlog = new Stack(); @@ -161,6 +161,16 @@ 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)) + { + backlog.Push(inputType); + } + } + while (backlog.TryPop(out var type)) { if (!touchedDefinitions.Add(type)) @@ -199,7 +209,7 @@ public static void RemoveUnreferencedDefinitions( var definitionsToRemove = new HashSet(); foreach (var type in schema.Types) { - if (touchedDefinitions.Contains(type) || preserveInputTypeNames.Contains(type.NamedType().Name)) + if (touchedDefinitions.Contains(type)) { continue; } @@ -246,6 +256,38 @@ private static List GetFusionLookupDirectives( .ToList(); } + /// + /// 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) + { + var preservedTypeNames = new HashSet(); + + foreach (var schema in sourceSchemas) + { + foreach (var type in schema.Types.OfType()) + { + foreach (var field in type.Fields) + { + foreach (var argument in field.Arguments) + { + var argumentInnerType = argument.Type.InnerType(); + + if ((argument.HasRequireDirective || field.IsLookup) + && argumentInnerType is INameProvider { Name: var name } + && !SpecScalarNames.IsSpecScalar(name)) + { + preservedTypeNames.Add(name); + } + } + } + } + } + + return preservedTypeNames; + } + private static void InspectComplexType( ISchemaDefinition schema, IComplexTypeDefinition complexType, diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs index 7498f629f4d..90a83330f44 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs @@ -99,7 +99,7 @@ public CompositionResult Merge() // Remove unreferenced definitions. if (_options.RemoveUnreferencedDefinitions) { - mergedSchema.RemoveUnreferencedDefinitions(GetPreservedInputTypeNames()); + mergedSchema.RemoveUnreferencedDefinitions(_schemas); } // Add Fusion definitions. @@ -273,37 +273,6 @@ private void AddNodeField(MutableSchemaDefinition mergedSchema) } } - /// - /// Returns a list of input type names for types that must be preserved in the merged schema - /// even if they are not directly referenced. - /// - private HashSet GetPreservedInputTypeNames() - { - var preservedInputTypeNames = new HashSet(); - - foreach (var schema in _schemas) - { - foreach (var type in schema.Types.OfType()) - { - foreach (var field in type.Fields) - { - foreach (var argument in field.Arguments) - { - var argumentInnerType = argument.Type.InnerType(); - - if (argumentInnerType is IInputObjectTypeDefinition inputObjectType - && (argument.HasRequireDirective || field.IsLookup)) - { - preservedInputTypeNames.Add(inputObjectType.Name); - } - } - } - } - } - - return preservedInputTypeNames; - } - /// /// Takes two arguments with the same name but possibly differing in type, description, or /// default value, and returns a single, unified argument definition. diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/SourceSchemaMergerTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/SourceSchemaMergerTests.cs index bf6bec9be74..e61b2396916 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/SourceSchemaMergerTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/SourceSchemaMergerTests.cs @@ -112,6 +112,55 @@ public void Merge_FourNamedSchemas_AddsFusionDefinitions() SchemaFormatter.FormatAsString(result.Value).MatchSnapshot(extension: ".graphql"); } + [Fact] + public void Merge_WithRequireCustomScalar_RetainsScalarType() + { + // arrange + var sourceSchemaTextA = + new SourceSchemaText( + "A", + """ + type Query { + product: Product + } + + type Product { + weight: Int! + } + """); + var sourceSchemaTextB = + new SourceSchemaText( + "B", + """ + type Product { + deliveryEstimate( + zip: String! + weight: Weight! @require(field: "weight") + ): Int! + } + + scalar Weight + """); + var compositionLog = new CompositionLog(); + var sourceSchemaParser1 = new SourceSchemaParser(sourceSchemaTextA, compositionLog); + var sourceSchemaParser2 = new SourceSchemaParser(sourceSchemaTextB, compositionLog); + var schema1 = sourceSchemaParser1.Parse().Value; + var schema2 = sourceSchemaParser2.Parse().Value; + var schemas = + ImmutableSortedSet.Create( + new SchemaByNameComparer(), schema1, schema2); + new SourceSchemaEnricher(schema1, schemas).Enrich(); + new SourceSchemaEnricher(schema2, schemas).Enrich(); + var merger = new SourceSchemaMerger(schemas); + + // act + var result = merger.Merge(); + + // assert + Assert.True(result.IsSuccess); + Assert.True(result.Value.Types.ContainsName("Weight")); + } + [Fact] public void Merge_WithRequireInputObject_RetainsInputObjectType() { @@ -162,4 +211,58 @@ input ProductDimensionInput @inaccessible { Assert.True(result.IsSuccess); Assert.True(result.Value.Types.ContainsName("ProductDimensionInput")); } + + [Fact] + public void Merge_WithRequireInputObject_ThatUsesCustomScalar_RetainsScalarDependency() + { + // arrange + var sourceSchemaTextA = + new SourceSchemaText( + "A", + """ + type Query { + product: Product + } + + type Product { + weight: Int! + } + """); + var sourceSchemaTextB = + new SourceSchemaText( + "B", + """ + type Product { + deliveryEstimate( + zip: String! + dimension: ProductDimensionInput! @require(field: "{ weight }") + ): Int! + } + + input ProductDimensionInput @inaccessible { + coordinate: Position! + } + + scalar Position + """); + var compositionLog = new CompositionLog(); + var sourceSchemaParser1 = new SourceSchemaParser(sourceSchemaTextA, compositionLog); + var sourceSchemaParser2 = new SourceSchemaParser(sourceSchemaTextB, compositionLog); + var schema1 = sourceSchemaParser1.Parse().Value; + var schema2 = sourceSchemaParser2.Parse().Value; + var schemas = + ImmutableSortedSet.Create( + new SchemaByNameComparer(), schema1, schema2); + new SourceSchemaEnricher(schema1, schemas).Enrich(); + new SourceSchemaEnricher(schema2, schemas).Enrich(); + var merger = new SourceSchemaMerger(schemas); + + // act + var result = merger.Merge(); + + // assert + Assert.True(result.IsSuccess); + Assert.True(result.Value.Types.ContainsName("ProductDimensionInput")); + Assert.True(result.Value.Types.ContainsName("Position")); + } }