package database import ( "math" "strconv" "time" "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 // MaxFiniteRetentionDays is the largest finite retention period the // reaper's cutoff arithmetic can represent, and therefore the // largest one a caller may request. It is derived from that // arithmetic rather than picked: retentionCutoff computes // retentionDays * hoursPerDay * time.Hour, and a time.Duration is // an int64 nanosecond count, so math.MaxInt64 nanoseconds divided // by an hour and then by a day is the exact ceiling — 106751 days, // a little over 292 years. // // One day more overflows int64, wraps the product negative, and // turns the cutoff into a timestamp in the far future that matches // every row in the webhook's database. That is why this bound is // enforced on input and why retentionCutoff saturates underneath // it. Note that RetentionForeverDays deliberately sits above this // ceiling: such webhooks are skipped before any cutoff is // computed, and never reach the arithmetic at all. MaxFiniteRetentionDays = int( math.MaxInt64 / int64(time.Hour) / hoursPerDay, ) ) // Webhook represents a webhook processing unit that groups entrypoints and targets // // Every method below takes a pointer receiver. BeforeSave has to, // because it mutates the record and GORM only invokes hooks declared // that way; the display helpers follow suit so the receiver kinds do // not mix. Handlers therefore put a *Webhook into template data: // html/template cannot call a pointer method on a value held in a map, // because a map element is not addressable. 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 a stored RetentionDays value means // "keep events indefinitely". It is the single definition of that // question, shared by Webhook.RetainsForever and by the reaper's // cutoff computation so the two cannot disagree about which webhooks // are exempt from reaping. // // 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 retainsForever(retentionDays int) bool { return retentionDays <= 0 || retentionDays >= RetentionForeverDays } // RetainsForever reports whether this webhook's events are kept // indefinitely. func (w *Webhook) RetainsForever() bool { return retainsForever(w.RetentionDays) } // 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" }