iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 4
1
Modern Web

Framework Surfing 30天系列 第 4

Day 4 - API server (Go, GraphQL) - part4

  • 分享至 

  • twitterImage
  •  

前言

昨天發完文之後,一直覺得怪怪的,於是今天就去翻了 graphql.go 還有 gqlgen 產生出來的 exec.go,發現有一些地方要修改,抱歉啦。

今天的目錄會長這樣(修改的地方有標注)

  • build
  • cmd
    • gql-server
      • main.go
  • internal
    • gql:這裡有修改
      • generated
        • exec.go
      • models
        • generated.go
      • resolvers
        • generated
          • generated.go
        • main.go
        • post.go
        • user.go
      • schemas
        • schema.graphql:這裡有修改
    • handlers
      • gql.go:這裡有修改
      • heartbeat.go
    • orm
      • migration
        • jobs
          • seed_users.go
        • main.go
      • models
        • base.go
        • post.go
        • user.go
      • main.go
  • pkg
    • server
      • main.go
  • scripts
    • build.sh
    • gqlgen.sh:這裡有修改
    • run.sh
  • .env
  • .env.example
  • .gitignore
  • go.mod
  • go.sum
  • gqlgen.yml:這裡有修改

第一步 - 修改 schema.graphql

$ vim internal/gql/schema/schema.graphql

scalar Time
# Types
type User {
  id: ID!
  email: String!
  userId: String
  name: String
  firstName: String
  lastName: String
  nickName: String
  description: String
  location: String
  createdAt: Time!
  updatedAt: Time
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String
  createdAt: Time!
  updatedAt: Time
  user: User
}

# Input Types
input UserInput {
  email: String
  userId: String
  displayName: String
  name: String
  firstName: String
  lastName: String
  nickName: String
  description: String
  location: String
}

input PostInput {
  title: String
  content: String
  userId: String
}

# Define mutations here
type Mutation {
  createUser(input: UserInput!): User!
  updateUser(id: ID!, input: UserInput!): User!
  deleteUser(id: ID!): Boolean!
  createPost(input: PostInput!): Post!
  updatePost(id: ID!, input: PostInput!): Post!
  deletePost(id: ID!): Boolean!
}

# Define queries here
type Query {
  users: [User!]!
  posts: [Post!]!
  user(id: ID!): User
  post(id: ID!): Post
}

第二步 - 修改 gqlgen.yml

修改這個是因為可以直接使用 orm 定義好的 models 來喂給 gelgen,這樣就不需要再將 orm 的 model 轉換成 gqlgen 的 model,而且生成的 exec.go 以及 resolvers.go 跟著改變,會幫我們將所有需要實作的 resolvers 都列出來,昨天的並沒有幫我們列出 User & Post 間需要的 resolvers。

$ vim gqlgem.yml

schema:
  - internal/gql/schemas/schema.graphql
# Let gqlgen know where to put the generated server
exec:
  filename: internal/gql/generated/exec.go # 這邊路徑有改
  package: generated
# Let gqlgen know where to put the generated models (if any)
model:
  filename: internal/gql/models/generated.go
  package: gqlmodels # 這邊 package name 有改
# Let gqlgen know where to put the generated resolvers
resolver:
  filename: internal/gql/resolvers/generated/generated.go
  type: Resolver
  package: resolvers
autobind: []
# 加上 models,並且指定到 orm 的 models
models:
  User:
    model: github.com/wtlin1228/go-gql-server/internal/orm/models.User
  Post:
    model: github.com/wtlin1228/go-gql-server/internal/orm/models.Post

修改後執行 $ scripts/gqlgen.sh

執行完 gqlgen 之後會發現自動產生的三個檔案都有變化

  1. gqlgen 的 model 裡面只剩下 PostInput 以及 UserInput
  2. gqlgen 的 exec.go 直接使用 orm 的 models
  3. resolvers 多了 postResolver 以及 userResolver

第二步 - 修改 Resolvers

一樣將 gqlgen 產生的 resolvers 檔案拆成三塊

  1. $ vim internal/gql/resolvers/main.go

    這裡跟昨天一樣幾乎沒變

     package resolvers
    
     import (
     	"github.com/wtlin1228/go-gql-server/internal/gql/generated"
     	"github.com/wtlin1228/go-gql-server/internal/orm"
     )
    
     type Resolver struct {
     	ORM *orm.ORM
     }
    
     func (r *Resolver) Mutation() generated.MutationResolver {
     	return &mutationResolver{r}
     }
     func (r *Resolver) Post() generated.PostResolver {
     	return &postResolver{r}
     }
     func (r *Resolver) Query() generated.QueryResolver {
     	return &queryResolver{r}
     }
     func (r *Resolver) User() generated.UserResolver {
     	return &userResolver{r}
     }
    
     type mutationResolver struct{ *Resolver }
    
     type queryResolver struct{ *Resolver }
    
  2. $ vim internal/gql/resolvers/post.go

    寫法跟昨天很像,只是精簡了很多,然後多了一些 resolvers

     package resolvers
    
     import (
     	"context"
    
     	"github.com/gofrs/uuid"
     	gqlmodels "github.com/wtlin1228/go-gql-server/internal/gql/models"
     	"github.com/wtlin1228/go-gql-server/internal/orm/models"
     )
    
     // Mutations
     func (r *mutationResolver) CreatePost(ctx context.Context, input gqlmodels.PostInput) (*models.Post, error) {
     	return postCreateUpdate(r, input, false)
     }
     func (r *mutationResolver) UpdatePost(ctx context.Context, id string, input gqlmodels.PostInput) (*models.Post, error) {
     	return postCreateUpdate(r, input, true, id)
     }
     func (r *mutationResolver) DeletePost(ctx context.Context, id string) (bool, error) {
     	return postDelete(r, id)
     }
    
     // Queries
     func (r *queryResolver) Posts(ctx context.Context) ([]*models.Post, error) {
     	var posts []*models.Post
     	r.ORM.DB.Find(&posts)
     	return posts, nil
     }
    
     func (r *queryResolver) Post(ctx context.Context, id string) (*models.Post, error) {
     	post := &models.Post{}
     	r.ORM.DB.First(&post)
     	return post, nil
     }
    
     type postResolver struct{ *Resolver }
    
     func (r *postResolver) ID(ctx context.Context, obj *models.Post) (string, error) {
     	return obj.ID.String(), nil
     }
     func (r *postResolver) User(ctx context.Context, obj *models.Post) (*models.User, error) {
     	return r.Query().User(ctx, obj.UserID.String())
     }
    
     // Mutation Helper functions
     func postCreateUpdate(r *mutationResolver, input gqlmodels.PostInput, update bool, ids ...string) (*models.Post, error) {
     	dbo, err := GQLInputPostToDBPost(&input, update, ids...)
     	if err != nil {
     		return nil, err
     	}
     	// Create scoped clean db interface
     	db := r.ORM.DB.New().Begin()
     	if !update {
     		db = db.Create(dbo).First(dbo) // Create the post
     	} else {
     		db = db.Model(&dbo).Update(dbo).First(dbo) // Or update it
     	}
     	if db.Error != nil {
     		db.RollbackUnlessCommitted()
     		return nil, db.Error
     	}
     	db = db.Commit()
     	return dbo, nil
     }
    
     func postDelete(r *mutationResolver, id string) (bool, error) {
     	whereID := "id = ?"
     	// Convert id from type string to type uuid.UUID
     	convertedID, err := uuid.FromString(id)
     	if err != nil {
     		return false, err
     	}
     	// Create scoped clean db interface
     	db := r.ORM.DB.New().Begin()
     	// Find the post
     	dbPost := &models.Post{}
     	err = db.Where(whereID, convertedID).First(dbPost).Error
     	if err != nil {
     		return false, err
     	}
     	// Delete the post
     	if err := db.Delete(dbPost).Error; err != nil {
     		db.RollbackUnlessCommitted()
     		return false, err
     	}
     	db = db.Commit()
     	return true, nil
     }
    
     // GQLInputPostToDBPost transforms [post] gql input to db model
     func GQLInputPostToDBPost(i *gqlmodels.PostInput, update bool, ids ...string) (o *models.Post, err error) {
     	o = &models.Post{
     		Title:   *i.Title,
     		Content: i.Content,
     	}
     	// convert the id from type String to type uuid.UUID
     	if len(ids) > 0 {
     		updID, err := uuid.FromString(ids[0])
     		if err != nil {
     			return nil, err
     		}
     		o.ID = updID
     	}
     	return o, err
     }
    
  3. $ vim internal/gql/resolvers/user.go

    寫法跟昨天很像,只是精簡了很多,然後多了一些 resolvers

     package resolvers
    
     import (
     	"context"
     	"errors"
    
     	"github.com/gofrs/uuid"
     	gqlmodels "github.com/wtlin1228/go-gql-server/internal/gql/models"
     	"github.com/wtlin1228/go-gql-server/internal/orm/models"
     )
    
     // Mutations
     func (r *mutationResolver) CreateUser(ctx context.Context, input gqlmodels.UserInput) (*models.User, error) {
     	return userCreateUpdate(r, input, false)
     }
     func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input gqlmodels.UserInput) (*models.User, error) {
     	return userCreateUpdate(r, input, true, id)
     }
     func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
     	return userDelete(r, id)
     }
    
     // Queries
     func (r *queryResolver) Users(ctx context.Context) ([]*models.User, error) {
     	var users []*models.User
     	r.ORM.DB.Preload("Posts").Find(&users)
     	return users, nil
     }
    
     func (r *queryResolver) User(ctx context.Context, id string) (*models.User, error) {
     	user := &models.User{}
     	r.ORM.DB.Preload("Posts").First(&user)
     	return user, nil
     }
    
     type userResolver struct{ *Resolver }
    
     func (r *userResolver) ID(ctx context.Context, obj *models.User) (string, error) {
     	return obj.ID.String(), nil
     }
    
     // Mutation Helper functions
     func userCreateUpdate(r *mutationResolver, input gqlmodels.UserInput, update bool, ids ...string) (*models.User, error) {
     	dbo, err := GQLInputUserToDBUser(&input, update, ids...)
     	if err != nil {
     		return nil, err
     	}
     	// Create scoped clean db interface
     	db := r.ORM.DB.New().Begin()
     	if !update {
     		db = db.Create(dbo).First(dbo) // Create the user
     	} else {
     		db = db.Model(&dbo).Update(dbo).First(dbo) // Or update it
     	}
     	if db.Error != nil {
     		db.RollbackUnlessCommitted()
     		return nil, db.Error
     	}
     	db = db.Commit()
     	return dbo, nil
     }
    
     func userDelete(r *mutationResolver, id string) (bool, error) {
     	whereID := "id = ?"
     	// Convert id to uuid.UUID from string
     	convertedID, err := uuid.FromString(id)
     	if err != nil {
     		return false, err
     	}
     	// Create scoped clean db interface
     	db := r.ORM.DB.New().Begin()
     	// Find the user
     	dbUser := &models.User{}
     	err = db.Where(whereID, convertedID).First(dbUser).Error
     	if err != nil {
     		return false, err
     	}
     	// Find the user's posts
     	dbPosts := []*models.Post{}
     	db.Model(&dbUser).Related(&dbPosts, "Posts")
     	// Delete posts
     	for _, dbPost := range dbPosts {
     		if err := db.Delete(dbPost).Error; err != nil {
     			db.RollbackUnlessCommitted()
     			return false, err
     		}
     	}
     	// Delete the user
     	if err := db.Delete(dbUser).Error; err != nil {
     		db.RollbackUnlessCommitted()
     		return false, err
     	}
     	db = db.Commit()
     	return true, nil
     }
    
     // GQLInputUserToDBUser transforms [user] gql input to db model
     func GQLInputUserToDBUser(i *gqlmodels.UserInput, update bool, ids ...string) (o *models.User, err error) {
     	o = &models.User{
     		UserID:      i.UserID,
     		Name:        i.Name,
     		FirstName:   i.FirstName,
     		LastName:    i.LastName,
     		NickName:    i.NickName,
     		Description: i.Description,
     		Location:    i.Location,
     	}
     	if i.Email == nil && !update {
     		return nil, errors.New("Field [email] is required")
     	}
     	if i.Email != nil {
     		o.Email = *i.Email
     	}
     	if len(ids) > 0 {
     		updID, err := uuid.FromString(ids[0])
     		if err != nil {
     			return nil, err
     		}
     		o.ID = updID
     	}
     	return o, err
     }
    

第三步 - 修改 handler/gql.go

package handlers

import (
	"github.com/99designs/gqlgen/handler"
	"github.com/gin-gonic/gin"
  // 改這邊
	gql "github.com/wtlin1228/go-gql-server/internal/gql/generated"
	"github.com/wtlin1228/go-gql-server/internal/gql/resolvers"
	"github.com/wtlin1228/go-gql-server/internal/orm"
)

// GraphqlHandler defines the GQLGen GraphQL server handler
func GraphqlHandler(orm *orm.ORM) gin.HandlerFunc {
	// NewExecutableSchema and Config are in the generated.go file
	c := gql.Config{
		Resolvers: &resolvers.Resolver{
			ORM: orm, // pass in the ORM instance in the resolvers to be used
		},
	}

	h := handler.GraphQL(gql.NewExecutableSchema(c))

	return func(c *gin.Context) {
		h.ServeHTTP(c.Writer, c.Request)
	}
}

// PlaygroundHandler Defines the Playground handler to expose our playground
func PlaygroundHandler(path string) gin.HandlerFunc {
	h := handler.Playground("Go GraphQL Server", path)
	return func(c *gin.Context) {
		h.ServeHTTP(c.Writer, c.Request)
	}
}

完成

$ scripts/run.sh

測試一下,沒有問題!

程式碼放在這邊 Here

推薦文章

https://gqlgen.com/getting-started/

https://stackedco.de/building-a-recipe-crud-api-with-golang-graphql-and-mysql

https://blog.csdn.net/liuyh73/article/details/85028977


上一篇
Day 3 - API server (Go, GraphQL) - part3
下一篇
Day 5 - 設計前端頁面 - part 1
系列文
Framework Surfing 30天6
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言