Skip to content
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

Custom struct tags #463

Closed
ghost opened this issue Dec 3, 2018 · 19 comments
Closed

Custom struct tags #463

ghost opened this issue Dec 3, 2018 · 19 comments
Labels

Comments

@ghost
Copy link

ghost commented Dec 3, 2018

Expected Behaviour

I would like to add custom tags to struct; which would be read and wrote by gqlgen generator.
For example: I have input declared in graphql schema, where I wish to define sanitization, validation and probably some convert.

All I'm saying is: let us define custom tags, you don't even need to know what they are for :)

Actual Behavior

Only json:"..." tag is generated.

Minimal graphql.schema and models to reproduce

Lets say we have input that looks something like this:

input UserInput {
  name String @input_sanitize("trim") @input_validate("not_empty")
  email String! @input_sanitize("trim,lower") @input_validate("not_empty,email")
  password String! @input_convert("bcrypt_hash") @input_validate("not_empty")
}

or

input UserInput {
  email String! @extra_tag(input_sanitize:"trim,lower" input_validate:"not_empty,email")
  ...
}

and gqlgen generator would output struct that look like this:

type UserInput struct {
	Name     *string  `input_sanitize:"trim" input_validate:"not_empty"`
	Email     string  `input_sanitize:"trim,lower" input_validate:"not_empty,email"`
	Password  string  `input_validate:"not_empty" input_convert:"bcrypt_hash"`
}

I know there is already a solution for modifying model struct, but for overwriting 300+ structs for only tag addition is really not a good clean solution.

@mathewbyrne
Copy link
Contributor

We are going to address model generation the release after next when we look at plugins. Our plan is to pull model generation out into a plugin by itself that you could then add features like this to.

As you point out, the meanwhile solution would be to have these tags manually on models and map them through to gqlgen. Not ideal, but perhaps you could add another codegen step with a custom script in the meantime?

@KellyLSB
Copy link

KellyLSB commented Jan 23, 2019

This could be great for generating Gorm compatible models (setting things like default values).

type Model struct {
	ID uuid.UUID `gorm:"type:uuid;primary_key;default:uuid_generate_v4()"`
}

@JulienBreux
Copy link

+1

input UserInput {
  email String! @extra_tag(input_sanitize:"trim,lower" input_validate:"not_empty,email")
  ...
}

@vektah vektah added v0.8 For release in v0.8 needs docs labels Feb 7, 2019
@vektah
Copy link
Collaborator

vektah commented Feb 7, 2019

model generation is now a plugin, which means you can write your own.

All thats missing is docs, and a 0.8 release.

@vektah
Copy link
Collaborator

vektah commented Mar 4, 2019

You can now do this by replacing the model generator plugin, see https://gqlgen.com/reference/plugins/

@vektah vektah closed this as completed Mar 4, 2019
@davsy
Copy link

davsy commented Apr 8, 2019

Hi all, having reviewed the documentation for model generation as a plugin, I wasn't able to find the correct location to create custom struct tags. Would very much appreciate some help. Thank you!

@IkiM0no
Copy link

IkiM0no commented Apr 11, 2019

Hi all, having reviewed the documentation for model generation as a plugin, I wasn't able to find the correct location to create custom struct tags. Would very much appreciate some help. Thank you!

Bump.
We are facing the same need for custom struct tags.
Is there an expected release date for 0.8? Thanks!

@andrdru
Copy link

andrdru commented Apr 29, 2019

Hi all, having reviewed the documentation for model generation as a plugin, I wasn't able to find the correct location to create custom struct tags. Would very much appreciate some help. Thank you!

Need it too. Is this feature already released? Could anyone provide link to doc? Thanks!

@iliasgal
Copy link

We need custom tags too. Where can we find more details and the documentation?

@giter
Copy link

giter commented Aug 16, 2019

+1 need documentation

@zhulinwei
Copy link

+1 need documentation!

@vektah vektah reopened this Feb 9, 2020
@AienTech
Copy link

AienTech commented Feb 26, 2020

for anyone who also has this issue, here is a workaround I used:

copy both models.go and models.gotpl

I copied both files from here to a folder called plugin into my project.

add a custom directive to your schema

so, we're going to introduce a new directive on our fields, you can use the following to the schema.graphql:

directive @meta(
    gorm: String,
) on OBJECT | FIELD_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION | ENUM | INPUT_OBJECT | ARGUMENT_DEFINITION

type Customer {
    website: String @meta(gorm: "VARCHAR(255)")
}

do the changes in models.go

  1. the Field struct should then look like this:
type Field struct {
	Description string
	Name        string
	Type        types.Type
	Tag         string
	Gorm        string		// this is what you are looking for
}
  1. change the plugin name:
func (m *Plugin) Name() string {
	return "mycustommodelgenerator"
}
  1. since the field you'll change is not a builtin field, you can skip it. So first get rid of the following part:
if cfg.Models.UserDefined(schemaType.Name) {	
	continue	
}

and replace it with:

if schemaType.BuiltIn {
	continue
}
  1. since you'll introduce your custom tag with a directive, you have to first check if the field actually contains the gorm tag. Therefore on the line 166-167 add the following:
gormType := ""
directive := field.Directives.ForName("meta")
if directive != nil {
	arg := directive.Arguments.ForName("gorm")
	if arg != nil {
		gormType = fmt.Sprintf("gorm:\"%s\"", arg.Value.Raw)
	}
}
  1. and at last, add it to the field:
it.Fields = append(it.Fields, &Field{
	Name:        name,
	Type:        typ,
	Description: field.Description,
	Tag:         `json:"` + field.Name + `"`,
	Gorm:        gormType,
})

change the template accordingly

what we did so far, was to tell the model generator that we will have a directive called meta and inside it, we have a field called gorm. So the model generator should actually print it to our gen_model.go or whatever name you gave it in gqlgen.yml. mine is:

model:
  filename: database/model/gql.go
  package: model

so, edit the models.gotpl you copied (I think it's between the lines 22 and 37):

{{ range $model := .Models }}
    {{with .Description }} {{.|prefixLines "// "}} {{end}}
    type {{ .Name|go }} struct {
    {{- range $field := .Fields }}
        {{- with .Description }}
            {{.|prefixLines "// "}}
        {{- end}}
        {{ $field.Name|go }} {{$field.Type | ref}} `{{$field.Tag}} {{$field.Gorm}}`
    {{- end }}
    }

    {{- range $iface := .Implements }}
        func ({{ $model.Name|go }}) Is{{ $iface|go }}() {}
    {{- end }}
{{- end}}

the part that has the {{$field.Gorm}} is where the magic happens.

declare the directive

we have to declare our directive to the configuration, you can take a look at here to get an idea of how it would be possible.
Here is my sample:

config.Directives.Meta = func(ctx context.Context, obj interface{}, next graphql.Resolver, gorm *string, baseModelIncluded *bool) (res interface{}, err error) {
	return next(ctx)
}

one other thing

What I experienced so far, the plugin gets called as soon as I run my project. So you might want to create a CLI command for your app, and there with a flag or something call the plugin. here's what I did:

bin/cli.go

package main

import (
	"github.com/99designs/gqlgen/api"
	"github.com/99designs/gqlgen/codegen/config"
	"gitlab.com/jupiter-agency/jupiter-projects/mission-control/database"
	"gitlab.com/jupiter-agency/jupiter-projects/mission-control/database/model"
	"gitlab.com/jupiter-agency/jupiter-projects/mission-control/util"
	"gitlab.com/jupiter-agency/jupiter-projects/mission-control/util/plugin"
	"log"
	"os"
	"reflect"

	"github.com/urfave/cli/v2"
)

func main() {
	app := &cli.App{
		Commands: []*cli.Command{
			{
				Name:  "generate",
				Usage: "generate graphql schema",
				Action: func(c *cli.Context) error {
					gqlgenConf, err := config.LoadConfigFromDefaultLocations()
					if err != nil {
						util.Log.Panicln("failed to load config", err.Error())
						os.Exit(2)
					}

					util.Log.Infoln("generating schema...")
					err = api.Generate(gqlgenConf,
						api.AddPlugin(plugin.New()), // This is the magic line
					)
					util.Log.Infoln("schema generation done...")
					if err != nil {
						util.Log.Panicln(err.Error())
						os.Exit(3)
					}
					os.Exit(0)
					return nil
				},
			},
	}

	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}

let me know if you encounter any bugs.

@mmcken3
Copy link

mmcken3 commented Apr 27, 2020

Another example of this for people who find this issue and do not find the proper documentation is here: https://gqlgen.com/recipes/modelgen-hook/

For the simple use case using the code located at the gqlgen docs linked above should solve the issue. For some of the more complex use cases you could expand this to accept directives and do something similar to what AienTech has done above. I like using the method in the docs because it adjusts the models at generation time.

@j75689 j75689 mentioned this issue Apr 28, 2020
2 tasks
@stale
Copy link

stale bot commented Jul 26, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Jul 26, 2020
@stale stale bot closed this as completed Aug 2, 2020
@frederikhors
Copy link
Collaborator

@AienTech your solution works amazingly fine! What does not work is autobind from config files. Some structs I'm using are already there and I need gqlgen to take them.

Maybe the step 3 is wrong?

Thanks a lot for your help.

@yerstd
Copy link

yerstd commented Apr 26, 2021

Ignore this code here

config.Directives.Meta = func(ctx context.Context, obj interface{}, next graphql.Resolver, gorm *string, baseModelIncluded *bool) (res interface{}, err error) { return next(ctx) }

Then add this line in the schema file.
directive @meta(gorm: String!) on FIELD_DEFINITION

#463 (comment)

@TuringJest
Copy link

TuringJest commented May 3, 2021

Just started to use gqlgen to add gorm flags and I also have the problem that the auto binding doesn't work with @AienTech `s solution. It will complain that the model is redeclared.
Did you find a solution for this @frederikhors?

@makivlach
Copy link

@AienTech 's solution worked for me flawlessly. Thank you!

@Virinchi2595
Copy link

Just started to use gqlgen to add gorm flags and I also have the problem that the auto binding doesn't work with @AienTech `s solution. It will complain that the model is redeclared.
Did you find a solution for this @frederikhors?

@frederikhors
any update on this?
are you able to solve autobind issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests