diff --git a/app/app.go b/app/app.go index 3dabf8f..c3033ef 100644 --- a/app/app.go +++ b/app/app.go @@ -60,7 +60,7 @@ func InitApp( newsletterUC := newsletters.InitUsecase(cfg, newsletterRepo, dbTx, jwt.NewJwt(cfg.JWT_SECRET_KEY)) eventUC := events.InitUsecase(cfg, eventRepo, imgRepo, dbTx) imgUc := images.InitUsecase(imgRepo, dbTx) - blogPostUc := blogPost.InitUseCase(blogPostRepo, jwtInstance) + blogPostUc := blogPost.InitUseCase(blogPostRepo, dbTx) transactionEventUsecase := transactionEventUC.NewUsecase(transactionEventRepository, eventRepo, xenditClient, cfg) // handler diff --git a/app/blog_post/blogPost.go b/app/blog_post/blogPost.go index 53e914b..bba86bf 100644 --- a/app/blog_post/blogPost.go +++ b/app/blog_post/blogPost.go @@ -6,15 +6,14 @@ import ( 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" - "github.com/hammer-code/lms-be/pkg/jwt" ) func InitRepository(db db.DatabaseTransaction) domain.BlogPostRepository { return blog_post_repo.NewRepository(db) } -func InitUseCase(repository domain.BlogPostRepository, jwt jwt.JWT) domain.BlogPostUsecase { - return blog_post_usecase.NewUsecase(repository, jwt) +func InitUseCase(repository domain.BlogPostRepository, db db.DatabaseTransaction) domain.BlogPostUsecase { + return blog_post_usecase.NewUsecase(repository, db) } func InitHandler(usecase domain.BlogPostUsecase) domain.BlogPostHandler { 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 index aa37be6..cc3e7a9 100644 --- a/app/blog_post/delivery/http/http.go +++ b/app/blog_post/delivery/http/http.go @@ -1,283 +1,13 @@ package http import ( - "encoding/json" - "io" - "net/http" - "strconv" - "strings" - "time" - - "github.com/gorilla/mux" "github.com/hammer-code/lms-be/domain" - "github.com/hammer-code/lms-be/utils" - "github.com/sirupsen/logrus" ) type Handler struct { usecase domain.BlogPostUsecase } -// 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 - } - - var token string - if hasToken := strings.HasPrefix(r.Header.Get("Authorization"), "Bearer "); hasToken { - token = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") - if token == "" { - resp := utils.CustomErrorResponse(utils.NewUnauthorizedError(r.Context(), "Authorization token is required", nil)) - utils.Response(resp, w) - return - } - } - - 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, token) - 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) - -} - -// 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) -} - -// 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) -} - -// 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) -} - -// 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) -} - var ( handlr *Handler ) 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..3e5f583 --- /dev/null +++ b/app/blog_post/repository/delete_blog_posts.go @@ -0,0 +1,11 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + "golang.org/x/net/context" +) + +// DeleteBlogPost implements domain.BlogPostRepository. +func (r *repository) DeleteBlogPost(ctx context.Context, id uint) error { + 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/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..f969ea0 --- /dev/null +++ b/app/blog_post/repository/get_all_blogs.go @@ -0,0 +1,29 @@ +package repository + +import ( + "github.com/hammer-code/lms-be/domain" + "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 { + return nil, 0, err + } + + query := r.db.DB(ctx).Preload("Author").Where("is_deleted = ?", false) + + if orderBy := pagination.GetOrderBy(); orderBy != "" { + query = query.Order(orderBy) + } + + err := query.Limit(pagination.GetLimit()).Offset(pagination.GetOffset()).Find(&data).Error + if err != nil { + return nil, 0, err + } + + 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/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 index 7b5f04c..acdcd56 100644 --- a/app/blog_post/repository/repository.go +++ b/app/blog_post/repository/repository.go @@ -1,233 +1,14 @@ package repository import ( - "errors" - "github.com/hammer-code/lms-be/domain" pkgDB "github.com/hammer-code/lms-be/pkg/db" - "github.com/sirupsen/logrus" - "golang.org/x/net/context" ) type repository struct { db pkgDB.DatabaseTransaction } -// GetDetailBlogPost implements domain.BlogPostRepository. -func (r *repository) GetDetailBlogPost(ctx context.Context, slug, typeFind string, id uint) (data domain.BlogPost, err error) { - db := r.db.DB(ctx).Preload("Author").Model(&domain.BlogPost{}).Where("is_deleted = ?", false) - - switch typeFind { - case "slug": - err = db.First(&data, "slug = ?", slug).Error - if err != nil { - logrus.Error("failed to get blog post detail: ", err) - return data, err - } - case "id": - err = db.First(&data, "id = ?", id).Error - if err != nil { - logrus.Error("failed to get blog post detail: ", err) - return data, err - } - default: - return domain.BlogPost{}, errors.New("invalid typeFind parameter, must be 'slug' or 'id'") - } - - var tags []string - if err := r.db.DB(ctx).Table("blog_post_tags"). - Select("tag"). - Where("blog_post_id = ?", data.Id). - Pluck("tag", &tags).Error; err != nil { - logrus.Error("failed to get tags for blog post ID ", data.Id, ": ", err) - } else { - data.Tags = tags - } - - return data, nil -} - -// 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 - }) -} - -// CreateBlogPost implements domain.BlogPostRepository. -func (r *repository) CreateBlogPost(ctx context.Context, data domain.BlogPost) error { - // Menggunakan StartTransaction yang sudah ada - err := r.db.StartTransaction(ctx, func(txCtx context.Context) error { - // 1. Periksa/Buat Author jika belum ada - var authorExists int64 - if err := r.db.DB(txCtx).Model(&domain.Author{}). - Preload("Author"). - Where("user_id = ?", data.Author.UserId). - Count(&authorExists).Error; err != nil { - logrus.Error("failed to check author existence: ", err) - return err - } - - if authorExists == 0 { - // Insert author - if err := r.db.DB(txCtx).Create(&data.Author).Error; err != nil { - logrus.Error("failed to create author: ", err) - return err - } - } - - data.UpdatedAt = nil - // Set AuthorID untuk relasi - data.AuthorID = data.Author.UserId - - // 2. Insert Blog Post - if err := r.db.DB(txCtx).Omit("updated_at").Create(&data).Error; err != nil { - logrus.Error("failed to create blog post: ", err) - return err - } - - // 3. Insert Tags jika ada - if len(data.Tags) > 0 { - for _, tag := range data.Tags { - blogPostTag := struct { - BlogPostId int `gorm:"column:blog_post_id"` - Tag string `gorm:"column:tag"` - }{ - BlogPostId: data.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 != nil { - return err - } - - return nil -} - -// 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 -} - -// 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 -} - func NewRepository(db pkgDB.DatabaseTransaction) domain.BlogPostRepository { return &repository{ db: db, 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 new file mode 100644 index 0000000..26bd016 --- /dev/null +++ b/app/blog_post/repository/update_blog.go @@ -0,0 +1,26 @@ +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 { + 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/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..d820af7 --- /dev/null +++ b/app/blog_post/usecase/get_all_blog_posts.go @@ -0,0 +1,50 @@ +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) { + // 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 new file mode 100644 index 0000000..d047677 --- /dev/null +++ b/app/blog_post/usecase/get_detail_blog_post.go @@ -0,0 +1,41 @@ +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 + } + } + + 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 + } + + 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 new file mode 100644 index 0000000..f27daf1 --- /dev/null +++ b/app/blog_post/usecase/update_blog_post.go @@ -0,0 +1,58 @@ +package usecase + +import ( + "time" + + "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 { + 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/blog_post/usecase/usecase.go b/app/blog_post/usecase/usecase.go index bfa6499..6a22547 100644 --- a/app/blog_post/usecase/usecase.go +++ b/app/blog_post/usecase/usecase.go @@ -1,105 +1,24 @@ package usecase import ( - "time" - "github.com/hammer-code/lms-be/domain" - "github.com/hammer-code/lms-be/pkg/jwt" - "github.com/sirupsen/logrus" - "golang.org/x/net/context" + "github.com/hammer-code/lms-be/pkg/db" ) type usecase struct { repo domain.BlogPostRepository - jwt jwt.JWT -} - -// CreateBlogPost implements domain.BlogPostUsecase. -func (uc *usecase) CreateBlogPost(ctx context.Context, data domain.BlogPost, token string) error { - - jwtData, err := uc.jwt.VerifyToken(token) - if err != nil { - logrus.Error("failed to verify token: ", err) - return err - } - - data.Author.UserId = jwtData.ID - data.Author.Name = jwtData.UserName - data.UpdatedAt = nil - - if data.Status == "published" { - timeNow := time.Now() - data.PublishedAt = &timeNow - } else { - data.PublishedAt = nil - } - - err = uc.repo.CreateBlogPost(ctx, data) - if err != nil { - logrus.Error("failed to create blog post: ", err) - return err - - } - - return nil - -} - -// 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 -} - -// 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 - } - paginationResponse := domain.NewPagination(totalCount, pagination) - - return blogPosts, paginationResponse, nil -} - -// GetDetailBlogPost implements domain.BlogPostUsecase. -func (uc *usecase) GetDetailBlogPost(ctx context.Context, slug string, id uint) (domain.BlogPost, error) { - typeFind := "slug" - if slug == "" { - typeFind = "id" - } - blogPost, err := uc.repo.GetDetailBlogPost(ctx, slug, typeFind, id) - if err != nil { - logrus.Error("failed to get blog post detail: ", err) - return domain.BlogPost{}, err - } - return blogPost, nil -} - -// 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 + dbTX db.DatabaseTransaction } var ( usec *usecase ) -func NewUsecase(repo domain.BlogPostRepository, jwt jwt.JWT) domain.BlogPostUsecase { +func NewUsecase(repo domain.BlogPostRepository, dbTX db.DatabaseTransaction) domain.BlogPostUsecase { if usec == nil { usec = &usecase{ repo: repo, - jwt: jwt, + dbTX: dbTX, } } return usec diff --git a/app/middlewares/auth_middleware.go b/app/middlewares/auth_middleware.go index e9c74ba..8dd6247 100644 --- a/app/middlewares/auth_middleware.go +++ b/app/middlewares/auth_middleware.go @@ -82,10 +82,13 @@ 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) + request = request.WithContext(ctxUser) + next.ServeHTTP(writer, request) }) } diff --git a/domain/blog_post.go b/domain/blog_post.go index 11d8409..96fcfa0 100644 --- a/domain/blog_post.go +++ b/domain/blog_post.go @@ -16,7 +16,7 @@ type BlogPostHandler interface { } type BlogPostUsecase interface { - CreateBlogPost(ctx context.Context, data BlogPost, token string) error + 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) @@ -24,11 +24,18 @@ type BlogPostUsecase interface { } type BlogPostRepository interface { - CreateBlogPost(ctx context.Context, data BlogPost) error + 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) - GetDetailBlogPost(ctx context.Context, slug, typeFind string, id uint) (BlogPost, error) + FindById(ctx context.Context, id uint) (BlogPost, error) + FindBySlug(ctx context.Context, slug string) (BlogPost, 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 { @@ -48,6 +55,12 @@ type BlogPost struct { 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"` diff --git a/go.mod b/go.mod index ed02da5..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 @@ -62,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 @@ -70,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