refac: atomic merge, lost-update race, RunPurge cost, duplicated walks
This commit is contained in:
1 parent
0fc3511e2b
commit
074944c99a
45 files changed
+2204
-267
No files matched your search
@@ -115,6 +115,21 @@ func (f Field) validDefault() bool {
|
||||
return !f.Unique && f.Violation(f.Default) == ""
|
||||
}
|
||||
|
||||
type MediaRef struct {
|
||||
Field Field
|
||||
ID uint32
|
||||
}
|
||||
|
||||
func MediaRefs(fields []Field, data map[string]any) []MediaRef {
|
||||
var refs []MediaRef
|
||||
for _, field := range MediaFields(fields) {
|
||||
if id, set := MediaID(data[field.Key]); set {
|
||||
refs = append(refs, MediaRef{Field: field, ID: id})
|
||||
}
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
func MediaFields(fields []Field) []Field {
|
||||
var media []Field
|
||||
for _, field := range fields {
|
||||
|
||||
@@ -51,12 +51,12 @@ func validateField(field contenttype.Field, data map[string]any) error {
|
||||
value := data[field.Key]
|
||||
if value == nil {
|
||||
if field.Required {
|
||||
return invalidField(field.Key, RuleRequired)
|
||||
return InvalidField(field.Key, RuleRequired)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if rule := field.Violation(value); rule != "" {
|
||||
return invalidField(field.Key, rule)
|
||||
return InvalidField(field.Key, rule)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func firstUnknownField(fields []contenttype.Field, data map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(unknown)
|
||||
return invalidField(unknown[0], RuleUnknownField)
|
||||
return InvalidField(unknown[0], RuleUnknownField)
|
||||
}
|
||||
|
||||
func UniqueValues(fields []contenttype.Field, data map[string]any) []UniqueValue {
|
||||
@@ -91,10 +91,15 @@ func UniqueValues(fields []contenttype.Field, data map[string]any) []UniqueValue
|
||||
|
||||
func MediaIDs(fields []contenttype.Field, data map[string]any) map[string]uint32 {
|
||||
ids := map[string]uint32{}
|
||||
for _, field := range contenttype.MediaFields(fields) {
|
||||
if id, ok := contenttype.MediaID(data[field.Key]); ok {
|
||||
ids[field.Key] = id
|
||||
}
|
||||
for _, ref := range contenttype.MediaRefs(fields, data) {
|
||||
ids[ref.Field.Key] = ref.ID
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func MissingMedia(field contenttype.Field, unchanged bool) error {
|
||||
if unchanged {
|
||||
return InvalidField(field.Key, RuleRequired)
|
||||
}
|
||||
return InvalidField(field.Key, contenttype.RuleType)
|
||||
}
|
||||
@@ -55,12 +55,8 @@ func (e FieldError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
func invalidField(key string, rule string) error {
|
||||
return FieldError{Key: key, Rule: rule, Cause: ErrInvalidEntry}
|
||||
}
|
||||
|
||||
func InvalidField(key string, rule string) error {
|
||||
return invalidField(key, rule)
|
||||
return FieldError{Key: key, Rule: rule, Cause: ErrInvalidEntry}
|
||||
}
|
||||
|
||||
func UniqueViolation(key string) error {
|
||||
|
||||
@@ -246,3 +246,52 @@ type Image struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func isStoredNameByte(char byte) bool {
|
||||
return char == '-' || char == '_' || char == '.' || char >= '0' && char <= '9' || char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
|
||||
}
|
||||
|
||||
func continuesName(body string, end int) bool {
|
||||
if end >= len(body) || !isStoredNameByte(body[end]) {
|
||||
return false
|
||||
}
|
||||
return body[end] != '.' || end+1 < len(body) && isStoredNameByte(body[end+1])
|
||||
}
|
||||
|
||||
func storedNameMatches(body string, name string) []int {
|
||||
var starts []int
|
||||
if name == "" {
|
||||
return starts
|
||||
}
|
||||
for from := 0; from < len(body); {
|
||||
found := strings.Index(body[from:], name)
|
||||
if found < 0 {
|
||||
break
|
||||
}
|
||||
start := from + found
|
||||
end := start + len(name)
|
||||
from = start + 1
|
||||
if start > 0 && isStoredNameByte(body[start-1]) || continuesName(body, end) {
|
||||
continue
|
||||
}
|
||||
starts = append(starts, start)
|
||||
from = end
|
||||
}
|
||||
return starts
|
||||
}
|
||||
|
||||
func ContainsStoredName(body string, name string) bool {
|
||||
return len(storedNameMatches(body, name)) > 0
|
||||
}
|
||||
|
||||
func ReplaceStoredName(body string, from string, to string) string {
|
||||
var rewritten strings.Builder
|
||||
last := 0
|
||||
for _, start := range storedNameMatches(body, from) {
|
||||
rewritten.WriteString(body[last:start])
|
||||
rewritten.WriteString(to)
|
||||
last = start + len(from)
|
||||
}
|
||||
rewritten.WriteString(body[last:])
|
||||
return rewritten.String()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package transaction
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/author"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"context"
|
||||
)
|
||||
|
||||
type Repositories struct {
|
||||
Articles article.Repository
|
||||
Sites site.Repository
|
||||
Authors author.Repository
|
||||
Entries entry.Repository
|
||||
Images image.Repository
|
||||
}
|
||||
|
||||
type Transactor interface {
|
||||
Run(ctx context.Context, work func(Repositories) error) error
|
||||
}
|
||||
Reference in new issue
Block a user