(ns metabase.actions.execution (:require [clojure.set :as set] [medley.core :as m] [metabase.actions.actions :as actions] [metabase.actions.http-action :as http-action] [metabase.actions.models :as action] [metabase.analytics.core :as analytics] [metabase.api.common :as api] [metabase.legacy-mbql.schema :as mbql.s] [metabase.lib.schema.actions :as lib.schema.actions] [metabase.lib.schema.id :as lib.schema.id] [metabase.model-persistence.core :as model-persistence] [metabase.models.query :as query] [metabase.query-processor :as qp] [metabase.query-processor.card :as qp.card] [metabase.query-processor.error-type :as qp.error-type] [metabase.query-processor.middleware.permissions :as qp.perms] [metabase.query-processor.writeback :as qp.writeback] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.log :as log] [metabase.util.malli :as mu] [toucan2.core :as t2])) | |
Execute a
| (defn- execute-query-action!
[{:keys [dataset_query model_id] :as action} request-parameters]
(log/tracef "Executing action\n\n%s" (u/pprint-to-str action))
(try
(let [parameters (for [parameter (:parameters action)]
(assoc parameter :value (get request-parameters (:id parameter))))
query (-> dataset_query
(update :type keyword)
(assoc :parameters parameters))]
(log/debugf "Query (before preprocessing):\n\n%s" (u/pprint-to-str query))
(binding [qp.perms/*card-id* model_id]
(qp.writeback/execute-write-query! query)))
(catch Throwable e
(if (= (:type (u/all-ex-data e)) qp.error-type/missing-required-permissions)
(api/throw-403 e)
(throw (ex-info (format "Error executing Action: %s" (ex-message e))
{:action action
:parameters request-parameters}
e)))))) |
(mu/defn- implicit-action-table
[card-id :- pos-int?]
(let [card (t2/select-one :model/Card :id card-id)
{:keys [table-id]} (query/query->database-and-table-ids (:dataset_query card))]
(t2/hydrate (t2/select-one :model/Table :id table-id) :fields))) | |
(defn- execute-custom-action! [action request-parameters]
(let [{action-type :type} action]
(actions/check-actions-enabled! action)
(let [model (t2/select-one :model/Card :id (:model_id action))]
(when (and (= action-type :query) (not= (:database_id model) (:database_id action)))
;; the above check checks the db of the model. We check the db of the query action here
(actions/check-actions-enabled-for-database!
(t2/select-one :model/Database :id (:database_id action)))))
(try
(case action-type
:query
(execute-query-action! action request-parameters)
:http
(http-action/execute-http-action! action request-parameters))
(catch Exception e
(log/error e "Error executing action.")
(if-let [ed (ex-data e)]
(let [ed (cond-> ed
(and (nil? (:status-code ed))
(= (:type ed) :missing-required-permissions))
(assoc :status-code 403)
(nil? (:message ed))
(assoc :message (ex-message e)))]
(if (= (ex-data e) ed)
(throw e)
(throw (ex-info (ex-message e) ed e))))
{:body {:message (or (ex-message e) (tru "Error executing action."))}
:status 500}))))) | |
Check that the given request parameters do not contain any parameters that are not in the given set of destination parameter ids | (defn- check-no-extra-parameters
[request-parameters destination-param-ids]
(let [extra-parameters (set/difference (set (keys request-parameters))
(set destination-param-ids))]
(api/check (empty? extra-parameters)
400
{:status-code 400
:message (tru "No destination parameter found for {0}. Found: {1}"
(pr-str extra-parameters)
(pr-str destination-param-ids))
:type qp.error-type/invalid-parameter
:parameters request-parameters
:destination-parameters destination-param-ids}))) |
(mu/defn- build-implicit-query :- [:map
[:query ::mbql.s/Query]
[:row-parameters ::lib.schema.actions/row]
;; TODO -- the schema for these should probably be
;; `:metabase.lib.schema.parameter/parameter` instead of `:any`, but I'm not
;; 100% sure about that.
[:prefetch-parameters {:optional true} [:tuple :any]]]
[{:keys [model_id parameters] :as _action} implicit-action request-parameters]
(let [{database-id :db_id
table-id :id :as table} (implicit-action-table model_id)
table-fields (:fields table)
pk-fields (filterv #(isa? (:semantic_type %) :type/PK) table-fields)
slug->field-name (->> table-fields
(map (juxt (comp u/slugify :name) :name))
(into {})
(m/filter-keys (set (map :id parameters))))
_ (api/check (action/unique-field-slugs? table-fields)
400
(tru "Cannot execute implicit action on a table with ambiguous column names."))
_ (api/check (= (count pk-fields) 1)
400
(tru "Must execute implicit action on a table with a single primary key."))
_ (check-no-extra-parameters request-parameters (keys slug->field-name))
pk-field (first pk-fields)
;; Ignore params with nil values; the client doesn't reliably omit blank, optional parameters from the
;; request. See discussion at #29049
simple-parameters (->> (update-keys request-parameters slug->field-name)
(filter (fn [[_k v]] (some? v)))
(into {}))
pk-field-name (:name pk-field)
row-parameters (cond-> simple-parameters
(not= implicit-action :row/create) (dissoc pk-field-name))
requires-pk? (contains? #{:row/delete :row/update} implicit-action)]
(api/check (or (not requires-pk?)
(some? (get simple-parameters pk-field-name)))
400
(tru "Missing primary key parameter: {0}"
(pr-str (u/slugify (:name pk-field)))))
(cond-> {:query {:database database-id,
:type :query,
:query {:source-table table-id}}
:row-parameters row-parameters}
requires-pk?
(assoc-in [:query :query :filter]
[:= [:field (:id pk-field) nil] (get simple-parameters pk-field-name)])
requires-pk?
(assoc :prefetch-parameters [{:target [:dimension [:field (:id pk-field) nil]]
:type "id"
:value [(get simple-parameters pk-field-name)]}])))) | |
(defn- execute-implicit-action!
[action request-parameters]
(let [implicit-action (keyword (:kind action))
{:keys [query row-parameters]} (build-implicit-query action implicit-action request-parameters)
_ (api/check (or (= implicit-action :row/delete) (seq row-parameters))
400
(tru "Implicit parameters must be provided."))
arg-map (cond-> query
(= implicit-action :row/create)
(assoc :create-row row-parameters)
(= implicit-action :row/update)
(assoc :update-row row-parameters))]
(binding [qp.perms/*card-id* (:model_id action)]
(actions/perform-action! implicit-action arg-map)))) | |
Execute the given action with the given parameters of shape `{ | (mu/defn execute-action!
[action request-parameters]
(let [;; if a value is supplied for a hidden parameter, it should raise an error
field-settings (get-in action [:visualization_settings :fields])
hidden-param-ids (->> (vals field-settings)
(filter :hidden)
(map :id))
destination-param-ids (set/difference (set (map :id (:parameters action))) (set hidden-param-ids))
_ (check-no-extra-parameters request-parameters destination-param-ids)
;; add default values for missing parameters (including hidden ones)
all-param-ids (set (map :id (:parameters action)))
provided-param-ids (set (keys request-parameters))
missing-param-ids (set/difference all-param-ids provided-param-ids)
missing-param-defaults (into {}
(keep (fn [param-id]
(when-let [default-value (get-in field-settings [param-id :defaultValue])]
[param-id default-value])))
missing-param-ids)
request-parameters (merge missing-param-defaults request-parameters)]
(case (:type action)
:implicit
(execute-implicit-action! action request-parameters)
(:query :http)
(execute-custom-action! action request-parameters)
(throw (ex-info (tru "Unknown action type {0}." (name (:type action))) action))))) |
Execute the given action in the dashboard/dashcard context with the given parameters
of shape `{ | (mu/defn execute-dashcard!
[dashboard-id :- ::lib.schema.id/dashboard
dashcard-id :- ::lib.schema.id/dashcard
request-parameters :- [:maybe [:map-of :string :any]]]
(let [dashcard (api/check-404 (t2/select-one :model/DashboardCard
:id dashcard-id
:dashboard_id dashboard-id))
action (api/check-404 (action/select-action :id (:action_id dashcard)))]
(analytics/track-event! :snowplow/action
{:event :action-executed
:source :dashboard
:type (:type action)
:action_id (:id action)})
(execute-action! action request-parameters))) |
(defn- fetch-implicit-action-values
[action request-parameters]
(api/check (contains? #{"row/update" "row/delete"} (:kind action))
400
(tru "Values can only be fetched for actions that require a Primary Key."))
(let [implicit-action (keyword (:kind action))
{:keys [prefetch-parameters]} (build-implicit-query action implicit-action request-parameters)
info {:executed-by api/*current-user-id*
:context :action
:action-id (:id action)}
card (t2/select-one :model/Card :id (:model_id action))
;; prefilling a form with day old data would be bad
result (model-persistence/with-persisted-substituion-disabled
(qp/process-query
(qp/userland-query
(qp.card/query-for-card card prefetch-parameters nil nil)
info)))
;; only expose values for fields that are not hidden
hidden-param-ids (keep #(when (:hidden %) (:id %))
(vals (get-in action [:visualization_settings :fields])))
exposed-param-ids (-> (set (map :id (:parameters action)))
(set/difference (set hidden-param-ids)))]
(m/filter-keys
#(contains? exposed-param-ids %)
(zipmap
(map (comp u/slugify :name) (get-in result [:data :cols]))
(first (get-in result [:data :rows])))))) | |
Fetch values to pre-fill implicit action execution - custom actions will return no values.
Must pass in parameters of shape | (defn fetch-values
[action request-parameters]
(if (= :implicit (:type action))
(fetch-implicit-action-values action request-parameters)
{})) |