From 18df531c4c6d4ab73824904cf715b8407722f941 Mon Sep 17 00:00:00 2001 From: Taufik Januar Date: Sun, 17 Apr 2022 15:35:00 +0700 Subject: [PATCH 1/6] feat #time 3h #comment implement service,repository like & comment features --- application/dependency.go | 16 +++ controller/blog.go | 28 ++++- infrastructure/http.go | 10 ++ model/blog.go | 14 +-- model/comment.go | 24 ++++ model/like.go | 10 ++ repository/blog.go | 6 + repository/comment.go | 223 ++++++++++++++++++++++++++++++++++++++ repository/like.go | 118 ++++++++++++++++++++ service/blog.go | 30 +++++ 10 files changed, 467 insertions(+), 12 deletions(-) create mode 100644 model/comment.go create mode 100644 model/like.go create mode 100644 repository/comment.go create mode 100644 repository/like.go diff --git a/application/dependency.go b/application/dependency.go index cd4d512..2f54bba 100644 --- a/application/dependency.go +++ b/application/dependency.go @@ -153,11 +153,27 @@ func setupBlogDependency(app *App) *controller.BlogController { DB: app.DB, } + commentRepo := &repository.CommentRepository{ + Context: app.Context, + Config: app.Config, + Logger: app.Logger, + DB: app.DB, + } + + likeRepo := &repository.LikeRepository{ + Context: app.Context, + Config: app.Config, + Logger: app.Logger, + DB: app.DB, + } + blogSvc := &service.BlogService{ Context: app.Context, Config: app.Config, Logger: app.Logger, BlogRepo: blogRepo, + CommentRepo: commentRepo, + LikeRepo: likeRepo, } blogCtrl := &controller.BlogController{ diff --git a/controller/blog.go b/controller/blog.go index 037594a..8b9707b 100644 --- a/controller/blog.go +++ b/controller/blog.go @@ -23,6 +23,14 @@ type ( DetailBlog(ctx *fiber.Ctx) error UpdateBlog(ctx *fiber.Ctx) error DeleteBlog(ctx *fiber.Ctx) error + + CreateComment(ctx *fiber.Ctx) error + UpdateComment(ctx *fiber.Ctx) error + ListComment(ctx *fiber.Ctx) error + DeleteComment(ctx *fiber.Ctx) error + + Like(ctx *fiber.Ctx) error + UnLike(ctx *fiber.Ctx) error } // BlogController is an app blog struct that consists of all the dependencies needed for blog controller @@ -198,4 +206,22 @@ func (bc *BlogController) DeleteBlog(ctx *fiber.Ctx) error { } return helper.ResponseFormatter[any](ctx,fiber.StatusOK,nil,"Success Delete Blog",nil,nil) -} \ No newline at end of file +} + +// CreateComment responsible to creating a comment from controller layer +func (bc *BlogController) CreateComment(ctx *fiber.Ctx) error {return nil} + +// UpdateComment responsible to updating a comment by id from controller layer +func (bc *BlogController) UpdateComment(ctx *fiber.Ctx) error {return nil} + +// ListComment responsible to getting comments by blog id from controller layer +func (bc *BlogController) ListComment(ctx *fiber.Ctx) error {return nil} + +// DeleteComment responsible to deleting a comment by id from controller layer +func (bc *BlogController) DeleteComment(ctx *fiber.Ctx) error {return nil} + +// Like responsible to creating a like / liking a blog +func (bc *BlogController) Like(ctx *fiber.Ctx) error {return nil} + +// UnLike responsible to deleting a like by id / unliking a blog +func (bc *BlogController) UnLike(ctx *fiber.Ctx) error {return nil} diff --git a/infrastructure/http.go b/infrastructure/http.go index c4341d8..b76530d 100644 --- a/infrastructure/http.go +++ b/infrastructure/http.go @@ -60,6 +60,16 @@ func setupRouter(app *application.App) { v1.Get("/blog/:slug",dep.BlogController.DetailBlog) v1.Put("/blog/:id",m.ValidateJWTMiddleware,m.AuthorOnlyMiddleware,dep.BlogController.UpdateBlog) v1.Delete("/blog/:id",m.ValidateJWTMiddleware,m.AuthorOnlyMiddleware,dep.BlogController.DeleteBlog) + + // BLOG COMMENT SECTION + v1.Post("/blog/:id/comment",m.ValidateJWTMiddleware,dep.BlogController.CreateComment) + v1.Put("/blog/:id/comment/:comment_id",m.ValidateJWTMiddleware,dep.BlogController.UpdateComment) + v1.Get("/blog/:id/comment",m.ValidateJWTMiddleware,dep.BlogController.ListBlog) + v1.Delete("/blog/:id/:comment_id",m.ValidateJWTMiddleware,dep.BlogController.DeleteComment) + + // BLOG LIKE SECTION + v1.Post("/blog/:id/like",m.ValidateJWTMiddleware,dep.BlogController.Like) + v1.Delete("/blog/:id/unlike",m.ValidateJWTMiddleware,dep.BlogController.DeleteComment) } } diff --git a/model/blog.go b/model/blog.go index ad74d90..5556a6f 100644 --- a/model/blog.go +++ b/model/blog.go @@ -2,15 +2,9 @@ package model import ( "database/sql" - "regexp" "time" ) -var ( - // NoSpecialChar is regex validation where str is not contains a special characters - NoSpecialChar *regexp.Regexp -) - type ( // CreateBlogRequest consist data for creating a blog CreateBlogRequest struct { @@ -42,6 +36,7 @@ type ( ViewBlogResponse struct { ID int `db:"id" json:"id"` Title string `db:"title" json:"title"` + Slug string `db:"slug" json:"slug"` Body string `db:"body" json:"body"` Footer string `db:"footer" json:"footer"` UserID int `db:"user_id" json:"user_id"` @@ -57,6 +52,7 @@ type ( BlogResponse struct { ID int `db:"id" json:"id"` Title string `db:"title" json:"title"` + Slug string `db:"slug" json:"slug"` Body string `db:"body" json:"body"` Footer string `db:"footer" json:"footer"` UserID int `db:"user_id" json:"user_id"` @@ -68,8 +64,4 @@ type ( UpdatedAt time.Time `db:"updated_at" json:"updated_at,omitempty"` UpdatedBy *string `db:"updated_by" json:"updated_by,omitempty"` } -) - -func init() { - NoSpecialChar = regexp.MustCompile(`[^\w]`) -} \ No newline at end of file +) \ No newline at end of file diff --git a/model/comment.go b/model/comment.go new file mode 100644 index 0000000..98f312e --- /dev/null +++ b/model/comment.go @@ -0,0 +1,24 @@ +package model + +import "time" + +type ( + + // CommentRequest consist data for requesting create/update comment + CommentRequest struct { + Comment string `json:"comment"` + UserID int `json:"user_id"` + } + + // ViewCommentResponse consist data of comments + ViewCommentResponse struct { + ID int `db:"id" json:"id"` + Comment string `db:"comment" json:"comment"` + BlogID int `db:"article_id" json:"blog_id"` + UserID int `db:"user_id" json:"user_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + CreatedBy string `db:"created_by" json:"created_by"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at,omitempty"` + UpdatedBy *string `db:"updated_by" json:"updated_by,omitempty"` + } +) \ No newline at end of file diff --git a/model/like.go b/model/like.go new file mode 100644 index 0000000..89180a1 --- /dev/null +++ b/model/like.go @@ -0,0 +1,10 @@ +package model + +type ( + + // LikeRequest consist data for requesting create like + LikeRequest struct { + Like int `json:"like"` + UserID int `json:"user_id"` + } +) \ No newline at end of file diff --git a/repository/blog.go b/repository/blog.go index 2deacdd..ce12c47 100644 --- a/repository/blog.go +++ b/repository/blog.go @@ -52,6 +52,7 @@ func (br *BlogRepository) GetAll(page int, size int, order string, field string, SELECT id, title, + slug, body, footer, user_id, @@ -152,6 +153,7 @@ func (br *BlogRepository) GetAll(page int, size int, order string, field string, err := rows.Scan( &data.ID, &data.Title, + &data.Slug, &data.Body, &data.Footer, &data.UserID, @@ -183,6 +185,7 @@ func (br *BlogRepository) GetBySlug(slug string) (*model.ViewBlogResponse,error) SELECT id, title, + slug, body, footer, user_id, @@ -201,6 +204,7 @@ func (br *BlogRepository) GetBySlug(slug string) (*model.ViewBlogResponse,error) err := row.Scan( &blog.ID, &blog.Title, + &blog.Slug, &blog.Body, &blog.Footer, &blog.UserID, @@ -228,6 +232,7 @@ func (br *BlogRepository) GetByID(id int) (*model.ViewBlogResponse,error) { SELECT id, title, + slug, body, footer, user_id, @@ -246,6 +251,7 @@ func (br *BlogRepository) GetByID(id int) (*model.ViewBlogResponse,error) { err := row.Scan( &blog.ID, &blog.Title, + &blog.Slug, &blog.Body, &blog.Footer, &blog.UserID, diff --git a/repository/comment.go b/repository/comment.go new file mode 100644 index 0000000..6f6bb41 --- /dev/null +++ b/repository/comment.go @@ -0,0 +1,223 @@ +package repository + +import ( + "context" + "fmt" + + "github.com/PickHD/pickablog/config" + "github.com/PickHD/pickablog/helper" + "github.com/PickHD/pickablog/model" + "github.com/jackc/pgx/v4" + "github.com/sirupsen/logrus" +) + +type ( + // ICommentRepository is an interface that has all the function to be implemented inside comment repository + ICommentRepository interface { + Create(blogID int,req model.CommentRequest,createdBy string) error + UpdateByID(id int,req map[string]interface{}, updatedBy string) error + GetAllByBlogID(blogID int,page int, size int, order string, field string) ([]model.ViewCommentResponse,int,error) + DeleteByID(blogID,commentID int) error + } + + // CommentRepository is an app comment struct that consists of all the dependencies needed for comment repository + CommentRepository struct { + Context context.Context + Config *config.Configuration + Logger *logrus.Logger + DB *pgx.Conn + } +) + +// Create repository layer for executing command to creating comments +func (cr *CommentRepository) Create(blogID int, req model.CommentRequest,createdBy string) error { + tx,err := cr.DB.Begin(cr.Context) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.Create BeginTX ERROR %v MSG %s",err,err.Error())) + return err + } + + qInsert := `INSERT INTO comments (comment,article_id,user_id,created_by) VALUES ($1,$2,$3,$4) RETURNING id` + + var commentID int + err = tx.QueryRow(cr.Context,qInsert,req.Comment,blogID,req.UserID,createdBy).Scan(&commentID) + if err != nil { + err = tx.Rollback(cr.Context) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.Create.QueryRow.Scan Rollback ERROR %v MSG %s",err,err.Error())) + return err + } + + cr.Logger.Error(fmt.Errorf("CommentRepository.Create.QueryRow Scan ERROR %v MSG %s",err,err.Error())) + return err + } + + qUpdate := `UPDATE article SET comments = ARRAY_APPEND(comments,$1) WHERE id = $2` + + _,err = tx.Exec(cr.Context,qUpdate,commentID,blogID) + if err != nil { + err = tx.Rollback(cr.Context) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.Create.Exec Rollback ERROR %v MSG %s",err,err.Error())) + return err + } + + cr.Logger.Error(fmt.Errorf("CommentRepository.Create Exec ERROR %v MSG %s",err,err.Error())) + return err + } + + err = tx.Commit(cr.Context) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.Create Commit ERROR %v MSG %s",err,err.Error())) + return err + } + + return nil +} + +// UpdateByID repository layer for executing command to updating a comment by id +func (cr *CommentRepository) UpdateByID(id int, req map[string]interface{}, updatedBy string) error { + req["updated_by"] = updatedBy + req["id"] = id + + tx,err := cr.DB.Begin(cr.Context) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.UpdateByID BeginTX ERROR %v MSG %s",err,err.Error())) + return err + } + + q,args,err := helper.QueryUpdateBuilder("comments",req,[]string{"id"}) + if err != nil { + err = tx.Rollback(cr.Context) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.UpdateByID.QueryUpdateBuilder Rollback ERROR %v MSG %s",err,err.Error())) + return err + } + + cr.Logger.Error(fmt.Errorf("CommentRepository.UpdateByID QueryUpdateBuilder ERROR %v MSG %s",err,err.Error())) + return err + } + + cr.Logger.Info(fmt.Sprintf("Query : %s Args : %v",q,args)) + + _,err = tx.Exec(cr.Context,q,args...) + if err != nil { + err = tx.Rollback(cr.Context) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.UpdateByID.Exec Rollback ERROR %v MSG %s",err,err.Error())) + return err + } + + + cr.Logger.Error(fmt.Errorf("CommentRepository.UpdateByID Exec ERROR %v MSG %s",err,err.Error())) + return err + } + + err = tx.Commit(cr.Context) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.UpdateByID Commit ERROR %v MSG %s",err,err.Error())) + return err + } + + return nil +} + +// GetAllByBlogID repository layer for querying command to getting all comments +func (cr *CommentRepository) GetAllByBlogID(blogID int,page int, size int, order string, field string) ([]model.ViewCommentResponse,int,error) { + q :=fmt.Sprintf(` + SELECT + id, + comment, + article_id, + user_id, + created_at, + created_by, + updated_at, + updated_by + FROM comments + WHERE article_id = %d + `,blogID) + qCount := fmt.Sprintf(`SELECT 1 FROM comments WHERE article_id = %d`,blogID) + + limit := size + 1 + offset := (page - 1) * size + orderBy := fmt.Sprintf(" ORDER BY %s %s LIMIT %d OFFSET %d ",field, order, limit, offset) + + query := fmt.Sprintf("%s %s", q, orderBy) + queryCount := fmt.Sprintf("SELECT COUNT (*) FROM ( %s ) AS article_count ",qCount) + + cr.Logger.Info(fmt.Sprintf("Query : %s",query)) + cr.Logger.Info(fmt.Sprintf("Query Count : %s",queryCount)) + + var totalData int + err := cr.DB.QueryRow(cr.Context,queryCount).Scan(&totalData) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.GetAllByBlogID Scan ERROR %v MSG %s",err,err.Error())) + return nil,0,err + } + + rows,err := cr.DB.Query(cr.Context,query) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.GetAllByBlogID Query ERROR %v MSG %s",err,err.Error())) + return nil,0,err + } + + var listData []model.ViewCommentResponse + for rows.Next() { + data := &model.ViewCommentResponse{} + err := rows.Scan(&data.ID,&data.Comment,&data.BlogID,&data.UserID,&data.CreatedAt,&data.CreatedBy,&data.UpdatedAt,&data.UpdatedBy) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.GetAllByBlogID rows.Next Scan ERROR %v MSG %s",err,err.Error())) + return nil,0,err + } + + listData = append(listData, *data) + } + + return listData,totalData,nil +} + +// DeleteByID repository layer for executing command to deleting a comment by id +func (cr *CommentRepository) DeleteByID(blogID int,commentID int) error { + tx,err := cr.DB.Begin(cr.Context) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.DeleteByID BeginTX ERROR %v MSG %s",err,err.Error())) + return err + } + + qDelete := `DELETE FROM comments WHERE id = $1` + + _,err = tx.Exec(cr.Context,qDelete,commentID) + if err != nil { + err = tx.Rollback(cr.Context) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.DeleteByID.Exec Rollback ERROR %v MSG %s",err,err.Error())) + return err + } + + cr.Logger.Error(fmt.Errorf("CommentRepository.DeleteByID Exec ERROR %v MSG %s",err,err.Error())) + return err + } + + qUpdate := `UPDATE article SET comments = ARRAY_REMOVE(comments,$1) WHERE id = $2` + + _,err = tx.Exec(cr.Context,qUpdate,blogID) + if err != nil { + err = tx.Rollback(cr.Context) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.DeleteByID.Exec Rollback ERROR %v MSG %s",err,err.Error())) + return err + } + + cr.Logger.Error(fmt.Errorf("CommentRepository.DeleteByID Exec ERROR %v MSG %s",err,err.Error())) + return err + } + + err = tx.Commit(cr.Context) + if err != nil { + cr.Logger.Error(fmt.Errorf("CommentRepository.DeleteByID Commit ERROR %v MSG %s",err,err.Error())) + return err + } + + return nil +} \ No newline at end of file diff --git a/repository/like.go b/repository/like.go new file mode 100644 index 0000000..49a6352 --- /dev/null +++ b/repository/like.go @@ -0,0 +1,118 @@ +package repository + +import ( + "context" + "fmt" + + "github.com/PickHD/pickablog/config" + "github.com/PickHD/pickablog/model" + "github.com/jackc/pgx/v4" + "github.com/sirupsen/logrus" +) + +type ( + // ILikeRepository is an interface that has all the function to be implemented inside like repository + ILikeRepository interface { + Create(blogID int,req model.LikeRequest,createdBy string) error + DeleteByID(blogID int,likeID int) error + } + + // LikeRepository is an app like struct that consists of all the dependencies needed for like repository + LikeRepository struct { + Context context.Context + Config *config.Configuration + Logger *logrus.Logger + DB *pgx.Conn + } +) + +// Create repository layer for executing command creating like +func (lr *LikeRepository) Create(blogID int,req model.LikeRequest,createdBy string) error { + tx,err := lr.DB.Begin(lr.Context) + if err != nil { + lr.Logger.Error(fmt.Errorf("LikeRepository.Create BeginTX ERROR %v MSG %s",err,err.Error())) + return err + } + + qInsert := `INSERT INTO likes (like_count,article_id,user_id,created_by) VALUES ($1,$2,$3,$4) RETURNING id` + + var commentID int + err = tx.QueryRow(lr.Context,qInsert,req.Like,blogID,req.UserID,createdBy).Scan(&commentID) + if err != nil { + err = tx.Rollback(lr.Context) + if err != nil { + lr.Logger.Error(fmt.Errorf("LikeRepository.Create.QueryRow.Scan Rollback ERROR %v MSG %s",err,err.Error())) + return err + } + + lr.Logger.Error(fmt.Errorf("LikeRepository.Create.QueryRow Scan ERROR %v MSG %s",err,err.Error())) + return err + } + + qUpdate := `UPDATE article SET likes = ARRAY_APPEND(likes,$1) WHERE id = $2` + + _,err = tx.Exec(lr.Context,qUpdate,commentID,blogID) + if err != nil { + err = tx.Rollback(lr.Context) + if err != nil { + lr.Logger.Error(fmt.Errorf("LikeRepository.Create.Exec Rollback ERROR %v MSG %s",err,err.Error())) + return err + } + + lr.Logger.Error(fmt.Errorf("LikeRepository.Create Exec ERROR %v MSG %s",err,err.Error())) + return err + } + + err = tx.Commit(lr.Context) + if err != nil { + lr.Logger.Error(fmt.Errorf("LikeRepository.Create Commit ERROR %v MSG %s",err,err.Error())) + return err + } + + return nil +} + +// DeleteByID repository layer for executing command deleting like by id +func (lr *LikeRepository) DeleteByID(blogID int,likeID int) error { + tx,err := lr.DB.Begin(lr.Context) + if err != nil { + lr.Logger.Error(fmt.Errorf("LikeRepository.DeleteByID BeginTX ERROR %v MSG %s",err,err.Error())) + return err + } + + qDelete := `DELETE FROM likes WHERE id = $1` + + _,err = tx.Exec(lr.Context,qDelete,likeID) + if err != nil { + err = tx.Rollback(lr.Context) + if err != nil { + lr.Logger.Error(fmt.Errorf("LikeRepository.DeleteByID.Exec Rollback ERROR %v MSG %s",err,err.Error())) + return err + } + + lr.Logger.Error(fmt.Errorf("LikeRepository.DeleteByID Exec ERROR %v MSG %s",err,err.Error())) + return err + } + + qUpdate := `UPDATE article SET comments = ARRAY_REMOVE(comments,$1) WHERE id = $2` + + _,err = tx.Exec(lr.Context,qUpdate,blogID) + if err != nil { + err = tx.Rollback(lr.Context) + if err != nil { + lr.Logger.Error(fmt.Errorf("LikeRepository.DeleteByID.Exec Rollback ERROR %v MSG %s",err,err.Error())) + return err + } + + lr.Logger.Error(fmt.Errorf("LikeRepository.DeleteByID Exec ERROR %v MSG %s",err,err.Error())) + return err + } + + err = tx.Commit(lr.Context) + if err != nil { + lr.Logger.Error(fmt.Errorf("LikeRepository.DeleteByID Commit ERROR %v MSG %s",err,err.Error())) + return err + } + + return nil +} \ No newline at end of file diff --git a/service/blog.go b/service/blog.go index 2f4475f..761e052 100644 --- a/service/blog.go +++ b/service/blog.go @@ -20,6 +20,14 @@ type ( GetBlogBySlugSvc(slug string) (*model.BlogResponse,error) UpdateBlogSvc(id int, req model.UpdateBlogRequest, updatedBy string) error DeleteBlogSvc(id int) error + + CreateCommentSvc(blogID int,req model.CommentRequest,createdBy string) error + UpdateCommentSvc(id int, req model.CommentRequest, updatedBy string) error + GetCommentsByBlogSvc(blogID int,page int, size int, order string, field string) ([]model.ViewCommentResponse,*model.Metadata,error) + DeleteCommentSvc(blogID int, commentID int) error + + CreateLikeSvc(blogID int,req model.LikeRequest,createdBy string) error + DeleteLikeSvc(blogID int,likeID int) error } // BlogRepository is an app blog struct that consists of all the dependencies needed for blog service @@ -28,6 +36,8 @@ type ( Config *config.Configuration Logger *logrus.Logger BlogRepo repository.IBlogRepository + CommentRepo repository.ICommentRepository + LikeRepo repository.ILikeRepository } ) @@ -75,6 +85,7 @@ func (bs *BlogService) GetAllBlogSvc(page int, size int, order string, field str d.ID= r.ID d.Title = r.Title + d.Slug = r.Slug d.Body = r.Body d.Footer = r.Footer d.UserID = r.UserID @@ -133,6 +144,7 @@ func (bs *BlogService) GetBlogBySlugSvc(slug string) (*model.BlogResponse,error) data.ID= r.ID data.Title = r.Title + data.Slug = r.Slug data.Body = r.Body data.Footer = r.Footer data.UserID = r.UserID @@ -211,6 +223,24 @@ func (bs *BlogService) DeleteBlogSvc(id int) error { return nil } +// CreateCommentSvc service layer for handling creating comments +func (bs *BlogService) CreateCommentSvc(blogID int, req model.CommentRequest,createdBy string) error {return nil} + +// UpdateCommentSvc service layer for handling updating comment by id +func (bs *BlogService) UpdateCommentSvc(id int, req model.CommentRequest,updatedBy string) error {return nil} + +// GetCommentsByBlogSvc service layer for handling getting comments with filter +func (bs *BlogService) GetCommentsByBlogSvc(blogID int, page int, size int, order string, field string) ([]model.ViewCommentResponse,*model.Metadata,error) {return nil,nil,nil} + +// DeleteCommentSvc service layer for handling deleting comment with id +func (bs *BlogService) DeleteCommentSvc(blogID int, commentID int) error {return nil} + +// CreateLikeSvc service layer for handling creating likes +func (bs *BlogService) CreateLikeSvc(blogID int, req model.LikeRequest,createdBy string) error {return nil} + +// DeleteLikeSvc service layer for handling deleting like with id +func (bs *BlogService) DeleteLikeSvc(blogID int,likeID int) error {return nil} + // validateCreateBlogRequest responsible to validating create blog request func validateCreateBlogRequest(req *model.CreateBlogRequest) error { if req.Title == "" || req.Body == "" || req.Footer == "" || len(req.Tags) < 1 { From f1306b6a4640296258cc4879090d1ccf94bdf7f9 Mon Sep 17 00:00:00 2001 From: Taufik Januar Date: Sun, 17 Apr 2022 15:38:32 +0700 Subject: [PATCH 2/6] fix #time 1m #comment fix route unlike handlers --- infrastructure/http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/http.go b/infrastructure/http.go index b76530d..ee71ba4 100644 --- a/infrastructure/http.go +++ b/infrastructure/http.go @@ -69,7 +69,7 @@ func setupRouter(app *application.App) { // BLOG LIKE SECTION v1.Post("/blog/:id/like",m.ValidateJWTMiddleware,dep.BlogController.Like) - v1.Delete("/blog/:id/unlike",m.ValidateJWTMiddleware,dep.BlogController.DeleteComment) + v1.Delete("/blog/:id/unlike",m.ValidateJWTMiddleware,dep.BlogController.UnLike) } } From bced2c1e8de72aba225a30bf57a1f9a6adb6b65a Mon Sep 17 00:00:00 2001 From: Taufik Januar Date: Sun, 17 Apr 2022 15:42:42 +0700 Subject: [PATCH 3/6] fix #time 3m #comment fixing args DB Exec --- repository/comment.go | 2 +- repository/like.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/repository/comment.go b/repository/comment.go index 6f6bb41..e5f0faf 100644 --- a/repository/comment.go +++ b/repository/comment.go @@ -201,7 +201,7 @@ func (cr *CommentRepository) DeleteByID(blogID int,commentID int) error { qUpdate := `UPDATE article SET comments = ARRAY_REMOVE(comments,$1) WHERE id = $2` - _,err = tx.Exec(cr.Context,qUpdate,blogID) + _,err = tx.Exec(cr.Context,qUpdate,commentID,blogID) if err != nil { err = tx.Rollback(cr.Context) if err != nil { diff --git a/repository/like.go b/repository/like.go index 49a6352..eb8be54 100644 --- a/repository/like.go +++ b/repository/like.go @@ -36,8 +36,8 @@ func (lr *LikeRepository) Create(blogID int,req model.LikeRequest,createdBy stri qInsert := `INSERT INTO likes (like_count,article_id,user_id,created_by) VALUES ($1,$2,$3,$4) RETURNING id` - var commentID int - err = tx.QueryRow(lr.Context,qInsert,req.Like,blogID,req.UserID,createdBy).Scan(&commentID) + var likeID int + err = tx.QueryRow(lr.Context,qInsert,req.Like,blogID,req.UserID,createdBy).Scan(&likeID) if err != nil { err = tx.Rollback(lr.Context) if err != nil { @@ -51,7 +51,7 @@ func (lr *LikeRepository) Create(blogID int,req model.LikeRequest,createdBy stri qUpdate := `UPDATE article SET likes = ARRAY_APPEND(likes,$1) WHERE id = $2` - _,err = tx.Exec(lr.Context,qUpdate,commentID,blogID) + _,err = tx.Exec(lr.Context,qUpdate,likeID,blogID) if err != nil { err = tx.Rollback(lr.Context) if err != nil { @@ -96,7 +96,7 @@ func (lr *LikeRepository) DeleteByID(blogID int,likeID int) error { qUpdate := `UPDATE article SET comments = ARRAY_REMOVE(comments,$1) WHERE id = $2` - _,err = tx.Exec(lr.Context,qUpdate,blogID) + _,err = tx.Exec(lr.Context,qUpdate,likeID,blogID) if err != nil { err = tx.Rollback(lr.Context) if err != nil { From 91431d82d44e4ac3e835b08552c341d04a25cf27 Mon Sep 17 00:00:00 2001 From: Taufik Januar Date: Fri, 22 Apr 2022 22:06:12 +0700 Subject: [PATCH 4/6] feat #time 2m #comment inject rwmutex inside struct blog --- application/dependency.go | 6 ++++++ service/blog.go | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/application/dependency.go b/application/dependency.go index 2f54bba..d3ed31b 100644 --- a/application/dependency.go +++ b/application/dependency.go @@ -1,6 +1,8 @@ package application import ( + "sync" + "github.com/PickHD/pickablog/controller" "github.com/PickHD/pickablog/repository" "github.com/PickHD/pickablog/requester" @@ -146,6 +148,9 @@ func setupUserDependency(app *App) *controller.UserController { // setupBlogDependency is a function to set up dependencies to be used inside blog controller layer func setupBlogDependency(app *App) *controller.BlogController { + //init mutex + mutex := &sync.RWMutex{} + blogRepo := &repository.BlogRepository{ Context: app.Context, Config: app.Config, @@ -174,6 +179,7 @@ func setupBlogDependency(app *App) *controller.BlogController { BlogRepo: blogRepo, CommentRepo: commentRepo, LikeRepo: likeRepo, + Mutex: mutex, } blogCtrl := &controller.BlogController{ diff --git a/service/blog.go b/service/blog.go index 761e052..d50b40a 100644 --- a/service/blog.go +++ b/service/blog.go @@ -2,14 +2,15 @@ package service import ( "context" + "sync" "github.com/PickHD/pickablog/config" "github.com/PickHD/pickablog/helper" "github.com/PickHD/pickablog/model" "github.com/PickHD/pickablog/repository" + "github.com/gosimple/slug" "github.com/jackc/pgx/v4" "github.com/sirupsen/logrus" - "github.com/gosimple/slug" ) type ( @@ -38,6 +39,7 @@ type ( BlogRepo repository.IBlogRepository CommentRepo repository.ICommentRepository LikeRepo repository.ILikeRepository + Mutex *sync.RWMutex } ) From 5efdce037cb48a83a0a7271bb083b331efd4665d Mon Sep 17 00:00:00 2001 From: Taufik Januar Date: Sat, 30 Apr 2022 19:44:19 +0700 Subject: [PATCH 5/6] feat #time 1h #comment implement like & comment services --- application/dependency.go | 8 ++ model/error.go | 4 + model/like.go | 8 ++ repository/comment.go | 32 ++++++ repository/like.go | 30 ++++++ service/blog.go | 200 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 276 insertions(+), 6 deletions(-) diff --git a/application/dependency.go b/application/dependency.go index d3ed31b..97c4c89 100644 --- a/application/dependency.go +++ b/application/dependency.go @@ -172,6 +172,13 @@ func setupBlogDependency(app *App) *controller.BlogController { DB: app.DB, } + userRepo := &repository.UserRepository{ + Context: app.Context, + Config: app.Config, + Logger: app.Logger, + DB: app.DB, + } + blogSvc := &service.BlogService{ Context: app.Context, Config: app.Config, @@ -179,6 +186,7 @@ func setupBlogDependency(app *App) *controller.BlogController { BlogRepo: blogRepo, CommentRepo: commentRepo, LikeRepo: likeRepo, + UserRepo: userRepo, Mutex: mutex, } diff --git a/model/error.go b/model/error.go index 0f32519..0f1d72c 100644 --- a/model/error.go +++ b/model/error.go @@ -31,6 +31,10 @@ var ( ErrTagNotFound = errors.New("tag is not found") // ErrBlogNotFound occurs when blog is not found in database ErrBlogNotFound = errors.New("blog is not found") + // ErrCommentNotFound occurs when comment is not found in database + ErrCommentNotFound = errors.New("comment is not found") + // ErrLikeNotFound occurs when like is not found in database + ErrLikeNotFound = errors.New("like is not found") // ErrInvalidPassword occurs when password user inputed is invalid ErrInvalidPassword = errors.New("invalid password") diff --git a/model/like.go b/model/like.go index 89180a1..785f859 100644 --- a/model/like.go +++ b/model/like.go @@ -7,4 +7,12 @@ type ( Like int `json:"like"` UserID int `json:"user_id"` } + + // ViewLikeResponse consist data of like response + ViewLikeResponse struct { + ID int `db:"id" json:"id"` + Like int `db:"like_count" json:"like"` + UserID int `db:"user_id" json:"user_id"` + BlogID int `db:"article_id" json:"blog_id"` + } ) \ No newline at end of file diff --git a/repository/comment.go b/repository/comment.go index e5f0faf..e4f93e1 100644 --- a/repository/comment.go +++ b/repository/comment.go @@ -15,6 +15,7 @@ type ( // ICommentRepository is an interface that has all the function to be implemented inside comment repository ICommentRepository interface { Create(blogID int,req model.CommentRequest,createdBy string) error + GetByID(id int) (*model.ViewCommentResponse,error) UpdateByID(id int,req map[string]interface{}, updatedBy string) error GetAllByBlogID(blogID int,page int, size int, order string, field string) ([]model.ViewCommentResponse,int,error) DeleteByID(blogID,commentID int) error @@ -75,6 +76,37 @@ func (cr *CommentRepository) Create(blogID int, req model.CommentRequest,created return nil } +// GetByID repository layer for querying command to getting detail comment by id +func (cr *CommentRepository) GetByID(id int) (*model.ViewCommentResponse,error) { + var comment model.ViewCommentResponse + + q := ` + SELECT + id, + comment, + article_id, + user_id, + created_at, + created_by, + updated_at, + updated_by + FROM comments + WHERE id = $1` + + row := cr.DB.QueryRow(cr.Context,q,id) + err := row.Scan(&comment.ID,&comment.Comment,&comment.BlogID,&comment.UserID,&comment.CreatedAt,&comment.CreatedBy,&comment.UpdatedAt,&comment.UpdatedBy) + if err != nil { + if err == pgx.ErrNoRows { + cr.Logger.Info(fmt.Errorf("CommentRepository.GetByID INFO %v MSG %s",err,err.Error())) + } else { + cr.Logger.Error(fmt.Errorf("CommentRepository.GetByID ERROR %v MSG %s",err,err.Error())) + } + return nil,err + } + + return &comment,nil +} + // UpdateByID repository layer for executing command to updating a comment by id func (cr *CommentRepository) UpdateByID(id int, req map[string]interface{}, updatedBy string) error { req["updated_by"] = updatedBy diff --git a/repository/like.go b/repository/like.go index eb8be54..372006e 100644 --- a/repository/like.go +++ b/repository/like.go @@ -14,6 +14,7 @@ type ( // ILikeRepository is an interface that has all the function to be implemented inside like repository ILikeRepository interface { Create(blogID int,req model.LikeRequest,createdBy string) error + GetByID(id int) (*model.ViewLikeResponse,error) DeleteByID(blogID int,likeID int) error } @@ -72,6 +73,35 @@ func (lr *LikeRepository) Create(blogID int,req model.LikeRequest,createdBy stri return nil } +// GetByID repository layer for querying command getting detail like by id +func (lr *LikeRepository) GetByID(id int) (*model.ViewLikeResponse,error) { + var like model.ViewLikeResponse + + q := ` + SELECT + id, + like_count, + user_id, + article_id + FROM likes + WHERE id = $1 + ` + + row := lr.DB.QueryRow(lr.Context,q,id) + err := row.Scan(&like.ID,&like.Like,&like.UserID,&like.BlogID) + if err != nil { + if err == pgx.ErrNoRows { + lr.Logger.Info(fmt.Errorf("LikeRepository.GetByID Scan INFO %v MSG %s",err,err.Error())) + } else { + lr.Logger.Error(fmt.Errorf("LikeRepository.GetByID Scan ERROR %v MSG %s",err,err.Error())) + } + + return nil,err + } + + return &like,nil +} + // DeleteByID repository layer for executing command deleting like by id func (lr *LikeRepository) DeleteByID(blogID int,likeID int) error { tx,err := lr.DB.Begin(lr.Context) diff --git a/service/blog.go b/service/blog.go index d50b40a..56ad4c7 100644 --- a/service/blog.go +++ b/service/blog.go @@ -39,6 +39,7 @@ type ( BlogRepo repository.IBlogRepository CommentRepo repository.ICommentRepository LikeRepo repository.ILikeRepository + UserRepo repository.IUserRepository Mutex *sync.RWMutex } ) @@ -226,22 +227,186 @@ func (bs *BlogService) DeleteBlogSvc(id int) error { } // CreateCommentSvc service layer for handling creating comments -func (bs *BlogService) CreateCommentSvc(blogID int, req model.CommentRequest,createdBy string) error {return nil} +func (bs *BlogService) CreateCommentSvc(blogID int, req model.CommentRequest,createdBy string) error { + _,err := bs.BlogRepo.GetByID(blogID) + if err != nil { + if err == pgx.ErrNoRows { + return model.ErrBlogNotFound + } + + return err + } + + err = validateCreateCommentRequest(&req) + if err != nil { + return err + } + + _,err = bs.UserRepo.GetByID(req.UserID) + if err != nil { + if err == pgx.ErrNoRows { + return model.ErrUserNotFound + } + + return err + } + + bs.Mutex.Lock() + err = bs.CommentRepo.Create(blogID,req,createdBy) + if err != nil { + return err + } + defer bs.Mutex.Unlock() + + return nil +} // UpdateCommentSvc service layer for handling updating comment by id -func (bs *BlogService) UpdateCommentSvc(id int, req model.CommentRequest,updatedBy string) error {return nil} +func (bs *BlogService) UpdateCommentSvc(id int, req model.CommentRequest,updatedBy string) error { + _,err := bs.CommentRepo.GetByID(id) + if err != nil { + if err == pgx.ErrNoRows { + return model.ErrCommentNotFound + } + + return err + } + + commentMap,err := validateUpdateCommentRequest(&req) + if err != nil { + return err + } + + _,err = bs.UserRepo.GetByID(req.UserID) + if err != nil { + if err == pgx.ErrNoRows { + return model.ErrUserNotFound + } + + return err + } + + bs.Mutex.Lock() + err = bs.CommentRepo.UpdateByID(id,commentMap,updatedBy) + if err != nil { + return err + } + defer bs.Mutex.Unlock() + + return nil +} // GetCommentsByBlogSvc service layer for handling getting comments with filter -func (bs *BlogService) GetCommentsByBlogSvc(blogID int, page int, size int, order string, field string) ([]model.ViewCommentResponse,*model.Metadata,error) {return nil,nil,nil} +func (bs *BlogService) GetCommentsByBlogSvc(blogID int, page int, size int, order string, field string) ([]model.ViewCommentResponse,*model.Metadata,error) { + res,totalData,err := bs.CommentRepo.GetAllByBlogID(blogID,page,size,order,field) + if err != nil { + return nil,nil,err + } + + if totalData < 1 { + return []model.ViewCommentResponse{},nil,nil + } + + totalPage := (int(totalData) + size - 1) / size + + if len(res) > size { + res = res[:len(res)-1] + } + + meta := helper.BuildMetaData(page,size,order,totalData,totalPage) + + return res,meta,nil +} // DeleteCommentSvc service layer for handling deleting comment with id -func (bs *BlogService) DeleteCommentSvc(blogID int, commentID int) error {return nil} +func (bs *BlogService) DeleteCommentSvc(blogID int, commentID int) error { + _,err := bs.BlogRepo.GetByID(blogID) + if err != nil { + if err == pgx.ErrNoRows { + return model.ErrBlogNotFound + } + + return err + } + + _,err = bs.CommentRepo.GetByID(commentID) + if err != nil { + if err == pgx.ErrNoRows { + return model.ErrCommentNotFound + } + + return err + } + + bs.Mutex.Lock() + err = bs.CommentRepo.DeleteByID(blogID,commentID) + if err != nil { + return err + } + defer bs.Mutex.Unlock() + + return nil +} // CreateLikeSvc service layer for handling creating likes -func (bs *BlogService) CreateLikeSvc(blogID int, req model.LikeRequest,createdBy string) error {return nil} +func (bs *BlogService) CreateLikeSvc(blogID int, req model.LikeRequest,createdBy string) error { + _,err := bs.BlogRepo.GetByID(blogID) + if err != nil { + if err == pgx.ErrNoRows { + return model.ErrBlogNotFound + } + + return err + } + + _,err = bs.UserRepo.GetByID(req.UserID) + if err != nil { + if err == pgx.ErrNoRows { + return model.ErrUserNotFound + } + + return err + } + + bs.Mutex.Lock() + err = bs.LikeRepo.Create(blogID,req,createdBy) + if err != nil { + return err + } + defer bs.Mutex.Unlock() + + return nil +} // DeleteLikeSvc service layer for handling deleting like with id -func (bs *BlogService) DeleteLikeSvc(blogID int,likeID int) error {return nil} +func (bs *BlogService) DeleteLikeSvc(blogID int,likeID int) error { + _,err := bs.BlogRepo.GetByID(blogID) + if err != nil { + if err == pgx.ErrNoRows { + return model.ErrBlogNotFound + } + + return err + } + + _,err = bs.LikeRepo.GetByID(likeID) + if err != nil { + if err == pgx.ErrNoRows { + return model.ErrLikeNotFound + } + + return err + } + + bs.Mutex.Lock() + err = bs.LikeRepo.DeleteByID(blogID,likeID) + if err != nil { + return err + } + defer bs.Mutex.Unlock() + + return nil +} // validateCreateBlogRequest responsible to validating create blog request func validateCreateBlogRequest(req *model.CreateBlogRequest) error { @@ -293,4 +458,27 @@ func validateUpdateBlogRequest(req *model.UpdateBlogRequest) (map[string]interfa blogMap["user_id"] = req.UserID return blogMap,nil +} + +// validateCreateCommentRequest responsible to validating create comment request +func validateCreateCommentRequest(req *model.CommentRequest) error { + if len(req.Comment) < 5 { + return model.ErrInvalidRequest + } + + return nil +} + +// validateUpdateCommentRequest responsible to validating update comment request +func validateUpdateCommentRequest(req *model.CommentRequest)(map[string]interface{},error) { + commentMap := make(map[string]interface{}) + + if len(req.Comment) < 5 { + return nil,model.ErrInvalidRequest + } + + commentMap["comment"] = req.Comment + commentMap["user_id"] = req.UserID + + return commentMap,nil } \ No newline at end of file From 70f09e6624304658ed9c94bc719eb4c590272ccd Mon Sep 17 00:00:00 2001 From: Taufik Januar Date: Sun, 1 May 2022 12:37:33 +0700 Subject: [PATCH 6/6] feat&fix #time 30m #comment implement like & comment controller, fixing query sql, route, blog|comment|like interfaces, blog services --- controller/blog.go | 268 +++++++++++++++++++++++++++++++++++++++-- infrastructure/http.go | 19 ++- model/error.go | 6 + repository/comment.go | 2 +- repository/like.go | 34 +++++- service/blog.go | 80 ++++++++---- 6 files changed, 369 insertions(+), 40 deletions(-) diff --git a/controller/blog.go b/controller/blog.go index 8b9707b..f0e7e70 100644 --- a/controller/blog.go +++ b/controller/blog.go @@ -171,6 +171,10 @@ func (bc *BlogController) UpdateBlog(ctx *fiber.Ctx) error { err = bc.BlogSvc.UpdateBlogSvc(id,blogReq,extData.FullName) if err != nil { + if errors.Is(err,model.ErrForbiddenUpdate) { + return helper.ResponseFormatter[any](ctx,fiber.StatusForbidden,err,err.Error(),nil,nil) + } + if errors.Is(err,model.ErrBlogNotFound) { return helper.ResponseFormatter[any](ctx,fiber.StatusNotFound,err,err.Error(),nil,nil) } @@ -196,8 +200,18 @@ func (bc *BlogController) DeleteBlog(ctx *fiber.Ctx) error { return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,nil,model.ErrBlogNotFound.Error(),nil,nil) } - err = bc.BlogSvc.DeleteBlogSvc(id) + data := ctx.Locals(model.KeyJWTValidAccess) + extData,err := util.ExtractPayloadJWT(data) if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusInternalServerError,err,err.Error(),nil,nil) + } + + err = bc.BlogSvc.DeleteBlogSvc(id,extData.UserID) + if err != nil { + if errors.Is(err,model.ErrForbiddenDelete) { + return helper.ResponseFormatter[any](ctx,fiber.StatusForbidden,err,err.Error(),nil,nil) + } + if errors.Is(err,model.ErrBlogNotFound) { return helper.ResponseFormatter[any](ctx,fiber.StatusNotFound,err,err.Error(),nil,nil) } @@ -209,19 +223,259 @@ func (bc *BlogController) DeleteBlog(ctx *fiber.Ctx) error { } // CreateComment responsible to creating a comment from controller layer -func (bc *BlogController) CreateComment(ctx *fiber.Ctx) error {return nil} +func (bc *BlogController) CreateComment(ctx *fiber.Ctx) error { + var commentReq model.CommentRequest + + id,err := ctx.ParamsInt("id",0) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + if id == 0 { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,nil,model.ErrBlogNotFound.Error(),nil,nil) + } + + data := ctx.Locals(model.KeyJWTValidAccess) + extData,err := util.ExtractPayloadJWT(data) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusInternalServerError,err,err.Error(),nil,nil) + } + + if err := ctx.BodyParser(&commentReq); err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + commentReq.UserID = extData.UserID + + err = bc.BlogSvc.CreateCommentSvc(id,commentReq,extData.FullName) + if err != nil { + + if errors.Is(err,model.ErrUserNotFound) || errors.Is(err,model.ErrBlogNotFound) { + return helper.ResponseFormatter[any](ctx,fiber.StatusNotFound,err,err.Error(),nil,nil) + } + + if errors.Is(err,model.ErrInvalidRequest) { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + return helper.ResponseFormatter[any](ctx,fiber.StatusInternalServerError,err,err.Error(),nil,nil) + } + + return helper.ResponseFormatter[any](ctx,fiber.StatusCreated,nil,"Success Create Comment",nil,nil) +} // UpdateComment responsible to updating a comment by id from controller layer -func (bc *BlogController) UpdateComment(ctx *fiber.Ctx) error {return nil} +func (bc *BlogController) UpdateComment(ctx *fiber.Ctx) error { + var commentReq model.CommentRequest + + commentId,err := ctx.ParamsInt("comment_id",0) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + if commentId == 0 { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,nil,model.ErrCommentNotFound.Error(),nil,nil) + } + + data := ctx.Locals(model.KeyJWTValidAccess) + extData,err := util.ExtractPayloadJWT(data) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusInternalServerError,err,err.Error(),nil,nil) + } + + if err := ctx.BodyParser(&commentReq); err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + commentReq.UserID = extData.UserID + + err = bc.BlogSvc.UpdateCommentSvc(commentId,commentReq,extData.FullName) + if err != nil { + if errors.Is(err,model.ErrForbiddenUpdate) { + return helper.ResponseFormatter[any](ctx,fiber.StatusForbidden,err,err.Error(),nil,nil) + } + + if errors.Is(err,model.ErrUserNotFound) || errors.Is(err,model.ErrCommentNotFound) { + return helper.ResponseFormatter[any](ctx,fiber.StatusNotFound,err,err.Error(),nil,nil) + } + + if errors.Is(err,model.ErrInvalidRequest) { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + return helper.ResponseFormatter[any](ctx,fiber.StatusInternalServerError,err,err.Error(),nil,nil) + } + + return helper.ResponseFormatter[any](ctx,fiber.StatusOK,nil,"Success Update Comment",nil,nil) +} // ListComment responsible to getting comments by blog id from controller layer -func (bc *BlogController) ListComment(ctx *fiber.Ctx) error {return nil} +func (bc *BlogController) ListComment(ctx *fiber.Ctx) error { + var ( + page = 1 + size = 10 + order = "ASC" + field = "id" + ) + + id,err := ctx.ParamsInt("id",0) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + if id == 0 { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,nil,model.ErrBlogNotFound.Error(),nil,nil) + } + + if p := ctx.Query("page",""); p != "" { + pNum,err := strconv.Atoi(p) + if err == nil && pNum > 0 { + page = pNum + } + } + + if s := ctx.Query("size",""); s != "" { + sNum, err := strconv.Atoi(s) + if err == nil && sNum > 0 { + size = sNum + } + } + + if o := ctx.Query("order",""); o != "" { + if len(o) > 0 { + order = o + } + } + + if f := ctx.Query("field",""); f != "" { + if len(f) > 0 { + field = f + } + } + + data,meta,err := bc.BlogSvc.GetCommentsByBlogSvc(id,page,size,order,field) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusInternalServerError,err,err.Error(),nil,nil) + } + + return helper.ResponseFormatter[any](ctx,fiber.StatusOK,nil,"Success Getting all Comments",data,meta) +} // DeleteComment responsible to deleting a comment by id from controller layer -func (bc *BlogController) DeleteComment(ctx *fiber.Ctx) error {return nil} +func (bc *BlogController) DeleteComment(ctx *fiber.Ctx) error { + blogID,err := ctx.ParamsInt("id",0) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + if blogID == 0 { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,nil,model.ErrBlogNotFound.Error(),nil,nil) + } + + commentID,err := ctx.ParamsInt("comment_id",0) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + if commentID == 0 { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,nil,model.ErrCommentNotFound.Error(),nil,nil) + } + + data := ctx.Locals(model.KeyJWTValidAccess) + extData,err := util.ExtractPayloadJWT(data) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusInternalServerError,err,err.Error(),nil,nil) + } + + err = bc.BlogSvc.DeleteCommentSvc(blogID,commentID,extData.UserID) + if err != nil { + if errors.Is(err,model.ErrForbiddenDelete) { + return helper.ResponseFormatter[any](ctx,fiber.StatusForbidden,err,err.Error(),nil,nil) + } + + if errors.Is(err,model.ErrBlogNotFound) || errors.Is(err,model.ErrCommentNotFound) { + return helper.ResponseFormatter[any](ctx,fiber.StatusNotFound,err,err.Error(),nil,nil) + } + + return helper.ResponseFormatter[any](ctx,fiber.StatusInternalServerError,err,err.Error(),nil,nil) + } + + return helper.ResponseFormatter[any](ctx,fiber.StatusOK,nil,"Success Delete Comment",nil,nil) +} // Like responsible to creating a like / liking a blog -func (bc *BlogController) Like(ctx *fiber.Ctx) error {return nil} +func (bc *BlogController) Like(ctx *fiber.Ctx) error { + var likeReq model.LikeRequest + + id,err := ctx.ParamsInt("id",0) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + if id == 0 { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,nil,model.ErrBlogNotFound.Error(),nil,nil) + } + + data := ctx.Locals(model.KeyJWTValidAccess) + extData,err := util.ExtractPayloadJWT(data) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusInternalServerError,err,err.Error(),nil,nil) + } + + likeReq.Like = 1 + likeReq.UserID = extData.UserID + + err = bc.BlogSvc.CreateLikeSvc(id,likeReq,extData.FullName) + if err != nil { + + if errors.Is(err,model.ErrUserNotFound) || errors.Is(err,model.ErrBlogNotFound) { + return helper.ResponseFormatter[any](ctx,fiber.StatusNotFound,err,err.Error(),nil,nil) + } + + if errors.Is(err,model.ErrInvalidRequest) || errors.Is(err,model.ErrAlreadyLikeBlog) { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + return helper.ResponseFormatter[any](ctx,fiber.StatusInternalServerError,err,err.Error(),nil,nil) + } + + return helper.ResponseFormatter[any](ctx,fiber.StatusCreated,nil,"Success Like",nil,nil) +} // UnLike responsible to deleting a like by id / unliking a blog -func (bc *BlogController) UnLike(ctx *fiber.Ctx) error {return nil} +func (bc *BlogController) UnLike(ctx *fiber.Ctx) error { + blogID,err := ctx.ParamsInt("id",0) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + if blogID == 0 { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,nil,model.ErrBlogNotFound.Error(),nil,nil) + } + + likeID,err := ctx.ParamsInt("like_id",0) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,err,err.Error(),nil,nil) + } + + if likeID == 0 { + return helper.ResponseFormatter[any](ctx,fiber.StatusBadRequest,nil,model.ErrLikeNotFound.Error(),nil,nil) + } + + data := ctx.Locals(model.KeyJWTValidAccess) + extData,err := util.ExtractPayloadJWT(data) + if err != nil { + return helper.ResponseFormatter[any](ctx,fiber.StatusInternalServerError,err,err.Error(),nil,nil) + } + + err = bc.BlogSvc.DeleteLikeSvc(blogID,likeID,extData.UserID) + if err != nil { + if errors.Is(err,model.ErrBlogNotFound) || errors.Is(err,model.ErrLikeNotFound) { + return helper.ResponseFormatter[any](ctx,fiber.StatusNotFound,err,err.Error(),nil,nil) + } + + return helper.ResponseFormatter[any](ctx,fiber.StatusInternalServerError,err,err.Error(),nil,nil) + } + + return helper.ResponseFormatter[any](ctx,fiber.StatusOK,nil,"Success UnLike",nil,nil) +} diff --git a/infrastructure/http.go b/infrastructure/http.go index ee71ba4..1cd80a5 100644 --- a/infrastructure/http.go +++ b/infrastructure/http.go @@ -1,10 +1,13 @@ package infrastructure import ( + "time" + "github.com/PickHD/pickablog/application" "github.com/PickHD/pickablog/helper" m "github.com/PickHD/pickablog/middleware" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/limiter" ) // ServeHTTP is wrapper function to start the apps infra in HTTP mode @@ -34,7 +37,13 @@ func setupRouter(app *application.App) { v1.Post("/auth/register",dep.AuthController.RegisterAuthor) v1.Get("/auth/google/login",dep.AuthController.GoogleLogin) v1.Get("/auth/google/callback",dep.AuthController.GoogleLoginCallback) - v1.Post("/auth/login",dep.AuthController.Login) + v1.Post("/auth/login",limiter.New(limiter.Config{ + Expiration: 15 * time.Minute, + Max: 5, + LimitReached: func (ctx *fiber.Ctx) error { + return helper.ResponseFormatter[any](ctx,fiber.StatusTooManyRequests,nil,"Login Attempts already reached the limit, tell our super admin about resetting a password, Thank you",nil,nil) + }, + }),dep.AuthController.Login) } // TAG SECTION @@ -64,12 +73,12 @@ func setupRouter(app *application.App) { // BLOG COMMENT SECTION v1.Post("/blog/:id/comment",m.ValidateJWTMiddleware,dep.BlogController.CreateComment) v1.Put("/blog/:id/comment/:comment_id",m.ValidateJWTMiddleware,dep.BlogController.UpdateComment) - v1.Get("/blog/:id/comment",m.ValidateJWTMiddleware,dep.BlogController.ListBlog) - v1.Delete("/blog/:id/:comment_id",m.ValidateJWTMiddleware,dep.BlogController.DeleteComment) + v1.Get("/blog/:id/comment",m.ValidateJWTMiddleware,dep.BlogController.ListComment) + v1.Delete("/blog/:id/comment/:comment_id",m.ValidateJWTMiddleware,dep.BlogController.DeleteComment) // BLOG LIKE SECTION - v1.Post("/blog/:id/like",m.ValidateJWTMiddleware,dep.BlogController.Like) - v1.Delete("/blog/:id/unlike",m.ValidateJWTMiddleware,dep.BlogController.UnLike) + v1.Get("/blog/:id/like",m.ValidateJWTMiddleware,dep.BlogController.Like) + v1.Delete("/blog/:id/like/:like_id",m.ValidateJWTMiddleware,dep.BlogController.UnLike) } } diff --git a/model/error.go b/model/error.go index 0f1d72c..0db720d 100644 --- a/model/error.go +++ b/model/error.go @@ -40,6 +40,8 @@ var ( ErrInvalidPassword = errors.New("invalid password") // ErrMismatchLogin occurs when user trying mismatch login method ErrMismatchLogin = errors.New("mismatch login, please use endpoint /api/v1/auth/google/login") + // ErrAlreadyLikeBlog occurs when user trying to liking a blog more than 1 times + ErrAlreadyLikeBlog = errors.New("already liking this blog") // ErrRedisKeyNotExisted occurs when key provided is not existed ErrRedisKeyNotExisted = errors.New("keys not existed") @@ -54,4 +56,8 @@ var ( // ErrForbiddenDeleteSelf occurs when user trying deleting their account by self ErrForbiddenDeleteSelf = errors.New("forbidden delete account self, make sure the id is corrent") + // ErrForbiddenUpdate occurs when user trying updating forbidden resource + ErrForbiddenUpdate = errors.New("forbidden updating data") + // ErrForbiddenDelete occurs when user trying deleting forbidden resource + ErrForbiddenDelete = errors.New("forbidden deleteing data") ) \ No newline at end of file diff --git a/repository/comment.go b/repository/comment.go index e4f93e1..ed599a5 100644 --- a/repository/comment.go +++ b/repository/comment.go @@ -38,7 +38,7 @@ func (cr *CommentRepository) Create(blogID int, req model.CommentRequest,created return err } - qInsert := `INSERT INTO comments (comment,article_id,user_id,created_by) VALUES ($1,$2,$3,$4) RETURNING id` + qInsert := `INSERT INTO comments (id,comment,article_id,user_id,created_by) VALUES (nextval('comment_seq'),$1,$2,$3,$4) RETURNING id` var commentID int err = tx.QueryRow(cr.Context,qInsert,req.Comment,blogID,req.UserID,createdBy).Scan(&commentID) diff --git a/repository/like.go b/repository/like.go index 372006e..31c3435 100644 --- a/repository/like.go +++ b/repository/like.go @@ -15,6 +15,7 @@ type ( ILikeRepository interface { Create(blogID int,req model.LikeRequest,createdBy string) error GetByID(id int) (*model.ViewLikeResponse,error) + GetByUserID(userID int) (*model.ViewLikeResponse,error) DeleteByID(blogID int,likeID int) error } @@ -35,7 +36,7 @@ func (lr *LikeRepository) Create(blogID int,req model.LikeRequest,createdBy stri return err } - qInsert := `INSERT INTO likes (like_count,article_id,user_id,created_by) VALUES ($1,$2,$3,$4) RETURNING id` + qInsert := `INSERT INTO likes (id,like_count,article_id,user_id,created_by) VALUES (nextval('like_seq'),$1,$2,$3,$4) RETURNING id` var likeID int err = tx.QueryRow(lr.Context,qInsert,req.Like,blogID,req.UserID,createdBy).Scan(&likeID) @@ -102,6 +103,35 @@ func (lr *LikeRepository) GetByID(id int) (*model.ViewLikeResponse,error) { return &like,nil } +// GetByUserID repository layer for querying command getting detail like by userID +func (lr *LikeRepository) GetByUserID(userID int) (*model.ViewLikeResponse,error) { + var like model.ViewLikeResponse + + q := ` + SELECT + id, + like_count, + user_id, + article_id + FROM likes + WHERE user_id = $1 + ` + + row := lr.DB.QueryRow(lr.Context,q,userID) + err := row.Scan(&like.ID,&like.Like,&like.UserID,&like.BlogID) + if err != nil { + if err == pgx.ErrNoRows { + lr.Logger.Info(fmt.Errorf("LikeRepository.GetByID Scan INFO %v MSG %s",err,err.Error())) + } else { + lr.Logger.Error(fmt.Errorf("LikeRepository.GetByID Scan ERROR %v MSG %s",err,err.Error())) + } + + return nil,err + } + + return &like,nil +} + // DeleteByID repository layer for executing command deleting like by id func (lr *LikeRepository) DeleteByID(blogID int,likeID int) error { tx,err := lr.DB.Begin(lr.Context) @@ -124,7 +154,7 @@ func (lr *LikeRepository) DeleteByID(blogID int,likeID int) error { return err } - qUpdate := `UPDATE article SET comments = ARRAY_REMOVE(comments,$1) WHERE id = $2` + qUpdate := `UPDATE article SET likes = ARRAY_REMOVE(likes,$1) WHERE id = $2` _,err = tx.Exec(lr.Context,qUpdate,likeID,blogID) if err != nil { diff --git a/service/blog.go b/service/blog.go index 56ad4c7..83d9367 100644 --- a/service/blog.go +++ b/service/blog.go @@ -20,15 +20,15 @@ type ( GetAllBlogSvc(page int, size int, order string, field string, search string, filter model.FilterBlogRequest) ([]model.BlogResponse,*model.Metadata,error) GetBlogBySlugSvc(slug string) (*model.BlogResponse,error) UpdateBlogSvc(id int, req model.UpdateBlogRequest, updatedBy string) error - DeleteBlogSvc(id int) error + DeleteBlogSvc(id int,userID int) error CreateCommentSvc(blogID int,req model.CommentRequest,createdBy string) error UpdateCommentSvc(id int, req model.CommentRequest, updatedBy string) error GetCommentsByBlogSvc(blogID int,page int, size int, order string, field string) ([]model.ViewCommentResponse,*model.Metadata,error) - DeleteCommentSvc(blogID int, commentID int) error + DeleteCommentSvc(blogID int, commentID int, userID int) error CreateLikeSvc(blogID int,req model.LikeRequest,createdBy string) error - DeleteLikeSvc(blogID int,likeID int) error + DeleteLikeSvc(blogID int,likeID int, userID int) error } // BlogRepository is an app blog struct that consists of all the dependencies needed for blog service @@ -97,7 +97,7 @@ func (bs *BlogService) GetAllBlogSvc(page int, size int, order string, field str d.UpdatedAt = r.UpdatedAt d.UpdatedBy = r.UpdatedBy - if len(r.Comments) > 1 { + if len(r.Comments) > 0 { for c := range r.Comments{ rc := r.Comments[c] if rc.Valid { @@ -107,7 +107,7 @@ func (bs *BlogService) GetAllBlogSvc(page int, size int, order string, field str } - if len(r.Tags) > 1 { + if len(r.Tags) > 0 { for t := range r.Tags{ rt := r.Tags[t] if rt.Valid { @@ -116,7 +116,7 @@ func (bs *BlogService) GetAllBlogSvc(page int, size int, order string, field str } } - if len(r.Likes) > 1 { + if len(r.Likes) > 0 { for l := range r.Likes{ rl := r.Likes[l] if rl.Valid { @@ -156,7 +156,7 @@ func (bs *BlogService) GetBlogBySlugSvc(slug string) (*model.BlogResponse,error) data.UpdatedAt = r.UpdatedAt data.UpdatedBy = r.UpdatedBy - if len(r.Comments) > 1 { + if len(r.Comments) > 0 { for c := range r.Comments{ rc := r.Comments[c] if rc.Valid { @@ -166,7 +166,7 @@ func (bs *BlogService) GetBlogBySlugSvc(slug string) (*model.BlogResponse,error) } - if len(r.Tags) > 1 { + if len(r.Tags) > 0 { for t := range r.Tags{ rt := r.Tags[t] if rt.Valid { @@ -175,7 +175,7 @@ func (bs *BlogService) GetBlogBySlugSvc(slug string) (*model.BlogResponse,error) } } - if len(r.Likes) > 1 { + if len(r.Likes) > 0 { for l := range r.Likes{ rl := r.Likes[l] if rl.Valid { @@ -189,14 +189,14 @@ func (bs *BlogService) GetBlogBySlugSvc(slug string) (*model.BlogResponse,error) // UpdateBlogSvc service layer for handling updating a blog by ID func (bs *BlogService) UpdateBlogSvc(id int, req model.UpdateBlogRequest, updatedBy string) error { - _,err := bs.BlogRepo.GetByID(id) + currentBlog,err := bs.BlogRepo.GetByID(id) if err != nil { if err == pgx.ErrNoRows { return model.ErrBlogNotFound } } - blogMap,err := validateUpdateBlogRequest(&req) + blogMap,err := validateUpdateBlogRequest(currentBlog,&req) if err != nil { return err } @@ -210,14 +210,18 @@ func (bs *BlogService) UpdateBlogSvc(id int, req model.UpdateBlogRequest, update } // DeleteBlogSvc service layer for handling deleting a blog by ID -func (bs *BlogService) DeleteBlogSvc(id int) error { - _,err := bs.BlogRepo.GetByID(id) +func (bs *BlogService) DeleteBlogSvc(id int,userID int) error { + currentBlog,err := bs.BlogRepo.GetByID(id) if err != nil { if err == pgx.ErrNoRows { return model.ErrBlogNotFound } } + if currentBlog.UserID != userID { + return model.ErrForbiddenDelete + } + err = bs.BlogRepo.DeleteByID(id) if err != nil { return err @@ -263,7 +267,7 @@ func (bs *BlogService) CreateCommentSvc(blogID int, req model.CommentRequest,cre // UpdateCommentSvc service layer for handling updating comment by id func (bs *BlogService) UpdateCommentSvc(id int, req model.CommentRequest,updatedBy string) error { - _,err := bs.CommentRepo.GetByID(id) + currentComment,err := bs.CommentRepo.GetByID(id) if err != nil { if err == pgx.ErrNoRows { return model.ErrCommentNotFound @@ -272,7 +276,7 @@ func (bs *BlogService) UpdateCommentSvc(id int, req model.CommentRequest,updated return err } - commentMap,err := validateUpdateCommentRequest(&req) + commentMap,err := validateUpdateCommentRequest(currentComment,&req) if err != nil { return err } @@ -319,7 +323,7 @@ func (bs *BlogService) GetCommentsByBlogSvc(blogID int, page int, size int, orde } // DeleteCommentSvc service layer for handling deleting comment with id -func (bs *BlogService) DeleteCommentSvc(blogID int, commentID int) error { +func (bs *BlogService) DeleteCommentSvc(blogID int, commentID int, userID int) error { _,err := bs.BlogRepo.GetByID(blogID) if err != nil { if err == pgx.ErrNoRows { @@ -329,7 +333,7 @@ func (bs *BlogService) DeleteCommentSvc(blogID int, commentID int) error { return err } - _,err = bs.CommentRepo.GetByID(commentID) + currentComment,err := bs.CommentRepo.GetByID(commentID) if err != nil { if err == pgx.ErrNoRows { return model.ErrCommentNotFound @@ -338,6 +342,10 @@ func (bs *BlogService) DeleteCommentSvc(blogID int, commentID int) error { return err } + if currentComment.UserID != userID { + return model.ErrForbiddenDelete + } + bs.Mutex.Lock() err = bs.CommentRepo.DeleteByID(blogID,commentID) if err != nil { @@ -368,18 +376,28 @@ func (bs *BlogService) CreateLikeSvc(blogID int, req model.LikeRequest,createdBy return err } - bs.Mutex.Lock() - err = bs.LikeRepo.Create(blogID,req,createdBy) + _,err = bs.LikeRepo.GetByUserID(req.UserID) if err != nil { + if err == pgx.ErrNoRows { + + bs.Mutex.Lock() + err = bs.LikeRepo.Create(blogID,req,createdBy) + if err != nil { + return err + } + defer bs.Mutex.Unlock() + + return nil + } + return err } - defer bs.Mutex.Unlock() - return nil + return model.ErrAlreadyLikeBlog } // DeleteLikeSvc service layer for handling deleting like with id -func (bs *BlogService) DeleteLikeSvc(blogID int,likeID int) error { +func (bs *BlogService) DeleteLikeSvc(blogID int,likeID int, userID int) error { _,err := bs.BlogRepo.GetByID(blogID) if err != nil { if err == pgx.ErrNoRows { @@ -389,7 +407,7 @@ func (bs *BlogService) DeleteLikeSvc(blogID int,likeID int) error { return err } - _,err = bs.LikeRepo.GetByID(likeID) + currentLike,err := bs.LikeRepo.GetByID(likeID) if err != nil { if err == pgx.ErrNoRows { return model.ErrLikeNotFound @@ -398,6 +416,10 @@ func (bs *BlogService) DeleteLikeSvc(blogID int,likeID int) error { return err } + if currentLike.UserID != userID { + return model.ErrForbiddenDelete + } + bs.Mutex.Lock() err = bs.LikeRepo.DeleteByID(blogID,likeID) if err != nil { @@ -430,7 +452,7 @@ func validateCreateBlogRequest(req *model.CreateBlogRequest) error { } // validateCreateBlogRequest responsible to validating update blog request -func validateUpdateBlogRequest(req *model.UpdateBlogRequest) (map[string]interface{},error) { +func validateUpdateBlogRequest(blog *model.ViewBlogResponse,req *model.UpdateBlogRequest) (map[string]interface{},error) { blogMap := make(map[string]interface{}) if req.Title == "" || req.Body == "" || req.Footer == "" { @@ -449,6 +471,10 @@ func validateUpdateBlogRequest(req *model.UpdateBlogRequest) (map[string]interfa return nil,model.ErrInvalidRequest } + if req.UserID != blog.UserID { + return nil,model.ErrForbiddenUpdate + } + req.Slug = slug.Make(req.Title) blogMap["title"] = req.Title @@ -470,13 +496,17 @@ func validateCreateCommentRequest(req *model.CommentRequest) error { } // validateUpdateCommentRequest responsible to validating update comment request -func validateUpdateCommentRequest(req *model.CommentRequest)(map[string]interface{},error) { +func validateUpdateCommentRequest(comment *model.ViewCommentResponse,req *model.CommentRequest)(map[string]interface{},error) { commentMap := make(map[string]interface{}) if len(req.Comment) < 5 { return nil,model.ErrInvalidRequest } + if comment.UserID != req.UserID { + return nil,model.ErrForbiddenUpdate + } + commentMap["comment"] = req.Comment commentMap["user_id"] = req.UserID