Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 63 additions & 9 deletions app/blog_post/delivery/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (h Handler) CreateBlogPost(w http.ResponseWriter, r *http.Request) {
return
}

data, err := h.usecase.CreateBlogPost(r.Context(), BlogPost, token)
err = h.usecase.CreateBlogPost(r.Context(), BlogPost, token)
if err != nil {
resp := utils.CustomErrorResponse(err)
utils.Response(resp, w)
Expand All @@ -54,7 +54,6 @@ func (h Handler) CreateBlogPost(w http.ResponseWriter, r *http.Request) {
utils.Response(domain.HttpResponse{
Code: http.StatusCreated,
Message: "Blog post created successfully",
Data: data,
}, w)

}
Expand Down Expand Up @@ -93,19 +92,65 @@ func (h Handler) DeleteBlogPost(w http.ResponseWriter, r *http.Request) {

// GetAllBlogPosts implements domain.BlogPostHandler.
func (h Handler) GetAllBlogPosts(w http.ResponseWriter, r *http.Request) {
resp, err := h.usecase.GetAllBlogPosts(r.Context())
// 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: resp,
Code: http.StatusOK,
Message: "Blog posts retrieved successfully",
Data: responseDTO,
Pagination: paginationResponse,
}, w)

}

// GetDetailBlogPost implements domain.BlogPostHandler.
Expand Down Expand Up @@ -192,6 +237,15 @@ func (h Handler) UpdateBlogPost(w http.ResponseWriter, r *http.Request) {
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 {
Expand All @@ -205,7 +259,8 @@ func (h Handler) UpdateBlogPost(w http.ResponseWriter, r *http.Request) {
}
}

updatedPost.UpdatedAt = time.Now()
timeNow := time.Now()
updatedPost.UpdatedAt = &timeNow

err = h.usecase.UpdateBlogPost(r.Context(), updatedPost, uint(id))
if err != nil {
Expand All @@ -220,7 +275,6 @@ func (h Handler) UpdateBlogPost(w http.ResponseWriter, r *http.Request) {
utils.Response(domain.HttpResponse{
Code: http.StatusOK,
Message: "Blog post updated successfully",
Data: updatedPost,
}, w)
}

Expand Down
81 changes: 51 additions & 30 deletions app/blog_post/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,22 @@ type repository struct {

// 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{})
db := r.db.DB(ctx).Preload("Author").Model(&domain.BlogPost{}).Where("is_deleted = ?", false)

if typeFind == "slug" {
err = db.First(&data, "slug = ?", slug).
Error
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
}
} else if typeFind == "id" {
err = db.First(&data, "id = ?", id).
Error
case "id":
err = db.First(&data, "id = ?", id).Error
if err != nil {
logrus.Error("failed to get blog post detail: ", err)
return data, err
}
} else {
default:
return domain.BlogPost{}, errors.New("invalid typeFind parameter, must be 'slug' or 'id'")
}

Expand Down Expand Up @@ -106,12 +105,9 @@ func (r *repository) UpdateBlogPost(ctx context.Context, data domain.BlogPost, i
}

// CreateBlogPost implements domain.BlogPostRepository.
func (r *repository) CreateBlogPost(ctx context.Context, data domain.BlogPost) (domain.BlogPost, error) {
var result domain.BlogPost
var err error

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 {
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{}).
Expand All @@ -130,18 +126,16 @@ func (r *repository) CreateBlogPost(ctx context.Context, data domain.BlogPost) (
}
}

data.UpdatedAt = nil
// Set AuthorID untuk relasi
data.AuthorID = data.Author.UserId

// 2. Insert Blog Post
if err := r.db.DB(txCtx).Create(&data).Error; err != nil {
if err := r.db.DB(txCtx).Omit("updated_at").Create(&data).Error; err != nil {
logrus.Error("failed to create blog post: ", err)
return err
}

// Update result dengan data yang sudah memiliki ID
result = data

// 3. Insert Tags jika ada
if len(data.Tags) > 0 {
for _, tag := range data.Tags {
Expand All @@ -164,34 +158,61 @@ func (r *repository) CreateBlogPost(ctx context.Context, data domain.BlogPost) (
})

if err != nil {
return domain.BlogPost{}, err
return err
}

return result, nil
return nil
}

// DeleteBlogPost implements domain.BlogPostRepository.
func (r *repository) DeleteBlogPost(ctx context.Context, id uint) error {
db := r.db.DB(ctx).Model(&domain.BlogPost{})
err := db.Delete(&domain.BlogPost{}, "id = ?", id).Error
if err != nil {
logrus.Error("failed to delete blog post: ", err)
return err

// 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) (data []domain.BlogPost, err error) {
// Mengambil data blog posts dengan author dalam satu query
err = r.db.DB(ctx).
Preload("Author").Find(&data).Error
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, err
return nil, 0, err
}

// Mengambil tags untuk setiap blog post
for i := range data {
var tags []string
if err := r.db.DB(ctx).Table("blog_post_tags").
Expand All @@ -204,7 +225,7 @@ func (r *repository) GetAllBlogPosts(ctx context.Context) (data []domain.BlogPos
}
}

return data, nil
return data, int(totalCount), nil
}

func NewRepository(db pkgDB.DatabaseTransaction) domain.BlogPostRepository {
Expand Down
37 changes: 20 additions & 17 deletions app/blog_post/usecase/usecase.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package usecase

import (
"crypto/rand"
"encoding/hex"
"time"

"github.com/hammer-code/lms-be/domain"
"github.com/hammer-code/lms-be/pkg/jwt"
Expand All @@ -16,31 +15,33 @@ type usecase struct {
}

// CreateBlogPost implements domain.BlogPostUsecase.
func (uc *usecase) CreateBlogPost(ctx context.Context, data domain.BlogPost, token string) (domain.BlogPost, error) {
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 domain.BlogPost{}, err
}

slugBytes := make([]byte, 32)
if _, err := rand.Read(slugBytes); err != nil {
return domain.BlogPost{}, err
return err
}

data.Author.UserId = jwtData.ID
data.Author.Name = jwtData.UserName
data.Slug = hex.EncodeToString(slugBytes)
data.UpdatedAt = nil

if data.Status == "published" {
timeNow := time.Now()
data.PublishedAt = &timeNow
} else {
data.PublishedAt = nil
}

blogPost, err := uc.repo.CreateBlogPost(ctx, data)
err = uc.repo.CreateBlogPost(ctx, data)
if err != nil {
logrus.Error("failed to create blog post: ", err)
return domain.BlogPost{}, err
return err

}

return blogPost, nil
return nil

}

Expand All @@ -55,13 +56,15 @@ func (uc *usecase) DeleteBlogPost(ctx context.Context, id uint) error {
}

// GetAllBlogPosts implements domain.BlogPostUsecase.
func (uc *usecase) GetAllBlogPosts(ctx context.Context) ([]domain.BlogPost, error) {
blogPosts, err := uc.repo.GetAllBlogPosts(ctx)
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, err
return nil, domain.Pagination{}, err
}
return blogPosts, nil
paginationResponse := domain.NewPagination(totalCount, pagination)

return blogPosts, paginationResponse, nil
}

// GetDetailBlogPost implements domain.BlogPostUsecase.
Expand Down
2 changes: 2 additions & 0 deletions cmd/serve_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ func registerHandler(app app.App) *mux.Router {
public.HandleFunc("/events/registrations", app.EventHandler.RegisterEvent).Methods(http.MethodPost)
public.HandleFunc("/events/registrations/{order_no}", app.EventHandler.RegistrationStatus).Methods(http.MethodGet)
public.HandleFunc("/events/pay", app.EventHandler.PayEvent).Methods(http.MethodPost)
public.HandleFunc("/blogs", app.BlogPostHandler.GetAllBlogPosts).Methods(http.MethodGet)
public.HandleFunc("/blogs/{slug}", app.BlogPostHandler.GetDetailBlogPost).Methods(http.MethodGet)

protectedV1Route := v1.NewRoute().Subrouter()
protectedV1Route.Use(app.Middleware.AuthMiddleware(constants.RoleUser))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';

ALTER TABLE blog_posts
ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN is_deleted BOOLEAN DEFAULT FALSE;



-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS blog_posts CASCADE;
-- +goose StatementEnd
3 changes: 3 additions & 0 deletions database/seeder/20250522114625_seed_users.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
-- +goose Up
-- +goose StatementBegin

TRUNCATE TABLE "public"."users" RESTART IDENTITY CASCADE;

INSERT INTO "public"."users" (
"username", "email", "password", "role", "fullname",
"date_of_birth", "gender", "phone_number", "address",
Expand Down
2 changes: 2 additions & 0 deletions database/seeder/20250522114645_seed_events.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
-- +goose Up
-- +goose StatementBegin
TRUNCATE TABLE "public"."events" RESTART IDENTITY CASCADE;

INSERT INTO "public"."events" (
"id", "title", "description", "author", "image", "date",
"reservation_start_date", "reservation_end_date", "type",
Expand Down
2 changes: 2 additions & 0 deletions database/seeder/20250522114656_seed_images.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
-- +goose Up
-- +goose StatementBegin
Truncate Table "public"."images" Restart Identity Cascade;

INSERT INTO "public"."images" (
"file_name", "file_path", "format", "content_type", "is_used", "file_size"
) VALUES
Expand Down
Loading