package useCases import ( "RenewCMS/internal/domain/article" "RenewCMS/internal/domain/audit" "RenewCMS/internal/domain/builtintype" "RenewCMS/internal/domain/role" "RenewCMS/internal/domain/transaction" "context" "errors" ) type DeleteStoryUseCase struct { transactor transaction.Transactor articles article.Repository recorder audit.Recorder } type DeleteStoryCommand struct { StoryID uint32 Caller ContentCaller } func NewDeleteStoryUseCase(transactor transaction.Transactor, articles article.Repository, recorder audit.Recorder) *DeleteStoryUseCase { return &DeleteStoryUseCase{transactor: transactor, articles: articles, recorder: recorder} } func (u *DeleteStoryUseCase) DeleteStory(ctx context.Context, command DeleteStoryCommand) error { if err := requireDeleteOnEveryVersion(command.Caller); err != nil { return err } var deleted []article.Article err := u.transactor.Run(ctx, func(tx transaction.Repositories) error { versions, err := lockStoryVersions(tx, command.StoryID, command.Caller) if err != nil { return err } deleted = sortByCreation(versions) return tx.Articles.DeleteStory(command.StoryID) }) if err != nil { u.recordRefusal(ctx, command, err) return err } for _, version := range deleted { event := articleEvent(audit.ActionArticleDelete, version) event.Params[audit.ParamStoryID] = command.StoryID u.recorder.Record(ctx, event) } return nil } func requireDeleteOnEveryVersion(caller ContentCaller) error { if caller.Grants.ScopeFor(role.KeyContentDelete, builtintype.KeyArticle) != role.ScopeAll { return role.MissingPermissionError{Key: role.KeyContentDelete} } return nil } func lockStoryVersions(tx transaction.Repositories, storyID uint32, caller ContentCaller) ([]article.Article, error) { locked := []uint32{} for attempt := 0; attempt < maxSiteLockRounds; attempt++ { versions, err := tx.Articles.ListByStory(storyID) if err != nil { return nil, err } if len(caller.readableVersions(versions)) == 0 { return nil, article.ErrArticleNotFound } needed := siteIDsOf(versions) if allLocked(locked, needed) { return versions, nil } if err := lockSites(tx, needed); err != nil { return nil, err } locked = needed } return nil, transaction.ErrLockContention } func (u *DeleteStoryUseCase) recordRefusal(ctx context.Context, command DeleteStoryCommand, cause error) { if isNotFound(cause) || errors.Is(cause, role.ErrMissingPermission) { return } versions, err := u.articles.ListByStory(command.StoryID) readable := sortByCreation(command.Caller.readableVersions(versions)) if err != nil || len(readable) == 0 { return } recordFailure(ctx, u.recorder, articleEvent(audit.ActionArticleDelete, readable[0]), cause) }