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,57 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CreateContentTypeUseCase struct {
|
||||
contentTypes contenttype.Repository
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type CreateContentTypeCommand struct {
|
||||
Key string
|
||||
Name string
|
||||
Fields []contenttype.Field
|
||||
}
|
||||
|
||||
func NewCreateContentTypeUseCase(contentTypes contenttype.Repository, now func() time.Time) *CreateContentTypeUseCase {
|
||||
return &CreateContentTypeUseCase{contentTypes, now}
|
||||
}
|
||||
|
||||
func (u *CreateContentTypeUseCase) Create(command CreateContentTypeCommand) (contenttype.ContentType, error) {
|
||||
if err := contenttype.ValidateKey(command.Key); err != nil {
|
||||
return contenttype.ContentType{}, err
|
||||
}
|
||||
if err := contenttype.ValidateName(command.Name); err != nil {
|
||||
return contenttype.ContentType{}, err
|
||||
}
|
||||
fields, err := contenttype.ValidateFields(command.Fields)
|
||||
if err != nil {
|
||||
return contenttype.ContentType{}, err
|
||||
}
|
||||
if err := u.ensureKeyFree(command.Key); err != nil {
|
||||
return contenttype.ContentType{}, err
|
||||
}
|
||||
created := u.now()
|
||||
return u.contentTypes.Create(contenttype.ContentType{
|
||||
Key: command.Key,
|
||||
Name: command.Name,
|
||||
Fields: fields,
|
||||
CreatedAt: created,
|
||||
UpdatedAt: created,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *CreateContentTypeUseCase) ensureKeyFree(key string) error {
|
||||
_, err := u.contentTypes.GetByKey(key)
|
||||
if errors.Is(err, contenttype.ErrContentTypeNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return contenttype.ErrKeyTaken
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CreateEntryUseCase struct {
|
||||
contentTypes contenttype.Repository
|
||||
entries entry.Repository
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type CreateEntryCommand struct {
|
||||
ContentTypeKey string
|
||||
Data map[string]any
|
||||
UserID uint32
|
||||
}
|
||||
|
||||
func NewCreateEntryUseCase(contentTypes contenttype.Repository, entries entry.Repository, now func() time.Time) *CreateEntryUseCase {
|
||||
return &CreateEntryUseCase{contentTypes, entries, now}
|
||||
}
|
||||
|
||||
func (u *CreateEntryUseCase) Create(command CreateEntryCommand) (TypedEntry, error) {
|
||||
owner, err := u.contentTypes.GetByKey(command.ContentTypeKey)
|
||||
if err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
data := entry.ApplyDefaults(owner.Fields, command.Data)
|
||||
if err := entry.Validate(owner.Fields, data); err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
if err := ensureUnique(u.entries, owner, data, 0); err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
created := u.now()
|
||||
stored, err := u.entries.Create(entry.Entry{
|
||||
ContentTypeID: owner.ID,
|
||||
Data: data,
|
||||
CreatedBy: command.UserID,
|
||||
UpdatedBy: command.UserID,
|
||||
CreatedAt: created,
|
||||
UpdatedAt: created,
|
||||
})
|
||||
if err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
return typedEntry(owner, stored), nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
)
|
||||
|
||||
type DeleteContentTypeUseCase struct {
|
||||
contentTypes contenttype.Repository
|
||||
entries entry.Repository
|
||||
}
|
||||
|
||||
func NewDeleteContentTypeUseCase(contentTypes contenttype.Repository, entries entry.Repository) *DeleteContentTypeUseCase {
|
||||
return &DeleteContentTypeUseCase{contentTypes, entries}
|
||||
}
|
||||
|
||||
func (u *DeleteContentTypeUseCase) Delete(key string) error {
|
||||
target, err := u.contentTypes.GetByKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count, err := u.entries.CountByContentType(target.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return contenttype.ErrHasEntries
|
||||
}
|
||||
return u.contentTypes.Delete(target.ID)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
)
|
||||
|
||||
type DeleteEntryUseCase struct {
|
||||
contentTypes contenttype.Repository
|
||||
entries entry.Repository
|
||||
}
|
||||
|
||||
func NewDeleteEntryUseCase(contentTypes contenttype.Repository, entries entry.Repository) *DeleteEntryUseCase {
|
||||
return &DeleteEntryUseCase{contentTypes, entries}
|
||||
}
|
||||
|
||||
func (u *DeleteEntryUseCase) Delete(contentTypeKey string, id uint32) error {
|
||||
owner, err := u.contentTypes.GetByKey(contentTypeKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return u.entries.Delete(owner.ID, id)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package useCases
|
||||
|
||||
import "RenewCMS/internal/domain/contenttype"
|
||||
|
||||
type GetContentTypeUseCase struct {
|
||||
contentTypes contenttype.Repository
|
||||
}
|
||||
|
||||
func NewGetContentTypeUseCase(contentTypes contenttype.Repository) *GetContentTypeUseCase {
|
||||
return &GetContentTypeUseCase{contentTypes}
|
||||
}
|
||||
|
||||
func (u *GetContentTypeUseCase) Get(key string) (contenttype.ContentType, error) {
|
||||
return u.contentTypes.GetByKey(key)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
)
|
||||
|
||||
type GetEntryUseCase struct {
|
||||
contentTypes contenttype.Repository
|
||||
entries entry.Repository
|
||||
}
|
||||
|
||||
func NewGetEntryUseCase(contentTypes contenttype.Repository, entries entry.Repository) *GetEntryUseCase {
|
||||
return &GetEntryUseCase{contentTypes, entries}
|
||||
}
|
||||
|
||||
func (u *GetEntryUseCase) Get(contentTypeKey string, id uint32) (TypedEntry, error) {
|
||||
owner, err := u.contentTypes.GetByKey(contentTypeKey)
|
||||
if err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
found, err := u.entries.Get(owner.ID, id)
|
||||
if err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
return typedEntry(owner, found), nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package useCases
|
||||
|
||||
import "RenewCMS/internal/domain/contenttype"
|
||||
|
||||
type ListContentTypesUseCase struct {
|
||||
contentTypes contenttype.Repository
|
||||
}
|
||||
|
||||
func NewListContentTypesUseCase(contentTypes contenttype.Repository) *ListContentTypesUseCase {
|
||||
return &ListContentTypesUseCase{contentTypes}
|
||||
}
|
||||
|
||||
func (u *ListContentTypesUseCase) List() ([]contenttype.ContentType, error) {
|
||||
found, err := u.contentTypes.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if found == nil {
|
||||
return []contenttype.ContentType{}, nil
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
)
|
||||
|
||||
type ListEntriesUseCase struct {
|
||||
contentTypes contenttype.Repository
|
||||
entries entry.Repository
|
||||
}
|
||||
|
||||
type ListEntriesCommand struct {
|
||||
ContentTypeKey string
|
||||
Page int
|
||||
Size int
|
||||
Sort entry.Sort
|
||||
}
|
||||
|
||||
type EntryPage struct {
|
||||
Items []TypedEntry
|
||||
Total int
|
||||
Page int
|
||||
Size int
|
||||
}
|
||||
|
||||
func NewListEntriesUseCase(contentTypes contenttype.Repository, entries entry.Repository) *ListEntriesUseCase {
|
||||
return &ListEntriesUseCase{contentTypes, entries}
|
||||
}
|
||||
|
||||
func (u *ListEntriesUseCase) ListPage(command ListEntriesCommand) (EntryPage, error) {
|
||||
if err := entry.ValidatePage(command.Page, command.Size); err != nil {
|
||||
return EntryPage{}, err
|
||||
}
|
||||
if !command.Sort.Valid() {
|
||||
return EntryPage{}, entry.ErrInvalidSort
|
||||
}
|
||||
owner, err := u.contentTypes.GetByKey(command.ContentTypeKey)
|
||||
if err != nil {
|
||||
return EntryPage{}, err
|
||||
}
|
||||
found, err := u.entries.ListPage(entry.Filters{ContentTypeID: owner.ID, Page: command.Page, Size: command.Size, Sort: command.Sort})
|
||||
if err != nil {
|
||||
return EntryPage{}, err
|
||||
}
|
||||
items := make([]TypedEntry, 0, len(found.Items))
|
||||
for _, record := range found.Items {
|
||||
items = append(items, typedEntry(owner, record))
|
||||
}
|
||||
return EntryPage{Items: items, Total: found.Total, Page: command.Page, Size: command.Size}, nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UpdateContentTypeUseCase struct {
|
||||
contentTypes contenttype.Repository
|
||||
entries entry.Repository
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type UpdateContentTypeCommand struct {
|
||||
Key string
|
||||
Name string
|
||||
Fields []contenttype.Field
|
||||
}
|
||||
|
||||
func NewUpdateContentTypeUseCase(contentTypes contenttype.Repository, entries entry.Repository, now func() time.Time) *UpdateContentTypeUseCase {
|
||||
return &UpdateContentTypeUseCase{contentTypes, entries, now}
|
||||
}
|
||||
|
||||
func (u *UpdateContentTypeUseCase) Update(command UpdateContentTypeCommand) (contenttype.ContentType, error) {
|
||||
current, err := u.contentTypes.GetByKey(command.Key)
|
||||
if err != nil {
|
||||
return contenttype.ContentType{}, err
|
||||
}
|
||||
if err := contenttype.ValidateName(command.Name); err != nil {
|
||||
return contenttype.ContentType{}, err
|
||||
}
|
||||
fields, err := contenttype.ValidateFields(command.Fields)
|
||||
if err != nil {
|
||||
return contenttype.ContentType{}, err
|
||||
}
|
||||
stored, err := u.storedEntries(current.ID)
|
||||
if err != nil {
|
||||
return contenttype.ContentType{}, err
|
||||
}
|
||||
if len(stored) > 0 {
|
||||
if err := checkCompatibility(current.Fields, fields, stored); err != nil {
|
||||
return contenttype.ContentType{}, err
|
||||
}
|
||||
}
|
||||
current.Name = command.Name
|
||||
current.Fields = fields
|
||||
current.UpdatedAt = u.now()
|
||||
return u.contentTypes.Update(current)
|
||||
}
|
||||
|
||||
func (u *UpdateContentTypeUseCase) storedEntries(contentTypeID uint32) ([]entry.Entry, error) {
|
||||
found, err := u.entries.ListPage(entry.Filters{ContentTypeID: contentTypeID, Unpaged: true, Sort: entry.DefaultSort()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return found.Items, nil
|
||||
}
|
||||
|
||||
func checkCompatibility(previous []contenttype.Field, next []contenttype.Field, stored []entry.Entry) error {
|
||||
if fieldTypeChanged(previous, next) {
|
||||
return contenttype.ErrInvalidField
|
||||
}
|
||||
seen := make(map[string][]any)
|
||||
for _, record := range stored {
|
||||
data := entry.ApplyDefaults(next, entry.Project(next, record.Data))
|
||||
if entry.Validate(next, data) != nil {
|
||||
return contenttype.ErrSchemaIncompatible
|
||||
}
|
||||
if collides(seen, entry.UniqueValues(next, data)) {
|
||||
return contenttype.ErrSchemaIncompatible
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fieldTypeChanged(previous []contenttype.Field, next []contenttype.Field) bool {
|
||||
types := make(map[string]contenttype.FieldType, len(previous))
|
||||
for _, field := range previous {
|
||||
types[field.Key] = field.Type
|
||||
}
|
||||
for _, field := range next {
|
||||
if before, existed := types[field.Key]; existed && before != field.Type {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func collides(seen map[string][]any, values []entry.UniqueValue) bool {
|
||||
for _, candidate := range values {
|
||||
for _, earlier := range seen[candidate.Key] {
|
||||
if entry.SameValue(earlier, candidate.Value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
seen[candidate.Key] = append(seen[candidate.Key], candidate.Value)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UpdateEntryUseCase struct {
|
||||
contentTypes contenttype.Repository
|
||||
entries entry.Repository
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type UpdateEntryCommand struct {
|
||||
ContentTypeKey string
|
||||
ID uint32
|
||||
Data map[string]any
|
||||
UserID uint32
|
||||
}
|
||||
|
||||
func NewUpdateEntryUseCase(contentTypes contenttype.Repository, entries entry.Repository, now func() time.Time) *UpdateEntryUseCase {
|
||||
return &UpdateEntryUseCase{contentTypes, entries, now}
|
||||
}
|
||||
|
||||
func (u *UpdateEntryUseCase) Update(command UpdateEntryCommand) (TypedEntry, error) {
|
||||
owner, err := u.contentTypes.GetByKey(command.ContentTypeKey)
|
||||
if err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
current, err := u.entries.Get(owner.ID, command.ID)
|
||||
if err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
if err := entry.Validate(owner.Fields, command.Data); err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
if err := ensureUnique(u.entries, owner, command.Data, current.ID); err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
current.Data = command.Data
|
||||
current.UpdatedBy = command.UserID
|
||||
current.UpdatedAt = u.now()
|
||||
stored, err := u.entries.Update(current)
|
||||
if err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
return typedEntry(owner, stored), nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
)
|
||||
|
||||
type TypedEntry struct {
|
||||
entry.Entry
|
||||
ContentTypeKey string
|
||||
}
|
||||
|
||||
func typedEntry(owner contenttype.ContentType, record entry.Entry) TypedEntry {
|
||||
record.Data = entry.Project(owner.Fields, record.Data)
|
||||
return TypedEntry{Entry: record, ContentTypeKey: owner.Key}
|
||||
}
|
||||
|
||||
func ensureUnique(entries entry.Repository, owner contenttype.ContentType, data map[string]any, excludeID uint32) error {
|
||||
for _, candidate := range entry.UniqueValues(owner.Fields, data) {
|
||||
taken, err := entries.FindByFieldValue(owner.ID, candidate.Key, candidate.Value, excludeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(taken) > 0 {
|
||||
return entry.UniqueViolation(candidate.Key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user