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

Support for payload errors (MutationConventions, 6a) #77

Open
FleloShe opened this issue Apr 19, 2022 · 12 comments
Open

Support for payload errors (MutationConventions, 6a) #77

FleloShe opened this issue Apr 19, 2022 · 12 comments
Milestone

Comments

@FleloShe
Copy link

Currently there seems to be no support for the Mutation Conventions as described here in 9.0.0-rc.1.

I think I identified the following issues:

  • Validators are not applied to parameters wrapped by the (virtual) "input" parameter
  • For the Error-middleware to work
    • the ValidationMiddleware must be inserted after the Error-middleware
    • the ValidationErrorsHandler must throw an AggregateException that includes the ValidationErrors

I tried to adopt some of your code for PoC-tests for HotChocolate 12.7.0 and got it working so far. Maybe this helps a bit :)

Usage

            services
                .AddGraphQLServer()
                .AddMutationConventions(
                    new MutationConventionOptions
                    {
                        ...
                    })
                ...
                .AddFairyBread()
                .TryAddTypeInterceptor<MyValidationMiddlewareInjector>()

Validator

        public MyTypeValidator()
        {
            RuleFor(x => x.ARandomString).Must(x => x.Any()).WithState(err => new MyException() { ErrorCode = MyErrorEnum.FieldStringIsWhitespace });
        }

ValidationErrorsHandler

Simply get all states and throw all exceptions as AggregationException

Injector and Middleware

    internal class MyValidationMiddlewareInjector : TypeInterceptor
    {
        private FieldMiddlewareDefinition? _validationFieldMiddlewareDef;

        public override void OnBeforeCompleteType(
            ITypeCompletionContext completionContext,
            DefinitionBase? definition,
            IDictionary<string, object?> contextData)
        {
            if (definition is not ObjectTypeDefinition objTypeDef)
            {
                return;
            }

            var options = completionContext.Services.GetRequiredService<IFairyBreadOptions>();
            var validatorRegistry = completionContext.Services.GetRequiredService<IValidatorRegistry>();

            foreach (var fieldDef in objTypeDef.Fields)
            {
                // Don't add validation middleware unless:
                // 1. we have args
                var needsValidationMiddleware = false;

                if (fieldDef.Arguments.Count == 1)
                {
                    var type = fieldDef.ResolverMember as MethodInfo;
                    var parameters = type?.GetParameters();

                    var argDef = fieldDef.Arguments.First();
                    var argCoord = new FieldCoordinate(objTypeDef.Name, fieldDef.Name, argDef.Name);

                    if (parameters is null)
                        continue;


                    if (argDef.ContextData.ContainsKey(WellKnownContextData.ValidatorDescriptors))
                        continue;

                    // 2. the argument should be validated according to options func
                    if (!options.ShouldValidateArgument(objTypeDef, fieldDef, argDef))
                    {
                        continue;
                    }

                    // 3. there's validators for it
                    Dictionary<string, List<ValidatorDescriptor>> validatorDescs;
                    try
                    {
                        validatorDescs = DetermineValidatorsForArg(validatorRegistry, parameters);
                        if (validatorDescs.Count < 1)
                        {
                            continue;
                        }
                    }
                    catch (Exception ex)
                    {
                        throw new Exception(
                            $"Problem getting runtime type for argument '{argDef.Name}' " +
                            $"in field '{fieldDef.Name}' on object type '{objTypeDef.Name}'.",
                            ex);
                    }

                    validatorDescs.TrimExcess();
                    needsValidationMiddleware = true;
                    argDef.ContextData["MySolution.Validators.Params"] = validatorDescs;
                }

                if (needsValidationMiddleware)
                {
                    if (_validationFieldMiddlewareDef is null)
                    {
                        _validationFieldMiddlewareDef = new FieldMiddlewareDefinition(
                            FieldClassMiddlewareFactory.Create<ValidationMiddleware>());
                    }
                    fieldDef.MiddlewareDefinitions.Add(_validationFieldMiddlewareDef);
                }
                else // if fairybread mw + errors mw exist, move the fairybread mw behind the errors mw
                {
                    var firstMiddleWare = fieldDef.MiddlewareDefinitions.FirstOrDefault(m => m.Middleware.Target.GetType().GetGenericArguments().Any(x => x.FullName == "FairyBread.ValidationMiddleware"));
                    var errorMiddleware = fieldDef.MiddlewareDefinitions.LastOrDefault(x => x.Key == "HotChocolate.Types.Mutations.Errors");
                    if (firstMiddleWare != null && errorMiddleware != null)
                    {
                        fieldDef.MiddlewareDefinitions.Remove(firstMiddleWare);
                        var indexOfError = fieldDef.MiddlewareDefinitions.IndexOf(errorMiddleware);
                        fieldDef.MiddlewareDefinitions.Insert(indexOfError+1,firstMiddleWare);
                    }
                        ;
                }
            }
        }

        private static Dictionary<string, List<ValidatorDescriptor>> DetermineValidatorsForArg(IValidatorRegistry validatorRegistry, ParameterInfo[] parameters)
        {
            var validators = new Dictionary<string, List<ValidatorDescriptor>>();


            foreach (var parameter in parameters)
            {
                var paramVals = new List<ValidatorDescriptor>();
                // If validation is explicitly disabled, return none so validation middleware won't be added
                if (parameter.CustomAttributes.Any(x => x.AttributeType == typeof(DontValidateAttribute)))
                {
                    continue;
                }


                // Include implicit validator/s first (if allowed)
                if (!parameter.CustomAttributes.Any(x => x.AttributeType == typeof(DontValidateImplicitlyAttribute)))
                {
                    // And if we can figure out the arg's runtime type
                    var argRuntimeType = parameter.ParameterType;
                    if (argRuntimeType is not null)
                    {
                        if (validatorRegistry.Cache.TryGetValue(argRuntimeType, out var implicitValidators) &&
                            implicitValidators is not null)
                        {
                            paramVals.AddRange(implicitValidators);
                        }
                    }
                }

                // Include explicit validator/s (that aren't already added implicitly)
                var explicitValidators = parameter.GetCustomAttributes().Where(x => x.GetType() == typeof(ValidateAttribute)).Cast<ValidateAttribute>().ToList();
                if (explicitValidators.Any())
                {
                    var validatorTypes = explicitValidators.SelectMany(x => x.ValidatorTypes);
                    // TODO: Potentially check and throw if there's a validator being explicitly applied for the wrong runtime type

                    foreach (var validatorType in validatorTypes)
                    {
                        if (paramVals.Any(v => v.ValidatorType == validatorType))
                        {
                            continue;
                        }

                        paramVals.Add(new ValidatorDescriptor(validatorType));
                    }
                }
                if (paramVals.Any())
                    validators[parameter.Name] = paramVals;
            }
            return validators;
        }

        private static Type? TryGetArgRuntimeType(ArgumentDefinition argDef)
        {
            if (argDef.Parameter?.ParameterType is { } argRuntimeType)
            {
                return argRuntimeType;
            }

            if (argDef.Type is ExtendedTypeReference extTypeRef)
            {
                return TryGetRuntimeType(extTypeRef.Type);
            }

            return null;
        }

        private static Type? TryGetRuntimeType(IExtendedType extType)
        {
            // It's already a runtime type, .Type(typeof(int))
            if (extType.Kind == ExtendedTypeKind.Runtime)
            {
                return extType.Source;
            }

            // Array (though not sure what produces this scenario as seems to always be list)
            if (extType.IsArray)
            {
                if (extType.ElementType is null)
                {
                    return null;
                }

                var elementRuntimeType = TryGetRuntimeType(extType.ElementType);
                if (elementRuntimeType is null)
                {
                    return null;
                }

                return Array.CreateInstance(elementRuntimeType, 0).GetType();
            }

            // List
            if (extType.IsList)
            {
                if (extType.ElementType is null)
                {
                    return null;
                }

                var elementRuntimeType = TryGetRuntimeType(extType.ElementType);
                if (elementRuntimeType is null)
                {
                    return null;
                }

                return typeof(List<>).MakeGenericType(elementRuntimeType);
            }

            // Input object
            if (typeof(InputObjectType).IsAssignableFrom(extType))
            {
                var currBaseType = extType.Type.BaseType;
                while (currBaseType is not null &&
                    (!currBaseType.IsGenericType ||
                    currBaseType.GetGenericTypeDefinition() != typeof(InputObjectType<>)))
                {
                    currBaseType = currBaseType.BaseType;
                }

                if (currBaseType is null)
                {
                    return null;
                }

                return currBaseType!.GenericTypeArguments[0];
            }

            // Singular scalar
            if (typeof(ScalarType).IsAssignableFrom(extType))
            {
                var currBaseType = extType.Type.BaseType;
                while (currBaseType is not null &&
                    (!currBaseType.IsGenericType ||
                    currBaseType.GetGenericTypeDefinition() != typeof(ScalarType<>)))
                {
                    currBaseType = currBaseType.BaseType;
                }

                if (currBaseType is null)
                {
                    return null;
                }

                var argRuntimeType = currBaseType.GenericTypeArguments[0];
                if (argRuntimeType.IsValueType && extType.IsNullable)
                {
                    return typeof(Nullable<>).MakeGenericType(argRuntimeType);
                }

                return argRuntimeType;
            }

            return null;
        }
    }

    internal static class WellKnownContextData
    {
        public const string Prefix = "FairyBread";

        public const string DontValidate =
            Prefix + ".DontValidate";

        public const string DontValidateImplicitly =
            Prefix + ".DontValidateImplicitly";

        public const string ExplicitValidatorTypes =
            Prefix + ".ExplicitValidatorTypes";

        public const string ValidatorDescriptors =
            Prefix + ".Validators";
    }

    internal class ValidationMiddleware
    {
        private readonly FieldDelegate _next;
        private readonly IValidatorProvider _validatorProvider;
        private readonly IValidationErrorsHandler _validationErrorsHandler;

        public ValidationMiddleware(
            FieldDelegate next,
            IValidatorProvider validatorProvider,
            IValidationErrorsHandler validationErrorsHandler)
        {
            _next = next;
            _validatorProvider = validatorProvider;
            _validationErrorsHandler = validationErrorsHandler;
        }

        public async Task InvokeAsync(IMiddlewareContext context)
        {
            var arguments = context.Selection.Field.Arguments;

            var invalidResults = new List<ArgumentValidationResult>();
            var type = context.Selection.Field.ResolverMember as MethodInfo;
            var parameters = type.GetParameters();



            foreach (var argument in context.Selection.Field.Arguments)
            {
                if (argument == null)
                {
                    continue;
                }

                var resolvedValidators = GetValidatorsTest(context, argument).GroupBy(x => x.param);
                if (resolvedValidators.Count() > 0)
                {
                    foreach (var resolvedValidatorGroup in resolvedValidators)
                    {
                        try
                        {
                            var value = context.ArgumentValue<object?>(resolvedValidatorGroup.Key);
                            if (value == null)
                            {
                                continue;
                            }

                            foreach (var resolvedValidator in resolvedValidatorGroup)
                            {
                                var validationContext = new ValidationContext<object?>(value);
                                var validationResult = await resolvedValidator.resolver.Validator.ValidateAsync(
                                    validationContext,
                                    context.RequestAborted);
                                if (validationResult != null &&
                                    !validationResult.IsValid)
                                {
                                    invalidResults.Add(
                                        new ArgumentValidationResult(
                                            resolvedValidatorGroup.Key,
                                            resolvedValidator.resolver.Validator,
                                            validationResult));
                                }
                            }
                        }
                        finally
                        {
                            foreach (var resolvedValidator in resolvedValidatorGroup)
                            {
                                resolvedValidator.resolver.Scope?.Dispose();
                            }
                        }
                    }
                }
            }

            if (invalidResults.Any())
            {
                _validationErrorsHandler.Handle(context, invalidResults);
                return;
            }

            await _next(context);
        }

        IEnumerable<(string param, ResolvedValidator resolver)> GetValidatorsTest(IMiddlewareContext context, IInputField argument)
        {
            if (!argument.ContextData.TryGetValue("MySolution.Validators.Params", out var validatorDescriptorsRaw)
                || validatorDescriptorsRaw is not Dictionary<string, List<ValidatorDescriptor>> validatorDescriptors)
            {
                yield break;
            }

            foreach (var validatorDescriptor in validatorDescriptors)
            {
                foreach (var validatorDescriptorEntry in validatorDescriptor.Value)
                {
                    if (validatorDescriptorEntry.RequiresOwnScope)
                    {
                        var scope = context.Services.CreateScope(); // disposed by middleware
                        var validator = (IValidator)scope.ServiceProvider.GetRequiredService(validatorDescriptorEntry.ValidatorType);
                        yield return (validatorDescriptor.Key, new ResolvedValidator(validator, scope));
                    }
                    else
                    {
                        var validator = (IValidator)context.Services.GetRequiredService(validatorDescriptorEntry.ValidatorType);
                        yield return (validatorDescriptor.Key, new ResolvedValidator(validator));
                    }
                }
            }
        }
    }
@FleloShe
Copy link
Author

#75 is kind of related to this, I guess.

@benmccallum
Copy link
Owner

Thanks @FleloShe , are you able to put your code in a branch and submit a draft PR? Just want to see what's changed more clearly :)

@FleloShe
Copy link
Author

FleloShe commented May 3, 2022

Sadly, my time is highly limited this month. However I will try to provide you a PR as soon as possible.

@wondering639
Copy link

any news on this? 😊

@benmccallum
Copy link
Owner

Sorry @wondering639 , I'm waiting on @FleloShe to do a PR so I can have a look at the changes

@FleloShe
Copy link
Author

FleloShe commented Jun 8, 2022

Sorry for the delay, I just came back from a trip. I hope I can provide a PR within the next days :)

@FleloShe
Copy link
Author

FleloShe commented Jun 8, 2022

See #80
Hope this helps ;)

@glen-84
Copy link

glen-84 commented Jun 16, 2022

@benmccallum

Does this look like something that will require large changes? I'm just getting started with HC & FB, using mutation conventions, and this issue has just hit me. 🙂

@benmccallum
Copy link
Owner

Thanks @FleloShe ! I'll try take a look soon but am currently very busy (buying a property).

@glen-84, awesome to have you, welcome! I think I need to digest the PR that FleloShe's done and see whetehr it's the way to go. #75 has my original thoughts on this one, but I think if there's a way to support both we can certainly give it a go :)

@glen-84
Copy link

glen-84 commented Jun 25, 2022

Michael mentioned on Slack that v13 will have a way to raise errors without throwing an exception – is that required, or should there be a temporary (?) solution to allow exceptions to be thrown?

I'd want to map the Fluent Validation errors/exceptions to my custom error interface in Hot Chocolate, so that everything ends up with the same shape.

@glen-84
Copy link

glen-84 commented Jul 18, 2022

@benmccallum Sorry to be a pain, but do you think that you'll have time soon to look into this? It would be great if validation errors could be surfaced using the same interface as other user errors.

@benmccallum
Copy link
Owner

FYI all that this is coming soon on HCv13.x. Just need to find some time 😅

@benmccallum benmccallum changed the title No Support for MutationConventions in 9.0.0-rc.1 Support for payload errors (MutationConventions, 6a) Apr 11, 2023
@benmccallum benmccallum modified the milestones: 10.0.0, 11.0.0 Apr 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants