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
@@ -0,0 +1,87 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
articleSlugIndexName = "idx_articles_site_slug"
|
||||
articleSlugIndexStatement = "CREATE UNIQUE INDEX " + articleSlugIndexName + " ON articles (site_id, slug)"
|
||||
)
|
||||
|
||||
type slugOwner struct {
|
||||
siteID uint32
|
||||
slug string
|
||||
}
|
||||
|
||||
type slugAssignment struct {
|
||||
id uint32
|
||||
slug string
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
assignments := planSlugAssignments(rows)
|
||||
for _, assignment := range assignments {
|
||||
if err := tx.Unscoped().Model(&entity.Article{}).Where("id = ?", assignment.id).UpdateColumn("slug", assignment.slug).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func planSlugAssignments(rows []entity.Article) []slugAssignment {
|
||||
assignments := []slugAssignment{}
|
||||
claimed := map[slugOwner]bool{}
|
||||
pending := []entity.Article{}
|
||||
for _, row := range rows {
|
||||
if row.DeletedAt.Valid {
|
||||
if tombstone := article.Tombstone(row.ID); row.Slug != tombstone {
|
||||
assignments = append(assignments, slugAssignment{row.ID, tombstone})
|
||||
}
|
||||
continue
|
||||
}
|
||||
owner := slugOwner{row.SiteID, row.Slug}
|
||||
if row.Slug == "" || claimed[owner] {
|
||||
pending = append(pending, row)
|
||||
continue
|
||||
}
|
||||
claimed[owner] = true
|
||||
}
|
||||
for _, row := range pending {
|
||||
base := row.Slug
|
||||
attempt := 2
|
||||
if base == "" {
|
||||
base = snapshotSlug(row.Title)
|
||||
attempt = 1
|
||||
}
|
||||
slug := firstFreeSnapshotSlug(claimed, row.SiteID, base, attempt)
|
||||
claimed[slugOwner{row.SiteID, slug}] = true
|
||||
assignments = append(assignments, slugAssignment{row.ID, slug})
|
||||
}
|
||||
return assignments
|
||||
}
|
||||
|
||||
func firstFreeSnapshotSlug(claimed map[slugOwner]bool, siteID uint32, base string, attempt int) string {
|
||||
for ; ; attempt++ {
|
||||
candidate := snapshotSlugWithSuffix(base, attempt)
|
||||
if !claimed[slugOwner{siteID, candidate}] {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ensureArticleSlugIndex(db *gorm.DB) error {
|
||||
if db.Migrator().HasIndex(&entity.Article{}, articleSlugIndexName) {
|
||||
return nil
|
||||
}
|
||||
return db.Exec(articleSlugIndexStatement).Error
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
snapshotMaxRunes = 100
|
||||
snapshotFallbackSlug = "article"
|
||||
)
|
||||
|
||||
var snapshotNonSlugRun = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
|
||||
var snapshotFolder = strings.NewReplacer(
|
||||
"à", "a", "á", "a", "â", "a", "ã", "a", "ä", "a", "å", "a", "ā", "a", "ă", "a", "ą", "a",
|
||||
"æ", "ae",
|
||||
"ç", "c", "ć", "c", "č", "c", "ĉ", "c",
|
||||
"ď", "d", "đ", "d",
|
||||
"è", "e", "é", "e", "ê", "e", "ë", "e", "ē", "e", "ė", "e", "ę", "e", "ě", "e",
|
||||
"ğ", "g",
|
||||
"ì", "i", "í", "i", "î", "i", "ï", "i", "ī", "i", "į", "i", "ı", "i",
|
||||
"ł", "l", "ľ", "l",
|
||||
"ñ", "n", "ń", "n", "ň", "n",
|
||||
"ò", "o", "ó", "o", "ô", "o", "õ", "o", "ö", "o", "ø", "o", "ō", "o", "ő", "o",
|
||||
"œ", "oe",
|
||||
"ř", "r",
|
||||
"ś", "s", "š", "s", "ş", "s", "ș", "s",
|
||||
"ß", "ss",
|
||||
"ť", "t", "ț", "t", "ţ", "t",
|
||||
"ù", "u", "ú", "u", "û", "u", "ü", "u", "ū", "u", "ů", "u", "ű", "u", "ų", "u",
|
||||
"ý", "y", "ÿ", "y",
|
||||
"ź", "z", "ż", "z", "ž", "z",
|
||||
)
|
||||
|
||||
func snapshotSlug(text string) string {
|
||||
folded := snapshotFolder.Replace(strings.ToLower(text))
|
||||
generated := strings.Trim(snapshotNonSlugRun.ReplaceAllString(folded, "-"), "-")
|
||||
generated = snapshotTruncate(generated, snapshotMaxRunes)
|
||||
if generated == "" {
|
||||
return snapshotFallbackSlug
|
||||
}
|
||||
return generated
|
||||
}
|
||||
|
||||
func snapshotSlugWithSuffix(base string, attempt int) string {
|
||||
if attempt <= 1 {
|
||||
return base
|
||||
}
|
||||
suffix := "-" + strconv.Itoa(attempt)
|
||||
return snapshotTruncate(base, snapshotMaxRunes-len(suffix)) + suffix
|
||||
}
|
||||
|
||||
func snapshotTruncate(candidate string, limit int) string {
|
||||
if len(candidate) <= limit {
|
||||
return candidate
|
||||
}
|
||||
return strings.TrimRight(candidate[:limit], "-")
|
||||
}
|
||||
@@ -17,7 +17,16 @@ func Migrate(db *gorm.DB, publicURL string) error {
|
||||
if err := BackfillUserRoles(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return SeedDefaultSite(db, publicURL)
|
||||
if err := SeedDefaultSite(db, publicURL); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := BackfillSiteTitleTemplates(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := BackfillArticleSlugs(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return ensureArticleSlugIndex(db)
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -36,6 +36,7 @@ func ensureDefaultSite(tx *gorm.DB, publicURL string) (entity.Site, error) {
|
||||
BaseURL: baseURL,
|
||||
DefaultRobots: domainSite.RobotsIndex,
|
||||
IsDefault: true,
|
||||
TitleTemplate: domainSite.DefaultTitleTemplate,
|
||||
}
|
||||
if err := tx.Create(&seeded).Error; err != nil {
|
||||
return entity.Site{}, err
|
||||
@@ -50,3 +51,7 @@ func firstDefaultSite(tx *gorm.DB) (entity.Site, error) {
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
|
||||
func BackfillSiteTitleTemplates(db *gorm.DB) error {
|
||||
return db.Model(&entity.Site{}).Where("title_template IS NULL OR title_template = ''").UpdateColumn("title_template", domainSite.DefaultTitleTemplate).Error
|
||||
}
|
||||
@@ -3,27 +3,76 @@ package mappers
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/infrastructure/persistence/models"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const emptyDismissed = "[]"
|
||||
|
||||
func decodeDismissed(raw string) []string {
|
||||
codes := []string{}
|
||||
if raw == "" {
|
||||
return codes
|
||||
}
|
||||
if err := json.Unmarshal([]byte(raw), &codes); err != nil || codes == nil {
|
||||
return []string{}
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
func encodeDismissed(codes []string) string {
|
||||
if len(codes) == 0 {
|
||||
return emptyDismissed
|
||||
}
|
||||
encoded, err := json.Marshal(codes)
|
||||
if err != nil {
|
||||
return emptyDismissed
|
||||
}
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
func ArticleToDomain(m models.Article) article.Article {
|
||||
return article.Article{
|
||||
ID: m.ID,
|
||||
Title: m.Title,
|
||||
Body: m.Body,
|
||||
IsOnline: m.IsOnline,
|
||||
SiteID: m.SiteID,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
ID: m.ID,
|
||||
Title: m.Title,
|
||||
Body: m.Body,
|
||||
IsOnline: m.IsOnline,
|
||||
SiteID: m.SiteID,
|
||||
Slug: m.Slug,
|
||||
Excerpt: m.Excerpt,
|
||||
SEOTitle: m.SEOTitle,
|
||||
SEODescription: m.SEODescription,
|
||||
CoverImageID: m.CoverImageID,
|
||||
CoverImageAlt: m.CoverImageAlt,
|
||||
SocialTitle: m.SocialTitle,
|
||||
SocialDescription: m.SocialDescription,
|
||||
SocialImageID: m.SocialImageID,
|
||||
CanonicalURL: m.CanonicalURL,
|
||||
Noindex: m.Noindex,
|
||||
SEODismissed: decodeDismissed(m.SEODismissed),
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ArticleToModel(a article.Article) models.Article {
|
||||
return models.Article{
|
||||
Title: a.Title,
|
||||
Body: a.Body,
|
||||
IsOnline: a.IsOnline,
|
||||
SiteID: a.SiteID,
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: a.UpdatedAt,
|
||||
Title: a.Title,
|
||||
Body: a.Body,
|
||||
IsOnline: a.IsOnline,
|
||||
SiteID: a.SiteID,
|
||||
Slug: a.Slug,
|
||||
Excerpt: a.Excerpt,
|
||||
SEOTitle: a.SEOTitle,
|
||||
SEODescription: a.SEODescription,
|
||||
CoverImageID: a.CoverImageID,
|
||||
CoverImageAlt: a.CoverImageAlt,
|
||||
SocialTitle: a.SocialTitle,
|
||||
SocialDescription: a.SocialDescription,
|
||||
SocialImageID: a.SocialImageID,
|
||||
CanonicalURL: a.CanonicalURL,
|
||||
Noindex: a.Noindex,
|
||||
SEODismissed: encodeDismissed(a.SEODismissed),
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: a.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@ import (
|
||||
)
|
||||
|
||||
func SiteToDomain(m models.Site, articleCount int) site.Site {
|
||||
titleTemplate := m.TitleTemplate
|
||||
if titleTemplate == "" {
|
||||
titleTemplate = site.DefaultTitleTemplate
|
||||
}
|
||||
return site.Site{
|
||||
ID: m.ID,
|
||||
Name: m.Name,
|
||||
@@ -16,6 +20,9 @@ func SiteToDomain(m models.Site, articleCount int) site.Site {
|
||||
Locale: m.Locale,
|
||||
IsDefault: m.IsDefault,
|
||||
ArticleCount: articleCount,
|
||||
SiteName: m.SiteName,
|
||||
TitleTemplate: titleTemplate,
|
||||
DefaultDescription: m.DefaultDescription,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,5 +36,8 @@ func SiteToModel(s site.Site) models.Site {
|
||||
DefaultRobots: s.DefaultRobots,
|
||||
Locale: s.Locale,
|
||||
IsDefault: s.IsDefault,
|
||||
SiteName: s.SiteName,
|
||||
TitleTemplate: s.TitleTemplate,
|
||||
DefaultDescription: s.DefaultDescription,
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,23 @@ import (
|
||||
|
||||
type Article struct {
|
||||
gorm.Model
|
||||
ID uint32 `gorm:"primary_key;auto_increment;not_null"`
|
||||
Title string
|
||||
Body string
|
||||
IsOnline bool `gorm:"not_null;default:false"`
|
||||
SiteID uint32 `gorm:"index"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
ID uint32 `gorm:"primary_key;auto_increment;not_null"`
|
||||
Title string
|
||||
Body string
|
||||
IsOnline bool `gorm:"not_null;default:false"`
|
||||
SiteID uint32 `gorm:"index"`
|
||||
Slug string `gorm:"size:100;not null;default:''"`
|
||||
Excerpt string `gorm:"size:300;not null;default:''"`
|
||||
SEOTitle string `gorm:"column:seo_title;size:120;not null;default:''"`
|
||||
SEODescription string `gorm:"column:seo_description;size:320;not null;default:''"`
|
||||
CoverImageID *uint32 `gorm:"index"`
|
||||
CoverImageAlt string `gorm:"size:200;not null;default:''"`
|
||||
SocialTitle string `gorm:"size:120;not null;default:''"`
|
||||
SocialDescription string `gorm:"size:320;not null;default:''"`
|
||||
SocialImageID *uint32 `gorm:"index"`
|
||||
CanonicalURL string `gorm:"column:canonical_url;size:2048;not null;default:''"`
|
||||
Noindex bool `gorm:"not null;default:false"`
|
||||
SEODismissed string `gorm:"column:seo_dismissed;type:text"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
}
|
||||
@@ -11,6 +11,9 @@ type Site struct {
|
||||
DefaultRobots string `gorm:"size:16;not null"`
|
||||
Locale *string `gorm:"size:35"`
|
||||
IsDefault bool `gorm:"not null"`
|
||||
SiteName string `gorm:"size:100;not null;default:''"`
|
||||
TitleTemplate string `gorm:"size:200"`
|
||||
DefaultDescription string `gorm:"size:320;not null;default:''"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
}
|
||||
@@ -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