Files
sendico/api/pkg/model/value.go
Stephan D 62a6631b9a
All checks were successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
service backend
2025-11-07 18:35:26 +01:00

752 lines
22 KiB
Go

// file: model/value.go
package model
import (
"time"
"github.com/mitchellh/mapstructure"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"github.com/tech/sendico/pkg/merrors"
)
// ----------------------------
// Assignment model (domain)
// ----------------------------
type Value struct {
PermissionBound `bson:",inline" json:",inline"`
Target ObjectRef `bson:"target" json:"target"`
Type PropertyType `bson:"type" json:"type"`
Cardinality Cardinality `bson:"cardinality" json:"cardinality"`
PropertySchemaRef primitive.ObjectID `bson:"propertySchemaRef" json:"propertySchemaRef"`
// Small typed shape via keys like: "string"/"strings", "integer"/"integers", etc.
Values SettingsT `bson:"data" json:"data" yaml:"data"`
}
type Money struct {
Amount primitive.Decimal128 `bson:"amount" json:"amount"`
Currency Currency `bson:"currency" json:"currency"`
}
type Object = map[string]Value
// ----------------------------
// SINGLE getters
// ----------------------------
func (v *Value) AsString() (string, error) {
if v.Type != PTString {
return "", invalidType(PTString, v.Type)
}
if v.Cardinality != One {
return "", merrors.InvalidArgument("invalid cardinality: expected one")
}
type payload struct {
Value string `mapstructure:"string" bson:"string" json:"string" yaml:"string"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return "", err
}
return p.Value, nil
}
func (v *Value) AsColor() (string, error) {
if v.Type != PTColor {
return "", invalidType(PTColor, v.Type)
}
if v.Cardinality != One {
return "", merrors.InvalidArgument("invalid cardinality: expected one")
}
type payload struct {
Value string `mapstructure:"color" bson:"color" json:"color" yaml:"color"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return "", err
}
return p.Value, nil
}
func (v *Value) AsInteger() (int64, error) {
if v.Type != PTInteger {
return 0, invalidType(PTInteger, v.Type)
}
if v.Cardinality != One {
return 0, merrors.InvalidArgument("invalid cardinality: expected one")
}
type payload struct {
Value int64 `mapstructure:"integer" bson:"integer" json:"integer" yaml:"integer"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return 0, err
}
return p.Value, nil
}
func (v *Value) AsFloat() (float64, error) {
if v.Type != PTFloat {
return 0, invalidType(PTFloat, v.Type)
}
if v.Cardinality != One {
return 0, merrors.InvalidArgument("invalid cardinality: expected one")
}
type payload struct {
Value float64 `mapstructure:"float" bson:"float" json:"float" yaml:"float"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return 0, err
}
return p.Value, nil
}
func (v *Value) AsDateTime() (time.Time, error) {
if v.Type != PTDateTime {
return time.Time{}, invalidType(PTDateTime, v.Type)
}
if v.Cardinality != One {
return time.Time{}, merrors.InvalidArgument("invalid cardinality: expected one")
}
type payload struct {
Value time.Time `mapstructure:"date_time" bson:"date_time" json:"date_time" yaml:"date_time"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return time.Time{}, err
}
return p.Value, nil
}
func (v *Value) AsMonetary() (Money, error) {
if v.Type != PTMonetary {
return Money{}, invalidType(PTMonetary, v.Type)
}
if v.Cardinality != One {
return Money{}, merrors.InvalidArgument("invalid cardinality: expected one")
}
type payload struct {
Value Money `mapstructure:"monetary" bson:"monetary" json:"monetary" yaml:"monetary"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return Money{}, err
}
return p.Value, nil
}
func (v *Value) AsReference() (primitive.ObjectID, error) {
if v.Type != PTReference {
return primitive.NilObjectID, invalidType(PTReference, v.Type)
}
if v.Cardinality != One {
return primitive.NilObjectID, merrors.InvalidArgument("invalid cardinality: expected one")
}
type payload struct {
Value primitive.ObjectID `mapstructure:"reference" bson:"reference" json:"reference" yaml:"reference"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return primitive.NilObjectID, err
}
return p.Value, nil
}
func (v *Value) AsObject() (Object, error) {
if v.Type != PTObject {
return nil, invalidType(PTObject, v.Type)
}
if v.Cardinality != One {
return nil, merrors.InvalidArgument("invalid cardinality: expected one")
}
type payload struct {
Value Object `mapstructure:"object" bson:"object" json:"object" yaml:"object"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return nil, err
}
return p.Value, nil
}
// ----------------------------
// ARRAY getters
// ----------------------------
func (v *Value) AsStrings() ([]string, error) {
if v.Type != PTString {
return nil, invalidType(PTString, v.Type)
}
if v.Cardinality != Many {
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
}
type payload struct {
Values []string `mapstructure:"strings" bson:"strings" json:"strings" yaml:"strings"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return nil, err
}
return p.Values, nil
}
func (v *Value) AsColors() ([]string, error) {
if v.Type != PTColor {
return nil, invalidType(PTColor, v.Type)
}
if v.Cardinality != Many {
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
}
type payload struct {
Values []string `mapstructure:"colors" bson:"colors" json:"colors" yaml:"colors"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return nil, err
}
return p.Values, nil
}
func (v *Value) AsIntegers() ([]int64, error) {
if v.Type != PTInteger {
return nil, invalidType(PTInteger, v.Type)
}
if v.Cardinality != Many {
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
}
type payload struct {
Values []int64 `mapstructure:"integers" bson:"integers" json:"integers" yaml:"integers"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return nil, err
}
return p.Values, nil
}
func (v *Value) AsFloats() ([]float64, error) {
if v.Type != PTFloat {
return nil, invalidType(PTFloat, v.Type)
}
if v.Cardinality != Many {
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
}
type payload struct {
Values []float64 `mapstructure:"floats" bson:"floats" json:"floats" yaml:"floats"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return nil, err
}
return p.Values, nil
}
func (v *Value) AsDateTimes() ([]time.Time, error) {
if v.Type != PTDateTime {
return nil, invalidType(PTDateTime, v.Type)
}
if v.Cardinality != Many {
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
}
type payload struct {
Values []time.Time `mapstructure:"date_times" bson:"date_times" json:"date_times" yaml:"date_times"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return nil, err
}
return p.Values, nil
}
func (v *Value) AsMonetaries() ([]Money, error) {
if v.Type != PTMonetary {
return nil, invalidType(PTMonetary, v.Type)
}
if v.Cardinality != Many {
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
}
type payload struct {
Values []Money `mapstructure:"monetaries" bson:"monetaries" json:"monetaries" yaml:"monetaries"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return nil, err
}
return p.Values, nil
}
func (v *Value) AsReferences() ([]primitive.ObjectID, error) {
if v.Type != PTReference {
return nil, invalidType(PTReference, v.Type)
}
if v.Cardinality != Many {
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
}
type payload struct {
Values []primitive.ObjectID `mapstructure:"references" bson:"references" json:"references" yaml:"references"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return nil, err
}
return p.Values, nil
}
func (v *Value) AsObjects() ([]Object, error) {
if v.Type != PTObject {
return nil, invalidType(PTObject, v.Type)
}
if v.Cardinality != Many {
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
}
type payload struct {
Values []Object `mapstructure:"objects" bson:"objects" json:"objects" yaml:"objects"`
}
var p payload
if err := mapstructure.Decode(v.Values, &p); err != nil {
return nil, err
}
return p.Values, nil
}
// ----------------------------
// FACTORIES (scheme + value)
// ----------------------------
// Strings
func NewStringValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v string) (Value, error) {
if scheme.Type != PTString {
return Value{}, invalidType(PTString, scheme.Type)
}
if err := scheme.ValidateStrings([]string{v}); err != nil {
return Value{}, err
}
return Value{
PermissionBound: scope,
Target: target,
Type: PTString,
Cardinality: One,
PropertySchemaRef: scheme.ID,
Values: SettingsT{VKString: v},
}, nil
}
func NewStringsValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []string) (Value, error) {
if scheme.Type != PTString {
return Value{}, invalidType(PTString, scheme.Type)
}
if err := scheme.ValidateStrings(vv); err != nil {
return Value{}, err
}
return Value{
PermissionBound: scope,
Target: target,
Type: PTString,
Cardinality: Many,
PropertySchemaRef: scheme.ID,
Values: SettingsT{VKStrings: vv},
}, nil
}
// Colors
func NewColorValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v string) (Value, error) {
if scheme.Type != PTColor {
return Value{}, invalidType(PTColor, scheme.Type)
}
if err := scheme.ValidateColors([]string{v}); err != nil {
return Value{}, err
}
return Value{scope, target, PTColor, One, scheme.ID, SettingsT{VKColor: v}}, nil
}
func NewColorsValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []string) (Value, error) {
if scheme.Type != PTColor {
return Value{}, invalidType(PTColor, scheme.Type)
}
if err := scheme.ValidateColors(vv); err != nil {
return Value{}, err
}
return Value{scope, target, PTColor, Many, scheme.ID, SettingsT{VKColors: vv}}, nil
}
// Integers
func NewIntegerValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v int64) (Value, error) {
if scheme.Type != PTInteger {
return Value{}, invalidType(PTInteger, scheme.Type)
}
if err := scheme.ValidateIntegers([]int64{v}); err != nil {
return Value{}, err
}
return Value{scope, target, PTInteger, One, scheme.ID, SettingsT{VKInteger: v}}, nil
}
func NewIntegersValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []int64) (Value, error) {
if scheme.Type != PTInteger {
return Value{}, invalidType(PTInteger, scheme.Type)
}
if err := scheme.ValidateIntegers(vv); err != nil {
return Value{}, err
}
return Value{scope, target, PTInteger, Many, scheme.ID, SettingsT{VKIntegers: vv}}, nil
}
// Floats
func NewFloatValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v float64) (Value, error) {
if scheme.Type != PTFloat {
return Value{}, invalidType(PTFloat, scheme.Type)
}
if err := scheme.ValidateFloats([]float64{v}); err != nil {
return Value{}, err
}
return Value{scope, target, PTFloat, One, scheme.ID, SettingsT{VKFloat: v}}, nil
}
func NewFloatsValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []float64) (Value, error) {
if scheme.Type != PTFloat {
return Value{}, invalidType(PTFloat, scheme.Type)
}
if err := scheme.ValidateFloats(vv); err != nil {
return Value{}, err
}
return Value{scope, target, PTFloat, Many, scheme.ID, SettingsT{VKFloats: vv}}, nil
}
// DateTimes
func NewDateTimeValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v time.Time) (Value, error) {
if scheme.Type != PTDateTime {
return Value{}, invalidType(PTDateTime, scheme.Type)
}
if err := scheme.ValidateDateTimes([]time.Time{v}); err != nil {
return Value{}, err
}
return Value{scope, target, PTDateTime, One, scheme.ID, SettingsT{VKDateTime: v}}, nil
}
func NewDateTimesValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []time.Time) (Value, error) {
if scheme.Type != PTDateTime {
return Value{}, invalidType(PTDateTime, scheme.Type)
}
if err := scheme.ValidateDateTimes(vv); err != nil {
return Value{}, err
}
return Value{scope, target, PTDateTime, Many, scheme.ID, SettingsT{VKDateTimes: vv}}, nil
}
// Monetary (needs org currency for validation if required by scheme)
func NewMonetaryValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v Money, orgCurrency Currency) (Value, error) {
if scheme.Type != PTMonetary {
return Value{}, invalidType(PTMonetary, scheme.Type)
}
if err := scheme.ValidateMonetaries([]Money{v}, orgCurrency); err != nil {
return Value{}, err
}
return Value{scope, target, PTMonetary, One, scheme.ID, SettingsT{VKMonetary: v}}, nil
}
func NewMonetariesValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []Money, orgCurrency Currency) (Value, error) {
if scheme.Type != PTMonetary {
return Value{}, invalidType(PTMonetary, scheme.Type)
}
if err := scheme.ValidateMonetaries(vv, orgCurrency); err != nil {
return Value{}, err
}
return Value{scope, target, PTMonetary, Many, scheme.ID, SettingsT{VKMonetaries: vv}}, nil
}
// References (needs exist-fn)
func NewReferenceValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v primitive.ObjectID, exist ExistFn) (Value, error) {
if scheme.Type != PTReference {
return Value{}, invalidType(PTReference, scheme.Type)
}
if err := scheme.ValidateReferences([]primitive.ObjectID{v}, exist); err != nil {
return Value{}, err
}
return Value{scope, target, PTReference, One, scheme.ID, SettingsT{VKReference: v}}, nil
}
func NewReferencesValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []primitive.ObjectID, exist ExistFn) (Value, error) {
if scheme.Type != PTReference {
return Value{}, invalidType(PTReference, scheme.Type)
}
if err := scheme.ValidateReferences(vv, exist); err != nil {
return Value{}, err
}
return Value{scope, target, PTReference, Many, scheme.ID, SettingsT{VKReferences: vv}}, nil
}
// Objects (opaque maps)
func NewObjectValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v Object) (Value, error) {
if scheme.Type != PTObject {
return Value{}, invalidType(PTObject, scheme.Type)
}
// Add your own ValidateObject if needed
return Value{scope, target, PTObject, One, scheme.ID, SettingsT{VKObject: v}}, nil
}
func NewObjectsValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []Object) (Value, error) {
if scheme.Type != PTObject {
return Value{}, invalidType(PTObject, scheme.Type)
}
return Value{scope, target, PTObject, Many, scheme.ID, SettingsT{VKObjects: vv}}, nil
}
// ----------------------------
// Custom BSON Marshalers/Unmarshalers
// ----------------------------
// MarshalBSON implements bson.Marshaler to ensure proper serialization
func (v Value) MarshalBSON() ([]byte, error) {
// Create a temporary struct that preserves the exact structure
temp := struct {
PermissionBound `bson:",inline"`
Target ObjectRef `bson:"target"`
Type PropertyType `bson:"type"`
Cardinality Cardinality `bson:"cardinality"`
PropertySchemaRef primitive.ObjectID `bson:"propertySchemaRef"`
Values SettingsTWrapper `bson:"data"`
}{
PermissionBound: v.PermissionBound,
Target: v.Target,
Type: v.Type,
Cardinality: v.Cardinality,
PropertySchemaRef: v.PropertySchemaRef,
Values: SettingsTWrapper(v.Values),
}
return bson.Marshal(temp)
}
// UnmarshalBSON implements bson.Unmarshaler to ensure proper deserialization
func (v *Value) UnmarshalBSON(data []byte) error {
// Create a temporary struct that matches the BSON structure
temp := struct {
PermissionBound `bson:",inline"`
Target ObjectRef `bson:"target"`
Type PropertyType `bson:"type"`
Cardinality Cardinality `bson:"cardinality"`
PropertySchemaRef primitive.ObjectID `bson:"propertySchemaRef"`
Values SettingsTWrapper `bson:"data"`
}{}
if err := bson.Unmarshal(data, &temp); err != nil {
return err
}
// Copy the values back to the original struct
v.PermissionBound = temp.PermissionBound
v.Target = temp.Target
v.Type = temp.Type
v.Cardinality = temp.Cardinality
v.PropertySchemaRef = temp.PropertySchemaRef
v.Values = SettingsT(temp.Values)
return nil
}
// ----------------------------
// Custom BSON Marshalers for SettingsT
// ----------------------------
// SettingsT is a type alias, so we need to define a wrapper type for methods
type SettingsTWrapper SettingsT
// MarshalBSON implements bson.Marshaler for SettingsT to preserve exact types
func (s SettingsTWrapper) MarshalBSON() ([]byte, error) {
// Convert SettingsT to bson.M to preserve exact types
doc := bson.M{}
for key, value := range s {
doc[key] = value
}
return bson.Marshal(doc)
}
// UnmarshalBSON implements bson.Unmarshaler for SettingsT to preserve exact types
func (s *SettingsTWrapper) UnmarshalBSON(data []byte) error {
// Unmarshal into a generic map first
var doc bson.M
if err := bson.Unmarshal(data, &doc); err != nil {
return err
}
// Convert back to SettingsT, preserving types
*s = make(SettingsT)
for key, value := range doc {
// Handle special cases where BSON converts types
switch v := value.(type) {
case primitive.A:
// Convert primitive.A back to appropriate slice type
if len(v) > 0 {
switch v[0].(type) {
case string:
strings := make([]string, len(v))
for i, item := range v {
strings[i] = item.(string)
}
(*s)[key] = strings
case int32, int64:
ints := make([]int64, len(v))
for i, item := range v {
switch val := item.(type) {
case int32:
ints[i] = int64(val)
case int64:
ints[i] = val
}
}
(*s)[key] = ints
case float32, float64:
floats := make([]float64, len(v))
for i, item := range v {
switch val := item.(type) {
case float32:
floats[i] = float64(val)
case float64:
floats[i] = val
}
}
(*s)[key] = floats
case primitive.DateTime:
times := make([]time.Time, len(v))
for i, item := range v {
times[i] = item.(primitive.DateTime).Time().Truncate(time.Millisecond)
}
(*s)[key] = times
case primitive.ObjectID:
refs := make([]primitive.ObjectID, len(v))
for i, item := range v {
refs[i] = item.(primitive.ObjectID)
}
(*s)[key] = refs
case bson.M:
// Handle nested objects (Money, Object, etc.)
if key == VKMonetaries {
// Handle Money slice
moneys := make([]Money, len(v))
for i, item := range v {
if itemMap, ok := item.(bson.M); ok {
var money Money
if amount, ok := itemMap[MKAmount].(primitive.Decimal128); ok {
money.Amount = amount
}
if currency, ok := itemMap[MKCurrency].(string); ok {
money.Currency = Currency(currency)
}
moneys[i] = money
}
}
(*s)[key] = moneys
} else {
// Handle Object slice
objects := make([]Object, len(v))
for i, item := range v {
obj := make(Object)
for k, val := range item.(bson.M) {
// Recursively handle nested Values
if valMap, ok := val.(bson.M); ok {
var nestedValue Value
if data, err := bson.Marshal(valMap); err == nil {
if err := bson.Unmarshal(data, &nestedValue); err == nil {
obj[k] = nestedValue
}
}
}
}
objects[i] = obj
}
(*s)[key] = objects
}
default:
// Fallback: keep as primitive.A
(*s)[key] = v
}
} else {
// Empty array - determine type from key name
switch key {
case VKStrings, VKColors:
(*s)[key] = []string{}
case VKIntegers:
(*s)[key] = []int64{}
case VKFloats:
(*s)[key] = []float64{}
case VKDateTimes:
(*s)[key] = []time.Time{}
case VKReferences:
(*s)[key] = []primitive.ObjectID{}
case VKMonetaries:
(*s)[key] = []Money{}
case VKObjects:
(*s)[key] = []Object{}
default:
(*s)[key] = []interface{}{}
}
}
case primitive.DateTime:
// Convert primitive.DateTime back to time.Time and truncate to millisecond precision
(*s)[key] = v.Time().Truncate(time.Millisecond)
case int64:
// Handle time.Time that gets converted to int64 (Unix timestamp)
if key == VKDateTime {
(*s)[key] = time.Unix(v, 0).UTC().Truncate(time.Millisecond)
} else {
(*s)[key] = v
}
case bson.M:
// Handle nested objects
if key == VKMonetary {
// Handle Money struct
var money Money
if amount, ok := v[MKAmount].(primitive.Decimal128); ok {
money.Amount = amount
}
if currency, ok := v[MKCurrency].(string); ok {
money.Currency = Currency(currency)
}
(*s)[key] = money
} else if key == VKMonetaries {
// Handle Money slice - this shouldn't happen in single values
(*s)[key] = v
} else if key == VKObject {
// Handle Object type
obj := make(Object)
for k, val := range v {
if valMap, ok := val.(bson.M); ok {
var nestedValue Value
if data, err := bson.Marshal(valMap); err == nil {
if err := bson.Unmarshal(data, &nestedValue); err == nil {
obj[k] = nestedValue
}
}
}
}
(*s)[key] = obj
} else {
// Generic map
(*s)[key] = v
}
case nil:
// Handle nil values - determine type from key name
switch key {
case VKStrings, VKColors:
(*s)[key] = []string(nil)
case VKIntegers:
(*s)[key] = []int64(nil)
case VKFloats:
(*s)[key] = []float64(nil)
case VKDateTimes:
(*s)[key] = []time.Time(nil)
case VKReferences:
(*s)[key] = []primitive.ObjectID(nil)
case VKMonetaries:
(*s)[key] = []Money(nil)
case VKObjects:
(*s)[key] = []Object(nil)
default:
(*s)[key] = nil
}
default:
// Keep as-is for primitive types
(*s)[key] = value
}
}
return nil
}