Skip to content
Snippets Groups Projects
Commit 1f77d6eb authored by Cam Saül's avatar Cam Saül
Browse files

::timestamped is now part of metabase.models.interface

parent 1c348ba7
No related branches found
No related tags found
No related merge requests found
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
"Korma database definition and helper functions for interacting with the database." "Korma database definition and helper functions for interacting with the database."
(:require [clojure.java.jdbc :as jdbc] (:require [clojure.java.jdbc :as jdbc]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[clojure.string :as str] (clojure [set :as set]
[string :as str])
[environ.core :refer [env]] [environ.core :refer [env]]
(korma [core :refer :all] (korma [core :refer :all]
[db :refer :all]) [db :refer :all])
...@@ -131,63 +132,7 @@ ...@@ -131,63 +132,7 @@
(apply setup-db args))) (apply setup-db args)))
;; # UTILITY FUNCTIONS ;; # ---------------------------------------- UTILITY FUNCTIONS ----------------------------------------
;; ## CAST-COLUMNS
;; TODO - Doesn't Korma have similar `transformations` functionality? Investigate.
(def ^:const ^:private type-fns
"A map of column type keywords to the functions that should be used to \"cast\"
them when going `:in` or `:out` of the database."
{:json {:in i/write-json
:out i/read-json}
:keyword {:in name
:out keyword}})
(defn types
"Tag columns in an entity definition with a type keyword.
This keyword will be used to automatically \"cast\" columns when they are present.
;; apply ((type-fns :json) :in) -- cheshire/generate-string -- to value of :details before inserting into DB
;; apply ((type-fns :json) :out) -- read-json -- to value of :details when reading from DB
(defentity Database
(types {:details :json}))"
[entity types-map]
{:pre [(every? keyword? (keys types-map))
(every? (partial contains? type-fns) (vals types-map))]}
(assoc entity ::types types-map))
(defn apply-type-fns
"Recursively apply a sequence of functions associated with COLUMN-TYPE-PAIRS to OBJ.
COLUMN-TYPE-PAIRS should be the value of `(seq (::types korma-entity))`.
DIRECTION should be either `:in` or `:out`."
{:arglists '([direction column-type-pairs obj])}
[direction [[column column-type] & rest-pairs] obj]
(if-not column obj
(recur direction rest-pairs (if-not (column obj) obj
(update-in obj [column] (-> type-fns column-type direction))))))
;; TODO - It would be good to allow custom types by just inserting the `{:in fn :out fn}` inline with the
;; entity definition
;; TODO - hydration-keys should be an entity function for the sake of prettiness
;; ## TIMESTAMPED
(defn timestamped
"Mark ENTITY as having `:created_at` *and* `:updated_at` fields.
(defentity Card
timestamped)
* When a new object is created via `ins`, values for both fields will be generated.
* When an object is updated via `upd`, `:updated_at` will be updated."
[entity]
(assoc entity ::timestamped true))
;; ## UPD ;; ## UPD
...@@ -202,10 +147,8 @@ ...@@ -202,10 +147,8 @@
{:pre [(integer? entity-id)]} {:pre [(integer? entity-id)]}
(let [obj (->> (assoc kwargs :id entity-id) (let [obj (->> (assoc kwargs :id entity-id)
(models/pre-update entity) (models/pre-update entity)
(#(dissoc % :id)) (models/internal-pre-update entity)
(apply-type-fns :in (seq (::types entity)))) (#(dissoc % :id)))
obj (cond-> obj
(::timestamped entity) (assoc :updated_at (u/new-sql-timestamp)))
result (-> (update entity (set-fields obj) (where {:id entity-id})) result (-> (update entity (set-fields obj) (where {:id entity-id}))
(> 0))] (> 0))]
(when result (when result
...@@ -335,7 +278,6 @@ ...@@ -335,7 +278,6 @@
(defn -sel-transform [entity result] (defn -sel-transform [entity result]
(->> result (->> result
(models/internal-post-select entity) (models/internal-post-select entity)
#_(apply-type-fns :out (seq (::types entity)))
(models/post-select entity))) (models/post-select entity)))
(defmacro -sel-select (defmacro -sel-select
...@@ -368,14 +310,10 @@ ...@@ -368,14 +310,10 @@
[entity & {:as kwargs}] [entity & {:as kwargs}]
(let [vals (->> kwargs (let [vals (->> kwargs
(models/pre-insert entity) (models/pre-insert entity)
(apply-type-fns :in (seq (::types entity)))) (models/internal-pre-insert entity))
vals (cond-> vals
(::timestamped entity) (assoc :created_at (u/new-sql-timestamp)
:updated_at (u/new-sql-timestamp)))
{:keys [id]} (-> (insert entity (values vals)) {:keys [id]} (-> (insert entity (values vals))
(clojure.set/rename-keys {(keyword "scope_identity()") :id}))] (set/rename-keys {(keyword "scope_identity()") :id}))]
(->> (sel :one entity :id id) (models/post-insert entity (entity id))))
(models/post-insert entity))))
;; ## EXISTS? ;; ## EXISTS?
......
...@@ -21,11 +21,9 @@ ...@@ -21,11 +21,9 @@
(defentity Card (defentity Card
[(table :report_card) [(table :report_card)
(types {:dataset_query :json (hydration-keys card)
:display :keyword (types :dataset_query :json, :display :keyword, :visualization_settings :json)
:visualization_settings :json}) timestamped]
timestamped
(assoc :hydration-keys #{:card})]
(post-select [_ {:keys [creator_id] :as card}] (post-select [_ {:keys [creator_id] :as card}]
(-> (assoc card (-> (assoc card
......
...@@ -19,11 +19,10 @@ ...@@ -19,11 +19,10 @@
(defentity Database (defentity Database
[(table :metabase_database) [(table :metabase_database)
(types {:details :json (hydration-keys database db)
:engine :keyword}) (types :details :json, :engine :keyword)
timestamped timestamped]
(assoc :hydration-keys #{:database
:db})]
(post-select [_ db] (post-select [_ db]
(map->DatabaseInstance (map->DatabaseInstance
(assoc db (assoc db
......
...@@ -76,11 +76,9 @@ ...@@ -76,11 +76,9 @@
(defentity Field (defentity Field
[(table :metabase_field) [(table :metabase_field)
timestamped (hydration-keys destination field origin)
(types {:base_type :keyword (types :base_type :keyword, :field_type :keyword, :special_type :keyword)
:field_type :keyword timestamped]
:special_type :keyword})
(hydration-keys destination field origin)]
(pre-insert [_ field] (pre-insert [_ field]
(let [defaults {:active true (let [defaults {:active true
......
...@@ -10,8 +10,7 @@ ...@@ -10,8 +10,7 @@
(defentity FieldValues (defentity FieldValues
[(table :metabase_fieldvalues) [(table :metabase_fieldvalues)
timestamped timestamped
(types {:human_readable_values :json (types :human_readable_values :json, :values :json)]
:values :json})]
(post-select [_ field-values] (post-select [_ field-values]
(update-in field-values [:human_readable_values] #(or % {})))) (update-in field-values [:human_readable_values] #(or % {}))))
......
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
(defentity ForeignKey (defentity ForeignKey
[(table :metabase_foreignkey) [(table :metabase_foreignkey)
timestamped (types :relationship :keyword)
(types {:relationship :keyword})] timestamped]
(post-select [_ {:keys [origin_id destination_id] :as fk}] (post-select [_ {:keys [origin_id destination_id] :as fk}]
(assoc fk (assoc fk
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
[clojure.walk :refer [macroexpand-all]] [clojure.walk :refer [macroexpand-all]]
[korma.core :as k] [korma.core :as k]
[medley.core :as m] [medley.core :as m]
[metabase.config :as config])) [metabase.config :as config]
[metabase.util :as u]))
;;; ## ---------------------------------------- ENTITIES ---------------------------------------- ;;; ## ---------------------------------------- ENTITIES ----------------------------------------
...@@ -33,8 +34,6 @@ ...@@ -33,8 +34,6 @@
The output of this function is ignored.") The output of this function is ignored.")
(internal-post-select [this instance])
(post-select [this instance] (post-select [this instance]
"Called on the results from a call to `sel`. Default implementation doesn't do anything, but "Called on the results from a call to `sel`. Default implementation doesn't do anything, but
you can provide custom implementations to do things like add hydrateable keys or remove sensitive fields.") you can provide custom implementations to do things like add hydrateable keys or remove sensitive fields.")
...@@ -48,21 +47,22 @@ ...@@ -48,21 +47,22 @@
(pre-cascade-delete [_ {database-id :id :as database}] (pre-cascade-delete [_ {database-id :id :as database}]
(cascade-delete Card :database_id database-id) (cascade-delete Card :database_id database-id)
...)")) ...)")
(internal-pre-insert [this instance])
(internal-pre-update [this instance])
(internal-post-select [this instance]))
(defn- identity-second (defn- identity-second [_ obj] obj)
"Return the second arg as-is." (def ^:private constantly-nil (constantly nil))
[_ obj]
obj)
(def ^:const ^:private default-entity-method-implementations (def ^:const ^:private default-entity-method-implementations
{:pre-insert identity-second {:pre-insert #'identity-second
:post-insert identity-second :post-insert #'identity-second
:pre-update identity-second :pre-update #'identity-second
:post-update '(constantly nil) :post-update #'constantly-nil
:post-select identity-second :post-select #'identity-second
:pre-cascade-delete '(constantly nil) :pre-cascade-delete #'constantly-nil})
:internal-post-select identity-second})
(def ^:const ^:private type-fns (def ^:const ^:private type-fns
{:json {:in 'metabase.db.internal/write-json {:json {:in 'metabase.db.internal/write-json
...@@ -70,8 +70,16 @@ ...@@ -70,8 +70,16 @@
:keyword {:in `name :keyword {:in `name
:out `keyword}}) :out `keyword}})
(defn- resolve-type-fns [types-map] (defmacro apply-type-fns [obj-binding direction entity-map]
(m/map-vals #(:out (type-fns %)) types-map)) {:pre [(symbol? obj-binding)
(keyword? direction)
(map? entity-map)]}
(let [fns (m/map-vals #(direction (type-fns %)) (::types entity-map))]
(if-not (seq fns) obj-binding
`(cond-> ~obj-binding
~@(mapcat (fn [[k f]]
[`(~k ~obj-binding) `(update-in [~k] ~f)])
fns)))))
(defn -invoke-entity [entity id] (defn -invoke-entity [entity id]
(future (future
...@@ -86,15 +94,18 @@ ...@@ -86,15 +94,18 @@
(internal-post-select entity) (internal-post-select entity)
(post-select entity))))) (post-select entity)))))
(defmacro make-internal-post-select [obj kvs] (defn- update-updated-at [obj]
`(cond-> ~obj (assoc obj :updated_at (u/new-sql-timestamp)))
~@(mapcat (fn [[k f]]
[`(~k ~obj) `(update-in [~k] ~f)]) (defn- update-created-at-updated-at [obj]
(seq kvs)))) (let [ts (u/new-sql-timestamp)]
(assoc obj :created_at ts, :updated_at ts)))
(defmacro macrolet-entity-map [entity & entity-forms] (defmacro macrolet-entity-map [entity & entity-forms]
`(macrolet [(~'default-fields [m# & fields#] `(assoc ~m# ::default-fields [~@(map keyword fields#)])) `(macrolet [(~'default-fields [m# & fields#] `(assoc ~m# ::default-fields [~@(map keyword fields#)]))
(~'hydration-keys [m# & fields#] `(assoc ~m# :hydration-keys #{~@(map keyword fields#)}))] (~'timestamped [m#] `(assoc ~m# ::timestamped true))
(~'types [m# & {:as fields#}] `(assoc ~m# ::types ~fields#))
(~'hydration-keys [m# & fields#] `(assoc ~m# :hydration-keys #{~@(map keyword fields#)}))]
(-> (k/create-entity ~(name entity)) (-> (k/create-entity ~(name entity))
~@entity-forms))) ~@entity-forms)))
...@@ -105,8 +116,7 @@ ...@@ -105,8 +116,7 @@
(every? list? methods)]} (every? list? methods)]}
(let [entity-symb (symbol (format "%sEntity" (name entity))) (let [entity-symb (symbol (format "%sEntity" (name entity)))
internal-post-select-symb (symbol (format "internal-post-select-%s" (name entity))) internal-post-select-symb (symbol (format "internal-post-select-%s" (name entity)))
entity-map (eval `(macrolet-entity-map ~entity ~@entity-forms)) entity-map (eval `(macrolet-entity-map ~entity ~@entity-forms))]
type-fns (resolve-type-fns (:metabase.db/types entity-map))]
`(do `(do
(defrecord ~entity-symb [] (defrecord ~entity-symb []
clojure.lang.IFn clojure.lang.IFn
...@@ -115,13 +125,20 @@ ...@@ -115,13 +125,20 @@
(extend ~entity-symb (extend ~entity-symb
IEntity ~(merge default-entity-method-implementations IEntity ~(merge default-entity-method-implementations
{:internal-post-select `(fn [~'_ ~'obj] {:internal-pre-insert `(fn [~'_ obj#]
~(macroexpand-1 `(make-internal-post-select ~'obj ~type-fns)))} (-> (apply-type-fns obj# :in ~entity-map)
~@(when (::timestamped entity-map)
[update-created-at-updated-at])))
:internal-pre-update `(fn [~'_ obj#]
(-> (apply-type-fns obj# :in ~entity-map)
~@(when (::timestamped entity-map)
[update-updated-at])))
:internal-post-select `(fn [~'_ obj#]
(apply-type-fns obj# :out ~entity-map))}
(into {} (into {}
(for [[method-name & impl] methods] (for [[method-name & impl] methods]
{(keyword method-name) `(fn ~@impl)})))) {(keyword method-name) `(fn ~@impl)}))))
(def ~entity
(def ~entity ; ~(vary-meta entity assoc :const true)
(~(symbol (format "map->%sEntity" (name entity))) ~entity-map))))) (~(symbol (format "map->%sEntity" (name entity))) ~entity-map)))))
......
...@@ -10,9 +10,7 @@ ...@@ -10,9 +10,7 @@
(defentity QueryExecution (defentity QueryExecution
[(table :query_queryexecution) [(table :query_queryexecution)
(default-fields id uuid version json_query raw_query status started_at finished_at running_time error result_rows) (default-fields id uuid version json_query raw_query status started_at finished_at running_time error result_rows)
(types {:json_query :json (types :json_query :json, :result_data :json, :status :keyword)]
:result_data :json
:status :keyword})]
(post-select [_ {:keys [result_rows] :as query-execution}] (post-select [_ {:keys [result_rows] :as query-execution}]
;; sadly we have 2 ways to reference the row count :( ;; sadly we have 2 ways to reference the row count :(
......
...@@ -14,9 +14,9 @@ ...@@ -14,9 +14,9 @@
(defentity Table (defentity Table
[(table :metabase_table) [(table :metabase_table)
timestamped
(hydration-keys table) (hydration-keys table)
(types {:entity_type :keyword})] (types :entity_type :keyword)
timestamped]
(post-select [_ {:keys [id db db_id description] :as table}] (post-select [_ {:keys [id db db_id description] :as table}]
(u/assoc* table (u/assoc* table
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment