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
@@ -216,7 +216,12 @@ func (a *ArticleRepository) withAuthorsOne(db *gorm.DB, article domain.Article)
|
||||
return loaded[0], nil
|
||||
}
|
||||
|
||||
const idChunkSize = 500
|
||||
var preservedArticleColumns = []string{"lang", "story_id", "content_revision"}
|
||||
|
||||
const (
|
||||
idChunkSize = 500
|
||||
initialContentRevision = 1
|
||||
)
|
||||
|
||||
func chunkIDs(ids []uint32) [][]uint32 {
|
||||
chunks := make([][]uint32, 0, len(ids)/idChunkSize+1)
|
||||
@@ -316,13 +321,17 @@ func (a *ArticleRepository) GetByName(name string) (domain.Article, error) {
|
||||
|
||||
func (a *ArticleRepository) Create(article domain.Article) (domain.Article, error) {
|
||||
created := mappers.ArticleToModel(article)
|
||||
created.ContentRevision = initialContentRevision
|
||||
err := a.db.Transaction(func(tx *gorm.DB) error {
|
||||
if article.SiteID != 0 {
|
||||
if _, err := lockSite(tx, article.SiteID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := ensureSlugFree(tx, article.SiteID, article.Slug, 0); err != nil {
|
||||
if err := ensureSlugFree(tx, created.SiteID, created.Lang, created.Slug, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := openStory(tx, &created); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&created).Error; err != nil {
|
||||
@@ -331,22 +340,34 @@ func (a *ArticleRepository) Create(article domain.Article) (domain.Article, erro
|
||||
return replaceCredits(tx, created.ID, authordomain.BylineIDs(article.Authors))
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Article{}, a.explainSlugFailure(article.SiteID, article.Slug, 0, err)
|
||||
return domain.Article{}, a.explainSlugFailure(created.SiteID, created.Lang, created.Slug, 0, err)
|
||||
}
|
||||
return a.withAuthorsOne(a.db, mappers.ArticleToDomain(created))
|
||||
}
|
||||
|
||||
func articleSlugInUse(db *gorm.DB, siteID uint32, slug string, exceptID uint32) (bool, error) {
|
||||
func openStory(tx *gorm.DB, created *entity.Article) error {
|
||||
if created.StoryID != 0 {
|
||||
return nil
|
||||
}
|
||||
story := entity.Story{}
|
||||
if err := tx.Create(&story).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
created.StoryID = story.ID
|
||||
return nil
|
||||
}
|
||||
|
||||
func articleSlugInLanguage(db *gorm.DB, siteID uint32, lang string, slug string, exceptID uint32) (bool, error) {
|
||||
var total int64
|
||||
err := db.Model(&entity.Article{}).Where("site_id = ? AND slug = ? AND id <> ?", siteID, slug, exceptID).Count(&total).Error
|
||||
err := db.Model(&entity.Article{}).Where("site_id = ? AND lang = ? AND slug = ? AND id <> ?", siteID, lang, slug, exceptID).Count(&total).Error
|
||||
return total > 0, err
|
||||
}
|
||||
|
||||
func ensureSlugFree(db *gorm.DB, siteID uint32, slug string, exceptID uint32) error {
|
||||
func ensureSlugFree(db *gorm.DB, siteID uint32, lang string, slug string, exceptID uint32) error {
|
||||
if slug == "" {
|
||||
return nil
|
||||
}
|
||||
taken, err := articleSlugInUse(db, siteID, slug, exceptID)
|
||||
taken, err := articleSlugInLanguage(db, siteID, lang, slug, exceptID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -356,18 +377,18 @@ func ensureSlugFree(db *gorm.DB, siteID uint32, slug string, exceptID uint32) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) explainSlugFailure(siteID uint32, slug string, exceptID uint32, cause error) error {
|
||||
func (a *ArticleRepository) explainSlugFailure(siteID uint32, lang string, slug string, exceptID uint32, cause error) error {
|
||||
if errors.Is(cause, domain.ErrSlugTaken) {
|
||||
return cause
|
||||
}
|
||||
if err := ensureSlugFree(a.db, siteID, slug, exceptID); err != nil && errors.Is(err, domain.ErrSlugTaken) {
|
||||
if err := ensureSlugFree(a.db, siteID, lang, slug, exceptID); err != nil && errors.Is(err, domain.ErrSlugTaken) {
|
||||
return err
|
||||
}
|
||||
return cause
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) SlugTaken(siteID uint32, slug string, exceptID uint32) (bool, error) {
|
||||
return articleSlugInUse(a.db, siteID, slug, exceptID)
|
||||
func (a *ArticleRepository) SlugTaken(siteID uint32, lang string, slug string, exceptID uint32) (bool, error) {
|
||||
return articleSlugInLanguage(a.db, siteID, lang, slug, exceptID)
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) GetAll() ([]domain.Article, error) {
|
||||
@@ -393,7 +414,7 @@ func (a *ArticleRepository) UpdateBody(id uint32, body string) (domain.Article,
|
||||
}
|
||||
|
||||
localArticle.Body = body
|
||||
err = a.db.Save(&localArticle).Error
|
||||
err = a.db.Omit(preservedArticleColumns...).Save(&localArticle).Error
|
||||
if err != nil {
|
||||
return domain.Article{}, err
|
||||
}
|
||||
@@ -419,11 +440,11 @@ func (a *ArticleRepository) Update(id uint32, update domain.Update) (domain.Arti
|
||||
}
|
||||
applyUpdate(&localArticle, update)
|
||||
if update.Slug != nil || update.SiteID != nil {
|
||||
if err := ensureSlugFree(tx, localArticle.SiteID, localArticle.Slug, id); err != nil {
|
||||
if err := ensureSlugFree(tx, localArticle.SiteID, localArticle.Lang, localArticle.Slug, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Save(&localArticle).Error; err != nil {
|
||||
if err := tx.Omit(preservedArticleColumns...).Save(&localArticle).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if update.AuthorIDs != nil {
|
||||
@@ -432,7 +453,7 @@ func (a *ArticleRepository) Update(id uint32, update domain.Update) (domain.Arti
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Article{}, a.explainSlugFailure(localArticle.SiteID, localArticle.Slug, id, err)
|
||||
return domain.Article{}, a.explainSlugFailure(localArticle.SiteID, localArticle.Lang, localArticle.Slug, id, err)
|
||||
}
|
||||
return a.withAuthorsOne(a.db, mappers.ArticleToDomain(localArticle))
|
||||
}
|
||||
@@ -478,7 +499,7 @@ func (a *ArticleRepository) UpdateIsOnline(id uint32, isOnline bool) (domain.Art
|
||||
}
|
||||
|
||||
localArticle.IsOnline = isOnline
|
||||
err = a.db.Save(&localArticle).Error
|
||||
err = a.db.Omit(preservedArticleColumns...).Save(&localArticle).Error
|
||||
if err != nil {
|
||||
return domain.Article{}, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/language"
|
||||
"RenewCMS/internal/infrastructure/persistence/mappers"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SiteLanguageRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewSiteLanguageRepository(db *gorm.DB) *SiteLanguageRepository {
|
||||
return &SiteLanguageRepository{db}
|
||||
}
|
||||
|
||||
func (s *SiteLanguageRepository) ListBySite(siteID uint32) ([]domain.SiteLanguage, error) {
|
||||
var found []entity.SiteLanguage
|
||||
if err := s.db.Where("site_id = ?", siteID).Order("is_default DESC, lang ASC").Find(&found).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
languages := make([]domain.SiteLanguage, 0, len(found))
|
||||
for _, row := range found {
|
||||
languages = append(languages, mappers.SiteLanguageToDomain(row))
|
||||
}
|
||||
return languages, nil
|
||||
}
|
||||
|
||||
func (s *SiteLanguageRepository) LockStartingLanguage(siteID uint32) (string, error) {
|
||||
if _, err := lockSite(s.db, siteID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defaultLanguage, found, err := findDefaultLanguage(s.db, siteID)
|
||||
if err != nil || !found || !defaultLanguage.Confirmed {
|
||||
return domain.Unassigned, err
|
||||
}
|
||||
return defaultLanguage.Lang, nil
|
||||
}
|
||||
|
||||
func (s *SiteLanguageRepository) ConfirmDefault(siteID uint32) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
if _, err := lockSite(tx, siteID); err != nil {
|
||||
return err
|
||||
}
|
||||
defaultLanguage, err := requireDefaultLanguage(tx, siteID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if defaultLanguage.Confirmed {
|
||||
return nil
|
||||
}
|
||||
if err := markConfirmed(tx, defaultLanguage); err != nil {
|
||||
return err
|
||||
}
|
||||
return assignUnassignedArticles(tx, defaultLanguage)
|
||||
})
|
||||
}
|
||||
|
||||
func findDefaultLanguage(db *gorm.DB, siteID uint32) (entity.SiteLanguage, bool, error) {
|
||||
var defaults []entity.SiteLanguage
|
||||
if err := db.Where("site_id = ? AND is_default = ?", siteID, true).Limit(1).Find(&defaults).Error; err != nil || len(defaults) == 0 {
|
||||
return entity.SiteLanguage{}, false, err
|
||||
}
|
||||
return defaults[0], true, nil
|
||||
}
|
||||
|
||||
func requireDefaultLanguage(db *gorm.DB, siteID uint32) (entity.SiteLanguage, error) {
|
||||
defaultLanguage, found, err := findDefaultLanguage(db, siteID)
|
||||
if err != nil {
|
||||
return entity.SiteLanguage{}, err
|
||||
}
|
||||
if !found {
|
||||
return entity.SiteLanguage{}, domain.ErrNoDefaultLanguage
|
||||
}
|
||||
return defaultLanguage, nil
|
||||
}
|
||||
|
||||
func markConfirmed(tx *gorm.DB, defaultLanguage entity.SiteLanguage) error {
|
||||
return tx.Model(&entity.SiteLanguage{}).Where("site_id = ? AND lang = ?", defaultLanguage.SiteID, defaultLanguage.Lang).UpdateColumn("confirmed", true).Error
|
||||
}
|
||||
|
||||
func assignUnassignedArticles(tx *gorm.DB, defaultLanguage entity.SiteLanguage) error {
|
||||
return tx.Unscoped().Model(&entity.Article{}).Where("site_id = ? AND lang = ?", defaultLanguage.SiteID, domain.Unassigned).UpdateColumn("lang", defaultLanguage.Lang).Error
|
||||
}
|
||||
|
||||
var _ domain.Repository = &SiteLanguageRepository{}
|
||||
@@ -20,11 +20,12 @@ func NewTransactor(db *gorm.DB) *Transactor {
|
||||
func (t *Transactor) Run(ctx context.Context, work func(domain.Repositories) error) error {
|
||||
return t.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return work(domain.Repositories{
|
||||
Articles: NewArticleRepository(tx),
|
||||
Sites: NewSiteRepository(tx),
|
||||
Authors: NewAuthorRepository(tx),
|
||||
Entries: NewEntryRepository(tx),
|
||||
Images: NewImageRepository(tx),
|
||||
Articles: NewArticleRepository(tx),
|
||||
Sites: NewSiteRepository(tx),
|
||||
Authors: NewAuthorRepository(tx),
|
||||
Entries: NewEntryRepository(tx),
|
||||
Images: NewImageRepository(tx),
|
||||
Languages: NewSiteLanguageRepository(tx),
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user