Feat triggers v0.2.2 (#6)
All checks were successful
All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #6.
This commit is contained in:
@@ -52,6 +52,8 @@ type Stores struct {
|
||||
ExtData ExtDataStore // v0.29.2: Extension namespaced table catalog
|
||||
Tickets TicketStore // v0.32.0: WS auth tickets (PG-backed for cross-pod)
|
||||
RateLimits RateLimitStore // v0.32.0: Distributed rate limiting
|
||||
Triggers TriggerStore // v0.2.2: Extension event/webhook triggers
|
||||
ScheduledTasks ScheduledTaskStore // v0.2.2: User-created cron tasks
|
||||
}
|
||||
|
||||
// TeamAvailableModel is returned by CatalogStore.ListTeamAvailable.
|
||||
|
||||
216
server/store/postgres/scheduled_tasks.go
Normal file
216
server/store/postgres/scheduled_tasks.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
params, _ := json.Marshal(t.TemplateParams)
|
||||
if len(params) == 0 {
|
||||
params = []byte("{}")
|
||||
}
|
||||
return DB.QueryRowContext(ctx,
|
||||
`INSERT INTO scheduled_tasks (name, description, creator_id, run_as, cron_expr,
|
||||
next_fire_at, enabled, script, template_id, template_params)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id, created_at, updated_at`,
|
||||
t.Name, t.Description, t.CreatorID, t.RunAs, t.CronExpr,
|
||||
t.NextFireAt, t.Enabled, t.Script, nilIfEmpty(t.TemplateID), params).
|
||||
Scan(&t.ID, &t.CreatedAt, &t.UpdatedAt)
|
||||
}
|
||||
|
||||
func (s *ScheduledTaskStore) GetByID(ctx context.Context, id string) (*models.ScheduledTask, error) {
|
||||
var t models.ScheduledTask
|
||||
var params []byte
|
||||
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 = $1`, id).
|
||||
Scan(&t.ID, &t.Name, &t.Description, &t.CreatorID, &t.RunAs, &t.CronExpr,
|
||||
&t.NextFireAt, &t.LastFireAt, &t.Enabled, &t.Script,
|
||||
&t.TemplateID, ¶ms, &t.FireCount,
|
||||
&t.LastError, &t.LastDurationMs, &t.CreatedAt, &t.UpdatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.TemplateParams = json.RawMessage(params)
|
||||
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("{}")
|
||||
}
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`UPDATE scheduled_tasks SET name = $1, description = $2, run_as = $3,
|
||||
cron_expr = $4, next_fire_at = $5, enabled = $6, script = $7,
|
||||
template_id = $8, template_params = $9, updated_at = NOW()
|
||||
WHERE id = $10`,
|
||||
t.Name, t.Description, t.RunAs, t.CronExpr, t.NextFireAt,
|
||||
t.Enabled, t.Script, nilIfEmpty(t.TemplateID), 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 = $1`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ScheduledTaskStore) List(ctx context.Context, opts store.ScheduledTaskListOptions) ([]models.ScheduledTask, int, error) {
|
||||
where := "1=1"
|
||||
args := []any{}
|
||||
n := 0
|
||||
|
||||
if opts.CreatorID != "" {
|
||||
n++
|
||||
where += fmt.Sprintf(" AND creator_id = $%d", n)
|
||||
args = append(args, opts.CreatorID)
|
||||
}
|
||||
if opts.Enabled != nil {
|
||||
n++
|
||||
where += fmt.Sprintf(" AND enabled = $%d", n)
|
||||
args = append(args, *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
|
||||
}
|
||||
n++
|
||||
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 $%d OFFSET $%d`,
|
||||
where, n, n+1)
|
||||
args = append(args, limit, opts.Offset)
|
||||
|
||||
rows, err := DB.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
tasks := scanScheduledTaskRows(rows)
|
||||
return tasks, total, rows.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 = $1 ORDER BY created_at DESC`, creatorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanScheduledTaskRowsErr(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 = true ORDER BY created_at`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanScheduledTaskRowsErr(rows)
|
||||
}
|
||||
|
||||
func (s *ScheduledTaskStore) SetEnabled(ctx context.Context, id string, enabled bool) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`UPDATE scheduled_tasks SET enabled = $1, updated_at = NOW() WHERE id = $2`, enabled, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ScheduledTaskStore) UpdateFireState(ctx context.Context, id string, lastFireAt time.Time, nextFireAt *time.Time, lastError string, durationMs int) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`UPDATE scheduled_tasks SET last_fire_at = $1, next_fire_at = $2,
|
||||
last_error = $3, last_duration_ms = $4, fire_count = fire_count + 1,
|
||||
updated_at = NOW() WHERE id = $5`,
|
||||
lastFireAt, nextFireAt, nilIfEmpty(lastError), durationMs, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────
|
||||
|
||||
func scanScheduledTaskRows(rows *sql.Rows) []models.ScheduledTask {
|
||||
var tasks []models.ScheduledTask
|
||||
for rows.Next() {
|
||||
t := scanOneScheduledTask(rows)
|
||||
if t != nil {
|
||||
tasks = append(tasks, *t)
|
||||
}
|
||||
}
|
||||
if tasks == nil {
|
||||
tasks = []models.ScheduledTask{}
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
func scanScheduledTaskRowsErr(rows *sql.Rows) ([]models.ScheduledTask, error) {
|
||||
var tasks []models.ScheduledTask
|
||||
for rows.Next() {
|
||||
var t models.ScheduledTask
|
||||
var params []byte
|
||||
if err := rows.Scan(&t.ID, &t.Name, &t.Description, &t.CreatorID, &t.RunAs,
|
||||
&t.CronExpr, &t.NextFireAt, &t.LastFireAt, &t.Enabled, &t.Script,
|
||||
&t.TemplateID, ¶ms, &t.FireCount,
|
||||
&t.LastError, &t.LastDurationMs, &t.CreatedAt, &t.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.TemplateParams = json.RawMessage(params)
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
if tasks == nil {
|
||||
tasks = []models.ScheduledTask{}
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func scanOneScheduledTask(rows *sql.Rows) *models.ScheduledTask {
|
||||
var t models.ScheduledTask
|
||||
var params []byte
|
||||
if err := rows.Scan(&t.ID, &t.Name, &t.Description, &t.CreatorID, &t.RunAs,
|
||||
&t.CronExpr, &t.NextFireAt, &t.LastFireAt, &t.Enabled, &t.Script,
|
||||
&t.TemplateID, ¶ms, &t.FireCount,
|
||||
&t.LastError, &t.LastDurationMs, &t.CreatedAt, &t.UpdatedAt); err != nil {
|
||||
return nil
|
||||
}
|
||||
t.TemplateParams = json.RawMessage(params)
|
||||
return &t
|
||||
}
|
||||
@@ -30,5 +30,7 @@ func NewStores(db *sql.DB) store.Stores {
|
||||
ExtData: NewExtDataStore(db),
|
||||
Tickets: NewTicketStore(),
|
||||
RateLimits: NewRateLimitStore(),
|
||||
Triggers: NewTriggerStore(),
|
||||
ScheduledTasks: NewScheduledTaskStore(),
|
||||
}
|
||||
}
|
||||
|
||||
287
server/store/postgres/triggers.go
Normal file
287
server/store/postgres/triggers.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"switchboard-core/models"
|
||||
"switchboard-core/store"
|
||||
)
|
||||
|
||||
type TriggerStore struct{}
|
||||
|
||||
func NewTriggerStore() *TriggerStore {
|
||||
return &TriggerStore{}
|
||||
}
|
||||
|
||||
func (s *TriggerStore) Create(ctx context.Context, t *models.Trigger) error {
|
||||
cfg, _ := json.Marshal(t.Config)
|
||||
if len(cfg) == 0 {
|
||||
cfg = []byte("{}")
|
||||
}
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`INSERT INTO triggers (id, package_id, type, enabled, slug, secret, event_pattern,
|
||||
entry_point, config)
|
||||
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id`,
|
||||
t.PackageID, t.Type, t.Enabled, nilIfEmpty(t.Slug), nilIfEmpty(t.Secret),
|
||||
nilIfEmpty(t.EventPattern), t.EntryPoint, cfg)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) GetByID(ctx context.Context, id string) (*models.Trigger, error) {
|
||||
var t models.Trigger
|
||||
var cfg []byte
|
||||
err := DB.QueryRowContext(ctx,
|
||||
`SELECT id, package_id, type, enabled, COALESCE(slug,''), secret,
|
||||
COALESCE(event_pattern,''), entry_point, config, fire_count,
|
||||
last_fire_at, COALESCE(last_error,''), created_at, updated_at
|
||||
FROM triggers WHERE id = $1`, id).
|
||||
Scan(&t.ID, &t.PackageID, &t.Type, &t.Enabled, &t.Slug, &t.Secret,
|
||||
&t.EventPattern, &t.EntryPoint, &cfg, &t.FireCount,
|
||||
&t.LastFireAt, &t.LastError, &t.CreatedAt, &t.UpdatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.Config = json.RawMessage(cfg)
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (s *TriggerStore) Update(ctx context.Context, t *models.Trigger) error {
|
||||
cfg, _ := json.Marshal(t.Config)
|
||||
if len(cfg) == 0 {
|
||||
cfg = []byte("{}")
|
||||
}
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`UPDATE triggers SET enabled = $1, slug = $2, secret = $3, event_pattern = $4,
|
||||
entry_point = $5, config = $6, updated_at = NOW()
|
||||
WHERE id = $7`,
|
||||
t.Enabled, nilIfEmpty(t.Slug), nilIfEmpty(t.Secret),
|
||||
nilIfEmpty(t.EventPattern), t.EntryPoint, cfg, t.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) Delete(ctx context.Context, id string) error {
|
||||
_, err := DB.ExecContext(ctx, `DELETE FROM triggers WHERE id = $1`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) List(ctx context.Context, opts store.TriggerListOptions) ([]models.Trigger, int, error) {
|
||||
where := "1=1"
|
||||
args := []any{}
|
||||
n := 0
|
||||
|
||||
if opts.PackageID != "" {
|
||||
n++
|
||||
where += fmt.Sprintf(" AND package_id = $%d", n)
|
||||
args = append(args, opts.PackageID)
|
||||
}
|
||||
if opts.Type != "" {
|
||||
n++
|
||||
where += fmt.Sprintf(" AND type = $%d", n)
|
||||
args = append(args, opts.Type)
|
||||
}
|
||||
if opts.Enabled != nil {
|
||||
n++
|
||||
where += fmt.Sprintf(" AND enabled = $%d", n)
|
||||
args = append(args, *opts.Enabled)
|
||||
}
|
||||
|
||||
// Count
|
||||
var total int
|
||||
countArgs := make([]any, len(args))
|
||||
copy(countArgs, args)
|
||||
err := DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM triggers WHERE "+where, countArgs...).Scan(&total)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Query
|
||||
limit := opts.Limit
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
n++
|
||||
query := fmt.Sprintf(
|
||||
`SELECT id, package_id, type, enabled, COALESCE(slug,''), COALESCE(event_pattern,''),
|
||||
entry_point, config, fire_count, last_fire_at, COALESCE(last_error,''),
|
||||
created_at, updated_at
|
||||
FROM triggers WHERE %s ORDER BY created_at DESC LIMIT $%d OFFSET $%d`,
|
||||
where, n, n+1)
|
||||
args = append(args, limit, opts.Offset)
|
||||
|
||||
rows, err := DB.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var triggers []models.Trigger
|
||||
for rows.Next() {
|
||||
var t models.Trigger
|
||||
var cfg []byte
|
||||
if err := rows.Scan(&t.ID, &t.PackageID, &t.Type, &t.Enabled, &t.Slug,
|
||||
&t.EventPattern, &t.EntryPoint, &cfg, &t.FireCount,
|
||||
&t.LastFireAt, &t.LastError, &t.CreatedAt, &t.UpdatedAt); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
t.Config = json.RawMessage(cfg)
|
||||
triggers = append(triggers, t)
|
||||
}
|
||||
if triggers == nil {
|
||||
triggers = []models.Trigger{}
|
||||
}
|
||||
return triggers, total, nil
|
||||
}
|
||||
|
||||
func (s *TriggerStore) ListByPackage(ctx context.Context, packageID string) ([]models.Trigger, error) {
|
||||
rows, err := DB.QueryContext(ctx,
|
||||
`SELECT id, package_id, type, enabled, COALESCE(slug,''), COALESCE(event_pattern,''),
|
||||
entry_point, config, fire_count, last_fire_at, COALESCE(last_error,''),
|
||||
created_at, updated_at
|
||||
FROM triggers WHERE package_id = $1 ORDER BY created_at`, packageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanTriggerRows(rows)
|
||||
}
|
||||
|
||||
func (s *TriggerStore) ListEnabledByType(ctx context.Context, triggerType string) ([]models.Trigger, error) {
|
||||
rows, err := DB.QueryContext(ctx,
|
||||
`SELECT id, package_id, type, enabled, COALESCE(slug,''), COALESCE(event_pattern,''),
|
||||
entry_point, config, fire_count, last_fire_at, COALESCE(last_error,''),
|
||||
created_at, updated_at
|
||||
FROM triggers WHERE type = $1 AND enabled = true ORDER BY created_at`, triggerType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanTriggerRows(rows)
|
||||
}
|
||||
|
||||
func (s *TriggerStore) SetEnabled(ctx context.Context, id string, enabled bool) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`UPDATE triggers SET enabled = $1, updated_at = NOW() WHERE id = $2`, enabled, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) UpdateFireState(ctx context.Context, id string, lastFireAt time.Time, lastError string) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`UPDATE triggers SET last_fire_at = $1, last_error = $2, fire_count = fire_count + 1,
|
||||
updated_at = NOW() WHERE id = $3`,
|
||||
lastFireAt, nilIfEmpty(lastError), id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) GetWebhook(ctx context.Context, packageID, slug string) (*models.Trigger, error) {
|
||||
var t models.Trigger
|
||||
var cfg []byte
|
||||
err := DB.QueryRowContext(ctx,
|
||||
`SELECT id, package_id, type, enabled, COALESCE(slug,''), secret,
|
||||
COALESCE(event_pattern,''), entry_point, config, fire_count,
|
||||
last_fire_at, COALESCE(last_error,''), created_at, updated_at
|
||||
FROM triggers
|
||||
WHERE package_id = $1 AND slug = $2 AND type = 'webhook'`, packageID, slug).
|
||||
Scan(&t.ID, &t.PackageID, &t.Type, &t.Enabled, &t.Slug, &t.Secret,
|
||||
&t.EventPattern, &t.EntryPoint, &cfg, &t.FireCount,
|
||||
&t.LastFireAt, &t.LastError, &t.CreatedAt, &t.UpdatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.Config = json.RawMessage(cfg)
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (s *TriggerStore) DeleteForPackage(ctx context.Context, packageID string) error {
|
||||
_, err := DB.ExecContext(ctx, `DELETE FROM triggers WHERE package_id = $1`, packageID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) LogExecution(ctx context.Context, log *models.TriggerLog) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`INSERT INTO trigger_logs (id, trigger_id, scheduled_task_id, fired_at, duration_ms, success, error, output)
|
||||
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7)`,
|
||||
nilIfEmpty(log.TriggerID), nilIfEmpty(log.ScheduledTaskID),
|
||||
log.FiredAt, log.DurationMs, log.Success, nilIfEmpty(log.Error), nilIfEmpty(log.Output))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) ListLogs(ctx context.Context, triggerID string, limit int) ([]models.TriggerLog, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := DB.QueryContext(ctx,
|
||||
`SELECT id, COALESCE(trigger_id::text,''), COALESCE(scheduled_task_id::text,''),
|
||||
fired_at, duration_ms, success, COALESCE(error,''), COALESCE(output,'')
|
||||
FROM trigger_logs
|
||||
WHERE trigger_id = $1
|
||||
ORDER BY fired_at DESC LIMIT $2`, triggerID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanLogRows(rows)
|
||||
}
|
||||
|
||||
func (s *TriggerStore) PruneLogs(ctx context.Context, before time.Time) (int64, error) {
|
||||
result, err := DB.ExecContext(ctx,
|
||||
`DELETE FROM trigger_logs WHERE fired_at < $1`, before)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────
|
||||
|
||||
func scanTriggerRows(rows *sql.Rows) ([]models.Trigger, error) {
|
||||
var triggers []models.Trigger
|
||||
for rows.Next() {
|
||||
var t models.Trigger
|
||||
var cfg []byte
|
||||
if err := rows.Scan(&t.ID, &t.PackageID, &t.Type, &t.Enabled, &t.Slug,
|
||||
&t.EventPattern, &t.EntryPoint, &cfg, &t.FireCount,
|
||||
&t.LastFireAt, &t.LastError, &t.CreatedAt, &t.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.Config = json.RawMessage(cfg)
|
||||
triggers = append(triggers, t)
|
||||
}
|
||||
if triggers == nil {
|
||||
triggers = []models.Trigger{}
|
||||
}
|
||||
return triggers, nil
|
||||
}
|
||||
|
||||
func scanLogRows(rows *sql.Rows) ([]models.TriggerLog, error) {
|
||||
var logs []models.TriggerLog
|
||||
for rows.Next() {
|
||||
var l models.TriggerLog
|
||||
if err := rows.Scan(&l.ID, &l.TriggerID, &l.ScheduledTaskID,
|
||||
&l.FiredAt, &l.DurationMs, &l.Success, &l.Error, &l.Output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logs = append(logs, l)
|
||||
}
|
||||
if logs == nil {
|
||||
logs = []models.TriggerLog{}
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func nilIfEmpty(s string) *string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
33
server/store/scheduled_task_iface.go
Normal file
33
server/store/scheduled_task_iface.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"switchboard-core/models"
|
||||
)
|
||||
|
||||
// ScheduledTaskStore manages user-created cron-scheduled Starlark scripts.
|
||||
type ScheduledTaskStore interface {
|
||||
// CRUD
|
||||
Create(ctx context.Context, t *models.ScheduledTask) error
|
||||
GetByID(ctx context.Context, id string) (*models.ScheduledTask, error)
|
||||
Update(ctx context.Context, t *models.ScheduledTask) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// Queries
|
||||
List(ctx context.Context, opts ScheduledTaskListOptions) ([]models.ScheduledTask, int, error)
|
||||
ListByCreator(ctx context.Context, creatorID string) ([]models.ScheduledTask, error)
|
||||
ListEnabled(ctx context.Context) ([]models.ScheduledTask, error)
|
||||
|
||||
// Lifecycle
|
||||
SetEnabled(ctx context.Context, id string, enabled bool) error
|
||||
UpdateFireState(ctx context.Context, id string, lastFireAt time.Time, nextFireAt *time.Time, lastError string, durationMs int) error
|
||||
}
|
||||
|
||||
// ScheduledTaskListOptions provides filtering for scheduled task list queries.
|
||||
type ScheduledTaskListOptions struct {
|
||||
ListOptions
|
||||
CreatorID string
|
||||
Enabled *bool
|
||||
}
|
||||
216
server/store/sqlite/scheduled_tasks.go
Normal file
216
server/store/sqlite/scheduled_tasks.go
Normal file
@@ -0,0 +1,216 @@
|
||||
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
|
||||
}
|
||||
@@ -30,5 +30,7 @@ func NewStores(db *sql.DB) store.Stores {
|
||||
ExtData: NewExtDataStore(),
|
||||
Tickets: NewTicketStore(),
|
||||
RateLimits: NewRateLimitStore(),
|
||||
Triggers: NewTriggerStore(),
|
||||
ScheduledTasks: NewScheduledTaskStore(),
|
||||
}
|
||||
}
|
||||
|
||||
269
server/store/sqlite/triggers.go
Normal file
269
server/store/sqlite/triggers.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"switchboard-core/database"
|
||||
"switchboard-core/models"
|
||||
"switchboard-core/store"
|
||||
)
|
||||
|
||||
type TriggerStore struct{}
|
||||
|
||||
func NewTriggerStore() *TriggerStore {
|
||||
return &TriggerStore{}
|
||||
}
|
||||
|
||||
func (s *TriggerStore) Create(ctx context.Context, t *models.Trigger) error {
|
||||
t.ID = uuid.New().String()
|
||||
cfg, _ := json.Marshal(t.Config)
|
||||
if len(cfg) == 0 {
|
||||
cfg = []byte("{}")
|
||||
}
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`INSERT INTO triggers (id, package_id, type, enabled, slug, secret, event_pattern,
|
||||
entry_point, config)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
t.ID, t.PackageID, t.Type, boolToInt(t.Enabled),
|
||||
nullIfEmpty(t.Slug), nullIfEmpty(t.Secret),
|
||||
nullIfEmpty(t.EventPattern), t.EntryPoint, string(cfg))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) GetByID(ctx context.Context, id string) (*models.Trigger, error) {
|
||||
var t models.Trigger
|
||||
var cfgStr string
|
||||
err := DB.QueryRowContext(ctx,
|
||||
`SELECT id, package_id, type, enabled, COALESCE(slug,''), secret,
|
||||
COALESCE(event_pattern,''), entry_point, config, fire_count,
|
||||
last_fire_at, COALESCE(last_error,''), created_at, updated_at
|
||||
FROM triggers WHERE id = ?`, id).
|
||||
Scan(&t.ID, &t.PackageID, &t.Type, &t.Enabled, &t.Slug, &t.Secret,
|
||||
&t.EventPattern, &t.EntryPoint, &cfgStr, &t.FireCount,
|
||||
database.SNT(&t.LastFireAt), &t.LastError,
|
||||
database.ST(&t.CreatedAt), database.ST(&t.UpdatedAt))
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.Config = json.RawMessage(cfgStr)
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (s *TriggerStore) Update(ctx context.Context, t *models.Trigger) error {
|
||||
cfg, _ := json.Marshal(t.Config)
|
||||
if len(cfg) == 0 {
|
||||
cfg = []byte("{}")
|
||||
}
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`UPDATE triggers SET enabled = ?, slug = ?, secret = ?, event_pattern = ?,
|
||||
entry_point = ?, config = ?, updated_at = datetime('now')
|
||||
WHERE id = ?`,
|
||||
boolToInt(t.Enabled), nullIfEmpty(t.Slug), nullIfEmpty(t.Secret),
|
||||
nullIfEmpty(t.EventPattern), t.EntryPoint, string(cfg), t.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) Delete(ctx context.Context, id string) error {
|
||||
_, err := DB.ExecContext(ctx, `DELETE FROM triggers WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) List(ctx context.Context, opts store.TriggerListOptions) ([]models.Trigger, int, error) {
|
||||
where := "1=1"
|
||||
args := []any{}
|
||||
|
||||
if opts.PackageID != "" {
|
||||
where += " AND package_id = ?"
|
||||
args = append(args, opts.PackageID)
|
||||
}
|
||||
if opts.Type != "" {
|
||||
where += " AND type = ?"
|
||||
args = append(args, opts.Type)
|
||||
}
|
||||
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 triggers 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, package_id, type, enabled, COALESCE(slug,''), COALESCE(event_pattern,''),
|
||||
entry_point, config, fire_count, last_fire_at, COALESCE(last_error,''),
|
||||
created_at, updated_at
|
||||
FROM triggers 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()
|
||||
|
||||
triggers, err := scanTriggerRowsSqlite(rows)
|
||||
return triggers, total, err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) ListByPackage(ctx context.Context, packageID string) ([]models.Trigger, error) {
|
||||
rows, err := DB.QueryContext(ctx,
|
||||
`SELECT id, package_id, type, enabled, COALESCE(slug,''), COALESCE(event_pattern,''),
|
||||
entry_point, config, fire_count, last_fire_at, COALESCE(last_error,''),
|
||||
created_at, updated_at
|
||||
FROM triggers WHERE package_id = ? ORDER BY created_at`, packageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanTriggerRowsSqlite(rows)
|
||||
}
|
||||
|
||||
func (s *TriggerStore) ListEnabledByType(ctx context.Context, triggerType string) ([]models.Trigger, error) {
|
||||
rows, err := DB.QueryContext(ctx,
|
||||
`SELECT id, package_id, type, enabled, COALESCE(slug,''), COALESCE(event_pattern,''),
|
||||
entry_point, config, fire_count, last_fire_at, COALESCE(last_error,''),
|
||||
created_at, updated_at
|
||||
FROM triggers WHERE type = ? AND enabled = 1 ORDER BY created_at`, triggerType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanTriggerRowsSqlite(rows)
|
||||
}
|
||||
|
||||
func (s *TriggerStore) SetEnabled(ctx context.Context, id string, enabled bool) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`UPDATE triggers SET enabled = ?, updated_at = datetime('now') WHERE id = ?`,
|
||||
boolToInt(enabled), id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) UpdateFireState(ctx context.Context, id string, lastFireAt time.Time, lastError string) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`UPDATE triggers SET last_fire_at = ?, last_error = ?, fire_count = fire_count + 1,
|
||||
updated_at = datetime('now') WHERE id = ?`,
|
||||
lastFireAt.UTC().Format(time.RFC3339), nullIfEmpty(lastError), id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) GetWebhook(ctx context.Context, packageID, slug string) (*models.Trigger, error) {
|
||||
var t models.Trigger
|
||||
var cfgStr string
|
||||
err := DB.QueryRowContext(ctx,
|
||||
`SELECT id, package_id, type, enabled, COALESCE(slug,''), secret,
|
||||
COALESCE(event_pattern,''), entry_point, config, fire_count,
|
||||
last_fire_at, COALESCE(last_error,''), created_at, updated_at
|
||||
FROM triggers
|
||||
WHERE package_id = ? AND slug = ? AND type = 'webhook'`, packageID, slug).
|
||||
Scan(&t.ID, &t.PackageID, &t.Type, &t.Enabled, &t.Slug, &t.Secret,
|
||||
&t.EventPattern, &t.EntryPoint, &cfgStr, &t.FireCount,
|
||||
database.SNT(&t.LastFireAt), &t.LastError,
|
||||
database.ST(&t.CreatedAt), database.ST(&t.UpdatedAt))
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.Config = json.RawMessage(cfgStr)
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (s *TriggerStore) DeleteForPackage(ctx context.Context, packageID string) error {
|
||||
_, err := DB.ExecContext(ctx, `DELETE FROM triggers WHERE package_id = ?`, packageID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) LogExecution(ctx context.Context, log *models.TriggerLog) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`INSERT INTO trigger_logs (id, trigger_id, scheduled_task_id, fired_at, duration_ms, success, error, output)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
uuid.New().String(), nullIfEmpty(log.TriggerID), nullIfEmpty(log.ScheduledTaskID),
|
||||
log.FiredAt, log.DurationMs, boolToInt(log.Success),
|
||||
nullIfEmpty(log.Error), nullIfEmpty(log.Output))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TriggerStore) ListLogs(ctx context.Context, triggerID string, limit int) ([]models.TriggerLog, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := DB.QueryContext(ctx,
|
||||
`SELECT id, COALESCE(trigger_id,''), COALESCE(scheduled_task_id,''),
|
||||
fired_at, duration_ms, success, COALESCE(error,''), COALESCE(output,'')
|
||||
FROM trigger_logs
|
||||
WHERE trigger_id = ?
|
||||
ORDER BY fired_at DESC LIMIT ?`, triggerID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanLogRowsSqlite(rows)
|
||||
}
|
||||
|
||||
func (s *TriggerStore) PruneLogs(ctx context.Context, before time.Time) (int64, error) {
|
||||
result, err := DB.ExecContext(ctx,
|
||||
`DELETE FROM trigger_logs WHERE fired_at < ?`, before.UTC().Format(time.RFC3339))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────
|
||||
|
||||
func scanTriggerRowsSqlite(rows *sql.Rows) ([]models.Trigger, error) {
|
||||
var triggers []models.Trigger
|
||||
for rows.Next() {
|
||||
var t models.Trigger
|
||||
var cfgStr string
|
||||
if err := rows.Scan(&t.ID, &t.PackageID, &t.Type, &t.Enabled, &t.Slug,
|
||||
&t.EventPattern, &t.EntryPoint, &cfgStr, &t.FireCount,
|
||||
database.SNT(&t.LastFireAt), &t.LastError,
|
||||
database.ST(&t.CreatedAt), database.ST(&t.UpdatedAt)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.Config = json.RawMessage(cfgStr)
|
||||
triggers = append(triggers, t)
|
||||
}
|
||||
if triggers == nil {
|
||||
triggers = []models.Trigger{}
|
||||
}
|
||||
return triggers, nil
|
||||
}
|
||||
|
||||
func scanLogRowsSqlite(rows *sql.Rows) ([]models.TriggerLog, error) {
|
||||
var logs []models.TriggerLog
|
||||
for rows.Next() {
|
||||
var l models.TriggerLog
|
||||
if err := rows.Scan(&l.ID, &l.TriggerID, &l.ScheduledTaskID,
|
||||
&l.FiredAt, &l.DurationMs, &l.Success, &l.Error, &l.Output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logs = append(logs, l)
|
||||
}
|
||||
if logs == nil {
|
||||
logs = []models.TriggerLog{}
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// boolToInt and nullIfEmpty are defined in workflows.go (same package).
|
||||
45
server/store/trigger_iface.go
Normal file
45
server/store/trigger_iface.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"switchboard-core/models"
|
||||
)
|
||||
|
||||
// TriggerStore manages extension-declared event and webhook triggers.
|
||||
type TriggerStore interface {
|
||||
// CRUD
|
||||
Create(ctx context.Context, t *models.Trigger) error
|
||||
GetByID(ctx context.Context, id string) (*models.Trigger, error)
|
||||
Update(ctx context.Context, t *models.Trigger) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// Queries
|
||||
List(ctx context.Context, opts TriggerListOptions) ([]models.Trigger, int, error)
|
||||
ListByPackage(ctx context.Context, packageID string) ([]models.Trigger, error)
|
||||
ListEnabledByType(ctx context.Context, triggerType string) ([]models.Trigger, error)
|
||||
|
||||
// Lifecycle
|
||||
SetEnabled(ctx context.Context, id string, enabled bool) error
|
||||
UpdateFireState(ctx context.Context, id string, lastFireAt time.Time, lastError string) error
|
||||
|
||||
// Webhook resolution
|
||||
GetWebhook(ctx context.Context, packageID, slug string) (*models.Trigger, error)
|
||||
|
||||
// Cleanup
|
||||
DeleteForPackage(ctx context.Context, packageID string) error
|
||||
|
||||
// Logs
|
||||
LogExecution(ctx context.Context, log *models.TriggerLog) error
|
||||
ListLogs(ctx context.Context, triggerID string, limit int) ([]models.TriggerLog, error)
|
||||
PruneLogs(ctx context.Context, before time.Time) (int64, error)
|
||||
}
|
||||
|
||||
// TriggerListOptions provides filtering for trigger list queries.
|
||||
type TriggerListOptions struct {
|
||||
ListOptions
|
||||
PackageID string
|
||||
Type string
|
||||
Enabled *bool
|
||||
}
|
||||
Reference in New Issue
Block a user