feat: SEO backend data (1/4)
This commit is contained in:
1 parent
cb1410f0b0
commit
0eb46b0d74
90 files changed
+6094
-350
No files matched your search
@@ -5,6 +5,7 @@ import (
|
||||
authordomain "RenewCMS/internal/domain/author"
|
||||
"RenewCMS/internal/infrastructure/persistence/mappers"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
"errors"
|
||||
|
||||
"strings"
|
||||
"unicode"
|
||||
@@ -260,35 +261,59 @@ func (a *ArticleRepository) GetByName(name string) (domain.Article, error) {
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) Create(article domain.Article) (domain.Article, error) {
|
||||
created := entity.Article{
|
||||
Title: article.Title,
|
||||
Body: article.Body,
|
||||
IsOnline: article.IsOnline,
|
||||
SiteID: article.SiteID,
|
||||
}
|
||||
created := mappers.ArticleToModel(article)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&created).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return replaceCredits(tx, created.ID, bylineIDs(article.Authors))
|
||||
return replaceCredits(tx, created.ID, authordomain.BylineIDs(article.Authors))
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Article{}, err
|
||||
return domain.Article{}, a.explainSlugFailure(article.SiteID, article.Slug, 0, err)
|
||||
}
|
||||
return a.withAuthorsOne(a.db, mappers.ArticleToDomain(created))
|
||||
}
|
||||
|
||||
func bylineIDs(bylines []authordomain.Byline) []uint32 {
|
||||
ids := make([]uint32, 0, len(bylines))
|
||||
for _, byline := range bylines {
|
||||
ids = append(ids, byline.ID)
|
||||
func articleSlugInUse(db *gorm.DB, siteID uint32, 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
|
||||
return total > 0, err
|
||||
}
|
||||
|
||||
func ensureSlugFree(db *gorm.DB, siteID uint32, slug string, exceptID uint32) error {
|
||||
if slug == "" {
|
||||
return nil
|
||||
}
|
||||
return ids
|
||||
taken, err := articleSlugInUse(db, siteID, slug, exceptID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if taken {
|
||||
return domain.ErrSlugTaken
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) explainSlugFailure(siteID uint32, 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) {
|
||||
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) GetAll() []domain.Article {
|
||||
@@ -333,11 +358,16 @@ func (a *ArticleRepository) Update(id uint32, update domain.Update) (domain.Arti
|
||||
if err := tx.Model(&entity.Article{}).First(&localArticle, id).Error; err != nil {
|
||||
return mapNotFoundErr(err, domain.ErrArticleNotFound)
|
||||
}
|
||||
if update.Body != nil {
|
||||
localArticle.Body = *update.Body
|
||||
if update.SiteID == nil && update.Slug != nil {
|
||||
if _, err := lockSite(tx, localArticle.SiteID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if update.SiteID != nil {
|
||||
localArticle.SiteID = *update.SiteID
|
||||
applyUpdate(&localArticle, update)
|
||||
if update.Slug != nil || update.SiteID != nil {
|
||||
if err := ensureSlugFree(tx, localArticle.SiteID, localArticle.Slug, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Save(&localArticle).Error; err != nil {
|
||||
return err
|
||||
@@ -348,11 +378,44 @@ func (a *ArticleRepository) Update(id uint32, update domain.Update) (domain.Arti
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Article{}, err
|
||||
return domain.Article{}, a.explainSlugFailure(localArticle.SiteID, localArticle.Slug, id, err)
|
||||
}
|
||||
return a.withAuthorsOne(a.db, mappers.ArticleToDomain(localArticle))
|
||||
}
|
||||
|
||||
func applyUpdate(target *entity.Article, update domain.Update) {
|
||||
setString(&target.Body, update.Body)
|
||||
setString(&target.Slug, update.Slug)
|
||||
setString(&target.Excerpt, update.Excerpt)
|
||||
setString(&target.SEOTitle, update.SEOTitle)
|
||||
setString(&target.SEODescription, update.SEODescription)
|
||||
setString(&target.CoverImageAlt, update.CoverImageAlt)
|
||||
setString(&target.SocialTitle, update.SocialTitle)
|
||||
setString(&target.SocialDescription, update.SocialDescription)
|
||||
setString(&target.CanonicalURL, update.CanonicalURL)
|
||||
if update.SiteID != nil {
|
||||
target.SiteID = *update.SiteID
|
||||
}
|
||||
if update.Noindex != nil {
|
||||
target.Noindex = *update.Noindex
|
||||
}
|
||||
if update.CoverImageID.Set {
|
||||
target.CoverImageID = update.CoverImageID.Value
|
||||
}
|
||||
if update.SocialImageID.Set {
|
||||
target.SocialImageID = update.SocialImageID.Value
|
||||
}
|
||||
if update.SEODismissed != nil {
|
||||
target.SEODismissed = mappers.ArticleToModel(domain.Article{SEODismissed: *update.SEODismissed}).SEODismissed
|
||||
}
|
||||
}
|
||||
|
||||
func setString(target *string, value *string) {
|
||||
if value != nil {
|
||||
*target = *value
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) UpdateIsOnline(id uint32, isOnline bool) (domain.Article, error) {
|
||||
var localArticle entity.Article
|
||||
err := a.db.Model(&entity.Article{}).First(&localArticle, id).Error
|
||||
@@ -371,6 +434,14 @@ func (a *ArticleRepository) UpdateIsOnline(id uint32, isOnline bool) (domain.Art
|
||||
|
||||
func (a *ArticleRepository) Delete(id uint32) error {
|
||||
return a.db.Transaction(func(tx *gorm.DB) error {
|
||||
var found entity.Article
|
||||
if err := tx.First(&found, id).Error; err != nil {
|
||||
return mapNotFoundErr(err, domain.ErrArticleNotFound)
|
||||
}
|
||||
tombstone := domain.Tombstone(id)
|
||||
if err := tx.Model(&entity.Article{}).Where("id = ?", id).UpdateColumn("slug", tombstone).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := deleteOrNotFound(tx.Delete(&entity.Article{}, id), domain.ErrArticleNotFound); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -388,4 +459,73 @@ func (a *ArticleRepository) FindByBodyContaining(filename string) ([]domain.Arti
|
||||
return toDomainArticles(entityArticles), nil
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) FindByImage(imageID uint32) ([]domain.Article, error) {
|
||||
var entityArticles []entity.Article
|
||||
err := a.db.Model(&entity.Article{}).Where("cover_image_id = ? OR social_image_id = ?", imageID, imageID).Order("id asc").Find(&entityArticles).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toDomainArticles(entityArticles), nil
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) ClearImage(imageID uint32) (domain.ImageReferences, error) {
|
||||
references := domain.ImageReferences{Cover: []uint32{}, Social: []uint32{}}
|
||||
err := a.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := clearColumn(tx, "cover_image_id", imageID, &references.Cover); err != nil {
|
||||
return err
|
||||
}
|
||||
return clearColumn(tx, "social_image_id", imageID, &references.Social)
|
||||
})
|
||||
if err != nil {
|
||||
return domain.ImageReferences{}, err
|
||||
}
|
||||
return references, nil
|
||||
}
|
||||
|
||||
func clearColumn(tx *gorm.DB, column string, imageID uint32, cleared *[]uint32) error {
|
||||
if err := tx.Model(&entity.Article{}).Where(column+" = ?", imageID).Order("id ASC").Pluck("id", cleared).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(*cleared) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Model(&entity.Article{}).Where("id IN ?", *cleared).UpdateColumn(column, nil).Error
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) RestoreImage(references domain.ImageReferences, imageID uint32) error {
|
||||
return a.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := assignColumn(tx, "cover_image_id", references.Cover, imageID); err != nil {
|
||||
return err
|
||||
}
|
||||
return assignColumn(tx, "social_image_id", references.Social, imageID)
|
||||
})
|
||||
}
|
||||
|
||||
func assignColumn(tx *gorm.DB, column string, articleIDs []uint32, imageID uint32) error {
|
||||
if len(articleIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Model(&entity.Article{}).Where("id IN ?", articleIDs).UpdateColumn(column, imageID).Error
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) RepointImage(fromImageIDs []uint32, toImageID uint32) ([]uint32, error) {
|
||||
changed := []uint32{}
|
||||
if len(fromImageIDs) == 0 {
|
||||
return changed, nil
|
||||
}
|
||||
err := a.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&entity.Article{}).Where("cover_image_id IN ? OR social_image_id IN ?", fromImageIDs, fromImageIDs).Order("id ASC").Pluck("id", &changed).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&entity.Article{}).Where("cover_image_id IN ?", fromImageIDs).UpdateColumn("cover_image_id", toImageID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&entity.Article{}).Where("social_image_id IN ?", fromImageIDs).UpdateColumn("social_image_id", toImageID).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
var _ domain.Repository = &ArticleRepository{}
|
||||
@@ -319,6 +319,14 @@ func (a *AuthorRepository) AvatarUsage() (map[uint32]int, error) {
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
func (a *AuthorRepository) FindByAvatar(imageID uint32) ([]domain.Author, error) {
|
||||
var found []entity.Author
|
||||
if err := a.db.Where("avatar_id = ?", imageID).Order("id ASC").Find(&found).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.toDomain(a.db, found)
|
||||
}
|
||||
|
||||
func (a *AuthorRepository) ClearAvatar(imageID uint32) ([]uint32, error) {
|
||||
ids := []uint32{}
|
||||
err := a.db.Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
@@ -106,11 +106,18 @@ func (s *SiteRepository) Create(site domain.Site) (domain.Site, error) {
|
||||
return tx.Create(&model).Error
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Site{}, err
|
||||
return domain.Site{}, s.explainKeyFailure(model.Key, err)
|
||||
}
|
||||
return mappers.SiteToDomain(model, 0), nil
|
||||
}
|
||||
|
||||
func (s *SiteRepository) explainKeyFailure(key string, cause error) error {
|
||||
if _, err := s.GetByKey(key); err == nil {
|
||||
return domain.ErrSiteKeyTaken
|
||||
}
|
||||
return cause
|
||||
}
|
||||
|
||||
func lockAllSites(tx *gorm.DB) error {
|
||||
var locked []entity.Site
|
||||
return tx.Clauses(clause.Locking{Strength: "UPDATE"}).Order("id ASC").Find(&locked).Error
|
||||
@@ -135,6 +142,9 @@ func (s *SiteRepository) Update(site domain.Site) (domain.Site, error) {
|
||||
"default_social_image_id": site.DefaultSocialImageID,
|
||||
"default_robots": site.DefaultRobots,
|
||||
"locale": site.Locale,
|
||||
"site_name": site.SiteName,
|
||||
"title_template": site.TitleTemplate,
|
||||
"default_description": site.DefaultDescription,
|
||||
}
|
||||
if _, err := s.Get(site.ID); err != nil {
|
||||
return domain.Site{}, err
|
||||
|
||||
Reference in new issue
Block a user