-
Notifications
You must be signed in to change notification settings - Fork 284
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
Validate at schema build time that @DgsData and @InputArgument match actual schema elements #1565
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes look great! Thanks for the fixes!
"on parameter named `$paramName` has no matching argument with name `$argName` in the GraphQL schema. " + | ||
arguments | ||
) | ||
is FallbackEnvironmentArgumentResolver -> throw DataFetcherInputArgumentSchemaMismatchException( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be good to add a comment on when the fallback resolver is invoked in this scenario for future reference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will do, FallbackEnvironmentArgumentResolver
covers the case where there is no @InputArgument
as a last resort it uses the method parameter name, below named name
:
@DgsComponent
class Fetcher {
@DgsData(parentType = "Query", field = "hello")
fun someFetcher(name: String?): String {
return "Hello, ${name ?: "no name"}"
}
}
type Query {
hello(name: String): String
}
It's covered by test case named: when no @InputArgument fallback to method param name to match argument on schema
private val parameterNameDiscoverer: ParameterNameDiscoverer = DefaultParameterNameDiscoverer() | ||
) { | ||
|
||
private val resolvers = ArgumentResolverComposite(argumentResolvers) | ||
|
||
fun checkInputArgumentsAreValid(method: Method, argumentNames: Set<String>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that this method needs to live in MethodDataFetcherFactory
. I think some of the checks themselves are basically a no-op in practice, since we're filtering parameters that are annotated with InputArgument
, and the InputArgumentResolver
matches any method parameter with this annotation. I think the SchemaProvider
could simply perform the check directly by just collecting field names from the annotated parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me reply to both of your comments in one go as they are related.
I'm not sure that this method needs to live in MethodDataFetcherFactory. I think some of the checks themselves are basically a no-op in practice, since we're filtering parameters that are annotated with InputArgument, and the InputArgumentResolver matches any method parameter with this annotation. I think the SchemaProvider could simply perform the check directly by just collecting field names from the annotated parameters.
I went back and forth on this, between adding the logic in the Factory
or in the SchemaProvider
and I opted for the Factory
for the following reasons:
-
if we were to add this in the schema provider, we would need to inject the resolvers that are also injected in the
Factory
. TheArgumentResolverComposite
would also possibly need to be replicated to match what is being done at runtime. This would make the argument resolution & validation logic more brittle as it would live in multiple places. -
The argument resolvers list is auto-wired and DGS owners could be injecting their own list of ArgumentResolvers. If that's the case our validation should run if and only if our specific
InputArgument
resolver would be selected by the composite. The way to do that without leaking the implementation elsewhere was to place it in theFactory
.
I also contemplated the idea of introducing a new ArgumentResolver
interface method so that each argument resolver could optionally perform their own validation, but I felt ArgumentResolver
is about resolution not validation and so I abandoned the idea. We could introduce yet another interface only for validation but I thought it would be overkill for now, at least. Happy to revisit.
Kind of related to my other comment. Is there a test that actually exercises this case? I don't know that it's possible, since we are only checking parameters that are annotated with @InputArgument, so basically by definition the resolver would always be InputArgumentResolver. IMO it isn't necessary to check the resolvers, we could simply validate that the field name from the annotation matches something from the schema, and I don't know that it needs to live here.
Great catch, I thought I added one but I think I got confused on the behavior of the FallbackEnvironmentArgumentResolver
initially and forgot to go back and update. The idea is, if the FallbackEnvironmentArgumentResolver
is selected, the resolved argument name will be the method parameter name and we can check whether there is a matching argument on the schema. I added a couple of tests and fixed the factory logic to look at all the method parameters instead of the ones annotated with @InputArgument
in my last commit.
Now the problem is the following test is failing An unknown argument should be null, and not result in an error
which leads me to believe that the behavior of having a null
value passed in when nothing matches was intended. We can definitely argue about whether it was a good decision, but at this point changing this behavior would be effectively breaking.
To conclude, I'm leaning towards only running validation for fields annotated with @InputArgument
, the same way I'm doing it now just we won't look at FallbackEnvironmentArgumentResolver
and keep the behavior of a fallback as a best effort, last resort resolver that doesn't get any schema build time validation benefit. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tinnou and I just had a quick discussion and decided on
(1) Removing the logic for checking the fallback resolver altogether, and that would allow anything without an @InputArgument to go through
(2) Introduce a helper that does line 40 and 51 in the MethodDataFetcherFactory. This will allow other classes to determine the resolver used for a given method
(3) Move the rest of the validation logic to the schema provider, which will use this new helper. It would be good I think to have the validation logic use the InputArgumentResolver to avoid leaking/reimplementing details of how @InputArgument annotation works - specifically since you can have something like @InputArgument(name=something) somethingElse: String
Does that seem reasonable?
"on parameter named `$paramName` has no matching argument with name `$argName` in the GraphQL schema. " + | ||
arguments | ||
) | ||
is FallbackEnvironmentArgumentResolver -> throw DataFetcherInputArgumentSchemaMismatchException( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kind of related to my other comment. Is there a test that actually exercises this case? I don't know that it's possible, since we are only checking parameters that are annotated with @InputArgument
, so basically by definition the resolver would always be InputArgumentResolver
. IMO it isn't necessary to check the resolvers, we could simply validate that the field name from the annotation matches something from the schema, and I don't know that it needs to live here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the changes, looks good to me! One minor suggestion for addition test case when schemaValidationWiring is disabled.
@@ -890,6 +891,139 @@ internal class DgsSchemaProviderTest { | |||
} | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add a test that verifies validation does NOT happen when the schemaValidationWiring flag is set to false?
… the schema. Ignoring Unions for now since developers might want to declare a data fetcher that applies to a subset of the possible types that actually have the field present.
…uments on the schema.
…Argument Add tests to cover the fallback behavior. The following test fails `An unknown argument should be null, and not result in an error` which puts in question the ability to validate parameters without @InputArgument.
@DgsQuery, @DgsMutation and @DgsSubscription are all aliases of @DgsData so the validation works naturally with them.
Pull request checklist
first
Pull Request type
Changes in this PR
Current issues:
@DgsData
that become orphan where there is no matching field on the schema that is wired to it. I can't think of any use case where this is not a developer error. Currently the framework will not fail (only log at warn level) when a request is processed. This is too late in the development workflow and can be easily missed by service owners.@InputArgument
can be missed if the annotation name value or method parameter name doesn't match any argument of the corresponding field on the schema.Proposed solution:
Add some validation at schema build time in the
SchemaProvider
to prevent the DGS from starting if there are obvious orphan parameters or data fetchers.Note: It is currently possible to wire a data fetcher on a field of every possible type of a union through the current wiring logic. However it is not guaranteed that every member type will have the requested field. In case of unions, I chose to not perform any validation as a subset of the types could have the field while others would not.
Alternatives considered
We could keep the status quo, as in, we would not perform any ahead-of-time validation and only log are a higher level. This does not come with the benefit of failing fast when building the schema vs at runtime when executing requests.