feat: editor write path

This commit is contained in:
Florian Sylvain committed 2026-11-02 03:24:30 +01:00
1 parent d9158025d4
commit a0b3c58c52
88 files changed
+5738 -263

No files matched your search

@@ -6,8 +6,11 @@ import (
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/builtintype"
"RenewCMS/internal/domain/image"
"RenewCMS/internal/domain/language"
"RenewCMS/internal/domain/localization"
"RenewCMS/internal/domain/role"
"RenewCMS/internal/domain/site"
"RenewCMS/internal/domain/transaction"
"context"
"slices"
"strconv"
@@ -21,9 +24,13 @@ type UpdateArticleUseCase struct {
sites site.Repository
authors author.Repository
images image.Repository
localization localization.Repository
storyMover StoryMover
}
type UpdateArticleCommand struct {
Title *string
IfRevision *uint32
Body *string
SiteID *uint32
AuthorIDs *[]uint32
@@ -42,8 +49,8 @@ type UpdateArticleCommand struct {
Format article.BodyFormat
}
func NewUpdateArticleUseCase(articleRepository article.Repository, bodyRenderer article.BodyRenderer, recorder audit.Recorder, sites site.Repository, authors author.Repository, images image.Repository) *UpdateArticleUseCase {
return &UpdateArticleUseCase{articleRepository: articleRepository, bodyRenderer: bodyRenderer, recorder: recorder, sites: sites, authors: authors, images: images}
func NewUpdateArticleUseCase(articleRepository article.Repository, bodyRenderer article.BodyRenderer, recorder audit.Recorder, sites site.Repository, authors author.Repository, images image.Repository, localizationRepository localization.Repository, storyMover StoryMover) *UpdateArticleUseCase {
return &UpdateArticleUseCase{articleRepository: articleRepository, bodyRenderer: bodyRenderer, recorder: recorder, sites: sites, authors: authors, images: images, localization: localizationRepository, storyMover: storyMover}
}
func (g *UpdateArticleUseCase) UpdateBody(ctx context.Context, id uint32, caller ContentCaller, body string, format article.BodyFormat) (article.Article, error) {
@@ -61,10 +68,29 @@ func (g *UpdateArticleUseCase) Update(ctx context.Context, id uint32, caller Con
if err := caller.authorize(role.KeyContentUpdate, builtintype.KeyArticle, before.CreatedBy); err != nil {
return article.Article{}, err
}
if !caller.seesArticle(before) {
return article.Article{}, article.ErrArticleNotFound
}
if command.IfRevision != nil && before.EditRevision != *command.IfRevision {
err := &article.RevisionConflictError{Current: before.EditRevision}
g.recordFailure(ctx, audit.ActionArticleEdit, before, err)
return article.Article{}, err
}
if command.Title != nil {
if err := article.ValidateTitle(*command.Title); err != nil {
g.recordFailure(ctx, audit.ActionArticleEdit, before, err)
return article.Article{}, err
}
}
destination, err := g.destinationSite(command)
if err != nil {
return article.Article{}, err
}
movesWholeStory, err := g.checkSingleVersionMove(before, caller, command)
if err != nil {
g.recordFailure(ctx, audit.ActionArticleEdit, before, err)
return article.Article{}, err
}
if err := g.ensureAuthorsExist(command.AuthorIDs); err != nil {
return article.Article{}, err
}
@@ -78,7 +104,28 @@ func (g *UpdateArticleUseCase) Update(ctx context.Context, id uint32, caller Con
return article.Article{}, err
}
update.Slug = slug
if movesWholeStory {
return g.updateWithStory(ctx, id, caller, before, command, destination, update, slug)
}
updated, err := g.articleRepository.Update(id, update)
return g.finishEdit(ctx, before, command, slug, destination, updated, err)
}
func (g *UpdateArticleUseCase) updateWithStory(ctx context.Context, id uint32, caller ContentCaller, before article.Article, command UpdateArticleCommand, destination *site.Site, update article.Update, slug *string) (article.Article, error) {
var updated article.Article
var writeErr error
move := MoveStoryCommand{StoryID: before.StoryID, SiteKey: destination.Key, Caller: caller, SkipArticleID: id}
_, err := g.storyMover.MoveStoryThen(ctx, move, func(tx transaction.Repositories) error {
updated, writeErr = tx.Articles.Update(id, update)
return writeErr
})
if err != nil && writeErr == nil {
return article.Article{}, err
}
return g.finishEdit(ctx, before, command, slug, destination, updated, err)
}
func (g *UpdateArticleUseCase) finishEdit(ctx context.Context, before article.Article, command UpdateArticleCommand, slug *string, destination *site.Site, updated article.Article, err error) (article.Article, error) {
if isSlugTakenError(err) {
err = g.raceConflict(before, command, slug)
}
@@ -92,6 +139,8 @@ func (g *UpdateArticleUseCase) Update(ctx context.Context, id uint32, caller Con
func (c UpdateArticleCommand) toUpdate() article.Update {
update := article.Update{
Title: c.Title,
IfRevision: c.IfRevision,
Body: c.Body,
SiteID: c.SiteID,
AuthorIDs: c.AuthorIDs,
@@ -188,6 +237,47 @@ func (g *UpdateArticleUseCase) destinationSite(command UpdateArticleCommand) (*s
return &destination, nil
}
func (g *UpdateArticleUseCase) checkSingleVersionMove(before article.Article, caller ContentCaller, command UpdateArticleCommand) (bool, error) {
if command.SiteID == nil || *command.SiteID == before.SiteID {
return false, nil
}
versions, err := g.articleRepository.ListByStory(before.StoryID)
if err != nil {
return false, err
}
if len(caller.readableVersions(versions)) > 1 {
return false, article.ErrStoryMoveRequired
}
if err := requireUpdateOnAll(caller, versions); err != nil {
return false, err
}
if len(versions) > 1 {
return true, nil
}
rows, err := g.localization.Rows(*command.SiteID)
if err != nil {
return false, err
}
if missing := localization.MissingTags(rows, []string{before.Lang}); len(missing) > 0 {
return false, &article.LanguageNotEnabledError{Missing: missing}
}
return false, nil
}
func (g *UpdateArticleUseCase) checkPublishable(before article.Article, isOnline bool) error {
if !isOnline || before.IsOnline || before.Lang == language.Unassigned {
return nil
}
rows, err := g.localization.Rows(before.SiteID)
if err != nil {
return err
}
if !localization.EnabledTag(rows, before.Lang) {
return article.ErrLanguageDisabled
}
return nil
}
func (g *UpdateArticleUseCase) ensureAuthorsExist(ids *[]uint32) error {
if ids == nil {
return nil
@@ -230,8 +320,7 @@ func (g *UpdateArticleUseCase) recordEdit(ctx context.Context, before article.Ar
if len(changes) == 0 {
return
}
event := audit.NewEvent(audit.ActionArticleEdit, audit.CategoryArticles)
event.Target = articleTarget(after)
event := articleEvent(audit.ActionArticleEdit, after)
event.Changes = changes
g.recorder.Record(ctx, event)
}
@@ -266,6 +355,10 @@ func (g *UpdateArticleUseCase) UpdateIsOnline(ctx context.Context, id uint32, ca
if err := caller.authorize(role.KeyContentPublish, builtintype.KeyArticle, before.CreatedBy); err != nil {
return article.Article{}, err
}
if err := g.checkPublishable(before, isOnline); err != nil {
g.recordFailure(ctx, publishAction(isOnline), before, err)
return article.Article{}, err
}
updated, err := g.articleRepository.UpdateIsOnline(id, isOnline)
if err != nil {
g.recordFailure(ctx, publishAction(isOnline), before, err)
@@ -287,14 +380,11 @@ func (g *UpdateArticleUseCase) recordChange(ctx context.Context, action string,
if len(changes) == 0 {
return
}
event := audit.NewEvent(action, audit.CategoryArticles)
event.Target = articleTarget(after)
event := articleEvent(action, after)
event.Changes = changes
g.recorder.Record(ctx, event)
}
func (g *UpdateArticleUseCase) recordFailure(ctx context.Context, action string, before article.Article, cause error) {
event := audit.NewEvent(action, audit.CategoryArticles)
event.Target = articleTarget(before)
recordFailure(ctx, g.recorder, event, cause)
recordFailure(ctx, g.recorder, articleEvent(action, before), cause)
}