diff --git a/internal/handler/rule.go b/internal/handler/rule.go index dfaca2f..f66059d 100644 --- a/internal/handler/rule.go +++ b/internal/handler/rule.go @@ -1,6 +1,7 @@ package handler import ( + "context" "encoding/json" "net/http" "strconv" @@ -78,7 +79,7 @@ func (h *RuleHandler) Create(c *gin.Context) { c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) return } - + rule.Channels = h.loadRuleChannels(c.Request.Context(), rule.ID) c.JSON(http.StatusCreated, rule) } @@ -95,6 +96,10 @@ func (h *RuleHandler) List(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + if err := h.fillRuleChannels(c, rules); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } c.JSON(http.StatusOK, gin.H{"data": rules, "total": total, "page": page.Page}) } @@ -105,9 +110,78 @@ func (h *RuleHandler) Get(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{"error": "rule not found"}) return } + rule.Channels = h.loadRuleChannels(c.Request.Context(), id) c.JSON(http.StatusOK, rule) } +// loadRuleChannels returns all channels bound to a rule, each with the per-rule +// enabled switch, ordered by rule_channel id. +func (h *RuleHandler) loadRuleChannels(ctx context.Context, ruleID int) []model.RuleChannelItem { + byRule, err := h.store.ListRuleChannels(ctx, []int{ruleID}) + if err != nil || len(byRule) == 0 { + return []model.RuleChannelItem{} + } + rcs := byRule[ruleID] + if len(rcs) == 0 { + return []model.RuleChannelItem{} + } + items := make([]model.RuleChannelItem, 0, len(rcs)) + for _, rc := range rcs { + ch, err := h.store.GetChannel(ctx, rc.ChannelID) + if err != nil { + continue + } + items = append(items, model.RuleChannelItem{ + ID: ch.ID, + Name: ch.Name, + Type: ch.Type, + Enabled: rc.Enabled, + }) + } + return items +} + +// fillRuleChannels bulk-loads bound channels for rules and attaches them. +func (h *RuleHandler) fillRuleChannels(ctx context.Context, rules []model.Rule) error { + if len(rules) == 0 { + return nil + } + ids := make([]int, 0, len(rules)) + for i := range rules { + ids = append(ids, rules[i].ID) + } + byRule, err := h.store.ListRuleChannels(ctx, ids) + if err != nil { + return err + } + if len(byRule) == 0 { + return nil + } + for i := range rules { + r := &rules[i] + rcs := byRule[r.ID] + if len(rcs) == 0 { + r.Channels = []model.RuleChannelItem{} + continue + } + items := make([]model.RuleChannelItem, 0, len(rcs)) + for _, rc := range rcs { + ch, err := h.store.GetChannel(ctx, rc.ChannelID) + if err != nil { + continue + } + items = append(items, model.RuleChannelItem{ + ID: ch.ID, + Name: ch.Name, + Type: ch.Type, + Enabled: rc.Enabled, + }) + } + r.Channels = items + } + return nil +} + func (h *RuleHandler) Update(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var req createRuleReq diff --git a/internal/model/model.go b/internal/model/model.go index 7ef32c9..8a246eb 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -35,6 +35,13 @@ type Channel struct { UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } +type RuleChannelItem struct { + ID int `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Enabled int `json:"enabled"` +} + type Rule struct { ID int `db:"id" json:"id"` Name string `db:"name" json:"name"` @@ -45,6 +52,9 @@ type Rule struct { Enabled int `db:"enabled" json:"enabled"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + // Channels holds the channels bound to this rule. Populated by handlers when + // reading rules; each item includes the per-rule enabled switch. + Channels []RuleChannelItem `json:"channels"` } type Condition struct { diff --git a/internal/store/rule.go b/internal/store/rule.go index 9793b6a..c4eea58 100644 --- a/internal/store/rule.go +++ b/internal/store/rule.go @@ -7,6 +7,8 @@ import ( "fmt" "aiaa-notification-service/internal/model" + + "github.com/jmoiron/sqlx" ) func (s *Store) CreateRule(ctx context.Context, r *model.Rule, channelIDs []int) error { @@ -158,6 +160,30 @@ func (s *Store) GetRuleChannels(ctx context.Context, ruleID int) ([]model.RuleCh return rcs, nil } +// ListRuleChannels maps each rule to its bound channels (regardless of enabled +// state) and the per-rule enabled switch. +func (s *Store) ListRuleChannels(ctx context.Context, ruleIDs []int) (map[int][]model.RuleChannel, error) { + result := make(map[int][]model.RuleChannel, len(ruleIDs)) + if len(ruleIDs) == 0 { + return result, nil + } + + query, args, err := sqlx.In(`SELECT id, rule_id, channel_id, enabled FROM notification_rule_channel WHERE rule_id IN (?)`, ruleIDs) + if err != nil { + return nil, fmt.Errorf("build rule channels query: %w", err) + } + query = s.DB.Rebind(query) + + rcs := make([]model.RuleChannel, 0) + if err := s.DB.SelectContext(ctx, &rcs, query, args...); err != nil { + return nil, fmt.Errorf("list rule channels: %w", err) + } + for _, rc := range rcs { + result[rc.RuleID] = append(result[rc.RuleID], rc) + } + return result, nil +} + func (s *Store) SetRuleChannelEnabled(ctx context.Context, ruleID, channelID int, enabled bool) error { v := 0 if enabled {