graphql-dotnet icon indicating copy to clipboard operation
graphql-dotnet copied to clipboard

Can't get directive to work

Open ygetarts opened this issue 1 year ago • 20 comments

Hello,

I'm extremely new to graphql and working on a new codebase and trying to implement custom directives. At the moment, I'm trying to implement a simple field validation, where a field called "count" has a min value of 1 and a max of 10, for example.

I'm trying to follow the guide here, but when I call FindAppliedDirective, it always returns null.

public class MaxLength : Directive
  {
    public MaxLength()
      : base("maxLength", DirectiveLocation.Mutation, DirectiveLocation.InputFieldDefinition)
    {
        Description = "Used to specify the minimum and/or maximum length for an input field or argument.";
        Arguments = new QueryArguments(
            new QueryArgument<IntGraphType>
            {
              Name = "min",
              Description = "If specified, specifies the minimum length that the input field or argument must have."
            },
            new QueryArgument<IntGraphType>
            {
              Name = "max",
              Description = "If specified, specifies the maximum length that the input field or argument must have."
            }
      );
    }
  }

  public class MaxLengthDirectiveVistor : BaseSchemaNodeVisitor
  {
    public override async void VisitObjectFieldDefinition(FieldType field, IObjectGraphType type, ISchema schema)
    {
      var applied = field.FindAppliedDirective("maxLength");
      if (applied != null)
      {....

this code is getting hit, it's just "applied" is always null. I've also tried passing in many different params into FindAppliedDirective, like 'count' and 'name' and other things, just to see if anything would show up, but it always returns null.

And I'm setting the direct like:

public class BookSummaryCreateArgInputType : InputObjectGraphType<BookSummaryCreateArg>
{
  public BookSummaryCreateArgInputType()
  {
    this.Name = "BookSummaryCreateArg";
    this.Field(_ => _.Count)
                          .Directive("maxLength", x =>
                            x.AddArgument(new DirectiveArgument("min") { Name = "min", Value = 1 })
                              .AddArgument(new DirectiveArgument("max") { Name = "max", Value = 10}));
  }
}

Not sure where I'm going wrong here?

ygetarts avatar Aug 24 '22 03:08 ygetarts

Custom directives that validate input values are not for the faint of heart! But my first thought is that perhaps you are overriding the wrong method within your custom node visitor, as I think VisitObjectFieldDefinition are only for output fields. Perhaps overloading VisitInputObjectFieldDefinition is what you are looking for.

Once you do so, I think you may find that the visitor is only for schema validation and not what you're looking for -- since it is not an AST visitor it cannot help with validating inputs fed to the mutation.

I would look at https://github.com/graphql-dotnet/graphql-dotnet/blob/master/src/GraphQL/Validation/Rules.Custom/InputFieldsAndArgumentsOfCorrectLength.cs for a sample use of a directive to validate string lengths. You'll see that there's code to validate literal values and separately code to validate variable values.

@sungam3r may be able to help further - I don't use custom directives for input validation.

Shane32 avatar Aug 24 '22 04:08 Shane32

But my first thought is that perhaps you are overriding the wrong method within your custom node visitor, as I think VisitObjectFieldDefinition are only for output fields

True. See #3302.

sungam3r avatar Aug 24 '22 08:08 sungam3r

Thanks for the reply.

I looked at the example you linked and am trying to implement that to get a handle on how this works. I'm using version 5.1, so I had to change the code a bit to:

public override void VisitField(ValidationContext context, GraphQLVariableDefinition variable, VariableName variableName, IInputObjectGraphType type, FieldType field, object? variableValue, object? parsedValue)
            {
                var lengthDirective = field.FindAppliedDirective("maxLength");
                if (lengthDirective != null)
               {
                 .....
              }
            }

so, hoping that works just as well. My question now is, how do I register this? Right now when I run my mutation, this doesn't seem to be getting hit. Prior, in my Schema, I had:

public Schema(IServiceProvider provider) : base(provider)
{
   this.Directives.Register(new MaxLength());
   this.RegistorVisitor(new MaxLengthDirectiveVisitor());
}

but now we don't implement ISchemaNodeVisitor any longer. How do I register this?

Thanks.

ygetarts avatar Aug 24 '22 16:08 ygetarts

But my first thought is that perhaps you are overriding the wrong method within your custom node visitor, as I think VisitObjectFieldDefinition are only for output fields

True. See #3302.

Yes. I made the modification Shane suggested above to use VisitInputObjectFieldDefinition and it worked. However, like he suggested, that's not apparently what I'm looking for!

ygetarts avatar Aug 24 '22 16:08 ygetarts

You can view that same file for v5 here:

https://github.com/graphql-dotnet/graphql-dotnet/blob/5.3.3/src/GraphQL/Validation/Rules.Custom/InputFieldsAndArgumentsOfCorrectLength.cs

@sungam3r would need to answer additional questions here. I’ve not used directives, sorry.

Shane32 avatar Aug 24 '22 16:08 Shane32

You can view that same file for v5 here:

https://github.com/graphql-dotnet/graphql-dotnet/blob/5.3.3/src/GraphQL/Validation/Rules.Custom/InputFieldsAndArgumentsOfCorrectLength.cs

@sungam3r would need to answer additional questions here. I’ve not used directives, sorry.

Thank you again.

You mention that you don't use directives for input validation. How do you normally do it?

ygetarts avatar Aug 24 '22 16:08 ygetarts

Typically I do it within the field resolver. I would agree that a validation rule based on directives is a better approach, as the mutation or query can be validated prior to any part executing. It just seems harder to implement. Besides often the input values need to be validated against a database or against other input arguments to the mutation, where it would be much more difficult to implement within a validation rule.

Shane32 avatar Aug 24 '22 16:08 Shane32

I looked through the source code and found that I need to register the validation rule when calling .AddGraphQl. Now, the code is getting called and that's great.

However, I'm experiencing a similar issue to what I first was, where calling FindAppliedDirective isn't returning anything.

using the code in https://github.com/graphql-dotnet/graphql-dotnet/blob/5.3.3/src/GraphQL/Validation/Rules.Custom/InputFieldsAndArgumentsOfCorrectLength.cs and modifying it slightly, I have:

 private static readonly INodeVisitor _nodeVisitor = new NodeVisitors(
            new MatchingNodeVisitor<GraphQLArgument>((arg, context) => CheckLength(arg, arg.Value, context.TypeInfo.GetArgument(), context)),
            new MatchingNodeVisitor<GraphQLObjectField>((field, context) =>
            {
                if (context.TypeInfo.GetInputType(1) is IInputObjectGraphType input)
                    CheckLength(field, field.Value, input.GetField(field.Name), context);
            })
        );

        private static void CheckLength(ASTNode node, GraphQLValue value, IProvideMetadata? provider, ValidationContext context)
        {
            var maxLengthDirective = provider?.FindAppliedDirective("maxLength");

and here "maxLengthDirective" is always returning null. Anything stand out that I'm doing wrong?

ygetarts avatar Aug 24 '22 23:08 ygetarts

Can you post the exact mutation you are using? Specifically wondering if the input field is a literal or a variable, within a literal object or variable object.

Shane32 avatar Aug 25 '22 00:08 Shane32

Not exactly sure why you'd be having a problem. I suggest reviewing the following:

You can see in the tests that there are comprehensive tests set up to validate the following:

  • Within a complex input object, with the directive set on the field (as in your example), and when the complex object with its string value is passed as literals, validation is correct for min/max/valid/null
  • Within a complex input object, with the directive set on the field, and when the complex object is passed as a literal with a string field passed into the literal as a variable, validation is correct for min/max/valid/null
  • Within a complex input object, with the directive set on the field, and when the complex object is passed as a variable, validation is correct for min/max/valid/null
  • When the directive is set directly on an argument, and when the argument is passed as a literal, validation is correct for min/max/valid/null
  • When the directive is set directly on an argument, and when the argument is passed as a variable, validation is correct for min/max/valid/null

So the tests appear to be testing every possible case that the included LengthDirective supports via the included InputFieldsAndArgumentsOfCorrectLength validation rule. If I was in your shoes, I'd check all the code to be sure there is no typographical errors as compared to the sample.

Note that you can look at the ValidationSchema.cs file to see how the schema was configured for the tests.

Shane32 avatar Aug 25 '22 01:08 Shane32

Agree to suggestion reviewing InputFieldsAndArgumentsOfCorrectLength and check the code for typographical errors.

sungam3r avatar Aug 25 '22 11:08 sungam3r

Can you post the exact mutation you are using? Specifically wondering if the input field is a literal or a variable, within a literal object or variable object.

Yeah, I'm sure something with this is wrong on my side. I'm not sure if this is helpful, but my code looks like:

public class BookSummaryCreateArgInputType : InputObjectGraphType<BookSummaryCreateArg>
{
  public BookSummaryCreateArgInputType()
  {
    this.Name = “BookSummaryCreateArg";
    this.Field(_ => _.Count)
                          .Directive("maxLength", x =>
                            x.AddArgument(new DirectiveArgument("min") { Name = "min", Value = 1 })
                              .AddArgument(new DirectiveArgument("max") { Name = "max", Value = 2 }));
  }
}

public class BookSummaryCreate : IGraphQLOperation
{
  public void Apply(ObjectGraphType graph)
  {
    graph.FieldDelegate<BookSummaryGraphType>(
        name: “bookSummaryCreate",
        arguments: new QueryArguments(
            new QueryArgument<NonNullGraphType<BookSummaryCreateArgInputType>> { Name = "createArg" }
        ),
        resolve: BookSummaryCreate.Resolve
    )
    .ApplyDirective("auth", "policy", “BookWriter");
  }

And I pretty much just copied and pasted from InputFieldsAndArgumentsOfCorrectLength, just substituting the name of my directive.

When I get to if (context.TypeInfo.GetInputType(1) is IInputObjectGraphType input), the type it evaluates to is BookSummaryCreateArgInputType. Not really sure if that's the issue.

ygetarts avatar Aug 25 '22 16:08 ygetarts

Sounds right. Does input.GetField(field.Name) return the proper field?

Shane32 avatar Aug 25 '22 16:08 Shane32

So if (context.TypeInfo.GetInputType(1) is IInputObjectGraphType input) always evaluates to false.

I only ever get to CheckLength when the code gets to new MatchingNodeVisitor<GraphQLArgument>((arg, context) => CheckLength(arg, arg.Value, context.TypeInfo.GetArgument(), context))

and then var lengthDirective = provider?.FindAppliedDirective("maxLength"); is always null.

ygetarts avatar Aug 25 '22 17:08 ygetarts

Must the type be IInputObjectGraphType for this to work?

Can it be of NonNullGraphType ? Like i mentioned above, the type returned when we do context.TypeInfo.GetInputType(1) is actually GraphQL.Types.NonNullGraphType<BookSummaryCreateArgInputType>

ygetarts avatar Aug 25 '22 18:08 ygetarts

And an update. I changed

public class BookSummaryCreate : IGraphQLOperation
{
  public void Apply(ObjectGraphType graph)
  {
    graph.FieldDelegate<BookSummaryGraphType>(
        name: “bookSummaryCreate",
        arguments: new QueryArguments(
            new QueryArgument<NonNullGraphType<BookSummaryCreateArgInputType>> { Name = "createArg" }
        ),
        resolve: BookSummaryCreate.Resolve
    )
    .ApplyDirective("auth", "policy", “BookWriter");
  }

to

arguments: new QueryArguments(
            new QueryArgument<BookSummaryCreateArgInputType> { Name = "createArg" }
        ),

and now it's working.

That's great, but, like I said above, is there anyway to get it working if it's wrapped in a NonNullGraphType??

EDIT: ok, looks like I can just unwrap the NonNullGraphType, something like

new MatchingNodeVisitor<GraphQLObjectField>((field, context) =>
      {
        if (context.TypeInfo.GetInputType(1) is NonNullGraphType input)
        {
          var test = input.GetNamedType();
          var test2 = test as IInputObjectGraphType;
          if (test2 != null)
          {

            CheckLength(field, field.Value, test2.GetField(field.Name), context);
          }
        }
      })

ygetarts avatar Aug 25 '22 18:08 ygetarts

if (context.TypeInfo.GetInputType(1)?.GetNamedType() is IInputObjectGraphType input) is probably what you want.

GetNamedType() unwraps list and non-null wrappers from types.

We should update our validation rule and add tests.

Shane32 avatar Aug 25 '22 20:08 Shane32

if (context.TypeInfo.GetInputType(1)?.GetNamedType() is IInputObjectGraphType input) is probably what you want.

GetNamedType() unwraps list and non-null wrappers from types.

We should update our validation rule and add tests.

This is working. Thank you for being so responsive and for the help.

ygetarts avatar Aug 26 '22 02:08 ygetarts

In the InputFieldsAndArgumentsOfCorrectLength.cs example, is it necessary to have the FieldVisitor? I removed most of the code to see if it would break anything, and it's still working. For example, I removed most of it so my file looks like:

private sealed class FieldVisitor : BaseVariableVisitor
  {
    public static readonly FieldVisitor Instance = new();
  }

  public IVariableVisitor GetVisitor(ValidationContext _) => FieldVisitor.Instance;

  public static readonly MaxAndMinValueValidationRule Instance = new();

  public ValueTask<INodeVisitor?> ValidateAsync(ValidationContext context) => new(_nodeVisitor);

  private static readonly INodeVisitor _nodeVisitor = new NodeVisitors(
........

and it seems to still visit the fields and everything works?

ygetarts avatar Aug 30 '22 15:08 ygetarts

I think it’s only used when passing values within variables. If you’re only testing literals, you probably wouldn’t notice a difference.

Shane32 avatar Aug 31 '22 04:08 Shane32