(ns metabase-enterprise.query-reference-validation.api (:require [medley.core :as m] [metabase.api.common :as api] [metabase.api.macros :as api.macros] [metabase.api.open-api :as open-api] [metabase.api.routes.common :refer [+auth]] [metabase.models.collection :as collection] [metabase.models.query-analysis :as query-analysis] [metabase.public-settings :as public-settings] [metabase.request.core :as request] [metabase.util.malli.schema :as ms] [toucan2.core :as t2])) | |
Columns that the card errors can be sorted by | (def ^:private valid-sort-columns
#{"name" "collection" "created_by" "last_edited_at"}) ;; We don't support sorting by error message for now |
We don't support sorting by error message for now | |
(def ^:private default-sort-column "name") | |
(def ^:private valid-sort-directions
#{"asc" "desc"}) | |
(def ^:private default-sort-direction "asc") | |
(defn- present [card]
(-> card
(select-keys [:id :description :collection_id :name :entity_id :archived :collection_position
:display :collection_preview :dataset_query :last_used_at :errors :collection
:creator])
(update :collection (fn present-collection [collection]
{:id (:id collection)
:name (:name collection)
:authority_level (:authority_level collection)
:type (:type collection)
:effective_ancestors (map #(select-keys % [:id :name :authority_level :type]) (:effective_ancestors collection))})))) | |
(defn- cards-with-reference-errors
[{:keys [sort-column sort-direction limit offset collection-ids]}]
(let [sort-dir-kw (keyword sort-direction)
sorting-joins (case sort-column
"name" []
"collection" [[(t2/table-name :model/Collection) :coll] [:= :coll.id :c.collection_id]]
"created_by" [[(t2/table-name :model/User) :u] [:= :u.id :c.creator_id]]
"last_edited_at" [])
sorting-selects (case sort-column
"name" [:c.name]
"collection" [[[:max :coll.name]]
;; ^^ All these `max`es are silly, but they're necessary since we
;; group by card
[[:not= nil [:max :coll.name]] :is_child_collection]]
"created_by" [:u.first_name :u.last_name :u.email]
"last_edited_at" [:c.updated_at])
order-by-clause (concat
(condp = sort-column
"collection" [[:is_child_collection sort-dir-kw]
[[:max :coll.name] sort-dir-kw]]
"created_by" [[[:coalesce [:|| :u.first_name " " :u.last_name]
:u.first_name :u.last_name :u.email]
sort-dir-kw]]
[(into sorting-selects [sort-dir-kw])])
;; fallbacks to ensure deterministic sorting:
;; - sort by card name,
;; - if even that's the same, sort by the ID
[[:c.name sort-dir-kw]
[:c.id sort-dir-kw]])
card-query (query-analysis/cards-with-reference-errors
(m/assoc-some
;; TODO this table has a lot of fields... we should whittle down to only the ones we need.
{:select (into [:c.*] sorting-selects)
:from [[(t2/table-name :model/Card) :c]]
:left-join sorting-joins
:where [:and
[:= :c.archived false]
[:or
[:in :c.collection_id collection-ids]
(when (contains? collection-ids nil)
[:is :c.collection_id nil])]]
:order-by order-by-clause
:group-by :c.id}
:limit limit
:offset offset))
cards (t2/select :model/Card card-query)
id->errors (query-analysis/reference-errors cards)
add-errors (fn [{:keys [id] :as card}]
(assoc card :errors (sort-by (juxt :table :field :type) (id->errors id))))]
{:data (map (comp present add-errors) (t2/hydrate cards [:collection :effective_ancestors] :creator))
:total (t2/count :model/Card (dissoc card-query :limit :offset))})) | |
(defn- invalid-cards [sort_column sort_direction collection_id]
(let [collection (if (nil? collection_id)
collection/root-collection
(t2/select-one :model/Collection :id collection_id))
collection-ids (conj (collection/descendant-ids collection) collection_id)]
(merge (cards-with-reference-errors {:sort-column (or sort_column default-sort-column)
:sort-direction (or sort_direction default-sort-direction)
:collection-ids (set collection-ids)
:limit (request/limit)
:offset (request/offset)})
{:limit (request/limit)
:offset (request/offset)}))) | |
(api.macros/defendpoint :get "/invalid-cards"
"List of cards that have an invalid reference in their query. Shape of each card is standard, with the addition of an
`errors` key. Supports pagination (`offset` and `limit`), so it returns something in the shape:
```
{:total 200
:data [card1, card2, ...]
:limit 50
:offset 100
```"
[_route-params
{:keys [sort_column sort_direction collection_id]} :- [:map
[:sort_column {:optional true} [:maybe (into [:enum] valid-sort-columns)]]
[:sort_direction {:optional true} [:maybe (into [:enum] valid-sort-directions)]]
[:collection_id {:optional true} [:maybe ms/PositiveInt]]]]
(invalid-cards sort_column sort_direction collection_id)) | |
Middleware that gates this API behind the associated feature flag | (defn +check-setting
[handler]
(open-api/handler-with-open-api-spec
(fn [request respond raise]
(if (public-settings/query-analysis-enabled)
(handler request respond raise)
(respond {:status 429 :body "Query Analysis must be enabled to use the Query Reference Validator"})))
(fn [prefix]
(open-api/open-api-spec handler prefix)))) |
| (def ^{:arglists '([request respond raise])} routes
(api.macros/ns-handler *ns* api/+check-superuser +auth +check-setting)) |