package tables import ( "strings" "time" "github.com/bytedance/sonic" bifrost "github.com/maximhq/bifrost/core" "gorm.io/gorm" ) // TableRoutingRule represents a routing rule in the database type TableRoutingRule struct { ID string `gorm:"primaryKey;type:varchar(255)" json:"id"` ConfigHash string `gorm:"type:varchar(255)" json:"config_hash"` // Hash of config.json version, used for change detection Name string `gorm:"type:varchar(255);not null;uniqueIndex:idx_routing_rule_scope_name" json:"name"` Description string `gorm:"type:text" json:"description"` Enabled bool `gorm:"not null;default:true" json:"enabled"` CelExpression string `gorm:"type:text;not null" json:"cel_expression"` // Routing Targets (output) — 1:many relationship; weights must sum to 1 Targets []TableRoutingTarget `gorm:"foreignKey:RuleID;constraint:OnDelete:CASCADE" json:"targets"` Fallbacks *string `gorm:"type:text" json:"-"` // JSON array of fallback chains ParsedFallbacks []string `gorm:"-" json:"fallbacks,omitempty"` // Parsed fallbacks from JSON Query *string `gorm:"type:text" json:"-"` ParsedQuery map[string]any `gorm:"-" json:"query,omitempty"` // Scope: where this rule applies Scope string `gorm:"type:varchar(50);not null;uniqueIndex:idx_routing_rule_scope_name" json:"scope"` // "global" | "team" | "customer" | "virtual_key" ScopeID *string `gorm:"type:varchar(255);uniqueIndex:idx_routing_rule_scope_name" json:"scope_id"` // nil for global, otherwise entity ID // Chaining ChainRule bool `gorm:"not null;default:false" json:"chain_rule"` // If true, re-evaluates routing chain after this rule matches // Execution Priority int `gorm:"type:int;not null;default:0;index" json:"priority"` // Lower = evaluated first within scope // Timestamps CreatedAt time.Time `gorm:"index;not null" json:"created_at"` UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"` } // TableName for TableRoutingRule func (TableRoutingRule) TableName() string { return "routing_rules" } // BeforeSave hook for TableRoutingRule to serialize JSON fields func (r *TableRoutingRule) BeforeSave(tx *gorm.DB) error { if len(r.ParsedFallbacks) > 0 { data, err := sonic.Marshal(r.ParsedFallbacks) if err != nil { return err } r.Fallbacks = bifrost.Ptr(string(data)) } else { r.Fallbacks = nil } if r.ParsedQuery != nil { data, err := sonic.Marshal(r.ParsedQuery) if err != nil { return err } r.Query = bifrost.Ptr(string(data)) } else { r.Query = nil } return nil } // AfterFind hook for TableRoutingRule to deserialize JSON fields func (r *TableRoutingRule) AfterFind(tx *gorm.DB) error { if r.Fallbacks != nil && strings.TrimSpace(*r.Fallbacks) != "" { if err := sonic.Unmarshal([]byte(*r.Fallbacks), &r.ParsedFallbacks); err != nil { return err } } if r.Query != nil && strings.TrimSpace(*r.Query) != "" { if err := sonic.Unmarshal([]byte(*r.Query), &r.ParsedQuery); err != nil { return err } } return nil } // TableRoutingTarget represents a weighted routing target for probabilistic routing. // Multiple targets can be associated with a single routing rule; weights determine // the probability of each target being selected and must sum to 1 across all targets in a rule. // The composite (RuleID, Provider, Model, KeyID) is unique to prevent duplicate target configs. type TableRoutingTarget struct { RuleID string `gorm:"type:varchar(255);not null;index;uniqueIndex:idx_routing_target_config" json:"-"` Provider *string `gorm:"type:varchar(255);uniqueIndex:idx_routing_target_config" json:"provider,omitempty"` // nil = use incoming provider Model *string `gorm:"type:varchar(255);uniqueIndex:idx_routing_target_config" json:"model,omitempty"` // nil = use incoming model KeyID *string `gorm:"type:varchar(255);uniqueIndex:idx_routing_target_config" json:"key_id,omitempty"` // nil = no key pin Weight float64 `gorm:"not null;default:1" json:"weight"` // must sum to 1 across all targets in a rule } // TableName for TableRoutingTarget func (TableRoutingTarget) TableName() string { return "routing_targets" }