graphql-dotnet
graphql-dotnet copied to clipboard
Can't get directive to work
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?
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.
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.
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.
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!
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.
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?
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.
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?
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.
Not exactly sure why you'd be having a problem. I suggest reviewing the following:
- InputFieldsAndArgumentsOfCorrectLength.cs
- LengthDirective.cs
- InputFieldsAndArgumentsOfCorrectLengthTests.cs
- ValidationSchema.cs
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.
Agree to suggestion reviewing InputFieldsAndArgumentsOfCorrectLength
and check the code for typographical errors.
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.
Sounds right. Does input.GetField(field.Name)
return the proper field?
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.
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>
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);
}
}
})
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.
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.
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?
I think it’s only used when passing values within variables. If you’re only testing literals, you probably wouldn’t notice a difference.