(ns metabase.db.custom-migrations.pulse-to-notification (:require [clojure.string :as str] [metabase.util.json :as json] [toucan2.core :as t2])) | |
Build a cron string from key-value pair parts. | (defn- cron-string
[{:keys [seconds minutes hours day-of-month month day-of-week year]}]
(str/join " " [(or seconds "0")
(or minutes "0")
(or hours "*")
(or day-of-month "*")
(or month "*")
(or day-of-week "?")
(or year "*")])) |
(def ^:private day-of-week->cron
{"sun" 1
"mon" 2
"tue" 3
"wed" 4
"thu" 5
"fri" 6
"sat" 7}) | |
(defn- frame->cron [frame day-of-week]
(if day-of-week
;; specific days of week like Mon or Fri
(assoc {:day-of-month "?"}
:day-of-week (case frame
"first" (str (day-of-week->cron day-of-week) "#1")
"last" (str (day-of-week->cron day-of-week) "L")))
;; specific CALENDAR DAYS like 1st or 15th
(assoc {:day-of-week "?"}
:day-of-month (case frame
"first" "1"
"mid" "15"
"last" "L")))) | |
Convert the frontend schedule map into a cron string. | (defn- schedule-map->cron-string
[{day-of-week :schedule_day, hour :schedule_hour, minute :schedule_minute,
frame :schedule_frame, schedule-type :schedule_type}]
(cron-string (case (keyword schedule-type)
:hourly {:minutes minute}
:daily {:hours (or hour 0)}
:weekly {:hours hour
:day-of-week (day-of-week->cron day-of-week)
:day-of-month "?"}
:monthly (assoc (frame->cron frame day-of-week)
:hours hour)))) |
Create a new notification with | (defn- create-notification!
[notification subscriptions handlers+recipients]
(let [notification-card-id (t2/insert-returning-pk! :notification_card (:payload notification))
instance (t2/insert-returning-instance! :notification (-> notification
(dissoc :payload)
(assoc :payload_id notification-card-id)))
notification-id (:id instance)]
(when (seq subscriptions)
(t2/insert! :notification_subscription (map #(assoc % :notification_id notification-id) subscriptions)))
(doseq [handler handlers+recipients]
(let [recipients (:recipients handler)
handler (-> handler
(dissoc :recipients)
(assoc :notification_id notification-id))
handler-id (t2/insert-returning-pk! :notification_handler handler)]
(when (seq recipients)
(t2/insert! :notification_recipient (map #(assoc % :notification_handler_id handler-id) recipients)))))
instance)) |
(defn- hydrate-recipients
[pcs]
(when (seq pcs)
(let [pc-id->recipients (group-by :pulse_channel_id
(t2/select :pulse_channel_recipient :pulse_channel_id [:in (map :id pcs)]))]
(map (fn [pc]
(assoc pc :recipients (get pc-id->recipients (:id pc))))
pcs)))) | |
Create a new notification with | (defn- alert->notification!
[pulse]
(let [pulse-id (:id pulse)
pcs (hydrate-recipients (t2/select :pulse_channel :pulse_id pulse-id :enabled true))
;; alerts have one pulse-card, but to be safe we select the latest one by id
pulse-card (t2/select-one :pulse_card :pulse_id pulse-id {:order-by [[:id :desc]]})]
;; the old schema allow one alert to have multiple pulse-channels. Practically they all have the same schedule
;; but to be safe we group them by schedule and create a notification for each group
(doall
(for [pcs (vals (group-by (juxt :schedule_type :schedule_hour :schedule_day :schedule_frame) pcs))]
(let [notification {:payload_type "notification/card"
:payload {:card_id (:card_id pulse-card)
:send_once (true? (:alert_first_only pulse))
:send_condition (if (= "goal" (:alert_condition pulse))
(if (:alert_above_goal pulse)
"goal_above"
"goal_below")
"has_result")
:created_at (:created_at pulse)
:updated_at (:updated_at pulse)}
:active (not (:archived pulse))
:creator_id (:creator_id pulse)
:created_at (:created_at pulse)
:updated_at (:updated_at pulse)}
pc (first pcs)
subscriptions [{:type "notification-subscription/cron"
:cron_schedule (schedule-map->cron-string pc)
:created_at (:created_at (first pcs))}]
handlers (map (fn [pc]
(merge
{:active (:enabled pc)}
(case (:channel_type pc)
"email"
{:channel_type "channel/email"
:template_id nil
:recipients (concat
(map (fn [recipient]
{:type "notification-recipient/user"
:user_id (:user_id recipient)})
(:recipients pc))
(map (fn [email]
{:type "notification-recipient/raw-value"
:details (json/encode {:value email})})
(some-> pc :details json/decode (get "emails"))))}
"slack"
{:channel_type "channel/slack"
:template_id nil
:recipients [{:type "notification-recipient/raw-value"
:details (json/encode {:value (-> pc :details json/decode (get "channel"))})}]}
"http"
{:channel_type "channel/http"
:channel_id (:channel_id pc)})))
pcs)]
(create-notification! notification subscriptions handlers)))))) |
Migrate alerts from | (defn migrate-alerts!
[]
#_{:clj-kondo/ignore [:unresolved-symbol]}
(run! alert->notification!
(t2/reducible-query {:select [:*]
:from [:pulse]
:where [:and [:in :alert_condition ["rows" "goal"]] [:not :archived]]}))) |
(comment
(t2/delete! :model/Notification)
(migrate-alerts!)
(t2/hydrate (t2/select :model/Notification) :subscriptions
[:handlers :recipients :template])) | |