A set of utility function to track model changes. Use this when you want to observe changes of database models when doing stuffs on UI. How to use this?
You can use [[reset-changes!]] to clear our all the current trackings. And [[untrack-all!]] or [[untrack!]] to stop tracking. | (ns dev.model-tracking (:require [clojure.string :as str] [metabase.util :as u] [methodical.core :as m] [toucan2.core :as t2] [toucan2.model :as t2.model] [toucan2.tools.before-delete :as t2.before-delete] [toucan2.tools.before-insert :as t2.before-insert] [toucan2.tools.before-update :as t2.before-update] [toucan2.util :as t2.util])) |
An atom to store all the changes of models that we currently track. | (def changes* (atom {})) |
(def ^:private tracked-models (atom #{})) | |
(defn- indent [prefix s] (str/join "\n" (map (fn [line] (str prefix line)) (str/split-lines s)))) | |
(defn- sort-map-for-printing [m] (into (sorted-map-by (fn [a b] (cond (= a :id) -1 (= b :id) 1 :else (compare a b)))) m)) | |
When a change occurred, execute this function. Currently it just prints the console out to the console. But if you prefer other method of debugging (i.e: tap), you can redef this function (alter-var-root #'model-tracking/on-change (fn [path change] (tap> [path change])))
| #_:clj-kondo/ignore ;; printlns (defn on-change [path change-info] (println (u/colorize :magenta :new-change) (u/colorize :magenta path)) (println (->> change-info sort-map-for-printing u/pprint-to-str (indent " ")))) |
(defn- clean-change [change] (dissoc change :updated_at :created_at)) | |
Add a change to the [[changes]] atom.
For insert, track the instance as a map. For update, only track the changes. | (defn- new-change [model action row-or-instance] (let [model (t2/resolve-model model) change-info (->> (case action :update (into {} (t2/changes row-or-instance)) (into {} row-or-instance)) clean-change) path [(t2/table-name model) action]] ;; ideally this should be debug, but for some reasons this doesn't get logged (on-change path change-info) (swap! changes* update-in path concat [change-info]))) |
(defn- new-change-thunk [model action] (fn [_model row] (new-change model action row) row)) | |
A list of toucan hooks that we will subscribed to when tracking a model. | (def ^:private hook+aux-method+action+deriveable [;; will be better if we could use after-insert to get the inserted id, but toucan2 doesn't define a multimethod for after-insert [#'t2.before-insert/before-insert :after :insert ::t2.before-insert/before-insert] [#'t2.before-update/before-update :after :update ::t2.before-update/before-update] ;; we do :before aux-method instead of :after for delete bacause the after method has input is number of affected rows [#'t2.before-delete/before-delete :before :delete ::t2.before-delete/before-delete]]) |
(defn- track-one! [model] (doseq [[hook aux-method action deriveable] hook+aux-method+action+deriveable] (when-not (m/primary-method @hook model) ;; aux-method will not be triggered if there isn't a primary method (t2.util/maybe-derive model deriveable) (m/add-primary-method! hook model (fn [_ _model row] row))) (m/add-aux-method-with-unique-key! hook aux-method model (new-change-thunk model action) ::tracking))) | |
Start tracking a list of models. (track! :model/Card :model/Dashboard) | (defn track! [& models] (doseq [model (map t2.model/resolve-model models)] (track-one! model) (swap! tracked-models conj model))) |
(defn- untrack-one! [model] (doseq [[hook aux-method _action] hook+aux-method+action+deriveable] (m/remove-aux-method-with-unique-key! hook aux-method model ::tracking) (swap! tracked-models disj model))) | |
Remove tracking for a list of models. (untrack! 'Card 'Dashboard) | (defn untrack! [& models] (doseq [model (map t2.model/resolve-model models)] (untrack-one! model))) |
Empty all the recorded changes. | (defn reset-changes! [] (reset! changes* {})) |
Quickly untrack all the tracked models. | (defn untrack-all! [] (reset-changes!) (apply untrack! @tracked-models) (reset! tracked-models #{})) |
Return all changes that were recorded. | (defn changes [] @changes*) |