Skip to content

Commit

Permalink
More work on mutation conventions support
Browse files Browse the repository at this point in the history
  • Loading branch information
benmccallum committed Dec 20, 2023
1 parent eac8c74 commit 741a70b
Show file tree
Hide file tree
Showing 12 changed files with 372 additions and 308 deletions.
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,23 @@ public Task CreateUser(CreateUserInput userInput) { ... }

### How validation errors will be handled

By default, errors will be written out into the GraphQL execution result in the `Errors` property with one error being reported per failure on a field.
You can change this behaviour by implementing your own `IValidationErrorsHandler`.
Validation errors can be reported by FairyBread in one of two fashions, depending on the field in question:
1. Globally, under the GraphQL response's `errors` array.
2. Inline, under the mutation field's `errors` array as per [mutation conventions]().

Currently, standard fields (i.e. not a mutation field), will always report errors globally.
This may be changed in the future as best practices and upstream support comes through.

Mutation fields on the other hand, when using a field is using Hot Chocolate's mutation convention
(and FairyBread's UseMutationConvention setting is on, as is the default),
the validation errors will be written out under the convention-based `errors` array field
under the mutation field.

Note: To assist in migration from the old behavior to the new one, you can use `[GlobalValidationErrors]` or
`.GlobalValidationErrors()` (fluent-style) on mutation fields to opt back into global errors field-by-field.

In both cases, one error will be reported per failure on a field.
You can change this behavior by implementing your own `IValidationErrorsHandler`.

### Implicit vs explicit configuration

Expand Down Expand Up @@ -117,6 +132,8 @@ services.AddFairyBread(options =>
{
options.ShouldValidateArgument = (objTypeDef, fieldTypeDef, argTypeDef) => ...;
options.ThrowIfNoValidatorsFound = true/false;
options.UseMutationConventions = true/false;
options.ValidationErrorType = typeof(...);
});
```

Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<Version>10.0.0</Version>

<FluentValidationVersion>10.0.0</FluentValidationVersion>
<HotChocolateVersion>13.6.0-preview.31</HotChocolateVersion>
<HotChocolateVersion>13.6.0</HotChocolateVersion>

<LibraryTargetFrameworks>net7.0; net6.0; netstandard2.0</LibraryTargetFrameworks>
<TestTargetFrameworks>net7.0; net6.0</TestTargetFrameworks>
Expand Down
4 changes: 2 additions & 2 deletions src/FairyBread/DefaultFairyBreadOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ public class DefaultFairyBreadOptions : IFairyBreadOptions
= true;

/// <inheritdoc/>
public virtual Type ValidationExceptionType { get; set; }
= typeof(DefaultValidationException);
public virtual Type ValidationErrorType { get; set; }
= typeof(DefaultValidationError);
}
20 changes: 20 additions & 0 deletions src/FairyBread/DefaultValidationError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace FairyBread;

[GraphQLName("ValidationError")]
public class DefaultValidationError
{
private DefaultValidationError(
string message)
{
Message = "A validation error occurred";
// TODO: Complete a nice rich implementation.
}

public static DefaultValidationError CreateErrorFrom(
ArgumentValidationResult argValRes)
{
return new("");
}

public string Message { get; }
}
7 changes: 3 additions & 4 deletions src/FairyBread/DefaultValidationErrorsHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ public virtual void Handle(
IMiddlewareContext context,
IEnumerable<ArgumentValidationResult> invalidResults)
{
if (context.Selection.Field.ContextData.TryGetValue(WellKnownContextData.UsesMutationConvention, out var usesMutationConventionObj) &&
usesMutationConventionObj is bool usesMutationConvention &&
usesMutationConvention == true)
// TODO: And options are configured to write this into conventions now
if (context.Selection.Field.ContextData.TryGetValue(WellKnownContextData.UsesInlineErrors, out var usesInlineErrorsObj) &&
usesInlineErrorsObj is bool usesInlineErrors &&
usesInlineErrors == true)
{
context.Result = new MutationError(
invalidResults
Expand Down
47 changes: 0 additions & 47 deletions src/FairyBread/DefaultValidationException.cs

This file was deleted.

56 changes: 56 additions & 0 deletions src/FairyBread/GlobalValidationErrorsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

namespace FairyBread;

/// <summary>
/// <para>
/// Instructs FairyBread, when using HotChocolate's mutation conventions
/// and with FairyBread configured with UseMutationConventions as true,
/// to report validation errors in the global errors array of the GraphQL
/// response (rather than inline in this mutation fields' local errors array).
/// </para>
/// <para>
/// This attribute is intended to allow a progressive transition from FairyBread's
/// former behavior with mutation conventions (global errors) to the
/// new behavior (local errors). Fields that you aren't ready to modernize can
/// be annotated in the meantime.
/// </para>
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class GlobalValidationErrorsAttribute : ObjectFieldDescriptorAttribute
{
protected override void OnConfigure(
IDescriptorContext context,
IObjectFieldDescriptor descriptor,
MemberInfo memberInfo)
{
descriptor.GlobalValidationErrors();
}
}

public static class GlobalValidationErrorsObjectFieldDescriptorExtensions
{
/// <summary>
/// <para>
/// Instructs FairyBread, when using HotChocolate's mutation conventions
/// and with FairyBread configured with UseMutationConventions as true,
/// to report validation errors in the global errors array of the GraphQL
/// response (rather than inline in this mutation fields' local errors array).
/// </para>
/// <para>
/// This is intended to allow a progressive transition from FairyBread's
/// former behavior with mutation conventions (global errors) to the
/// new behavior (local errors). Fields that you aren't ready to modernize can
/// use this in the meantime.
/// </para>
/// </summary>
public static IObjectFieldDescriptor GlobalValidationErrors(
this IObjectFieldDescriptor descriptor)
{
descriptor.Extend().OnBeforeNaming((completionContext, objFieldDef) =>
{
objFieldDef.ContextData[WellKnownContextData.UsesGlobalErrors] = true;
});

return descriptor;
}
}
2 changes: 1 addition & 1 deletion src/FairyBread/IFairyBreadOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ public interface IFairyBreadOptions

bool UseMutationConventions { get; set; }

Type ValidationExceptionType { get; set; }
Type ValidationErrorType { get; set; }
}
42 changes: 42 additions & 0 deletions src/FairyBread/ValidationErrorConfigurer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using HotChocolate.Types;

namespace FairyBread;

internal class ValidationErrorConfigurer : MutationErrorConfiguration
{
public override void OnConfigure(
IDescriptorContext context,
ObjectFieldDefinition fieldDef)
{
var options = context.Services
.GetRequiredService<IFairyBreadOptions>();
if (!options.UseMutationConventions)
{
return;
}

var validatorRegistry = context.Services
.GetRequiredService<IValidatorRegistry>();

// TODO: Can I get this? I think I need to PR this as Michael said he'd pass this through but hasn't.
var objTypeDef = new ObjectTypeDefinition();

if (ValidationMiddlewareHelpers.NeedsMiddleware(options, validatorRegistry, objTypeDef, fieldDef))
{
ValidationMiddlewareHelpers.AddMiddleware(fieldDef);

if (!fieldDef.ContextData.TryGetValue(WellKnownContextData.UsesGlobalErrors, out var usesGlobalErrorsObj) ||
usesGlobalErrorsObj is not bool usesGlobalErrors ||
usesGlobalErrors == false)
{
fieldDef.AddErrorType(context, options.ValidationErrorType);

// Set a flag for the errors handler
fieldDef.ContextData[WellKnownContextData.UsesInlineErrors] = true;

// Cleanup
//fieldDef.ContextData.Remove(WellKnownContextData.UsesGlobalErrors);
}
}
}
}
Loading

0 comments on commit 741a70b

Please sign in to comment.