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 UserID string `gorm:"type:uuid;not null" json:"userId"` Name string `gorm:"not null" json:"name"` Description string `json:"description"` // 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 User User `json:"user,omitzero"` 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" }