Metabase API endpoints for viewing publicly-accessible Cards and Dashboards. | (ns metabase.api.public (:require [medley.core :as m] [metabase.actions.core :as actions] [metabase.analytics.core :as analytics] [metabase.api.card :as api.card] [metabase.api.common :as api] [metabase.api.common.validation :as validation] [metabase.api.dashboard :as api.dashboard] [metabase.api.dataset :as api.dataset] [metabase.api.field :as api.field] [metabase.api.macros :as api.macros] [metabase.db.query :as mdb.query] [metabase.events :as events] [metabase.lib.schema.id :as lib.schema.id] [metabase.lib.schema.info :as lib.schema.info] [metabase.lib.util.match :as lib.util.match] [metabase.models.card :as card] [metabase.models.interface :as mi] [metabase.models.params :as params] [metabase.query-processor.card :as qp.card] [metabase.query-processor.dashboard :as qp.dashboard] [metabase.query-processor.error-type :as qp.error-type] [metabase.query-processor.middleware.constraints :as qp.constraints] [metabase.query-processor.middleware.permissions :as qp.perms] [metabase.query-processor.pipeline :as qp.pipeline] [metabase.query-processor.pivot :as qp.pivot] [metabase.query-processor.streaming :as qp.streaming] [metabase.request.core :as request] [metabase.util :as u] [metabase.util.embed :as embed] [metabase.util.i18n :refer [tru]] [metabase.util.json :as json] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] [throttle.core :as throttle] [toucan2.core :as t2]) (:import (clojure.lang ExceptionInfo))) |
(set! *warn-on-reflection* true) | |
(def ^:private ^:const ^Integer default-embed-max-height 800) (def ^:private ^:const ^Integer default-embed-max-width 1024) | |
-------------------------------------------------- Public Cards -------------------------------------------------- | |
Update On native queries parameters exists in 2 forms: - parameters - dataset_query.native.template-tags In most cases, these 2 are sync, meaning, if you have a template-tag, there will be a parameter. However, since card.parameters is a recently added feature, there may be instances where a template-tag is not present in the parameters. This function ensures that all template-tags are converted to parameters and added to card.parameters. | (defn combine-parameters-and-template-tags
[{:keys [parameters] :as card}]
(let [template-tag-parameters (card/template-tag-parameters card)
id->template-tags-parameter (m/index-by :id template-tag-parameters)
id->parameter (m/index-by :id parameters)]
(assoc card :parameters (vals (reduce-kv (fn [acc id parameter]
;; order importance: we want the info from `template-tag` to be merged last
(update acc id #(merge % parameter)))
id->parameter
id->template-tags-parameter))))) |
Remove everyting from public | (defn- remove-card-non-public-columns
[card]
;; We need to check this to resolve params - we set `request/as-admin` there
(if qp.perms/*param-values-query*
card
(mi/instance
:model/Card
(u/select-nested-keys card [:id :name :description :display :visualization_settings :parameters
[:dataset_query :type [:native :template-tags]]])))) |
Return a public Card matching key-value | (defn public-card
[& conditions]
(binding [params/*ignore-current-user-perms-and-return-all-field-values* true]
(-> (api/check-404 (apply t2/select-one [:model/Card :id :dataset_query :description :display :name :parameters :visualization_settings]
:archived false, conditions))
remove-card-non-public-columns
combine-parameters-and-template-tags
(t2/hydrate :param_values :param_fields)))) |
(defn- card-with-uuid [uuid] (public-card :public_uuid uuid)) | |
(api.macros/defendpoint :get "/card/:uuid"
"Fetch a publicly-accessible Card an return query results as well as `:card` information. Does not require auth
credentials. Public sharing must be enabled."
[{:keys [uuid]} :- [:map
[:uuid ms/UUIDString]]]
(validation/check-public-sharing-enabled)
(u/prog1 (card-with-uuid uuid)
(events/publish-event! :event/card-read {:object-id (:id <>), :user-id api/*current-user-id*, :context :question}))) | |
Transform results to be suitable for a public endpoint | (defmulti ^:private transform-qp-result
{:arglists '([results])}
:status) |
(defmethod transform-qp-result :default [x] x) | |
(defmethod transform-qp-result :completed
[results]
(u/select-nested-keys
results
[[:data :cols :rows :rows_truncated :insights :requested_timezone :results_timezone]
[:json_query :parameters]
:status])) | |
(defmethod transform-qp-result :failed
[{error-type :error_type, :as results}]
;; if the query failed instead, unless the error type is specified and is EXPLICITLY allowed to be shown for embeds,
;; instead of returning anything about the query just return a generic error message
(merge
(select-keys results [:status :error :error_type])
(when-not (qp.error-type/show-in-embeds? error-type)
{:error (tru "An error occurred while running the query.")}))) | |
Create the | (defn- process-query-for-card-with-id-run-fn
[qp export-format]
(fn run [query info]
(qp.streaming/streaming-response [rff export-format (u/slugify (:card-name info))]
(binding [qp.pipeline/*result* (comp qp.pipeline/*result* transform-qp-result)]
(request/as-admin
(qp (update query :info merge info) rff)))))) |
(mu/defn- export-format->context :- ::lib.schema.info/context
[export-format]
(case export-format
"csv" :public-csv-download
"xlsx" :public-xlsx-download
"json" :public-json-download
:public-question)) | |
Run the query belonging to Card with | (mu/defn process-query-for-card-with-id
[card-id :- ::lib.schema.id/card
export-format
parameters
& {:keys [qp]
:or {qp qp.card/process-query-for-card-default-qp}
:as options}]
;; run this query with full superuser perms
;;
;; we actually need to bind the current user perms here twice, once so `card-api` will have the full perms when it
;; tries to do the `read-check`, and a second time for when the query is ran (async) so the QP middleware will have
;; the correct perms
(request/as-admin
(m/mapply qp.card/process-query-for-card card-id export-format
:parameters parameters
:context (export-format->context export-format)
:qp qp
:make-run process-query-for-card-with-id-run-fn
options))) |
Run query for a public Card with UUID. If public sharing is not enabled, this throws an exception. Returns a
| (defn ^:private process-query-for-card-with-public-uuid
[uuid export-format parameters & options]
(validation/check-public-sharing-enabled)
(let [card-id (api/check-404 (t2/select-one-pk :model/Card :public_uuid uuid, :archived false))]
(apply process-query-for-card-with-id card-id export-format parameters options))) |
(api.macros/defendpoint :get "/card/:uuid/query"
"Fetch a publicly-accessible Card an return query results as well as `:card` information. Does not require auth
credentials. Public sharing must be enabled."
[{:keys [uuid]} :- [:map
[:uuid ms/UUIDString]]
{:keys [parameters]} :- [:map
[:parameters {:optional true} [:maybe ms/JSONString]]]]
(process-query-for-card-with-public-uuid uuid :api (json/decode+kw parameters))) | |
(api.macros/defendpoint :get "/card/:uuid/query/:export-format"
"Fetch a publicly-accessible Card and return query results in the specified format. Does not require auth
credentials. Public sharing must be enabled."
[{:keys [uuid export-format]} :- [:map
[:uuid ms/UUIDString]
[:export-format api.dataset/ExportFormat]]
{:keys [parameters format_rows pivot_results]} :- [:map
[:format_rows {:default false} :boolean]
[:pivot_results {:default false} :boolean]
[:parameters {:optional true} [:maybe ms/JSONString]]]]
(process-query-for-card-with-public-uuid
uuid
export-format
(json/decode+kw parameters)
:constraints nil
:middleware {:process-viz-settings? true
:js-int-to-string? false
:format-rows? format_rows
:pivot? pivot_results})) | |
----------------------------------------------- Public Dashboards ------------------------------------------------ | |
The only keys for an action that should be visible to the general public. | (def ^:private action-public-keys
#{:name
:id
:database_id ;; needed to check if the database has actions enabled on the frontend
:visualization_settings
:parameters}) |
Returns a public version of | (defn- public-action
[action]
(let [hidden-parameter-ids (->> (get-in action [:visualization_settings :fields])
vals
(keep (fn [x]
(when (true? (:hidden x))
(:id x))))
set)]
(-> action
(update :parameters (fn [parameters]
(remove #(contains? hidden-parameter-ids (:id %)) parameters)))
(update-in [:visualization_settings :fields] (fn [fields]
(m/remove-keys hidden-parameter-ids fields)))
(select-keys action-public-keys)))) |
Return a public Dashboard matching key-value | (defn public-dashboard
[& conditions]
{:pre [(even? (count conditions))]}
(binding [params/*ignore-current-user-perms-and-return-all-field-values* true
params/*field-id-context* (atom params/empty-field-id-context)]
(-> (api/check-404 (apply t2/select-one [:model/Dashboard :name :description :id :parameters :auto_apply_filters :width], :archived false, conditions))
(t2/hydrate [:dashcards :card :series :dashcard/action] :tabs :param_values :param_fields)
api.dashboard/add-query-average-durations
(update :dashcards (fn [dashcards]
(for [dashcard dashcards]
(-> (select-keys dashcard [:id :card :card_id :dashboard_id :series :col :row :size_x :dashboard_tab_id
:size_y :parameter_mappings :visualization_settings :action])
(update :card remove-card-non-public-columns)
(update :series (fn [series]
(for [series series]
(remove-card-non-public-columns series))))
(m/update-existing :action public-action)))))))) |
(defn- dashboard-with-uuid [uuid] (public-dashboard :public_uuid uuid)) | |
(api.macros/defendpoint :get "/dashboard/:uuid"
"Fetch a publicly-accessible Dashboard. Does not require auth credentials. Public sharing must be enabled."
[{:keys [uuid]} :- [:map
[:uuid ms/UUIDString]]]
(validation/check-public-sharing-enabled)
(u/prog1 (dashboard-with-uuid uuid)
(events/publish-event! :event/dashboard-read {:object-id (:id <>), :user-id api/*current-user-id*}))) | |
Return the results of running a query for Card with
Throws a 404 immediately if the Card isn't part of the Dashboard. Returns a | (defn process-query-for-dashcard
{:arglists '([& {:keys [dashboard-id card-id dashcard-id export-format parameters] :as options}])}
[& {:keys [export-format parameters qp]
:or {qp qp.card/process-query-for-card-default-qp
export-format :api}
:as options}]
(let [options (merge
{:context :public-dashboard
:constraints (qp.constraints/default-query-constraints)}
options
{:parameters (cond-> parameters
(string? parameters) json/decode+kw)
:export-format export-format
:qp qp
:make-run process-query-for-card-with-id-run-fn})]
;; Run this query with full superuser perms. We don't want the various perms checks failing because there are no
;; current user perms; if this Dashcard is public you're by definition allowed to run it without a perms check
;; anyway
(request/as-admin
(m/mapply qp.dashboard/process-query-for-dashcard options)))) |
(api.macros/defendpoint :get "/dashboard/:uuid/dashcard/:dashcard-id/card/:card-id"
"Fetch the results for a Card in a publicly-accessible Dashboard. Does not require auth credentials. Public
sharing must be enabled."
[{:keys [uuid dashcard-id card-id]} :- [:map
[:uuid ms/UUIDString]
[:dashcard-id ms/PositiveInt]
[:card-id ms/PositiveInt]]
{:keys [parameters]} :- [:map
[:parameters {:optional true} [:maybe ms/JSONString]]]]
(validation/check-public-sharing-enabled)
(api/check-404 (t2/select-one-pk :model/Card :id card-id :archived false))
(let [dashboard-id (api/check-404 (t2/select-one-pk :model/Dashboard :public_uuid uuid, :archived false))]
(u/prog1 (process-query-for-dashcard
:dashboard-id dashboard-id
:card-id card-id
:dashcard-id dashcard-id
:export-format :api
:parameters parameters)
(events/publish-event! :event/card-read {:object-id card-id, :user-id api/*current-user-id*, :context :dashboard})))) | |
(api.macros/defendpoint :post ["/dashboard/:uuid/dashcard/:dashcard-id/card/:card-id/:export-format"
:export-format api.dataset/export-format-regex]
"Fetch the results of running a publicly-accessible Card belonging to a Dashboard and return the data in one of the
export formats. Does not require auth credentials. Public sharing must be enabled."
[{:keys [uuid dashcard-id card-id export-format]} :- [:map
[:uuid ms/UUIDString]
[:dashcard-id ms/PositiveInt]
[:card-id ms/PositiveInt]
[:export-format (into [:enum] api.dataset/export-formats)]]
_query-parameters
{:keys [format_rows pivot_results parameters]} :- [:map
[:parameters {:optional true} [:maybe
{:decode/api
(fn [x]
(cond-> x
(string? x) json/decode+kw))}
[:sequential :map]]]
[:format_rows {:default false} ms/BooleanValue]
[:pivot_results {:default false} ms/BooleanValue]]]
(validation/check-public-sharing-enabled)
(api/check-404 (t2/select-one-pk :model/Card :id card-id :archived false))
(let [dashboard-id (api/check-404 (t2/select-one-pk :model/Dashboard :public_uuid uuid, :archived false))]
(u/prog1 (process-query-for-dashcard
:dashboard-id dashboard-id
:card-id card-id
:dashcard-id dashcard-id
:export-format export-format
:parameters parameters
:constraints nil
:middleware {:process-viz-settings? true
:format-rows? format_rows
:pivot? pivot_results})))) | |
(api.macros/defendpoint :get "/dashboard/:uuid/dashcard/:dashcard-id/execute"
"Fetches the values for filling in execution parameters. Pass PK parameters and values to select."
[{:keys [uuid dashcard-id]} :- [:map
[:uuid ms/UUIDString]
[:dashcard-id ms/PositiveInt]]
{:keys [parameters]} :- [:map
[:parameters ms/JSONString]]]
(validation/check-public-sharing-enabled)
(api/check-404 (t2/select-one-pk :model/Dashboard :public_uuid uuid :archived false))
(actions/fetch-values
(api/check-404 (actions/dashcard->action dashcard-id))
(json/decode parameters))) | |
(def ^:private dashcard-execution-throttle (throttle/make-throttler :dashcard-id :attempts-threshold 5000)) | |
(api.macros/defendpoint :post "/dashboard/:uuid/dashcard/:dashcard-id/execute"
"Execute the associated Action in the context of a `Dashboard` and `DashboardCard` that includes it.
`parameters` should be the mapped dashboard parameters with values."
[{:keys [uuid dashcard-id]} :- [:map
[:uuid ms/UUIDString]
[:dashcard-id ms/PositiveInt]]
_query-params
{:keys [parameters], :as _body} :- [:map
[:parameters {:optional true} [:maybe [:map-of :keyword :any]]]]]
(let [throttle-message (try
(throttle/check dashcard-execution-throttle dashcard-id)
nil
(catch ExceptionInfo e
(get-in (ex-data e) [:errors :dashcard-id])))
throttle-time (when throttle-message
(second (re-find #"You must wait ([0-9]+) seconds" throttle-message)))]
(if throttle-message
(cond-> {:status 429
:body throttle-message}
throttle-time (assoc :headers {"Retry-After" throttle-time}))
(do
(validation/check-public-sharing-enabled)
(let [dashboard-id (api/check-404 (t2/select-one-pk :model/Dashboard :public_uuid uuid, :archived false))]
;; Run this query with full superuser perms. We don't want the various perms checks
;; failing because there are no current user perms; if this Dashcard is public
;; you're by definition allowed to run it without a perms check anyway
(request/as-admin
;; Undo middleware string->keyword coercion
(actions/execute-dashcard! dashboard-id dashcard-id (update-keys parameters name)))))))) | |
(api.macros/defendpoint :get "/oembed"
"oEmbed endpoint used to retreive embed code and metadata for a (public) Metabase URL."
[_route-params
{:keys [url maxheight maxwidth]}
:- [:map
[:url ms/NonBlankString]
[:format {:optional true} [:maybe
{:description (str "The format param is not used by the API, but is required as"
" part of the oEmbed spec: http://oembed.com/#section2 just"
" return an error if `format` is specified and it's anything"
" other than `json`.")}
[:enum "json"]]]
[:maxheight {:default default-embed-max-height} pos-int?]
[:maxwidth {:default default-embed-max-width} pos-int?]]]
{:version "1.0"
:type "rich"
:width maxwidth
:height maxheight
:html (embed/iframe url maxwidth maxheight)}) | |
----------------------------------------------- Public Action ------------------------------------------------ | |
(api.macros/defendpoint :get "/action/:uuid"
"Fetch a publicly-accessible Action. Does not require auth credentials. Public sharing must be enabled."
[{:keys [uuid]} :- [:map
[:uuid ms/UUIDString]]]
(validation/check-public-sharing-enabled)
(let [action (api/check-404 (actions/select-action :public_uuid uuid :archived false))]
(actions/check-actions-enabled! action)
(public-action action))) | |
+----------------------------------------------------------------------------------------------------------------+ | FieldValues, Search, Remappings | +----------------------------------------------------------------------------------------------------------------+ | |
-------------------------------------------------- Field Values -------------------------------------------------- | |
Get the IDs of all Fields referenced by an MBQL | (defn- query->referenced-field-ids [query] (lib.util.match/match (:query query) [:field id _] id)) |
Return a set of all Field IDs referenced by | (defn- card->referenced-field-ids
[card]
(set (concat (query->referenced-field-ids (:dataset_query card))
(params/card->template-tag-field-ids card)))) |
Check to make sure the query for Card with | (defn- check-field-is-referenced-by-card
[field-id card-id]
(let [card (api/check-404 (t2/select-one [:model/Card :dataset_query] :id card-id))
referenced-field-ids (card->referenced-field-ids card)]
(api/check-404 (contains? referenced-field-ids field-id)))) |
Check whether a search Field is allowed to be used in conjunction with another Field. A search Field is allowed if any of the following conditions is true:
If none of these conditions are met, you are not allowed to use the search field in combination with the other field, and an 400 exception will be thrown. | (defn- check-search-field-is-allowed
[field-id search-field-id]
{:pre [(integer? field-id) (integer? search-field-id)]}
(api/check-400
(or (= field-id search-field-id)
(t2/exists? :model/Dimension :field_id field-id, :human_readable_field_id search-field-id)
;; just do a couple small queries to figure this out, we could write a fancy query to join Field against itself
;; and do this in one but the extra code complexity isn't worth it IMO
(when-let [table-id (t2/select-one-fn :table_id :model/Field :id field-id, :semantic_type (mdb.query/isa :type/PK))]
(t2/exists? :model/Field :id search-field-id, :table_id table-id, :semantic_type (mdb.query/isa :type/Name)))))) |
Check that | (defn- check-field-is-referenced-by-dashboard
[field-id dashboard-id]
(let [dashboard (-> (t2/select-one :model/Dashboard :id dashboard-id)
api/check-404
(t2/hydrate [:dashcards :card]))
param-field-ids (params/dashcards->param-field-ids (:dashcards dashboard))]
(api/check-404 (contains? param-field-ids field-id)))) |
Return the FieldValues for a Field with | (defn card-and-field-id->values [card-id field-id] (check-field-is-referenced-by-card field-id card-id) (api.field/field->values (t2/select-one :model/Field :id field-id))) |
(api.macros/defendpoint :get "/card/:uuid/field/:field-id/values"
"Fetch FieldValues for a Field that is referenced by a public Card."
[{:keys [uuid field-id]} :- [:map
[:uuid ms/UUIDString]
[:field-id ms/PositiveInt]]]
(validation/check-public-sharing-enabled)
(let [card-id (t2/select-one-pk :model/Card :public_uuid uuid, :archived false)]
(card-and-field-id->values card-id field-id))) | |
Return the FieldValues for a Field with | (defn dashboard-and-field-id->values [dashboard-id field-id] (check-field-is-referenced-by-dashboard field-id dashboard-id) (api.field/field->values (t2/select-one :model/Field :id field-id))) |
(api.macros/defendpoint :get "/dashboard/:uuid/field/:field-id/values"
"Fetch FieldValues for a Field that is referenced by a Card in a public Dashboard."
[{:keys [uuid field-id]} :- [:map
[:uuid ms/UUIDString]
[:field-id ms/PositiveInt]]]
(validation/check-public-sharing-enabled)
(let [dashboard-id (api/check-404 (t2/select-one-pk :model/Dashboard :public_uuid uuid, :archived false))]
(dashboard-and-field-id->values dashboard-id field-id))) | |
--------------------------------------------------- Searching ---------------------------------------------------- | |
Wrapper for | (defn search-card-fields [card-id field-id search-id value limit] (check-field-is-referenced-by-card field-id card-id) (check-search-field-is-allowed field-id search-id) (api.field/search-values (t2/select-one :model/Field :id field-id) (t2/select-one :model/Field :id search-id) value limit)) |
Wrapper for | (defn search-dashboard-fields [dashboard-id field-id search-id value limit] (check-field-is-referenced-by-dashboard field-id dashboard-id) (check-search-field-is-allowed field-id search-id) (api.field/search-values (t2/select-one :model/Field :id field-id) (t2/select-one :model/Field :id search-id) value limit)) |
(api.macros/defendpoint :get "/card/:uuid/field/:field-id/search/:search-field-id"
"Search for values of a Field that is referenced by a public Card."
[{:keys [uuid field-id search-field-id]} :- [:map
[:uuid ms/UUIDString]
[:field-id ms/PositiveInt]
[:search-field-id ms/PositiveInt]]
{:keys [value limit]} :- [:map
[:value ms/NonBlankString]
[:limit {:optional true} [:maybe ms/PositiveInt]]]]
(validation/check-public-sharing-enabled)
(let [card-id (t2/select-one-pk :model/Card :public_uuid uuid, :archived false)]
(search-card-fields card-id field-id search-field-id value limit))) | |
(api.macros/defendpoint :get "/dashboard/:uuid/field/:field-id/search/:search-field-id"
"Search for values of a Field that is referenced by a Card in a public Dashboard."
[{:keys [uuid field-id search-field-id]} :- [:map
[:uuid ms/UUIDString]
[:field-id ms/PositiveInt]
[:search-field-id ms/PositiveInt]]
{:keys [value limit]} :- [:map
[:value ms/NonBlankString]
[:limit {:optional true} [:maybe ms/PositiveInt]]]]
(validation/check-public-sharing-enabled)
(let [dashboard-id (api/check-404 (t2/select-one-pk :model/Dashboard :public_uuid uuid, :archived false))]
(search-dashboard-fields dashboard-id field-id search-field-id value limit))) | |
--------------------------------------------------- Remappings --------------------------------------------------- | |
(defn- field-remapped-values [field-id remapped-field-id, ^String value-str]
(let [field (api/check-404 (t2/select-one :model/Field :id field-id))
remapped-field (api/check-404 (t2/select-one :model/Field :id remapped-field-id))]
(check-search-field-is-allowed field-id remapped-field-id)
(api.field/remapped-value field remapped-field (api.field/parse-query-param-value-for-field field value-str)))) | |
Return the reampped Field values for a Field referenced by a Card. This explanation is almost useless, so see the
one in | (defn card-field-remapped-values [card-id field-id remapped-field-id, ^String value-str] (check-field-is-referenced-by-card field-id card-id) (field-remapped-values field-id remapped-field-id value-str)) |
Return the reampped Field values for a Field referenced by a Dashboard. This explanation is almost useless, so see
the one in | (defn dashboard-field-remapped-values [dashboard-id field-id remapped-field-id, ^String value-str] (check-field-is-referenced-by-dashboard field-id dashboard-id) (field-remapped-values field-id remapped-field-id value-str)) |
(api.macros/defendpoint :get "/card/:uuid/field/:field-id/remapping/:remapped-id"
"Fetch remapped Field values. This is the same as `GET /api/field/:id/remapping/:remapped-id`, but for use with public
Cards."
[{:keys [uuid field-id remapped-id]} :- [:map
[:uuid ms/UUIDString]
[:field-id ms/PositiveInt]
[:remapped-id ms/PositiveInt]]
{:keys [value]} :- [:map
[:value ms/NonBlankString]]]
(validation/check-public-sharing-enabled)
(let [card-id (api/check-404 (t2/select-one-pk :model/Card :public_uuid uuid, :archived false))]
(card-field-remapped-values card-id field-id remapped-id value))) | |
(api.macros/defendpoint :get "/dashboard/:uuid/field/:field-id/remapping/:remapped-id"
"Fetch remapped Field values. This is the same as `GET /api/field/:id/remapping/:remapped-id`, but for use with public
Dashboards."
[{:keys [uuid field-id remapped-id]} :- [:map
[:uuid ms/UUIDString]
[:field-id ms/PositiveInt]
[:remapped-id ms/PositiveInt]]
{:keys [value]} :- [:map
[:value ms/NonBlankString]]]
(validation/check-public-sharing-enabled)
(let [dashboard-id (t2/select-one-pk :model/Dashboard :public_uuid uuid, :archived false)]
(dashboard-field-remapped-values dashboard-id field-id remapped-id value))) | |
------------------------------------------------ Param Values ------------------------------------------------- | |
(api.macros/defendpoint :get "/card/:uuid/params/:param-key/values"
"Fetch values for a parameter on a public card."
[{:keys [uuid param-key]} :- [:map
[:uuid ms/UUIDString]
[:param-key ms/NonBlankString]]]
(validation/check-public-sharing-enabled)
(let [card (t2/select-one :model/Card :public_uuid uuid, :archived false)]
(request/as-admin
(api.card/param-values card param-key)))) | |
(api.macros/defendpoint :get "/card/:uuid/params/:param-key/search/:query"
"Fetch values for a parameter on a public card containing `query`."
[{:keys [uuid param-key query]} :- [:map
[:uuid ms/UUIDString]
[:param-key ms/NonBlankString]
[:query ms/NonBlankString]]]
(validation/check-public-sharing-enabled)
(let [card (t2/select-one :model/Card :public_uuid uuid, :archived false)]
(request/as-admin
(api.card/param-values card param-key query)))) | |
(api.macros/defendpoint :get "/dashboard/:uuid/params/:param-key/values"
"Fetch filter values for dashboard parameter `param-key`."
[{:keys [uuid param-key]} :- [:map
[:uuid ms/UUIDString]
[:param-key ms/NonBlankString]]
constraint-param-key->value]
(let [dashboard (dashboard-with-uuid uuid)]
(request/as-admin
(binding [qp.perms/*param-values-query* true]
(api.dashboard/param-values dashboard param-key constraint-param-key->value))))) | |
(api.macros/defendpoint :get "/dashboard/:uuid/params/:param-key/search/:query"
"Fetch filter values for dashboard parameter `param-key`, containing specified `query`."
[{:keys [uuid param-key query]} :- [:map
[:uuid ms/UUIDString]
[:param-key ms/NonBlankString]
[:query ms/NonBlankString]]
constraint-param-key->value]
(let [dashboard (dashboard-with-uuid uuid)]
(request/as-admin
(binding [qp.perms/*param-values-query* true]
(api.dashboard/param-values dashboard param-key constraint-param-key->value query))))) | |
----------------------------------------------------- Pivot Tables ----------------------------------------------- | |
TODO -- why do these endpoints START with | (api.macros/defendpoint :get "/pivot/card/:uuid/query"
"Fetch a publicly-accessible Card an return query results as well as `:card` information. Does not require auth
credentials. Public sharing must be enabled."
[{:keys [uuid]} :- [:map
[:uuid ms/UUIDString]]
{:keys [parameters]} :- [:map
[:parameters {:optional true} [:maybe ms/JSONString]]]]
(process-query-for-card-with-public-uuid uuid :api (json/decode+kw parameters)
:qp qp.pivot/run-pivot-query)) |
(api.macros/defendpoint :get "/pivot/dashboard/:uuid/dashcard/:dashcard-id/card/:card-id"
"Fetch the results for a Card in a publicly-accessible Dashboard. Does not require auth credentials. Public
sharing must be enabled."
[{:keys [uuid dashcard-id card-id]} :- [:map
[:uuid ms/UUIDString]
[:card-id ms/PositiveInt]
[:dashcard-id ms/PositiveInt]]
{:keys [parameters]} :- [:map
[:parameters {:optional true} [:maybe ms/JSONString]]]]
(validation/check-public-sharing-enabled)
(api/check-404 (t2/select-one-pk :model/Card :id card-id :archived false))
(let [dashboard-id (api/check-404 (t2/select-one-pk :model/Dashboard :public_uuid uuid, :archived false))]
(u/prog1 (process-query-for-dashcard
:dashboard-id dashboard-id
:card-id card-id
:dashcard-id dashcard-id
:export-format :api
:parameters parameters
:qp qp.pivot/run-pivot-query)
(events/publish-event! :event/card-read {:object-id card-id, :user-id api/*current-user-id*, :context :dashboard})))) | |
Rate limit at 10 actions per 1000 ms on a per action basis. The goal of rate limiting should be to prevent very obvious abuse, but it should be relatively lax so we don't annoy legitimate users. | (def ^:private action-execution-throttle
(throttle/make-throttler :action-uuid
:attempts-threshold 10
:initial-delay-ms 1000
:attempt-ttl-ms 1000
:delay-exponent 1)) |
(api.macros/defendpoint :post "/action/:uuid/execute"
"Execute the Action.
`parameters` should be the mapped dashboard parameters with values."
[{:keys [uuid]} :- [:map
[:uuid ms/UUIDString]]
_query-params
{:keys [parameters], :as _body} :- [:map
[:parameters {:optional true} [:maybe [:map-of :keyword any?]]]]]
(let [throttle-message (try
(throttle/check action-execution-throttle uuid)
nil
(catch ExceptionInfo e
(get-in (ex-data e) [:errors :action-uuid])))
throttle-time (when throttle-message
(second (re-find #"You must wait ([0-9]+) seconds" throttle-message)))]
(if throttle-message
(cond-> {:status 429
:body throttle-message}
throttle-time (assoc :headers {"Retry-After" throttle-time}))
(do
(validation/check-public-sharing-enabled)
;; Run this query with full superuser perms. We don't want the various perms checks
;; failing because there are no current user perms; if this Dashcard is public
;; you're by definition allowed to run it without a perms check anyway
(request/as-admin
(let [action (api/check-404 (actions/select-action :public_uuid uuid :archived false))]
(analytics/track-event! :snowplow/action
{:event :action-executed
:source :public_form
:type (:type action)
:action_id (:id action)})
;; Undo middleware string->keyword coercion
(actions/execute-action! action (update-keys parameters name)))))))) | |
----------------------------------------- Route Definitions & Complaints ----------------------------------------- | |
TODO - why don't we just make these routes have a bit of middleware that includes the
TODO - also a smart person would probably just parse the UUIDs automatically in middleware as appropriate for
| |