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
1 change: 1 addition & 0 deletions boost/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,5 @@ func (a *App) Boot() {
router.Education()
router.Recommendations()
router.Posts()
router.Categories()
}
11 changes: 10 additions & 1 deletion boost/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,19 @@ 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)
}

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")

Expand Down
39 changes: 39 additions & 0 deletions database/repository/categories.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -12,6 +13,44 @@ 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{}).
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").
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(), group); err != nil {
return nil, err
}

offset := (paginate.Page - 1) * paginate.Limit

err := query.
Preload("Posts", "posts.deleted_at IS NULL AND posts.published_at IS NOT NULL").
Offset(offset).
Limit(paginate.Limit).
Order("categories.name asc").
Group(group).
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{}

Expand Down
3 changes: 2 additions & 1 deletion database/repository/pagination/pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions database/repository/pagination/support.go
Original file line number Diff line number Diff line change
@@ -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 err := sql.Count(numItems).Error; err != nil {
return err
}

return nil
}
8 changes: 2 additions & 6 deletions database/repository/posts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -26,11 +26,7 @@ func (p Posts) GetPosts(filters queries.PostFilters, paginate pagination.Paginat

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
}

Expand Down
54 changes: 54 additions & 0 deletions handler/categories.go
Original file line number Diff line number Diff line change
@@ -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/payload"
"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, 5),
)

if err != nil {
slog.Error("Error getting categories", "err", err)
return http.InternalError("Error getting categories")
}

items := pagination.HydratePagination(
result,
func(s database.Category) payload.CategoryResponse {
return payload.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
}
43 changes: 43 additions & 0 deletions handler/paginate/paginate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package paginate

import (
"github.com/oullin/database/repository/pagination"
"net/url"
"strconv"
"strings"
)

func MakeFrom(url *url.URL, pageSize int) pagination.Paginate {
page := pagination.MinPage
values := url.Query()
path := strings.TrimSpace((*url).Path)

if values.Get("page") != "" {
if tPage, err := strconv.Atoi(values.Get("page")); err == nil {
page = tPage
}
}

if values.Get("limit") != "" {
if limit, err := strconv.Atoi(values.Get("limit")); err == nil {
pageSize = limit
}
}

if strings.Contains(path, "categories") && pageSize > pagination.CategoriesMaxLimit {
pageSize = pagination.CategoriesMaxLimit
}

if strings.Contains(path, "posts") && pageSize > pagination.PostsMaxLimit {
pageSize = pagination.PostsMaxLimit
}

if page < pagination.MinPage {
page = pagination.MinPage
}

return pagination.Paginate{
Page: page,
Limit: pageSize,
}
}
25 changes: 25 additions & 0 deletions handler/payload/categories.go
Original file line number Diff line number Diff line change
@@ -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
}
78 changes: 78 additions & 0 deletions handler/payload/posts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package payload

import (
"github.com/oullin/database"
"github.com/oullin/database/repository/queries"
"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,
Text: request.Text,
}
}

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,
Slug: p.Slug,
Title: p.Title,
Excerpt: p.Excerpt,
Content: p.Content,
CoverImageURL: p.CoverImageURL,
PublishedAt: p.PublishedAt,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
Categories: GetCategoriesResponse(p.Categories),
Tags: GetTagsResponse(p.Tags),
Author: UserResponse{
UUID: p.Author.UUID,
FirstName: p.Author.FirstName,
LastName: p.Author.LastName,
Username: p.Author.Username,
DisplayName: p.Author.DisplayName,
Bio: p.Author.Bio,
PictureFileName: p.Author.PictureFileName,
ProfilePictureURL: p.Author.ProfilePictureURL,
IsAdmin: p.Author.IsAdmin,
},
}
}
25 changes: 25 additions & 0 deletions handler/payload/tags.go
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 13 additions & 0 deletions handler/payload/users.go
Original file line number Diff line number Diff line change
@@ -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"`
}
Loading
Loading