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

Mutation Convention Refinements #5586

Merged
merged 9 commits into from
Dec 6, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ namespace CookieCrumble;

public static class SnapshotExtensions
{
public static void MatchSnapshot(this Snapshot value)
=> value.Match();

public static void MatchSnapshot(
this object? value,
object? postFix = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace CookieCrumble;

internal static class WriterExtensions
public static class WriterExtensions
{
private static readonly Encoding _utf8 = Encoding.UTF8;

Expand Down
21 changes: 21 additions & 0 deletions src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,27 @@ public static class Schema
/// An object type implementing the node interface does not provide a node resolver.
/// </summary>
public const string NodeResolverMissing = "HC0068";

/// <summary>
/// A mutation payload type must be an object type.
/// </summary>
public const string MutationPayloadMustBeObject = "HC0069";

/// <summary>
/// The schema building directive `@mutation`
/// can only be applied on object fields.
/// </summary>
public const string MutationConvDirectiveWrongLocation = "HC0070";

/// <summary>
/// A schema building directive had an argument with an unexpected value.
/// </summary>
public const string DirectiveArgumentUnexpectedValue = "HC0071";

/// <summary>
/// The specified directive argument does not exist.
/// </summary>
public const string UnknownDirectiveArgument = "HC0072";
}

public static class Scalars
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Data;

namespace HotChocolate;

/// <summary>
Expand Down Expand Up @@ -223,4 +225,10 @@ public static class WellKnownContextData
/// The key to skip the execution depth analysis.
/// </summary>
public const string SkipDepthAnalysis = "HotChocolate.Execution.SkipDepthAnalysis";

/// <summary>
/// The key of the marker setting that a field on the mutation type represents
/// the query field.
/// </summary>
public const string MutationQueryField = "HotChocolate.Relay.Mutations.QueryField";
}
6 changes: 6 additions & 0 deletions src/HotChocolate/Core/src/Abstractions/WellKnownMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ public static class WellKnownMiddleware
/// </summary>
public const string MutationErrors = "HotChocolate.Types.Mutations.Errors";

/// <summary>
/// This key identifies the mutation convention middleware
/// that nulls fields when an error was detected.
/// </summary>
public const string MutationErrorNull = "HotChocolate.Types.Mutations.Errors.Null";

/// <summary>
///The key identifies the mutation result middleware.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static IRequestExecutorBuilder AddJsonSupport(this IRequestExecutorBuilde
throw new ArgumentNullException(nameof(builder));
}

builder.ConfigureSchema(sb => sb.AddSchemaDirective(new FromJsonSchemaDirective()));
builder.ConfigureSchema(sb => sb.TryAddSchemaDirective(new FromJsonSchemaDirective()));
return builder;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using HotChocolate;
using HotChocolate.Execution.Configuration;

namespace Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -60,7 +61,8 @@ public static class MutationRequestExecutorBuilderExtensions
builder
.ConfigureSchema(c => c.ContextData[MutationContextDataKeys.Options] = options)
.TryAddTypeInterceptor<MutationConventionTypeInterceptor>()
.AddTypeDiscoveryHandler(c => new MutationResultTypeDiscoveryHandler(c.TypeInspector));
.AddTypeDiscoveryHandler(c => new MutationResultTypeDiscoveryHandler(c.TypeInspector))
.ConfigureSchema(c => c.TryAddSchemaDirective(new MutationDirective()));

return builder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace HotChocolate.Types;

internal sealed class ErrorNullMiddleware
{
private readonly FieldDelegate _next;

public ErrorNullMiddleware(FieldDelegate next)
=> _next = next ?? throw new ArgumentNullException(nameof(next));

public async ValueTask InvokeAsync(IMiddlewareContext context)
{
if (context.ScopedContextData.ContainsKey(ErrorContextDataKeys.Errors))
{
context.Result = null;
}
else
{
await _next(context).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace HotChocolate.Types;
/// This internal data structure is used to store the effective mutation options of a field
/// on the context so that the type interceptor can access them.
/// </summary>
internal class MutationContextData
internal sealed class MutationContextData
{
public MutationContextData(
ObjectFieldDefinition definition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
using static HotChocolate.Types.Descriptors.TypeReference;
using static HotChocolate.Resolvers.FieldClassMiddlewareFactory;
using static HotChocolate.Types.ErrorContextDataKeys;
using static HotChocolate.Types.ThrowHelper;
using static HotChocolate.Utilities.ThrowHelper;
using static HotChocolate.WellKnownContextData;

#nullable enable

Expand All @@ -21,6 +23,7 @@ internal sealed class MutationConventionTypeInterceptor : TypeInterceptor
private ITypeCompletionContext _completionContext = default!;
private ITypeReference? _errorInterfaceTypeRef;
private ObjectTypeDefinition? _mutationTypeDef;
private FieldMiddlewareDefinition? _errorNullMiddleware;

internal override void InitializeContext(
IDescriptorContext context,
Expand Down Expand Up @@ -121,12 +124,12 @@ public override void OnAfterMergeTypeExtensions()

if (unprocessed.Count > 0)
{
throw ThrowHelper.NonMutationFields(unprocessed);
throw NonMutationFields(unprocessed);
}
}
}

private void ApplyResultMiddleware(ObjectFieldDefinition mutation)
private static void ApplyResultMiddleware(ObjectFieldDefinition mutation)
{
var middlewareDef = new FieldMiddlewareDefinition(
next => async context =>
Expand Down Expand Up @@ -226,22 +229,111 @@ argument.Formatters.Count switch
if (!_typeLookup.TryNormalizeReference(typeRef!, out typeRef) ||
!_typeRegistry.TryGetType(typeRef, out var registration))
{
throw ThrowHelper.CannotResolvePayloadType();
throw CannotResolvePayloadType();
}

// before starting to build the payload type we first will look for error definitions
// an the mutation.
var errorDefinitions = GetErrorDefinitions(mutation);
FieldDef? errorField = null;

// if the mutation result type matches the payload type name pattern
// we expect it to be already a proper payload and will not transform the
// result type to a payload.
// result type to a payload. However, we will check if we need to add the
// errors field to it.
if (registration.Type.Name.EqualsOrdinal(payloadTypeName))
{
if (errorDefinitions.Count <= 0)
{
return;
}

// first we retrieve the payload type that was defined by the user.
var payloadType = _completionContext.GetType<IType>(typeRef);

// we ensure that the payload type is an object type; otherwise we raise an error.
if (payloadType.IsListType() || payloadType.NamedType() is not ObjectType obj)
{
_completionContext.ReportError(
MutationPayloadMustBeObject(payloadType.NamedType()));
return;
}

// we grab the definition to mutate the payload type.
var payloadTypeDef = obj.Definition!;

// next we create a null middleware which will return null for any payload
// field that we have on the payload type if a mutation error was returned.
var nullMiddleware = _errorNullMiddleware ??=
new FieldMiddlewareDefinition(
Create<ErrorNullMiddleware>(),
key: MutationErrorNull,
isRepeatable: false);

foreach (var resultField in payloadTypeDef.Fields)
{
// if the field is the query mutation field we will allow it to stay non-nullable
// since it does not need the parent.
if (resultField.CustomSettingExists(MutationQueryField))
{
continue;
}

// first we ensure that all fields on the mutation payload are nullable.
resultField.Type = EnsureNullable(resultField.Type);

// next we will add the null middleware as the first middleware element
resultField.MiddlewareDefinitions.Insert(0, nullMiddleware);
}

// We will ensure that the mutation return value is actually not nullable.
// If there was an actual GraphQL error on the mutation it ensures that
// the next mutation execution is aborted.
//
// mutation {
// currentMutationThatErrors <---
// nextMutationWillNotBeInvoked
// }
//
// Mutations are executed sequentially and by having the payload
// non-nullable the complete result is erased (non-null propagation)
// which causes the execution to stop.
mutation.Type = EnsureNonNull(mutation.Type!);

// now that everything is put in place we will create the error types and
// the error middleware.
var errorTypeName = options.FormatErrorTypeName(mutation.Name);
RegisterErrorType(CreateErrorType(errorTypeName, errorDefinitions), mutation.Name);
var errorListTypeRef = Parse($"[{errorTypeName}!]");
payloadTypeDef.Fields.Add(
new ObjectFieldDefinition(
options.PayloadErrorsFieldName,
type: errorListTypeRef,
resolver: ctx =>
{
ctx.ScopedContextData.TryGetValue(Errors, out var errors);
return new ValueTask<object?>(errors);
}));

// collect error factories for middleware
var errorFactories = errorDefinitions.Select(t => t.Factory).ToArray();

// create middleware
var errorMiddleware =
new FieldMiddlewareDefinition(
Create<ErrorMiddleware>(
(typeof(IReadOnlyList<CreateError>), errorFactories)),
key: MutationErrors,
isRepeatable: false);

// last but not least we insert the error middleware to the mutation field.
mutation.MiddlewareDefinitions.Insert(0, errorMiddleware);

// we return here since we handled the case where the user has provided a custom
// payload type.
return;
}

// before starting to build the payload type we first will look for error definitions
// an the mutation.
var errorDefinitions = GetErrorDefinitions(mutation);
FieldDef? errorField = null;

// if this mutation has error definitions we will create the error middleware,
// the payload error type and the error field definition that will be exposed
// on the payload.
Expand Down Expand Up @@ -581,6 +673,18 @@ private ITypeReference EnsureNullable(ITypeReference typeRef)
return Create(CreateTypeNode(nt.Type));
}

private ITypeReference EnsureNonNull(ITypeReference typeRef)
{
var type = _completionContext.GetType<IType>(typeRef);

if (type.Kind is TypeKind.NonNull)
{
return typeRef;
}

return Create(CreateTypeNode(new NonNullType(type)));
}

private ITypeNode CreateTypeNode(IType type)
=> type switch
{
Expand Down