package models import ( "context" "database/sql" "errors" "fmt" "time" "git.eeqj.de/sneak/upaas/internal/database" ) // WebhookEvent represents a received webhook event. type WebhookEvent struct { db *database.Database ID int64 AppID string EventType string Branch string CommitSHA sql.NullString Payload sql.NullString Matched bool Processed bool CreatedAt time.Time } // NewWebhookEvent creates a new WebhookEvent with a database reference. func NewWebhookEvent(db *database.Database) *WebhookEvent { return &WebhookEvent{db: db} } // Save inserts or updates the webhook event in the database. func (w *WebhookEvent) Save(ctx context.Context) error { if w.ID == 0 { return w.insert(ctx) } return w.update(ctx) } // Reload refreshes the webhook event from the database. func (w *WebhookEvent) Reload(ctx context.Context) error { query := ` SELECT id, app_id, event_type, branch, commit_sha, payload, matched, processed, created_at FROM webhook_events WHERE id = ?` row := w.db.QueryRow(ctx, query, w.ID) return w.scan(row) } func (w *WebhookEvent) insert(ctx context.Context) error { query := ` INSERT INTO webhook_events ( app_id, event_type, branch, commit_sha, payload, matched, processed ) VALUES (?, ?, ?, ?, ?, ?, ?)` result, err := w.db.Exec(ctx, query, w.AppID, w.EventType, w.Branch, w.CommitSHA, w.Payload, w.Matched, w.Processed, ) if err != nil { return err } id, err := result.LastInsertId() if err != nil { return err } w.ID = id return w.Reload(ctx) } func (w *WebhookEvent) update(ctx context.Context) error { query := "UPDATE webhook_events SET processed = ? WHERE id = ?" _, err := w.db.Exec(ctx, query, w.Processed, w.ID) return err } func (w *WebhookEvent) scan(row *sql.Row) error { return row.Scan( &w.ID, &w.AppID, &w.EventType, &w.Branch, &w.CommitSHA, &w.Payload, &w.Matched, &w.Processed, &w.CreatedAt, ) } // FindWebhookEvent finds a webhook event by ID. // //nolint:nilnil // returning nil,nil is idiomatic for "not found" in Active Record func FindWebhookEvent( ctx context.Context, db *database.Database, id int64, ) (*WebhookEvent, error) { event := NewWebhookEvent(db) event.ID = id row := db.QueryRow(ctx, ` SELECT id, app_id, event_type, branch, commit_sha, payload, matched, processed, created_at FROM webhook_events WHERE id = ?`, id, ) err := event.scan(row) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, fmt.Errorf("scanning webhook event: %w", err) } return event, nil } // FindWebhookEventsByAppID finds recent webhook events for an app. func FindWebhookEventsByAppID( ctx context.Context, db *database.Database, appID string, limit int, ) ([]*WebhookEvent, error) { query := ` SELECT id, app_id, event_type, branch, commit_sha, payload, matched, processed, created_at FROM webhook_events WHERE app_id = ? ORDER BY created_at DESC LIMIT ?` rows, err := db.Query(ctx, query, appID, limit) if err != nil { return nil, fmt.Errorf("querying webhook events by app: %w", err) } defer func() { _ = rows.Close() }() var events []*WebhookEvent for rows.Next() { event := NewWebhookEvent(db) scanErr := rows.Scan( &event.ID, &event.AppID, &event.EventType, &event.Branch, &event.CommitSHA, &event.Payload, &event.Matched, &event.Processed, &event.CreatedAt, ) if scanErr != nil { return nil, scanErr } events = append(events, event) } return events, rows.Err() } // FindUnprocessedWebhookEvents finds unprocessed matched webhook events. func FindUnprocessedWebhookEvents( ctx context.Context, db *database.Database, ) ([]*WebhookEvent, error) { query := ` SELECT id, app_id, event_type, branch, commit_sha, payload, matched, processed, created_at FROM webhook_events WHERE matched = 1 AND processed = 0 ORDER BY created_at ASC` rows, err := db.Query(ctx, query) if err != nil { return nil, fmt.Errorf("querying unprocessed webhook events: %w", err) } defer func() { _ = rows.Close() }() var events []*WebhookEvent for rows.Next() { event := NewWebhookEvent(db) scanErr := rows.Scan( &event.ID, &event.AppID, &event.EventType, &event.Branch, &event.CommitSHA, &event.Payload, &event.Matched, &event.Processed, &event.CreatedAt, ) if scanErr != nil { return nil, scanErr } events = append(events, event) } return events, rows.Err() }