(ns metabase.search.filter (:require [honey.sql.helpers :as sql.helpers] [metabase.driver.common.parameters.dates :as params.dates] [metabase.premium-features.core :as premium-features] [metabase.search.config :as search.config] [metabase.search.permissions :as search.permissions] [metabase.search.spec :as search.spec] [metabase.util.date-2 :as u.date] [metabase.util.i18n :refer [tru]] [toucan2.core :as t2]) (:import (java.time LocalDate))) | |
(defn- remove-if-falsey [m k] (if (m k) m (dissoc m k))) | |
(defn- visible-to? [search-ctx {:keys [visibility] :as _spec}]
(case visibility
:all true
:app-user (not (search.permissions/sandboxed-or-impersonated-user? search-ctx)))) | |
Map the context keys to their corresponding filters | (def ^:private context-key->filter
(reduce-kv (fn [m k {:keys [context-key]}]
(assoc m context-key k))
{}
;; TODO remove special handling of :id
(dissoc search.config/filters :id))) |
Returns a set of models that are applicable given the search context. If the context has optional filters, the models will be restricted for the set of supported models only. | (defn search-context->applicable-models
[search-ctx]
;; Archived is an eccentric one - we treat it as false for models that don't map it, rather than removing them.
;; TODO move this behavior to the spec somehow
(let [required (->> (remove-if-falsey search-ctx :archived?) keys (keep context-key->filter))]
(into #{}
(remove nil?)
(for [search-model (:models search-ctx)
:let [spec (search.spec/spec search-model)]]
(when (and (visible-to? search-ctx spec) (every? (:attrs spec) required))
(:name spec)))))) |
A list of the search models which are not associated with collections, even indirectly. | (defn models-without-collection
[]
(->> (search.spec/specifications)
vals
(filter #(-> % :attrs :collection-id false?))
(map :name))) |
(defn- date-range-filter-clause
[dt-col dt-val]
(let [date-range (try
(params.dates/date-string->range dt-val {:inclusive-end? false})
(catch Exception _e
(throw (ex-info (tru "Failed to parse datetime value: {0}" dt-val) {:status-code 400}))))
start (some-> (:start date-range) u.date/parse)
end (some-> (:end date-range) u.date/parse)
dt-col (if (some #(instance? LocalDate %) [start end])
[:cast dt-col :date]
dt-col)]
(cond
(= start end)
[:= dt-col start]
(nil? start)
[:< dt-col end]
(nil? end)
[:> dt-col start]
:else
[:and [:>= dt-col start] [:< dt-col end]]))) | |
(defmulti ^:private where-clause*
{:arglists '([filter-type column v])}
(fn [filter-type _column _v] filter-type)) | |
(defmethod where-clause* ::single-value [_ k v] [:= k v]) | |
(defmethod where-clause* ::date-range [_ k v] (date-range-filter-clause k v)) | |
(defmethod where-clause* ::list [_ k v] [:in k v]) | |
Build a clause limiting the entries to those (not) within or within personal collections, if relevant. WARNING: this method queries the appdb, and its approach will get very slow when there are many users! | (defn personal-collections-where-clause
[{filter-type :filter-items-in-personal-collection :keys [current-user-id] :as search-ctx} collection-id-col]
(case (or filter-type "all")
"all" nil
"only-mine"
[:or
[:= :collection.personal_owner_id current-user-id]
[:like :collection.location (format "/%d/%%" (t2/select-one-pk :model/Collection :personal_owner_id [:= current-user-id]))]]
"exclude-others"
(let [with-filter #(personal-collections-where-clause
(assoc search-ctx :filter-items-in-personal-collection %)
collection-id-col)]
[:or (with-filter "only-mine") (with-filter "exclude")])
(let [personal-ids (t2/select-pks-vec :model/Collection :personal_owner_id [:not= nil])
child-patterns (for [id personal-ids] (format "/%d/%%" id))]
(case filter-type
"only"
`[:or
;; top level personal collections
[:and [:not= :collection.personal_owner_id nil] [:= :collection.location "/"]]
;; their sub-collections
~@(for [p child-patterns] [:like :collection.location p])]
"exclude"
`[:or
;; not in a collection
[:= ~collection-id-col nil]
[:and
;; neither in a top-level personal collection
[:= :collection.personal_owner_id nil]
;; nor within one of their sub-collections
~@(for [p child-patterns] [:not-like :collection.location p])]])))) |
Return a HoneySQL clause corresponding to all the optional search filters. | (defn with-filters
[search-context qry]
(as-> qry qry
(sql.helpers/where qry (if (seq (:models search-context))
[:in :search_index.model (:models search-context)]
;; Ideally, we would not get this far, and bail out earlier.
[:= 1 2]))
(sql.helpers/where qry (when-let [ids (:ids search-context)]
[:and
[:in :search_index.model_id (map str ids)]
;; NOTE: we limit id-based search to only a subset of the models
;; TODO this should just become part of the model spec e.g. :search-by-id?
[:in :search_index.model ["card" "dataset" "metric" "dashboard" "action"]]]))
(sql.helpers/where qry [:or
;; leverage the fact that only card-related models populate this attribute
[:= nil :search_index.dashboard_id]
(when (:include-dashboard-questions? search-context)
[:not= [:inline 0] [:coalesce :search_index.dashboardcard_count [:inline 0]]])])
(reduce (fn [qry {t :type :keys [context-key required-feature supported-value? field]}]
(or (when-some [v (get search-context context-key)]
(assert (supported-value? v) (str "Unsupported value for " context-key " - " v))
(when (or (nil? required-feature) (premium-features/has-feature? required-feature))
(when-some [c (where-clause* t (keyword (str "search_index." field)) v)]
(sql.helpers/where qry c))))
qry))
qry
(vals (dissoc search.config/filters :id :native-query))))) |