package store import ( "database/sql" "errors" "fmt" "strings" ) // CreateEventTrigger inserts a new trigger row. ID is assigned by the // auto-increment column and returned on the populated struct. func (s *Store) CreateEventTrigger(t EventTrigger) (EventTrigger, error) { if strings.TrimSpace(t.Name) == "" { return EventTrigger{}, fmt.Errorf("event_trigger: name is required") } if t.ActionType == "" { t.ActionType = EventTriggerActionWebhook } if t.ActionType != EventTriggerActionWebhook { return EventTrigger{}, fmt.Errorf("event_trigger: unsupported action_type %q", t.ActionType) } if strings.TrimSpace(t.ActionTarget) == "" { return EventTrigger{}, fmt.Errorf("event_trigger: action_target is required") } now := Now() t.CreatedAt = now t.UpdatedAt = now res, err := s.db.Exec( `INSERT INTO event_triggers (name, filter_severity, filter_source, filter_message_regex, action_type, action_target, action_secret, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, t.Name, t.FilterSeverity, t.FilterSource, t.FilterMessageRegex, t.ActionType, t.ActionTarget, t.ActionSecret, boolToInt(t.Enabled), t.CreatedAt, t.UpdatedAt, ) if err != nil { return EventTrigger{}, fmt.Errorf("insert event trigger: %w", err) } id, err := res.LastInsertId() if err != nil { return EventTrigger{}, fmt.Errorf("get event trigger id: %w", err) } t.ID = id return t, nil } // ListEventTriggers returns every trigger row, ordered by id so the UI // rendering is stable across requests. Trigger counts are expected to // be small (operator-curated), so unbounded listing is fine. func (s *Store) ListEventTriggers() ([]EventTrigger, error) { rows, err := s.db.Query( `SELECT id, name, filter_severity, filter_source, filter_message_regex, action_type, action_target, action_secret, enabled, created_at, updated_at FROM event_triggers ORDER BY id`, ) if err != nil { return nil, fmt.Errorf("query event triggers: %w", err) } defer rows.Close() out := []EventTrigger{} for rows.Next() { t, err := scanEventTrigger(rows) if err != nil { return nil, err } out = append(out, t) } return out, rows.Err() } // ListEnabledEventTriggers returns only the rows with enabled=1. The // dispatcher hot path uses this so a disabled trigger costs nothing. func (s *Store) ListEnabledEventTriggers() ([]EventTrigger, error) { rows, err := s.db.Query( `SELECT id, name, filter_severity, filter_source, filter_message_regex, action_type, action_target, action_secret, enabled, created_at, updated_at FROM event_triggers WHERE enabled = 1 ORDER BY id`, ) if err != nil { return nil, fmt.Errorf("query enabled event triggers: %w", err) } defer rows.Close() out := []EventTrigger{} for rows.Next() { t, err := scanEventTrigger(rows) if err != nil { return nil, err } out = append(out, t) } return out, rows.Err() } // GetEventTrigger returns one trigger by ID or ErrNotFound. func (s *Store) GetEventTrigger(id int64) (EventTrigger, error) { row := s.db.QueryRow( `SELECT id, name, filter_severity, filter_source, filter_message_regex, action_type, action_target, action_secret, enabled, created_at, updated_at FROM event_triggers WHERE id = ?`, id, ) t, err := scanEventTriggerRow(row) if errors.Is(err, sql.ErrNoRows) { return EventTrigger{}, fmt.Errorf("event trigger %d: %w", id, ErrNotFound) } if err != nil { return EventTrigger{}, fmt.Errorf("query event trigger: %w", err) } return t, nil } // UpdateEventTrigger overwrites the editable columns of an existing row. // CreatedAt is preserved; UpdatedAt is refreshed. func (s *Store) UpdateEventTrigger(t EventTrigger) (EventTrigger, error) { if t.ID == 0 { return EventTrigger{}, fmt.Errorf("event_trigger: id is required for update") } if strings.TrimSpace(t.Name) == "" { return EventTrigger{}, fmt.Errorf("event_trigger: name is required") } if t.ActionType == "" { t.ActionType = EventTriggerActionWebhook } if t.ActionType != EventTriggerActionWebhook { return EventTrigger{}, fmt.Errorf("event_trigger: unsupported action_type %q", t.ActionType) } if strings.TrimSpace(t.ActionTarget) == "" { return EventTrigger{}, fmt.Errorf("event_trigger: action_target is required") } t.UpdatedAt = Now() res, err := s.db.Exec( `UPDATE event_triggers SET name = ?, filter_severity = ?, filter_source = ?, filter_message_regex = ?, action_type = ?, action_target = ?, action_secret = ?, enabled = ?, updated_at = ? WHERE id = ?`, t.Name, t.FilterSeverity, t.FilterSource, t.FilterMessageRegex, t.ActionType, t.ActionTarget, t.ActionSecret, boolToInt(t.Enabled), t.UpdatedAt, t.ID, ) if err != nil { return EventTrigger{}, fmt.Errorf("update event trigger: %w", err) } n, _ := res.RowsAffected() if n == 0 { return EventTrigger{}, fmt.Errorf("event trigger %d: %w", t.ID, ErrNotFound) } return s.GetEventTrigger(t.ID) } // DeleteEventTrigger removes a trigger by ID. Idempotent on the // caller's side: returns ErrNotFound if the row is already gone so a // double-click in the UI gives a clean error rather than 500. func (s *Store) DeleteEventTrigger(id int64) error { res, err := s.db.Exec(`DELETE FROM event_triggers WHERE id = ?`, id) if err != nil { return fmt.Errorf("delete event trigger: %w", err) } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("event trigger %d: %w", id, ErrNotFound) } return nil } func scanEventTrigger(rows *sql.Rows) (EventTrigger, error) { var t EventTrigger var enabled int if err := rows.Scan( &t.ID, &t.Name, &t.FilterSeverity, &t.FilterSource, &t.FilterMessageRegex, &t.ActionType, &t.ActionTarget, &t.ActionSecret, &enabled, &t.CreatedAt, &t.UpdatedAt, ); err != nil { return EventTrigger{}, fmt.Errorf("scan event trigger: %w", err) } t.Enabled = enabled != 0 return t, nil } func scanEventTriggerRow(row *sql.Row) (EventTrigger, error) { var t EventTrigger var enabled int if err := row.Scan( &t.ID, &t.Name, &t.FilterSeverity, &t.FilterSource, &t.FilterMessageRegex, &t.ActionType, &t.ActionTarget, &t.ActionSecret, &enabled, &t.CreatedAt, &t.UpdatedAt, ); err != nil { return EventTrigger{}, err } t.Enabled = enabled != 0 return t, nil } func boolToInt(b bool) int { if b { return 1 } return 0 }