refac: article reads, story delete, no leaks, stored SEO status, rate limit, cleanup (%site%, article repo split, old lang repos, one default per site rule to domain)
This commit is contained in:
1 parent
9d284e65a9
commit
7c15206170
85 files changed
+1788
-593
No files matched your search
@@ -140,6 +140,8 @@ type Article struct {
|
||||
EditRevision uint32 `json:"-"`
|
||||
SourceArticleID *uint32 `json:"-"`
|
||||
SourceRevision uint32 `json:"-"`
|
||||
SEO SEOSummary `json:"-"`
|
||||
SEOVersion uint32 `json:"-"`
|
||||
}
|
||||
|
||||
func (a Article) UsesImage(imageID uint32) bool {
|
||||
|
||||
@@ -78,12 +78,14 @@ func (e *SlugConflictError) Is(target error) bool {
|
||||
}
|
||||
|
||||
type Translation struct {
|
||||
ID uint32 `json:"id"`
|
||||
Lang string `json:"lang"`
|
||||
Title string `json:"title"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
LanguageEnabled bool `json:"language_enabled"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
ID uint32 `json:"id"`
|
||||
Lang string `json:"lang"`
|
||||
Title string `json:"title"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
LanguageEnabled bool `json:"language_enabled"`
|
||||
ExcludedFromPublic bool `json:"excluded_from_public"`
|
||||
SourceArticleID *uint32 `json:"source_article_id"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package article
|
||||
|
||||
import "math"
|
||||
|
||||
const (
|
||||
StatusOnline = "online"
|
||||
StatusDraft = "draft"
|
||||
@@ -37,13 +39,23 @@ type Filters struct {
|
||||
Size int
|
||||
Q string
|
||||
Status string
|
||||
Lang *string
|
||||
SEOStatus string
|
||||
Lang string
|
||||
Langs *[]string
|
||||
AuthorID *uint32
|
||||
Sort Sort
|
||||
Unpaged bool
|
||||
}
|
||||
|
||||
func (f Filters) Window() (int, int) {
|
||||
size := max(f.Size, 0)
|
||||
skipped := max(f.Page, 1) - 1
|
||||
if size > 0 && skipped > math.MaxInt/size {
|
||||
return math.MaxInt, size
|
||||
}
|
||||
return skipped * size, size
|
||||
}
|
||||
|
||||
type Nullable[T any] struct {
|
||||
Set bool
|
||||
Value *T
|
||||
@@ -97,10 +109,7 @@ type Repository interface {
|
||||
CountByVisibility(onlineOnly bool) (Counts, error)
|
||||
CountVisibleTo(userID uint32) (Counts, error)
|
||||
CountVersions(filters Filters) (Counts, error)
|
||||
FindStoryPage(filters StoryFilters) (StoryPage, error)
|
||||
CountStories(filters StoryFilters) (StoryCounts, error)
|
||||
ListByStories(storyIDs []uint32, readable StoryFilters) ([]Article, error)
|
||||
FindTitleMatches(q string, readable StoryFilters) ([]uint32, error)
|
||||
CountSuggestions(filters Filters) (int, error)
|
||||
Get(id uint32) (Article, error)
|
||||
GetByName(name string) (Article, error)
|
||||
GetByLangSlug(siteID uint32, lang string, slug string) (Article, error)
|
||||
@@ -120,4 +129,26 @@ type Repository interface {
|
||||
AssignUnassignedLanguage(siteID uint32, lang string) (int, error)
|
||||
ListByStory(storyID uint32) ([]Article, error)
|
||||
MoveToSite(ids []uint32, siteID uint32) error
|
||||
DeleteStory(storyID uint32) error
|
||||
}
|
||||
|
||||
type StoryReader interface {
|
||||
FindStoryPage(filters StoryFilters) (StoryPage, error)
|
||||
CountStories(filters StoryFilters) (StoryCounts, error)
|
||||
CountSuggestedStories(readable StoryFilters) (int, error)
|
||||
ListByStories(storyIDs []uint32, readable StoryFilters) ([]Article, error)
|
||||
ListVersions(ids []uint32) ([]Article, error)
|
||||
FindTitleMatches(q string, readable StoryFilters) ([]uint32, error)
|
||||
}
|
||||
|
||||
type SEOIndex interface {
|
||||
StaleSEO(afterID uint32, limit int) ([]Article, error)
|
||||
StoreSEO(records []SEORecord) error
|
||||
MarkSiteSEOStale(siteID uint32) error
|
||||
}
|
||||
|
||||
type ListingRepository interface {
|
||||
Repository
|
||||
StoryReader
|
||||
SEOIndex
|
||||
}
|
||||
@@ -13,6 +13,7 @@ type StoryFilters struct {
|
||||
Lang string
|
||||
Missing bool
|
||||
Status string
|
||||
SEOStatus string
|
||||
AuthorID *uint32
|
||||
VersionIDs *[]uint32
|
||||
Sort Sort
|
||||
|
||||
@@ -37,6 +37,16 @@ type SEOSummary struct {
|
||||
Suggestions int
|
||||
}
|
||||
|
||||
func (s SEOSummary) Stale() bool {
|
||||
return s.Status == ""
|
||||
}
|
||||
|
||||
type SEORecord struct {
|
||||
ArticleID uint32
|
||||
Version uint32
|
||||
Summary SEOSummary
|
||||
}
|
||||
|
||||
func (a SEOAssessment) Summary() SEOSummary {
|
||||
return SEOSummary{Status: a.Status, Suggestions: len(a.Suggestions)}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
var (
|
||||
ErrInvalidLanguage = errors.New("invalid language tag")
|
||||
ErrUnsupportedLanguage = errors.New("unsupported language")
|
||||
ErrNoDefaultLanguage = errors.New("the site has no default language")
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -64,27 +63,3 @@ func Suggest(locale *string) Suggestion {
|
||||
}
|
||||
return Suggestion{}
|
||||
}
|
||||
|
||||
type SiteLanguage struct {
|
||||
SiteID uint32
|
||||
Tag string
|
||||
IsDefault bool
|
||||
Enabled bool
|
||||
Confirmed bool
|
||||
}
|
||||
|
||||
func DefaultOf(languages []SiteLanguage) (SiteLanguage, bool) {
|
||||
index := slices.IndexFunc(languages, func(candidate SiteLanguage) bool { return candidate.IsDefault })
|
||||
if index < 0 {
|
||||
return SiteLanguage{}, false
|
||||
}
|
||||
return languages[index], true
|
||||
}
|
||||
|
||||
func StartingTag(languages []SiteLanguage) string {
|
||||
defaultLanguage, found := DefaultOf(languages)
|
||||
if !found || !defaultLanguage.Confirmed {
|
||||
return Unassigned
|
||||
}
|
||||
return defaultLanguage.Tag
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package language
|
||||
|
||||
type Repository interface {
|
||||
ListBySite(siteID uint32) ([]SiteLanguage, error)
|
||||
LockSiteLanguages(siteID uint32) ([]SiteLanguage, error)
|
||||
MarkConfirmed(siteID uint32, tag string) error
|
||||
}
|
||||
@@ -13,6 +13,7 @@ var (
|
||||
ErrSetupInProgress = errors.New("another language setup is running")
|
||||
ErrSetupIncomplete = errors.New("every pending site needs a language")
|
||||
ErrDuplicateSite = errors.New("the same site is listed twice")
|
||||
ErrSingleDefault = errors.New("a site has exactly one default language")
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -128,6 +128,34 @@ func MissingTags(rows []Row, tags []string) []string {
|
||||
return missing
|
||||
}
|
||||
|
||||
func ConfirmedDefaultTag(rows []Row) string {
|
||||
if NeedsSetup(rows) {
|
||||
return language.Unassigned
|
||||
}
|
||||
return DefaultTag(rows)
|
||||
}
|
||||
|
||||
func SiteTextAllowed(defaultTag string, tag string) bool {
|
||||
return tag == language.Unassigned || tag == defaultTag
|
||||
}
|
||||
|
||||
func PublicTag(rows []Row, tag string) bool {
|
||||
return tag != language.Unassigned && EnabledTag(rows, tag)
|
||||
}
|
||||
|
||||
func RequireSingleDefault(rows []Row) error {
|
||||
defaults := 0
|
||||
for _, row := range rows {
|
||||
if row.IsDefault {
|
||||
defaults++
|
||||
}
|
||||
}
|
||||
if defaults != 1 {
|
||||
return ErrSingleDefault
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DefaultTag(rows []Row) string {
|
||||
for _, row := range rows {
|
||||
if row.IsDefault {
|
||||
|
||||
@@ -46,7 +46,8 @@ const (
|
||||
MaxKeyRunes = 32
|
||||
|
||||
TitlePlaceholder = "%title%"
|
||||
DefaultTitleTemplate = "%title% | %site%"
|
||||
SitePlaceholder = "%site%"
|
||||
DefaultTitleTemplate = TitlePlaceholder + " | " + SitePlaceholder
|
||||
)
|
||||
|
||||
var keyPattern = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"RenewCMS/internal/domain/author"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/language"
|
||||
"RenewCMS/internal/domain/localization"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"context"
|
||||
@@ -20,8 +19,8 @@ type Repositories struct {
|
||||
Authors author.Repository
|
||||
Entries entry.Repository
|
||||
Images image.Repository
|
||||
Languages language.Repository
|
||||
Localization localization.Repository
|
||||
ArticleSEO article.SEOIndex
|
||||
}
|
||||
|
||||
type Transactor interface {
|
||||
|
||||
Reference in new issue
Block a user