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

Add support for Mutations to Fusion. #5953

Merged
merged 9 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
@@ -1,3 +1,4 @@
using System.Text.Json;
using CookieCrumble;
using HotChocolate.AspNetCore;
using HotChocolate.AspNetCore.Subscriptions;
Expand Down Expand Up @@ -174,9 +175,9 @@ public Task Send_Subscribe_ValidationError()
// assert
await foreach (var result in socketResult.ReadResultsAsync().WithCancellation(ct))
{
Assert.Null(result.Data);
Assert.NotNull(result.Errors);
Assert.Null(result.Extensions);
Assert.Equal(JsonValueKind.Undefined, result.Data.ValueKind);
Assert.Equal(JsonValueKind.Array, result.Errors.ValueKind);
Assert.Equal(JsonValueKind.Undefined, result.Extensions.ValueKind);
snapshot.Add(result.Errors);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Result:
---------------
{
"errors": [
{
"message": "Unexpected Execution Error",
"locations": [
{
"line": 2,
"column": 25
}
],
"path": [
"rootExecutable"
]
}
],
"data": {
"rootExecutable": null
}
}
---------------

SQL:
---------------
Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[HotChocolate.Data.Projections.QueryableFirstOrDefaultTests+Bar]
---------------
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Result:
---------------
{
"errors": [
{
"message": "Unexpected Execution Error",
"locations": [
{
"line": 2,
"column": 25
}
],
"path": [
"rootExecutable"
]
}
],
"data": {
"rootExecutable": null
}
}
---------------

SQL:
---------------
Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[HotChocolate.Data.Projections.QueryableFirstOrDefaultTests+Bar]
---------------
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ internal FusionGraphComposer(
.Use<PrepareFusionSchemaMiddleware>()
.Use<MergeEntityMiddleware>()
.Use(() => new MergeTypeMiddleware(mergeHandlers))
.Use<MergeQueryTypeMiddleware>()
.Use<MergeQueryAndMutationTypeMiddleware>()
.Use<MergeSubscriptionTypeMiddleware>()
.Use<RemoveFusionTypesMiddleware>()
.Build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace HotChocolate.Fusion.Composition.Pipeline;

internal sealed class MergeQueryTypeMiddleware : IMergeMiddleware
internal sealed class MergeQueryAndMutationTypeMiddleware : IMergeMiddleware
{
public async ValueTask InvokeAsync(CompositionContext context, MergeDelegate next)
{
Expand All @@ -19,39 +19,20 @@ public async ValueTask InvokeAsync(CompositionContext context, MergeDelegate nex
context.FusionGraph.Types.Add(queryType);
}

foreach (var field in schema.QueryType.Fields)
MergeRootFields(context, schema, schema.QueryType, queryType);
}

if (schema.MutationType is not null)
{
var queryType = context.FusionGraph.MutationType!;

if (context.FusionGraph.MutationType is null)
{
if (queryType.Fields.TryGetField(field.Name, out var targetField))
{
context.MergeField(field, targetField, queryType.Name);
}
else
{
targetField = context.CreateField(field, context.FusionGraph);
queryType.Fields.Add(targetField);
}

var arguments = new List<ArgumentNode>();

var selection = new FieldNode(
null,
new NameNode(field.GetOriginalName()),
null,
null,
Array.Empty<DirectiveNode>(),
arguments,
null);

var selectionSet = new SelectionSetNode(new[] { selection });

foreach (var arg in field.Arguments)
{
arguments.Add(new ArgumentNode(arg.Name, new VariableNode(arg.Name)));
context.ApplyVariable(targetField, arg, schema.Name);
}

context.ApplyResolvers(targetField, selectionSet, schema.Name);
queryType = context.FusionGraph.MutationType = new ObjectType("Mutation");
context.FusionGraph.Types.Add(queryType);
}

MergeRootFields(context, schema, schema.MutationType, queryType);
}
}

Expand All @@ -60,6 +41,47 @@ public async ValueTask InvokeAsync(CompositionContext context, MergeDelegate nex
await next(context).ConfigureAwait(false);
}
}

private static void MergeRootFields(
CompositionContext context,
Schema sourceSchema,
ObjectType sourceRootType,
ObjectType targetRootType)
{
foreach (var field in sourceRootType.Fields)
{
if (targetRootType.Fields.TryGetField(field.Name, out var targetField))
{
context.MergeField(field, targetField, targetRootType.Name);
}
else
{
targetField = context.CreateField(field, context.FusionGraph);
targetRootType.Fields.Add(targetField);
}

var arguments = new List<ArgumentNode>();

var selection = new FieldNode(
null,
new NameNode(field.GetOriginalName()),
null,
null,
Array.Empty<DirectiveNode>(),
arguments,
null);

var selectionSet = new SelectionSetNode(new[] { selection });

foreach (var arg in field.Arguments)
{
arguments.Add(new ArgumentNode(arg.Name, new VariableNode(arg.Name)));
context.ApplyVariable(targetField, arg, sourceSchema.Name);
}

context.ApplyResolvers(targetField, selectionSet, sourceSchema.Name);
}
}
}

static file class MergeQueryTypeMiddlewareExtensions
Expand Down
6 changes: 6 additions & 0 deletions src/HotChocolate/Fusion/src/Core/FusionResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/HotChocolate/Fusion/src/Core/FusionResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,7 @@
<data name="FusionRequestExecutorBuilderExtensions_AddFusionGatewayServer_NoSchema" xml:space="preserve">
<value>A valid service configuration must always produce a schema document.</value>
</data>
<data name="ThrowHelper_Requirement_Is_Missing" xml:space="preserve">
<value>The variable value `{0}` was not provided but is required.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public FusionGraphConfiguration(
{
if (!binding.Name.EqualsOrdinal(type.Name))
{
_typeNameLookup.Add((binding.SchemaName, binding.Name), type.Name);
_typeNameLookup.Add((binding.SubgraphName, binding.Name), type.Name);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ private FusionGraphConfiguration ReadServiceDefinition(DocumentNode document)

var types = new List<IType>();
var typeNames = FusionTypeNames.From(document);
var typeNameBindings = new Dictionary<string, MemberBinding>();
var httpClientConfigs = ReadHttpClientConfigs(typeNames, schemaDef.Directives);
var webSocketClientConfigs = ReadWebSocketClientConfigs(typeNames, schemaDef.Directives);
var typeNameField = CreateTypeNameField(_subgraphNames);
var typeNameField = CreateTypeNameField(typeNameBindings);

foreach (var definition in document.Definitions)
{
Expand All @@ -52,6 +53,11 @@ private FusionGraphConfiguration ReadServiceDefinition(DocumentNode document)
}
}

foreach (var subgraphName in _subgraphNames)
{
typeNameBindings.Add(subgraphName, new MemberBinding(subgraphName, typeNameField.Name));
}

if (httpClientConfigs is not { Count: > 0 })
{
throw ServiceConfNoClientsSpecified();
Expand Down Expand Up @@ -102,13 +108,15 @@ private ObjectFieldCollection ReadObjectFields(
return new ObjectFieldCollection(collection);
}

private static ObjectField CreateTypeNameField(IEnumerable<string> subgraphNames)
=> new ObjectField(
IntrospectionFields.TypeName,
new MemberBindingCollection(
subgraphNames.Select(t => new MemberBinding(t, IntrospectionFields.TypeName))),
FieldVariableDefinitionCollection.Empty,
ResolverDefinitionCollection.Empty);
private static ObjectField CreateTypeNameField(
Dictionary<string, MemberBinding> bindings)
{
return new ObjectField(
IntrospectionFields.TypeName,
new MemberBindingCollection(bindings),
FieldVariableDefinitionCollection.Empty,
ResolverDefinitionCollection.Empty);
}

private IReadOnlyList<HttpClientConfiguration> ReadHttpClientConfigs(
FusionTypeNames typeNames,
Expand Down Expand Up @@ -521,7 +529,7 @@ private MemberBindingCollection ReadMemberBindings(

foreach (var binding in definitions)
{
_assert.Add(binding.SchemaName);
_assert.Add(binding.SubgraphName);
}

foreach (var resolver in resolvers)
Expand Down
31 changes: 23 additions & 8 deletions src/HotChocolate/Fusion/src/Core/Metadata/MemberBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,40 @@ internal class MemberBinding
/// <summary>
/// Initializes a new instance of <see cref="MemberBinding"/>.
/// </summary>
/// <param name="schemaName">
/// The schema to which the type system member is bound to.
/// <param name="subgraphName">
/// The name of the subgraph to which the type system member is bound to.
/// </param>
/// <param name="name">
/// The name which the type system member has in the <see cref="SchemaName"/>.
/// The name which the type system member has in the <see cref="SubgraphName"/>.
/// </param>
public MemberBinding(string schemaName, string name)
public MemberBinding(string subgraphName, string name)
{
SchemaName = schemaName;
if (string.IsNullOrWhiteSpace(subgraphName))
{
throw new ArgumentException(
$"'{nameof(subgraphName)}' cannot be null or whitespace.",
nameof(subgraphName));
}

if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException(
$"'{nameof(name)}' cannot be null or whitespace.",
nameof(name));
}

SubgraphName = subgraphName;
Name = name;
}

/// <summary>
/// Gets the schema to which the type system member is bound to.
/// Gets the name of the subgraph to which the type system member is bound to.
/// </summary>
public string SchemaName { get; }
public string SubgraphName { get; }

/// <summary>
/// Gets the name which the type system member has in the <see cref="SchemaName"/>.
/// Gets the name which the type system member has on a certain the subgraph
/// represented by the <see cref="SubgraphName" />.
/// </summary>
public string Name { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,22 @@ internal sealed class MemberBindingCollection : IEnumerable<MemberBinding>

public MemberBindingCollection(IEnumerable<MemberBinding> bindings)
{
_bindings = bindings.ToDictionary(t => t.SchemaName, StringComparer.Ordinal);
if (bindings is null)
{
throw new ArgumentNullException(nameof(bindings));
}

_bindings = bindings.ToDictionary(t => t.SubgraphName, StringComparer.Ordinal);
}

public MemberBindingCollection(Dictionary<string, MemberBinding> bindings)
{
if (bindings is null)
{
throw new ArgumentNullException(nameof(bindings));
}

_bindings = bindings;
}

public int Count => _bindings.Count;
Expand Down
15 changes: 12 additions & 3 deletions src/HotChocolate/Fusion/src/Core/Planning/ExecutionPlanBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System.Diagnostics;
using System.Runtime.InteropServices.JavaScript;
using HotChocolate.Execution.Processing;
using HotChocolate.Fusion.Metadata;
using HotChocolate.Language;
using HotChocolate.Resolvers;
using HotChocolate.Types.Introspection;
using HotChocolate.Utilities;
using static HotChocolate.Fusion.Metadata.ResolverKind;

Expand Down Expand Up @@ -97,7 +97,9 @@ private QueryPlanNode BuildQueryTree(QueryPlanContext context)

while (current.Length > 0)
{
if (current.Length is 1)
if (current.Length is 1 ||
(_schema.MutationType is not null &&
_schema.MutationType.Name.EqualsOrdinal(current[0].Key.SelectionSetType.Name)))
{
var node = current[0];
var selectionSet = ResolveSelectionSet(context, node.Key);
Expand Down Expand Up @@ -382,7 +384,8 @@ private SelectionSetNode CreateSelectionSetNode(

foreach (var selection in selectionSet.Selections)
{
if (executionStep.AllSelections.Contains(selection))
if (executionStep.AllSelections.Contains(selection) ||
selection.Field.Name.EqualsOrdinal(IntrospectionFields.TypeName))
{
var field = typeContext.Fields[selection.Field.Name];
var selectionNode = CreateSelectionNode(
Expand All @@ -400,6 +403,12 @@ private SelectionSetNode CreateSelectionSetNode(
{
selectionNodes.Add(selection);
}

if(selectionNodes.Count == 0)
{
// TODO : ThrowHelper
throw new InvalidOperationException("A selection set must not be empty.");
}
}

return new SelectionSetNode(selectionNodes);
Expand Down
Loading