feat: custom content types backend
This commit is contained in:
1 parent
ed67b7e70d
commit
58d58f5a04
41 files changed
+4086
-2
No files matched your search
@@ -0,0 +1,90 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func SameValue(left any, right any) bool {
|
||||
return reflect.DeepEqual(left, right)
|
||||
}
|
||||
|
||||
type UniqueValue struct {
|
||||
Key string
|
||||
Value any
|
||||
}
|
||||
|
||||
func ApplyDefaults(fields []contenttype.Field, data map[string]any) map[string]any {
|
||||
filled := make(map[string]any, len(data))
|
||||
for key, value := range data {
|
||||
filled[key] = value
|
||||
}
|
||||
for _, field := range fields {
|
||||
if _, present := filled[field.Key]; !present && field.Default != nil {
|
||||
filled[field.Key] = field.Default
|
||||
}
|
||||
}
|
||||
return filled
|
||||
}
|
||||
|
||||
func Project(fields []contenttype.Field, data map[string]any) map[string]any {
|
||||
projected := make(map[string]any, len(data))
|
||||
for _, field := range fields {
|
||||
if value, present := data[field.Key]; present {
|
||||
projected[field.Key] = value
|
||||
}
|
||||
}
|
||||
return projected
|
||||
}
|
||||
|
||||
func Validate(fields []contenttype.Field, data map[string]any) error {
|
||||
for _, field := range fields {
|
||||
if err := validateField(field, data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return firstUnknownField(fields, data)
|
||||
}
|
||||
|
||||
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 nil
|
||||
}
|
||||
if rule := field.Violation(value); rule != "" {
|
||||
return invalidField(field.Key, rule)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstUnknownField(fields []contenttype.Field, data map[string]any) error {
|
||||
known := make(map[string]bool, len(fields))
|
||||
for _, field := range fields {
|
||||
known[field.Key] = true
|
||||
}
|
||||
var unknown []string
|
||||
for key := range data {
|
||||
if !known[key] {
|
||||
unknown = append(unknown, key)
|
||||
}
|
||||
}
|
||||
if len(unknown) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(unknown)
|
||||
return invalidField(unknown[0], RuleUnknownField)
|
||||
}
|
||||
|
||||
func UniqueValues(fields []contenttype.Field, data map[string]any) []UniqueValue {
|
||||
var values []UniqueValue
|
||||
for _, field := range fields {
|
||||
if value := data[field.Key]; field.Unique && value != nil {
|
||||
values = append(values, UniqueValue{Key: field.Key, Value: value})
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEntryNotFound = errors.New("entry not found")
|
||||
ErrInvalidEntry = errors.New("invalid entry")
|
||||
ErrUniqueViolation = errors.New("a unique value is already used")
|
||||
ErrInvalidSort = errors.New("invalid sort")
|
||||
ErrInvalidPage = errors.New("invalid page")
|
||||
)
|
||||
|
||||
const (
|
||||
MaxPage = 1000000
|
||||
MaxPageSize = 100
|
||||
)
|
||||
|
||||
func ValidatePage(page int, size int) error {
|
||||
if page > MaxPage || size > MaxPageSize {
|
||||
return ErrInvalidPage
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
RuleRequired = "required"
|
||||
RuleUnknownField = "unknown_field"
|
||||
RuleUnique = "unique"
|
||||
)
|
||||
|
||||
type Entry struct {
|
||||
ID uint32 `json:"id"`
|
||||
ContentTypeID uint32 `json:"content_type_id"`
|
||||
Data map[string]any `json:"data"`
|
||||
CreatedBy uint32 `json:"created_by"`
|
||||
UpdatedBy uint32 `json:"updated_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type FieldError struct {
|
||||
Key string
|
||||
Rule string
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e FieldError) Error() string {
|
||||
return e.Key + ": " + e.Rule
|
||||
}
|
||||
|
||||
func (e FieldError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
func invalidField(key string, rule string) error {
|
||||
return FieldError{Key: key, Rule: rule, Cause: ErrInvalidEntry}
|
||||
}
|
||||
|
||||
func UniqueViolation(key string) error {
|
||||
return FieldError{Key: key, Rule: RuleUnique, Cause: ErrUniqueViolation}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package entry
|
||||
|
||||
import "strings"
|
||||
|
||||
type SortField string
|
||||
|
||||
type SortDirection string
|
||||
|
||||
const (
|
||||
SortByID SortField = "id"
|
||||
SortByCreatedAt SortField = "created_at"
|
||||
SortByUpdatedAt SortField = "updated_at"
|
||||
|
||||
SortAsc SortDirection = "asc"
|
||||
SortDesc SortDirection = "desc"
|
||||
)
|
||||
|
||||
type Sort struct {
|
||||
Field SortField
|
||||
Direction SortDirection
|
||||
}
|
||||
|
||||
func (s Sort) Valid() bool {
|
||||
switch s.Field {
|
||||
case SortByID, SortByCreatedAt, SortByUpdatedAt:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return s.Direction == SortAsc || s.Direction == SortDesc
|
||||
}
|
||||
|
||||
func (s Sort) String() string {
|
||||
return string(s.Field) + ":" + string(s.Direction)
|
||||
}
|
||||
|
||||
func ParseSort(value string) (Sort, error) {
|
||||
field, direction, found := strings.Cut(value, ":")
|
||||
parsed := Sort{Field: SortField(field), Direction: SortDirection(direction)}
|
||||
if !found || !parsed.Valid() {
|
||||
return Sort{}, ErrInvalidSort
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func DefaultSort() Sort {
|
||||
return Sort{Field: SortByUpdatedAt, Direction: SortDesc}
|
||||
}
|
||||
|
||||
type Filters struct {
|
||||
ContentTypeID uint32
|
||||
Page int
|
||||
Size int
|
||||
Sort Sort
|
||||
Unpaged bool
|
||||
}
|
||||
|
||||
type PageResult struct {
|
||||
Items []Entry
|
||||
Total int
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
Create(entry Entry) (Entry, error)
|
||||
Get(contentTypeID uint32, id uint32) (Entry, error)
|
||||
ListPage(filters Filters) (PageResult, error)
|
||||
Update(entry Entry) (Entry, error)
|
||||
Delete(contentTypeID uint32, id uint32) error
|
||||
DeleteByContentType(contentTypeID uint32) error
|
||||
CountByContentType(contentTypeID uint32) (int, error)
|
||||
FindByFieldValue(contentTypeID uint32, fieldKey string, value any, excludeID uint32) ([]Entry, error)
|
||||
}
|
||||
Reference in new issue
Block a user