(ns metabase.models.interface (:require [buddy.core.codecs :as codecs] [clojure.core.memoize :as memoize] [clojure.spec.alpha :as s] [clojure.string :as str] [clojure.walk :as walk] [malli.error :as me] [medley.core :as m] [metabase.legacy-mbql.normalize :as mbql.normalize] [metabase.legacy-mbql.schema :as mbql.s] [metabase.lib.binning :as lib.binning] [metabase.lib.core :as lib] [metabase.lib.metadata.protocols :as lib.metadata.protocols] [metabase.lib.temporal-bucket :as lib.temporal-bucket] [metabase.models.dispatch :as models.dispatch] [metabase.models.json-migration :as jm] [metabase.models.resolution] [metabase.plugins.classloader :as classloader] [metabase.util :as u] [metabase.util.cron :as u.cron] [metabase.util.encryption :as encryption] [metabase.util.i18n :refer [tru]] [metabase.util.json :as json] [metabase.util.log :as log] [metabase.util.malli :as mu] [metabase.util.malli.registry :as mr] [metabase.util.string :as string] [methodical.core :as methodical] [potemkin :as p] [taoensso.nippy :as nippy] [toucan2.core :as t2] [toucan2.model :as t2.model] [toucan2.protocols :as t2.protocols] [toucan2.tools.before-insert :as t2.before-insert] [toucan2.tools.hydrate :as t2.hydrate] [toucan2.tools.identity-query :as t2.identity-query] [toucan2.util :as t2.u]) (:import (java.io BufferedInputStream ByteArrayInputStream DataInputStream) (java.sql Blob) (java.util.zip GZIPInputStream) (toucan2.instance Instance))) | |
(set! *warn-on-reflection* true) | |
(comment ;; load this so dynamic model resolution works as expected metabase.models.resolution/keep-me) | |
(p/import-vars [models.dispatch toucan-instance? instance-of? model instance]) | |
This is dynamically bound to true when deserializing. A few pieces of the Toucan magic are undesirable for
deserialization. Most notably, we don't want to generate an | (def ^:dynamic *deserializing?* false) |
+----------------------------------------------------------------------------------------------------------------+ | Toucan Extensions | +----------------------------------------------------------------------------------------------------------------+ | |
[[define-simple-hydration-method]] and [[define-batched-hydration-method]] | |
(s/def ::define-hydration-method (s/cat :fn-name symbol? :hydration-key keyword? :docstring string? :fn-tail (s/alt :arity-1 :clojure.core.specs.alpha/params+body :arity-n (s/+ (s/spec :clojure.core.specs.alpha/params+body))))) | |
(defonce ^:private defined-hydration-methods (atom {})) | |
(defn- define-hydration-method [hydration-type fn-name hydration-key fn-tail] {:pre [(#{:hydrate :batched-hydrate} hydration-type)]} ;; Let's be EXTRA nice and make sure there are no duplicate hydration keys! (let [fn-symb (symbol (str (ns-name *ns*)) (name fn-name))] (when-let [existing-fn-symb (get @defined-hydration-methods hydration-key)] (when (not= fn-symb existing-fn-symb) (throw (ex-info (str (format "Hydration key %s already exists at %s" hydration-key existing-fn-symb) "\n\n" "You can remove it with" "\n" (pr-str (list 'swap! `defined-hydration-methods 'dissoc hydration-key))) {:hydration-key hydration-key :existing-definition existing-fn-symb})))) (swap! defined-hydration-methods assoc hydration-key fn-symb)) `(do (defn ~fn-name ~@fn-tail) ~(case hydration-type :hydrate `(methodical/defmethod t2.hydrate/simple-hydrate [:default ~hydration-key] [~'_model k# row#] (assoc row# k# (~fn-name row#))) :batched-hydrate `(methodical/defmethod t2.hydrate/batched-hydrate [:default ~hydration-key] [~'_model ~'_k rows#] (~fn-name rows#))))) | |
Define a Toucan hydration function (Toucan 1) or method (Toucan 2) to do 'simple' hydration (this function is called
for each individual object that gets hydrated). This helper is in place to make the switch to Toucan 2 easier to
accomplish. Toucan 2 uses multimethods instead of regular functions with | (defmacro define-simple-hydration-method {:style/indent :defn} [fn-name hydration-key & fn-tail] (define-hydration-method :hydrate fn-name hydration-key fn-tail)) |
(s/fdef define-simple-hydration-method :args ::define-hydration-method :ret any?) | |
Like [[define-simple-hydration-method]], but defines a Toucan 'batched' hydration function (Toucan 1) or method (Toucan 2). 'Batched' hydration means this function can be used to hydrate a sequence of objects in one call. See docstring for [[define-simple-hydration-method]] for more information as to why this macro exists. | (defmacro define-batched-hydration-method {:style/indent :defn} [fn-name hydration-key & fn-tail] (define-hydration-method :batched-hydrate fn-name hydration-key fn-tail)) |
(s/fdef define-batched-hydration-method :args ::define-hydration-method :ret any?) | |
+----------------------------------------------------------------------------------------------------------------+ | Toucan 2 Extensions | +----------------------------------------------------------------------------------------------------------------+ --- transforms methods | |
Default in function for columns given a Toucan type | (defn json-in [obj] (if (string? obj) obj (json/encode obj))) |
(defn- json-out [s keywordize-keys?] (if (string? s) (try (json/decode s keywordize-keys?) (catch Throwable e (log/error e "Error parsing JSON") s)) s)) | |
Default out function for columns given a Toucan type | (defn json-out-with-keywordization [obj] (json-out obj true)) |
Out function for columns given a Toucan type | (defn json-out-without-keywordization [obj] (json-out obj false)) |
(defn- elide-data [obj] (walk/postwalk (fn [x] (cond (string? x) (string/elide x 250) (and (sequential? x) (> (count x) 50)) (take 50 x) :else x)) obj)) | |
(defn- json-in-with-eliding [obj] (if (string? obj) obj (json/encode (elide-data obj)))) | |
Transform for json. | (def transform-json {:in json-in :out json-out-with-keywordization}) |
Serializes object as JSON, but: - elides any long strings to a max of 250 chars - limits sequences to the first 50 entries Useful for debugging/human-consuming information which can be unbounded-ly large | (def transform-json-eliding {:in json-in-with-eliding :out json-out-with-keywordization}) |
Saving MLv2 queries​ we can assume MLv2 queries are normalized enough already, but remove the metadata provider before saving it, because it's not something that lends itself well to serialization. | (defn- serialize-mlv2-query [query] (dissoc query :lib/metadata)) |
Reading MLv2 queries​: normalize them, then attach a MetadataProvider based on their Database. | (defn- deserialize-mlv2-query [query] (let [metadata-provider (if (lib.metadata.protocols/metadata-provider? (:lib/metadata query)) ;; in case someone passes in an already-normalized query to [[maybe-normalize-query]] below, ;; preserve the existing metadata provider. (:lib/metadata query) ((requiring-resolve 'metabase.lib.metadata.jvm/application-database-metadata-provider) (u/the-id (some #(get query %) [:database "database"]))))] (lib/query metadata-provider query))) |
For top-level query maps like | (mu/defn maybe-normalize-query [in-or-out :- [:enum :in :out] query] (letfn [(normalize [query] (let [f (if (= (lib/normalized-query-type query) :mbql/query) ;; MLv2 queries (case in-or-out :in serialize-mlv2-query :out deserialize-mlv2-query) ;; legacy queries: just normalize them with the legacy normalization code for now... in the near ;; future we'll probably convert to MLv2 before saving so everything in the app DB is MLv2 (case in-or-out :in mbql.normalize/normalize :out mbql.normalize/normalize))] (f query)))] (cond-> query (and (map? query) (seq query)) normalize))) |
Wraps normalization fn | (defn catch-normalization-exceptions [f] (fn [query] (try (doall (f query)) (catch Throwable e (log/errorf e "Unable to normalize:\n%s" (u/pprint-to-str 'red query)) nil)))) |
Normalize | (defn normalize-parameters-list [parameters] (or (mbql.normalize/normalize-fragment [:parameters] parameters) [])) |
(defn- keywordize-temporal_units [parameter] (m/update-existing parameter :temporal_units (fn [units] (mapv keyword units)))) | |
Normalize | (defn normalize-card-parameters-list [parameters] (->> parameters normalize-parameters-list (mapv keywordize-temporal_units))) |
Transform for metabase-query. | (def transform-metabase-query {:in (comp json-in (partial maybe-normalize-query :in)) :out (comp (catch-normalization-exceptions (partial maybe-normalize-query :out)) json-out-without-keywordization)}) |
Transform for parameters list. | (def transform-parameters-list {:in (comp json-in normalize-parameters-list) :out (comp (catch-normalization-exceptions normalize-parameters-list) json-out-with-keywordization)}) |
Transform for parameters list. | (def transform-card-parameters-list {:in (comp json-in normalize-card-parameters-list) :out (comp (catch-normalization-exceptions normalize-card-parameters-list) json-out-with-keywordization)}) |
Transform field refs | (def transform-field-ref {:in json-in :out (comp (catch-normalization-exceptions mbql.normalize/normalize-field-ref) json-out-with-keywordization)}) |
Transform the Card result metadata as it comes out of the DB. Convert columns to keywords where appropriate. | (defn- result-metadata-out [metadata] ;; TODO -- can we make this whole thing a lazy seq? (when-let [metadata (not-empty (json-out-with-keywordization metadata))] (not-empty (mapv #(-> % mbql.normalize/normalize-source-metadata ;; This is necessary, because in the wild, there may be cards created prior to this change. lib.temporal-bucket/ensure-temporal-unit-in-display-name lib.binning/ensure-binning-in-display-name) metadata)))) |
Transform for card.result_metadata like columns. | (def transform-result-metadata {:in json-in :out result-metadata-out}) |
Transform for keywords. | (def transform-keyword {:in u/qualified-name :out keyword}) |
Transform for json-no-keywordization | (def transform-json-no-keywordization {:in json-in :out json-out-without-keywordization}) |
Assert that a value is one of the values in | (mu/defn assert-enum [enum :- [:set :any] value] (when-not (contains? enum value) (throw (ex-info (format "Invalid value %s. Must be one of %s" value (str/join ", " enum)) {:status-code 400 :value value})))) |
Assert that a value is a namespaced keyword under | (mu/defn assert-namespaced [qualified-ns :- string? value] (when-not (= qualified-ns (-> value keyword namespace)) (throw (ex-info (format "Must be a namespaced keyword under :%s, got: %s" qualified-ns value) {:status-code 400 :value value})))) |
Given a transform, returns a transform that call E.g: A keyword transfomer that throw an error if the value is not namespaced (transform-validator transform-keyword (fn [x] (when-not (-> x namespace some?) (throw (ex-info "Value is not namespaced"))))) | (defn transform-validator [tf assert-fn] (-> tf ;; deserialization (update :out (fn [f] (fn [x] (let [out (f x)] (assert-fn out) out)))) ;; serialization (update :in (fn [f] (fn [x] (assert-fn x) (f x)))))) |
Serialize encrypted json. | (def encrypted-json-in (comp encryption/maybe-encrypt json-in)) |
Deserialize encrypted json. | (defn encrypted-json-out [v] (let [decrypted (encryption/maybe-decrypt v)] (try (json/decode+kw decrypted) (catch Throwable e (if (or (encryption/possibly-encrypted-string? decrypted) (encryption/possibly-encrypted-bytes? decrypted)) (log/error e "Could not decrypt encrypted field! Have you forgot to set MB_ENCRYPTION_SECRET_KEY?") (log/error e "Error parsing JSON")) ; same message as in `json-out` v)))) |
cache the decryption/JSON parsing because it's somewhat slow (~500µs vs ~100µs on a fast computer) cache the decrypted JSON for one hour | (def ^:private cached-encrypted-json-out (memoize/ttl encrypted-json-out :ttl/threshold (* 60 60 1000))) |
Transform for encrypted json. | (def transform-encrypted-json {:in encrypted-json-in :out cached-encrypted-json-out}) |
The frontend uses JSON-serialized versions of MBQL clauses as keys in | (defn normalize-visualization-settings [viz-settings] (letfn [(normalize-column-settings-key [k] (some-> k u/qualified-name json/decode mbql.normalize/normalize json/encode)) (normalize-column-settings [column-settings] (into {} (for [[k v] column-settings] [(normalize-column-settings-key k) (walk/keywordize-keys v)]))) (mbql-field-clause? [form] (and (vector? form) (#{"field-id" "fk->" "datetime-field" "joined-field" "binning-strategy" "field" "aggregation" "expression"} (first form)))) (normalize-mbql-clauses [form] (walk/postwalk (fn [form] (try (cond-> form (mbql-field-clause? form) mbql.normalize/normalize) (catch Exception e (log/warnf "Unable to normalize visualization-settings part %s: %s" (u/pprint-to-str 'red form) (ex-message e)) form))) form))] (cond-> (walk/keywordize-keys (dissoc viz-settings "column_settings" "graph.metrics")) ;; "key" is an old unused value true (m/update-existing :table.columns (fn [cols] (mapv #(dissoc % :key) cols))) (get viz-settings "column_settings") (assoc :column_settings (normalize-column-settings (get viz-settings "column_settings"))) true normalize-mbql-clauses ;; exclude graph.metrics from normalization as it may start with ;; the word "expression" but it is not MBQL (metabase#15882) (get viz-settings "graph.metrics") (assoc :graph.metrics (get viz-settings "graph.metrics"))))) |
(jm/def-json-migration migrate-viz-settings*) | |
(def ^:private viz-settings-current-version 2) | |
(defmethod ^:private migrate-viz-settings* [1 2] [viz-settings _] (let [{percent? :pie.show_legend_perecent ;; [sic] legend? :pie.show_legend} viz-settings new-visibility (cond legend? "inside" percent? "legend") new-linktype (when (= "page" (-> viz-settings :click_behavior :linkType)) "dashboard")] (cond-> viz-settings ;; if nothing was explicitly set don't default to "off", let the FE deal with it new-visibility (assoc :pie.percent_visibility new-visibility) new-linktype (assoc-in [:click_behavior :linkType] new-linktype)))) | |
(defn- migrate-viz-settings [viz-settings] (let [new-viz-settings (migrate-viz-settings* viz-settings viz-settings-current-version)] (cond-> new-viz-settings (not= new-viz-settings viz-settings) (jm/update-version viz-settings-current-version)))) | |
migrate-viz settings was introduced with v. 2, so we'll never be in a situation where we can downgrade from 2 to 1. See sample code in SHA d597b445333f681ddd7e52b2e30a431668d35da8 | |
Transform for viz-settings. | (def transform-visualization-settings {:in (comp json-in migrate-viz-settings) :out (comp migrate-viz-settings normalize-visualization-settings json-out-without-keywordization)}) |
(def ^{:arglists '([s])} ^:private validate-cron-string (let [validator (mr/validator u.cron/CronScheduleString)] (partial mu/validate-throw validator))) | |
Transform for encrypted json. | (def transform-cron-string {:in validate-cron-string :out identity}) |
(mr/def ::legacy-metric-segment-definition [:map [:filter {:optional true} [:maybe mbql.s/Filter]] [:aggregation {:optional true} [:maybe [:sequential ::mbql.s/Aggregation]]]]) | |
(defn- validate-legacy-metric-segment-definition [definition] (if-let [error (mr/explain ::legacy-metric-segment-definition definition)] (let [humanized (me/humanize error)] (throw (ex-info (tru "Invalid Metric or Segment: {0}" (pr-str humanized)) {:error error :humanized humanized}))) definition)) | |
| (defn- normalize-legacy-metric-segment-definition [definition] (when (seq definition) (u/prog1 (mbql.normalize/normalize-fragment [:query] definition) (validate-legacy-metric-segment-definition <>)))) |
Transform for inner queries like those in Metric definitions. | (def transform-legacy-metric-segment-definition {:in (comp json-in normalize-legacy-metric-segment-definition) :out (comp (catch-normalization-exceptions normalize-legacy-metric-segment-definition) json-out-with-keywordization)}) |
(defn- blob->bytes [^Blob b] (.getBytes ^Blob b 0 (.length ^Blob b))) | |
(defn- maybe-blob->bytes [v] (if (instance? Blob v) (blob->bytes v) v)) | |
Transform for secret value. | (def transform-secret-value {:in (comp encryption/maybe-encrypt-bytes codecs/to-bytes) :out (comp encryption/maybe-decrypt maybe-blob->bytes)}) |
Decompress | (defn decompress [compressed-bytes] (if (instance? Blob compressed-bytes) (recur (blob->bytes compressed-bytes)) (with-open [bis (ByteArrayInputStream. compressed-bytes) bif (BufferedInputStream. bis) gz-in (GZIPInputStream. bif) data-in (DataInputStream. gz-in)] (nippy/thaw-from-in! data-in)))) |
Transform for compressed fields. | #_{:clj-kondo/ignore [:unused-public-var]} (def transform-compressed {:in identity :out decompress}) |
--- predefined hooks | |
Return a HoneySQL form for a SQL function call to get current moment in time. Currently this is | (defn now [] (classloader/require 'metabase.driver.sql.query-processor) (let [db-type ((requiring-resolve 'metabase.db/db-type))] ((resolve 'metabase.driver.sql.query-processor/current-datetime-honeysql-form) db-type))) |
(defn- add-created-at-timestamp [obj & _] (cond-> obj (not (:created_at obj)) (assoc :created_at (now)))) | |
(defn- add-updated-at-timestamp [obj] ;; don't stomp on `:updated_at` if it's already explicitly specified. (let [changes-already-include-updated-at? (if (t2/instance? obj) (:updated_at (t2/changes obj)) (:updated_at obj))] (cond-> obj (not changes-already-include-updated-at?) (assoc :updated_at (now))))) | |
(t2/define-before-insert :hook/timestamped? [instance] (-> instance add-updated-at-timestamp add-created-at-timestamp)) | |
(t2/define-before-update :hook/timestamped? [instance] (-> instance add-updated-at-timestamp)) | |
(t2/define-before-insert :hook/created-at-timestamped? [instance] (-> instance add-created-at-timestamp)) | |
(t2/define-before-insert :hook/updated-at-timestamped? [instance] (-> instance add-updated-at-timestamp)) | |
(t2/define-before-update :hook/updated-at-timestamped? [instance] (-> instance add-updated-at-timestamp)) | |
(defn- add-entity-id [obj & _] (if (or (contains? obj :entity_id) *deserializing?*) ;; Don't generate a new entity_id if either: (a) there's already one set; or (b) we're deserializing. ;; Generating them at deserialization time can lead to duplicated entities if they're deserialized again. obj (assoc obj :entity_id (u/generate-nano-id)))) | |
(t2/define-before-insert :hook/entity-id [instance] (-> instance add-entity-id)) | |
(methodical/prefer-method! #'t2.before-insert/before-insert :hook/timestamped? :hook/entity-id) (methodical/prefer-method! #'t2.before-insert/before-insert :hook/updated-at-timestamped? :hook/entity-id) | |
The row merged with the changes in pre-update hooks. This is to match the input of pre-update for toucan1 methods --- helper fns | (defn changes-with-pk [row] (t2.protocols/with-current row (merge (t2.model/primary-key-values-map row) (t2.protocols/changes row)))) |
Do [[toucan2.tools.after-select]] stuff for row map | (defn do-after-select [modelable row-map] {:pre [(map? row-map)]} (let [model (t2/resolve-model modelable)] (try (t2/select-one model (t2.identity-query/identity-query [row-map])) (catch Throwable e (throw (ex-info (format "Error doing after-select for model %s: %s" model (ex-message e)) {:model model} e)))))) |
+----------------------------------------------------------------------------------------------------------------+ | New Permissions Stuff | +----------------------------------------------------------------------------------------------------------------+ | |
TODO -- consider moving all this stuff into the | |
Helper dispatch function for multimethods. Dispatches on the first arg, using [[models.dispatch/model]]. | (def ^{:arglists '([x & _args])} dispatch-on-model ;; make sure model namespace gets loaded e.g. `:model/Database` should load `metabase.model.database` if needed. (comp t2/resolve-model t2.u/dispatch-on-first-arg)) |
Return a set of permissions object paths that a user must have access to in order to access this object. This should be something like #{"/db/1/schema/public/table/20/"}
| (defmulti perms-objects-set {:arglists '([instance read-or-write])} dispatch-on-model) |
(defmethod perms-objects-set :default [_instance _read-or-write] nil) | |
Return whether [[metabase.api.common/current-user]] has read permissions for an object. You should typically use one of these implementations:
| (defmulti can-read? {:arglists '([instance] [model pk])} dispatch-on-model) |
Return whether [[metabase.api.common/current-user]] has write permissions for an object. You should typically use one of these implementations:
| (defmulti can-write? {:arglists '([instance] [model pk])} dispatch-on-model) |
#_{:clj-kondo/ignore [:unused-private-var]} (define-simple-hydration-method ^:private hydrate-can-write :can_write "Hydration method for `:can_write`." [instance] (can-write? instance)) | |
NEW! Check whether or not current user is allowed to CREATE a new instance of Because this method was added YEARS after [[can-read?]] and [[can-write?]], most models do not have an implementation
for this method, and instead | (defmulti can-create? {:added "0.32.0", :arglists '([model m])} dispatch-on-model) |
(defmethod can-create? :default [model _m] (throw (NoSuchMethodException. (str (format "%s does not yet have an implementation for [[can-create?]]. " (name model)) "Please consider adding one. See dox for [[can-create?]] for more details.")))) | |
NEW! Check whether or not the current user is allowed to update an object and by updating properties to values in
the (toucan2.core/update! model id changes) This method is appropriate for powering | (defmulti can-update? {:added "0.36.0", :arglists '([instance changes])} dispatch-on-model) |
(defmethod can-update? :default [instance _changes] (throw (NoSuchMethodException. (str (format "%s does not yet have an implementation for `can-update?`. " (name (models.dispatch/model instance))) "Please consider adding one. See dox for `can-update?` for more details.")))) | |
Is [[metabase.api.common/current-user]] is a superuser? Ignores args. Intended for use as an implementation of [[can-read?]] and/or [[can-write?]]. | (defn superuser? [& _] @(requiring-resolve 'metabase.api.common/*is-superuser?*)) |
Return the ID of the current user. | (defn current-user-id [] @(requiring-resolve 'metabase.api.common/*current-user-id*)) |
(defn- current-user-permissions-set [] @@(requiring-resolve 'metabase.api.common/*current-user-permissions-set*)) | |
(defn- current-user-has-root-permissions? [] (contains? (current-user-permissions-set) "/")) | |
(mu/defn- check-perms-with-fn ([fn-symb :- qualified-symbol? read-or-write :- [:enum :read :write] a-model :- qualified-keyword? object-id :- [:or pos-int? string?]] (or (current-user-has-root-permissions?) (check-perms-with-fn fn-symb read-or-write (t2/select-one a-model (first (t2/primary-keys a-model)) object-id)))) ([fn-symb :- qualified-symbol? read-or-write :- [:enum :read :write] object :- :map] (and object (check-perms-with-fn fn-symb (perms-objects-set object read-or-write)))) ([fn-symb :- qualified-symbol? perms-set :- [:set :string]] (let [f (requiring-resolve fn-symb)] (assert f) (u/prog1 (f (current-user-permissions-set) perms-set) (log/tracef "Perms check: %s -> %s" (pr-str (list fn-symb (current-user-permissions-set) perms-set)) <>))))) | |
Implementation of [[can-read?]]/[[can-write?]] for the old permissions system. | (def ^{:arglists '([read-or-write model object-id] [read-or-write object] [perms-set])} current-user-has-full-permissions? (partial check-perms-with-fn 'metabase.permissions.models.permissions/set-has-full-permissions-for-set?)) |
Implementation of [[can-read?]]/[[can-write?]] for the old permissions system. | (def ^{:arglists '([read-or-write model object-id] [read-or-write object] [perms-set])} current-user-has-partial-permissions? (partial check-perms-with-fn 'metabase.permissions.models.permissions/set-has-partial-permissions-for-set?)) |
(defmethod can-read? ::read-policy.always-allow ([_instance] true) ([_model _pk] true)) | |
(defmethod can-write? ::write-policy.always-allow ([_instance] true) ([_model _pk] true)) | |
(defmethod can-read? ::read-policy.partial-perms-for-perms-set ([instance] (current-user-has-partial-permissions? :read instance)) ([model pk] (current-user-has-partial-permissions? :read model pk))) | |
(defmethod can-read? ::read-policy.full-perms-for-perms-set ([instance] (current-user-has-full-permissions? :read instance)) ([model pk] (current-user-has-full-permissions? :read model pk))) | |
(defmethod can-write? ::write-policy.partial-perms-for-perms-set ([instance] (current-user-has-partial-permissions? :write instance)) ([model pk] (current-user-has-partial-permissions? :write model pk))) | |
(defmethod can-write? ::write-policy.full-perms-for-perms-set ([instance] (current-user-has-full-permissions? :write instance)) ([model pk] (current-user-has-full-permissions? :write model pk))) | |
(defmethod can-read? ::read-policy.superuser ([_instance] (superuser?)) ([_model _pk] (superuser?))) | |
(defmethod can-write? ::write-policy.superuser ([_instance] (superuser?)) ([_model _pk] (superuser?))) | |
(defmethod can-create? ::create-policy.superuser [_model _m] (superuser?)) | |
[[to-json]] | |
Serialize an | (methodical/defmulti to-json {:arglists '([instance json-generator]) :defmethod-arities #{2} :dispatch-value-spec (some-fn keyword? symbol?)} ; dispatch value should be either keyword model name or symbol t2.u/dispatch-on-first-arg) |
(methodical/defmethod to-json :default "Default method for encoding instances of a Toucan model to JSON." [instance json-generator] (json/generate-map instance json-generator)) | |
(json/add-encoder Instance #'to-json) | |
etc | |
Trigger errors when hydrate encounters a key that has no corresponding method defined. | (reset! t2.hydrate/global-error-on-unknown-key true) |
(methodical/defmethod t2.hydrate/fk-keys-for-automagic-hydration :default "In Metabase the FK key used for automagic hydration should use underscores (work around upstream Toucan 2 issue)." [_original-model dest-key _hydrated-key] [(u/->snake_case_en (keyword (str (name dest-key) "_id")))]) | |
Helper function to write batched hydrations.
Assoc to each (instances-with-hydrated-data (t2/select :model/Database) :tables #(t2/select-fn->fn :db_id identity :model/Table) :id) ;; => [{:id 1 :tables [...tables-from-db-1]} {:id 2 :tables [...tables-from-db-2]}]
| (mu/defn instances-with-hydrated-data ;; TODO: this example is wrong, we don't get a vector of tables [instances :- [:sequential :any] hydration-key :- :keyword instance-key->hydrated-data-fn :- fn? instance-key :- :keyword & [{:keys [default] :as _options}]] (when (seq instances) (let [key->hydrated-items (instance-key->hydrated-data-fn)] (for [item instances] (when item (assoc item hydration-key (get key->hydrated-items (get item instance-key) default))))))) |
Returns a HoneySQL expression to exclude instances of the model that were created automatically as part of internally
used content, such as Metabase Analytics, the sample database, or the sample dashboard. If a | (defmulti exclude-internal-content-hsql {:arglists '([model & {:keys [table-alias]}])} dispatch-on-model) |
(defmethod exclude-internal-content-hsql :default [_model & _] [:= [:inline 1] [:inline 1]]) | |