feat: storage, language rules and migration
This commit is contained in:
1 parent
8c4a1825ab
commit
0806733d5c
70 files changed
+1771
-132
No files matched your search
@@ -11,11 +11,13 @@ import (
|
||||
const (
|
||||
articleSlugIndexName = "idx_articles_site_slug"
|
||||
articleSlugBackfillBatchSize = 500
|
||||
articleSlugIndexStatement = "CREATE UNIQUE INDEX idx_articles_site_slug ON articles (site_id, slug)"
|
||||
articleSlugIndexStatement = "CREATE UNIQUE INDEX idx_articles_site_slug ON articles (site_id, lang, slug)"
|
||||
articleSlugLanguageMarker = "article_slug_index_per_language"
|
||||
)
|
||||
|
||||
type slugOwner struct {
|
||||
siteID uint32
|
||||
lang string
|
||||
slug string
|
||||
}
|
||||
|
||||
@@ -26,7 +28,7 @@ type slugAssignment struct {
|
||||
|
||||
func BackfillArticleSlugs(db *gorm.DB) error {
|
||||
var rows []entity.Article
|
||||
if err := db.Unscoped().Select("id", "site_id", "title", "slug", "deleted_at").Order("id ASC").Find(&rows).Error; err != nil {
|
||||
if err := db.Unscoped().Select("id", "site_id", "lang", "title", "slug", "deleted_at").Order("id ASC").Find(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
assignments := planSlugAssignments(rows)
|
||||
@@ -60,7 +62,7 @@ func planSlugAssignments(rows []entity.Article) []slugAssignment {
|
||||
}
|
||||
continue
|
||||
}
|
||||
owner := slugOwner{row.SiteID, row.Slug}
|
||||
owner := slugOwner{row.SiteID, row.Lang, row.Slug}
|
||||
if row.Slug == "" || claimed[owner] {
|
||||
pending = append(pending, row)
|
||||
continue
|
||||
@@ -74,25 +76,41 @@ func planSlugAssignments(rows []entity.Article) []slugAssignment {
|
||||
base = snapshotSlug(row.Title)
|
||||
attempt = 1
|
||||
}
|
||||
slug := firstFreeSnapshotSlug(claimed, row.SiteID, base, attempt)
|
||||
claimed[slugOwner{row.SiteID, slug}] = true
|
||||
slug := firstFreeSnapshotSlug(claimed, row.SiteID, row.Lang, base, attempt)
|
||||
claimed[slugOwner{row.SiteID, row.Lang, slug}] = true
|
||||
assignments = append(assignments, slugAssignment{row.ID, slug})
|
||||
}
|
||||
return assignments
|
||||
}
|
||||
|
||||
func firstFreeSnapshotSlug(claimed map[slugOwner]bool, siteID uint32, base string, attempt int) string {
|
||||
func firstFreeSnapshotSlug(claimed map[slugOwner]bool, siteID uint32, lang string, base string, attempt int) string {
|
||||
for ; ; attempt++ {
|
||||
candidate := snapshotSlugWithSuffix(base, attempt)
|
||||
if !claimed[slugOwner{siteID, candidate}] {
|
||||
if !claimed[slugOwner{siteID, lang, candidate}] {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ensureArticleSlugIndex(db *gorm.DB) error {
|
||||
if db.Migrator().HasIndex(&entity.Article{}, articleSlugIndexName) {
|
||||
replaced, err := markerApplied(db, articleSlugLanguageMarker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
indexed := db.Migrator().HasIndex(&entity.Article{}, articleSlugIndexName)
|
||||
if replaced && indexed {
|
||||
return nil
|
||||
}
|
||||
return db.Exec(articleSlugIndexStatement).Error
|
||||
if indexed {
|
||||
if err := db.Migrator().DropIndex(&entity.Article{}, articleSlugIndexName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := db.Exec(articleSlugIndexStatement).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if replaced {
|
||||
return nil
|
||||
}
|
||||
return applyMarker(db, articleSlugLanguageMarker)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
articleStoriesMarker = "article_stories"
|
||||
articleStoryLanguageIndex = "idx_articles_story_lang"
|
||||
articleStoryLanguageIndexSQL = "CREATE UNIQUE INDEX idx_articles_story_lang ON articles (story_id, lang)"
|
||||
)
|
||||
|
||||
func BackfillArticleStories(db *gorm.DB) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
applied, err := markerApplied(tx, articleStoriesMarker)
|
||||
if err != nil || applied {
|
||||
return err
|
||||
}
|
||||
var ids []uint32
|
||||
if err := tx.Unscoped().Model(&entity.Article{}).Where("story_id = ?", 0).Order("id ASC").Pluck("id", &ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range ids {
|
||||
story := entity.Story{}
|
||||
if err := tx.Create(&story).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Unscoped().Model(&entity.Article{}).Where("id = ?", id).UpdateColumn("story_id", story.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return applyMarker(tx, articleStoriesMarker)
|
||||
})
|
||||
}
|
||||
|
||||
func ensureArticleStoryLanguageIndex(db *gorm.DB) error {
|
||||
if db.Migrator().HasIndex(&entity.Article{}, articleStoryLanguageIndex) {
|
||||
return nil
|
||||
}
|
||||
return db.Exec(articleStoryLanguageIndexSQL).Error
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func Migrate(db *gorm.DB, publicURL string) error {
|
||||
err := tableOptionsFor(db).AutoMigrate(&models.Article{}, &models.Image{}, &models.User{}, &models.Invite{}, &models.APIKey{}, &models.PurgeSettings{}, &models.AuditEvent{}, &models.AuditSettings{}, &models.ImageVariant{}, &models.ImageVariantFile{}, &models.ImageVariantRejectionDay{}, &models.Site{}, &models.Author{}, &models.ArticleAuthor{}, &models.ContentType{}, &models.Entry{}, &models.RevokedSession{}, &models.Role{}, &models.RolePermission{}, &models.MigrationMarker{})
|
||||
err := tableOptionsFor(db).AutoMigrate(&models.Article{}, &models.Image{}, &models.User{}, &models.Invite{}, &models.APIKey{}, &models.PurgeSettings{}, &models.AuditEvent{}, &models.AuditSettings{}, &models.ImageVariant{}, &models.ImageVariantFile{}, &models.ImageVariantRejectionDay{}, &models.Site{}, &models.Author{}, &models.ArticleAuthor{}, &models.ContentType{}, &models.Entry{}, &models.RevokedSession{}, &models.Role{}, &models.RolePermission{}, &models.MigrationMarker{}, &models.Story{}, &models.SiteLanguage{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -35,6 +35,15 @@ func Migrate(db *gorm.DB, publicURL string) error {
|
||||
if err := BackfillArticleSlugs(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := BackfillArticleStories(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := SeedSiteLanguageSuggestions(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureArticleStoryLanguageIndex(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return ensureArticleSlugIndex(db)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/language"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const siteLanguageSuggestionsMarker = "site_language_suggestions"
|
||||
|
||||
func SeedSiteLanguageSuggestions(db *gorm.DB) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
applied, err := markerApplied(tx, siteLanguageSuggestionsMarker)
|
||||
if err != nil || applied {
|
||||
return err
|
||||
}
|
||||
var sites []entity.Site
|
||||
if err := tx.Order("id ASC").Find(&sites).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, site := range sites {
|
||||
suggestion := language.Suggest(site.Locale)
|
||||
if !suggestion.Found {
|
||||
continue
|
||||
}
|
||||
row := entity.SiteLanguage{SiteID: site.ID, Lang: suggestion.Tag, IsDefault: true, Enabled: true, Confirmed: false}
|
||||
if err := tx.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return applyMarker(tx, siteLanguageSuggestionsMarker)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user