因為之後預計要寫的主題有 React, Vue, Angular, Electron ... 等等的前端框架,所以就先用 Go 架一個 GraphQL API Server 吧。
今天會增加一個 Post model,也就是文章,一個 User 可以擁有很多篇文章。
明天會設計前端畫面,設計完畫面之後會再回來修改 schema。
新增一個 post model
touch /internal/orm/models/post.go
 // internal/orm/models/post.go
 package models
 import "github.com/gofrs/uuid"
 // Post defines a post for the app
 type Post struct {
 	BaseModelSoftDelete 
 	Title               string
 	Content             *string
 	UserID              uuid.UUID // User has many Posts
 }
修改 user model
 // internal/orm/models/user.go
 package models
 // User defines a user for the app
 type User struct {
 	BaseModelSoftDelete
 	Email       string  `gorm:"not null;unique_index:idx_email"`
 	UserID      *string // External user ID
 	Name        *string
 	NickName    *string
 	FirstName   *string
 	LastName    *string
 	Location    *string `gorm:"size:1048"`
 	Description *string `gorm:"size:1048"`
 	// 修改 user model
 	Posts       []*Post // User has many Posts
 }
修改 migration
 // internal/orm/migration/main.go
 package migration
 import (
 	"fmt"
 	log "log"
 	"github.com/jinzhu/gorm"
 	"github.com/wtlin1228/go-gql-server/internal/orm/migration/jobs"
 	"github.com/wtlin1228/go-gql-server/internal/orm/models"
 	"gopkg.in/gormigrate.v1"
 )
 func updateMigration(db *gorm.DB) error {
 	return db.AutoMigrate(
 		&models.User{},
 		// 修改 migration 
 		&models.Post{},
 	).Error
 }
 // ServiceAutoMigration migrates all the tables and modifications to the connected source
 func ServiceAutoMigration(db *gorm.DB) error {
 	// Keep a list of migrations here
 	m := gormigrate.New(db, gormigrate.DefaultOptions, nil)
 	m.InitSchema(func(db *gorm.DB) error {
 		log.Println("[Migration.InitSchema] Initializing database schema")
 		switch db.Dialect().GetName() {
 		case "postgres":
 			// Let's create the UUID extension, the user has to have superuser
 			// permission for now
 			db.Exec("create extension \"uuid-ossp\";")
 		}
 		if err := updateMigration(db); err != nil {
 			return fmt.Errorf("[Migration.InitSchema]: %v", err)
 		}
 		// Add more jobs, etc here
 		return nil
 	})
 	m.Migrate()
 	if err := updateMigration(db); err != nil {
 		return err
 	}
 	m = gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
 		jobs.SeedUsers,
 	})
 	return m.Migrate()
 }
修改 seed
除了新增一個使用者以外,也幫這個使用者新增一篇文章
 // internal/orm/migration/jobs/seed_users.go
 package jobs
 import (
 	"github.com/jinzhu/gorm"
 	"github.com/wtlin1228/go-gql-server/internal/orm/models"
 	"gopkg.in/gormigrate.v1"
 )
 var (
 	uname                    = "Test User"
 	fname                    = "Test"
 	lname                    = "User"
 	nname                    = "Foo Bar"
 	description              = "This is the first user ever!"
 	location                 = "His house, maybe? Wouldn't know"
 	pcontent                 = "Test post Content"
 	firstUser   *models.User = &models.User{
 		Email:       "test@test.com",
 		Name:        &uname,
 		FirstName:   &fname,
 		LastName:    &lname,
 		NickName:    &nname,
 		Description: &description,
 		Location:    &location,
 		// 修改 seed
 		Posts: []*models.Post{
 			&models.Post{
 				Title:   "Test post title",
 				Content: &pcontent,
 			},
 		},
 	}
 )
 // SeedUsers inserts the first users
 var SeedUsers *gormigrate.Migration = &gormigrate.Migration{
 	ID: "SEED_USERS",
 	Migrate: func(db *gorm.DB) error {
 		return db.Create(&firstUser).Error
 	},
 	Rollback: func(db *gorm.DB) error {
 		return db.Delete(&firstUser).Error
 	},
 }
重新 migration
$ scripts/run.sh
如此一來,資料庫裡面就會多一張 Post 的表
因為我們加了 post,所以要先修改 schema
 # internal/gql/schemas/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
 }
 # List Types
 type Users {
   count: Int # You want to return count for a grid for example
   list: [User!]! # that is why we need to specify the users object this way
 }
 type Posts {
   count: Int
   list: [Post!]!
 }
 # 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(id: ID): Users!
   posts(id: ID): Posts!
 }
用 gqlgen 產生新的 graphql 檔案,並將這些檔案複製出來
$ scripts/gqlgen.sh
完成後,就像前兩天一樣,把產生的檔案複製到對應的資料夾後 (也就是外面一層,因為我們把 gqlgen 產出的檔案都放在 generated/ 底下,只要拉出來就可以了)。
gql-server 的檔案不需要改太多,只要把 import path 後面的 generated 刪掉即可。
models 我們將 user 和 post 分開兩個檔案來放他們。
修改 resolvers
先把檔案複製出來並且分成 main.go、post.go、user.go
$ touch internal/gql/resolvers/post.go
 // internal/gql/resolvers/main.go
 package resolvers
 import (
 	"github.com/wtlin1228/go-gql-server/internal/gql"
 	"github.com/wtlin1228/go-gql-server/internal/orm"
 )
 type Resolver struct {
 	ORM *orm.ORM
 }
 func (r *Resolver) Mutation() gql.MutationResolver {
 	return &mutationResolver{r}
 }
 func (r *Resolver) Query() gql.QueryResolver {
 	return &queryResolver{r}
 }
 type mutationResolver struct{ *Resolver }
 type queryResolver struct{ *Resolver }
 -----
 // internal/gql/resolvers/post.go
 func (r *mutationResolver) CreatePost(ctx context.Context, input models.PostInput) (*models.Post, error) {
 	panic("not implemented")
 }
 func (r *mutationResolver) UpdatePost(ctx context.Context, id string, input models.PostInput) (*models.Post, error) {
 	panic("not implemented")
 }
 func (r *mutationResolver) DeletePost(ctx context.Context, id string) (bool, error) {
 	panic("not implemented")
 }
 func (r *queryResolver) Posts(ctx context.Context, id *string) (*models.Posts, error) {
 	panic("not implemented")
 }
 -----
 // internal/gql/resolvers/user.go
 func (r *mutationResolver) CreateUser(ctx context.Context, input models.UserInput) (*models.User, error) {
 	panic("not implemented")
 }
 func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input models.UserInput) (*models.User, error) {
 	panic("not implemented")
 }
 func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
 	panic("not implemented")
 }
 func (r *queryResolver) Users(ctx context.Context, id *string) (*models.Users, error) {
 	panic("not implemented")
 }
像昨天一樣,我們需要寫一個橋接器,將 orm 和 graphql 的 model 接起來,讓他們可以互相轉換
$ touch internal/gql/resolvers/transformations/post.go
 // internal/gql/resolvers/transformations/post.go 
 package transformations
 import (
 	"github.com/gofrs/uuid"
 	gql "github.com/wtlin1228/go-gql-server/internal/gql/models"
 	dbm "github.com/wtlin1228/go-gql-server/internal/orm/models"
 )
 // DBPostToGQLPost transforms [post] db input to gql type
 func DBPostToGQLPost(i *dbm.Post) (o *gql.Post, err error) {
 	o = &gql.Post{
 		ID:        i.ID.String(),
 		Title:     i.Title,
 		Content:   i.Content,
 		CreatedAt: i.CreatedAt,
 		UpdatedAt: i.UpdatedAt,
 	}
 	return o, err
 }
 // GQLInputPostToDBPost transforms [post] gql input to db model
 func GQLInputPostToDBPost(i *gql.PostInput, update bool, ids ...string) (o *dbm.Post, err error) {
 	o = &dbm.Post{
 		Title:   *i.Title,
 		Content: i.Content,
 	}
 	updUserID, err := uuid.FromString(*i.UserID)
 	if err != nil {
 		return nil, err
 	}
 	o.UserID = updUserID
 	if len(ids) > 0 {
 		updID, err := uuid.FromString(ids[0])
 		if err != nil {
 			return nil, err
 		}
 		o.ID = updID
 	}
 	return o, err
 }
將 post 和 user 的 resolvers 完成
 // internal/gql/resolvers/post.go
 package resolvers
 import (
 	"context"
 	"log"
 	"github.com/gofrs/uuid"
 	models "github.com/wtlin1228/go-gql-server/internal/gql/models"
 	tf "github.com/wtlin1228/go-gql-server/internal/gql/resolvers/transformations"
 	dbm "github.com/wtlin1228/go-gql-server/internal/orm/models"
 )
 // THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.
 func (r *mutationResolver) CreatePost(ctx context.Context, input models.PostInput) (*models.Post, error) {
 	return postCreateUpdate(r, input, false)
 }
 func (r *mutationResolver) UpdatePost(ctx context.Context, id string, input models.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)
 }
 func (r *queryResolver) Posts(ctx context.Context, id *string) (*models.Posts, error) {
 	return postList(r, id)
 }
 // ## Helper functions
 func postCreateUpdate(r *mutationResolver, input models.PostInput, update bool, ids ...string) (*models.Post, error) {
 	dbo, err := tf.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
 	}
 	gql, err := tf.DBPostToGQLPost(dbo)
 	if err != nil {
 		db.RollbackUnlessCommitted()
 		return nil, err
 	}
 	db = db.Commit()
 	return gql, db.Error
 }
 func postDelete(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 post
 	dbPost := &dbm.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
 }
 func postList(r *queryResolver, id *string) (*models.Posts, error) {
 	entity := "posts"
 	whereID := "id = ?"
 	record := &models.Posts{}
 	dbRecords := []*dbm.Post{}
 	db := r.ORM.DB.New()
 	if id != nil {
 		db = db.Where(whereID, *id)
 	}
 	db = db.Find(&dbRecords).Count(&record.Count)
 	for _, dbRec := range dbRecords {
 		if rec, err := tf.DBPostToGQLPost(dbRec); err != nil {
 			log.Println(entity, err)
 		} else {
 			// get the post's owner
 			dbUser := &dbm.User{}
 			db.Where(whereID, dbRec.UserID).First(dbUser)
 			if gqlUser, err := tf.DBUserToGQLUser(dbUser); err != nil {
 				log.Println(entity, err)
 			} else {
 				rec.User = gqlUser
 			}
 			record.List = append(record.List, rec)
 		}
 	}
 	return record, db.Error
 }
 // internal/gql/resolvers/user.go
 package resolvers
 import (
 	"context"
 	"log"
 	"github.com/gofrs/uuid"
 	models "github.com/wtlin1228/go-gql-server/internal/gql/models"
 	tf "github.com/wtlin1228/go-gql-server/internal/gql/resolvers/transformations"
 	dbm "github.com/wtlin1228/go-gql-server/internal/orm/models"
 )
 // THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.
 // CreateUser creates a record
 func (r *mutationResolver) CreateUser(ctx context.Context, input models.UserInput) (*models.User, error) {
 	return userCreateUpdate(r, input, false)
 }
 // UpdateUser updates a record
 func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input models.UserInput) (*models.User, error) {
 	return userCreateUpdate(r, input, true, id)
 }
 // DeleteUser deletes a record
 func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
 	return userDelete(r, id)
 }
 // Users lists records
 func (r *queryResolver) Users(ctx context.Context, id *string) (*models.Users, error) {
 	return userList(r, id)
 }
 // ## Helper functions
 func userCreateUpdate(r *mutationResolver, input models.UserInput, update bool, ids ...string) (*models.User, error) {
 	dbo, err := tf.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
 	}
 	gql, err := tf.DBUserToGQLUser(dbo)
 	if err != nil {
 		db.RollbackUnlessCommitted()
 		return nil, err
 	}
 	db = db.Commit()
 	return gql, db.Error
 }
 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 := &dbm.User{}
 	err = db.Where(whereID, convertedID).First(dbUser).Error
 	if err != nil {
 		return false, err
 	}
 	// Find the user's posts
 	dbPosts := []*dbm.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
 }
 func userList(r *queryResolver, id *string) (*models.Users, error) {
 	entity := "users"
 	whereID := "id = ?"
 	record := &models.Users{}
 	dbRecords := []*dbm.User{}
 	db := r.ORM.DB.New()
 	if id != nil {
 		db = db.Where(whereID, *id)
 	}
 	db = db.Find(&dbRecords).Count(&record.Count)
 	for _, dbRec := range dbRecords {
 		if rec, err := tf.DBUserToGQLUser(dbRec); err != nil {
 			log.Println(entity, err)
 		} else {
 			// get user's posts
 			dbPosts := []*dbm.Post{}
 			db.Model(&dbRec).Related(&dbPosts, "Posts")
 			for _, dbPost := range dbPosts {
 				if gqlPost, err := tf.DBPostToGQLPost(dbPost); err != nil {
 					log.Println("posts", err)
 				} else {
 					rec.Posts = append(rec.Posts, gqlPost)
 				}
 			}
 			record.List = append(record.List, rec)
 		}
 	}
 	return record, db.Error
 }
$ scripts/run.sh
新增 user
 mutation {
   createUser (
     input: {
     	name: "Leo"
       email: "wtlin1228@gmail.com"
       userId: "user-007"
       displayName: "唐唐"
     }
   ) {
     id
 		email
 		createdAt
   }
 }
新增 post
 mutation {
   createPost (input: {
     title: "今天是鐵人賽第三天"
     content: "覺得開心"
     userId: "3220bb83-59ec-44f4-9568-7b1d240bc783"
   }) {
     title
     content
     createdAt
   }
 }
列出 post
 query {
   allPosts: posts {
     count,
     list {
       id
       title
       content
     }
   }
 }
列出 post 以及這篇 post 的擁有者
 query {
   allPosts: posts {
     count,
     list {
       id
       title
       content
       user {
         id
         email
         description
       }
     }
   }
 }
列出 user 和 user 的 post
 query {
 	allUsers: users {
     count,
     list {
       id
       email
 	  userId
 	  name
 	  firstName
 	  lastName
 	  nickName
 	  description
 	  location
 	  createdAt
 	  updatedAt
       posts {
         id
         title
         content
       }
     }
   }
 }
刪除 post
 mutation {
   deletePost(
     id: "c0a66568-ea81-4f56-ac8e-dc2cba285539"
   )
 }
刪除 user 和屬於這個 user 的 post
只需要刪除 user,屬於他的 posts 就會一起被刪除
 mutation {
   deleteUser(
     id: "b557d7af-c65e-48bd-9e4d-f82f6ea54d80"
   )
 }