Skip to content

Commit

Permalink
Update extra fields type definition and plus docs about the feature (#…
Browse files Browse the repository at this point in the history
…2655)

* Update extra fields type definition and plus docs about the feature

* Update docs
  • Loading branch information
auvn authored May 30, 2023
1 parent adf5da2 commit 8e29502
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 42 deletions.
30 changes: 26 additions & 4 deletions codegen/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,10 @@ func (c *Config) injectTypesFromSchema() error {
}

type TypeMapEntry struct {
Model StringList `yaml:"model"`
Fields map[string]TypeMapField `yaml:"fields,omitempty"`
Model StringList `yaml:"model"`
Fields map[string]TypeMapField `yaml:"fields,omitempty"`

// Key is the Go name of the field.
ExtraFields map[string]ModelExtraField `yaml:"extraFields,omitempty"`
}

Expand All @@ -335,9 +337,29 @@ type TypeMapField struct {
}

type ModelExtraField struct {
Type string `yaml:"type"`
IsPointer bool `yaml:"isPointer"`
// Type is the Go type of the field.
//
// It supports the builtin basic types (like string or int64), named types
// (qualified by the full package path), pointers to those types (prefixed
// with `*`), and slices of those types (prefixed with `[]`).
//
// For example, the following are valid types:
// string
// *github.com/author/package.Type
// []string
// []*github.com/author/package.Type
//
// Note that the type will be referenced from the generated/graphql, which
// means the package it lives in must not reference the generated/graphql
// package to avoid circular imports.
// restrictions.
Type string `yaml:"type"`

// OverrideTags is an optional override of the Go field tag.
OverrideTags string `yaml:"overrideTags"`

// Description is an optional the Go field doc-comment.
Description string `yaml:"description"`
}

type StringList []string
Expand Down
47 changes: 47 additions & 0 deletions docs/content/recipes/extra_fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: "Generation of additional extra fields for internal use"
description: Pass implementation specific fields to the children resolvers without being forced to define your own types for a GraphQL model.
linkTitle: Generated Model Extra Fields
menu: { main: { parent: 'recipes' } }
---

Extra fields allows you to generate additional fields for your models.
These fields can be used at runtime when implementing field resolvers.

## Extending your models
Imagine you have a model named User and you want to extend a generated struct with additional data used in your service.

The schema is:

```graphql
type User {
id: ID!
name: String!
}
```

Extra fields can be defined in gqlgen.yaml configuration:

```yaml
models:
User:
extraFields:
Session:
description: "A Session used by this user"
type: "github.com/author/mypkg.Session"
```

The generated code would look like:

```go
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.

type User struct {
ID string
Name string
// A Session used by this user.
Session mypkg.Session
}
```

After these steps you have an extra field for your server implementation and the field is not being exposed to a caller.
24 changes: 2 additions & 22 deletions plugin/modelgen/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/99designs/gqlgen/codegen/config"
"github.com/99designs/gqlgen/codegen/templates"
"github.com/99designs/gqlgen/internal/code"
"github.com/99designs/gqlgen/plugin"
"github.com/vektah/gqlparser/v2/ast"
)
Expand Down Expand Up @@ -415,26 +414,7 @@ func (m *Plugin) generateFields(cfg *config.Config, schemaType *ast.Definition)
if len(modelcfg.ExtraFields) > 0 {
ff := make([]*Field, 0, len(modelcfg.ExtraFields))
for fname, fspec := range modelcfg.ExtraFields {
var ftype types.Type
pkg, typeName := code.PkgAndType(fspec.Type)
if pkg != "" {
var err error
ftype, err = binder.FindType(pkg, typeName)
if err != nil {
return nil, fmt.Errorf("extra fields: find type %s.%s: %w", pkg, typeName, err)
}
} else {
// switching to builtin types
ftype = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), typeName, nil),
nil,
nil,
)
}

if fspec.IsPointer {
ftype = types.NewPointer(ftype)
}
ftype := buildType(fspec.Type)

tag := `json:"-"`
if fspec.OverrideTags != "" {
Expand All @@ -446,7 +426,7 @@ func (m *Plugin) generateFields(cfg *config.Config, schemaType *ast.Definition)
Name: fname,
GoName: fname,
Type: ftype,
Description: "User defined extra field",
Description: fspec.Description,
Tag: tag,
})
}
Expand Down
1 change: 1 addition & 0 deletions plugin/modelgen/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ func TestModelGeneration(t *testing.T) {
require.IsType(t, m.FieldInt, int64(0))
require.IsType(t, m.FieldInternalType, extrafields.Type{})
require.IsType(t, m.FieldStringPtr, new(string))
require.IsType(t, m.FieldIntSlice, []int64{})
})
}

Expand Down
16 changes: 10 additions & 6 deletions plugin/modelgen/out/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions plugin/modelgen/out_nullable_input_omittable/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions plugin/modelgen/out_struct_pointers/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions plugin/modelgen/testdata/gqlgen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ models:
ExtraFieldsTest:
extraFields:
FieldInternalType:
description: "Internal field"
type: github.com/99designs/gqlgen/plugin/modelgen/internal/extrafields.Type
FieldStringPtr:
type: "string"
isPointer: true
type: "*string"
FieldInt:
type: "int64"
overrideTags: 'json:"field_int_tag"'
FieldIntSlice:
type: "[]int64"
47 changes: 47 additions & 0 deletions plugin/modelgen/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package modelgen

import (
"go/types"
"strings"
)

// buildType constructs a types.Type for the given string (using the syntax
// from the extra field config Type field).
func buildType(typeString string) types.Type {
switch {
case typeString[0] == '*':
return types.NewPointer(buildType(typeString[1:]))
case strings.HasPrefix(typeString, "[]"):
return types.NewSlice(buildType(typeString[2:]))
default:
return buildNamedType(typeString)
}
}

// buildNamedType returns the specified named or builtin type.
//
// Note that we don't look up the full types.Type object from the appropriate
// package -- gqlgen doesn't give us the package-map we'd need to do so.
// Instead we construct a placeholder type that has all the fields gqlgen
// wants. This is roughly what gqlgen itself does, anyway:
// https://github.com/99designs/gqlgen/blob/master/plugin/modelgen/models.go#L119
func buildNamedType(fullName string) types.Type {
dotIndex := strings.LastIndex(fullName, ".")
if dotIndex == -1 { // builtinType
return types.Universe.Lookup(fullName).Type()
}

// type is pkg.Name
pkgPath := fullName[:dotIndex]
typeName := fullName[dotIndex+1:]

pkgName := pkgPath
slashIndex := strings.LastIndex(pkgPath, "/")
if slashIndex != -1 {
pkgName = pkgPath[slashIndex+1:]
}

pkg := types.NewPackage(pkgPath, pkgName)
// gqlgen doesn't use some of the fields, so we leave them 0/nil
return types.NewNamed(types.NewTypeName(0, pkg, typeName, nil), nil, nil)
}

0 comments on commit 8e29502

Please sign in to comment.