Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scope the projection selection properly when using the mutation conventions. #6444

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ public static class WellKnownContextData
/// </summary>
public const string MutationQueryField = "HotChocolate.Relay.Mutations.QueryField";

/// <summary>
/// The key to the name of the data field when using the mutation convention.
/// </summary>
public const string MutationConventionDataField = "HotChocolate.Types.Mutations.Conventions.DataField";

/// <summary>
/// The key to get the Cache-Control header value from the context data.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ private static ObjectType CreatePayloadType(

return parent;
});
objectDef.ContextData.Add(MutationConventionDataField, dataFieldDef.Name);
objectDef.Fields.Add(dataFieldDef);

// if the mutation has domain errors we will add the errors
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Security.AccessControl;
using System.Threading;
Expand Down Expand Up @@ -204,11 +205,29 @@ value is ObjectType objectType &&
var selection = CreateProxySelection(context.Selection, fieldProxy);
context = new MiddlewareContextProxy(context, selection, objectType);
}

//for use case when projection is used with Mutation Conventions
else if (context.Operation.Definition.Operation == OperationType.Mutation
&& context.Selection.Field.Type.NamedType() is ObjectType mutationPayloadType
&& mutationPayloadType.ContextData.GetValueOrDefault(MutationConventionDataField, null) is string
dataFieldName)
michaelstaib marked this conversation as resolved.
Show resolved Hide resolved
{
var dataField = mutationPayloadType.Fields[dataFieldName];
var selection = UnwrapMutationPayloadSelect(context, dataField);
context = new MiddlewareContextProxy(context, selection, dataField.DeclaringType);
}
return executor.Invoke(next).Invoke(context);
};
}

private static ISelection UnwrapMutationPayloadSelect(IMiddlewareContext context, IObjectField field)
{
var selectionVariant =
context.Operation.SelectionVariants.First(sv => sv.GetPossibleTypes().Contains(field.DeclaringType));
var unwrap = selectionVariant.GetSelectionSet(field.DeclaringType).Selections
.First(s => s.Field.Name == field.Name);
return unwrap;
}

private sealed class MiddlewareContextProxy : IMiddlewareContext
{
private readonly IMiddlewareContext _context;
Expand Down Expand Up @@ -329,7 +348,7 @@ public ArgumentValue ReplaceArgument(string argumentName, ArgumentValue newArgum
IResolverContext IResolverContext.Clone() => _context.Clone();
}

private static Selection CreateProxySelection(ISelection selection, NodeFieldProxy field)
private static Selection CreateProxySelection(ISelection selection, IObjectField field)
{
var includeConditionsSource = ((Selection)selection).IncludeConditions;
var includeConditions = new long[includeConditionsSource.Length];
Expand Down
210 changes: 210 additions & 0 deletions src/HotChocolate/Data/test/Data.Projections.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
Expand Down Expand Up @@ -280,6 +281,180 @@ ... on Bar { fieldOfBar }

result.MatchSnapshot();
}

[Fact]
public async Task Mutation_Convention_Select()
{
var executor = await new ServiceCollection()
.AddGraphQL()
.AddQueryType<Query>() //error thrown without query, it's not needed for the test though
.AddMutationType<Mutation>()
.AddProjections()
.AddMutationConventions()
.BuildRequestExecutorAsync();

var result = await executor.ExecuteAsync(
"""
mutation {
modify {
foo {
bar
}
}
}
""");

result.MatchSnapshot();
}

[Fact]
public async Task Mutation_Convention_HasError()
{
var executor = await new ServiceCollection()
.AddGraphQL()
.AddQueryType<Query>() //error thrown without query, it's not needed for the test though
.AddMutationType<Mutation>()
.AddProjections()
.AddMutationConventions()
.BuildRequestExecutorAsync();

var result = await executor.ExecuteAsync(
"""
mutation {
createRecord(input: {throwError: false}) {
foo {
bar
}
errors {
... on Error {
message
}
}
}
}
""");

result.MatchSnapshot();
}

[Fact]
public async Task Mutation_Convention_ThrowsError()
{
var executor = await new ServiceCollection()
.AddGraphQL()
.AddQueryType<Query>() //error thrown without query, it's not needed for the test though
.AddMutationType<Mutation>()
.AddProjections()
.AddMutationConventions()
.BuildRequestExecutorAsync();

var result = await executor.ExecuteAsync(
"""
mutation {
createRecord(input: {throwError: true}) {
foo {
bar
}
errors {
... on Error {
message
}
}
}
}
""");

result.MatchSnapshot();
}

[Fact]
public async Task Mutation_Convention_Select_With_SingleOrDefault()
{
var executor = await new ServiceCollection()
.AddGraphQL()
.AddQueryType<Query>() //error thrown without query, it's not needed for the test though
.AddMutationType<Mutation>()
.AddProjections()
.AddMutationConventions()
.BuildRequestExecutorAsync();

var result = await executor.ExecuteAsync(
"""
mutation {
modifySingleOrDefault {
foo {
bar
}
}
}
""");

result.MatchSnapshot();
}

[Fact]
public async Task Mutation_Convention_With_Relay_Projection_Schema()
{
var schema = await new ServiceCollection()
.AddGraphQL()
.AddQueryType<QueryWithNodeResolvers>()
.AddObjectType<Foo>(d => d.ImplementsNode().IdField(t => t.Bar))
.AddObjectType<Bar>(d => d.ImplementsNode().IdField(t => t.IdOfBar))
.AddObjectType<Baz>(d => d.ImplementsNode().IdField(t => t.Bar2))
.AddGlobalObjectIdentification()
.AddMutationType<Mutation>()
.AddQueryFieldToMutationPayloads()
.AddProjections()
.AddMutationConventions()
.BuildSchemaAsync();

schema.MatchSnapshot();
}

[Fact]
public async Task Mutation_Convention_With_Relay_Projection()
{
var executor = await new ServiceCollection()
.AddGraphQL()
.AddQueryType<QueryWithNodeResolvers>()
.AddObjectType<Foo>(d => d.ImplementsNode().IdField(t => t.Bar))
.AddObjectType<Bar>(d => d.ImplementsNode().IdField(t => t.IdOfBar))
.AddObjectType<Baz>(d => d.ImplementsNode().IdField(t => t.Bar2))
.AddGlobalObjectIdentification()
.AddMutationType<Mutation>()
.AddQueryFieldToMutationPayloads()
.AddProjections()
.AddMutationConventions()
.BuildRequestExecutorAsync();

var result = await executor.ExecuteAsync(
"""
mutation {
createRecord(input: {throwError: false}) {
foo {
id
fieldOfFoo
}
errors {
... on Error {
message
}
}
query {
node(id: "QmFyCmRB") {
id
__typename
... on Baz { fieldOfBaz }
... on Foo { fieldOfFoo }
... on Bar { fieldOfBar }
}
}
}
}
""");

result.MatchSnapshot();
}
}

public class Query
Expand All @@ -289,6 +464,41 @@ public IQueryable<Foo> Foos
=> new Foo[] { new() { Bar = "A" }, new() { Bar = "B" } }.AsQueryable();
}

public class Mutation
{
[UseMutationConvention]
[UseProjection]
public IQueryable<Foo> Modify()
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
{
return new Foo[] { new() { Bar = "A" }, new() { Bar = "B" } }.AsQueryable();
}

[UseMutationConvention]
[UseSingleOrDefault]
[UseProjection]
public IQueryable<Foo> ModifySingleOrDefault()
{
return new Foo[] { new() { Bar = "A" } }.AsQueryable();
}

[Error<AnError>]
[UseMutationConvention]
[UseProjection]
public IQueryable<Foo> CreateRecord(bool throwError)
{
if (throwError) throw new AnError("this is only a test");
return new Foo[] { new() { Bar = "A" }, new() { Bar = "B" } }.AsQueryable();
}

public class AnError : Exception
{
public AnError(string message) : base(message)
{

}
}
}

[ExtendObjectType(typeof(Foo))]
public class FooExtensions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"data": {
"createRecord": {
"foo": [
{
"bar": "A"
},
{
"bar": "B"
}
],
"errors": null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"data": {
"modify": {
"foo": [
{
"bar": "A"
},
{
"bar": "B"
}
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"data": {
"modifySingleOrDefault": {
"foo": {
"bar": "A"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"data": {
"createRecord": {
"foo": null,
"errors": [
{
"message": "this is only a test"
}
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"data": {
"createRecord": {
"foo": [
{
"id": "Rm9vCmRB",
"fieldOfFoo": "fieldOfFoo"
},
{
"id": "Rm9vCmRC",
"fieldOfFoo": "fieldOfFoo"
}
],
"errors": null,
"query": {
"node": {
"id": "QmFyCmRB",
"__typename": "Bar",
"fieldOfBar": "fieldOfBar"
}
}
}
}
}
Loading