diff --git a/go.sum b/go.sum index f7fcf21..56358d6 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,7 @@ github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+Licev github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/internal/di/model.go b/internal/di/model.go index 3d7c9eb..5be93c5 100644 --- a/internal/di/model.go +++ b/internal/di/model.go @@ -7,6 +7,7 @@ import ( ih "github.com/hokkung/go-groceries/internal/handler/item" ph "github.com/hokkung/go-groceries/internal/handler/product" uh "github.com/hokkung/go-groceries/internal/handler/user" + "github.com/hokkung/go-groceries/internal/middleware" repository2 "github.com/hokkung/go-groceries/internal/repository" "github.com/hokkung/go-groceries/internal/server" "github.com/hokkung/go-groceries/internal/service/item" @@ -54,6 +55,7 @@ var HandlerSet = wire.NewSet( ph.ProvideProductHandler, uh.ProvideUserHandler, ih.ProvideItemHandler, + middleware.ProvideGormMiddleware, ) var ClientSet = wire.NewSet( diff --git a/internal/di/wire_gen.go b/internal/di/wire_gen.go index e2c6ef3..4c4a314 100644 --- a/internal/di/wire_gen.go +++ b/internal/di/wire_gen.go @@ -13,6 +13,7 @@ import ( item2 "github.com/hokkung/go-groceries/internal/handler/item" product2 "github.com/hokkung/go-groceries/internal/handler/product" user2 "github.com/hokkung/go-groceries/internal/handler/user" + "github.com/hokkung/go-groceries/internal/middleware" "github.com/hokkung/go-groceries/internal/repository" "github.com/hokkung/go-groceries/internal/server" "github.com/hokkung/go-groceries/internal/service/item" @@ -29,6 +30,7 @@ func InitializeApplication(context2 context.Context) (*ApplicationAPI, func(), e if err != nil { return nil, nil, err } + gormMiddleware := middleware.ProvideGormMiddleware(db) productRepository, cleanup, err := repository.ProvideProductRepository(db) if err != nil { return nil, nil, err @@ -50,7 +52,7 @@ func InitializeApplication(context2 context.Context) (*ApplicationAPI, func(), e } itemItem := item.ProvideItem(catAPI) itemHandler := item2.ProvideItemHandler(itemItem) - serverCustomizer, cleanup3, err := server.ProvideCustomizer(product3, handler, itemHandler) + serverCustomizer, cleanup3, err := server.ProvideCustomizer(gormMiddleware, product3, handler, itemHandler) if err != nil { cleanup2() cleanup() diff --git a/internal/handler/user/user.go b/internal/handler/user/user.go index e52d207..131e5b2 100644 --- a/internal/handler/user/user.go +++ b/internal/handler/user/user.go @@ -1,10 +1,10 @@ package user import ( + "github.com/gin-gonic/gin" "github.com/hokkung/go-groceries/internal/service/user" "net/http" - - "github.com/gin-gonic/gin" + "strconv" ) type Handler struct { @@ -27,6 +27,19 @@ func ProvideUserHandler(userService user.UserService) *Handler { return NewUserHandler(userService) } -func (h *Handler) Get(id int) int { - return h.userService.Get(id) +func (h *Handler) Get(c *gin.Context) { + id := c.Param("id") + + id2, err := strconv.Atoi(id) + if err != nil { + c.JSON(500, gin.H{}) + } + + res, err := h.userService.Get(c, id2) + if err != nil { + c.JSON(500, gin.H{}) + return + } + + c.JSON(200, gin.H{"user": res}) } diff --git a/internal/middleware/gorm.go b/internal/middleware/gorm.go new file mode 100644 index 0000000..743a440 --- /dev/null +++ b/internal/middleware/gorm.go @@ -0,0 +1,37 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type GormMiddleware struct { + db *gorm.DB +} + +func NewGormMiddleware(db *gorm.DB) *GormMiddleware { + return &GormMiddleware{ + db: db, + } +} + +func ProvideGormMiddleware(db *gorm.DB) *GormMiddleware { + return NewGormMiddleware(db) +} + +func (mw *GormMiddleware) Middleware() gin.HandlerFunc { + return func(c *gin.Context) { + tx := mw.db.Begin() + c.Set("db", tx) + c.Next() + + if len(c.Errors) <= 0 { + if err := tx.Commit(); err != nil { + tx.Rollback() + } + return + } + + tx.Rollback() + } +} diff --git a/internal/repository/base.go b/internal/repository/base.go new file mode 100644 index 0000000..46f5d64 --- /dev/null +++ b/internal/repository/base.go @@ -0,0 +1,24 @@ +package repository + +import ( + "context" + "gorm.io/gorm" +) + +type Base struct { + db *gorm.DB +} + +func NewBase(db *gorm.DB) *Base { + return &Base{ + db: db, + } +} + +func (b *Base) getDB(ctx context.Context) *gorm.DB { + tx, ok := ctx.Value(GormContext).(*gorm.DB) + if !ok { + return b.db + } + return tx +} diff --git a/internal/repository/db.go b/internal/repository/db.go index bf4fd14..2b4fdfa 100644 --- a/internal/repository/db.go +++ b/internal/repository/db.go @@ -9,6 +9,10 @@ import ( entity2 "github.com/hokkung/go-groceries/internal/entity" ) +type GormKey string + +const GormContext GormKey = "gormContext" + type Entity interface { Table() string } diff --git a/internal/repository/mock/mock_user.go b/internal/repository/mock/mock_user.go index 6c666d9..085b06f 100644 --- a/internal/repository/mock/mock_user.go +++ b/internal/repository/mock/mock_user.go @@ -5,9 +5,11 @@ package mock_repository import ( + context "context" reflect "reflect" gomock "github.com/golang/mock/gomock" + entity "github.com/hokkung/go-groceries/internal/entity" ) // MockUserRepository is a mock of UserRepository interface. @@ -33,6 +35,21 @@ func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder { return m.recorder } +// FindByName mocks base method. +func (m *MockUserRepository) FindByName(ctx context.Context, name string) (entity.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByName", ctx, name) + ret0, _ := ret[0].(entity.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindByName indicates an expected call of FindByName. +func (mr *MockUserRepositoryMockRecorder) FindByName(ctx, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByName", reflect.TypeOf((*MockUserRepository)(nil).FindByName), ctx, name) +} + // Model mocks base method. func (m *MockUserRepository) Model() string { m.ctrl.T.Helper() diff --git a/internal/repository/txn.go b/internal/repository/txn.go new file mode 100644 index 0000000..8395a58 --- /dev/null +++ b/internal/repository/txn.go @@ -0,0 +1,20 @@ +package repository + +import ( + "context" + "gorm.io/gorm" +) + +var gormDB *gorm.DB + +func New(db *gorm.DB) { + gormDB = db +} + +func Txn(ctx context.Context, fn func(tx *gorm.DB) error) error { + val, ok := ctx.Value(GormContext).(*gorm.DB) + if !ok { + return fn(gormDB) + } + return fn(val) +} diff --git a/internal/repository/user.go b/internal/repository/user.go index d16e952..7d8e061 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -1,27 +1,40 @@ package repository -import "gorm.io/gorm" +import ( + "context" + "github.com/hokkung/go-groceries/internal/entity" + "gorm.io/gorm" +) - -//go:generate mockgen -source ./user.go -destination ./mock/mock_user.go +//go:generate mockgen -source ./user.go -destination ./mock/mock_user.go type UserRepository interface { Model() string + FindByName(ctx context.Context, name string) (entity.User, error) } type userRepository struct { - DB *gorm.DB + *Base } func (r *userRepository) Model() string { return "users" } +func (r *userRepository) FindByName(ctx context.Context, name string) (entity.User, error) { + var user entity.User + err := r.getDB(ctx).Model(&entity.User{}).Where("name = ?", name).First(&user).Error + if err != nil { + return entity.User{}, err + } + return user, nil +} + func NewUserRepository(db *gorm.DB) *userRepository { return &userRepository{ - DB: db, + Base: NewBase(db), } } func ProvideUserRepository(db *gorm.DB) (UserRepository, func(), error) { - return NewUserRepository(db), func(){}, nil + return NewUserRepository(db), func() {}, nil } diff --git a/internal/server/customizer.go b/internal/server/customizer.go index 3201bde..e846971 100644 --- a/internal/server/customizer.go +++ b/internal/server/customizer.go @@ -3,11 +3,11 @@ package server import ( "fmt" "github.com/99designs/gqlgen/graphql/handler" - "github.com/99designs/gqlgen/graphql/playground" "github.com/hokkung/go-groceries/graph" "github.com/hokkung/go-groceries/internal/handler/item" ph "github.com/hokkung/go-groceries/internal/handler/product" "github.com/hokkung/go-groceries/internal/handler/user" + "github.com/hokkung/go-groceries/internal/middleware" "net/http" @@ -16,6 +16,7 @@ import ( ) type Customizer struct { + gormMiddleware *middleware.GormMiddleware productHandler *ph.Product userHandler *user.Handler itemHandler *item.ItemHandler @@ -23,6 +24,7 @@ type Customizer struct { // NewCustomizer creates instance func NewCustomizer( + gormMiddleware *middleware.GormMiddleware, productHandler *ph.Product, userHandler *user.Handler, itemHandler *item.ItemHandler, @@ -31,16 +33,19 @@ func NewCustomizer( productHandler: productHandler, userHandler: userHandler, itemHandler: itemHandler, + gormMiddleware: gormMiddleware, } } // ProvideCustomizer provides instance for di func ProvideCustomizer( + gormMiddleware *middleware.GormMiddleware, productHandler *ph.Product, userHandler *user.Handler, itemHandler *item.ItemHandler, ) (srv.ServerCustomizer, func(), error) { return NewCustomizer( + gormMiddleware, productHandler, userHandler, itemHandler, @@ -48,6 +53,8 @@ func ProvideCustomizer( } func (c *Customizer) Register(s *srv.Server) { + s.Engine.Use(c.gormMiddleware.Middleware()) + s.Engine.GET("/ping", func(ctx *gin.Context) { id := c.productHandler.Get(1) ctx.JSON(http.StatusOK, gin.H{ @@ -56,26 +63,15 @@ func (c *Customizer) Register(s *srv.Server) { }) userGroup := s.Engine.Group("/users") - userGroup.POST( - "/login", - c.userHandler.Login, - ) + userGroup.POST("/login", c.userHandler.Login) + userGroup.GET("/:id", c.userHandler.Get) itemGroup := s.Engine.Group("/items") - itemGroup.GET( - "/search", - c.itemHandler.Search, - ) + itemGroup.GET("/search", c.itemHandler.Search) // TODO: refactor this gqlSrv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{}})) s.Engine.POST("/query", func(c *gin.Context) { gqlSrv.ServeHTTP(c.Writer, c.Request) }) - - // TODO: refactor this - gqlPgh := playground.Handler("GraphQL playground", "/query") - s.Engine.GET("/", func(c *gin.Context) { - gqlPgh.ServeHTTP(c.Writer, c.Request) - }) } diff --git a/internal/service/user/user.go b/internal/service/user/user.go index 3800272..a4bf77b 100644 --- a/internal/service/user/user.go +++ b/internal/service/user/user.go @@ -1,11 +1,12 @@ package user import ( + "context" "github.com/hokkung/go-groceries/internal/repository" ) type UserService interface { - Get(id int) int + Get(ctx context.Context, id int) (int, error) } type User struct { @@ -22,6 +23,6 @@ func ProvideUserService(userRepository repository.UserRepository) *User { return NewUserService(userRepository) } -func (s *User) Get(id int) int { - return id +func (s *User) Get(ctx context.Context, id int) (int, error) { + return id, nil } diff --git a/internal/service/user/user_test.go b/internal/service/user/user_test.go index 6a1d8f9..663d33e 100644 --- a/internal/service/user/user_test.go +++ b/internal/service/user/user_test.go @@ -1,6 +1,7 @@ package user_test import ( + "context" "github.com/hokkung/go-groceries/internal/service/user" "testing" @@ -24,9 +25,11 @@ func (s *UserServiceTestSuite) SetupTest() { func (s *UserServiceTestSuite) TestGet() { one := 1 - res := s.underTest.Get(one) + mockCtx := context.Background() + res, err := s.underTest.Get(mockCtx, one) s.Equal(one, res) + s.Nil(err) } func TestUserServiceTestSuite(t *testing.T) {