From f3d747eedf2dff68f5a7a311d0727f5f64174cdb Mon Sep 17 00:00:00 2001 From: Bastian Ike Date: Thu, 23 Jun 2022 16:27:11 +0200 Subject: [PATCH] feat(graphql): support directives --- command.go | 26 +++++++++++ example/graphql/generated.go | 44 ++++++++++++++++++- example/graphql/module.go | 2 +- example/graphql/resolver.go | 22 +++++++--- ...le_user_interfaces_graphql-Service.graphql | 4 +- example/user/infrastructure/user.go | 3 +- example/user/interfaces/graphql/resolver.go | 21 +++++++++ .../user/interfaces/graphql/schema.graphql | 4 +- example/user/interfaces/graphql/service.go | 3 ++ helper.go | 19 ++++++-- templates/module.go.tpl | 2 +- 11 files changed, 135 insertions(+), 15 deletions(-) diff --git a/command.go b/command.go index 40edf16..f1350c2 100644 --- a/command.go +++ b/command.go @@ -203,6 +203,15 @@ func (m *plugin) GenerateCode(data *codegen.Data) error { "gmethod": func(from, to string) string { return m.types.resolver[from][to][2] }, + "gdpkg": func(name string) string { + return m.types.directives["@"+name][0] + }, + "gdtyp": func(name string) string { + return m.types.directives["@"+name][1] + }, + "gdmethod": func(name string) string { + return m.types.directives["@"+name][2] + }, }, PackageDoc: "//+build !graphql\n", Template: ` @@ -231,6 +240,9 @@ type {{$root.TypeName}} struct { {{lcFirst $root.TypeName}}{{$object.Name}} *{{lcFirst $root.TypeName}}{{$object.Name}} {{- end }} {{- end }} + {{ range $directive := .AllDirectives }} + {{$directive.Name}}Resolver *{{lookupImport (gdpkg $directive.Name)}}.{{gdtyp $directive.Name}} + {{- end }} } func (r *{{$root.TypeName}}) Inject ( @@ -239,12 +251,26 @@ func (r *{{$root.TypeName}}) Inject ( {{lcFirst $root.TypeName}}{{$object.Name}} *{{lcFirst $root.TypeName}}{{$object.Name}}, {{- end }} {{- end }} + {{ range $directive := .AllDirectives }} + {{$directive.Name}}Resolver *{{lookupImport (gdpkg $directive.Name)}}.{{gdtyp $directive.Name}}, + {{- end }} ) { {{- range $object := .Objects }} {{- if $object.HasResolvers }} r.{{lcFirst $root.TypeName}}{{$object.Name}} = {{lcFirst $root.TypeName}}{{$object.Name}} {{- end }} {{- end }} + {{ range $directive := .AllDirectives }} + r.{{$directive.Name}}Resolver = {{$directive.Name}}Resolver + {{- end }} +} + +func (r *{{$root.TypeName}}) directives() DirectiveRoot { + return DirectiveRoot{ + {{ range $directive := .AllDirectives -}} + {{ucFirst $directive.Name}}: r.{{$directive.Name}}Resolver.{{gdmethod $directive.Name}}, + {{end}} + } } {{ range $object := .Objects -}} diff --git a/example/graphql/generated.go b/example/graphql/generated.go index 63ba44c..49188d7 100644 --- a/example/graphql/generated.go +++ b/example/graphql/generated.go @@ -44,6 +44,7 @@ type ResolverRoot interface { } type DirectiveRoot struct { + UserAttributeFilter func(ctx context.Context, obj interface{}, next graphql.Resolver, prefix string) (res interface{}, err error) } type ComplexityRoot struct { @@ -311,6 +312,21 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** +func (ec *executionContext) dir_userAttributeFilter_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["prefix"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("prefix")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["prefix"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_TodoAdd_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1065,8 +1081,32 @@ func (ec *executionContext) _User_attributes(ctx context.Context, field graphql. } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Attributes, nil + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Attributes, nil + } + directive1 := func(ctx context.Context) (interface{}, error) { + prefix, err := ec.unmarshalNString2string(ctx, "secret") + if err != nil { + return nil, err + } + if ec.directives.UserAttributeFilter == nil { + return nil, errors.New("directive userAttributeFilter is not implemented") + } + return ec.directives.UserAttributeFilter(ctx, obj, directive0, prefix) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(domain1.Attributes); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be flamingo.me/graphql/example/user/domain.Attributes`, tmp) }) if err != nil { ec.Error(ctx, err) diff --git a/example/graphql/module.go b/example/graphql/module.go index 8127e53..f74d70b 100644 --- a/example/graphql/module.go +++ b/example/graphql/module.go @@ -21,7 +21,7 @@ type Module struct{} // Configure sets the graphql.ExecutableSchema binding via a provider, passing in the correct root resolver func (*Module) Configure(injector *dingo.Injector) { injector.Bind(new(graphql.ExecutableSchema)).ToProvider(func(root *rootResolver) graphql.ExecutableSchema { - return NewExecutableSchema(Config{Resolvers: root}) + return NewExecutableSchema(Config{Resolvers: root, Directives: root.directives()}) }) injector.BindMulti(new(cobra.Command)).ToProvider(func(root *rootResolver) *cobra.Command { diff --git a/example/graphql/resolver.go b/example/graphql/resolver.go index 5aaff85..0897be0 100644 --- a/example/graphql/resolver.go +++ b/example/graphql/resolver.go @@ -8,11 +8,11 @@ package graphql import ( "context" - graphql1 "flamingo.me/graphql" + graphql2 "flamingo.me/graphql" "flamingo.me/graphql/example/todo" "flamingo.me/graphql/example/todo/domain" domain1 "flamingo.me/graphql/example/user/domain" - graphql2 "flamingo.me/graphql/example/user/interfaces/graphql" + graphql1 "flamingo.me/graphql/example/user/interfaces/graphql" ) var _ ResolverRoot = new(rootResolver) @@ -21,16 +21,28 @@ type rootResolver struct { rootResolverMutation *rootResolverMutation rootResolverQuery *rootResolverQuery rootResolverUser *rootResolverUser + + userAttributeFilterResolver *graphql1.UserQueryResolver } func (r *rootResolver) Inject( rootResolverMutation *rootResolverMutation, rootResolverQuery *rootResolverQuery, rootResolverUser *rootResolverUser, + + userAttributeFilterResolver *graphql1.UserQueryResolver, ) { r.rootResolverMutation = rootResolverMutation r.rootResolverQuery = rootResolverQuery r.rootResolverUser = rootResolverUser + + r.userAttributeFilterResolver = userAttributeFilterResolver +} + +func (r *rootResolver) directives() DirectiveRoot { + return DirectiveRoot{ + UserAttributeFilter: r.userAttributeFilterResolver.UserAttributeFilter, + } } func (r *rootResolver) Mutation() MutationResolver { @@ -50,7 +62,7 @@ type rootResolverMutation struct { } func (r *rootResolverMutation) Inject( - mutationFlamingo *graphql1.FlamingoQueryResolver, + mutationFlamingo *graphql2.FlamingoQueryResolver, mutationTodoAdd *todo.MutationResolver, mutationTodoDone *todo.MutationResolver, ) { @@ -75,8 +87,8 @@ type rootResolverQuery struct { } func (r *rootResolverQuery) Inject( - queryFlamingo *graphql1.FlamingoQueryResolver, - queryUser *graphql2.UserQueryResolver, + queryFlamingo *graphql2.FlamingoQueryResolver, + queryUser *graphql1.UserQueryResolver, ) { r.resolveFlamingo = queryFlamingo.Flamingo r.resolveUser = queryUser.User diff --git a/example/graphql/schema/flamingo.me_graphql_example_user_interfaces_graphql-Service.graphql b/example/graphql/schema/flamingo.me_graphql_example_user_interfaces_graphql-Service.graphql index 64fbca9..70cf6da 100644 --- a/example/graphql/schema/flamingo.me_graphql_example_user_interfaces_graphql-Service.graphql +++ b/example/graphql/schema/flamingo.me_graphql_example_user_interfaces_graphql-Service.graphql @@ -4,7 +4,9 @@ type User { attributes: User_Attributes! } -type User_Attributes { +directive @userAttributeFilter(prefix: String!) on OBJECT + +type User_Attributes @userAttributeFilter(prefix: "secret") { keys: [String!]! get(key: String!): String! } diff --git a/example/user/infrastructure/user.go b/example/user/infrastructure/user.go index f6fb18d..26128da 100644 --- a/example/user/infrastructure/user.go +++ b/example/user/infrastructure/user.go @@ -15,7 +15,8 @@ func (us *UserServiceImpl) UserByID(_ context.Context, id string) (*domain.User, Name: "User " + id, Nicknames: []string{"nick", id}, Attributes: map[string]interface{}{ - "movie": "starwars", + "movie": "starwars", + "secret_crush": "r2d2", }, }, nil } diff --git a/example/user/interfaces/graphql/resolver.go b/example/user/interfaces/graphql/resolver.go index 3db623f..fc012e5 100644 --- a/example/user/interfaces/graphql/resolver.go +++ b/example/user/interfaces/graphql/resolver.go @@ -2,8 +2,11 @@ package graphql import ( "context" + "fmt" + "strings" "flamingo.me/graphql/example/user/domain" + "github.com/99designs/gqlgen/graphql" ) // UserQueryResolver resolver for the user service @@ -21,3 +24,21 @@ func (r *UserQueryResolver) Inject(userService domain.UserService) *UserQueryRes func (r *UserQueryResolver) User(ctx context.Context, id string) (*domain.User, error) { return r.userService.UserByID(ctx, id) } + +// UserAttributeFilter directive +func (r *UserQueryResolver) UserAttributeFilter(ctx context.Context, obj interface{}, next graphql.Resolver, prefix string) (res interface{}, err error) { + rawAttributes, err := next(ctx) + if err != nil { + return nil, err + } + attributes, ok := rawAttributes.(domain.Attributes) + if !ok { + return nil, fmt.Errorf("no attributes returned") + } + for k := range attributes { + if strings.HasPrefix(k, prefix) { + delete(attributes, k) + } + } + return attributes, nil +} diff --git a/example/user/interfaces/graphql/schema.graphql b/example/user/interfaces/graphql/schema.graphql index 64fbca9..70cf6da 100644 --- a/example/user/interfaces/graphql/schema.graphql +++ b/example/user/interfaces/graphql/schema.graphql @@ -4,7 +4,9 @@ type User { attributes: User_Attributes! } -type User_Attributes { +directive @userAttributeFilter(prefix: String!) on OBJECT + +type User_Attributes @userAttributeFilter(prefix: "secret") { keys: [String!]! get(key: String!): String! } diff --git a/example/user/interfaces/graphql/service.go b/example/user/interfaces/graphql/service.go index e212a32..53102fd 100644 --- a/example/user/interfaces/graphql/service.go +++ b/example/user/interfaces/graphql/service.go @@ -2,6 +2,7 @@ package graphql import ( // embed schema.grapqhl + _ "embed" "flamingo.me/graphql" @@ -24,4 +25,6 @@ func (*Service) Types(types *graphql.Types) { types.Map("User", domain.User{}) types.Map("User_Attributes", domain.Attributes{}) types.Resolve("Query", "User", UserQueryResolver{}, "User") + + types.Directive("@userAttributeFilter", UserQueryResolver{}, "UserAttributeFilter") } diff --git a/helper.go b/helper.go index d7bd395..3183409 100644 --- a/helper.go +++ b/helper.go @@ -7,9 +7,10 @@ import ( // Types represent information on Object->Go type mappings and resolvers type Types struct { - names map[string]string - resolver map[string]map[string][3]string - fields map[string]map[string]string + names map[string]string + resolver map[string]map[string][3]string + fields map[string]map[string]string + directives map[string][3]string } // Map references a graphql type to a go type @@ -53,6 +54,18 @@ func (tc *Types) GoField(graphqlType, graphqlField, goField string) { tc.fields[graphqlType][graphqlField] = goField } +// Directive specifies a directive resolver for a graphql directive +func (tc *Types) Directive(graphqlDirective string, typ interface{}, method string) { + if tc.directives == nil { + tc.directives = make(map[string][3]string) + } + t := reflect.TypeOf(typ) + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + tc.directives[graphqlDirective] = [3]string{t.PkgPath(), t.Name(), method} +} + // FlamingoQueryResolver always resolves to the string "flamingo" for the default schemas. type FlamingoQueryResolver struct{} diff --git a/templates/module.go.tpl b/templates/module.go.tpl index 8127e53..f74d70b 100644 --- a/templates/module.go.tpl +++ b/templates/module.go.tpl @@ -21,7 +21,7 @@ type Module struct{} // Configure sets the graphql.ExecutableSchema binding via a provider, passing in the correct root resolver func (*Module) Configure(injector *dingo.Injector) { injector.Bind(new(graphql.ExecutableSchema)).ToProvider(func(root *rootResolver) graphql.ExecutableSchema { - return NewExecutableSchema(Config{Resolvers: root}) + return NewExecutableSchema(Config{Resolvers: root, Directives: root.directives()}) }) injector.BindMulti(new(cobra.Command)).ToProvider(func(root *rootResolver) *cobra.Command {