A notification have: - a payload - more than one subscriptions - more than one handlers where each handler has a channel, optionally a template, and more than one recpients.

(ns metabase.models.notification
  (:require
   [medley.core :as m]
   [metabase.models.interface :as mi]
   [metabase.util :as u]
   [metabase.util.malli :as mu]
   [metabase.util.malli.schema :as ms]
   [methodical.core :as methodical]
   [toucan2.core :as t2]))
(set! *warn-on-reflection* true)
(methodical/defmethod t2/table-name :model/Notification             [_model] :notification)
(methodical/defmethod t2/table-name :model/NotificationSubscription [_model] :notification_subscription)
(methodical/defmethod t2/table-name :model/NotificationHandler      [_model] :notification_handler)
(methodical/defmethod t2/table-name :model/NotificationRecipient    [_model] :notification_recipient)
(doseq [model [:model/Notification
               :model/NotificationSubscription
               :model/NotificationHandler
               :model/NotificationRecipient]]
  (doto model
    (derive :metabase/model)
    (derive (if (= model :model/NotificationSubscription)
              :hook/created-at-timestamped?
              :hook/timestamped?))))

------------------------------------------------------------------------------------------------;; :model/Notification ;; ------------------------------------------------------------------------------------------------;;

Set of valid notification types.

(def notification-types
  #{:notification/system-event
    :notification/dashboard
    :notification/card
    ;; for testing only
    :notification/testing})
(t2/deftransforms :model/Notification
  {:payload_type (mi/transform-validator mi/transform-keyword (partial mi/assert-enum notification-types))})
(t2/define-after-select :model/Notification
  [notification]
  (dissoc notification :internal_id))
(methodical/defmethod t2/batched-hydrate [:model/Notification :subscriptions]
  "Batch hydration NotificationSubscriptions for a list of Notifications."
  [_model k notifications]
  (mi/instances-with-hydrated-data
   notifications k
   #(group-by :notification_id
              (t2/select :model/NotificationSubscription :notification_id [:in (map :id notifications)]))
   :id
   {:default nil}))
(methodical/defmethod t2/batched-hydrate [:model/Notification :handlers]
  "Batch hydration NotificationHandlers for a list of Notifications"
  [_model k notifications]
  (mi/instances-with-hydrated-data
   notifications k
   #(group-by :notification_id
              (t2/select :model/NotificationHandler :notification_id [:in (map :id notifications)]))
   :id
   {:default nil}))
(defn- delete-trigger-for-subscription!
  [& args]
  (apply (requiring-resolve 'metabase.task.notification/delete-trigger-for-subscription!) args))
(t2/define-before-delete :model/Notification
  [instance]
  (doseq [subscription-ids (t2/select-pks-set :model/NotificationSubscription :notification_id (:id instance))]
    (delete-trigger-for-subscription! subscription-ids))
  instance)

------------------------------------------------------------------------------------------------;; :model/NotificationSubscription ;; ------------------------------------------------------------------------------------------------;;

(def ^:private subscription-types
  #{:notification-subscription/system-event
    :notification-subscription/cron})
(t2/deftransforms :model/NotificationSubscription
  {:type       (mi/transform-validator mi/transform-keyword (partial mi/assert-enum subscription-types))
   :event_name (mi/transform-validator mi/transform-keyword (partial mi/assert-namespaced "event"))})
(def ^:private NotificationSubscription
  [:merge [:map
           [:type            (apply ms/enum-keywords-and-strings subscription-types)]]
   [:multi {:dispatch (comp keyword :type)}
    [:notification-subscription/system-event
     [:map
      [:type                           [:enum :notification-subscription/system-event]]
      [:event_name                     [:or :keyword :string]]
      [:cron_schedule {:optional true} nil?]]]
    [:notification-subscription/cron
     [:map
      [:type                           [:enum :notification-subscription/cron]]
      [:cron_schedule                  :string]
      [:event_name    {:optional true} nil?]]]]])

Validate a NotificationSubscription.

(defn- validate-subscription
  [subscription]
  (mu/validate-throw NotificationSubscription subscription))
(t2/define-before-insert :model/NotificationSubscription
  [instance]
  (validate-subscription instance)
  instance)
(defn- update-subscription-trigger!
  [& args]
  (apply (requiring-resolve 'metabase.task.notification/update-subscription-trigger!) args))
(t2/define-after-insert :model/NotificationSubscription
  [instance]
  (update-subscription-trigger! instance)
  instance)
(t2/define-before-update :model/NotificationSubscription
  [instance]
  (validate-subscription instance)
  (update-subscription-trigger! instance)
  instance)
(t2/define-before-delete :model/NotificationSubscription
  [instance]
  (delete-trigger-for-subscription! (:id instance))
  instance)

------------------------------------------------------------------------------------------------;; :model/NotificationHandler ;; ------------------------------------------------------------------------------------------------;;

(t2/deftransforms :model/NotificationHandler
  {:channel_type (mi/transform-validator mi/transform-keyword (partial mi/assert-namespaced "channel"))})
(methodical/defmethod t2/batched-hydrate [:model/NotificationHandler :channel]
  "Batch hydration Channels for a list of NotificationHandlers"
  [_model k notification-handlers]
  (mi/instances-with-hydrated-data
   notification-handlers k
   #(t2/select-fn->fn :id identity :model/Channel
                      :id [:in (map :channel_id notification-handlers)]
                      :active true)
   :channel_id
   {:default nil}))
(methodical/defmethod t2/batched-hydrate [:model/NotificationHandler :template]
  "Batch hydration ChannelTemplates for a list of NotificationHandlers"
  [_model k notification-handlers]
  (mi/instances-with-hydrated-data
   notification-handlers k
   #(t2/select-fn->fn :id identity :model/ChannelTemplate
                      :id [:in (map :template_id notification-handlers)])
   :template_id
   {:default nil}))
(methodical/defmethod t2/batched-hydrate [:model/NotificationHandler :recipients]
  "Batch hydration NotificationRecipients for a list of NotificationHandlers"
  [_model k notification-handlers]
  (mi/instances-with-hydrated-data
   notification-handlers
   k
   #(group-by :notification_handler_id
              (let [recipients       (t2/select :model/NotificationRecipient
                                                :notification_handler_id [:in (map :id notification-handlers)])
                    type->recipients (group-by :type recipients)]
                (-> type->recipients
                    (m/update-existing :notification-recipient/user
                                       (fn [recipients]
                                         (t2/hydrate recipients :user)))
                    (m/update-existing :notification-recipient/group
                                       (fn [recipients]
                                         (t2/hydrate recipients [:permissions_group :members])))
                    vals
                    flatten)))
   :id
   {:default []}))
(defn- cross-check-channel-type-and-template-type
  [notification-handler]
  (when-let [template-id (:template_id notification-handler)]
    (let [channel-type  (keyword (:channel_type notification-handler))
          template-type (t2/select-one-fn :channel_type [:model/ChannelTemplate :channel_type] template-id)]
      (when (not= channel-type template-type)
        (throw (ex-info "Channel type and template type mismatch"
                        {:status        400
                         :channel-type  channel-type
                         :template-type template-type}))))))
(t2/define-before-insert :model/NotificationHandler
  [instance]
  (cross-check-channel-type-and-template-type instance)
  instance)
(t2/define-before-update :model/NotificationHandler
  [instance]
  (when (or (contains? (t2/changes instance) :channel_id)
            (contains? (t2/changes instance) :template_id))
    (cross-check-channel-type-and-template-type instance)
    instance))

------------------------------------------------------------------------------------------------;; :model/NotificationRecipient ;; ------------------------------------------------------------------------------------------------;;

(def ^:private notification-recipient-types
  #{:notification-recipient/user
    :notification-recipient/group
    :notification-recipient/external-email
    :notification-recipient/template})
(t2/deftransforms :model/NotificationRecipient
  {:type    (mi/transform-validator mi/transform-keyword (partial mi/assert-enum notification-recipient-types))
   :details mi/transform-json})

Schema for :model/NotificationRecipient.

(def NotificationRecipient
  [:merge [:map
           [:type (into [:enum] notification-recipient-types)]
           [:notification_handler_id ms/PositiveInt]]
   [:multi {:dispatch :type}
    [:notification-recipient/user
     [:map
      [:user_id                               ms/PositiveInt]
      [:permissions_group_id {:optional true} [:fn nil?]]
      [:details              {:optional true} [:fn empty?]]]]
    [:notification-recipient/group
     [:map
      [:permissions_group_id                  ms/PositiveInt]
      [:user_id              {:optional true} [:fn nil?]]
      [:details              {:optional true} [:fn empty?]]]]
    [:notification-recipient/external-email
     [:map
      [:details                               [:map {:closed true}
                                               [:email ms/Email]]]
      [:user_id              {:optional true} [:fn nil?]]
      [:permissions_group_id {:optional true} [:fn nil?]]]]
    [:notification-recipient/template
     [:map
      [:details                               [:map {:closed true}
                                               [:pattern                      :string]
                                               [:is_optional {:optional true} :boolean]]]
      [:user_id              {:optional true} [:fn nil?]]
      [:permissions_group_id {:optional true} [:fn nil?]]]]]])
(defn- check-valid-recipient
  [recipient]
  (mu/validate-throw NotificationRecipient recipient))
(t2/define-before-insert :model/NotificationRecipient
  [instance]
  (check-valid-recipient instance)
  instance)
(t2/define-before-update :model/NotificationRecipient
  [instance]
  (check-valid-recipient instance)
  instance)

------------------------------------------------------------------------------------------------;; Public APIs ;; ------------------------------------------------------------------------------------------------;;

Find all active notifications for a given event.

(defn notifications-for-event
  [event-name]
  (t2/select :model/Notification
             {:select    [:n.*]
              :from      [[:notification :n]]
              :left-join [[:notification_subscription :ns] [:= :n.id :ns.notification_id]]
              :where     [:and
                          [:= :n.active true]
                          [:= :ns.event_name (u/qualified-name event-name)]
                          [:= :ns.type (u/qualified-name :notification-subscription/system-event)]]}))

Create a new notification with subsciptions. Return the created notification.

(defn create-notification!
  [notification subscriptions handlers+recipients]
  (t2/with-transaction [_conn]
    (let [instance (t2/insert-returning-instance! :model/Notification notification)
          id       (:id instance)]
      (when (seq subscriptions)
        (t2/insert! :model/NotificationSubscription (map #(assoc % :notification_id id) subscriptions)))
      (doseq [handler handlers+recipients]
        (let [recipients (:recipients handler)
              handler    (-> handler
                             (dissoc :recipients)
                             (assoc :notification_id id))
              handler-id (t2/insert-returning-pk! :model/NotificationHandler handler)]
          (t2/insert! :model/NotificationRecipient (map #(assoc % :notification_handler_id handler-id) recipients))))
      instance)))