store: migration 0014 — notification_channels + notification_log
This commit is contained in:
@@ -36,3 +36,36 @@ func TestMigration0013AlertsLastSeen(t *testing.T) {
|
||||
t.Fatalf("alerts.last_seen_at not present after migration; cols=%v", cols)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigration0014NotificationsTables(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
st, err := Open(context.Background(), filepath.Join(dir, "rm.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
for _, want := range []string{"notification_channels", "notification_log"} {
|
||||
var n int
|
||||
if err := st.DB().QueryRow(
|
||||
`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?`, want,
|
||||
).Scan(&n); err != nil {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Errorf("table %q missing after migration", want)
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity: kind CHECK accepts all three v1 kinds.
|
||||
for _, k := range []string{"webhook", "ntfy", "smtp"} {
|
||||
_, err := st.DB().Exec(
|
||||
`INSERT INTO notification_channels (id, kind, name, config, created_at, updated_at)
|
||||
VALUES (?, ?, ?, x'00', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')`,
|
||||
"test-"+k, k, "test-"+k)
|
||||
if err != nil {
|
||||
t.Errorf("insert %q rejected by CHECK: %v", k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
-- 0014_notifications.sql
|
||||
--
|
||||
-- Notification channels (operator-configured destinations: webhook,
|
||||
-- ntfy, SMTP) and the dispatch log. Both are net-new — no rebuild
|
||||
-- pattern needed.
|
||||
--
|
||||
-- config is an AEAD-encrypted JSON blob. Per-kind shape lives in
|
||||
-- internal/notification/{webhook,ntfy,smtp}.go. The CHECK keeps wire
|
||||
-- consistency — adding a new kind requires a follow-up migration
|
||||
-- (forces the implementer to think about it).
|
||||
|
||||
CREATE TABLE notification_channels (
|
||||
id TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('webhook', 'ntfy', 'smtp')),
|
||||
name TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
||||
config BLOB NOT NULL, -- AEAD-encrypted JSON; per-kind shape
|
||||
default_priority TEXT, -- ntfy only; null for webhook + smtp
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
last_fired_at TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX notification_channels_enabled
|
||||
ON notification_channels(enabled) WHERE enabled = 1;
|
||||
|
||||
CREATE TABLE notification_log (
|
||||
id TEXT PRIMARY KEY,
|
||||
channel_id TEXT NOT NULL REFERENCES notification_channels(id) ON DELETE CASCADE,
|
||||
alert_id TEXT REFERENCES alerts(id) ON DELETE SET NULL,
|
||||
event TEXT NOT NULL, -- alert.raised | alert.acknowledged | alert.resolved | alert.test
|
||||
ok INTEGER NOT NULL CHECK (ok IN (0, 1)),
|
||||
status_code INTEGER,
|
||||
latency_ms INTEGER,
|
||||
error TEXT,
|
||||
fired_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX notification_log_channel
|
||||
ON notification_log(channel_id, fired_at DESC);
|
||||
CREATE INDEX notification_log_alert
|
||||
ON notification_log(alert_id);
|
||||
Reference in New Issue
Block a user