From a9f2aec4ec53205989e03139eef5c33f7c04b36c Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Sat, 8 Feb 2025 02:25:33 +0700 Subject: [PATCH] feat: add feature create article --- config/database.go | 1 + dto/article_dto.go | 50 +++++++++++++++++++++ internal/handler/article_handler.go | 35 +++++++++++++++ internal/repositories/article_repo.go | 61 +++++++++++++++++++++++++ internal/services/article_service.go | 64 +++++++++++++++++++++++++++ model/article_model.go | 14 ++++++ presentation/article_route.go | 21 +++++++++ router/setup_routes.go.go | 1 + 8 files changed, 247 insertions(+) create mode 100644 dto/article_dto.go create mode 100644 internal/handler/article_handler.go create mode 100644 internal/repositories/article_repo.go create mode 100644 internal/services/article_service.go create mode 100644 model/article_model.go create mode 100644 presentation/article_route.go diff --git a/config/database.go b/config/database.go index 375ac1d..61fdd06 100644 --- a/config/database.go +++ b/config/database.go @@ -43,6 +43,7 @@ func ConnectDatabase() { &model.Role{}, &model.UserPin{}, &model.Address{}, + &model.Article{}, // ==main feature== ) if err != nil { diff --git a/dto/article_dto.go b/dto/article_dto.go new file mode 100644 index 0000000..b947674 --- /dev/null +++ b/dto/article_dto.go @@ -0,0 +1,50 @@ +package dto + +import ( + "strings" +) + +type ArticleResponseDTO struct { + ID string `json:"article_id"` + Title string `json:"title"` + CoverImage string `json:"coverImage"` + Author string `json:"author"` + Heading string `json:"heading"` + Content string `json:"content"` + PublishedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +type RequestArticleDTO struct { + Title string `json:"title"` + CoverImage string `json:"coverImage"` + Author string `json:"author"` + Heading string `json:"heading"` + Content string `json:"content"` +} + +func (r *RequestArticleDTO) Validate() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Title) == "" { + errors["title"] = append(errors["title"], "Title is required") + } + if strings.TrimSpace(r.CoverImage) == "" { + errors["coverImage"] = append(errors["coverImage"], "Cover image is required") + } + if strings.TrimSpace(r.Author) == "" { + errors["author"] = append(errors["author"], "Author is required") + } + if strings.TrimSpace(r.Heading) == "" { + errors["heading"] = append(errors["heading"], "Heading is required") + } + if strings.TrimSpace(r.Content) == "" { + errors["content"] = append(errors["content"], "Content is required") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/handler/article_handler.go b/internal/handler/article_handler.go new file mode 100644 index 0000000..6040324 --- /dev/null +++ b/internal/handler/article_handler.go @@ -0,0 +1,35 @@ +package handler + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/utils" +) + +type ArticleHandler struct { + ArticleService services.ArticleService +} + +func NewArticleHandler(articleService services.ArticleService) *ArticleHandler { + return &ArticleHandler{ArticleService: articleService} +} + +func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error { + var requestArticleDTO dto.RequestArticleDTO + if err := c.BodyParser(&requestArticleDTO); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := requestArticleDTO.Validate() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + articleResponse, err := h.ArticleService.CreateArticle(requestArticleDTO) + if err != nil { + return utils.GenericErrorResponse(c, fiber.StatusBadRequest, err.Error()) + } + + return utils.CreateResponse(c, articleResponse, "Article created successfully") +} diff --git a/internal/repositories/article_repo.go b/internal/repositories/article_repo.go new file mode 100644 index 0000000..87f9c20 --- /dev/null +++ b/internal/repositories/article_repo.go @@ -0,0 +1,61 @@ +package repositories + +import ( + "github.com/pahmiudahgede/senggoldong/model" + "gorm.io/gorm" +) + +type ArticleRepository interface { + CreateArticle(article *model.Article) error + FindArticleByID(id string) (*model.Article, error) + FindAllArticles(page, limit int) ([]model.Article, int, error) +} + +type articleRepository struct { + DB *gorm.DB +} + +func NewArticleRepository(db *gorm.DB) ArticleRepository { + return &articleRepository{DB: db} +} + +func (r *articleRepository) CreateArticle(article *model.Article) error { + err := r.DB.Create(article).Error + if err != nil { + return err + } + return nil +} + +func (r *articleRepository) FindArticleByID(id string) (*model.Article, error) { + var article model.Article + err := r.DB.Where("id = ?", id).First(&article).Error + if err != nil { + return nil, err + } + return &article, nil +} + +func (r *articleRepository) FindAllArticles(page, limit int) ([]model.Article, int, error) { + var articles []model.Article + var total int64 + + err := r.DB.Model(&model.Article{}).Count(&total).Error + if err != nil { + return nil, 0, err + } + + if page > 0 && limit > 0 { + err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&articles).Error + if err != nil { + return nil, 0, err + } + } else { + err := r.DB.Find(&articles).Error + if err != nil { + return nil, 0, err + } + } + + return articles, int(total), nil +} diff --git a/internal/services/article_service.go b/internal/services/article_service.go new file mode 100644 index 0000000..3572d3b --- /dev/null +++ b/internal/services/article_service.go @@ -0,0 +1,64 @@ +package services + +import ( + "fmt" + "time" + + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "github.com/pahmiudahgede/senggoldong/model" + "github.com/pahmiudahgede/senggoldong/utils" +) + +type ArticleService interface { + CreateArticle(articleDTO dto.RequestArticleDTO) (*dto.ArticleResponseDTO, error) +} + +type articleService struct { + ArticleRepo repositories.ArticleRepository +} + +func NewArticleService(articleRepo repositories.ArticleRepository) ArticleService { + return &articleService{ArticleRepo: articleRepo} +} + +func (s *articleService) CreateArticle(articleDTO dto.RequestArticleDTO) (*dto.ArticleResponseDTO, error) { + + article := &model.Article{ + Title: articleDTO.Title, + CoverImage: articleDTO.CoverImage, + Author: articleDTO.Author, + Heading: articleDTO.Heading, + Content: articleDTO.Content, + } + + err := s.ArticleRepo.CreateArticle(article) + if err != nil { + return nil, fmt.Errorf("failed to create article: %v", err) + } + + publishedAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(article.UpdatedAt) + + articleResponseDTO := &dto.ArticleResponseDTO{ + ID: article.ID, + Title: article.Title, + CoverImage: article.CoverImage, + Author: article.Author, + Heading: article.Heading, + Content: article.Content, + PublishedAt: publishedAt, + UpdatedAt: updatedAt, + } + + cacheKey := fmt.Sprintf("article:%s", article.ID) + cacheData := map[string]interface{}{ + "data": articleResponseDTO, + } + err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) + if err != nil { + fmt.Printf("Error caching article to Redis: %v\n", err) + } + + return articleResponseDTO, nil +} diff --git a/model/article_model.go b/model/article_model.go new file mode 100644 index 0000000..f27acf5 --- /dev/null +++ b/model/article_model.go @@ -0,0 +1,14 @@ +package model + +import "time" + +type Article struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + Title string `gorm:"not null" json:"title"` + CoverImage string `gorm:"not null" json:"coverImage"` + Author string `gorm:"not null" json:"author"` + Heading string `gorm:"not null" json:"heading"` + Content string `gorm:"not null" json:"content"` + PublishedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +} diff --git a/presentation/article_route.go b/presentation/article_route.go new file mode 100644 index 0000000..8610a19 --- /dev/null +++ b/presentation/article_route.go @@ -0,0 +1,21 @@ +package presentation + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/config" + "github.com/pahmiudahgede/senggoldong/internal/handler" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/middleware" + "github.com/pahmiudahgede/senggoldong/utils" +) + +func ArticleRouter(api fiber.Router) { + articleRepo := repositories.NewArticleRepository(config.DB) + articleService := services.NewArticleService(articleRepo) + articleHandler := handler.NewArticleHandler(articleService) + + articleAPI := api.Group("/article-rijik") + + articleAPI.Post("/create-article", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), articleHandler.CreateArticle) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 52f1637..94806eb 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -16,4 +16,5 @@ func SetupRoutes(app *fiber.App) { presentation.RoleRouter(api) presentation.WilayahRouter(api) presentation.AddressRouter(api) + presentation.ArticleRouter(api) }