UpdateWebhook applies a partial update to a webhook. Only fields with a non-nil pointer in WebhookUpdate are touched. Returns the updated row. Validation (charset, count caps, agent ownership) is the handler's job; the storage layer enforces only the re-enable cooldown and the per-row CHECK constra
(ctx context.Context, webhookID, userID string, u WebhookUpdate)
| 372 | // job; the storage layer enforces only the re-enable cooldown and |
| 373 | // the per-row CHECK constraints (events non-empty, url non-empty). |
| 374 | func (s *Store) UpdateWebhook(ctx context.Context, webhookID, userID string, u WebhookUpdate) (*Webhook, error) { |
| 375 | // Re-enable cooldown — read the current state once before |
| 376 | // running the UPDATE so we can return a typed error. |
| 377 | if u.Enabled != nil && *u.Enabled { |
| 378 | var autoDisabledAt *time.Time |
| 379 | err := s.pool.QueryRow(ctx, |
| 380 | `SELECT auto_disabled_at FROM webhooks WHERE id = $1 AND user_id = $2`, |
| 381 | webhookID, userID, |
| 382 | ).Scan(&autoDisabledAt) |
| 383 | if err != nil { |
| 384 | if errors.Is(err, pgx.ErrNoRows) { |
| 385 | return nil, ErrWebhookNotFound |
| 386 | } |
| 387 | return nil, err |
| 388 | } |
| 389 | if autoDisabledAt != nil && time.Since(*autoDisabledAt) < reEnableCooldown { |
| 390 | return nil, ErrWebhookCooldown |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | // Build a dynamic UPDATE based on which fields are present. Using |
| 395 | // COALESCE keeps the query simple at the cost of always touching |
| 396 | // every column; at slice-1 webhook counts this isn't a concern. |
| 397 | args := []interface{}{webhookID, userID} |
| 398 | sets := []string{} |
| 399 | add := func(col string, val interface{}) { |
| 400 | args = append(args, val) |
| 401 | sets = append(sets, fmt.Sprintf("%s = $%d", col, len(args))) |
| 402 | } |
| 403 | if u.URL != nil { |
| 404 | add("url", *u.URL) |
| 405 | } |
| 406 | if u.Description != nil { |
| 407 | add("description", *u.Description) |
| 408 | } |
| 409 | if u.Events != nil { |
| 410 | add("events", *u.Events) |
| 411 | } |
| 412 | if u.Filters != nil { |
| 413 | filtersJSON, err := json.Marshal(*u.Filters) |
| 414 | if err != nil { |
| 415 | return nil, fmt.Errorf("marshal filters: %w", err) |
| 416 | } |
| 417 | add("filters", filtersJSON) |
| 418 | } |
| 419 | if u.Enabled != nil { |
| 420 | add("enabled", *u.Enabled) |
| 421 | // Re-enabling clears auto_disabled_at so a subsequent fail |
| 422 | // burst can re-trip it cleanly. |
| 423 | if *u.Enabled { |
| 424 | args = append(args, nil) |
| 425 | sets = append(sets, fmt.Sprintf("auto_disabled_at = $%d", len(args))) |
| 426 | } |
| 427 | } |
| 428 | |
| 429 | if len(sets) == 0 { |
| 430 | // No-op PATCH. Return the current row. |
| 431 | return s.GetWebhookByID(ctx, webhookID, userID) |