Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
params.clj 9.96 KiB
(ns metabase.models.params
  "Utility functions for dealing with parameters for Dashboards and Cards."
  (:require [clojure.set :as set]
            [metabase.query-processor.middleware.expand :as ql]
            metabase.query-processor.interface
            [metabase
             [db :as mdb]
             [util :as u]]
            [toucan
             [db :as db]
             [hydrate :refer [hydrate]]])
  (:import metabase.query_processor.interface.FieldPlaceholder))

;;; +----------------------------------------------------------------------------------------------------------------+
;;; |                                                     SHARED                                                     |
;;; +----------------------------------------------------------------------------------------------------------------+

(defn- field-form->id
  "Expand a `field-id` or `fk->` FORM and return the ID of the Field it references.

     (field-form->id [:field-id 100])  ; -> 100"
  [field-form]
  (when-let [field-placeholder (u/ignore-exceptions (ql/expand-ql-sexpr field-form))]
    (when (instance? FieldPlaceholder field-placeholder)
      (:field-id field-placeholder))))

(defn- field-ids->param-field-values
  "Given a collection of PARAM-FIELD-IDS return a map of FieldValues for the Fields they reference.
   This map is returned by various endpoints as `:param_values`."
  [param-field-ids]
  (when (seq param-field-ids)
    (u/key-by :field_id (db/select ['FieldValues :values :human_readable_values :field_id]
                          :field_id [:in param-field-ids]))))

(defn- template-tag->field-form
  "Fetch the `field-id` or `fk->` form from DASHCARD referenced by TEMPLATE-TAG.

     (template-tag->field-form [:template-tag :company] some-dashcard) ; -> [:field-id 100]"
  [[_ tag] dashcard]
  (get-in dashcard [:card :dataset_query :native :template_tags (keyword tag) :dimension]))

(defn- param-target->field-id
  "Parse a Card parameter TARGET form, which looks something like `[:dimension [:field-id 100]]`, and return the Field
  ID it references (if any)."
  [target dashcard]
  (when (ql/is-clause? :dimension target)
    (let [[_ dimension] target]
      (field-form->id (if (ql/is-clause? :template-tag dimension)
                        (template-tag->field-form dimension dashcard)
                        dimension)))))


(defn- pk-fields
  "Return the `fields` that are PK Fields."
  [fields]
  (filter #(isa? (:special_type %) :type/PK) fields))

(def ^:private Field:params-columns-only
  "Form for use in Toucan `db/select` expressions (as a drop-in replacement for using `Field`) that returns Fields with
  only the columns that are appropriate for returning in public/embedded API endpoints, which make heavy use of the
  functions in this namespace. Use `conj` to add additional Fields beyond the ones already here. Use `rest` to get
  just the column identifiers, perhaps for use with something like `select-keys`. Clutch!

    (db/select Field:params-columns-only)"
  ['Field :id :table_id :display_name :base_type :special_type])

(defn- fields->table-id->name-field
  "Given a sequence of `fields,` return a map of Table ID -> to a `:type/Name` Field in that Table, if one exists. In
  cases where more than one name Field exists for a Table, this just adds the first one it finds."
  [fields]
  (when-let [table-ids (seq (map :table_id fields))]
    (u/key-by :table_id (db/select (conj Field:params-columns-only :table_id)
                          :table_id     [:in table-ids]
                          :special_type (mdb/isa :type/Name)))))

(defn add-name-fields
  "For all `fields` that are `:type/PK` Fields, look for a `:type/Name` Field belonging to the same Table. For each
  Field, if a matching name Field exists, add it under the `:name_field` key. This is so the Fields can be used in
  public/embedded field values search widgets. This only includes the information needed to power those widgets, and
  no more."
  {:batched-hydrate :name_field}
  [fields]
  (let [table-id->name-field (fields->table-id->name-field (pk-fields fields))]
    (for [field fields]
      ;; add matching `:name_field` if it's a PK
      (assoc field :name_field (when (isa? (:special_type field) :type/PK)
                                 (table-id->name-field (:table_id field)))))))


;; We hydrate the `:human_readable_field` for each Dimension using the usual hydration logic, so it contains columns we
;; don't want to return. The two functions below work to remove the unneeded ones.

(defn- remove-dimension-nonpublic-columns
  "Strip nonpublic columns from a `dimension` and from its hydrated human-readable Field."
  [dimension]
  (-> dimension
      (update :human_readable_field #(select-keys % (rest Field:params-columns-only)))
      ;; these aren't exactly secret but you the frontend doesn't need them either so while we're at it let's go ahead
      ;; and strip them out
      (dissoc :created_at :updated_at)))

(defn- remove-dimensions-nonpublic-columns
  "Strip nonpublic columns from the hydrated human-readable Field in the hydrated Dimensions in `fields`."
  [fields]
  (for [field fields]
    (update field :dimensions
            (fn [dimension-or-dimensions]
              ;; as disucssed in `metabase.models.field` the hydration code for `:dimensions` is
              ;; WRONG and the value ends up either being a single Dimension or an empty vector.
              ;; However at some point we will fix this so deal with either a map or a sequence of
              ;; maps
              (cond
                (map? dimension-or-dimensions)
                (remove-dimension-nonpublic-columns dimension-or-dimensions)

                (sequential? dimension-or-dimensions)
                (map remove-dimension-nonpublic-columns dimension-or-dimensions))))))


(defn- param-field-ids->fields
  "Get the Fields (as a map of Field ID -> Field) that shoudl be returned for hydrated `:param_fields` for a Card or
  Dashboard. These only contain the minimal amount of information neccesary needed to power public or embedded
  parameter widgets."
  [field-ids]
  (when (seq field-ids)
    (u/key-by :id (-> (db/select (conj Field:params-columns-only :table_id) :id [:in field-ids])
                      (hydrate :has_field_values :name_field [:dimensions :human_readable_field])
                      remove-dimensions-nonpublic-columns))))

(defmulti ^:private ^{:hydrate :param_values} param-values
  "Add a `:param_values` map (Field ID -> FieldValues) containing FieldValues for the Fields referenced by the
  parameters of a Card or a Dashboard. Implementations are in respective sections below."
  name)

(defmulti ^:private ^{:hydrate :param_fields} param-fields
  "Add a `:param_fields` map (Field ID -> Field) for all of the Fields referenced by the parameters of a Card or
  Dashboard. Implementations are below in respective sections."
  name)

;;; +----------------------------------------------------------------------------------------------------------------+
;;; |                                               DASHBOARD-SPECIFIC                                               |
;;; +----------------------------------------------------------------------------------------------------------------+

(defn- dashboard->parameter-mapping-field-ids
  "Return the IDs of any Fields referenced directly by the Dashboard's `:parameters` (i.e., 'explicit' parameters) by
  looking at the appropriate `:parameter_mappings` entries for its Dashcards."
  [dashboard]
  (when-let [ids (seq (for [dashcard (:ordered_cards dashboard)
                            param    (:parameter_mappings dashcard)
                            :let     [field-id (param-target->field-id (:target param) dashcard)]
                            :when    field-id]
                        field-id))]
    (set ids)))

(declare card->template-tag-field-ids)

(defn- dashboard->card-param-field-ids
  "Return the IDs of any Fields referenced in the 'implicit' template tag field filter parameters for native queries in
  the Cards in `dashboard`."
  [dashboard]
  (reduce
   set/union
   (for [{card :card} (:ordered_cards dashboard)]
     (card->template-tag-field-ids card))))

(defn dashboard->param-field-ids
  "Return a set of Field IDs referenced by parameters in Cards in this DASHBOARD, or `nil` if none are referenced. This
  also includes IDs of Fields that are to be found in the 'implicit' parameters for SQL template tag Field filters."
  [dashboard]
  (let [dashboard (hydrate dashboard [:ordered_cards :card])]
    (set/union
     (dashboard->parameter-mapping-field-ids dashboard)
     (dashboard->card-param-field-ids dashboard))))

(defn- dashboard->param-field-values
  "Return a map of Field ID to FieldValues (if any) for any Fields referenced by Cards in DASHBOARD,
   or `nil` if none are referenced or none of them have FieldValues."
  [dashboard]
  (field-ids->param-field-values (dashboard->param-field-ids dashboard)))

(defmethod param-values "Dashboard" [dashboard]
  (dashboard->param-field-values dashboard))

(defmethod param-fields "Dashboard" [dashboard]
  (-> dashboard dashboard->param-field-ids param-field-ids->fields))


;;; +----------------------------------------------------------------------------------------------------------------+
;;; |                                                 CARD-SPECIFIC                                                  |
;;; +----------------------------------------------------------------------------------------------------------------+

(defn card->template-tag-field-ids
  "Return a set of Field IDs referenced in template tag parameters in CARD."
  [card]
  (set (for [[_ {dimension :dimension}] (get-in card [:dataset_query :native :template_tags])
             :when                      dimension
             :let                       [field-id (field-form->id dimension)]
             :when                      field-id]
         field-id)))

(defmethod param-values "Card" [card]
  (field-ids->param-field-values (card->template-tag-field-ids card)))

(defmethod param-fields "Card" [card]
  (-> card card->template-tag-field-ids param-field-ids->fields))