diff --git a/src/HotChocolate/Core/HotChocolate.Core.sln b/src/HotChocolate/Core/HotChocolate.Core.sln index 25f57accf19..40b1b53df28 100644 --- a/src/HotChocolate/Core/HotChocolate.Core.sln +++ b/src/HotChocolate/Core/HotChocolate.Core.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29721.120 @@ -99,7 +98,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.Mutation EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.Mutations.Tests", "test\Types.Mutations.Tests\HotChocolate.Types.Mutations.Tests.csproj", "{F6FFA925-8C49-4594-9FF2-8F571C2D6389}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types", "src\Types\HotChocolate.Types.csproj", "{08A8915E-CFB2-46F6-9D24-CCB28BDA1446}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types", "src\Types\HotChocolate.Types.csproj", "{151FB103-4BCA-41D2-9C8E-C5BDA7A68320}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -627,18 +626,18 @@ Global {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Release|x64.Build.0 = Release|Any CPU {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Release|x86.ActiveCfg = Release|Any CPU {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Release|x86.Build.0 = Release|Any CPU - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446}.Debug|x64.ActiveCfg = Debug|Any CPU - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446}.Debug|x64.Build.0 = Debug|Any CPU - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446}.Debug|x86.ActiveCfg = Debug|Any CPU - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446}.Debug|x86.Build.0 = Debug|Any CPU - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446}.Release|Any CPU.Build.0 = Release|Any CPU - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446}.Release|x64.ActiveCfg = Release|Any CPU - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446}.Release|x64.Build.0 = Release|Any CPU - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446}.Release|x86.ActiveCfg = Release|Any CPU - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446}.Release|x86.Build.0 = Release|Any CPU + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Debug|Any CPU.Build.0 = Debug|Any CPU + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Debug|x64.ActiveCfg = Debug|Any CPU + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Debug|x64.Build.0 = Debug|Any CPU + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Debug|x86.ActiveCfg = Debug|Any CPU + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Debug|x86.Build.0 = Debug|Any CPU + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Release|Any CPU.ActiveCfg = Release|Any CPU + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Release|Any CPU.Build.0 = Release|Any CPU + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Release|x64.ActiveCfg = Release|Any CPU + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Release|x64.Build.0 = Release|Any CPU + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Release|x86.ActiveCfg = Release|Any CPU + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -687,7 +686,7 @@ Global {4AD904D1-7727-48EF-88D9-7B38D98EB314} = {7637D30E-7339-4D4E-9424-87CF2394D234} {2B7E7416-E093-4763-B839-504CBFBC8F1C} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} {F6FFA925-8C49-4594-9FF2-8F571C2D6389} = {7462D089-D350-44D6-8131-896D949A65B7} - {08A8915E-CFB2-46F6-9D24-CCB28BDA1446} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} + {151FB103-4BCA-41D2-9C8E-C5BDA7A68320} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E4D94C77-6657-4630-9D42-0A9AC5153A1B} diff --git a/src/HotChocolate/Core/src/Types.Mutations/Payload/PayloadAttribute.cs b/src/HotChocolate/Core/src/Types.Mutations/Payload/PayloadAttribute.cs index 21f0c034805..924b2f2bca7 100644 --- a/src/HotChocolate/Core/src/Types.Mutations/Payload/PayloadAttribute.cs +++ b/src/HotChocolate/Core/src/Types.Mutations/Payload/PayloadAttribute.cs @@ -1,6 +1,3 @@ -using System.Reflection; -using HotChocolate.Types.Descriptors; - #nullable enable namespace HotChocolate.Types; diff --git a/src/HotChocolate/Core/src/Types.Mutations/Payload/PayloadMiddleware.cs b/src/HotChocolate/Core/src/Types.Mutations/Payload/PayloadMiddleware.cs index f73ca303f06..ede139a4866 100644 --- a/src/HotChocolate/Core/src/Types.Mutations/Payload/PayloadMiddleware.cs +++ b/src/HotChocolate/Core/src/Types.Mutations/Payload/PayloadMiddleware.cs @@ -1,6 +1,3 @@ -using System.Threading.Tasks; -using HotChocolate.Resolvers; - #nullable enable namespace HotChocolate.Types; diff --git a/src/HotChocolate/Core/src/Types.Mutations/Payload/PayloadTypeInterceptor.cs b/src/HotChocolate/Core/src/Types.Mutations/Payload/PayloadTypeInterceptor.cs index d4542c8d150..7cb85f323db 100644 --- a/src/HotChocolate/Core/src/Types.Mutations/Payload/PayloadTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types.Mutations/Payload/PayloadTypeInterceptor.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using HotChocolate.Configuration; using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors; @@ -20,45 +21,60 @@ public override void OnBeforeRegisterDependencies( return; } - foreach (var field in def.Fields) + foreach (ObjectFieldDefinition? field in def.Fields) { - if (field.ContextData.TryGetValue(PayloadContextData.Payload, out var contextObj) && - contextObj is PayloadContextData context && - field.Type is { }) + if (!(field.ContextData.TryGetValue(PayloadContextData.Payload, out var contextObj) && + contextObj is PayloadContextData context && + field.Type is { })) { - string name = - context.FieldName ?? - field.ResultType?.Name.ToFieldName() ?? - "payload"; + continue; + } + + ITypeReference? fieldType = field.Type; + + FieldMiddlewareDefinition middlewareDefinition = + new(FieldClassMiddlewareFactory.Create(), + false, + PayloadMiddleware.MiddlewareIdentifier); + + field.MiddlewareDefinitions.Insert(0, middlewareDefinition); - ITypeReference? fieldType = field.Type; + NameString typeName = + context.TypeName ?? field.Name.ToTypeName(suffix: "Payload"); - FieldMiddlewareDefinition middlewareDefinition = - new(FieldClassMiddlewareFactory.Create(), - false, - PayloadMiddleware.MiddlewareIdentifier); + field.Type = new DependantFactoryTypeReference( + typeName, + fieldType, + CreateType, + TypeContext.Output); - field.MiddlewareDefinitions.Insert(0, middlewareDefinition); + TypeSystemObjectBase CreateType(IDescriptorContext descriptorContext) => + new ObjectType(descriptor => + { + descriptor.Name(typeName); - NameString typeName = context.TypeName ?? field.Name.ToTypeName(suffix: "Payload"); + const string placeholder = "payload"; - field.Type = new DependantFactoryTypeReference( - typeName, - fieldType, - CreateType, - TypeContext.Output); + IObjectFieldDescriptor resultField = + descriptor.Field(x => x.Result).Name(placeholder); - TypeSystemObjectBase CreateType(IDescriptorContext descriptorContext) => - new ObjectType(descriptor => + resultField.Extend().OnBeforeCreate(x => x.Type = fieldType); + resultField.Extend().OnBeforeNaming(OnBeforeNaming); + + void OnBeforeNaming( + ITypeCompletionContext ctx, + ObjectFieldDefinition fieldDefinition) { - descriptor.Name(typeName); - descriptor - .Field(x => x.Result) - .Name(name) - .Extend() - .Definition.Type = fieldType; - }); - } + if (context.FieldName is not null) + { + fieldDefinition.Name = context.FieldName; + return; + } + + IType type = ctx.GetType(fieldType); + fieldDefinition.Name = type.NamedType().Name.Value.ToFieldName(); + } + }); } } } diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/PayloadTests.cs b/src/HotChocolate/Core/test/Types.Mutations.Tests/PayloadTests.cs index 9ab48063346..1ac5ed06715 100644 --- a/src/HotChocolate/Core/test/Types.Mutations.Tests/PayloadTests.cs +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/PayloadTests.cs @@ -113,6 +113,115 @@ public async Task PayloadAttribute_Should_UserResultTypeForField_When_NoFieldNam executor.Schema.Print().MatchSnapshot(); } + [Fact] + public async Task PayloadMiddleware_Should_TransformPayload_When_ItIsATask() + { + // Arrange + IRequestExecutor executor = + await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .AddMutationType() + .EnableMutationConvention() + .BuildRequestExecutorAsync(); + + // Act + IExecutionResult res = await executor + .ExecuteAsync(@" + mutation { + createTask { + foo { + bar + } + } + createValueTask { + foo { + bar + } + } + createTaskNoName { + foo { + bar + } + } + createValueTaskNoName { + foo { + bar + } + } + } + "); + + // Assert + res.ToJson().MatchSnapshot(); + SnapshotFullName fullName = Snapshot.FullName(); + SnapshotFullName snapshotName = new(fullName.Filename + "_schema", fullName.FolderPath); + executor.Schema.Print().MatchSnapshot(snapshotName); + } + + [Fact] + public async Task PayloadMiddleware_Should_TransformPayload_When_ItIsNullable() + { + // Arrange + IRequestExecutor executor = + await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .AddMutationType() + .EnableMutationConvention() + .BuildRequestExecutorAsync(); + + // Act + IExecutionResult res = await executor + .ExecuteAsync(@" + mutation { + nullableNumber { + foo + } + nullableNumberNoName { + int + } + } + "); + + // Assert + res.ToJson().MatchSnapshot(); + SnapshotFullName fullName = Snapshot.FullName(); + SnapshotFullName snapshotName = new(fullName.Filename + "_schema", fullName.FolderPath); + executor.Schema.Print().MatchSnapshot(snapshotName); + } + + [Fact] + public async Task PayloadMiddleware_Should_TransformPayload_When_ObjectIsNamed() + { + // Arrange + IRequestExecutor executor = + await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .AddMutationType() + .EnableMutationConvention() + .BuildRequestExecutorAsync(); + + // Act + IExecutionResult res = await executor + .ExecuteAsync(@" + mutation { + createBaz { + baz { + bar + } + } + } + "); + + // Assert + res.ToJson().MatchSnapshot(); + SnapshotFullName fullName = Snapshot.FullName(); + SnapshotFullName snapshotName = new(fullName.Filename + "_schema", fullName.FolderPath); + executor.Schema.Print().MatchSnapshot(snapshotName); + } + public class Foo { public Foo(string bar) @@ -123,6 +232,23 @@ public Foo(string bar) public string Bar { get; set; } } + public class MutationRenamed + { + [Payload] + public BazDto CreateBaz() => new BazDto("Bar"); + } + + [GraphQLName("Baz")] + public class BazDto + { + public BazDto(string bar) + { + Bar = bar; + } + + public string Bar { get; set; } + } + public class Query { public Foo GetFoo() => new Foo("Bar"); @@ -145,4 +271,28 @@ public class Mutation [Payload("foo")] public Foo CreateFoo() => new Foo("Bar"); } + + public class MutationTask + { + [Payload("foo")] + public Task CreateTask() => Task.FromResult(new Foo("Bar")); + + [Payload("foo")] + public ValueTask CreateValueTask() => new(new Foo("Bar")); + + [Payload] + public Task CreateTaskNoName() => Task.FromResult(new Foo("Bar")); + + [Payload] + public ValueTask CreateValueTaskNoName() => new(new Foo("Bar")); + } + + public class MutationNullable + { + [Payload("foo")] + public int? NullableNumber() => null; + + [Payload] + public int? NullableNumberNoName() => null; + } } diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ItIsATask.snap b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ItIsATask.snap new file mode 100644 index 00000000000..ae0c46297ff --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ItIsATask.snap @@ -0,0 +1,24 @@ +{ + "data": { + "createTask": { + "foo": { + "bar": "Bar" + } + }, + "createValueTask": { + "foo": { + "bar": "Bar" + } + }, + "createTaskNoName": { + "foo": { + "bar": "Bar" + } + }, + "createValueTaskNoName": { + "foo": { + "bar": "Bar" + } + } + } +} diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ItIsATask.snap_schema b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ItIsATask.snap_schema new file mode 100644 index 00000000000..4c34fa71c43 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ItIsATask.snap_schema @@ -0,0 +1,41 @@ +schema { + query: Query + mutation: MutationTask +} + +type CreateTaskNoNamePayload { + foo: Foo! +} + +type CreateTaskPayload { + foo: Foo! +} + +type CreateValueTaskNoNamePayload { + foo: Foo! +} + +type CreateValueTaskPayload { + foo: Foo! +} + +type Foo { + bar: String! +} + +type MutationTask { + createTask: CreateTaskPayload + createValueTask: CreateValueTaskPayload + createTaskNoName: CreateTaskNoNamePayload + createValueTaskNoName: CreateValueTaskNoNamePayload +} + +type Query { + foo: Foo! +} + +"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." +directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." +directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! = 0 "Streamed when true." if: Boolean) on FIELD diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ItIsNullable.snap b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ItIsNullable.snap new file mode 100644 index 00000000000..0bd741aea0b --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ItIsNullable.snap @@ -0,0 +1,10 @@ +{ + "data": { + "nullableNumber": { + "foo": null + }, + "nullableNumberNoName": { + "int": null + } + } +} diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ItIsNullable.snap_schema b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ItIsNullable.snap_schema new file mode 100644 index 00000000000..30539f0a92d --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ItIsNullable.snap_schema @@ -0,0 +1,31 @@ +schema { + query: Query + mutation: MutationNullable +} + +type Foo { + bar: String! +} + +type MutationNullable { + nullableNumber: NullableNumberPayload + nullableNumberNoName: NullableNumberNoNamePayload +} + +type NullableNumberNoNamePayload { + int: Int +} + +type NullableNumberPayload { + foo: Int +} + +type Query { + foo: Foo! +} + +"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." +directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." +directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! = 0 "Streamed when true." if: Boolean) on FIELD diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ObjectIsNamed.snap b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ObjectIsNamed.snap new file mode 100644 index 00000000000..314dfd6e219 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ObjectIsNamed.snap @@ -0,0 +1,9 @@ +{ + "data": { + "createBaz": { + "baz": { + "bar": "Bar" + } + } + } +} diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ObjectIsNamed.snap_schema b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ObjectIsNamed.snap_schema new file mode 100644 index 00000000000..3d2be05c631 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/PayloadTests.PayloadMiddleware_Should_TransformPayload_When_ObjectIsNamed.snap_schema @@ -0,0 +1,30 @@ +schema { + query: Query + mutation: MutationRenamed +} + +type Baz { + bar: String! +} + +type CreateBazPayload { + baz: Baz! +} + +type Foo { + bar: String! +} + +type MutationRenamed { + createBaz: CreateBazPayload +} + +type Query { + foo: Foo! +} + +"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." +directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." +directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! = 0 "Streamed when true." if: Boolean) on FIELD