feat: articles pagination
This commit is contained in:
1 parent
a2d224240c
commit
73f4b539fe
45 files changed
+3528
-317
No files matched your search
@@ -5,6 +5,12 @@ import (
|
||||
"RenewCMS/internal/infrastructure/persistence/mappers"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -26,12 +32,128 @@ func (a *ArticleRepository) FindByFilters(filters domain.Filters) ([]domain.Arti
|
||||
var entityArticles []entity.Article
|
||||
err := query.Find(&entityArticles).Error
|
||||
|
||||
return toDomainArticles(entityArticles), err
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) scoped(filters domain.Filters) *gorm.DB {
|
||||
query := a.db.Model(&entity.Article{})
|
||||
if filters.IsOnline != nil {
|
||||
query = query.Where("is_online = ?", *filters.IsOnline)
|
||||
}
|
||||
switch filters.Status {
|
||||
case domain.StatusOnline:
|
||||
query = query.Where("is_online = ?", true)
|
||||
case domain.StatusDraft:
|
||||
query = query.Where("is_online = ?", false)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
var sortColumns = map[domain.SortField]string{
|
||||
domain.SortByTitle: "LOWER(title)",
|
||||
domain.SortByStatus: "is_online",
|
||||
domain.SortByCreatedAt: "created_at",
|
||||
domain.SortByUpdatedAt: "updated_at",
|
||||
}
|
||||
|
||||
var sortDirections = map[domain.SortDirection]string{
|
||||
domain.SortAsc: "ASC",
|
||||
domain.SortDesc: "DESC",
|
||||
}
|
||||
|
||||
func orderClause(sort domain.Sort) string {
|
||||
column, ok := sortColumns[sort.Field]
|
||||
direction, directionOk := sortDirections[sort.Direction]
|
||||
if !ok || !directionOk {
|
||||
column, direction = sortColumns[domain.SortByUpdatedAt], sortDirections[domain.SortDesc]
|
||||
}
|
||||
return column + " " + direction + ", id DESC"
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) FindPage(filters domain.Filters) (domain.PageResult, error) {
|
||||
filters.Page = max(filters.Page, 1)
|
||||
filters.Size = max(filters.Size, 0)
|
||||
filters.Q = strings.TrimSpace(filters.Q)
|
||||
if filters.Q != "" {
|
||||
return a.findMatchingTitle(filters)
|
||||
}
|
||||
var total int64
|
||||
if err := a.scoped(filters).Count(&total).Error; err != nil {
|
||||
return domain.PageResult{}, err
|
||||
}
|
||||
var entityArticles []entity.Article
|
||||
err := a.scoped(filters).Order(orderClause(filters.Sort)).Limit(filters.Size).Offset((filters.Page - 1) * filters.Size).Find(&entityArticles).Error
|
||||
if err != nil {
|
||||
return domain.PageResult{}, err
|
||||
}
|
||||
return domain.PageResult{Items: toDomainArticles(entityArticles), Total: int(total)}, nil
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) findMatchingTitle(filters domain.Filters) (domain.PageResult, error) {
|
||||
var candidates []entity.Article
|
||||
if err := a.scoped(filters).Select("id", "title").Order(orderClause(filters.Sort)).Find(&candidates).Error; err != nil {
|
||||
return domain.PageResult{}, err
|
||||
}
|
||||
needle := foldForSearch(filters.Q)
|
||||
matchingIds := make([]uint32, 0)
|
||||
for _, candidate := range candidates {
|
||||
if strings.Contains(foldForSearch(candidate.Title), needle) {
|
||||
matchingIds = append(matchingIds, candidate.ID)
|
||||
}
|
||||
}
|
||||
start := min((filters.Page-1)*filters.Size, len(matchingIds))
|
||||
end := min(start+filters.Size, len(matchingIds))
|
||||
pageIds := matchingIds[start:end]
|
||||
page := domain.PageResult{Items: []domain.Article{}, Total: len(matchingIds)}
|
||||
if len(pageIds) == 0 {
|
||||
return page, nil
|
||||
}
|
||||
var entityArticles []entity.Article
|
||||
if err := a.db.Model(&entity.Article{}).Where("id IN ?", pageIds).Find(&entityArticles).Error; err != nil {
|
||||
return domain.PageResult{}, err
|
||||
}
|
||||
byId := make(map[uint32]entity.Article, len(entityArticles))
|
||||
for _, entityArticle := range entityArticles {
|
||||
byId[entityArticle.ID] = entityArticle
|
||||
}
|
||||
ordered := make([]entity.Article, 0, len(pageIds))
|
||||
for _, id := range pageIds {
|
||||
if entityArticle, found := byId[id]; found {
|
||||
ordered = append(ordered, entityArticle)
|
||||
}
|
||||
}
|
||||
page.Items = toDomainArticles(ordered)
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) CountByVisibility(onlineOnly bool) (domain.Counts, error) {
|
||||
var online, draft int64
|
||||
if err := a.db.Model(&entity.Article{}).Where("is_online = ?", true).Count(&online).Error; err != nil {
|
||||
return domain.Counts{}, err
|
||||
}
|
||||
if !onlineOnly {
|
||||
if err := a.db.Model(&entity.Article{}).Where("is_online = ?", false).Count(&draft).Error; err != nil {
|
||||
return domain.Counts{}, err
|
||||
}
|
||||
}
|
||||
return domain.Counts{All: int(online + draft), Online: int(online), Draft: int(draft)}, nil
|
||||
}
|
||||
|
||||
func toDomainArticles(entityArticles []entity.Article) []domain.Article {
|
||||
domainArticles := make([]domain.Article, 0, len(entityArticles))
|
||||
for _, entityArticle := range entityArticles {
|
||||
domainArticles = append(domainArticles, mappers.ArticleToDomain(entityArticle))
|
||||
}
|
||||
return domainArticles
|
||||
}
|
||||
|
||||
return domainArticles, err
|
||||
func foldForSearch(value string) string {
|
||||
folder := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
||||
folded, _, err := transform.String(folder, value)
|
||||
if err != nil {
|
||||
return strings.ToLower(value)
|
||||
}
|
||||
return strings.ToLower(folded)
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) Get(id uint32) (domain.Article, error) {
|
||||
@@ -131,11 +253,7 @@ func (a *ArticleRepository) FindByBodyContaining(filename string) ([]domain.Arti
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domainArticles := make([]domain.Article, 0, len(entityArticles))
|
||||
for _, entityArticle := range entityArticles {
|
||||
domainArticles = append(domainArticles, mappers.ArticleToDomain(entityArticle))
|
||||
}
|
||||
return domainArticles, nil
|
||||
return toDomainArticles(entityArticles), nil
|
||||
}
|
||||
|
||||
var _ domain.Repository = &ArticleRepository{}
|
||||
@@ -11,21 +11,51 @@ func NewListArticlesUseCase(articleRepository article.Repository, bodyRenderer a
|
||||
return &ListArticlesUseCase{articleRepository, bodyRenderer}
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) ListOnlineArticles(format article.BodyFormat) ([]article.Article, error) {
|
||||
online := true
|
||||
articles, err := u.articleRepository.FindByFilters(article.Filters{
|
||||
IsOnline: &online,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return renderArticles(u.bodyRenderer, articles, format)
|
||||
type ListArticlesCommand struct {
|
||||
Format article.BodyFormat
|
||||
Verified bool
|
||||
Page int
|
||||
Size int
|
||||
Q string
|
||||
Status string
|
||||
Sort article.Sort
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) ListAllArticles(format article.BodyFormat) ([]article.Article, error) {
|
||||
articles, err := u.articleRepository.FindByFilters(article.Filters{})
|
||||
type ArticlePage struct {
|
||||
Items []article.Article
|
||||
Total int
|
||||
Page int
|
||||
Size int
|
||||
Counts article.Counts
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) ListArticlesPage(command ListArticlesCommand) (ArticlePage, error) {
|
||||
counts, err := u.articleRepository.CountByVisibility(!command.Verified)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
return renderArticles(u.bodyRenderer, articles, format)
|
||||
page := ArticlePage{Items: []article.Article{}, Page: command.Page, Size: command.Size, Counts: counts}
|
||||
if !command.Verified && command.Status == article.StatusDraft {
|
||||
return page, nil
|
||||
}
|
||||
found, err := u.articleRepository.FindPage(pageFilters(command))
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
rendered, err := renderArticles(u.bodyRenderer, found.Items, command.Format)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
page.Items = rendered
|
||||
page.Total = found.Total
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func pageFilters(command ListArticlesCommand) article.Filters {
|
||||
filters := article.Filters{Page: command.Page, Size: command.Size, Q: command.Q, Status: command.Status, Sort: command.Sort}
|
||||
if !command.Verified {
|
||||
online := true
|
||||
filters.IsOnline = &online
|
||||
}
|
||||
return filters
|
||||
}
|
||||
Reference in new issue
Block a user