Skip to content

Commit 0879dbc

Browse files
authored
Merge pull request #20 from hammercode-dev/api/blog-posts
Api/blog posts
2 parents 3c55fb1 + 7f29501 commit 0879dbc

12 files changed

+185
-76
lines changed

app/blog_post/delivery/http/http.go

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (h Handler) CreateBlogPost(w http.ResponseWriter, r *http.Request) {
4444
return
4545
}
4646

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

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

9493
// GetAllBlogPosts implements domain.BlogPostHandler.
9594
func (h Handler) GetAllBlogPosts(w http.ResponseWriter, r *http.Request) {
96-
resp, err := h.usecase.GetAllBlogPosts(r.Context())
95+
// Ambil parameter pagination dari request
96+
pagination, err := domain.GetPaginationFromCtx(r)
97+
if err != nil {
98+
logrus.Error("failed to parse pagination parameters: ", err)
99+
utils.Response(domain.HttpResponse{
100+
Code: http.StatusBadRequest,
101+
Message: "Invalid pagination parameters",
102+
}, w)
103+
return
104+
}
105+
106+
// Panggil usecase dengan parameter pagination
107+
data, paginationResponse, err := h.usecase.GetAllBlogPosts(r.Context(), pagination)
97108
if err != nil {
98109
resp := utils.CustomErrorResponse(err)
99110
utils.Response(resp, w)
100111
return
101112
}
102113

114+
type response struct {
115+
Id int `json:"id" gorm:"primaryKey"`
116+
Title string `json:"title"`
117+
Excerpt string `json:"excerpt"`
118+
Author domain.Author `json:"author" gorm:"foreignKey:AuthorID;references:UserId"`
119+
AuthorID int `json:"author_id" gorm:"column:author_id"`
120+
Tags []string `json:"tags" gorm:"-"`
121+
Category string `json:"category"`
122+
Status string `json:"status" gorm:"type:enum('draft', 'published', 'archived')"`
123+
Slug string `json:"slug"`
124+
PublishedAt *time.Time `json:"published_at"`
125+
UpdatedAt *time.Time `json:"updated_at"`
126+
CreatedAt *time.Time `json:"created_at"`
127+
}
128+
129+
responseDTO := []response{}
130+
for _, post := range data {
131+
resp := response{
132+
Id: post.Id,
133+
Title: post.Title,
134+
Excerpt: post.Excerpt,
135+
Author: post.Author,
136+
AuthorID: post.AuthorID,
137+
Tags: post.Tags,
138+
Category: post.Category,
139+
Status: post.Status,
140+
Slug: post.Slug,
141+
PublishedAt: post.PublishedAt,
142+
UpdatedAt: post.UpdatedAt,
143+
CreatedAt: post.CreatedAt,
144+
}
145+
responseDTO = append(responseDTO, resp)
146+
}
147+
103148
utils.Response(domain.HttpResponse{
104-
Code: http.StatusOK,
105-
Message: "Blog posts retrieved successfully",
106-
Data: resp,
149+
Code: http.StatusOK,
150+
Message: "Blog posts retrieved successfully",
151+
Data: responseDTO,
152+
Pagination: paginationResponse,
107153
}, w)
108-
109154
}
110155

111156
// GetDetailBlogPost implements domain.BlogPostHandler.
@@ -192,6 +237,15 @@ func (h Handler) UpdateBlogPost(w http.ResponseWriter, r *http.Request) {
192237
if status, ok := patchData["status"].(string); ok {
193238
updatedPost.Status = status
194239
}
240+
if patchData["status"] == "published" {
241+
if updatedPost.PublishedAt == nil {
242+
timeNow := time.Now()
243+
updatedPost.PublishedAt = &timeNow
244+
}
245+
} else {
246+
updatedPost.PublishedAt = nil
247+
}
248+
195249
if tags, ok := patchData["tags"].([]interface{}); ok {
196250
updatedPost.Tags = make([]string, len(tags))
197251
for i, tag := range tags {
@@ -205,7 +259,8 @@ func (h Handler) UpdateBlogPost(w http.ResponseWriter, r *http.Request) {
205259
}
206260
}
207261

208-
updatedPost.UpdatedAt = time.Now()
262+
timeNow := time.Now()
263+
updatedPost.UpdatedAt = &timeNow
209264

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

app/blog_post/repository/repository.go

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,22 @@ type repository struct {
1515

1616
// GetDetailBlogPost implements domain.BlogPostRepository.
1717
func (r *repository) GetDetailBlogPost(ctx context.Context, slug, typeFind string, id uint) (data domain.BlogPost, err error) {
18-
db := r.db.DB(ctx).Preload("Author").Model(&domain.BlogPost{})
18+
db := r.db.DB(ctx).Preload("Author").Model(&domain.BlogPost{}).Where("is_deleted = ?", false)
1919

20-
if typeFind == "slug" {
21-
err = db.First(&data, "slug = ?", slug).
22-
Error
20+
switch typeFind {
21+
case "slug":
22+
err = db.First(&data, "slug = ?", slug).Error
2323
if err != nil {
2424
logrus.Error("failed to get blog post detail: ", err)
2525
return data, err
2626
}
27-
} else if typeFind == "id" {
28-
err = db.First(&data, "id = ?", id).
29-
Error
27+
case "id":
28+
err = db.First(&data, "id = ?", id).Error
3029
if err != nil {
3130
logrus.Error("failed to get blog post detail: ", err)
3231
return data, err
3332
}
34-
} else {
33+
default:
3534
return domain.BlogPost{}, errors.New("invalid typeFind parameter, must be 'slug' or 'id'")
3635
}
3736

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

108107
// CreateBlogPost implements domain.BlogPostRepository.
109-
func (r *repository) CreateBlogPost(ctx context.Context, data domain.BlogPost) (domain.BlogPost, error) {
110-
var result domain.BlogPost
111-
var err error
112-
108+
func (r *repository) CreateBlogPost(ctx context.Context, data domain.BlogPost) error {
113109
// Menggunakan StartTransaction yang sudah ada
114-
err = r.db.StartTransaction(ctx, func(txCtx context.Context) error {
110+
err := r.db.StartTransaction(ctx, func(txCtx context.Context) error {
115111
// 1. Periksa/Buat Author jika belum ada
116112
var authorExists int64
117113
if err := r.db.DB(txCtx).Model(&domain.Author{}).
@@ -130,18 +126,16 @@ func (r *repository) CreateBlogPost(ctx context.Context, data domain.BlogPost) (
130126
}
131127
}
132128

129+
data.UpdatedAt = nil
133130
// Set AuthorID untuk relasi
134131
data.AuthorID = data.Author.UserId
135132

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

142-
// Update result dengan data yang sudah memiliki ID
143-
result = data
144-
145139
// 3. Insert Tags jika ada
146140
if len(data.Tags) > 0 {
147141
for _, tag := range data.Tags {
@@ -164,34 +158,61 @@ func (r *repository) CreateBlogPost(ctx context.Context, data domain.BlogPost) (
164158
})
165159

166160
if err != nil {
167-
return domain.BlogPost{}, err
161+
return err
168162
}
169163

170-
return result, nil
164+
return nil
171165
}
172166

173167
// DeleteBlogPost implements domain.BlogPostRepository.
174168
func (r *repository) DeleteBlogPost(ctx context.Context, id uint) error {
175169
db := r.db.DB(ctx).Model(&domain.BlogPost{})
176-
err := db.Delete(&domain.BlogPost{}, "id = ?", id).Error
177-
if err != nil {
178-
logrus.Error("failed to delete blog post: ", err)
179-
return err
170+
171+
// Perform soft delete by updating is_deleted field
172+
result := db.Where("id = ?", id).Updates(map[string]interface{}{
173+
"is_deleted": true,
174+
})
175+
176+
if result.Error != nil {
177+
logrus.Error("failed to soft delete blog post: ", result.Error)
178+
return result.Error
179+
}
180+
181+
if result.RowsAffected == 0 {
182+
logrus.Warn("no blog post found to delete with id: ", id)
183+
return errors.New("blog post not found")
180184
}
181185
return nil
182186
}
183187

184188
// GetAllBlogPosts implements domain.BlogPostRepository.
185-
func (r *repository) GetAllBlogPosts(ctx context.Context) (data []domain.BlogPost, err error) {
186-
// Mengambil data blog posts dengan author dalam satu query
187-
err = r.db.DB(ctx).
188-
Preload("Author").Find(&data).Error
189+
func (r *repository) GetAllBlogPosts(ctx context.Context, pagination domain.FilterPagination) ([]domain.BlogPost, int, error) {
190+
var data []domain.BlogPost
191+
var totalCount int64
192+
193+
if err := r.db.DB(ctx).Model(&domain.BlogPost{}).Where("is_deleted = ?", false).Count(&totalCount).Error; err != nil {
194+
logrus.Error("failed to count blog posts: ", err)
195+
return nil, 0, err
196+
}
197+
198+
offset := pagination.GetOffset()
199+
limit := pagination.GetLimit()
200+
orderBy := pagination.GetOrderBy()
201+
202+
query := r.db.DB(ctx).Preload("Author").Where("is_deleted = ?", false)
203+
204+
if orderBy != "" {
205+
query = query.Order(orderBy)
206+
} else {
207+
query = query.Order("id DESC")
208+
}
209+
210+
err := query.Limit(limit).Offset(offset).Find(&data).Error
189211
if err != nil {
190212
logrus.Error("failed to get all blog posts: ", err)
191-
return nil, err
213+
return nil, 0, err
192214
}
193215

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

207-
return data, nil
228+
return data, int(totalCount), nil
208229
}
209230

210231
func NewRepository(db pkgDB.DatabaseTransaction) domain.BlogPostRepository {

app/blog_post/usecase/usecase.go

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
package usecase
22

33
import (
4-
"crypto/rand"
5-
"encoding/hex"
4+
"time"
65

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

1817
// CreateBlogPost implements domain.BlogPostUsecase.
19-
func (uc *usecase) CreateBlogPost(ctx context.Context, data domain.BlogPost, token string) (domain.BlogPost, error) {
18+
func (uc *usecase) CreateBlogPost(ctx context.Context, data domain.BlogPost, token string) error {
2019

2120
jwtData, err := uc.jwt.VerifyToken(token)
2221
if err != nil {
2322
logrus.Error("failed to verify token: ", err)
24-
return domain.BlogPost{}, err
25-
}
26-
27-
slugBytes := make([]byte, 32)
28-
if _, err := rand.Read(slugBytes); err != nil {
29-
return domain.BlogPost{}, err
23+
return err
3024
}
3125

3226
data.Author.UserId = jwtData.ID
3327
data.Author.Name = jwtData.UserName
34-
data.Slug = hex.EncodeToString(slugBytes)
28+
data.UpdatedAt = nil
29+
30+
if data.Status == "published" {
31+
timeNow := time.Now()
32+
data.PublishedAt = &timeNow
33+
} else {
34+
data.PublishedAt = nil
35+
}
3536

36-
blogPost, err := uc.repo.CreateBlogPost(ctx, data)
37+
err = uc.repo.CreateBlogPost(ctx, data)
3738
if err != nil {
3839
logrus.Error("failed to create blog post: ", err)
39-
return domain.BlogPost{}, err
40+
return err
4041

4142
}
4243

43-
return blogPost, nil
44+
return nil
4445

4546
}
4647

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

5758
// GetAllBlogPosts implements domain.BlogPostUsecase.
58-
func (uc *usecase) GetAllBlogPosts(ctx context.Context) ([]domain.BlogPost, error) {
59-
blogPosts, err := uc.repo.GetAllBlogPosts(ctx)
59+
func (uc *usecase) GetAllBlogPosts(ctx context.Context, pagination domain.FilterPagination) ([]domain.BlogPost, domain.Pagination, error) {
60+
blogPosts, totalCount, err := uc.repo.GetAllBlogPosts(ctx, pagination)
6061
if err != nil {
6162
logrus.Error("failed to get all blog posts: ", err)
62-
return nil, err
63+
return nil, domain.Pagination{}, err
6364
}
64-
return blogPosts, nil
65+
paginationResponse := domain.NewPagination(totalCount, pagination)
66+
67+
return blogPosts, paginationResponse, nil
6568
}
6669

6770
// GetDetailBlogPost implements domain.BlogPostUsecase.

cmd/serve_http.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ func registerHandler(app app.App) *mux.Router {
154154
public.HandleFunc("/events/registrations", app.EventHandler.RegisterEvent).Methods(http.MethodPost)
155155
public.HandleFunc("/events/registrations/{order_no}", app.EventHandler.RegistrationStatus).Methods(http.MethodGet)
156156
public.HandleFunc("/events/pay", app.EventHandler.PayEvent).Methods(http.MethodPost)
157+
public.HandleFunc("/blogs", app.BlogPostHandler.GetAllBlogPosts).Methods(http.MethodGet)
158+
public.HandleFunc("/blogs/{slug}", app.BlogPostHandler.GetDetailBlogPost).Methods(http.MethodGet)
157159

158160
protectedV1Route := v1.NewRoute().Subrouter()
159161
protectedV1Route.Use(app.Middleware.AuthMiddleware(constants.RoleUser))
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- +goose Up
2+
-- +goose StatementBegin
3+
SELECT 'up SQL query';
4+
5+
ALTER TABLE blog_posts
6+
ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
7+
ADD COLUMN is_deleted BOOLEAN DEFAULT FALSE;
8+
9+
10+
11+
-- +goose StatementEnd
12+
13+
-- +goose Down
14+
-- +goose StatementBegin
15+
DROP TABLE IF EXISTS blog_posts CASCADE;
16+
-- +goose StatementEnd

database/seeder/20250522114625_seed_users.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
-- +goose Up
22
-- +goose StatementBegin
3+
4+
TRUNCATE TABLE "public"."users" RESTART IDENTITY CASCADE;
5+
36
INSERT INTO "public"."users" (
47
"username", "email", "password", "role", "fullname",
58
"date_of_birth", "gender", "phone_number", "address",

database/seeder/20250522114645_seed_events.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
-- +goose Up
22
-- +goose StatementBegin
3+
TRUNCATE TABLE "public"."events" RESTART IDENTITY CASCADE;
4+
35
INSERT INTO "public"."events" (
46
"id", "title", "description", "author", "image", "date",
57
"reservation_start_date", "reservation_end_date", "type",

database/seeder/20250522114656_seed_images.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
-- +goose Up
22
-- +goose StatementBegin
3+
Truncate Table "public"."images" Restart Identity Cascade;
4+
35
INSERT INTO "public"."images" (
46
"file_name", "file_path", "format", "content_type", "is_used", "file_size"
57
) VALUES

0 commit comments

Comments
 (0)