| (ns metabase.actions.api (:require [metabase.actions.actions :as actions] [metabase.actions.execution :as actions.execution] [metabase.actions.http-action :as actions.http-action] [metabase.actions.models :as action] [metabase.analytics.core :as analytics] [metabase.api.common :as api] [metabase.api.common.validation :as validation] [metabase.api.macros :as api.macros] [metabase.models.card :as card] [metabase.models.collection :as collection] [metabase.util :as u] [metabase.util.i18n :refer [deferred-tru tru]] [metabase.util.json :as json] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] [toucan2.core :as t2])) |
(set! *warn-on-reflection* true) | |
TODO -- by convention these should either have class-like names e.g. | (def ^:private json-query-schema
[:and
string?
(mu/with-api-error-message
[:fn #(actions.http-action/apply-json-query {} %)]
(deferred-tru "must be a valid json-query, something like ''.item.title''"))]) |
(def ^:private supported-action-type (mu/with-api-error-message [:enum "http" "query" "implicit"] (deferred-tru "Unsupported action type"))) | |
(def ^:private implicit-action-kind
(mu/with-api-error-message
(into [:enum]
(for [ns ["row" "bulk"]
action ["create" "update" "delete"]]
(str ns "/" action)))
(deferred-tru "Unsupported implicit action kind"))) | |
(def ^:private http-action-template
[:map {:closed true}
[:method [:enum "GET" "POST" "PUT" "DELETE" "PATCH"]]
[:url [string? {:min 1}]]
[:body {:optional true} [:maybe string?]]
[:headers {:optional true} [:maybe string?]]
[:parameters {:optional true} [:maybe [:sequential map?]]]
[:parameter_mappings {:optional true} [:maybe map?]]]) | |
(api.macros/defendpoint :get "/"
"Returns actions that can be used for QueryActions. By default lists all viewable actions. Pass optional
`?model-id=<model-id>` to limit to actions on a particular model."
[_route-params
{:keys [model-id]} :- [:map
[:model-id {:optional true} [:maybe ms/PositiveInt]]]]
(letfn [(actions-for [models]
(if (seq models)
(t2/hydrate (action/select-actions models
:model_id [:in (map :id models)]
:archived false)
:creator)
[]))]
;; We don't check the permissions on the actions, we assume they are readable if the model is readable.
(let [models (if model-id
[(api/read-check :model/Card model-id)]
(t2/select :model/Card {:where
[:and
[:= :type "model"]
[:= :archived false]
;; action permission keyed off of model permission
(collection/visible-collection-filter-clause)]}))]
(actions-for models)))) | |
(api.macros/defendpoint :get "/public" "Fetch a list of Actions with public UUIDs. These actions are publicly-accessible *if* public sharing is enabled." [] (validation/check-has-application-permission :setting) (validation/check-public-sharing-enabled) (t2/select [:model/Action :name :id :public_uuid :model_id], :public_uuid [:not= nil], :archived false)) | |
(api.macros/defendpoint :get "/:action-id"
"Fetch an Action."
[{:keys [action-id]} :- [:map
[:action-id ms/PositiveInt]]]
(-> (action/select-action :id action-id :archived false)
(t2/hydrate :creator)
api/read-check)) | |
(api.macros/defendpoint :delete "/:action-id"
"Delete an Action."
[{:keys [action-id]} :- [:map
[:action-id ms/PositiveInt]]]
(let [action (api/write-check :model/Action action-id)]
(analytics/track-event! :snowplow/action
{:event :action-deleted
:type (:type action)
:action_id action-id}))
(t2/delete! :model/Action :id action-id)
api/generic-204-no-content) | |
(api.macros/defendpoint :post "/"
"Create a new action."
[_route-params
_query-params
{:keys [type model_id parameters database_id]
:as action} :- [:map
[:name :string]
[:model_id ms/PositiveInt]
[:type {:optional true} [:maybe supported-action-type]]
[:description {:optional true} [:maybe :string]]
[:parameters {:optional true} [:maybe [:sequential map?]]]
[:parameter_mappings {:optional true} [:maybe map?]]
[:visualization_settings {:optional true} [:maybe map?]]
[:kind {:optional true} [:maybe implicit-action-kind]]
[:database_id {:optional true} [:maybe ms/PositiveInt]]
[:dataset_query {:optional true} [:maybe map?]]
[:template {:optional true} [:maybe http-action-template]]
[:response_handle {:optional true} [:maybe json-query-schema]]
[:error_handle {:optional true} [:maybe json-query-schema]]]]
(when (and (nil? database_id)
(= "query" type))
(throw (ex-info (tru "Must provide a database_id for query actions")
{:type type
:status-code 400})))
(let [model (api/write-check :model/Card model_id)]
(when (and (= "implicit" type)
(not (card/model-supports-implicit-actions? model)))
(throw (ex-info (tru "Implicit actions are not supported for models with clauses.")
{:status-code 400})))
(doseq [db-id (cond-> [(:database_id model)] database_id (conj database_id))]
(actions/check-actions-enabled-for-database!
(t2/select-one :model/Database :id db-id))))
(let [action-id (action/insert! (assoc action :creator_id api/*current-user-id*))]
(analytics/track-event! :snowplow/action
{:event :action-created
:type type
:action_id action-id
:num_parameters (count parameters)})
(if action-id
(action/select-action :id action-id)
;; t2/insert! does not return a value when used with h2
;; so we return the most recently updated http action.
(last (action/select-actions nil :type type))))) | |
(api.macros/defendpoint :put "/:id"
"Update an Action."
[{:keys [id]} :- [:map
[:id ms/PositiveInt]]
_query-params
action :- [:map
[:archived {:optional true} [:maybe :boolean]]
[:database_id {:optional true} [:maybe ms/PositiveInt]]
[:dataset_query {:optional true} [:maybe :map]]
[:description {:optional true} [:maybe :string]]
[:error_handle {:optional true} [:maybe json-query-schema]]
[:kind {:optional true} [:maybe implicit-action-kind]]
[:model_id {:optional true} [:maybe ms/PositiveInt]]
[:name {:optional true} [:maybe :string]]
[:parameter_mappings {:optional true} [:maybe :map]]
[:parameters {:optional true} [:maybe [:sequential :map]]]
[:response_handle {:optional true} [:maybe json-query-schema]]
[:template {:optional true} [:maybe http-action-template]]
[:type {:optional true} [:maybe supported-action-type]]
[:visualization_settings {:optional true} [:maybe :map]]]]
(actions/check-actions-enabled! id)
(let [existing-action (api/write-check :model/Action id)]
(action/update! (assoc action :id id) existing-action))
(let [{:keys [parameters type] :as action} (action/select-action :id id)]
(analytics/track-event! :snowplow/action
{:event :action-updated
:type type
:action_id id
:num_parameters (count parameters)})
action)) | |
(api.macros/defendpoint :post "/:id/public_link"
"Generate publicly-accessible links for this Action. Returns UUID to be used in public links. (If this
Action has already been shared, it will return the existing public link rather than creating a new one.) Public
sharing must be enabled."
[{:keys [id]} :- [:map
[:id ms/PositiveInt]]]
(api/check-superuser)
(validation/check-public-sharing-enabled)
(let [action (api/read-check :model/Action id :archived false)]
(actions/check-actions-enabled! action)
{:uuid (or (:public_uuid action)
(u/prog1 (str (random-uuid))
(t2/update! :model/Action id
{:public_uuid <>
:made_public_by_id api/*current-user-id*})))})) | |
(api.macros/defendpoint :delete "/:id/public_link"
"Delete the publicly-accessible link to this Dashboard."
[{:keys [id]} :- [:map
[:id ms/PositiveInt]]]
;; check the /application/setting permission, not superuser because removing a public link is possible from /admin/settings
(validation/check-has-application-permission :setting)
(validation/check-public-sharing-enabled)
(api/check-exists? :model/Action :id id, :public_uuid [:not= nil], :archived false)
(actions/check-actions-enabled! id)
(t2/update! :model/Action id {:public_uuid nil, :made_public_by_id nil})
{:status 204, :body nil}) | |
(api.macros/defendpoint :get "/:action-id/execute"
"Fetches the values for filling in execution parameters. Pass PK parameters and values to select."
[{:keys [action-id]} :- [:map
[:action-id ms/PositiveInt]]
{:keys [parameters]} :- [:map
[:parameters ms/JSONString]]]
(actions/check-actions-enabled! action-id)
(-> (action/select-action :id action-id :archived false)
api/read-check
(actions.execution/fetch-values (json/decode parameters)))) | |
(api.macros/defendpoint :post "/:id/execute"
"Execute the Action.
`parameters` should be the mapped dashboard parameters with values."
[{:keys [id]} :- [:map
[:id ms/PositiveInt]]
_query-params
{:keys [parameters], :as _body} :- [:maybe [:map
[:parameters {:optional true} [:maybe [:map-of :keyword any?]]]]]]
(let [{:keys [type] :as action} (api/check-404 (action/select-action :id id :archived false))]
(analytics/track-event! :snowplow/action
{:event :action-executed
:source :model_detail
:type type
:action_id id})
(actions.execution/execute-action! action (update-keys parameters name)))) | |