From faa9c263dbe433b1021b5928d715d69fad188994 Mon Sep 17 00:00:00 2001 From: Kocannn Date: Fri, 26 Dec 2025 22:19:32 +0800 Subject: [PATCH 1/2] refactor(blog_post): make code to mudah di baca --- app/app.go | 2 + app/blog_post/blogPost.go | 21 ++++ .../delivery/http/create_blog_post.go | 43 +++++++ .../delivery/http/delete_blog_posts.go | 43 +++++++ app/blog_post/delivery/http/get_all_blogs.go | 73 +++++++++++ .../delivery/http/get_detail_blog.go | 34 ++++++ app/blog_post/delivery/http/http.go | 23 ++++ app/blog_post/delivery/http/update_blog.go | 114 ++++++++++++++++++ app/blog_post/repository/create_author.go | 13 ++ app/blog_post/repository/create_blog_post.go | 13 ++ app/blog_post/repository/create_tags.go | 13 ++ app/blog_post/repository/delete_blog_posts.go | 30 +++++ .../repository/find_author_by_user_id.go | 13 ++ app/blog_post/repository/find_by_id.go | 14 +++ app/blog_post/repository/find_by_slug.go | 14 +++ app/blog_post/repository/get_all_blogs.go | 50 ++++++++ .../repository/get_tags_by_blogs_id.go | 13 ++ app/blog_post/repository/repository.go | 16 +++ app/blog_post/repository/update_blog.go | 64 ++++++++++ app/blog_post/usecase/create_blog_post.go | 59 +++++++++ app/blog_post/usecase/delete_blog_post.go | 16 +++ app/blog_post/usecase/get_all_blog_posts.go | 17 +++ app/blog_post/usecase/get_detail_blog_post.go | 39 ++++++ app/blog_post/usecase/update_blog_post.go | 17 +++ app/blog_post/usecase/usecase.go | 25 ++++ app/middlewares/auth_middleware.go | 3 + domain/blog_post.go | 66 ++++++++++ go.mod | 3 +- 28 files changed, 850 insertions(+), 1 deletion(-) create mode 100644 app/blog_post/blogPost.go create mode 100644 app/blog_post/delivery/http/create_blog_post.go create mode 100644 app/blog_post/delivery/http/delete_blog_posts.go create mode 100644 app/blog_post/delivery/http/get_all_blogs.go create mode 100644 app/blog_post/delivery/http/get_detail_blog.go create mode 100644 app/blog_post/delivery/http/http.go create mode 100644 app/blog_post/delivery/http/update_blog.go create mode 100644 app/blog_post/repository/create_author.go create mode 100644 app/blog_post/repository/create_blog_post.go create mode 100644 app/blog_post/repository/create_tags.go create mode 100644 app/blog_post/repository/delete_blog_posts.go create mode 100644 app/blog_post/repository/find_author_by_user_id.go create mode 100644 app/blog_post/repository/find_by_id.go create mode 100644 app/blog_post/repository/find_by_slug.go create mode 100644 app/blog_post/repository/get_all_blogs.go create mode 100644 app/blog_post/repository/get_tags_by_blogs_id.go create mode 100644 app/blog_post/repository/repository.go create mode 100644 app/blog_post/repository/update_blog.go create mode 100644 app/blog_post/usecase/create_blog_post.go create mode 100644 app/blog_post/usecase/delete_blog_post.go create mode 100644 app/blog_post/usecase/get_all_blog_posts.go create mode 100644 app/blog_post/usecase/get_detail_blog_post.go create mode 100644 app/blog_post/usecase/update_blog_post.go create mode 100644 app/blog_post/usecase/usecase.go create mode 100644 domain/blog_post.go diff --git a/app/app.go b/app/app.go index e74d51d..620d45a 100644 --- a/app/app.go +++ b/app/app.go @@ -48,6 +48,8 @@ func InitApp( newsletterUC := newsletters.InitUsecase(cfg, newsletterRepo, dbTx, jwt.NewJwt(cfg.JWT_SECRET_KEY)) eventUC := events.InitUsecase(eventRepo, imgRepo, dbTx) imgUc := images.InitUsecase(imgRepo, dbTx) + blogPostUc := blogPost.InitUseCase(blogPostRepo, dbTx) + transactionEventUsecase := transactionEventUC.NewUsecase(transactionEventRepository, eventRepo, xenditClient, cfg) // handler userHandler := users.InitHandler(userUsecase) diff --git a/app/blog_post/blogPost.go b/app/blog_post/blogPost.go new file mode 100644 index 0000000..bba86bf --- /dev/null +++ b/app/blog_post/blogPost.go @@ -0,0 +1,21 @@ +package blog_post + +import ( + blog_post_handler "github.com/hammer-code/lms-be/app/blog_post/delivery/http" + blog_post_repo "github.com/hammer-code/lms-be/app/blog_post/repository" + blog_post_usecase "github.com/hammer-code/lms-be/app/blog_post/usecase" + "github.com/hammer-code/lms-be/domain" + "github.com/hammer-code/lms-be/pkg/db" +) + +func InitRepository(db db.DatabaseTransaction) domain.BlogPostRepository { + return blog_post_repo.NewRepository(db) +} + +func InitUseCase(repository domain.BlogPostRepository, db db.DatabaseTransaction) domain.BlogPostUsecase { + return blog_post_usecase.NewUsecase(repository, db) +} + +func InitHandler(usecase domain.BlogPostUsecase) domain.BlogPostHandler { + return blog_post_handler.NewHandler(usecase) +} diff --git a/app/blog_post/delivery/http/create_blog_post.go b/app/blog_post/delivery/http/create_blog_post.go new file mode 100644 index 0000000..fa91177 --- /dev/null +++ b/app/blog_post/delivery/http/create_blog_post.go @@ -0,0 +1,43 @@ +package http + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/hammer-code/lms-be/domain" + contextkey "github.com/hammer-code/lms-be/pkg/context_key" + "github.com/hammer-code/lms-be/utils" +) + +// CreateBlogPost implements domain.BlogPostHandler. +func (h Handler) CreateBlogPost(w http.ResponseWriter, r *http.Request) { + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + resp := utils.CustomErrorResponse(err) + utils.Response(resp, w) + return + } + + user := r.Context().Value(contextkey.UserKey).(domain.User) + + BlogPost := domain.BlogPost{} + if err = json.Unmarshal(bodyBytes, &BlogPost); err != nil { + resp := utils.CustomErrorResponse(err) + utils.Response(resp, w) + return + } + + err = h.usecase.CreateBlogPost(r.Context(), BlogPost, user) + if err != nil { + resp := utils.CustomErrorResponse(err) + utils.Response(resp, w) + return + } + + utils.Response(domain.HttpResponse{ + Code: http.StatusCreated, + Message: "Blog post created successfully", + }, w) + +} diff --git a/app/blog_post/delivery/http/delete_blog_posts.go b/app/blog_post/delivery/http/delete_blog_posts.go new file mode 100644 index 0000000..000b791 --- /dev/null +++ b/app/blog_post/delivery/http/delete_blog_posts.go @@ -0,0 +1,43 @@ +package http + +import ( + "net/http" + "strconv" + + "github.com/gorilla/mux" + "github.com/hammer-code/lms-be/domain" + "github.com/hammer-code/lms-be/utils" + "github.com/sirupsen/logrus" +) + +// DeleteBlogPost implements domain.BlogPostHandler. +func (h Handler) DeleteBlogPost(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + idString := vars["id"] + + value, err := strconv.ParseUint(idString, 10, 32) + if err != nil { + logrus.Error("failed to convert string to uint: ", err) + utils.Response(domain.HttpResponse{ + Code: 500, + Message: err.Error(), + }, w) + return + } + + err = h.usecase.DeleteBlogPost(r.Context(), uint(value)) + if err != nil { + logrus.Error("failed to delete event : ", err) + utils.Response(domain.HttpResponse{ + Code: 500, + Message: err.Error(), + }, w) + return + } + + utils.Response(domain.HttpResponse{ + Code: 200, + Message: "success", + Data: nil, + }, w) +} diff --git a/app/blog_post/delivery/http/get_all_blogs.go b/app/blog_post/delivery/http/get_all_blogs.go new file mode 100644 index 0000000..b652358 --- /dev/null +++ b/app/blog_post/delivery/http/get_all_blogs.go @@ -0,0 +1,73 @@ +package http + +import ( + "net/http" + "time" + + "github.com/hammer-code/lms-be/domain" + "github.com/hammer-code/lms-be/utils" + "github.com/sirupsen/logrus" +) + +// GetAllBlogPosts implements domain.BlogPostHandler. +func (h Handler) GetAllBlogPosts(w http.ResponseWriter, r *http.Request) { + // Ambil parameter pagination dari request + pagination, err := domain.GetPaginationFromCtx(r) + if err != nil { + logrus.Error("failed to parse pagination parameters: ", err) + utils.Response(domain.HttpResponse{ + Code: http.StatusBadRequest, + Message: "Invalid pagination parameters", + }, w) + return + } + + // Panggil usecase dengan parameter pagination + data, paginationResponse, err := h.usecase.GetAllBlogPosts(r.Context(), pagination) + if err != nil { + resp := utils.CustomErrorResponse(err) + utils.Response(resp, w) + return + } + + type response struct { + Id int `json:"id" gorm:"primaryKey"` + Title string `json:"title"` + Excerpt string `json:"excerpt"` + Author domain.Author `json:"author" gorm:"foreignKey:AuthorID;references:UserId"` + AuthorID int `json:"author_id" gorm:"column:author_id"` + Tags []string `json:"tags" gorm:"-"` + Category string `json:"category"` + Status string `json:"status" gorm:"type:enum('draft', 'published', 'archived')"` + Slug string `json:"slug"` + PublishedAt *time.Time `json:"published_at"` + UpdatedAt *time.Time `json:"updated_at"` + CreatedAt *time.Time `json:"created_at"` + } + + responseDTO := []response{} + for _, post := range data { + resp := response{ + Id: post.Id, + Title: post.Title, + Excerpt: post.Excerpt, + Author: post.Author, + AuthorID: post.AuthorID, + Tags: post.Tags, + Category: post.Category, + Status: post.Status, + Slug: post.Slug, + PublishedAt: post.PublishedAt, + UpdatedAt: post.UpdatedAt, + CreatedAt: post.CreatedAt, + } + responseDTO = append(responseDTO, resp) + } + + utils.Response(domain.HttpResponse{ + Code: http.StatusOK, + Message: "Blog posts retrieved successfully", + Data: responseDTO, + Pagination: &paginationResponse, + }, w) +} diff --git a/app/blog_post/delivery/http/get_detail_blog.go b/app/blog_post/delivery/http/get_detail_blog.go new file mode 100644 index 0000000..50006f5 --- /dev/null +++ b/app/blog_post/delivery/http/get_detail_blog.go @@ -0,0 +1,34 @@ +package http + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/hammer-code/lms-be/domain" + "github.com/hammer-code/lms-be/utils" +) + +// GetDetailBlogPost implements domain.BlogPostHandler. +func (h Handler) GetDetailBlogPost(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + slug := vars["slug"] + + if slug == "" { + resp := utils.CustomErrorResponse(utils.NewBadRequestError(r.Context(), "Slug is required", nil)) + utils.Response(resp, w) + return + } + + resp, err := h.usecase.GetDetailBlogPost(r.Context(), slug, 0) + if err != nil { + resp := utils.CustomErrorResponse(err) + utils.Response(resp, w) + return + } + + utils.Response(domain.HttpResponse{ + Code: http.StatusOK, + Message: "Blog post retrieved successfully", + Data: resp, + }, w) +} diff --git a/app/blog_post/delivery/http/http.go b/app/blog_post/delivery/http/http.go new file mode 100644 index 0000000..cc3e7a9 --- /dev/null +++ b/app/blog_post/delivery/http/http.go @@ -0,0 +1,23 @@ +package http + +import ( + "github.com/hammer-code/lms-be/domain" +) + +type Handler struct { + usecase domain.BlogPostUsecase +} + +var ( + handlr *Handler +) + +func NewHandler(usecase domain.BlogPostUsecase) domain.BlogPostHandler { + if handlr == nil { + handlr = &Handler{ + usecase: usecase, + } + + } + return *handlr +} diff --git a/app/blog_post/delivery/http/update_blog.go b/app/blog_post/delivery/http/update_blog.go new file mode 100644 index 0000000..7b2be99 --- /dev/null +++ b/app/blog_post/delivery/http/update_blog.go @@ -0,0 +1,114 @@ +package http + +import ( + "encoding/json" + "io" + "net/http" + "strconv" + "time" + + "github.com/gorilla/mux" + "github.com/hammer-code/lms-be/domain" + "github.com/hammer-code/lms-be/utils" + "github.com/sirupsen/logrus" +) + +// UpdateBlogPost implements domain.BlogPostHandler. +func (h Handler) UpdateBlogPost(w http.ResponseWriter, r *http.Request) { + idS := mux.Vars(r)["id"] + id, err := strconv.ParseUint(idS, 10, 32) + if err != nil { + logrus.Error("failed to convert string to uint: ", err) + utils.Response(domain.HttpResponse{ + Code: http.StatusBadRequest, + Message: "Invalid ID format", + }, w) + return + } + + existingPost, err := h.usecase.GetDetailBlogPost(r.Context(), "", uint(id)) + if err != nil { + logrus.Error("failed to get existing blog post: ", err) + utils.Response(domain.HttpResponse{ + Code: http.StatusNotFound, + Message: "Blog post not found", + }, w) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + logrus.Error("failed to read body: ", err) + utils.Response(domain.HttpResponse{ + Code: http.StatusBadRequest, + Message: "Invalid request body", + }, w) + return + } + + var patchData map[string]interface{} + if err := json.Unmarshal(body, &patchData); err != nil { + logrus.Error("failed to unmarshal: ", err) + utils.Response(domain.HttpResponse{ + Code: http.StatusBadRequest, + Message: "Invalid request format", + }, w) + return + } + + updatedPost := existingPost + if title, ok := patchData["title"].(string); ok { + updatedPost.Title = title + } + if content, ok := patchData["content"].(string); ok { + updatedPost.Content = content + } + if excerpt, ok := patchData["excerpt"].(string); ok { + updatedPost.Excerpt = excerpt + } + if category, ok := patchData["category"].(string); ok { + updatedPost.Category = category + } + if status, ok := patchData["status"].(string); ok { + updatedPost.Status = status + } + if patchData["status"] == "published" { + if updatedPost.PublishedAt == nil { + timeNow := time.Now() + updatedPost.PublishedAt = &timeNow + } + } else { + updatedPost.PublishedAt = nil + } + + if tags, ok := patchData["tags"].([]interface{}); ok { + updatedPost.Tags = make([]string, len(tags)) + for i, tag := range tags { + updatedPost.Tags[i] = tag.(string) + } + } + + if authorData, ok := patchData["author"].(map[string]interface{}); ok { + if avatar, ok := authorData["avatar"].(string); ok && avatar != "" { + updatedPost.Author.Avatar = avatar + } + } + + timeNow := time.Now() + updatedPost.UpdatedAt = &timeNow + + err = h.usecase.UpdateBlogPost(r.Context(), updatedPost, uint(id)) + if err != nil { + logrus.Error("failed to update blog post: ", err) + utils.Response(domain.HttpResponse{ + Code: http.StatusInternalServerError, + Message: "Failed to update blog post", + }, w) + return + } + + utils.Response(domain.HttpResponse{ + Code: http.StatusOK, + Message: "Blog post updated successfully", + }, w) +} diff --git a/app/blog_post/repository/create_author.go b/app/blog_post/repository/create_author.go new file mode 100644 index 0000000..9450ada --- /dev/null +++ b/app/blog_post/repository/create_author.go @@ -0,0 +1,13 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + "golang.org/x/net/context" +) + +func (r *repository) CreateAuthor(ctx context.Context, data domain.Author) error { + if err := r.db.DB(ctx).Create(&data).Error; err != nil { + return err + } + return nil +} diff --git a/app/blog_post/repository/create_blog_post.go b/app/blog_post/repository/create_blog_post.go new file mode 100644 index 0000000..865e86f --- /dev/null +++ b/app/blog_post/repository/create_blog_post.go @@ -0,0 +1,13 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + "golang.org/x/net/context" +) + +func (r *repository) CreateBlogPost(ctx context.Context, data domain.BlogPost) (domain.BlogPost, error) { + if err := r.db.DB(ctx).Omit("updated_at").Create(&data).Error; err != nil { + return data, err + } + return data, nil +} diff --git a/app/blog_post/repository/create_tags.go b/app/blog_post/repository/create_tags.go new file mode 100644 index 0000000..f723fd4 --- /dev/null +++ b/app/blog_post/repository/create_tags.go @@ -0,0 +1,13 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + "golang.org/x/net/context" +) + +func (r *repository) CreateTags(ctx context.Context, tag []domain.BlogPostTag) error { + if err := r.db.DB(ctx).Table("blog_post_tags").Create(&tag).Error; err != nil { + return err + } + return nil +} diff --git a/app/blog_post/repository/delete_blog_posts.go b/app/blog_post/repository/delete_blog_posts.go new file mode 100644 index 0000000..add92fa --- /dev/null +++ b/app/blog_post/repository/delete_blog_posts.go @@ -0,0 +1,30 @@ +package repository + +import ( + "errors" + + "github.com/hammer-code/lms-be/domain" + "github.com/sirupsen/logrus" + "golang.org/x/net/context" +) + +// DeleteBlogPost implements domain.BlogPostRepository. +func (r *repository) DeleteBlogPost(ctx context.Context, id uint) error { + db := r.db.DB(ctx).Model(&domain.BlogPost{}) + + // Perform soft delete by updating is_deleted field + result := db.Where("id = ?", id).Updates(map[string]interface{}{ + "is_deleted": true, + }) + + if result.Error != nil { + logrus.Error("failed to soft delete blog post: ", result.Error) + return result.Error + } + + if result.RowsAffected == 0 { + logrus.Warn("no blog post found to delete with id: ", id) + return errors.New("blog post not found") + } + return nil +} diff --git a/app/blog_post/repository/find_author_by_user_id.go b/app/blog_post/repository/find_author_by_user_id.go new file mode 100644 index 0000000..d0e5997 --- /dev/null +++ b/app/blog_post/repository/find_author_by_user_id.go @@ -0,0 +1,13 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + "golang.org/x/net/context" +) + +func (r *repository) FindAuthorByUserID(ctx context.Context, userID uint) (data domain.Author, err error) { + if err := r.db.DB(ctx).Where("user_id = ?", userID).First(&data).Error; err != nil { + return data, err + } + return data, nil +} diff --git a/app/blog_post/repository/find_by_id.go b/app/blog_post/repository/find_by_id.go new file mode 100644 index 0000000..86ae93e --- /dev/null +++ b/app/blog_post/repository/find_by_id.go @@ -0,0 +1,14 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + "golang.org/x/net/context" +) + +// FindById implements domain.BlogPostRepository. +func (r *repository) FindById(ctx context.Context, id uint) (data domain.BlogPost, err error) { + db := r.db.DB(ctx).Preload("Author").Model(&domain.BlogPost{}).Where("is_deleted = ?", false) + err = db.First(&data, "id = ?", id).Error + + return data, err +} diff --git a/app/blog_post/repository/find_by_slug.go b/app/blog_post/repository/find_by_slug.go new file mode 100644 index 0000000..5ea5e88 --- /dev/null +++ b/app/blog_post/repository/find_by_slug.go @@ -0,0 +1,14 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + "golang.org/x/net/context" +) + +// FindBySlug implements domain.BlogPostRepository. +func (r *repository) FindBySlug(ctx context.Context, slug string) (data domain.BlogPost, err error) { + db := r.db.DB(ctx).Preload("Author").Model(&domain.BlogPost{}).Where("is_deleted = ?", false) + err = db.First(&data, "slug = ?", slug).Error + + return data, err +} diff --git a/app/blog_post/repository/get_all_blogs.go b/app/blog_post/repository/get_all_blogs.go new file mode 100644 index 0000000..fb3f383 --- /dev/null +++ b/app/blog_post/repository/get_all_blogs.go @@ -0,0 +1,50 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + "github.com/sirupsen/logrus" + "golang.org/x/net/context" +) + +// GetAllBlogPosts implements domain.BlogPostRepository. +func (r *repository) GetAllBlogPosts(ctx context.Context, pagination domain.FilterPagination) ([]domain.BlogPost, int, error) { + var data []domain.BlogPost + var totalCount int64 + + if err := r.db.DB(ctx).Model(&domain.BlogPost{}).Where("is_deleted = ?", false).Count(&totalCount).Error; err != nil { + logrus.Error("failed to count blog posts: ", err) + return nil, 0, err + } + + offset := pagination.GetOffset() + limit := pagination.GetLimit() + orderBy := pagination.GetOrderBy() + + query := r.db.DB(ctx).Preload("Author").Where("is_deleted = ?", false) + + if orderBy != "" { + query = query.Order(orderBy) + } else { + query = query.Order("id DESC") + } + + err := query.Limit(limit).Offset(offset).Find(&data).Error + if err != nil { + logrus.Error("failed to get all blog posts: ", err) + return nil, 0, err + } + + for i := range data { + var tags []string + if err := r.db.DB(ctx).Table("blog_post_tags"). + Select("tag"). + Where("blog_post_id = ?", data[i].Id). + Pluck("tag", &tags).Error; err != nil { + logrus.Error("failed to get tags for blog post ID ", data[i].Id, ": ", err) + } else { + data[i].Tags = tags + } + } + + return data, int(totalCount), nil +} diff --git a/app/blog_post/repository/get_tags_by_blogs_id.go b/app/blog_post/repository/get_tags_by_blogs_id.go new file mode 100644 index 0000000..c5f740e --- /dev/null +++ b/app/blog_post/repository/get_tags_by_blogs_id.go @@ -0,0 +1,13 @@ +package repository + +import "golang.org/x/net/context" + +func (r *repository) GetTagsByBlogPostID(ctx context.Context, blogPostID uint) (tags []string, err error) { + if err := r.db.DB(ctx).Table("blog_post_tags"). + Select("tag"). + Where("blog_post_id = ?", blogPostID). + Pluck("tag", &tags).Error; err != nil { + return nil, err + } + return tags, nil +} diff --git a/app/blog_post/repository/repository.go b/app/blog_post/repository/repository.go new file mode 100644 index 0000000..acdcd56 --- /dev/null +++ b/app/blog_post/repository/repository.go @@ -0,0 +1,16 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + pkgDB "github.com/hammer-code/lms-be/pkg/db" +) + +type repository struct { + db pkgDB.DatabaseTransaction +} + +func NewRepository(db pkgDB.DatabaseTransaction) domain.BlogPostRepository { + return &repository{ + db: db, + } +} diff --git a/app/blog_post/repository/update_blog.go b/app/blog_post/repository/update_blog.go new file mode 100644 index 0000000..8d50b19 --- /dev/null +++ b/app/blog_post/repository/update_blog.go @@ -0,0 +1,64 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + "github.com/sirupsen/logrus" + "golang.org/x/net/context" +) + +// UpdateBlogPost implements domain.BlogPostRepository. +func (r *repository) UpdateBlogPost(ctx context.Context, data domain.BlogPost, id uint) error { + return r.db.StartTransaction(ctx, func(txCtx context.Context) error { + if err := r.db.DB(txCtx).Model(&domain.BlogPost{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "title": data.Title, + "content": data.Content, + "excerpt": data.Excerpt, + "published_at": data.PublishedAt, + "updated_at": data.UpdatedAt, + "category": data.Category, + "status": data.Status, + }).Error; err != nil { + logrus.Error("failed to update blog post: ", err) + return err + } + if data.Author.Avatar != "" { + if err := r.db.DB(txCtx).Model(&domain.Author{}). + Where("user_id = ?", data.AuthorID). + Updates(map[string]interface{}{ + "avatar": data.Author.Avatar, + }).Error; err != nil { + logrus.Error("failed to update author avatar: ", err) + return err + } + } + + if len(data.Tags) > 0 { + if err := r.db.DB(txCtx).Table("blog_post_tags"). + Where("blog_post_id = ?", id). + Delete(nil).Error; err != nil { + logrus.Error("failed to delete old tags: ", err) + return err + } + + for _, tag := range data.Tags { + blogPostTag := struct { + BlogPostId int `gorm:"column:blog_post_id"` + Tag string `gorm:"column:tag"` + }{ + BlogPostId: int(id), + Tag: tag, + } + + if err := r.db.DB(txCtx).Table("blog_post_tags"). + Create(&blogPostTag).Error; err != nil { + logrus.Error("failed to create blog post tag: ", err) + return err + } + } + } + + return nil + }) +} diff --git a/app/blog_post/usecase/create_blog_post.go b/app/blog_post/usecase/create_blog_post.go new file mode 100644 index 0000000..07cac57 --- /dev/null +++ b/app/blog_post/usecase/create_blog_post.go @@ -0,0 +1,59 @@ +package usecase + +import ( + "errors" + "time" + + "github.com/hammer-code/lms-be/domain" + "golang.org/x/net/context" + "gorm.io/gorm" +) + +// CreateBlogPost implements domain.BlogPostUsecase. +func (uc *usecase) CreateBlogPost(ctx context.Context, data domain.BlogPost, user domain.User) error { + + data.Author.UserId = user.ID + data.Author.Name = user.Username + data.UpdatedAt = nil + data.PublishedAt = nil + + if data.Status == "published" { + timeNow := time.Now() + data.PublishedAt = &timeNow + } + + if err := uc.dbTX.StartTransaction(ctx, func(txCtx context.Context) error { + _, err := uc.repo.FindAuthorByUserID(txCtx, uint(user.ID)) + if errors.Is(err, gorm.ErrRecordNotFound) { + + if err := uc.repo.CreateAuthor(txCtx, data.Author); err != nil { + return err + } + } + data.AuthorID = data.Author.UserId + + data, err := uc.repo.CreateBlogPost(txCtx, data) + if err != nil { + return err + } + + tags := make([]domain.BlogPostTag, 0, len(data.Tags)) + + for _, tag := range data.Tags { + tags = append(tags, domain.BlogPostTag{ + BlogPostId: data.Id, + Tag: tag, + }) + } + + if err := uc.repo.CreateTags(txCtx, tags); err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil + +} diff --git a/app/blog_post/usecase/delete_blog_post.go b/app/blog_post/usecase/delete_blog_post.go new file mode 100644 index 0000000..cc142a4 --- /dev/null +++ b/app/blog_post/usecase/delete_blog_post.go @@ -0,0 +1,16 @@ +package usecase + +import ( + "github.com/sirupsen/logrus" + "golang.org/x/net/context" +) + +// DeleteBlogPost implements domain.BlogPostUsecase. +func (uc *usecase) DeleteBlogPost(ctx context.Context, id uint) error { + if err := uc.repo.DeleteBlogPost(ctx, id); err != nil { + logrus.Error("failed to delete blog post detail: ", err) + return err + } + + return nil +} diff --git a/app/blog_post/usecase/get_all_blog_posts.go b/app/blog_post/usecase/get_all_blog_posts.go new file mode 100644 index 0000000..a82f38c --- /dev/null +++ b/app/blog_post/usecase/get_all_blog_posts.go @@ -0,0 +1,17 @@ +package usecase + +import ( + "github.com/hammer-code/lms-be/domain" + "github.com/sirupsen/logrus" + "golang.org/x/net/context" +) + +// GetAllBlogPosts implements domain.BlogPostUsecase. +func (uc *usecase) GetAllBlogPosts(ctx context.Context, pagination domain.FilterPagination) ([]domain.BlogPost, domain.Pagination, error) { + blogPosts, totalCount, err := uc.repo.GetAllBlogPosts(ctx, pagination) + if err != nil { + logrus.Error("failed to get all blog posts: ", err) + return nil, domain.Pagination{}, err + } + return blogPosts, domain.NewPagination(totalCount, pagination), nil +} diff --git a/app/blog_post/usecase/get_detail_blog_post.go b/app/blog_post/usecase/get_detail_blog_post.go new file mode 100644 index 0000000..1fb841c --- /dev/null +++ b/app/blog_post/usecase/get_detail_blog_post.go @@ -0,0 +1,39 @@ +package usecase + +import ( + "github.com/hammer-code/lms-be/domain" + "github.com/sirupsen/logrus" + "golang.org/x/net/context" +) + +// GetDetailBlogPost implements domain.BlogPostUsecase. +func (uc *usecase) GetDetailBlogPost(ctx context.Context, slug string, id uint) (data domain.BlogPost, err error) { + typeFind := "slug" + if slug == "" { + typeFind = "id" + } + + if typeFind == "id" { + data, err = uc.repo.FindById(ctx, id) + if err != nil { + logrus.Error("failed to find blog post by id: ", err) + return domain.BlogPost{}, err + } + } else { + data, err = uc.repo.FindBySlug(ctx, slug) + if err != nil { + logrus.Error("failed to find blog post by slug: ", err) + return domain.BlogPost{}, err + } + } + + tags, err := uc.repo.GetTagsByBlogPostID(ctx, uint(data.Id)) + if err != nil { + logrus.Error("failed to get tags for blog post ID ", data.Id, ": ", err) + return domain.BlogPost{}, err + } + + data.Tags = tags + + return data, nil +} diff --git a/app/blog_post/usecase/update_blog_post.go b/app/blog_post/usecase/update_blog_post.go new file mode 100644 index 0000000..16e4e3d --- /dev/null +++ b/app/blog_post/usecase/update_blog_post.go @@ -0,0 +1,17 @@ +package usecase + +import ( + "github.com/hammer-code/lms-be/domain" + "github.com/sirupsen/logrus" + "golang.org/x/net/context" +) + +// UpdateBlogPost implements domain.BlogPostUsecase. +func (uc *usecase) UpdateBlogPost(ctx context.Context, data domain.BlogPost, id uint) error { + err := uc.repo.UpdateBlogPost(ctx, data, id) + if err != nil { + logrus.Error("failed to update blog post: ", err) + return err + } + return nil +} diff --git a/app/blog_post/usecase/usecase.go b/app/blog_post/usecase/usecase.go new file mode 100644 index 0000000..6a22547 --- /dev/null +++ b/app/blog_post/usecase/usecase.go @@ -0,0 +1,25 @@ +package usecase + +import ( + "github.com/hammer-code/lms-be/domain" + "github.com/hammer-code/lms-be/pkg/db" +) + +type usecase struct { + repo domain.BlogPostRepository + dbTX db.DatabaseTransaction +} + +var ( + usec *usecase +) + +func NewUsecase(repo domain.BlogPostRepository, dbTX db.DatabaseTransaction) domain.BlogPostUsecase { + if usec == nil { + usec = &usecase{ + repo: repo, + dbTX: dbTX, + } + } + return usec +} diff --git a/app/middlewares/auth_middleware.go b/app/middlewares/auth_middleware.go index 7628129..942daac 100644 --- a/app/middlewares/auth_middleware.go +++ b/app/middlewares/auth_middleware.go @@ -81,6 +81,9 @@ func (m *Middleware) AuthMiddleware(allowedRole string) domain.MiddlewareFunc { writer.Header().Set("x-user-id", strconv.Itoa(user.ID)) writer.Header().Set("x-username", user.Username) + ctxUser := context.WithValue(request.Context(), contextkey.UserKey, user) + request = request.WithContext(ctxUser) + next.ServeHTTP(writer, request) }) } diff --git a/domain/blog_post.go b/domain/blog_post.go new file mode 100644 index 0000000..a3e16f5 --- /dev/null +++ b/domain/blog_post.go @@ -0,0 +1,66 @@ +package domain + +import ( + "net/http" + "time" + + "golang.org/x/net/context" +) + +type BlogPostHandler interface { + CreateBlogPost(w http.ResponseWriter, r *http.Request) + UpdateBlogPost(w http.ResponseWriter, r *http.Request) + DeleteBlogPost(w http.ResponseWriter, r *http.Request) + GetAllBlogPosts(w http.ResponseWriter, r *http.Request) + GetDetailBlogPost(w http.ResponseWriter, r *http.Request) +} + +type BlogPostUsecase interface { + CreateBlogPost(ctx context.Context, data BlogPost, user User) error + UpdateBlogPost(ctx context.Context, data BlogPost, id uint) error + DeleteBlogPost(ctx context.Context, id uint) error + GetAllBlogPosts(ctx context.Context, pagination FilterPagination) ([]BlogPost, Pagination, error) + GetDetailBlogPost(ctx context.Context, slug string, id uint) (BlogPost, error) +} + +type BlogPostRepository interface { + CreateBlogPost(ctx context.Context, data BlogPost) (BlogPost, error) + UpdateBlogPost(ctx context.Context, data BlogPost, id uint) error + DeleteBlogPost(ctx context.Context, id uint) error + GetAllBlogPosts(ctx context.Context, pagination FilterPagination) ([]BlogPost, int, error) + FindById(ctx context.Context, id uint) (BlogPost, error) + FindBySlug(ctx context.Context, slug string) (BlogPost, error) + GetTagsByBlogPostID(ctx context.Context, blogPostID uint) (tags []string, err error) + FindAuthorByUserID(ctx context.Context, userID uint) (Author, error) + CreateAuthor(ctx context.Context, data Author) error + CreateTags(ctx context.Context, tag []BlogPostTag) error +} + +type BlogPost struct { + Id int `json:"id" gorm:"primaryKey"` + Title string `json:"title"` + Content string `json:"content"` + Excerpt string `json:"excerpt"` + Author Author `json:"author" gorm:"foreignKey:AuthorID;references:UserId"` + AuthorID int `json:"author_id" gorm:"column:author_id"` + Tags []string `json:"tags" gorm:"-"` + Category string `json:"category"` + Status string `json:"status" gorm:"type:enum('draft', 'published', 'archived')"` + Slug string `json:"slug"` + PublishedAt *time.Time `json:"published_at"` + UpdatedAt *time.Time `json:"updated_at"` + CreatedAt *time.Time `json:"created_at"` + IsDeleted bool `json:"-"` +} + +type BlogPostTag struct { + Id int `json:"id" gorm:"primaryKey"` + BlogPostId int `json:"blog_post_id"` + Tag string `json:"tag"` +} + +type Author struct { + UserId int `json:"user_id"` + Name string `json:"name"` + Avatar string `json:"avatar"` +} diff --git a/go.mod b/go.mod index ed4a592..d340182 100644 --- a/go.mod +++ b/go.mod @@ -14,12 +14,14 @@ require ( github.com/spf13/viper v1.18.2 github.com/swaggo/http-swagger v1.3.4 github.com/swaggo/swag v1.16.3 + github.com/xendit/xendit-go/v6 v6.4.0 go.opentelemetry.io/otel v1.36.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 go.opentelemetry.io/otel/sdk v1.36.0 go.opentelemetry.io/otel/trace v1.36.0 golang.org/x/crypto v0.38.0 + golang.org/x/net v0.40.0 google.golang.org/grpc v1.72.1 gopkg.in/guregu/null.v4 v4.0.0 gorm.io/driver/postgres v1.5.6 @@ -69,7 +71,6 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/net v0.40.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect From f7ab2d89de9187f90d70c557bd52fd1a57d20fa5 Mon Sep 17 00:00:00 2001 From: Kocannn Date: Fri, 26 Dec 2025 23:46:28 +0800 Subject: [PATCH 2/2] refactor(blog): optimize query to avoid n+1 & make code easy to maintain --- app/blog_post/repository/delete_blog_posts.go | 21 +----- .../repository/delete_tags_by_blog_id.go | 11 +++ app/blog_post/repository/get_all_blogs.go | 25 +------ .../repository/get_tags_by_blog_ids.go | 20 ++++++ .../repository/update_author_avatar.go | 14 ++++ app/blog_post/repository/update_blog.go | 68 ++++--------------- app/blog_post/usecase/get_all_blog_posts.go | 33 +++++++++ app/blog_post/usecase/get_detail_blog_post.go | 6 +- app/blog_post/usecase/update_blog_post.go | 47 ++++++++++++- app/middlewares/auth_middleware.go | 4 +- domain/blog_post.go | 4 +- go.mod | 1 - 12 files changed, 149 insertions(+), 105 deletions(-) create mode 100644 app/blog_post/repository/delete_tags_by_blog_id.go create mode 100644 app/blog_post/repository/get_tags_by_blog_ids.go create mode 100644 app/blog_post/repository/update_author_avatar.go diff --git a/app/blog_post/repository/delete_blog_posts.go b/app/blog_post/repository/delete_blog_posts.go index add92fa..3e5f583 100644 --- a/app/blog_post/repository/delete_blog_posts.go +++ b/app/blog_post/repository/delete_blog_posts.go @@ -1,30 +1,11 @@ package repository import ( - "errors" - "github.com/hammer-code/lms-be/domain" - "github.com/sirupsen/logrus" "golang.org/x/net/context" ) // DeleteBlogPost implements domain.BlogPostRepository. func (r *repository) DeleteBlogPost(ctx context.Context, id uint) error { - db := r.db.DB(ctx).Model(&domain.BlogPost{}) - - // Perform soft delete by updating is_deleted field - result := db.Where("id = ?", id).Updates(map[string]interface{}{ - "is_deleted": true, - }) - - if result.Error != nil { - logrus.Error("failed to soft delete blog post: ", result.Error) - return result.Error - } - - if result.RowsAffected == 0 { - logrus.Warn("no blog post found to delete with id: ", id) - return errors.New("blog post not found") - } - return nil + return r.db.DB(ctx).Model(&domain.BlogPost{}).Where("id = ?", id).Updates(map[string]interface{}{"is_deleted": true}).Error } diff --git a/app/blog_post/repository/delete_tags_by_blog_id.go b/app/blog_post/repository/delete_tags_by_blog_id.go new file mode 100644 index 0000000..0694511 --- /dev/null +++ b/app/blog_post/repository/delete_tags_by_blog_id.go @@ -0,0 +1,11 @@ +package repository + +import ( + "golang.org/x/net/context" +) + +func (r *repository) DeleteTagsByBlogPostID(ctx context.Context, blogPostID uint) error { + return r.db.DB(ctx).Table("blog_post_tags"). + Where("blog_post_id = ?", blogPostID). + Delete(nil).Error +} diff --git a/app/blog_post/repository/get_all_blogs.go b/app/blog_post/repository/get_all_blogs.go index fb3f383..f969ea0 100644 --- a/app/blog_post/repository/get_all_blogs.go +++ b/app/blog_post/repository/get_all_blogs.go @@ -2,7 +2,6 @@ package repository import ( "github.com/hammer-code/lms-be/domain" - "github.com/sirupsen/logrus" "golang.org/x/net/context" ) @@ -12,39 +11,19 @@ func (r *repository) GetAllBlogPosts(ctx context.Context, pagination domain.Filt var totalCount int64 if err := r.db.DB(ctx).Model(&domain.BlogPost{}).Where("is_deleted = ?", false).Count(&totalCount).Error; err != nil { - logrus.Error("failed to count blog posts: ", err) return nil, 0, err } - offset := pagination.GetOffset() - limit := pagination.GetLimit() - orderBy := pagination.GetOrderBy() - query := r.db.DB(ctx).Preload("Author").Where("is_deleted = ?", false) - if orderBy != "" { + if orderBy := pagination.GetOrderBy(); orderBy != "" { query = query.Order(orderBy) - } else { - query = query.Order("id DESC") } - err := query.Limit(limit).Offset(offset).Find(&data).Error + err := query.Limit(pagination.GetLimit()).Offset(pagination.GetOffset()).Find(&data).Error if err != nil { - logrus.Error("failed to get all blog posts: ", err) return nil, 0, err } - for i := range data { - var tags []string - if err := r.db.DB(ctx).Table("blog_post_tags"). - Select("tag"). - Where("blog_post_id = ?", data[i].Id). - Pluck("tag", &tags).Error; err != nil { - logrus.Error("failed to get tags for blog post ID ", data[i].Id, ": ", err) - } else { - data[i].Tags = tags - } - } - return data, int(totalCount), nil } diff --git a/app/blog_post/repository/get_tags_by_blog_ids.go b/app/blog_post/repository/get_tags_by_blog_ids.go new file mode 100644 index 0000000..46583d3 --- /dev/null +++ b/app/blog_post/repository/get_tags_by_blog_ids.go @@ -0,0 +1,20 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + "golang.org/x/net/context" +) + +// GetTagsByBlogPostIDs returns raw blog post tags for given IDs. +func (r *repository) GetTagsByBlogPostIDs(ctx context.Context, blogPostIDs []int) ([]domain.BlogPostTag, error) { + var tags []domain.BlogPostTag + if len(blogPostIDs) == 0 { + return tags, nil + } + if err := r.db.DB(ctx).Table("blog_post_tags"). + Where("blog_post_id IN (?)", blogPostIDs). + Find(&tags).Error; err != nil { + return nil, err + } + return tags, nil +} diff --git a/app/blog_post/repository/update_author_avatar.go b/app/blog_post/repository/update_author_avatar.go new file mode 100644 index 0000000..ce0bede --- /dev/null +++ b/app/blog_post/repository/update_author_avatar.go @@ -0,0 +1,14 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + "golang.org/x/net/context" +) + +func (r *repository) UpdateAuthorAvatar(ctx context.Context, userID uint, avatar string) error { + return r.db.DB(ctx).Model(&domain.Author{}). + Where("user_id = ?", userID). + Updates(map[string]interface{}{ + "avatar": avatar, + }).Error +} diff --git a/app/blog_post/repository/update_blog.go b/app/blog_post/repository/update_blog.go index 8d50b19..26bd016 100644 --- a/app/blog_post/repository/update_blog.go +++ b/app/blog_post/repository/update_blog.go @@ -8,57 +8,19 @@ import ( // UpdateBlogPost implements domain.BlogPostRepository. func (r *repository) UpdateBlogPost(ctx context.Context, data domain.BlogPost, id uint) error { - return r.db.StartTransaction(ctx, func(txCtx context.Context) error { - if err := r.db.DB(txCtx).Model(&domain.BlogPost{}). - Where("id = ?", id). - Updates(map[string]interface{}{ - "title": data.Title, - "content": data.Content, - "excerpt": data.Excerpt, - "published_at": data.PublishedAt, - "updated_at": data.UpdatedAt, - "category": data.Category, - "status": data.Status, - }).Error; err != nil { - logrus.Error("failed to update blog post: ", err) - return err - } - if data.Author.Avatar != "" { - if err := r.db.DB(txCtx).Model(&domain.Author{}). - Where("user_id = ?", data.AuthorID). - Updates(map[string]interface{}{ - "avatar": data.Author.Avatar, - }).Error; err != nil { - logrus.Error("failed to update author avatar: ", err) - return err - } - } - - if len(data.Tags) > 0 { - if err := r.db.DB(txCtx).Table("blog_post_tags"). - Where("blog_post_id = ?", id). - Delete(nil).Error; err != nil { - logrus.Error("failed to delete old tags: ", err) - return err - } - - for _, tag := range data.Tags { - blogPostTag := struct { - BlogPostId int `gorm:"column:blog_post_id"` - Tag string `gorm:"column:tag"` - }{ - BlogPostId: int(id), - Tag: tag, - } - - if err := r.db.DB(txCtx).Table("blog_post_tags"). - Create(&blogPostTag).Error; err != nil { - logrus.Error("failed to create blog post tag: ", err) - return err - } - } - } - - return nil - }) + if err := r.db.DB(ctx).Model(&domain.BlogPost{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "title": data.Title, + "content": data.Content, + "excerpt": data.Excerpt, + "published_at": data.PublishedAt, + "updated_at": data.UpdatedAt, + "category": data.Category, + "status": data.Status, + }).Error; err != nil { + logrus.Error("failed to update blog post: ", err) + return err + } + return nil } diff --git a/app/blog_post/usecase/get_all_blog_posts.go b/app/blog_post/usecase/get_all_blog_posts.go index a82f38c..d820af7 100644 --- a/app/blog_post/usecase/get_all_blog_posts.go +++ b/app/blog_post/usecase/get_all_blog_posts.go @@ -8,10 +8,43 @@ import ( // GetAllBlogPosts implements domain.BlogPostUsecase. func (uc *usecase) GetAllBlogPosts(ctx context.Context, pagination domain.FilterPagination) ([]domain.BlogPost, domain.Pagination, error) { + // ensure default ordering lives in usecase layer + if pagination.GetOrderBy() == "" { + pagination.SetOrderBy("id DESC") + } + blogPosts, totalCount, err := uc.repo.GetAllBlogPosts(ctx, pagination) if err != nil { logrus.Error("failed to get all blog posts: ", err) return nil, domain.Pagination{}, err } + + // Batch fetch tags to avoid N+1 queries + ids := make([]int, 0, len(blogPosts)) + for i := range blogPosts { + ids = append(ids, blogPosts[i].Id) + } + + if len(ids) > 0 { + rawTags, tagErr := uc.repo.GetTagsByBlogPostIDs(ctx, ids) + if tagErr != nil { + logrus.Error("failed to batch get tags: ", tagErr) + } else { + // Transform []BlogPostTag to map for easier lookup + tagsMap := make(map[int][]string) + for _, t := range rawTags { + tagsMap[t.BlogPostId] = append(tagsMap[t.BlogPostId], t.Tag) + } + // Attach tags to blog posts + for i := range blogPosts { + if tags, ok := tagsMap[blogPosts[i].Id]; ok { + blogPosts[i].Tags = tags + } else { + blogPosts[i].Tags = nil + } + } + } + } + return blogPosts, domain.NewPagination(totalCount, pagination), nil } diff --git a/app/blog_post/usecase/get_detail_blog_post.go b/app/blog_post/usecase/get_detail_blog_post.go index 1fb841c..d047677 100644 --- a/app/blog_post/usecase/get_detail_blog_post.go +++ b/app/blog_post/usecase/get_detail_blog_post.go @@ -27,13 +27,15 @@ func (uc *usecase) GetDetailBlogPost(ctx context.Context, slug string, id uint) } } - tags, err := uc.repo.GetTagsByBlogPostID(ctx, uint(data.Id)) + rawTags, err := uc.repo.GetTagsByBlogPostIDs(ctx, []int{data.Id}) if err != nil { logrus.Error("failed to get tags for blog post ID ", data.Id, ": ", err) return domain.BlogPost{}, err } - data.Tags = tags + for _, t := range rawTags { + data.Tags = append(data.Tags, t.Tag) + } return data, nil } diff --git a/app/blog_post/usecase/update_blog_post.go b/app/blog_post/usecase/update_blog_post.go index 16e4e3d..f27daf1 100644 --- a/app/blog_post/usecase/update_blog_post.go +++ b/app/blog_post/usecase/update_blog_post.go @@ -1,6 +1,8 @@ package usecase import ( + "time" + "github.com/hammer-code/lms-be/domain" "github.com/sirupsen/logrus" "golang.org/x/net/context" @@ -8,10 +10,49 @@ import ( // UpdateBlogPost implements domain.BlogPostUsecase. func (uc *usecase) UpdateBlogPost(ctx context.Context, data domain.BlogPost, id uint) error { - err := uc.repo.UpdateBlogPost(ctx, data, id) - if err != nil { - logrus.Error("failed to update blog post: ", err) + now := time.Now() + data.UpdatedAt = &now + if data.Status == "published" && data.PublishedAt == nil { + data.PublishedAt = &now + } + + if err := uc.dbTX.StartTransaction(ctx, func(txCtx context.Context) error { + if err := uc.repo.UpdateBlogPost(txCtx, data, id); err != nil { + logrus.Error("failed to update blog post: ", err) + return err + } + + if data.Author.Avatar != "" { + if err := uc.repo.UpdateAuthorAvatar(txCtx, uint(data.AuthorID), data.Author.Avatar); err != nil { + logrus.Error("failed to update author avatar: ", err) + return err + } + } + + if len(data.Tags) > 0 { + if err := uc.repo.DeleteTagsByBlogPostID(txCtx, id); err != nil { + logrus.Error("failed to delete old tags: ", err) + return err + } + + tags := make([]domain.BlogPostTag, 0, len(data.Tags)) + for _, tag := range data.Tags { + tags = append(tags, domain.BlogPostTag{ + BlogPostId: int(id), + Tag: tag, + }) + } + + if err := uc.repo.CreateTags(txCtx, tags); err != nil { + logrus.Error("failed to create blog post tags: ", err) + return err + } + } + + return nil + }); err != nil { return err } + return nil } diff --git a/app/middlewares/auth_middleware.go b/app/middlewares/auth_middleware.go index b7ffd5b..8dd6247 100644 --- a/app/middlewares/auth_middleware.go +++ b/app/middlewares/auth_middleware.go @@ -82,11 +82,11 @@ func (m *Middleware) AuthMiddleware(allowedRole string) domain.MiddlewareFunc { writer.Header().Set("x-user-id", strconv.Itoa(user.ID)) writer.Header().Set("x-username", user.Username) - + ctxUser := context.WithValue(request.Context(), contextkey.UserKey, user) request = request.WithContext(ctxUser) - ctxUser := context.WithValue(request.Context(), contextkey.UserKey, user) + ctxUser = context.WithValue(request.Context(), contextkey.UserKey, user) request = request.WithContext(ctxUser) next.ServeHTTP(writer, request) diff --git a/domain/blog_post.go b/domain/blog_post.go index a3e16f5..96fcfa0 100644 --- a/domain/blog_post.go +++ b/domain/blog_post.go @@ -30,10 +30,12 @@ type BlogPostRepository interface { GetAllBlogPosts(ctx context.Context, pagination FilterPagination) ([]BlogPost, int, error) FindById(ctx context.Context, id uint) (BlogPost, error) FindBySlug(ctx context.Context, slug string) (BlogPost, error) - GetTagsByBlogPostID(ctx context.Context, blogPostID uint) (tags []string, err error) + GetTagsByBlogPostIDs(ctx context.Context, blogPostIDs []int) ([]BlogPostTag, error) + DeleteTagsByBlogPostID(ctx context.Context, blogPostID uint) error FindAuthorByUserID(ctx context.Context, userID uint) (Author, error) CreateAuthor(ctx context.Context, data Author) error CreateTags(ctx context.Context, tag []BlogPostTag) error + UpdateAuthorAvatar(ctx context.Context, userID uint, avatar string) error } type BlogPost struct { diff --git a/go.mod b/go.mod index b0ddf84..d340182 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,6 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/swaggo/files v1.0.1 // indirect - github.com/xendit/xendit-go/v6 v6.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect