feat: editor write path
This commit is contained in:
1 parent
d9158025d4
commit
a0b3c58c52
88 files changed
+5738
-263
No files matched your search
@@ -155,3 +155,18 @@ const EmailUnavailableErrorCode = "email_unavailable"
|
||||
const LastAdminErrorCode = "last_admin"
|
||||
const AccountNotDeletableErrorCode = "account_not_deletable"
|
||||
const LastRoleHeader = "X-Last-Role"
|
||||
|
||||
const RevisionConflictErrorMessage = "revision_conflict"
|
||||
const CurrentRevisionHeader = "X-Current-Revision"
|
||||
const LanguageUnavailableErrorMessage = "language_unavailable"
|
||||
const LanguageExistsErrorMessage = "language_exists"
|
||||
const ExistingArticleHeader = "X-Existing-Article-Id"
|
||||
const LanguageNotEnabledErrorMessage = "language_not_enabled"
|
||||
const MissingLanguagesHeader = "X-Missing-Languages"
|
||||
const LanguageDisabledErrorMessage = "language_disabled"
|
||||
const SourceUnavailableErrorMessage = "source_unavailable"
|
||||
const SourceOtherStoryErrorMessage = "source_other_story"
|
||||
const SlugConflictErrorMessage = "slug_conflict"
|
||||
const ConflictingArticlesHeader = "X-Conflicting-Articles"
|
||||
const StoryMoveRequiredErrorMessage = "story_move_required"
|
||||
const StoryBusyErrorMessage = "story_busy"
|
||||
@@ -0,0 +1,99 @@
|
||||
package articleerr
|
||||
|
||||
import (
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
domain "RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/author"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/language"
|
||||
"RenewCMS/internal/domain/localization"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"RenewCMS/internal/domain/transaction"
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Write(w http.ResponseWriter, err error) bool {
|
||||
var conflict *domain.RevisionConflictError
|
||||
var existing *domain.LanguageExistsError
|
||||
var notEnabled *domain.LanguageNotEnabledError
|
||||
var addresses *domain.SlugConflictError
|
||||
var taken *domain.SlugTakenError
|
||||
switch {
|
||||
case errors.As(err, &conflict):
|
||||
w.Header().Set(apperr.CurrentRevisionHeader, strconv.FormatUint(uint64(conflict.Current), 10))
|
||||
http.Error(w, apperr.RevisionConflictErrorMessage, http.StatusConflict)
|
||||
case errors.As(err, &existing):
|
||||
w.Header().Set(apperr.ExistingArticleHeader, strconv.FormatUint(uint64(existing.ArticleID), 10))
|
||||
http.Error(w, apperr.LanguageExistsErrorMessage, http.StatusConflict)
|
||||
case errors.As(err, ¬Enabled):
|
||||
if len(notEnabled.Missing) > 0 {
|
||||
w.Header().Set(apperr.MissingLanguagesHeader, strings.Join(notEnabled.Missing, ","))
|
||||
}
|
||||
http.Error(w, apperr.LanguageNotEnabledErrorMessage, http.StatusUnprocessableEntity)
|
||||
case errors.As(err, &addresses):
|
||||
if len(addresses.ArticleIDs) > 0 {
|
||||
w.Header().Set(apperr.ConflictingArticlesHeader, joinIDs(addresses.ArticleIDs))
|
||||
}
|
||||
http.Error(w, apperr.SlugConflictErrorMessage, http.StatusConflict)
|
||||
case errors.Is(err, domain.ErrSlugTaken):
|
||||
if errors.As(err, &taken) && taken.Suggestion != "" {
|
||||
w.Header().Set(apperr.SlugSuggestionHeader, taken.Suggestion)
|
||||
}
|
||||
http.Error(w, apperr.ArticleSlugTakenErrorMessage, http.StatusConflict)
|
||||
default:
|
||||
return writeSentinel(w, err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func joinIDs(ids []uint32) string {
|
||||
parts := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
parts = append(parts, strconv.FormatUint(uint64(id), 10))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
type sentinelResponse struct {
|
||||
cause error
|
||||
message string
|
||||
status int
|
||||
}
|
||||
|
||||
var articleSentinels = []sentinelResponse{
|
||||
{domain.ErrInvalidTitle, apperr.ArticleTitleErrorMessage, http.StatusBadRequest},
|
||||
{domain.ErrArticleNotFound, apperr.ArticleNotFoundErrorMessage, http.StatusNotFound},
|
||||
{site.ErrSiteNotFound, apperr.SiteNotFoundErrorMessage, http.StatusNotFound},
|
||||
{author.ErrAuthorNotFound, apperr.AuthorNotFoundErrorMessage, http.StatusNotFound},
|
||||
{domain.ErrInvalidSlug, apperr.InvalidSlugErrorMessage, http.StatusBadRequest},
|
||||
{domain.ErrInvalidCanonicalURL, apperr.InvalidURLErrorMessage, http.StatusUnprocessableEntity},
|
||||
{image.ErrImageNotFound, apperr.ArticleImageNotFoundErrorMessage, http.StatusBadRequest},
|
||||
}
|
||||
|
||||
var localizationSentinels = []sentinelResponse{
|
||||
{domain.ErrLanguageUnavailable, apperr.LanguageUnavailableErrorMessage, http.StatusConflict},
|
||||
{domain.ErrLanguageTaken, apperr.LanguageUnavailableErrorMessage, http.StatusConflict},
|
||||
{domain.ErrLanguageDisabled, apperr.LanguageDisabledErrorMessage, http.StatusUnprocessableEntity},
|
||||
{domain.ErrSourceUnavailable, apperr.SourceUnavailableErrorMessage, http.StatusNotFound},
|
||||
{domain.ErrSourceOtherStory, apperr.SourceOtherStoryErrorMessage, http.StatusUnprocessableEntity},
|
||||
{domain.ErrInvalidTranslation, apperr.BodyErrorMessage, http.StatusBadRequest},
|
||||
{domain.ErrStoryMoveRequired, apperr.StoryMoveRequiredErrorMessage, http.StatusConflict},
|
||||
{localization.ErrSetupRequired, apperr.SetupRequiredErrorMessage, http.StatusConflict},
|
||||
{language.ErrInvalidLanguage, apperr.InvalidLanguageErrorMessage, http.StatusUnprocessableEntity},
|
||||
{language.ErrUnsupportedLanguage, apperr.UnsupportedLanguageErrorMessage, http.StatusUnprocessableEntity},
|
||||
{transaction.ErrLockContention, apperr.StoryBusyErrorMessage, http.StatusConflict},
|
||||
}
|
||||
|
||||
func writeSentinel(w http.ResponseWriter, err error) bool {
|
||||
for _, candidate := range slices.Concat(articleSentinels, localizationSentinels) {
|
||||
if errors.Is(err, candidate.cause) {
|
||||
http.Error(w, candidate.message, candidate.status)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"RenewCMS/internal/domain/site"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
@@ -19,6 +20,7 @@ const AuthorNameTag = "authorname"
|
||||
const AuthorSlugTag = "authorslug"
|
||||
const SocialTypeTag = "socialtype"
|
||||
const ArticleSlugTag = "articleslug"
|
||||
const SEODismissedTag = "seodismissed"
|
||||
|
||||
var inviteEmailPattern = regexp.MustCompile(`^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$`)
|
||||
|
||||
@@ -34,6 +36,7 @@ func InitValidator() {
|
||||
_ = Validate.RegisterValidation(AuthorSlugTag, isAuthorSlug, true)
|
||||
_ = Validate.RegisterValidation(SocialTypeTag, isSocialType, true)
|
||||
_ = Validate.RegisterValidation(ArticleSlugTag, isArticleSlug, true)
|
||||
_ = Validate.RegisterValidation(SEODismissedTag, isSEODismissed)
|
||||
}
|
||||
|
||||
func isArticleSlug(field validator.FieldLevel) bool {
|
||||
@@ -44,6 +47,10 @@ func isArticleSlug(field validator.FieldLevel) bool {
|
||||
return value == "" || article.IsValidSlug(value)
|
||||
}
|
||||
|
||||
func isSEODismissed(field validator.FieldLevel) bool {
|
||||
return slices.Contains(article.DismissCodes(), field.Field().String())
|
||||
}
|
||||
|
||||
func isAbsent(field validator.FieldLevel) bool {
|
||||
return field.Field().Kind() == reflect.Ptr && field.Field().IsNil()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user