package sqlite import ( "context" "database/sql" "encoding/json" "fmt" "time" "github.com/google/uuid" "switchboard-core/database" "switchboard-core/models" "switchboard-core/store" ) type ScheduledTaskStore struct{} func NewScheduledTaskStore() *ScheduledTaskStore { return &ScheduledTaskStore{} } func (s *ScheduledTaskStore) Create(ctx context.Context, t *models.ScheduledTask) error { t.ID = uuid.New().String() now := time.Now().UTC().Format(time.RFC3339) t.CreatedAt, _ = time.Parse(time.RFC3339, now) t.UpdatedAt = t.CreatedAt params, _ := json.Marshal(t.TemplateParams) if len(params) == 0 { params = []byte("{}") } var nextFire *string if t.NextFireAt != nil { s := t.NextFireAt.UTC().Format(time.RFC3339) nextFire = &s } _, err := DB.ExecContext(ctx, `INSERT INTO scheduled_tasks (id, name, description, creator_id, run_as, cron_expr, next_fire_at, enabled, script, template_id, template_params, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, t.ID, t.Name, t.Description, t.CreatorID, t.RunAs, t.CronExpr, nextFire, boolToInt(t.Enabled), t.Script, nullIfEmpty(t.TemplateID), string(params), now, now) return err } func (s *ScheduledTaskStore) GetByID(ctx context.Context, id string) (*models.ScheduledTask, error) { var t models.ScheduledTask var paramsStr string err := DB.QueryRowContext(ctx, `SELECT id, name, description, creator_id, run_as, cron_expr, next_fire_at, last_fire_at, enabled, script, COALESCE(template_id,''), template_params, fire_count, COALESCE(last_error,''), last_duration_ms, created_at, updated_at FROM scheduled_tasks WHERE id = ?`, id). Scan(&t.ID, &t.Name, &t.Description, &t.CreatorID, &t.RunAs, &t.CronExpr, database.SNT(&t.NextFireAt), database.SNT(&t.LastFireAt), &t.Enabled, &t.Script, &t.TemplateID, ¶msStr, &t.FireCount, &t.LastError, &t.LastDurationMs, database.ST(&t.CreatedAt), database.ST(&t.UpdatedAt)) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } t.TemplateParams = json.RawMessage(paramsStr) return &t, nil } func (s *ScheduledTaskStore) Update(ctx context.Context, t *models.ScheduledTask) error { params, _ := json.Marshal(t.TemplateParams) if len(params) == 0 { params = []byte("{}") } var nextFire *string if t.NextFireAt != nil { s := t.NextFireAt.UTC().Format(time.RFC3339) nextFire = &s } _, err := DB.ExecContext(ctx, `UPDATE scheduled_tasks SET name = ?, description = ?, run_as = ?, cron_expr = ?, next_fire_at = ?, enabled = ?, script = ?, template_id = ?, template_params = ?, updated_at = datetime('now') WHERE id = ?`, t.Name, t.Description, t.RunAs, t.CronExpr, nextFire, boolToInt(t.Enabled), t.Script, nullIfEmpty(t.TemplateID), string(params), t.ID) return err } func (s *ScheduledTaskStore) Delete(ctx context.Context, id string) error { _, err := DB.ExecContext(ctx, `DELETE FROM scheduled_tasks WHERE id = ?`, id) return err } func (s *ScheduledTaskStore) List(ctx context.Context, opts store.ScheduledTaskListOptions) ([]models.ScheduledTask, int, error) { where := "1=1" args := []any{} if opts.CreatorID != "" { where += " AND creator_id = ?" args = append(args, opts.CreatorID) } if opts.Enabled != nil { where += " AND enabled = ?" args = append(args, boolToInt(*opts.Enabled)) } var total int countArgs := make([]any, len(args)) copy(countArgs, args) err := DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM scheduled_tasks WHERE "+where, countArgs...).Scan(&total) if err != nil { return nil, 0, err } limit := opts.Limit if limit <= 0 { limit = 50 } query := fmt.Sprintf( `SELECT id, name, description, creator_id, run_as, cron_expr, next_fire_at, last_fire_at, enabled, script, COALESCE(template_id,''), template_params, fire_count, COALESCE(last_error,''), last_duration_ms, created_at, updated_at FROM scheduled_tasks WHERE %s ORDER BY created_at DESC LIMIT ? OFFSET ?`, where) args = append(args, limit, opts.Offset) rows, err := DB.QueryContext(ctx, query, args...) if err != nil { return nil, 0, err } defer rows.Close() tasks, err := scanScheduledTaskRowsSqlite(rows) return tasks, total, err } func (s *ScheduledTaskStore) ListByCreator(ctx context.Context, creatorID string) ([]models.ScheduledTask, error) { rows, err := DB.QueryContext(ctx, `SELECT id, name, description, creator_id, run_as, cron_expr, next_fire_at, last_fire_at, enabled, script, COALESCE(template_id,''), template_params, fire_count, COALESCE(last_error,''), last_duration_ms, created_at, updated_at FROM scheduled_tasks WHERE creator_id = ? ORDER BY created_at DESC`, creatorID) if err != nil { return nil, err } defer rows.Close() return scanScheduledTaskRowsSqlite(rows) } func (s *ScheduledTaskStore) ListEnabled(ctx context.Context) ([]models.ScheduledTask, error) { rows, err := DB.QueryContext(ctx, `SELECT id, name, description, creator_id, run_as, cron_expr, next_fire_at, last_fire_at, enabled, script, COALESCE(template_id,''), template_params, fire_count, COALESCE(last_error,''), last_duration_ms, created_at, updated_at FROM scheduled_tasks WHERE enabled = 1 ORDER BY created_at`) if err != nil { return nil, err } defer rows.Close() return scanScheduledTaskRowsSqlite(rows) } func (s *ScheduledTaskStore) SetEnabled(ctx context.Context, id string, enabled bool) error { _, err := DB.ExecContext(ctx, `UPDATE scheduled_tasks SET enabled = ?, updated_at = datetime('now') WHERE id = ?`, boolToInt(enabled), id) return err } func (s *ScheduledTaskStore) UpdateFireState(ctx context.Context, id string, lastFireAt time.Time, nextFireAt *time.Time, lastError string, durationMs int) error { var nextFire *string if nextFireAt != nil { s := nextFireAt.UTC().Format(time.RFC3339) nextFire = &s } _, err := DB.ExecContext(ctx, `UPDATE scheduled_tasks SET last_fire_at = ?, next_fire_at = ?, last_error = ?, last_duration_ms = ?, fire_count = fire_count + 1, updated_at = datetime('now') WHERE id = ?`, lastFireAt.UTC().Format(time.RFC3339), nextFire, nullIfEmpty(lastError), durationMs, id) return err } // ── Helpers ────────────────────────────────── func scanScheduledTaskRowsSqlite(rows *sql.Rows) ([]models.ScheduledTask, error) { var tasks []models.ScheduledTask for rows.Next() { var t models.ScheduledTask var paramsStr string if err := rows.Scan(&t.ID, &t.Name, &t.Description, &t.CreatorID, &t.RunAs, &t.CronExpr, database.SNT(&t.NextFireAt), database.SNT(&t.LastFireAt), &t.Enabled, &t.Script, &t.TemplateID, ¶msStr, &t.FireCount, &t.LastError, &t.LastDurationMs, database.ST(&t.CreatedAt), database.ST(&t.UpdatedAt)); err != nil { return nil, err } t.TemplateParams = json.RawMessage(paramsStr) tasks = append(tasks, t) } if tasks == nil { tasks = []models.ScheduledTask{} } return tasks, nil }