Gqlgen: Custom struct tags

Created on 3 Dec 2018  路  16Comments  路  Source: 99designs/gqlgen

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.

needs docs plugin stale v0.8

Most helpful comment

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!

All 16 comments

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?

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()"`
}

+1

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

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

All thats missing is docs, and a 0.8 release.

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

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!

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!

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!

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

+1 need documentation

+1 need documentation!

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.

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.

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.

Nope, stale!

Please reopen, guys.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

theoks picture theoks  路  3Comments

ksoda picture ksoda  路  3Comments

huanghantao picture huanghantao  路  3Comments

bieber picture bieber  路  4Comments

RobertoOrtis picture RobertoOrtis  路  3Comments