feat: relation field and picker
This commit is contained in:
1 parent
b45c99be3b
commit
37a77c9c22
92 files changed
+6826
-80
No files matched your search
@@ -13,6 +13,7 @@ var (
|
||||
ErrInvalidIcon = errors.New("invalid content type icon")
|
||||
ErrInvalidField = errors.New("invalid field")
|
||||
ErrHasEntries = errors.New("the content type still has entries")
|
||||
ErrReferenced = errors.New("the content type is the target of a relation field")
|
||||
ErrSchemaIncompatible = errors.New("the schema is incompatible with the stored entries")
|
||||
)
|
||||
|
||||
@@ -26,6 +27,7 @@ const (
|
||||
TypeDate FieldType = "date"
|
||||
TypeSelect FieldType = "select"
|
||||
TypeMedia FieldType = "media"
|
||||
TypeRelation FieldType = "relation"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,6 +56,7 @@ type Field struct {
|
||||
Min any `json:"min"`
|
||||
Max any `json:"max"`
|
||||
Options []string `json:"options"`
|
||||
Relation *Relation `json:"relation"`
|
||||
}
|
||||
|
||||
type ContentType struct {
|
||||
|
||||
@@ -21,6 +21,7 @@ var kinds = map[FieldType]kind{
|
||||
TypeDate: {accepts: isDate, measure: dateDays, bound: dateDays, unique: true},
|
||||
TypeSelect: {accepts: isString, unique: true},
|
||||
TypeMedia: {accepts: isMediaID},
|
||||
TypeRelation: {},
|
||||
}
|
||||
|
||||
func MediaID(value any) (uint32, bool) {
|
||||
|
||||
@@ -63,7 +63,7 @@ func (f Field) validSchema() bool {
|
||||
if f.Unique && !fieldKind.unique {
|
||||
return false
|
||||
}
|
||||
return f.validOptions() && f.validBounds(fieldKind) && f.validDefault()
|
||||
return f.validRelation() && f.validOptions() && f.validBounds(fieldKind) && f.validDefault()
|
||||
}
|
||||
|
||||
func (f Field) validOptions() bool {
|
||||
@@ -109,7 +109,7 @@ func (f Field) validDefault() bool {
|
||||
if f.Default == nil {
|
||||
return true
|
||||
}
|
||||
if f.Type == TypeMedia {
|
||||
if f.Type == TypeMedia || f.Type == TypeRelation {
|
||||
return false
|
||||
}
|
||||
return !f.Unique && f.Violation(f.Default) == ""
|
||||
|
||||
@@ -3,6 +3,9 @@ package contenttype
|
||||
import "slices"
|
||||
|
||||
func (f Field) Violation(value any) string {
|
||||
if f.Type == TypeRelation {
|
||||
return f.relationViolation(value)
|
||||
}
|
||||
fieldKind, known := kinds[f.Type]
|
||||
if !known || !fieldKind.accepts(value) {
|
||||
return RuleType
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package contenttype
|
||||
|
||||
const MaxRelationIDs = 50
|
||||
|
||||
type RelationKind string
|
||||
|
||||
const (
|
||||
RelationOneToOne RelationKind = "one_to_one"
|
||||
RelationOneToMany RelationKind = "one_to_many"
|
||||
RelationManyToMany RelationKind = "many_to_many"
|
||||
)
|
||||
|
||||
type Relation struct {
|
||||
ContentType string `json:"content_type"`
|
||||
Kind RelationKind `json:"kind"`
|
||||
}
|
||||
|
||||
func (r Relation) validKind() bool {
|
||||
return r.Kind == RelationOneToOne || r.Kind == RelationOneToMany || r.Kind == RelationManyToMany
|
||||
}
|
||||
|
||||
func (f Field) validRelation() bool {
|
||||
if f.Type != TypeRelation {
|
||||
return f.Relation == nil
|
||||
}
|
||||
return f.Relation != nil && f.Relation.ContentType != "" && f.Relation.validKind()
|
||||
}
|
||||
|
||||
func (f Field) IsRelation() bool {
|
||||
return f.Type == TypeRelation && f.Relation != nil
|
||||
}
|
||||
|
||||
func (f Field) HoldsManyRelations() bool {
|
||||
return f.IsRelation() && f.Relation.Kind == RelationManyToMany
|
||||
}
|
||||
|
||||
func (f Field) IsExclusiveRelation() bool {
|
||||
return f.IsRelation() && f.Relation.Kind == RelationOneToOne
|
||||
}
|
||||
|
||||
func (f Field) RelationIDs(value any) ([]uint32, bool) {
|
||||
if value == nil {
|
||||
return []uint32{}, true
|
||||
}
|
||||
if !f.HoldsManyRelations() {
|
||||
id, ok := MediaID(value)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return []uint32{id}, true
|
||||
}
|
||||
return manyRelationIDs(value)
|
||||
}
|
||||
|
||||
func manyRelationIDs(value any) ([]uint32, bool) {
|
||||
var items []any
|
||||
switch typed := value.(type) {
|
||||
case []any:
|
||||
items = typed
|
||||
case []uint32:
|
||||
for _, id := range typed {
|
||||
items = append(items, id)
|
||||
}
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
if len(items) > MaxRelationIDs {
|
||||
return nil, false
|
||||
}
|
||||
ids := make([]uint32, 0, len(items))
|
||||
seen := make(map[uint32]bool, len(items))
|
||||
for _, item := range items {
|
||||
id, ok := MediaID(item)
|
||||
if !ok || seen[id] {
|
||||
return nil, false
|
||||
}
|
||||
seen[id] = true
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, true
|
||||
}
|
||||
|
||||
func (f Field) relationViolation(value any) string {
|
||||
if _, ok := f.RelationIDs(value); !ok {
|
||||
return RuleType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func EmptyRelation(field Field, value any) bool {
|
||||
if !field.HoldsManyRelations() {
|
||||
return false
|
||||
}
|
||||
ids, ok := field.RelationIDs(value)
|
||||
return ok && len(ids) == 0
|
||||
}
|
||||
|
||||
type RelationRef struct {
|
||||
Field Field
|
||||
IDs []uint32
|
||||
}
|
||||
|
||||
func RelationRefs(fields []Field, data map[string]any) []RelationRef {
|
||||
var refs []RelationRef
|
||||
for _, field := range RelationFields(fields) {
|
||||
if ids, ok := field.RelationIDs(data[field.Key]); ok && len(ids) > 0 {
|
||||
refs = append(refs, RelationRef{Field: field, IDs: ids})
|
||||
}
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
func RelationFields(fields []Field) []Field {
|
||||
var relations []Field
|
||||
for _, field := range fields {
|
||||
if field.IsRelation() {
|
||||
relations = append(relations, field)
|
||||
}
|
||||
}
|
||||
return relations
|
||||
}
|
||||
|
||||
func ReferencesType(fields []Field, key string) bool {
|
||||
for _, field := range RelationFields(fields) {
|
||||
if field.Relation.ContentType == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func SameRelation(left Field, right Field) bool {
|
||||
return left.IsRelation() && right.IsRelation() && *left.Relation == *right.Relation
|
||||
}
|
||||
Reference in new issue
Block a user