-
Notifications
You must be signed in to change notification settings - Fork 493
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
Added ability to use your own directives usage #446
Conversation
Thank you for your contribution. I really want custom directives to be supported in the library! There are few things to discuss, though.
func (r *Resolver) Something(ctx context.Context, req *graphql.Request) (*SomethingResponse, error) {
// req contains input args, directives and selected fields. Potentially we can add more things in future without changing the signature.
} |
Thanks for your quick & clear reply!
type PaginationArg struct {
Offset int32
}
func (r *Resolver) Something(ctx context.Context, req *graphql.Request) (*SomethingResponse, error) {
myDirective := req.Directives.Get("myCustomDirective") // No problem here, directives are graphql-go types
myPaginationArg, ok := req.Args.Get("Pagination").(*PaginationArg)
if ok {
offset := myPaginationArg.Offset
}
} Anyway, maybe it's acceptable, I will try to implement this in the coming days and will keep you informed. I just seen this PR #428 that adds selected fields into the contex, maybe it is a good solution to put the directives into the context too and provide a method to retrieve them? What is your point of view? |
@pavelnikolov After thinking about your suggestions, a colleague @iotafr 👋 point me to what Apollo is doing and I think this is a good way: https://www.apollographql.com/docs/apollo-server/schema/creating-directives/ I've updated the PR to use this kind of Visitor pattern. I think it takes the following advantages:
Directive visitor must implement the following interface and can act before the resolver to be called or after: type DirectiveVisitor interface {
Before(directive *Directive, input interface{}) error
After(directive *Directive, output interface{}) (interface{}, error)
} What do you think? I've kept the old previous logic on a branch in case you don't like this one ;) |
4a97316
to
ada73c5
Compare
pkg/common/directive.go
Outdated
@@ -1,5 +1,10 @@ | |||
package common | |||
|
|||
type DirectiveVisitor interface { |
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.
Maybe in some cases we need to access to the context at the directive level. What do you think to add ctx context.Context
in After
and Before
definitions ?
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.
You're right, I just added the context
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.
One such case would be with a caching directive, since caching info can be stored in the context.
@eko I really like the visitor pattern! |
@pavelnikolov Great thanks! I've just updated the PR with types exported by #437. I've tried to take a look at how I can retrieve the object directives but without success actually, I've to spend more time to understand the internal workflow. |
There are some conflicts. @eko can I help you to get this working? |
5f8e662
to
a030af0
Compare
Hi @pavelnikolov, Sorry for the delay, I was off for a couple of days. I just rebased the pull request in case you want to merge the first part. Unfortunately, I will not have any much time to work on other directives implementations. Thank you |
@eko thank you very much for rebasing this code. I left one comment re where to pass the directive visitors. I like this pattern but it will require some more work. |
Hello @pavelnikolov, Thank you, you're right, this is so much better to have them at the schema parse section, I just updated the PR in order to add a new Concerning the other directives implementations: yes, this implementation works with FIELD_DEFINITION type. That's a first step but other types have to also be implemented later. |
@eko OK, this is much better now that the directive visitors are not passed with |
I also found it was interesting to have them declared as schema options but let me know if you prefer to have them on root resolver. |
Merci beaucoup, @eko! I was thinking the same thing. Then I thought to myself "What if there is a resolver field named 'DirectiveVisotrs'". I know that this is highly unlikely. The notion of Schema options is that they are optional and we can turn the options on and off. However, in the case of directives, is it possible that they are included in the schema, but not included in the options? Then what do we do? Do we error out or do we ignore them? If we ignore them, some people might be confused in the other case other people will get confused. If we leave them in the resolver, how do we pass them around so that the method for directives doesn't interfere with resolver fields/methods? 🤔 |
These before/after visitors are only going to be invoked if the fields have method resolvers. How about field-resolvers? |
} | ||
} | ||
} | ||
} |
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.
These visitors will only be invoked if we use method resolvers. How about when we use struct fields as resolvers?
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 talking about lines 263 to 269
values = append(values, inValue.Interface()) | ||
} | ||
|
||
if visitorErr := visitor.Before(ctx, directive, values); err != nil { |
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.
this should be
if visitorErr := visitor.Before(ctx, directive, values); visitorErr != nil {
for _, directive := range f.field.Directives { | ||
if visitor, ok := r.Visitors[directive.Name.Name]; ok { | ||
returned, visitorErr := visitor.After(ctx, directive, result.Interface()) | ||
if err != nil { |
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.
this should be if visitorErr != nil
This only works on token based authorization, when using non-internal tokens. Existing functionality was kept backwards compatible on this POC, so if you have a token with "user:all" or "site-admin:sudo" everything works like before. Similarly session based auth still works the same. Added a directive `@authz(scopes: ["some_scope"])` which controls scopes that are required when calling the graphql API with a token based authentication. When this directive is present on a field, query, mutation or type, it is required that the token has scopes that are listed. The beauty of this approach is, that we can add different scopes to different queries/fields/mutations and single source of truth is our graphql schema. - unit tests - failing authorization on queries/mutations/fields without the directive - adding scopes to all needed graphql entities - UI changes to create tokens with more scopes - making backwards incompatible changes - authorizing internal API access Tested locally. To test locally, you need to modify `go.mod` to point to your own local fork of graph-gophers/graphql-go#446 It is also needed to apply the patch suggested in this comment: https://github.com/graph-gophers/graphql-go/pull/446/files#r914374506 To create a token with different scopes, create a normal token as you would usually by going to Settings -> Access tokens. You need to modify the scopes directly in the database (yikes!). Search the `schema.graphql` file for `@authz` directive scope definitions that are required with these changes. You then need to use the token directly with curl or similar. When using the token without proper scopes, you should see graphql errors instead of data.
This only works on token based authorization, when using non-internal tokens. Existing functionality was kept backwards compatible on this POC, so if you have a token with "user:all" or "site-admin:sudo" everything works like before. Similarly session based auth still works the same. Added a directive `@authz(scopes: ["some_scope"])` which controls scopes that are required when calling the graphql API with a token based authentication. When this directive is present on a field, query, mutation or type, it is required that the token has scopes that are listed. The beauty of this approach is, that we can add different scopes to different queries/fields/mutations and single source of truth is our graphql schema. - unit tests - failing authorization on queries/mutations/fields without the directive - adding scopes to all needed graphql entities - UI changes to create tokens with more scopes - making backwards incompatible changes - authorizing internal API access Tested locally. To test locally, you need to modify `go.mod` to point to your own local fork of graph-gophers/graphql-go#446 It is also needed to apply the patch suggested in this comment: https://github.com/graph-gophers/graphql-go/pull/446/files#r914374506 To create a token with different scopes, create a normal token as you would usually by going to Settings -> Access tokens. You need to modify the scopes directly in the database (yikes!). Search the `schema.graphql` file for `@authz` directive scope definitions that are required with these changes. You then need to use the token directly with curl or similar. When using the token without proper scopes, you should see graphql errors instead of data.
based on graph-gophers#446 and work by @eko
based on graph-gophers#446 and work by @eko
based on graph-gophers#446 and work by @eko
I am closing this PR in favor of #543 |
Hi,
Description
This PR goal is to bring ability to use so custom directives, as mentioned in issue #151.
I would be interested in having feedbacks, mainly on the way that I am currently re-using the
internal/common
package exported to be used publicly inpkg/common
: maybe I could just copy objects I need (but I need a lot of these defined in package ;-)).Example usage
Assuming that I have the following schema:
I can use the directive in my resolver by adding a new argument:
Thank you!