package db import ( "context" "encoding/json" "fmt" "time" "github.com/jackc/pgx/v5" ) // ScheduleRow is the persisted form of a schedule. type ScheduleRow struct { ID string InstanceID string Description string TriggerKind string // "cron" | "event" CronSpec string // populated for TriggerKind == "cron" EventSpec string // populated for TriggerKind == "event" — CEL expression returning bool EventSustainedSeconds int32 // for event: fire only after condition true this many seconds Action json.RawMessage Enabled bool LastFiredAt *time.Time LastStatus string LastDetail string CreatedAt time.Time UpdatedAt time.Time } func (db *DB) InsertSchedule(ctx context.Context, r ScheduleRow) error { var cronSpec, eventSpec *string if r.CronSpec != "" { cronSpec = &r.CronSpec } if r.EventSpec != "" { eventSpec = &r.EventSpec } _, err := db.pool.Exec(ctx, ` INSERT INTO schedules (id, instance_id, description, trigger_kind, cron_spec, event_spec, event_sustained_seconds, action, enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) `, r.ID, r.InstanceID, r.Description, r.TriggerKind, cronSpec, eventSpec, r.EventSustainedSeconds, []byte(r.Action), r.Enabled) if err != nil { return fmt.Errorf("insert schedule: %w", err) } return nil } func (db *DB) DeleteSchedule(ctx context.Context, id string) error { _, err := db.pool.Exec(ctx, `DELETE FROM schedules WHERE id = $1`, id) if err != nil { return fmt.Errorf("delete schedule: %w", err) } return nil } func (db *DB) SetScheduleEnabled(ctx context.Context, id string, enabled bool) error { _, err := db.pool.Exec(ctx, `UPDATE schedules SET enabled = $2, updated_at = NOW() WHERE id = $1`, id, enabled) if err != nil { return fmt.Errorf("set schedule enabled: %w", err) } return nil } // UpdateSchedule persists changes to an existing schedule row. func (db *DB) UpdateSchedule(ctx context.Context, r ScheduleRow) error { _, err := db.pool.Exec(ctx, ` UPDATE schedules SET description = $2, trigger_kind = $3, cron_spec = $4, event_spec = $5, event_sustained_seconds = $6, action = $7, enabled = $8, updated_at = NOW() WHERE id = $1`, r.ID, r.Description, r.TriggerKind, r.CronSpec, r.EventSpec, r.EventSustainedSeconds, []byte(r.Action), r.Enabled) if err != nil { return fmt.Errorf("update schedule: %w", err) } return nil } // UpdateScheduleFireResult records the outcome of a trigger fire — used by // the engine to surface success/failure in the UI. func (db *DB) UpdateScheduleFireResult(ctx context.Context, id, status, detail string) error { _, err := db.pool.Exec(ctx, ` UPDATE schedules SET last_fired_at = NOW(), last_status = $2, last_detail = $3, updated_at = NOW() WHERE id = $1 `, id, status, detail) if err != nil { return fmt.Errorf("update schedule result: %w", err) } return nil } // ListSchedules returns every schedule, optionally filtered by instance_id. func (db *DB) ListSchedules(ctx context.Context, instanceFilter string) ([]ScheduleRow, error) { const selectAll = ` SELECT id, instance_id, description, trigger_kind, COALESCE(cron_spec, ''), COALESCE(event_spec, ''), event_sustained_seconds, action, enabled, last_fired_at, COALESCE(last_status, ''), COALESCE(last_detail, ''), created_at, updated_at FROM schedules ` var rows pgx.Rows var err error if instanceFilter != "" { rows, err = db.pool.Query(ctx, selectAll+` WHERE instance_id = $1 ORDER BY id`, instanceFilter) } else { rows, err = db.pool.Query(ctx, selectAll+` ORDER BY instance_id, id`) } if err != nil { return nil, fmt.Errorf("list schedules: %w", err) } defer rows.Close() var out []ScheduleRow for rows.Next() { var r ScheduleRow var action []byte var lastFired *time.Time if err := rows.Scan( &r.ID, &r.InstanceID, &r.Description, &r.TriggerKind, &r.CronSpec, &r.EventSpec, &r.EventSustainedSeconds, &action, &r.Enabled, &lastFired, &r.LastStatus, &r.LastDetail, &r.CreatedAt, &r.UpdatedAt, ); err != nil { return nil, fmt.Errorf("scan schedule: %w", err) } r.Action = action r.LastFiredAt = lastFired out = append(out, r) } return out, rows.Err() }