diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs index 9c19eebbb5f..ca0dd6d36cf 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs @@ -1293,6 +1293,21 @@ ... @defer(label: "b") { "Expected both labeled deferred payloads to include product.name."); } + private static void AssertContainsOverlapIncrementalLegacyPayload(string content) + { + const string subPathPayload = + "\"incremental\":[{\"data\":{\"name\":\"Abc\",\"description\":\"Abc desc\",\"reviews\":[{\"rating\":5}]},\"path\":[\"product\"],\"label\":\"foo\"}]"; + + const string rootPathPayload = + "\"incremental\":[{\"data\":{\"product\":{\"name\":\"Abc\",\"description\":\"Abc desc\",\"reviews\":[{\"rating\":5}]}}," + + "\"path\":[],\"label\":\"foo\"}]"; + + Assert.True( + content.Contains(subPathPayload, StringComparison.Ordinal) + || content.Contains(rootPathPayload, StringComparison.Ordinal), + "Expected overlap incremental payload in either legacy-compatible shape."); + } + private TestServer CreateDeferServer( HttpTransportVersion serverTransportVersion = HttpTransportVersion.Latest) { diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/VariableCoercionHelper.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/VariableCoercionHelper.cs index 94a97c4b0d0..cd2607955da 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/VariableCoercionHelper.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/VariableCoercionHelper.cs @@ -150,7 +150,7 @@ private static IInputType AssertInputType( throw ThrowHelper.VariableIsNotAnInputType(variableDefinition); } - private IValueNode CoerceInputLiteral( + private static IValueNode CoerceInputLiteral( JsonElement inputValue, IInputType type, IFeatureProvider context, @@ -227,18 +227,20 @@ private IValueNode CoerceInputLiteral( } case TypeKind.List: - if (inputValue.ValueKind is not JsonValueKind.Array) - { - throw new InvalidOperationException(); - } - var items = new List(); var elementType = type.ElementType().EnsureInputType(); var elementDepth = depth + 1; - foreach (var item in inputValue.EnumerateArray()) + if (inputValue.ValueKind is JsonValueKind.Array) + { + foreach (var item in inputValue.EnumerateArray()) + { + items.Add(CoerceInputLiteral(item, elementType, context, elementDepth)); + } + } + else { - items.Add(CoerceInputLiteral(item, elementType, context, elementDepth)); + items.Add(CoerceInputLiteral(inputValue, elementType, context, elementDepth)); } return new ListValueNode(items); diff --git a/src/HotChocolate/Core/src/Types/Types/InputParser.cs b/src/HotChocolate/Core/src/Types/Types/InputParser.cs index cc12911432b..a4e15986273 100644 --- a/src/HotChocolate/Core/src/Types/Types/InputParser.cs +++ b/src/HotChocolate/Core/src/Types/Types/InputParser.cs @@ -475,21 +475,24 @@ private object DeserializeList( InputField? field, IFeatureProvider context) { + var list = CreateList(type); + if (inputValue.ValueKind is JsonValueKind.Array) { - var list = CreateList(type); - var i = 0; foreach (var element in inputValue.EnumerateArray()) { var newPath = path.Append(i++); list.Add(Deserialize(element, type.ElementType, newPath, field, context)); } - - return list; + } + else + { + var newPath = path.Append(0); + list.Add(Deserialize(inputValue, type.ElementType, newPath, field, context)); } - throw ParseList_InvalidValueKind(type, path); + return list; } private object DeserializeObject(JsonElement inputValue, InputObjectType type, Path path, IFeatureProvider context) diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs index 9c02a7321b9..37be9a71ce7 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs @@ -574,6 +574,107 @@ void Action() => helper.CoerceVariableValues( """); } + [Fact] + public void Single_Value_Can_Be_Coerced_Into_List_Variable() + { + // arrange + var schema = SchemaBuilder.New().AddStarWarsTypes().Create(); + + var variableDefinitions = new List + { + new(null, + new VariableNode("abc"), + description: null, + new ListTypeNode(new NamedTypeNode("String")), + null, + Array.Empty()) + }; + + var variableValues = JsonDocument.Parse("""{"abc": "xyz"}"""); + var coercedValues = new Dictionary(); + var featureProvider = new MockFeatureProvider(); + var helper = new VariableCoercionHelper(new()); + + // act + helper.CoerceVariableValues( + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); + + // assert + var entry = Assert.Single(coercedValues); + Assert.Equal("abc", entry.Key); + + var runtimeValues = Assert.IsAssignableFrom(entry.Value.RuntimeValue); + var runtimeValue = Assert.Single(runtimeValues.Cast()); + Assert.Equal("xyz", runtimeValue); + + entry.Value.ValueLiteral.MatchInlineSnapshot( + """ + [ + "xyz" + ] + """); + } + + [Fact] + public void StringValue_Representing_EnumValue_In_Single_Object_For_List_ShouldBe_Rewritten() + { + // arrange + var schema = SchemaBuilder.New() + .AddDocumentFromString( + @" + type Query { + test(list: [FooInput]): String + } + + input FooInput { + enum: TestEnum + } + + enum TestEnum { + Foo + Bar + }") + .Use(_ => _ => default) + .Create(); + + var variableDefinitions = new List + { + new(null, + new VariableNode("abc"), + description: null, + new ListTypeNode(new NamedTypeNode("FooInput")), + null, + Array.Empty()) + }; + + var variableValues = JsonDocument.Parse( + """ + { + "abc": { "enum": "Foo" } + } + """); + + var coercedValues = new Dictionary(); + var featureProvider = new MockFeatureProvider(); + var helper = new VariableCoercionHelper(new()); + + // act + helper.CoerceVariableValues( + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); + + // assert + var entry = Assert.Single(coercedValues); + Assert.Equal("abc", entry.Key); + entry.Value.ValueLiteral.MatchInlineSnapshot( + """ + [ + { + enum: Foo + } + ] + """); + } + [Fact] public void StringValues_Representing_EnumValues_In_Lists_ShouldBe_Rewritten() { diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/InputParserTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/InputParserTests.cs index 02cd1b9266f..f0c0733e2e6 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/InputParserTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/InputParserTests.cs @@ -400,6 +400,25 @@ public void Parse_InputObject_NullableEnumList() t => Assert.Equal(Bar.Baz, t)); } + [Fact] + public void Deserialize_List_Can_Be_Coerced_From_Single_Value() + { + // arrange + var parser = new InputParser(new DefaultTypeConverter()); + var type = (IType)new ListType(new BooleanType()); + var inputValue = JsonDocument.Parse("true"); + + var context = new Mock(); + context.Setup(t => t.Features).Returns(FeatureCollection.Empty); + + // act + var runtimeValue = + parser.ParseInputValue(inputValue.RootElement, type, context.Object, Path.Root.Append("root")); + + // assert + Assert.Collection(Assert.IsType>(runtimeValue), Assert.True); + } + [Fact] public async Task Integration_InputObjectDefaultValue_ValueIsInitialized() {