Consistent instance-independent naming scheme that replaces IDs with human-readable paths.  | (ns metabase-enterprise.serialization.names (:require [clojure.string :as str] [metabase.db :as mdb] [metabase.lib.schema.id :as lib.schema.id] [metabase.models.interface :as mi] [metabase.util.log :as log] [metabase.util.malli.registry :as mr] [metabase.util.malli.schema :as ms] [ring.util.codec :as codec] [toucan2.core :as t2] [toucan2.protocols :as t2.protocols]))  | 
(set! *warn-on-reflection* true)  | |
(def ^:private root-collection-path "/collections/root")  | |
Return entity name URL encoded except that spaces are retained.  | (defn safe-name [entity] (some-> entity ((some-fn :email :name)) codec/url-encode (str/replace "%20" " ")))  | 
Inverse of   | (def unescape-name codec/url-decode)  | 
(defmulti ^:private fully-qualified-name*
  {:arglists '([instance])}
  mi/model) | |
Get the logical path for entity   | (def ^{:arglists '([entity] [model id])} fully-qualified-name
  (mdb/memoize-for-application-db
   (fn
     ([entity] (fully-qualified-name* entity))
     ([model id]
      (if (string? id)
        id
        (fully-qualified-name* (t2/select-one model :id id))))))) | 
(defmethod fully-qualified-name* :model/Database [db] (str "/databases/" (safe-name db)))  | |
(defmethod fully-qualified-name* :model/Table
  [table]
  (if (:schema table)
    (format "%s/schemas/%s/tables/%s"
            (->> table :db_id (fully-qualified-name :model/Database))
            (:schema table)
            (safe-name table))
    (format "%s/tables/%s"
            (->> table :db_id (fully-qualified-name :model/Database))
            (safe-name table)))) | |
(defmethod fully-qualified-name* :model/Field
  [field]
  (if (:fk_target_field_id field)
    (str (->> field :table_id (fully-qualified-name :model/Table)) "/fks/" (safe-name field))
    (str (->> field :table_id (fully-qualified-name :model/Table)) "/fields/" (safe-name field)))) | |
(defmethod fully-qualified-name* :model/Segment [segment] (str (->> segment :table_id (fully-qualified-name :model/Table)) "/segments/" (safe-name segment)))  | |
(defn- local-collection-name [collection]
  (let [ns-part (when-let [coll-ns (:namespace collection)]
                  (str ":" (if (keyword? coll-ns) (name coll-ns) coll-ns) "/"))]
    (str "/collections/" ns-part (safe-name collection)))) | |
(defmethod fully-qualified-name* :model/Collection
  [collection]
  (let [parents (some->> (str/split (:location collection) #"/")
                         rest
                         not-empty
                         (map #(local-collection-name (t2/select-one :model/Collection :id (Integer/parseInt %))))
                         (apply str))]
    (str root-collection-path parents (local-collection-name collection)))) | |
(defmethod fully-qualified-name* :model/Dashboard
  [dashboard]
  (format "%s/dashboards/%s"
          (or (some->> dashboard :collection_id (fully-qualified-name :model/Collection))
              root-collection-path)
          (safe-name dashboard))) | |
(defmethod fully-qualified-name* :model/Pulse
  [pulse]
  (format "%s/pulses/%s"
          (or (some->> pulse :collection_id (fully-qualified-name :model/Collection))
              root-collection-path)
          (safe-name pulse))) | |
(defmethod fully-qualified-name* :model/Card
  [card]
  (format "%s/cards/%s"
          (or (some->> card
                       :collection_id
                       (fully-qualified-name :model/Collection))
              root-collection-path)
          (safe-name card))) | |
(defmethod fully-qualified-name* :model/User [user] (str "/users/" (:email user)))  | |
(defmethod fully-qualified-name* :model/NativeQuerySnippet
  [snippet]
  (format "%s/snippets/%s"
          (or (some->> snippet :collection_id (fully-qualified-name :model/Collection))
              root-collection-path)
          (safe-name snippet))) | |
(defmethod fully-qualified-name* nil [_] nil)  | |
All the references in the dumps should resolved to entities already loaded.  | (def ^:private Context
  [:map {:closed true}
   [:database   {:optional true} ms/PositiveInt]
   [:table      {:optional true} ms/PositiveInt]
   [:schema     {:optional true} [:maybe :string]]
   [:field      {:optional true} ms/PositiveInt]
   [:metric     {:optional true} ms/PositiveInt]
   [:segment    {:optional true} ms/PositiveInt]
   [:card       {:optional true} ms/PositiveInt]
   [:dashboard  {:optional true} ms/PositiveInt]
   [:collection {:optional true} [:maybe ms/PositiveInt]] ; root collection
   [:pulse      {:optional true} ms/PositiveInt]
   [:user       {:optional true} ms/PositiveInt]
   [:snippet    {:optional true} [:maybe ms/PositiveInt]]]) | 
(defmulti ^:private path->context*
  {:arglists '([context model model-attrs entity-name])}
  (fn [_ model _ _]
    model)) | |
Extract entities from a logical path.  | (def ^:private ^{:arglists '([context model model-attrs entity-name])} path->context
  path->context*) | 
(defmethod path->context* "databases"
  [context _ _ db-name]
  (assoc context :database (if (= db-name "__virtual")
                             lib.schema.id/saved-questions-virtual-database-id
                             (t2/select-one-pk :model/Database :name db-name)))) | |
(defmethod path->context* "schemas" [context _ _ schema] (assoc context :schema schema))  | |
(defmethod path->context* "tables"
  [context _ _ table-name]
  (assoc context :table (t2/select-one-pk :model/Table
                                          :db_id  (:database context)
                                          :schema (:schema context)
                                          :name   table-name))) | |
(defmethod path->context* "fields"
  [context _ _ field-name]
  (assoc context :field (t2/select-one-pk :model/Field
                                          :table_id (:table context)
                                          :name     field-name))) | |
(defmethod path->context* "fks" [context _ _ field-name] (path->context* context "fields" nil field-name))  | |
(defmethod path->context* "segments"
  [context _ _ segment-name]
  (assoc context :segment (t2/select-one-pk :model/Segment
                                            :table_id (:table context)
                                            :name     segment-name))) | |
(defmethod path->context* "collections"
  [context _ model-attrs collection-name]
  (if (= collection-name "root")
    (assoc context :collection nil)
    (assoc context :collection (t2/select-one-pk :model/Collection
                                                 :name      collection-name
                                                 :namespace (:namespace model-attrs)
                                                 :location  (or (letfn [(collection-location [id]
                                                                          (t2/select-one-fn :location :model/Collection :id id))]
                                                                  (some-> context
                                                                          :collection
                                                                          collection-location
                                                                          (str (:collection context) "/")))
                                                                "/"))))) | |
(defmethod path->context* "dashboards"
  [context _ _ dashboard-name]
  (assoc context :dashboard (t2/select-one-pk :model/Dashboard
                                              :collection_id (:collection context)
                                              :name          dashboard-name))) | |
(defmethod path->context* "pulses"
  [context _ _ pulse-name]
  (assoc context :dashboard (t2/select-one-pk :model/Pulse
                                              :collection_id (:collection context)
                                              :name          pulse-name))) | |
(defmethod path->context* "cards"
  [context _ _ dashboard-name]
  (assoc context :card (t2/select-one-pk :model/Card
                                         :collection_id (:collection context)
                                         :name          dashboard-name))) | |
(defmethod path->context* "users"
  [context _ _ email]
  (assoc context :user (t2/select-one-pk :model/User
                                         :email email))) | |
(defmethod path->context* "snippets"
  [context _ _ snippet-name]
  (assoc context :snippet (t2/select-one-pk :model/NativeQuerySnippet
                                            :collection_id (:collection context)
                                            :name          snippet-name))) | |
(def ^:private separator-pattern #"\/")  | |
Dynamic boolean var that controls whether warning messages will NOT be logged on a failed name lookup (from within
  | (def ^:dynamic *suppress-log-name-lookup-exception* false)  | 
Returns true if the given   | (defn fully-qualified-field-name?
  [field-name]
  (and (some? field-name)
       (str/starts-with? field-name "/databases/")
       (or (str/includes? field-name "/fks/") (str/includes? field-name "/fields/")))) | 
Returns true if the given   | (defn fully-qualified-table-name?
  [table-name]
  (and (some? table-name)
       (string? table-name)
       (str/starts-with? table-name "/databases/")
       (not (str/starts-with? table-name "card__")))) | 
Returns true if the given   | (defn fully-qualified-card-name?
  [card-name]
  (and (some? card-name)
       (string? card-name)
       (str/starts-with? card-name "/collections/root/")
       (str/includes? card-name "/cards/"))) | 
WARNING: THIS MUST APPEAR AFTER ALL path->context* IMPLEMENTATIONS  | (def ^:private all-entities (-> path->context*
                                methods
                                keys
                                set)) | 
This is more complicated than it needs to be due to potential clashes between an entity name (ex: a table called
"users" and a model name (ex: "users"). Could fix in a number of ways, including special prefix of model names,
but that would require changing the format and updating all the   | (defn- partition-name-components
  ([name-comps]
   (partition-name-components {::name-components [] ::current-component []} name-comps))
  ([acc [c & more-comps]]
   (cond
     (nil? more-comps)
     (conj (::name-components acc) (conj (::current-component acc) c))
     (::prev-model-name? acc)
     (if (= \: (first c))
       (partition-name-components (update acc ::current-component conj c) more-comps)
       (partition-name-components (-> (assoc acc ::prev-model-name? false)
                                      (update ::current-component
                                              conj
                                              c))
                                  more-comps))
     (contains? all-entities c)
     (partition-name-components (cond-> (assoc acc ::prev-model-name? true
                                               ::current-component [c])
                                  (not-empty (::current-component acc))
                                  (update ::name-components conj (::current-component acc)))
                                more-comps)))) | 
Parse a logical path into a context map.  | (defn fully-qualified-name->context
  [fully-qualified-name]
  (when fully-qualified-name
    (let [components (->> (str/split fully-qualified-name separator-pattern)
                          rest          ; we start with a /
                          partition-name-components
                          (map (fn [[model-name & entity-parts]]
                                 (cond-> {::model-name model-name ::entity-name (last entity-parts)}
                                   (and (= "collections" model-name) (> (count entity-parts) 1))
                                   (assoc :namespace (->> entity-parts
                                                          first ; ns is first/only item after "collections"
                                                          rest  ; strip the starting :
                                                          (apply str)))))))
          context (loop [acc-context {}
                         [{::keys [model-name entity-name] :as model-map} & more] components]
                    (let [model-attrs (dissoc model-map ::model-name ::entity-name)
                          new-context (path->context acc-context model-name model-attrs (unescape-name entity-name))]
                      (if (empty? more)
                        new-context
                        (recur new-context more))))]
      (if (and
           (not (mr/validate [:maybe Context] context))
           (not *suppress-log-name-lookup-exception*))
        (log/warn
         (ex-info (format "Can't resolve %s in fully qualified name %s"
                          (str/join ", " (map name (keys context)))
                          fully-qualified-name)
                  {:fully-qualified-name fully-qualified-name
                   :resolve-name-failed? true
                   :context              context}))
        context)))) | 
Return a string representation of entity suitable for logs  | (defn name-for-logging
  ([entity] (name-for-logging (t2.protocols/model entity) entity))
  ([model {:keys [name id]}]
   (cond
     (and name id) (format "%s \"%s\" (ID %s)" model name id)
     name          (format "%s \"%s\"" model name)
     id            (format "%s %s" model id)
     :else         model))) |