Transform entity into a form suitable for serialization. | (ns metabase-enterprise.serialization.serialize (:require [clojure.string :as str] [medley.core :as m] [metabase-enterprise.serialization.names :refer [fully-qualified-name]] [metabase.legacy-mbql.normalize :as mbql.normalize] [metabase.lib.schema.id :as lib.schema.id] [metabase.lib.util.match :as lib.util.match] [metabase.models.field :as field] [metabase.models.interface :as mi] [metabase.models.visualization-settings :as mb.viz] [metabase.util :as u] [toucan2.core :as t2])) |
(set! *warn-on-reflection* true) | |
Current serialization protocol version. This gets stored with each dump, so we can correctly recover old dumps. | (def ^:const ^Long serialization-protocol-version ;; version 2 - start adding namespace portion to /collections/ paths 2) |
If entity_id should be included in v1 serialization dump | (def ^:dynamic *include-entity-id* false) |
Is given form an MBQL entity reference? | (def ^:private ^{:arglists '([form])} mbql-entity-reference?
(partial mbql.normalize/is-clause? #{:field :field-id :fk-> :metric :segment})) |
(defn- mbql-id->fully-qualified-name
[mbql]
(-> mbql
mbql.normalize/normalize-tokens
(lib.util.match/replace
;; `integer?` guard is here to make the operation idempotent
[:field (id :guard integer?) opts]
[:field (fully-qualified-name :model/Field id) (mbql-id->fully-qualified-name opts)]
;; field-id is still used within parameter mapping dimensions
;; example relevant clause - [:dimension [:fk-> [:field-id 1] [:field-id 2]]]
[:field-id (id :guard integer?)]
[:field-id (fully-qualified-name :model/Field id)]
;; source-field is also used within parameter mapping dimensions
;; example relevant clause - [:field 2 {:source-field 1}]
{:source-field (id :guard integer?)}
(assoc &match :source-field (fully-qualified-name :model/Field id))
[:segment (id :guard integer?)]
[:segment (fully-qualified-name :model/Segment id)]))) | |
(defn- ids->fully-qualified-names
[entity]
(lib.util.match/replace entity
mbql-entity-reference?
(mbql-id->fully-qualified-name &match)
map?
(as-> &match entity
(m/update-existing entity :database (fn [db-id]
(if (= db-id lib.schema.id/saved-questions-virtual-database-id)
"database/__virtual"
(fully-qualified-name :model/Database db-id))))
(m/update-existing entity :card_id (partial fully-qualified-name :model/Card)) ; attibutes that refer to db fields use _
(m/update-existing entity :card-id (partial fully-qualified-name :model/Card)) ; template-tags use dash
(m/update-existing entity :source-table (fn [source-table]
(if (and (string? source-table)
(str/starts-with? source-table "card__"))
(fully-qualified-name :model/Card (-> source-table
(str/split #"__")
second
Integer/parseInt))
(fully-qualified-name :model/Table source-table))))
(m/update-existing entity :breakout (fn [breakout]
(map mbql-id->fully-qualified-name breakout)))
(m/update-existing entity :aggregation (fn [aggregation]
(m/map-vals mbql-id->fully-qualified-name aggregation)))
(m/update-existing entity :filter (fn [filter]
(m/map-vals mbql-id->fully-qualified-name filter)))
(m/update-existing entity ::mb.viz/param-mapping-source (partial fully-qualified-name :model/Field))
(m/update-existing entity :snippet-id (partial fully-qualified-name :model/NativeQuerySnippet))
(m/map-vals ids->fully-qualified-names entity)))) | |
Removes unneeded fields that can either be reconstructed from context or are meaningless (eg. :created_at). | (defn- strip-crud
[entity]
(cond-> (dissoc entity :id :creator_id :created_at :updated_at :db_id :location :last_used_at
:dashboard_id :fields_hash :personal_owner_id :made_public_by_id :collection_id
:pulse_id :result_metadata :action_id)
(not *include-entity-id*) (dissoc :entity_id)
(some #(instance? % entity) (map type [:model/Field :model/Segment])) (dissoc :table_id))) |
(defmulti ^:private serialize-one
{:arglists '([instance])}
mi/model) | |
Serialize entity | (def ^{:arglists '([entity])} serialize
(comp ids->fully-qualified-names strip-crud serialize-one)) |
(defmethod serialize-one :default [instance] instance) | |
(defmethod serialize-one :model/Database [db] (dissoc db :features)) | |
(defmethod serialize-one :model/Field
[field]
(let [field (-> field
(update :parent_id (partial fully-qualified-name :model/Field))
(update :fk_target_field_id (partial fully-qualified-name :model/Field)))]
(if (contains? field :values)
(update field :values u/select-non-nil-keys [:values :human_readable_values])
(assoc field :values (-> field
field/values
(u/select-non-nil-keys [:values :human_readable_values])))))) | |
(defn- convert-column-settings-key [k]
(if-let [field-id (::mb.viz/field-id k)]
(-> (t2/select-one :model/Field :id field-id)
fully-qualified-name
mb.viz/field-str->column-ref)
k)) | |
The | (defn- convert-param-mapping-key [k] (mbql-id->fully-qualified-name k)) |
(defn- convert-param-ref [new-id param-ref]
(cond-> param-ref
(= "dimension" (::mb.viz/param-ref-type param-ref)) ids->fully-qualified-names
(some? new-id) (update ::mb.viz/param-ref-id new-id))) | |
(defn- convert-param-mapping-val [new-id v]
(-> v
(m/update-existing ::mb.viz/param-mapping-source (partial convert-param-ref new-id))
(m/update-existing ::mb.viz/param-mapping-target (partial convert-param-ref new-id))
(m/assoc-some ::mb.viz/param-mapping-id (or new-id (::mb.viz/param-mapping-id v))))) | |
(defn- convert-parameter-mapping [param-mapping]
(if (nil? param-mapping)
nil
(reduce-kv (fn [acc k v]
(assoc acc (convert-param-mapping-key k)
(convert-param-mapping-val nil v))) {} param-mapping))) | |
(defn- convert-click-behavior [{:keys [::mb.viz/link-type ::mb.viz/link-target-id] :as click}]
(-> (if-let [new-target-id (case link-type
::mb.viz/card (-> (t2/select-one :model/Card :id link-target-id)
fully-qualified-name)
::mb.viz/dashboard (-> (t2/select-one :model/Dashboard :id link-target-id)
fully-qualified-name)
nil)]
(assoc click ::mb.viz/link-target-id new-target-id)
click)
(m/update-existing ::mb.viz/parameter-mapping convert-parameter-mapping))) | |
(defn- convert-column-settings-value [{:keys [::mb.viz/click-behavior] :as v}]
(cond (not-empty click-behavior) (assoc v ::mb.viz/click-behavior (convert-click-behavior click-behavior))
:else v)) | |
(defn- convert-column-settings [acc k v] (assoc acc (convert-column-settings-key k) (convert-column-settings-value v))) | |
(defn- convert-viz-settings [viz-settings]
(-> (mb.viz/db->norm viz-settings)
(m/update-existing ::mb.viz/column-settings (fn [col-settings]
(reduce-kv convert-column-settings {} col-settings)))
(m/update-existing ::mb.viz/click-behavior convert-click-behavior)
mb.viz/norm->db)) | |
(defn- dashboard-cards-for-dashboard
[dashboard]
(let [dashboard-cards (t2/select :model/DashboardCard :dashboard_id (u/the-id dashboard))
series (when (not-empty dashboard-cards)
(t2/select :model/DashboardCardSeries
:dashboardcard_id [:in (map u/the-id dashboard-cards)]))]
(for [dashboard-card dashboard-cards]
(-> dashboard-card
(assoc :series (for [series series
:when (= (:dashboardcard_id series) (u/the-id dashboard-card))]
(-> series
(update :card_id (partial fully-qualified-name :model/Card))
(dissoc :id :dashboardcard_id))))
(assoc :visualization_settings (convert-viz-settings (:visualization_settings dashboard-card)))
strip-crud)))) | |
(defmethod serialize-one :model/Dashboard [dashboard] (assoc dashboard :dashboard_cards (dashboard-cards-for-dashboard dashboard))) | |
(defmethod serialize-one :model/Card
[card]
(-> card
(m/update-existing :table_id (partial fully-qualified-name :model/Table))
(update :database_id (partial fully-qualified-name :model/Database))
(m/update-existing :visualization_settings convert-viz-settings))) | |
(defmethod serialize-one :model/Pulse
[pulse]
(assoc pulse
:cards (for [card (t2/select :model/PulseCard :pulse_id (u/the-id pulse))]
(-> card
(dissoc :id :pulse_id)
(update :card_id (partial fully-qualified-name :model/Card))))
:channels (for [channel (t2/select :model/PulseChannel :pulse_id (u/the-id pulse))]
(strip-crud channel)))) | |
(defmethod serialize-one :model/User [user] (select-keys user [:first_name :last_name :email :is_superuser])) | |
(defmethod serialize-one :model/Dimension
[dimension]
(-> dimension
(update :field_id (partial fully-qualified-name :model/Field))
(update :human_readable_field_id (partial fully-qualified-name :model/Field)))) | |
(defmethod serialize-one :model/NativeQuerySnippet [snippet] (select-keys snippet [:name :description :content])) | |