fix: technical debts left by the Sites, Authors and SEO slices, plus backend support for the new Sites and Authors pagination
This commit is contained in:
1 parent
9f4ce426a8
commit
daf3594316
86 files changed
+4223
-1485
No files matched your search
@@ -3,13 +3,15 @@ package database
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
"slices"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
articleSlugIndexName = "idx_articles_site_slug"
|
||||
articleSlugIndexStatement = "CREATE UNIQUE INDEX " + articleSlugIndexName + " ON articles (site_id, slug)"
|
||||
articleSlugIndexName = "idx_articles_site_slug"
|
||||
articleSlugBackfillBatchSize = 500
|
||||
articleSlugIndexStatement = "CREATE UNIQUE INDEX idx_articles_site_slug ON articles (site_id, slug)"
|
||||
)
|
||||
|
||||
type slugOwner struct {
|
||||
@@ -23,13 +25,22 @@ type slugAssignment struct {
|
||||
}
|
||||
|
||||
func BackfillArticleSlugs(db *gorm.DB) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
var rows []entity.Article
|
||||
if err := tx.Unscoped().Select("id", "site_id", "title", "slug", "deleted_at").Order("id ASC").Find(&rows).Error; err != nil {
|
||||
var rows []entity.Article
|
||||
if err := db.Unscoped().Select("id", "site_id", "title", "slug", "deleted_at").Order("id ASC").Find(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
assignments := planSlugAssignments(rows)
|
||||
for batch := range slices.Chunk(assignments, articleSlugBackfillBatchSize) {
|
||||
if err := writeSlugAssignments(db, batch); err != nil {
|
||||
return err
|
||||
}
|
||||
assignments := planSlugAssignments(rows)
|
||||
for _, assignment := range assignments {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeSlugAssignments(db *gorm.DB, batch []slugAssignment) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, assignment := range batch {
|
||||
if err := tx.Unscoped().Model(&entity.Article{}).Where("id = ?", assignment.id).UpdateColumn("slug", assignment.slug).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
domain "RenewCMS/internal/domain/site"
|
||||
"RenewCMS/internal/infrastructure/persistence/mappers"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -59,6 +60,40 @@ func (s *SiteRepository) List() ([]domain.Site, error) {
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
func (s *SiteRepository) Find(query domain.ListQuery) (domain.Page, error) {
|
||||
all, err := s.List()
|
||||
if err != nil {
|
||||
return domain.Page{}, err
|
||||
}
|
||||
matching := all
|
||||
if needle := foldForSearch(strings.TrimSpace(query.Q)); needle != "" {
|
||||
matching = make([]domain.Site, 0, len(all))
|
||||
for _, candidate := range all {
|
||||
if siteMatches(candidate, needle) {
|
||||
matching = append(matching, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
page := domain.Page{Items: []domain.Site{}, Total: len(matching)}
|
||||
if query.Size <= 0 {
|
||||
page.Items = matching
|
||||
return page, nil
|
||||
}
|
||||
start := pageStart(max(query.Page, 1), query.Size, len(matching))
|
||||
end := min(start+query.Size, len(matching))
|
||||
page.Items = matching[start:end]
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func siteMatches(candidate domain.Site, needle string) bool {
|
||||
for _, field := range []string{candidate.Name, candidate.Key, candidate.BaseURL} {
|
||||
if strings.Contains(foldForSearch(field), needle) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SiteRepository) load(db *gorm.DB, model entity.Site) (domain.Site, error) {
|
||||
total, err := s.articleCount(db, model.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -42,6 +42,11 @@ func (u *ListArticlesUseCase) ListArticlesPage(command ListArticlesCommand) (Art
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
summaries, err := u.summarizeVisible(!command.Verified)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
counts.Suggestions = countSuggestions(summaries)
|
||||
page := ArticlePage{Items: []article.Article{}, Page: command.Page, Size: command.Size, Counts: counts, SEO: map[uint32]article.SEOSummary{}}
|
||||
if !command.Verified && command.Status == article.StatusDraft {
|
||||
return page, nil
|
||||
@@ -62,10 +67,6 @@ func (u *ListArticlesUseCase) ListArticlesPage(command ListArticlesCommand) (Art
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
summaries, err := u.summarize(found.Items)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
items, total := found.Items, found.Total
|
||||
if command.SEO != "" {
|
||||
items, total = pageBySEOStatus(found.Items, summaries, command)
|
||||
@@ -80,6 +81,29 @@ func (u *ListArticlesUseCase) ListArticlesPage(command ListArticlesCommand) (Art
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) summarizeVisible(onlineOnly bool) (map[uint32]article.SEOSummary, error) {
|
||||
filters := article.Filters{Unpaged: true}
|
||||
if onlineOnly {
|
||||
online := true
|
||||
filters.IsOnline = &online
|
||||
}
|
||||
found, err := u.articleRepository.FindPage(filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u.summarize(found.Items)
|
||||
}
|
||||
|
||||
func countSuggestions(summaries map[uint32]article.SEOSummary) int {
|
||||
suggestions := 0
|
||||
for _, summary := range summaries {
|
||||
if summary.Status == article.SEOStatusSuggestions {
|
||||
suggestions++
|
||||
}
|
||||
}
|
||||
return suggestions
|
||||
}
|
||||
|
||||
func pageBySEOStatus(items []article.Article, summaries map[uint32]article.SEOSummary, command ListArticlesCommand) ([]article.Article, int) {
|
||||
matching := make([]article.Article, 0, len(items))
|
||||
for _, item := range items {
|
||||
|
||||
@@ -82,8 +82,7 @@ func (l *ListMediaUseCase) List(query image.ListQuery) (MediaPage, error) {
|
||||
}
|
||||
}
|
||||
|
||||
offset := (query.Page - 1) * query.Size
|
||||
pageStart := min(offset, len(matching))
|
||||
pageStart := min(min(query.Page-1, len(matching))*query.Size, len(matching))
|
||||
pageEnd := min(pageStart+query.Size, len(matching))
|
||||
names := newUploaderNames(l.userRepository)
|
||||
items := []MediaItem{}
|
||||
@@ -106,6 +105,14 @@ func (l *ListMediaUseCase) List(query image.ListQuery) (MediaPage, error) {
|
||||
return MediaPage{Items: items, Total: len(matching), Page: query.Page, Size: query.Size, Counts: counts}, nil
|
||||
}
|
||||
|
||||
func (l *ListMediaUseCase) Get(id uint32) (MediaItem, error) {
|
||||
found, err := l.imageRepository.Get(id)
|
||||
if err != nil {
|
||||
return MediaItem{}, err
|
||||
}
|
||||
return l.Describe(found)
|
||||
}
|
||||
|
||||
func (l *ListMediaUseCase) Describe(img image.Image) (MediaItem, error) {
|
||||
articleCount, err := l.articleUsageCount(img)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,17 @@ type ListSitesUseCase struct {
|
||||
siteRepository site.Repository
|
||||
}
|
||||
|
||||
type ListSitesCommand struct {
|
||||
Q string
|
||||
Page int
|
||||
Size int
|
||||
}
|
||||
|
||||
type SitePage struct {
|
||||
Items []site.Site
|
||||
Total int
|
||||
}
|
||||
|
||||
func NewListSitesUseCase(siteRepository site.Repository) *ListSitesUseCase {
|
||||
return &ListSitesUseCase{siteRepository}
|
||||
}
|
||||
@@ -13,3 +24,11 @@ func NewListSitesUseCase(siteRepository site.Repository) *ListSitesUseCase {
|
||||
func (u *ListSitesUseCase) List() ([]site.Site, error) {
|
||||
return u.siteRepository.List()
|
||||
}
|
||||
|
||||
func (u *ListSitesUseCase) ListPage(command ListSitesCommand) (SitePage, error) {
|
||||
found, err := u.siteRepository.Find(site.ListQuery{Q: command.Q, Page: command.Page, Size: command.Size})
|
||||
if err != nil {
|
||||
return SitePage{}, err
|
||||
}
|
||||
return SitePage{Items: found.Items, Total: found.Total}, nil
|
||||
}
|
||||
Reference in new issue
Block a user