appany / AppAny.HotChocolate.FluentValidation

Input field HotChocolate GraphQL + FluentValidation integration
MIT License
71 stars 16 forks source link

Strongly typed errors (part of the graph) #92

Open leebenson opened 2 years ago

leebenson commented 2 years ago

Is your feature request related to a problem? Please describe.

Instead of returning an errors object when validation fails with the typical 'extensions' key, I'd like to convert a mutation return type into a union of TSuccess | FormError[], where FormError contains a strongly typed { field: String!, error: String! }.

Describe the solution you'd like

Parsing errors in a front-end client is difficult, because its shape is unknown. It could be a form error, an authentication issue, an internal server error or something else. Many web GraphQL clients treat it as a 'hard' error by default.

I'd like to validate user input, converting 'errors' into a new FormError type. Where validation on user input is relevant, the return type should be modified to include a union accommodating both the 'success' type(s) and the potential for a user error.

i.e. I'd like to wind up with something like this:

mutation LoginWithEmailAndPassword($input: LoginWithEmailAndPasswordInput!) {
  loginWithEmailAndPassword(input: $input) {
    # When user input fails... e.g. missing email address, bad password format, etc
    ... on FormError {
      field
      error
    }
    # This is the 'success' response
    ... on LoginWithEmailAndPasswordSuccess {
      jwt
    }
    # This is also a legit. response... in this case, validation passed, but
    # logging in the user failed
    ... on LoginWithEmailAndPasswordFailure {
      error
    }
  }
}

... so that if an errors does occur, it generally means something more severe than user input is off!

Describe alternatives you've considered

I could alternatively create a wrapper for a GraphQL client (I'm using React Query, FWIW) that looks for 400 responses, parses the errors key, and determines the type of error.

But this has two drawbacks:

Another alternative is to write a helper in C# that validates in the function, and converts errors into a standard FormError type. This is fine, but it requires modifying the signature of a mutation to accommodate every use-case where input is used... which is a little repetitive.

Additional context

User input errors are common, and IMO relying on the errors key with a dynamic/untyped extensions meta to determine the kind of error makes it difficult to parse what is a normal part of users interacting with the API.

Rather than creating a wrapper that catches errors and handles them at the top-level, I'd rather make form errors a first-class feature of my graph. Fluent Validation is ideal for that, but I'm unsure whether there's a simple way to have this library modify the graph to allow for a union of a 'success' and FormError[] response where it's used, or I should focus instead on creating a wrapper.

In any case, thanks for a great 'glue' library! It's very useful indeed.

sergeyshaykhullin commented 2 years ago

I did already some attempts to implement this approach, but where are a lot of edge cases(e.g. multiple inputs, error types):

If you want to use domain errors you can try HC 12 Mutation conventions https://chillicream.com/docs/hotchocolate/defining-a-schema/mutations#input-and-payload

If you have an idea how to implement this without breaking existing codebases, please share with me :)

leebenson commented 2 years ago

Thanks for the update 👍🏻

The implementation I was thinking of if I did this myself was something like:

public ISomeReturnType ResolverMethod([Validation(typeof(SomeAbstractValidator))] CreateUserInput input) {}

The idea being, an AbstractValidator<T> could be provided to the attribute. Whenever input was created on a new query, it would run something akin to:

var validation = await input.ValidateAsync(input);
if (!validation.IsValid)
{
    return validation.ToGraphQLFormErrors()
}

The ISomeReturnType would need to be a union that contains a FormErrors type to compile.

I'm still trying to parse the internals of Hot Chocolate and understand how its event API works for input. It seems like it should be simple to do validation and short-circuit by returning a FormErrors type if validation fails, in much the same way that you're currently raising an error... but I'm still following code paths to understand how that works.