255 lines
8.1 KiB
Go
255 lines
8.1 KiB
Go
package useCases
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/article"
|
|
"RenewCMS/internal/domain/audit"
|
|
"RenewCMS/internal/domain/author"
|
|
"RenewCMS/internal/domain/builtintype"
|
|
"RenewCMS/internal/domain/language"
|
|
"RenewCMS/internal/domain/localization"
|
|
"RenewCMS/internal/domain/role"
|
|
"RenewCMS/internal/domain/transaction"
|
|
"context"
|
|
"errors"
|
|
"slices"
|
|
)
|
|
|
|
type CreateTranslationUseCase struct {
|
|
transactor transaction.Transactor
|
|
articles article.Repository
|
|
localization localization.Repository
|
|
bodyRenderer article.BodyRenderer
|
|
recorder audit.Recorder
|
|
}
|
|
|
|
type CreateTranslationCommand struct {
|
|
ArticleID uint32
|
|
StoryID uint32
|
|
Lang string
|
|
Title string
|
|
Copy *bool
|
|
SourceArticleID *uint32
|
|
Format article.BodyFormat
|
|
Caller ContentCaller
|
|
}
|
|
|
|
type translationPlan struct {
|
|
base article.Article
|
|
tag string
|
|
title string
|
|
sourceID uint32
|
|
copy bool
|
|
}
|
|
|
|
func NewCreateTranslationUseCase(transactor transaction.Transactor, articles article.Repository, localizationRepository localization.Repository, bodyRenderer article.BodyRenderer, recorder audit.Recorder) *CreateTranslationUseCase {
|
|
return &CreateTranslationUseCase{transactor: transactor, articles: articles, localization: localizationRepository, bodyRenderer: bodyRenderer, recorder: recorder}
|
|
}
|
|
|
|
func (u *CreateTranslationUseCase) CreateTranslation(ctx context.Context, command CreateTranslationCommand) (article.Article, error) {
|
|
if err := command.Caller.requireHolding(role.KeyContentCreate, builtintype.KeyArticle); err != nil {
|
|
return article.Article{}, err
|
|
}
|
|
base, versions, err := u.resolveBase(command)
|
|
if err != nil {
|
|
return article.Article{}, err
|
|
}
|
|
event := articleEvent(audit.ActionArticleCreate, base)
|
|
plan, err := u.plan(command, base, versions)
|
|
if err != nil {
|
|
recordFailure(ctx, u.recorder, event, err)
|
|
return article.Article{}, err
|
|
}
|
|
created, err := u.createWithRetry(ctx, command.Caller, plan)
|
|
if err != nil {
|
|
recordFailure(ctx, u.recorder, event, err)
|
|
return article.Article{}, err
|
|
}
|
|
u.recordCreated(ctx, created)
|
|
return renderArticle(u.bodyRenderer, created, command.Format)
|
|
}
|
|
|
|
func (u *CreateTranslationUseCase) resolveBase(command CreateTranslationCommand) (article.Article, []article.Article, error) {
|
|
if command.StoryID == 0 {
|
|
base, err := u.articles.Get(command.ArticleID)
|
|
if err != nil {
|
|
return article.Article{}, nil, err
|
|
}
|
|
if !command.Caller.seesArticle(base) {
|
|
return article.Article{}, nil, article.ErrArticleNotFound
|
|
}
|
|
versions, err := u.storyVersions(base)
|
|
return base, versions, err
|
|
}
|
|
versions, err := u.articles.ListByStory(command.StoryID)
|
|
if err != nil {
|
|
return article.Article{}, nil, err
|
|
}
|
|
readable := sortByCreation(command.Caller.readableVersions(versions))
|
|
if len(readable) == 0 {
|
|
return article.Article{}, nil, article.ErrArticleNotFound
|
|
}
|
|
return readable[0], versions, nil
|
|
}
|
|
|
|
func (u *CreateTranslationUseCase) storyVersions(base article.Article) ([]article.Article, error) {
|
|
if base.StoryID == 0 {
|
|
return []article.Article{base}, nil
|
|
}
|
|
return u.articles.ListByStory(base.StoryID)
|
|
}
|
|
|
|
func (u *CreateTranslationUseCase) plan(command CreateTranslationCommand, base article.Article, versions []article.Article) (translationPlan, error) {
|
|
if command.Copy == nil || (!*command.Copy && command.SourceArticleID != nil) {
|
|
return translationPlan{}, article.ErrInvalidTranslation
|
|
}
|
|
tag, err := language.Normalize(command.Lang)
|
|
if err != nil {
|
|
return translationPlan{}, err
|
|
}
|
|
if base.Lang == language.Unassigned {
|
|
return translationPlan{}, localization.ErrSetupRequired
|
|
}
|
|
plan := translationPlan{base: base, tag: tag, title: command.Title, copy: *command.Copy}
|
|
if plan.copy {
|
|
plan.sourceID, err = u.sourceID(command, base)
|
|
if err != nil {
|
|
return translationPlan{}, err
|
|
}
|
|
}
|
|
return plan, u.checkLanguage(command.Caller, base, tag, versions)
|
|
}
|
|
|
|
func (u *CreateTranslationUseCase) sourceID(command CreateTranslationCommand, base article.Article) (uint32, error) {
|
|
sourceID := base.ID
|
|
if command.SourceArticleID != nil {
|
|
sourceID = *command.SourceArticleID
|
|
}
|
|
source, err := u.articles.Get(sourceID)
|
|
if errors.Is(err, article.ErrArticleNotFound) || err == nil && !command.Caller.seesArticle(source) {
|
|
return 0, article.ErrSourceUnavailable
|
|
}
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if source.StoryID != base.StoryID {
|
|
return 0, article.ErrSourceOtherStory
|
|
}
|
|
return sourceID, nil
|
|
}
|
|
|
|
func (u *CreateTranslationUseCase) checkLanguage(caller ContentCaller, base article.Article, tag string, versions []article.Article) error {
|
|
rows, err := u.localization.Rows(base.SiteID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !localization.HasRow(rows, tag) {
|
|
return &article.LanguageNotEnabledError{}
|
|
}
|
|
if existing, found := existingVersion(versions, tag); found {
|
|
if caller.seesArticle(existing) {
|
|
return &article.LanguageExistsError{ArticleID: existing.ID}
|
|
}
|
|
return article.ErrLanguageUnavailable
|
|
}
|
|
if !localization.EnabledTag(rows, tag) {
|
|
return article.ErrLanguageUnavailable
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func existingVersion(versions []article.Article, tag string) (article.Article, bool) {
|
|
index := slices.IndexFunc(versions, func(version article.Article) bool { return version.Lang == tag })
|
|
if index < 0 {
|
|
return article.Article{}, false
|
|
}
|
|
return versions[index], true
|
|
}
|
|
|
|
func (u *CreateTranslationUseCase) createWithRetry(ctx context.Context, caller ContentCaller, plan translationPlan) (article.Article, error) {
|
|
var err error
|
|
for attempt := 0; attempt < maxCreateSlugAttempts; attempt++ {
|
|
var created article.Article
|
|
created, err = u.createInTransaction(ctx, caller, plan)
|
|
if err == nil {
|
|
return created, nil
|
|
}
|
|
if !isSlugTakenError(err) && !errors.Is(err, transaction.ErrLockContention) {
|
|
break
|
|
}
|
|
}
|
|
if errors.Is(err, article.ErrLanguageTaken) {
|
|
return article.Article{}, article.ErrLanguageUnavailable
|
|
}
|
|
return article.Article{}, err
|
|
}
|
|
|
|
func (u *CreateTranslationUseCase) createInTransaction(ctx context.Context, caller ContentCaller, plan translationPlan) (article.Article, error) {
|
|
var created article.Article
|
|
err := u.transactor.Run(ctx, func(tx transaction.Repositories) error {
|
|
if err := tx.Sites.LockSite(plan.base.SiteID); err != nil {
|
|
return err
|
|
}
|
|
draft, err := u.draft(tx, caller, plan)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
draft.Slug, err = freeSlug(tx.Articles, draft.SiteID, draft.Lang, article.GenerateSlug(draft.Title), 0, firstSlugAttempt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
created, err = tx.Articles.Create(draft)
|
|
return err
|
|
})
|
|
return created, err
|
|
}
|
|
|
|
func (u *CreateTranslationUseCase) draft(tx transaction.Repositories, caller ContentCaller, plan translationPlan) (article.Article, error) {
|
|
base, err := tx.Articles.Get(plan.base.ID)
|
|
if err != nil {
|
|
return article.Article{}, err
|
|
}
|
|
if base.SiteID != plan.base.SiteID || base.StoryID != plan.base.StoryID {
|
|
return article.Article{}, transaction.ErrLockContention
|
|
}
|
|
draft := article.Article{
|
|
Title: plan.title,
|
|
SiteID: base.SiteID,
|
|
StoryID: base.StoryID,
|
|
Lang: plan.tag,
|
|
CreatedBy: caller.UserID,
|
|
SEODismissed: []string{},
|
|
}
|
|
if !plan.copy {
|
|
draft.Authors, err = bylinesOf(tx.Authors, caller.UserID)
|
|
return draft, err
|
|
}
|
|
source, err := tx.Articles.Get(plan.sourceID)
|
|
if errors.Is(err, article.ErrArticleNotFound) || err == nil && (!caller.seesArticle(source) || source.StoryID != base.StoryID) {
|
|
return article.Article{}, article.ErrSourceUnavailable
|
|
}
|
|
if err != nil {
|
|
return article.Article{}, err
|
|
}
|
|
return source.CopiedInto(draft), nil
|
|
}
|
|
|
|
func bylinesOf(authors author.Repository, userID uint32) ([]author.Byline, error) {
|
|
found, err := authors.GetByUserID(userID)
|
|
if errors.Is(err, author.ErrAuthorNotFound) {
|
|
return []author.Byline{}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []author.Byline{found.Byline()}, nil
|
|
}
|
|
|
|
func (u *CreateTranslationUseCase) recordCreated(ctx context.Context, created article.Article) {
|
|
event := articleEvent(audit.ActionArticleCreate, created)
|
|
event.Params[audit.ParamStoryID] = created.StoryID
|
|
if created.SourceArticleID != nil {
|
|
event.Params[audit.ParamSourceArticleID] = *created.SourceArticleID
|
|
}
|
|
u.recorder.Record(ctx, event)
|
|
}
|