feat(API): media field type

This commit is contained in:
Florian Sylvain committed 2026-10-21 05:07:36 +02:00
1 parent ad5faf3d19
commit a2d596f4ca
63 files changed
+2567 -259

No files matched your search

@@ -0,0 +1,190 @@
package useCases
import (
"RenewCMS/internal/domain/contenttype"
"RenewCMS/internal/domain/entry"
"RenewCMS/internal/domain/image"
"cmp"
"errors"
"slices"
)
type EntryMedia struct {
ID uint32
Missing bool
Filename string
Width *int
Height *int
Size int64
ThumbnailURL string
}
type mediaResolver struct {
images image.Repository
cache map[uint32]EntryMedia
}
func newMediaResolver(images image.Repository) *mediaResolver {
return &mediaResolver{images: images, cache: map[uint32]EntryMedia{}}
}
func (r *mediaResolver) resolve(owner contenttype.ContentType, record entry.Entry) (map[string]EntryMedia, error) {
resolved := map[string]EntryMedia{}
for _, field := range contenttype.MediaFields(owner.Fields) {
id, set := contenttype.MediaID(record.Data[field.Key])
if !set {
continue
}
media, err := r.lookup(id)
if err != nil {
return nil, err
}
resolved[field.Key] = media
}
return resolved, nil
}
func (r *mediaResolver) lookup(id uint32) (EntryMedia, error) {
if cached, found := r.cache[id]; found {
return cached, nil
}
found, err := r.images.Get(id)
if errors.Is(err, image.ErrImageNotFound) {
r.cache[id] = EntryMedia{ID: id, Missing: true}
return r.cache[id], nil
}
if err != nil {
return EntryMedia{}, err
}
media := EntryMedia{ID: found.ID, Filename: found.Filename, Width: knownDimension(found.Width), Height: knownDimension(found.Height), Size: found.Size, ThumbnailURL: found.Path}
r.cache[id] = media
return media, nil
}
func knownDimension(value int) *int {
if value <= 0 {
return nil
}
return &value
}
type mediaEntry struct {
owner contenttype.ContentType
record entry.Entry
}
type entryMediaScanner struct {
contentTypes contenttype.Repository
entries entry.Repository
}
func newEntryMediaScanner(contentTypes contenttype.Repository, entries entry.Repository) *entryMediaScanner {
return &entryMediaScanner{contentTypes: contentTypes, entries: entries}
}
func (s *entryMediaScanner) scan() ([]mediaEntry, error) {
types, err := s.contentTypes.List()
if err != nil {
return nil, err
}
var found []mediaEntry
for _, owner := range types {
if len(contenttype.MediaFields(owner.Fields)) == 0 {
continue
}
page, err := s.entries.ListPage(entry.Filters{ContentTypeID: owner.ID, Unpaged: true, Sort: entry.DefaultSort()})
if err != nil {
return nil, err
}
for _, record := range page.Items {
found = append(found, mediaEntry{owner: owner, record: record})
}
}
slices.SortFunc(found, func(a, b mediaEntry) int {
return cmp.Or(cmp.Compare(a.owner.Name, b.owner.Name), cmp.Compare(a.owner.ID, b.owner.ID), cmp.Compare(a.record.ID, b.record.ID))
})
return found, nil
}
func (m mediaEntry) keysPointingAt(ids map[uint32]bool) []string {
keys := []string{}
for _, field := range contenttype.MediaFields(m.owner.Fields) {
if id, set := contenttype.MediaID(m.record.Data[field.Key]); set && ids[id] {
keys = append(keys, field.Key)
}
}
return keys
}
func (s *entryMediaScanner) usageCounts() (map[uint32]int, error) {
found, err := s.scan()
if err != nil {
return nil, err
}
counts := map[uint32]int{}
for _, candidate := range found {
distinct := map[uint32]bool{}
for _, id := range entry.MediaIDs(candidate.owner.Fields, candidate.record.Data) {
distinct[id] = true
}
for id := range distinct {
counts[id]++
}
}
return counts, nil
}
func (s *entryMediaScanner) referencing(imageID uint32) ([]mediaEntry, error) {
found, err := s.scan()
if err != nil {
return nil, err
}
targets := map[uint32]bool{imageID: true}
return slices.DeleteFunc(found, func(candidate mediaEntry) bool {
return len(candidate.keysPointingAt(targets)) == 0
}), nil
}
type entryRepoint struct {
original entry.Entry
updated entry.Entry
}
func (s *entryMediaScanner) repoint(copyIDs []uint32, keptID uint32) ([]entryRepoint, error) {
found, err := s.scan()
if err != nil {
return nil, err
}
copies := map[uint32]bool{}
for _, id := range copyIDs {
copies[id] = true
}
applied := []entryRepoint{}
for _, candidate := range found {
keys := candidate.keysPointingAt(copies)
if len(keys) == 0 {
continue
}
change := entryRepoint{original: candidate.record, updated: candidate.record}
change.updated.Data = make(map[string]any, len(candidate.record.Data))
for key, value := range candidate.record.Data {
change.updated.Data[key] = value
}
for _, key := range keys {
change.updated.Data[key] = float64(keptID)
}
if _, err := s.entries.Update(change.updated); err != nil {
s.restore(applied)
return nil, err
}
applied = append(applied, change)
}
return applied, nil
}
func (s *entryMediaScanner) restore(applied []entryRepoint) {
for _, change := range applied {
_, err := s.entries.Update(change.original)
logCompensation("restore entry media", err)
}
}