Skip to content

Commit

Permalink
Add gqlgen custom code generator
Browse files Browse the repository at this point in the history
  • Loading branch information
yasuflatland-lf committed Jul 26, 2024
1 parent 3fede10 commit 0413eec
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 43 deletions.
2 changes: 1 addition & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ gen: ## Generate GraphQL implementations
printf "${GREEN} Update gqlgen\n\n"; \
go get github.com/99designs/gqlgen@latest; cd ${ORG_PATH} ; \
printf "${GREEN} Generating GraphQL related sources\n\n"; \
go run github.com/99designs/gqlgen generate; cd ${ORG_PATH}; \
go run ./cmd/gqlgenerate; cd ${ORG_PATH}; \
printf "${GREEN}Done\n"; \

.PHONY: fmt
Expand Down
5 changes: 5 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ This backend uses some generated code. Here are the steps to generate code.
2. At `/backend`, run `make parsergen`

# Tips
## How to generate GraphQL Model under /graph/model
Right under the
```
go run ./cmd/gqlgenerate
```
## How to see the Make Commands
```
make help
Expand Down
76 changes: 75 additions & 1 deletion backend/cmd/gqlgenerate/main.go
Original file line number Diff line number Diff line change
@@ -1 +1,75 @@
package gqlgenerate
package main

import (
"fmt"
"log"
"os"

"github.com/99designs/gqlgen/api"
"github.com/99designs/gqlgen/codegen/config"
"github.com/99designs/gqlgen/plugin/modelgen"
"github.com/vektah/gqlparser/v2/ast"
)

// Exit codes
const (
exitCodeConfigLoadError = 2
exitCodeGenerateError = 3
)

func main() {
// Initialize logger
logger := log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)

// Load the GraphQL configuration
cfg, err := loadGraphQLConfig()
if err != nil {
logger.Fatalf("Error loading GraphQL config: %v", err)
}

// Generate the GraphQL server code
err = generateGraphQLCode(cfg, logger)
if err != nil {
logger.Fatalf("Error generating GraphQL code: %v", err)
}
}

// loadGraphQLConfig loads the GraphQL configuration from default locations
func loadGraphQLConfig() (*config.Config, error) {
cfg, err := config.LoadConfigFromDefaultLocations()
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
return cfg, nil
}

// generateGraphQLCode generates the GraphQL server code using the provided config
func generateGraphQLCode(cfg *config.Config, logger *log.Logger) error {
// Attaching the mutation function onto modelgen plugin
p := modelgen.Plugin{
FieldHook: ValidationFieldHook,
}

// Generate the code using the API
err := api.Generate(cfg, api.ReplacePlugin(&p))
if err != nil {
return fmt.Errorf("code generation failed: %w", err)
}
logger.Println("GraphQL code generation successful")
return nil
}

// ValidationFieldHook is a custom hook for adding validation tags to fields based on directives
func ValidationFieldHook(td *ast.Definition, fd *ast.FieldDefinition, f *modelgen.Field) (*modelgen.Field, error) {
// Look for the "validation" directive on the field
c := fd.Directives.ForName("validation")
if c != nil {
// Add validation tag based on the "format" argument in the directive
formatConstraint := c.Arguments.ForName("format")
if formatConstraint != nil {
// Use a format that avoids double quoting
f.Tag += fmt.Sprintf(` validate:"%s"`, formatConstraint.Value.Raw)
}
}
return f, nil
}
61 changes: 61 additions & 0 deletions backend/cmd/gqlgenerate/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"log"
"os"
"testing"

"github.com/99designs/gqlgen/plugin/modelgen"
"github.com/vektah/gqlparser/v2/ast"
)

func TestLoadGraphQLConfig(t *testing.T) {
_, err := loadGraphQLConfig()
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}

func TestGenerateGraphQLCode(t *testing.T) {
logger := log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
cfg, err := loadGraphQLConfig()
if err != nil {
t.Fatalf("Failed to load config: %v", err)
}

err = generateGraphQLCode(cfg, logger)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
}

func TestValidationFieldHook(t *testing.T) {
// Mock field definitions and directive
fd := &ast.FieldDefinition{
Directives: ast.DirectiveList{
{
Name: "validation",
Arguments: ast.ArgumentList{
{
Name: "format",
Value: &ast.Value{
Raw: "email",
Kind: ast.StringValue,
},
},
},
},
},
}
f := &modelgen.Field{}

_, err := ValidationFieldHook(nil, fd, f)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

expectedTag := ` validate:"email"`
if f.Tag != expectedTag {
t.Fatalf("Expected tag %v, got %v", expectedTag, f.Tag)
}
}
3 changes: 3 additions & 0 deletions backend/gqlgen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,6 @@ models:
Time:
model:
- github.com/99designs/gqlgen/graphql.Time
directives:
validation:
skip_runtime: true
30 changes: 15 additions & 15 deletions backend/graph/db/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,36 @@ import (
type User struct {
ID int64 `gorm:"column:id;primaryKey"`
Name string `gorm:"column:name;not null" validate:"required,fl_name"`
Created time.Time `gorm:"column:created;autoCreateTime" validate:"required"`
Updated time.Time `gorm:"column:updated;autoCreateTime" validate:"required"`
Created time.Time `gorm:"column:created;autoCreateTime"`
Updated time.Time `gorm:"column:updated;autoCreateTime"`
CardGroups []Cardgroup `gorm:"many2many:cardgroup_users"`
Roles []Role `gorm:"many2many:user_roles"`
}

type Card struct {
ID int64 `gorm:"column:id;primaryKey"`
Front string `gorm:"column:front;not null" validate:"required"`
Back string `gorm:"column:back;not null" validate:"required"`
ReviewDate time.Time `gorm:"column:review_date;not null" validate:"required"`
IntervalDays int `gorm:"column:interval_days;default:1;not null" validate:"required"`
Created time.Time `gorm:"column:created;autoCreateTime" validate:"required"`
Updated time.Time `gorm:"column:updated;autoCreateTime" validate:"required"`
CardGroupID int64 `gorm:"column:cardgroup_id" validate:"required"`
Front string `gorm:"column:front;not null" validate:"required,min=1"`
Back string `gorm:"column:back;not null" validate:"required,min=1"`
ReviewDate time.Time `gorm:"column:review_date;not null"`
IntervalDays int `gorm:"column:interval_days;default:1;not null" validate:"gee=1"`
Created time.Time `gorm:"column:created;autoCreateTime"`
Updated time.Time `gorm:"column:updated;autoCreateTime"`
CardGroupID int64 `gorm:"column:cardgroup_id"`
}

type Cardgroup struct {
ID int64 `gorm:"column:id;primaryKey"`
Name string `gorm:"column:name;not null" validate:"required,fl_name"`
Created time.Time `gorm:"column:created;autoCreateTime" validate:"required"`
Updated time.Time `gorm:"column:updated;autoCreateTime" validate:"required"`
Name string `gorm:"column:name;not null" validate:"required,fl_name,min=1"`
Created time.Time `gorm:"column:created;autoCreateTime"`
Updated time.Time `gorm:"column:updated;autoCreateTime"`
Cards []Card `gorm:"foreignKey:CardGroupID"`
Users []User `gorm:"many2many:cardgroup_users"`
}

type Role struct {
ID int64 `gorm:"column:id;primaryKey"`
Name string `gorm:"column:name;not null" validate:"required,fl_name"`
Name string `gorm:"column:name;not null" validate:"required,fl_name,min=1"`
Users []User `gorm:"many2many:user_roles"`
Created time.Time `gorm:"column:created;autoCreateTime" validate:"required"`
Updated time.Time `gorm:"column:updated;autoCreateTime" validate:"required"`
Created time.Time `gorm:"column:created;autoCreateTime"`
Updated time.Time `gorm:"column:updated;autoCreateTime"`
}
50 changes: 50 additions & 0 deletions backend/graph/directive.go
Original file line number Diff line number Diff line change
@@ -1 +1,51 @@
package graph

import (
"context"
"errors"
"regexp"

"github.com/99designs/gqlgen/graphql"
)

//gqlgen

//
// #directive @constraint(
//# minLength: Int,
//# maxLength: Int,
//# min: Int,
//# max: Int,
//# pattern: String) on INPUT_FIELD_DEFINITION

func Constraint(ctx context.Context, obj interface{}, next graphql.Resolver, minLength *int, maxLength *int, min *int, max *int, pattern *string) (interface{}, error) {
val, err := next(ctx)
if err != nil {
return nil, err
}

switch v := val.(type) {
case string:
if minLength != nil && len(v) < *minLength {
return nil, errors.New("value is too short")
}
if maxLength != nil && len(v) > *maxLength {
return nil, errors.New("value is too long")
}
if pattern != nil {
matched, _ := regexp.MatchString(*pattern, v)
if !matched {
return nil, errors.New("value does not match pattern")
}
}
case int:
if min != nil && v < *min {
return nil, errors.New("value is too small")
}
if max != nil && v > *max {
return nil, errors.New("value is too large")
}
}

return val, nil
}
40 changes: 39 additions & 1 deletion backend/graph/generated.go

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

Loading

0 comments on commit 0413eec

Please sign in to comment.