因為之後預計要寫的主題有 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"
)
}