fix: fixing caching and response api

This commit is contained in:
pahmiudahgede 2025-02-15 02:38:46 +07:00
parent 170fc79148
commit fc38a43050
3 changed files with 133 additions and 144 deletions

View File

@ -1,6 +1,8 @@
package handler package handler
import ( import (
"fmt"
"mime/multipart"
"strconv" "strconv"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -43,7 +45,6 @@ func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error {
} }
func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error { func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error {
page, err := strconv.Atoi(c.Query("page", "0")) page, err := strconv.Atoi(c.Query("page", "0"))
if err != nil || page < 1 { if err != nil || page < 1 {
page = 0 page = 0
@ -54,24 +55,17 @@ func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error {
limit = 0 limit = 0
} }
var articles []dto.ArticleResponseDTO articles, totalArticles, err := h.ArticleService.GetAllArticles(page, limit)
var totalArticles int
if page == 0 && limit == 0 {
articles, totalArticles, err = h.ArticleService.GetAllArticles(0, 0)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch articles")
}
return utils.NonPaginatedResponse(c, articles, totalArticles, "Articles fetched successfully")
}
articles, totalArticles, err = h.ArticleService.GetAllArticles(page, limit)
if err != nil { if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch articles") return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch articles")
} }
fmt.Printf("Total Articles: %d\n", totalArticles)
if page == 0 && limit == 0 {
return utils.NonPaginatedResponse(c, articles, totalArticles, "Articles fetched successfully")
}
return utils.PaginatedResponse(c, articles, page, limit, totalArticles, "Articles fetched successfully") return utils.PaginatedResponse(c, articles, page, limit, totalArticles, "Articles fetched successfully")
} }
@ -83,7 +77,8 @@ func (h *ArticleHandler) GetArticleByID(c *fiber.Ctx) error {
article, err := h.ArticleService.GetArticleByID(id) article, err := h.ArticleService.GetArticleByID(id)
if err != nil { if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, err.Error())
return utils.GenericResponse(c, fiber.StatusNotFound, "Article not found")
} }
return utils.SuccessResponse(c, article, "Article fetched successfully") return utils.SuccessResponse(c, article, "Article fetched successfully")
@ -96,7 +91,6 @@ func (h *ArticleHandler) UpdateArticle(c *fiber.Ctx) error {
} }
var request dto.RequestArticleDTO var request dto.RequestArticleDTO
if err := c.BodyParser(&request); err != nil { if err := c.BodyParser(&request); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
} }
@ -106,6 +100,7 @@ func (h *ArticleHandler) UpdateArticle(c *fiber.Ctx) error {
return utils.ValidationErrorResponse(c, errors) return utils.ValidationErrorResponse(c, errors)
} }
var coverImage *multipart.FileHeader
coverImage, err := c.FormFile("coverImage") coverImage, err := c.FormFile("coverImage")
if err != nil && err.Error() != "no such file" { if err != nil && err.Error() != "no such file" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Cover image is required") return utils.GenericResponse(c, fiber.StatusBadRequest, "Cover image is required")

View File

@ -1,6 +1,8 @@
package repositories package repositories
import ( import (
"fmt"
"github.com/pahmiudahgede/senggoldong/model" "github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -28,7 +30,10 @@ func (r *articleRepository) FindArticleByID(id string) (*model.Article, error) {
var article model.Article var article model.Article
err := r.DB.Where("id = ?", id).First(&article).Error err := r.DB.Where("id = ?", id).First(&article).Error
if err != nil { if err != nil {
return nil, err if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("article with ID %s not found", id)
}
return nil, fmt.Errorf("failed to fetch article: %v", err)
} }
return &article, nil return &article, nil
} }
@ -37,21 +42,21 @@ func (r *articleRepository) FindAllArticles(page, limit int) ([]model.Article, i
var articles []model.Article var articles []model.Article
var total int64 var total int64
err := r.DB.Model(&model.Article{}).Count(&total).Error if err := r.DB.Model(&model.Article{}).Count(&total).Error; err != nil {
if err != nil { return nil, 0, fmt.Errorf("failed to count articles: %v", err)
return nil, 0, err
} }
fmt.Printf("Total Articles Count: %d\n", total)
if page > 0 && limit > 0 { if page > 0 && limit > 0 {
err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&articles).Error err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&articles).Error
if err != nil { if err != nil {
return nil, 0, err return nil, 0, fmt.Errorf("failed to fetch articles: %v", err)
} }
} else { } else {
err := r.DB.Find(&articles).Error err := r.DB.Find(&articles).Error
if err != nil { if err != nil {
return nil, 0, err return nil, 0, fmt.Errorf("failed to fetch articles: %v", err)
} }
} }
@ -59,5 +64,5 @@ func (r *articleRepository) FindAllArticles(page, limit int) ([]model.Article, i
} }
func (r *articleRepository) UpdateArticle(id string, article *model.Article) error { func (r *articleRepository) UpdateArticle(id string, article *model.Article) error {
return r.DB.Save(article).Error return r.DB.Model(&model.Article{}).Where("id = ?", id).Updates(article).Error
} }

View File

@ -1,6 +1,7 @@
package services package services
import ( import (
"encoding/json"
"fmt" "fmt"
"mime/multipart" "mime/multipart"
"os" "os"
@ -36,8 +37,9 @@ func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage
return nil, fmt.Errorf("failed to create directory for cover image: %v", err) return nil, fmt.Errorf("failed to create directory for cover image: %v", err)
} }
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true}
extension := filepath.Ext(coverImage.Filename) extension := filepath.Ext(coverImage.Filename)
if extension != ".jpg" && extension != ".jpeg" && extension != ".png" { if !allowedExtensions[extension] {
return nil, fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") return nil, fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed")
} }
@ -90,83 +92,77 @@ func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage
cacheData := map[string]interface{}{ cacheData := map[string]interface{}{
"data": articleResponseDTO, "data": articleResponseDTO,
} }
if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil {
fmt.Printf("Error caching article to Redis: %v\n", err) fmt.Printf("Error caching article to Redis: %v\n", err)
} }
articlesCacheKey := "articles:all" articles, total, err := s.ArticleRepo.FindAllArticles(0, 0)
if err := utils.DeleteData(articlesCacheKey); err != nil { if err != nil {
fmt.Printf("Error fetching all articles: %v\n", err)
fmt.Printf("Error deleting articles cache: %v\n", err)
} }
articles, _, err := s.ArticleRepo.FindAllArticles(0, 0) var articleDTOs []dto.ArticleResponseDTO
if err == nil { for _, a := range articles {
var articleDTOs []dto.ArticleResponseDTO createdAt, _ := utils.FormatDateToIndonesianFormat(a.PublishedAt)
for _, a := range articles { updatedAt, _ := utils.FormatDateToIndonesianFormat(a.UpdatedAt)
createdAt, _ := utils.FormatDateToIndonesianFormat(a.PublishedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(a.UpdatedAt)
articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{ articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{
ID: a.ID, ID: a.ID,
Title: a.Title, Title: a.Title,
CoverImage: a.CoverImage, CoverImage: a.CoverImage,
Author: a.Author, Author: a.Author,
Heading: a.Heading, Heading: a.Heading,
Content: a.Content, Content: a.Content,
PublishedAt: createdAt, PublishedAt: createdAt,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
}) })
} }
cacheData = map[string]interface{}{ articlesCacheKey := "articles:all"
"data": articleDTOs, cacheData = map[string]interface{}{
} "data": articleDTOs,
if err := utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24); err != nil { "total": total,
}
fmt.Printf("Error caching all articles to Redis: %v\n", err) if err := utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24); err != nil {
} fmt.Printf("Error caching all articles to Redis: %v\n", err)
} else {
fmt.Printf("Error fetching all articles: %v\n", err)
} }
return articleResponseDTO, nil return articleResponseDTO, nil
} }
func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseDTO, int, error) { func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseDTO, int, error) {
var cacheKey string
cacheKey := fmt.Sprintf("articles_page:%d_limit:%d", page, limit) if page == 0 && limit == 0 {
cacheKey = "articles:all"
cachedData, err := utils.GetJSONData(cacheKey)
if err == nil && cachedData != nil {
if data, ok := cachedData["data"].([]interface{}); ok {
var articles []dto.ArticleResponseDTO
for _, item := range data {
articleData, ok := item.(map[string]interface{})
if ok {
articles = append(articles, dto.ArticleResponseDTO{
ID: articleData["article_id"].(string),
Title: articleData["title"].(string),
CoverImage: articleData["coverImage"].(string),
Author: articleData["author"].(string),
Heading: articleData["heading"].(string),
Content: articleData["content"].(string),
PublishedAt: articleData["publishedAt"].(string),
UpdatedAt: articleData["updatedAt"].(string),
})
}
}
cachedData, err := utils.GetJSONData(cacheKey) if total, ok := cachedData["total"].(float64); ok {
if err == nil && cachedData != nil { fmt.Printf("Cached Total Articles: %f\n", total)
var articles []dto.ArticleResponseDTO return articles, int(total), nil
} else {
if data, ok := cachedData["data"].([]interface{}); ok { fmt.Println("Total articles not found in cache, using 0 as fallback.")
for _, item := range data { return articles, 0, nil
articleData, ok := item.(map[string]interface{})
if ok {
articles = append(articles, dto.ArticleResponseDTO{
ID: articleData["article_id"].(string),
Title: articleData["title"].(string),
CoverImage: articleData["coverImage"].(string),
Author: articleData["author"].(string),
Heading: articleData["heading"].(string),
Content: articleData["content"].(string),
PublishedAt: articleData["publishedAt"].(string),
UpdatedAt: articleData["updatedAt"].(string),
})
} }
} }
total, ok := cachedData["total"].(float64)
if !ok {
return nil, 0, fmt.Errorf("invalid total count in cache")
}
return articles, int(total), nil
} }
} }
@ -175,6 +171,8 @@ func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseD
return nil, 0, fmt.Errorf("failed to fetch articles: %v", err) return nil, 0, fmt.Errorf("failed to fetch articles: %v", err)
} }
fmt.Printf("Total Articles from Database: %d\n", total)
var articleDTOs []dto.ArticleResponseDTO var articleDTOs []dto.ArticleResponseDTO
for _, article := range articles { for _, article := range articles {
publishedAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt) publishedAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt)
@ -192,12 +190,14 @@ func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseD
}) })
} }
cacheKey = fmt.Sprintf("articles_page:%d_limit:%d", page, limit)
cacheData := map[string]interface{}{ cacheData := map[string]interface{}{
"data": articleDTOs, "data": articleDTOs,
"total": total, "total": total,
} }
err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24)
if err != nil { fmt.Printf("Setting cache with total: %d\n", total)
if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil {
fmt.Printf("Error caching articles to Redis: %v\n", err) fmt.Printf("Error caching articles to Redis: %v\n", err)
} }
@ -205,24 +205,15 @@ func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseD
} }
func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, error) { func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, error) {
cacheKey := fmt.Sprintf("article:%s", id)
cacheKey := fmt.Sprintf("article:%s", id)
cachedData, err := utils.GetJSONData(cacheKey) cachedData, err := utils.GetJSONData(cacheKey)
if err == nil && cachedData != nil { if err == nil && cachedData != nil {
articleData, ok := cachedData["data"].(map[string]interface{}) articleResponse := &dto.ArticleResponseDTO{}
if ok { if data, ok := cachedData["data"].(string); ok {
if err := json.Unmarshal([]byte(data), articleResponse); err == nil {
article := dto.ArticleResponseDTO{ return articleResponse, nil
ID: articleData["article_id"].(string),
Title: articleData["title"].(string),
CoverImage: articleData["coverImage"].(string),
Author: articleData["author"].(string),
Heading: articleData["heading"].(string),
Content: articleData["content"].(string),
PublishedAt: articleData["publishedAt"].(string),
UpdatedAt: articleData["updatedAt"].(string),
} }
return &article, nil
} }
} }
@ -248,9 +239,7 @@ func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, err
cacheData := map[string]interface{}{ cacheData := map[string]interface{}{
"data": articleResponseDTO, "data": articleResponseDTO,
} }
err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil {
if err != nil {
fmt.Printf("Error caching article to Redis: %v\n", err) fmt.Printf("Error caching article to Redis: %v\n", err)
} }
@ -258,6 +247,7 @@ func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, err
} }
func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) { func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) {
article, err := s.ArticleRepo.FindArticleByID(id) article, err := s.ArticleRepo.FindArticleByID(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("article not found: %v", err) return nil, fmt.Errorf("article not found: %v", err)
@ -270,39 +260,10 @@ func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO,
var coverImagePath string var coverImagePath string
if coverImage != nil { if coverImage != nil {
coverImageDir := "./public/uploads/articles" coverImagePath, err = s.saveCoverImage(coverImage)
if _, err := os.Stat(coverImageDir); os.IsNotExist(err) {
err := os.MkdirAll(coverImageDir, os.ModePerm)
if err != nil {
return nil, fmt.Errorf("failed to create directory for cover image: %v", err)
}
}
extension := filepath.Ext(coverImage.Filename)
if extension != ".jpg" && extension != ".jpeg" && extension != ".png" {
return nil, fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed")
}
coverImageFileName := fmt.Sprintf("%s_cover%s", uuid.New().String(), extension)
coverImagePath = filepath.Join(coverImageDir, coverImageFileName)
src, err := coverImage.Open()
if err != nil {
return nil, fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(coverImagePath)
if err != nil {
return nil, fmt.Errorf("failed to create cover image file: %v", err)
}
defer dst.Close()
_, err = dst.ReadFrom(src)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to save cover image: %v", err) return nil, fmt.Errorf("failed to save cover image: %v", err)
} }
article.CoverImage = coverImagePath article.CoverImage = coverImagePath
} }
@ -331,15 +292,7 @@ func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO,
} }
articleCacheKey := fmt.Sprintf("article:%s", updatedArticle.ID) articleCacheKey := fmt.Sprintf("article:%s", updatedArticle.ID)
err = utils.DeleteData(articleCacheKey) err = utils.SetJSONData(articleCacheKey, map[string]interface{}{"data": articleResponseDTO}, time.Hour*24)
if err != nil {
fmt.Printf("Error deleting old cache for article: %v\n", err)
}
cacheData := map[string]interface{}{
"data": articleResponseDTO,
}
err = utils.SetJSONData(articleCacheKey, cacheData, time.Hour*24)
if err != nil { if err != nil {
fmt.Printf("Error caching updated article to Redis: %v\n", err) fmt.Printf("Error caching updated article to Redis: %v\n", err)
} }
@ -371,7 +324,7 @@ func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO,
}) })
} }
cacheData = map[string]interface{}{ cacheData := map[string]interface{}{
"data": articleDTOs, "data": articleDTOs,
} }
err = utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24) err = utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24)
@ -382,3 +335,39 @@ func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO,
return articleResponseDTO, nil return articleResponseDTO, nil
} }
func (s *articleService) saveCoverImage(coverImage *multipart.FileHeader) (string, error) {
coverImageDir := "./public/uploads/articles"
if _, err := os.Stat(coverImageDir); os.IsNotExist(err) {
if err := os.MkdirAll(coverImageDir, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create directory for cover image: %v", err)
}
}
extension := filepath.Ext(coverImage.Filename)
if extension != ".jpg" && extension != ".jpeg" && extension != ".png" {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed")
}
coverImageFileName := fmt.Sprintf("%s_cover%s", uuid.New().String(), extension)
coverImagePath := filepath.Join(coverImageDir, coverImageFileName)
src, err := coverImage.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(coverImagePath)
if err != nil {
return "", fmt.Errorf("failed to create cover image file: %v", err)
}
defer dst.Close()
_, err = dst.ReadFrom(src)
if err != nil {
return "", fmt.Errorf("failed to save cover image: %v", err)
}
return coverImagePath, nil
}