diff --git a/docs/content/recipes/modelgen-hook.md b/docs/content/recipes/modelgen-hook.md new file mode 100644 index 0000000000..edc28861fb --- /dev/null +++ b/docs/content/recipes/modelgen-hook.md @@ -0,0 +1,79 @@ +--- +title: "Allowing mutation of generated models before rendering" +description: How to use a model mutation function to insert a ORM-specific tags onto struct fields. +linkTitle: "Modelgen hook" +menu: { main: { parent: 'recipes' } } +--- + +The following recipe shows how to use a `modelgen` plugin hook to mutate generated +models before they are rendered into a resulting file. This feature has many uses but +the example focuses only on inserting ORM-specific tags into generated struct fields. This +is a common use case since it allows for better field matching of DB queries and +the generated data structure. + +First of all, we need to create a function that will mutate the generated model. +Then we can attach the function to the plugin and use it like any other plugin. + +``` go +import ( + "fmt" + "os" + + "github.com/99designs/gqlgen/api" + "github.com/99designs/gqlgen/codegen/config" + "github.com/99designs/gqlgen/plugin/modelgen" +) + +// Defining mutation function +func mutateHook(b *ModelBuild) *ModelBuild { + for _, model := range b.Models { + for _, field := range model.Fields { + field.Tag += ` orm_binding:"` + model.Name + `.` + field.Name + `"` + } + } + + return b +} + +func main() { + cfg, err := config.LoadConfigFromDefaultLocations() + if err != nil { + fmt.Fprintln(os.Stderr, "failed to load config", err.Error()) + os.Exit(2) + } + + // Attaching the mutation function onto modelgen plugin + p := modelgen.Plugin{ + MutateHook: mutateHook, + } + + err = api.Generate(cfg, + api.NoPlugins(), + api.AddPlugin(p), + ) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(3) + } +} +``` + +Now fields from generated models will contain a additional tag `orm_binding`. + +This schema: + +```graphql +type Object { + field1: String + field2: Int +} +``` + +Will gen generated into: + +```go +type Object struct { + field1 *string `json:"field1" orm_binding:"Object.field1"` + field2 *int `json:"field2" orm_binding:"Object.field2"` +} +``` diff --git a/plugin/modelgen/models.go b/plugin/modelgen/models.go index 765d83320f..b7b224d7cb 100644 --- a/plugin/modelgen/models.go +++ b/plugin/modelgen/models.go @@ -11,6 +11,12 @@ import ( "github.com/vektah/gqlparser/ast" ) +type BuildMutateHook = func(b *ModelBuild) *ModelBuild + +func defaultBuildMutateHook(b *ModelBuild) *ModelBuild { + return b +} + type ModelBuild struct { PackageName string Interfaces []*Interface @@ -50,10 +56,14 @@ type EnumValue struct { } func New() plugin.Plugin { - return &Plugin{} + return &Plugin{ + MutateHook: defaultBuildMutateHook, + } } -type Plugin struct{} +type Plugin struct { + MutateHook BuildMutateHook +} var _ plugin.ConfigMutator = &Plugin{} @@ -221,6 +231,10 @@ func (m *Plugin) MutateConfig(cfg *config.Config) error { return nil } + if m.MutateHook != nil { + b = m.MutateHook(b) + } + return templates.Render(templates.Options{ PackageName: cfg.Model.Package, Filename: cfg.Model.Filename, diff --git a/plugin/modelgen/models_test.go b/plugin/modelgen/models_test.go index 619157625c..5478576659 100644 --- a/plugin/modelgen/models_test.go +++ b/plugin/modelgen/models_test.go @@ -14,7 +14,9 @@ import ( func TestModelGeneration(t *testing.T) { cfg, err := config.LoadConfig("testdata/gqlgen.yml") require.NoError(t, err) - p := Plugin{} + p := Plugin{ + MutateHook: mutateHook, + } require.NoError(t, p.MutateConfig(cfg)) require.True(t, cfg.Models.UserDefined("MissingTypeNotNull")) @@ -42,4 +44,32 @@ func TestModelGeneration(t *testing.T) { require.True(t, len(words) > 1, "expected description %q to have more than one word", text) } }) + + t.Run("tags are applied", func(t *testing.T) { + file, err := ioutil.ReadFile("./out/generated.go") + require.NoError(t, err) + + fileText := string(file) + + expectedTags := []string{ + `json:"missing2" database:"MissingTypeNotNullmissing2"`, + `json:"name" database:"MissingInputname"`, + `json:"missing2" database:"MissingTypeNullablemissing2"`, + `json:"name" database:"TypeWithDescriptionname"`, + } + + for _, tag := range expectedTags { + require.True(t, strings.Contains(fileText, tag)) + } + }) +} + +func mutateHook(b *ModelBuild) *ModelBuild { + for _, model := range b.Models { + for _, field := range model.Fields { + field.Tag += ` database:"` + model.Name + field.Name + `"` + } + } + + return b } diff --git a/plugin/modelgen/out/generated.go b/plugin/modelgen/out/generated.go index 4914aa7c52..d37542d5e7 100644 --- a/plugin/modelgen/out/generated.go +++ b/plugin/modelgen/out/generated.go @@ -27,16 +27,16 @@ type UnionWithDescription interface { } type MissingInput struct { - Name *string `json:"name"` - Enum *MissingEnum `json:"enum"` + Name *string `json:"name" database:"MissingInputname"` + Enum *MissingEnum `json:"enum" database:"MissingInputenum"` } type MissingTypeNotNull struct { - Name string `json:"name"` - Enum MissingEnum `json:"enum"` - Int MissingInterface `json:"int"` - Existing *ExistingType `json:"existing"` - Missing2 *MissingTypeNullable `json:"missing2"` + Name string `json:"name" database:"MissingTypeNotNullname"` + Enum MissingEnum `json:"enum" database:"MissingTypeNotNullenum"` + Int MissingInterface `json:"int" database:"MissingTypeNotNullint"` + Existing *ExistingType `json:"existing" database:"MissingTypeNotNullexisting"` + Missing2 *MissingTypeNullable `json:"missing2" database:"MissingTypeNotNullmissing2"` } func (MissingTypeNotNull) IsMissingInterface() {} @@ -45,11 +45,11 @@ func (MissingTypeNotNull) IsMissingUnion() {} func (MissingTypeNotNull) IsExistingUnion() {} type MissingTypeNullable struct { - Name *string `json:"name"` - Enum *MissingEnum `json:"enum"` - Int MissingInterface `json:"int"` - Existing *ExistingType `json:"existing"` - Missing2 *MissingTypeNotNull `json:"missing2"` + Name *string `json:"name" database:"MissingTypeNullablename"` + Enum *MissingEnum `json:"enum" database:"MissingTypeNullableenum"` + Int MissingInterface `json:"int" database:"MissingTypeNullableint"` + Existing *ExistingType `json:"existing" database:"MissingTypeNullableexisting"` + Missing2 *MissingTypeNotNull `json:"missing2" database:"MissingTypeNullablemissing2"` } func (MissingTypeNullable) IsMissingInterface() {} @@ -59,7 +59,7 @@ func (MissingTypeNullable) IsExistingUnion() {} // TypeWithDescription is a type with a description type TypeWithDescription struct { - Name *string `json:"name"` + Name *string `json:"name" database:"TypeWithDescriptionname"` } func (TypeWithDescription) IsUnionWithDescription() {}