From 63c5c03a9cc3532d79e1367276d23985b91503c7 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 28 Jul 2025 14:32:54 +0800 Subject: [PATCH 1/8] add categories endpoint --- boost/app.go | 1 + boost/router.go | 9 ++++ database/repository/categories.go | 37 ++++++++++++++++ database/repository/posts.go | 2 +- handler/categories.go | 54 ++++++++++++++++++++++++ handler/paginate/paginate.go | 37 ++++++++++++++++ handler/posts.go | 5 ++- handler/posts/response.go | 70 ++++++++++++++----------------- handler/posts/transformer.go | 53 ++++------------------- 9 files changed, 181 insertions(+), 87 deletions(-) create mode 100644 handler/categories.go create mode 100644 handler/paginate/paginate.go diff --git a/boost/app.go b/boost/app.go index 19ce435a..34f760c9 100644 --- a/boost/app.go +++ b/boost/app.go @@ -71,4 +71,5 @@ func (a *App) Boot() { router.Education() router.Recommendations() router.Posts() + router.Categories() } diff --git a/boost/router.go b/boost/router.go index be4921ee..bc0b43c7 100644 --- a/boost/router.go +++ b/boost/router.go @@ -42,6 +42,15 @@ func (r *Router) Posts() { r.Mux.HandleFunc("GET /posts/{slug}", show) } +func (r *Router) Categories() { + repo := repository.Categories{DB: r.Db} + abstract := handler.MakeCategoriesHandler(&repo) + + index := r.PipelineFor(abstract.Index) + + r.Mux.HandleFunc("GET /categories", index) +} + func (r *Router) Profile() { abstract := handler.MakeProfileHandler("./storage/fixture/profile.json") diff --git a/database/repository/categories.go b/database/repository/categories.go index b7e65d2c..7983d9c4 100644 --- a/database/repository/categories.go +++ b/database/repository/categories.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/google/uuid" "github.com/oullin/database" + "github.com/oullin/database/repository/pagination" "github.com/oullin/pkg/gorm" "strings" ) @@ -12,6 +13,42 @@ type Categories struct { DB *database.Connection } +func (c Categories) GetAll(paginate pagination.Paginate) (*pagination.Pagination[database.Category], error) { + var numItems int64 + var categories []database.Category + + query := c.DB.Sql(). + Model(&database.Category{}). + Where("categories.deleted_at is null"). + Limit(paginate.Limit). + Order("categories.name asc") + + countQuery := query. + Session(c.DB.GetSession()). // clone the based query. + Distinct("categories.id") // remove duplicated posts to get the actual count. + + if err := countQuery.Count(&numItems).Error; err != nil { + return nil, err + } + + offset := (paginate.Page - 1) * paginate.Limit + + err := query.Preload("Posts"). + Limit(paginate.Limit). + Offset(offset). + Distinct(). + Find(&categories).Error + + if err != nil { + return nil, err + } + + paginate.SetNumItems(numItems) + result := pagination.MakePagination[database.Category](categories, paginate) + + return result, nil +} + func (c Categories) FindBy(slug string) *database.Category { category := database.Category{} diff --git a/database/repository/posts.go b/database/repository/posts.go index 4b224e9f..a78ae7b3 100644 --- a/database/repository/posts.go +++ b/database/repository/posts.go @@ -15,7 +15,7 @@ type Posts struct { Tags *Tags } -func (p Posts) GetPosts(filters queries.PostFilters, paginate pagination.Paginate) (*pagination.Pagination[database.Post], error) { +func (p Posts) GetAll(filters queries.PostFilters, paginate pagination.Paginate) (*pagination.Pagination[database.Post], error) { var numItems int64 var posts []database.Post diff --git a/handler/categories.go b/handler/categories.go new file mode 100644 index 00000000..4c2555cf --- /dev/null +++ b/handler/categories.go @@ -0,0 +1,54 @@ +package handler + +import ( + "encoding/json" + "github.com/oullin/database" + "github.com/oullin/database/repository" + "github.com/oullin/database/repository/pagination" + "github.com/oullin/handler/paginate" + "github.com/oullin/handler/posts" + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" +) + +type CategoriesHandler struct { + Categories *repository.Categories +} + +func MakeCategoriesHandler(categories *repository.Categories) CategoriesHandler { + return CategoriesHandler{ + Categories: categories, + } +} + +func (h *CategoriesHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { + result, err := h.Categories.GetAll( + paginate.MakeFrom(r.URL.Query()), + ) + + if err != nil { + slog.Error("Error getting categories", "err", err) + return http.InternalError("Error getting categories") + } + + items := pagination.HydratePagination( + result, + func(s database.Category) posts.CategoryResponse { + return posts.CategoryResponse{ + UUID: s.UUID, + Name: s.Name, + Slug: s.Slug, + Description: s.Description, + } + }, + ) + + if err := json.NewEncoder(w).Encode(items); err != nil { + slog.Error("failed to encode response", "err", err) + + return http.InternalError("There was an issue processing the response. Please, try later.") + } + + return nil +} diff --git a/handler/paginate/paginate.go b/handler/paginate/paginate.go new file mode 100644 index 00000000..2365bc2b --- /dev/null +++ b/handler/paginate/paginate.go @@ -0,0 +1,37 @@ +package paginate + +import ( + "github.com/oullin/database/repository/pagination" + "net/url" + "strconv" +) + +func MakeFrom(url url.Values) pagination.Paginate { + page := pagination.MinPage + pageSize := pagination.MaxLimit + + if url.Get("page") != "" { + if tPage, err := strconv.Atoi(url.Get("page")); err == nil { + page = tPage + } + } + + if url.Get("limit") != "" { + if limit, err := strconv.Atoi(url.Get("limit")); err == nil { + pageSize = limit + } + } + + if page < pagination.MinPage { + page = pagination.MinPage + } + + if pageSize > pagination.MaxLimit || pageSize < 1 { + pageSize = pagination.MaxLimit + } + + return pagination.Paginate{ + Page: page, + Limit: pageSize, + } +} diff --git a/handler/posts.go b/handler/posts.go index b7aa2157..d223f4e5 100644 --- a/handler/posts.go +++ b/handler/posts.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/oullin/database/repository" "github.com/oullin/database/repository/pagination" + "github.com/oullin/handler/paginate" "github.com/oullin/handler/posts" "github.com/oullin/pkg" "github.com/oullin/pkg/http" @@ -33,9 +34,9 @@ func (h *PostsHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *ht return http.InternalError("There was an issue reading the request. Please, try again later.") } - result, err := h.Posts.GetPosts( + result, err := h.Posts.GetAll( posts.GetFiltersFrom(payload), - posts.GetPaginateFrom(r.URL.Query()), + paginate.MakeFrom(r.URL.Query()), ) if err != nil { diff --git a/handler/posts/response.go b/handler/posts/response.go index ea07333c..2bac8896 100644 --- a/handler/posts/response.go +++ b/handler/posts/response.go @@ -13,50 +13,44 @@ type IndexRequestBody struct { } type PostResponse struct { - UUID string `json:"uuid"` - Author UserData `json:"author"` - Slug string `json:"slug"` - Title string `json:"title"` - Excerpt string `json:"excerpt"` - Content string `json:"content"` - CoverImageURL string `json:"cover_image_url"` - PublishedAt *time.Time `json:"published_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + UUID string `json:"uuid"` + Author UserResponse `json:"author"` + Slug string `json:"slug"` + Title string `json:"title"` + Excerpt string `json:"excerpt"` + Content string `json:"content"` + CoverImageURL string `json:"cover_image_url"` + PublishedAt *time.Time `json:"published_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // Associations - Categories []CategoryData `json:"categories"` - Tags []TagData `json:"tags"` + Categories []CategoryResponse `json:"categories"` + Tags []TagResponse `json:"tags"` } -type UserData struct { - UUID string `json:"uuid"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Username string `json:"username"` - DisplayName string `json:"display_name"` - Bio string `json:"bio"` - PictureFileName string `json:"picture_file_name"` - ProfilePictureURL string `json:"profile_picture_url"` - IsAdmin bool `json:"is_admin"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` +type UserResponse struct { + UUID string `json:"uuid"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Username string `json:"username"` + DisplayName string `json:"display_name"` + Bio string `json:"bio"` + PictureFileName string `json:"picture_file_name"` + ProfilePictureURL string `json:"profile_picture_url"` + IsAdmin bool `json:"is_admin"` } -type CategoryData struct { - UUID string `json:"uuid"` - Name string `json:"name"` - Slug string `json:"slug"` - Description string `json:"description"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` +type CategoryResponse struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` } -type TagData struct { - UUID string `json:"uuid"` - Name string `json:"name"` - Slug string `json:"slug"` - Description string `json:"description"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` +type TagResponse struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` } diff --git a/handler/posts/transformer.go b/handler/posts/transformer.go index b87177e8..ed885c68 100644 --- a/handler/posts/transformer.go +++ b/handler/posts/transformer.go @@ -2,12 +2,9 @@ package posts import ( "github.com/oullin/database" - "github.com/oullin/database/repository/pagination" "github.com/oullin/database/repository/queries" "github.com/oullin/pkg" baseHttp "net/http" - "net/url" - "strconv" "strings" ) @@ -24,7 +21,7 @@ func GetPostsResponse(p database.Post) PostResponse { UpdatedAt: p.UpdatedAt, Categories: GetCategoriesResponse(p.Categories), Tags: GetTagsResponse(p.Tags), - Author: UserData{ + Author: UserResponse{ UUID: p.Author.UUID, FirstName: p.Author.FirstName, LastName: p.Author.LastName, @@ -34,76 +31,40 @@ func GetPostsResponse(p database.Post) PostResponse { PictureFileName: p.Author.PictureFileName, ProfilePictureURL: p.Author.ProfilePictureURL, IsAdmin: p.Author.IsAdmin, - CreatedAt: p.Author.CreatedAt, - UpdatedAt: p.Author.UpdatedAt, }, } } -func GetCategoriesResponse(categories []database.Category) []CategoryData { - var data []CategoryData +func GetCategoriesResponse(categories []database.Category) []CategoryResponse { + var data []CategoryResponse for _, category := range categories { - data = append(data, CategoryData{ + data = append(data, CategoryResponse{ UUID: category.UUID, Name: category.Name, Slug: category.Slug, Description: category.Description, - CreatedAt: category.CreatedAt, - UpdatedAt: category.UpdatedAt, }) } return data } -func GetTagsResponse(tags []database.Tag) []TagData { - var data []TagData +func GetTagsResponse(tags []database.Tag) []TagResponse { + var data []TagResponse for _, tag := range tags { - data = append(data, TagData{ + data = append(data, TagResponse{ UUID: tag.UUID, Name: tag.Name, Slug: tag.Slug, Description: tag.Description, - CreatedAt: tag.CreatedAt, - UpdatedAt: tag.UpdatedAt, }) } return data } -func GetPaginateFrom(url url.Values) pagination.Paginate { - page := pagination.MinPage - pageSize := pagination.MaxLimit - - if url.Get("page") != "" { - if tPage, err := strconv.Atoi(url.Get("page")); err == nil { - page = tPage - } - } - - if url.Get("limit") != "" { - if limit, err := strconv.Atoi(url.Get("limit")); err == nil { - pageSize = limit - } - } - - if page < pagination.MinPage { - page = pagination.MinPage - } - - if pageSize > pagination.MaxLimit || pageSize < 1 { - pageSize = pagination.MaxLimit - } - - return pagination.Paginate{ - Page: page, - Limit: pageSize, - } -} - func GetFiltersFrom(request IndexRequestBody) queries.PostFilters { return queries.PostFilters{ Title: request.Title, From e56de2b6b439d68e06f8381421a68eefb5323b92 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 28 Jul 2025 14:57:02 +0800 Subject: [PATCH 2/8] extract count function --- database/repository/categories.go | 6 +----- database/repository/pagination/support.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 database/repository/pagination/support.go diff --git a/database/repository/categories.go b/database/repository/categories.go index 7983d9c4..782daf03 100644 --- a/database/repository/categories.go +++ b/database/repository/categories.go @@ -23,11 +23,7 @@ func (c Categories) GetAll(paginate pagination.Paginate) (*pagination.Pagination Limit(paginate.Limit). Order("categories.name asc") - countQuery := query. - Session(c.DB.GetSession()). // clone the based query. - Distinct("categories.id") // remove duplicated posts to get the actual count. - - if err := countQuery.Count(&numItems).Error; err != nil { + if err := pagination.Count[*int64](&numItems, query, c.DB.GetSession(), "categories.id"); err != nil { return nil, err } diff --git a/database/repository/pagination/support.go b/database/repository/pagination/support.go new file mode 100644 index 00000000..7c32fe1d --- /dev/null +++ b/database/repository/pagination/support.go @@ -0,0 +1,15 @@ +package pagination + +import "gorm.io/gorm" + +func Count[T *int64](numItems T, query *gorm.DB, session *gorm.Session, distinct string) error { + sql := query. + Session(session). // clone the based query. + Distinct(distinct) // remove duplicated; if any to get the actual count. + + if sql.Count(numItems).Error != nil { + return sql.Error + } + + return nil +} From 1ea4ac484a18f321af9d1a67c05bd657f74bd179 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 28 Jul 2025 15:01:09 +0800 Subject: [PATCH 3/8] update the posts method too --- database/repository/posts.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/database/repository/posts.go b/database/repository/posts.go index a78ae7b3..0c6e5220 100644 --- a/database/repository/posts.go +++ b/database/repository/posts.go @@ -26,11 +26,7 @@ func (p Posts) GetAll(filters queries.PostFilters, paginate pagination.Paginate) queries.ApplyPostsFilters(&filters, query) - countQuery := query. - Session(p.DB.GetSession()). // clone the based query. - Distinct("posts.id") // remove duplicated posts to get the actual count. - - if err := countQuery.Count(&numItems).Error; err != nil { + if err := pagination.Count[*int64](&numItems, query, p.DB.GetSession(), "posts.id"); err != nil { return nil, err } From a6bbffeb6c24d4cbc50b3fb1720fc8874e19afb2 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 28 Jul 2025 15:17:57 +0800 Subject: [PATCH 4/8] flatten handlers folder --- handler/categories.go | 6 +- handler/payload/categories.go | 25 ++++++ .../transformer.go => payload/posts.go} | 88 +++++++++---------- handler/payload/tags.go | 25 ++++++ handler/payload/users.go | 13 +++ handler/posts.go | 12 +-- handler/posts/response.go | 56 ------------ 7 files changed, 114 insertions(+), 111 deletions(-) create mode 100644 handler/payload/categories.go rename handler/{posts/transformer.go => payload/posts.go} (59%) create mode 100644 handler/payload/tags.go create mode 100644 handler/payload/users.go delete mode 100644 handler/posts/response.go diff --git a/handler/categories.go b/handler/categories.go index 4c2555cf..f3e833e7 100644 --- a/handler/categories.go +++ b/handler/categories.go @@ -6,7 +6,7 @@ import ( "github.com/oullin/database/repository" "github.com/oullin/database/repository/pagination" "github.com/oullin/handler/paginate" - "github.com/oullin/handler/posts" + "github.com/oullin/handler/payload" "github.com/oullin/pkg/http" "log/slog" baseHttp "net/http" @@ -34,8 +34,8 @@ func (h *CategoriesHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request items := pagination.HydratePagination( result, - func(s database.Category) posts.CategoryResponse { - return posts.CategoryResponse{ + func(s database.Category) payload.CategoryResponse { + return payload.CategoryResponse{ UUID: s.UUID, Name: s.Name, Slug: s.Slug, diff --git a/handler/payload/categories.go b/handler/payload/categories.go new file mode 100644 index 00000000..fedb7380 --- /dev/null +++ b/handler/payload/categories.go @@ -0,0 +1,25 @@ +package payload + +import "github.com/oullin/database" + +type CategoryResponse struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` +} + +func GetCategoriesResponse(categories []database.Category) []CategoryResponse { + var data []CategoryResponse + + for _, category := range categories { + data = append(data, CategoryResponse{ + UUID: category.UUID, + Name: category.Name, + Slug: category.Slug, + Description: category.Description, + }) + } + + return data +} diff --git a/handler/posts/transformer.go b/handler/payload/posts.go similarity index 59% rename from handler/posts/transformer.go rename to handler/payload/posts.go index ed885c68..566179ca 100644 --- a/handler/posts/transformer.go +++ b/handler/payload/posts.go @@ -1,4 +1,4 @@ -package posts +package payload import ( "github.com/oullin/database" @@ -6,8 +6,49 @@ import ( "github.com/oullin/pkg" baseHttp "net/http" "strings" + "time" ) +type IndexRequestBody struct { + Title string `json:"title"` + Author string `json:"author"` + Category string `json:"category"` + Tag string `json:"tag"` + Text string `json:"text"` +} + +type PostResponse struct { + UUID string `json:"uuid"` + Author UserResponse `json:"author"` + Slug string `json:"slug"` + Title string `json:"title"` + Excerpt string `json:"excerpt"` + Content string `json:"content"` + CoverImageURL string `json:"cover_image_url"` + PublishedAt *time.Time `json:"published_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + // Associations + Categories []CategoryResponse `json:"categories"` + Tags []TagResponse `json:"tags"` +} + +func GetPostsFiltersFrom(request IndexRequestBody) queries.PostFilters { + return queries.PostFilters{ + Title: request.Title, + Author: request.Author, + Category: request.Category, + Tag: request.Tag, + } +} + +func GetSlugFrom(r *baseHttp.Request) string { + str := pkg.MakeStringable(r.PathValue("slug")) + + return strings.TrimSpace(str.ToLower()) +} + func GetPostsResponse(p database.Post) PostResponse { return PostResponse{ UUID: p.UUID, @@ -34,48 +75,3 @@ func GetPostsResponse(p database.Post) PostResponse { }, } } - -func GetCategoriesResponse(categories []database.Category) []CategoryResponse { - var data []CategoryResponse - - for _, category := range categories { - data = append(data, CategoryResponse{ - UUID: category.UUID, - Name: category.Name, - Slug: category.Slug, - Description: category.Description, - }) - } - - return data -} - -func GetTagsResponse(tags []database.Tag) []TagResponse { - var data []TagResponse - - for _, tag := range tags { - data = append(data, TagResponse{ - UUID: tag.UUID, - Name: tag.Name, - Slug: tag.Slug, - Description: tag.Description, - }) - } - - return data -} - -func GetFiltersFrom(request IndexRequestBody) queries.PostFilters { - return queries.PostFilters{ - Title: request.Title, - Author: request.Author, - Category: request.Category, - Tag: request.Tag, - } -} - -func GetSlugFrom(r *baseHttp.Request) string { - str := pkg.MakeStringable(r.PathValue("slug")) - - return strings.TrimSpace(str.ToLower()) -} diff --git a/handler/payload/tags.go b/handler/payload/tags.go new file mode 100644 index 00000000..6a4c2472 --- /dev/null +++ b/handler/payload/tags.go @@ -0,0 +1,25 @@ +package payload + +import "github.com/oullin/database" + +type TagResponse struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` +} + +func GetTagsResponse(tags []database.Tag) []TagResponse { + var data []TagResponse + + for _, tag := range tags { + data = append(data, TagResponse{ + UUID: tag.UUID, + Name: tag.Name, + Slug: tag.Slug, + Description: tag.Description, + }) + } + + return data +} diff --git a/handler/payload/users.go b/handler/payload/users.go new file mode 100644 index 00000000..ddb0a3d2 --- /dev/null +++ b/handler/payload/users.go @@ -0,0 +1,13 @@ +package payload + +type UserResponse struct { + UUID string `json:"uuid"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Username string `json:"username"` + DisplayName string `json:"display_name"` + Bio string `json:"bio"` + PictureFileName string `json:"picture_file_name"` + ProfilePictureURL string `json:"profile_picture_url"` + IsAdmin bool `json:"is_admin"` +} diff --git a/handler/posts.go b/handler/posts.go index d223f4e5..04020dd3 100644 --- a/handler/posts.go +++ b/handler/posts.go @@ -6,7 +6,7 @@ import ( "github.com/oullin/database/repository" "github.com/oullin/database/repository/pagination" "github.com/oullin/handler/paginate" - "github.com/oullin/handler/posts" + "github.com/oullin/handler/payload" "github.com/oullin/pkg" "github.com/oullin/pkg/http" "log/slog" @@ -26,7 +26,7 @@ func MakePostsHandler(posts *repository.Posts) PostsHandler { func (h *PostsHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { defer pkg.CloseWithLog(r.Body) - payload, err := http.ParseRequestBody[posts.IndexRequestBody](r) + requestBody, err := http.ParseRequestBody[payload.IndexRequestBody](r) if err != nil { slog.Error("failed to parse request body", "err", err) @@ -35,7 +35,7 @@ func (h *PostsHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *ht } result, err := h.Posts.GetAll( - posts.GetFiltersFrom(payload), + payload.GetPostsFiltersFrom(requestBody), paginate.MakeFrom(r.URL.Query()), ) @@ -47,7 +47,7 @@ func (h *PostsHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *ht items := pagination.HydratePagination( result, - posts.GetPostsResponse, + payload.GetPostsResponse, ) if err := json.NewEncoder(w).Encode(items); err != nil { @@ -60,7 +60,7 @@ func (h *PostsHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *ht } func (h *PostsHandler) Show(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - slug := posts.GetSlugFrom(r) + slug := payload.GetSlugFrom(r) if slug == "" { return http.BadRequestError("Slugs are required to show posts content") @@ -71,7 +71,7 @@ func (h *PostsHandler) Show(w baseHttp.ResponseWriter, r *baseHttp.Request) *htt return http.NotFound(fmt.Sprintf("The given post '%s' was not found", slug)) } - items := posts.GetPostsResponse(*post) + items := payload.GetPostsResponse(*post) if err := json.NewEncoder(w).Encode(items); err != nil { slog.Error(err.Error()) diff --git a/handler/posts/response.go b/handler/posts/response.go deleted file mode 100644 index 2bac8896..00000000 --- a/handler/posts/response.go +++ /dev/null @@ -1,56 +0,0 @@ -package posts - -import ( - "time" -) - -type IndexRequestBody struct { - Title string `json:"title"` - Author string `json:"author"` - Category string `json:"category"` - Tag string `json:"tag"` - Text string `json:"text"` -} - -type PostResponse struct { - UUID string `json:"uuid"` - Author UserResponse `json:"author"` - Slug string `json:"slug"` - Title string `json:"title"` - Excerpt string `json:"excerpt"` - Content string `json:"content"` - CoverImageURL string `json:"cover_image_url"` - PublishedAt *time.Time `json:"published_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - - // Associations - Categories []CategoryResponse `json:"categories"` - Tags []TagResponse `json:"tags"` -} - -type UserResponse struct { - UUID string `json:"uuid"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Username string `json:"username"` - DisplayName string `json:"display_name"` - Bio string `json:"bio"` - PictureFileName string `json:"picture_file_name"` - ProfilePictureURL string `json:"profile_picture_url"` - IsAdmin bool `json:"is_admin"` -} - -type CategoryResponse struct { - UUID string `json:"uuid"` - Name string `json:"name"` - Slug string `json:"slug"` - Description string `json:"description"` -} - -type TagResponse struct { - UUID string `json:"uuid"` - Name string `json:"name"` - Slug string `json:"slug"` - Description string `json:"description"` -} From 7dfc2f9c5c072f53989c75fcbb52f0819ee2acd6 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 28 Jul 2025 15:27:23 +0800 Subject: [PATCH 5/8] fix error handling --- database/repository/pagination/support.go | 4 ++-- handler/payload/posts.go | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/database/repository/pagination/support.go b/database/repository/pagination/support.go index 7c32fe1d..f8c99ba0 100644 --- a/database/repository/pagination/support.go +++ b/database/repository/pagination/support.go @@ -7,8 +7,8 @@ func Count[T *int64](numItems T, query *gorm.DB, session *gorm.Session, distinct Session(session). // clone the based query. Distinct(distinct) // remove duplicated; if any to get the actual count. - if sql.Count(numItems).Error != nil { - return sql.Error + if err := sql.Count(numItems).Error; err != nil { + return err } return nil diff --git a/handler/payload/posts.go b/handler/payload/posts.go index 566179ca..29381850 100644 --- a/handler/payload/posts.go +++ b/handler/payload/posts.go @@ -40,6 +40,7 @@ func GetPostsFiltersFrom(request IndexRequestBody) queries.PostFilters { Author: request.Author, Category: request.Category, Tag: request.Tag, + Text: request.Text, } } From cdd63bad62fc376f0cf6c0fc7bb95becf6a657cf Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 28 Jul 2025 16:32:29 +0800 Subject: [PATCH 6/8] better query --- database/repository/categories.go | 18 +++++++++----- database/repository/pagination/pagination.go | 3 ++- handler/categories.go | 2 +- handler/paginate/paginate.go | 26 ++++++++++++-------- handler/posts.go | 2 +- 5 files changed, 32 insertions(+), 19 deletions(-) diff --git a/database/repository/categories.go b/database/repository/categories.go index 782daf03..f9f11c27 100644 --- a/database/repository/categories.go +++ b/database/repository/categories.go @@ -19,20 +19,26 @@ func (c Categories) GetAll(paginate pagination.Paginate) (*pagination.Pagination query := c.DB.Sql(). Model(&database.Category{}). + Joins("JOIN post_categories ON post_categories.category_id = categories.id"). + Joins("JOIN posts ON posts.id = post_categories.post_id"). Where("categories.deleted_at is null"). - Limit(paginate.Limit). - Order("categories.name asc") + Where("posts.deleted_at is null"). + Where("posts.published_at is not null") + + group := "categories.id, categories.slug" - if err := pagination.Count[*int64](&numItems, query, c.DB.GetSession(), "categories.id"); err != nil { + if err := pagination.Count[*int64](&numItems, query, c.DB.GetSession(), group); err != nil { return nil, err } offset := (paginate.Page - 1) * paginate.Limit - err := query.Preload("Posts"). - Limit(paginate.Limit). + err := query. + Preload("Posts"). Offset(offset). - Distinct(). + Limit(paginate.Limit). + Order("categories.name asc"). + Group(group). Find(&categories).Error if err != nil { diff --git a/database/repository/pagination/pagination.go b/database/repository/pagination/pagination.go index 0e957808..c9600c7b 100644 --- a/database/repository/pagination/pagination.go +++ b/database/repository/pagination/pagination.go @@ -3,7 +3,8 @@ package pagination import "math" const MinPage = 1 -const MaxLimit = 100 +const PostsMaxLimit = 10 +const CategoriesMaxLimit = 5 // Pagination holds the data for a single page along with all pagination metadata. // It's generic and can be used for any data type. diff --git a/handler/categories.go b/handler/categories.go index f3e833e7..1ef2c451 100644 --- a/handler/categories.go +++ b/handler/categories.go @@ -24,7 +24,7 @@ func MakeCategoriesHandler(categories *repository.Categories) CategoriesHandler func (h *CategoriesHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { result, err := h.Categories.GetAll( - paginate.MakeFrom(r.URL.Query()), + paginate.MakeFrom(r.URL, 5), ) if err != nil { diff --git a/handler/paginate/paginate.go b/handler/paginate/paginate.go index 2365bc2b..5251b9a6 100644 --- a/handler/paginate/paginate.go +++ b/handler/paginate/paginate.go @@ -4,30 +4,36 @@ import ( "github.com/oullin/database/repository/pagination" "net/url" "strconv" + "strings" ) -func MakeFrom(url url.Values) pagination.Paginate { +func MakeFrom(url *url.URL, pageSize int) pagination.Paginate { page := pagination.MinPage - pageSize := pagination.MaxLimit + values := url.Query() + path := strings.TrimSpace((*url).Path) - if url.Get("page") != "" { - if tPage, err := strconv.Atoi(url.Get("page")); err == nil { + if values.Get("page") != "" { + if tPage, err := strconv.Atoi(values.Get("page")); err == nil { page = tPage } } - if url.Get("limit") != "" { - if limit, err := strconv.Atoi(url.Get("limit")); err == nil { + if values.Get("limit") != "" { + if limit, err := strconv.Atoi(values.Get("limit")); err == nil { pageSize = limit } } - if page < pagination.MinPage { - page = pagination.MinPage + if strings.Contains(path, "categories") && pageSize > pagination.CategoriesMaxLimit { + pageSize = pagination.CategoriesMaxLimit + } + + if strings.Contains(path, "posts") && pageSize > pagination.PostsMaxLimit { + pageSize = pagination.PostsMaxLimit } - if pageSize > pagination.MaxLimit || pageSize < 1 { - pageSize = pagination.MaxLimit + if page < pagination.MinPage { + page = pagination.MinPage } return pagination.Paginate{ diff --git a/handler/posts.go b/handler/posts.go index 04020dd3..efd75816 100644 --- a/handler/posts.go +++ b/handler/posts.go @@ -36,7 +36,7 @@ func (h *PostsHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *ht result, err := h.Posts.GetAll( payload.GetPostsFiltersFrom(requestBody), - paginate.MakeFrom(r.URL.Query()), + paginate.MakeFrom(r.URL, 10), ) if err != nil { From 5e5dae4a2d37bd46979e0e873b188f091b782afa Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 28 Jul 2025 17:20:49 +0800 Subject: [PATCH 7/8] method --- boost/router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boost/router.go b/boost/router.go index bc0b43c7..95c88a91 100644 --- a/boost/router.go +++ b/boost/router.go @@ -38,7 +38,7 @@ func (r *Router) Posts() { index := r.PipelineFor(abstract.Index) show := r.PipelineFor(abstract.Show) - r.Mux.HandleFunc("GET /posts", index) + r.Mux.HandleFunc("POST /posts", index) r.Mux.HandleFunc("GET /posts/{slug}", show) } From aaf3c228ddd99501da6c487fe8851e997eb952d9 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 28 Jul 2025 17:22:20 +0800 Subject: [PATCH 8/8] filter posts too --- database/repository/categories.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/repository/categories.go b/database/repository/categories.go index f9f11c27..17a2f92d 100644 --- a/database/repository/categories.go +++ b/database/repository/categories.go @@ -34,7 +34,7 @@ func (c Categories) GetAll(paginate pagination.Paginate) (*pagination.Pagination offset := (paginate.Page - 1) * paginate.Limit err := query. - Preload("Posts"). + Preload("Posts", "posts.deleted_at IS NULL AND posts.published_at IS NOT NULL"). Offset(offset). Limit(paginate.Limit). Order("categories.name asc").