(ns metabase-enterprise.query-reference-validation.api (:require [compojure.core :refer [GET]] [medley.core :as m] [metabase.api.common :as 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)}))) | |
/invalid-cards | (api/defendpoint GET "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 ```" [sort_column sort_direction collection_id] {sort_column [:maybe (into [:enum] valid-sort-columns)] sort_direction [:maybe (into [:enum] valid-sort-directions)] collection_id [: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] (with-meta (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"}))) (meta handler))) |
(api/define-routes api/+check-superuser +auth +check-setting) | |