Allow retention_days of 0 to mean retain forever (closes #79)
All checks were successful
check / check (push) Successful in 3m4s
All checks were successful
check / check (push) Successful in 3m4s
RetentionDays carried gorm:"default:30", so GORM substituted 30 for a zero value while building the insert. A webhook could therefore never be configured to keep its events indefinitely: the reaper's retain-forever branch existed but was unreachable from the normal create and edit flows. Introduce database.RetentionForeverDays = 365 * 1000 as the sentinel for "retain forever" and a Webhook.BeforeSave hook that rewrites any non-positive RetentionDays to it. The rewrite has to live in the hook rather than at the call sites: GORM applies the column default while converting the model to insert values, which happens after BeforeSave, so anything later loses that race. Putting it on the model also means a future call site, such as the planned REST API, cannot bypass it. The reaper now skips a webhook when Webhook.RetainsForever reports true, which recognises the sentinel and keeps honouring the old <= 0 values for rows written before it existed. Without this the sentinel, being positive, would have produced a cutoff a thousand years in the past and a DELETE matching nothing on every sweep. Form handling is shared by create and edit through parseRetentionDays so the two cannot drift: an empty field keeps the previous behaviour (default on create, unchanged on edit), 0 is honoured, and an unparseable or negative value is a 400 that re-renders the form rather than a silently substituted default. The retention inputs drop max="365". That cap was not cosmetic: the edit form pre-fills the stored value, so a retain-forever webhook rendered 365000 into an input capped at 365 and browser validation would have blocked saving any edit to it. min becomes 0 with a hint explaining what 0 does, and the list and detail views render a RetentionLabel of "forever" instead of a raw day count. The 30-day default is consolidated into database.DefaultRetentionDays, referenced from the handler and from the create form's pre-filled value, with a test asserting it agrees with the struct tag that cannot reference it.
This commit is contained in:
@@ -1,6 +1,39 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultRetentionDays is the event retention period applied to a
|
||||
// webhook created without an explicit retention value. It is the
|
||||
// single source of truth for that policy and must stay in sync
|
||||
// with the `gorm:"default:30"` column default on
|
||||
// Webhook.RetentionDays below; a struct tag cannot reference a
|
||||
// constant, so a test asserts the two agree.
|
||||
DefaultRetentionDays = 30
|
||||
|
||||
// RetentionForeverDays is the sentinel RetentionDays value meaning
|
||||
// "retain events forever". Users express that intent as 0, which
|
||||
// Webhook.BeforeSave rewrites to this value: the column default
|
||||
// substitutes DefaultRetentionDays for a zero value at insert
|
||||
// time, so a zero can never survive a round trip to the database.
|
||||
// Nothing outside this file may hardcode the number.
|
||||
RetentionForeverDays = 365 * 1000
|
||||
)
|
||||
|
||||
// Webhook represents a webhook processing unit that groups entrypoints and targets
|
||||
//
|
||||
// The receiver kinds below are deliberately mixed. BeforeSave has to
|
||||
// take a pointer because it mutates the record, and GORM only invokes
|
||||
// hooks declared that way. RetainsForever and RetentionLabel have to
|
||||
// take values because html/template calls them on webhooks held in a
|
||||
// template data map, which reflection cannot address; a pointer
|
||||
// receiver there fails at render time rather than at compile time.
|
||||
//
|
||||
//nolint:recvcheck // GORM needs a pointer hook; templates need values.
|
||||
type Webhook struct {
|
||||
BaseModel
|
||||
|
||||
@@ -8,7 +41,9 @@ type Webhook struct {
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Description string `json:"description"`
|
||||
|
||||
// RetentionDays is the number of days to retain events.
|
||||
// RetentionDays is the number of days to retain events. A value of
|
||||
// RetentionForeverDays means retain forever. The column default
|
||||
// must equal DefaultRetentionDays.
|
||||
RetentionDays int `gorm:"default:30" json:"retentionDays"`
|
||||
|
||||
// Relations
|
||||
@@ -16,3 +51,44 @@ type Webhook struct {
|
||||
Entrypoints []Entrypoint `json:"entrypoints,omitempty"`
|
||||
Targets []Target `json:"targets,omitempty"`
|
||||
}
|
||||
|
||||
// BeforeSave normalises RetentionDays on every insert and update. A
|
||||
// non-positive value is the user's way of asking for "retain forever",
|
||||
// which is stored as the RetentionForeverDays sentinel.
|
||||
//
|
||||
// This has to happen in a hook rather than at the call sites. GORM
|
||||
// substitutes the column default (DefaultRetentionDays) for a zero
|
||||
// value while building the insert statement, which runs after
|
||||
// BeforeSave; rewriting any later than this loses that race and the
|
||||
// row lands at 30 days. Living on the model also means a future call
|
||||
// site — a REST API, a fixture, a migration — cannot bypass it.
|
||||
func (w *Webhook) BeforeSave(_ *gorm.DB) error {
|
||||
if w.RetentionDays <= 0 {
|
||||
w.RetentionDays = RetentionForeverDays
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RetainsForever reports whether this webhook's events are kept
|
||||
// indefinitely. It accepts the RetentionForeverDays sentinel written by
|
||||
// BeforeSave and, defensively, the non-positive values that rows
|
||||
// written before the sentinel existed may still carry.
|
||||
func (w Webhook) RetainsForever() bool {
|
||||
return w.RetentionDays <= 0 ||
|
||||
w.RetentionDays >= RetentionForeverDays
|
||||
}
|
||||
|
||||
// RetentionLabel returns the webhook's retention policy as display
|
||||
// text, so that no template has to know about the sentinel value.
|
||||
func (w Webhook) RetentionLabel() string {
|
||||
if w.RetainsForever() {
|
||||
return "forever"
|
||||
}
|
||||
|
||||
if w.RetentionDays == 1 {
|
||||
return "1 day"
|
||||
}
|
||||
|
||||
return strconv.Itoa(w.RetentionDays) + " days"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user