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 @@
"Korma database definition and helper functions for interacting with the database."
(:require [clojure.java.jdbc :as jdbc]
[clojure.tools.logging :as log]
[clojure.string :as str]
(clojure [set :as set]
[string :as str])
[environ.core :refer [env]]
(korma [core :refer :all]
[db :refer :all])
......@@ -131,63 +132,7 @@
(apply setup-db args)))
;; # 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))
;; # ---------------------------------------- UTILITY FUNCTIONS ----------------------------------------
;; ## UPD
......@@ -202,10 +147,8 @@
{:pre [(integer? entity-id)]}
(let [obj (->> (assoc kwargs :id entity-id)
(models/pre-update entity)
(#(dissoc % :id))
(apply-type-fns :in (seq (::types entity))))
obj (cond-> obj
(::timestamped entity) (assoc :updated_at (u/new-sql-timestamp)))
(models/internal-pre-update entity)
(#(dissoc % :id)))
result (-> (update entity (set-fields obj) (where {:id entity-id}))
(> 0))]
(when result
......@@ -335,7 +278,6 @@
(defn -sel-transform [entity result]
(->> result
(models/internal-post-select entity)
#_(apply-type-fns :out (seq (::types entity)))
(models/post-select entity)))
(defmacro -sel-select
......@@ -368,14 +310,10 @@
[entity & {:as kwargs}]
(let [vals (->> kwargs
(models/pre-insert entity)
(apply-type-fns :in (seq (::types entity))))
vals (cond-> vals
(::timestamped entity) (assoc :created_at (u/new-sql-timestamp)
:updated_at (u/new-sql-timestamp)))
(models/internal-pre-insert entity))
{:keys [id]} (-> (insert entity (values vals))
(clojure.set/rename-keys {(keyword "scope_identity()") :id}))]
(->> (sel :one entity :id id)
(models/post-insert entity))))
(set/rename-keys {(keyword "scope_identity()") :id}))]
(models/post-insert entity (entity id))))
;; ## EXISTS?
......
......@@ -21,11 +21,9 @@
(defentity Card
[(table :report_card)
(types {:dataset_query :json
:display :keyword
:visualization_settings :json})
timestamped
(assoc :hydration-keys #{:card})]
(hydration-keys card)
(types :dataset_query :json, :display :keyword, :visualization_settings :json)
timestamped]
(post-select [_ {:keys [creator_id] :as card}]
(-> (assoc card
......
......@@ -19,11 +19,10 @@
(defentity Database
[(table :metabase_database)
(types {:details :json
:engine :keyword})
timestamped
(assoc :hydration-keys #{:database
:db})]
(hydration-keys database db)
(types :details :json, :engine :keyword)
timestamped]
(post-select [_ db]
(map->DatabaseInstance
(assoc db
......
......@@ -76,11 +76,9 @@
(defentity Field
[(table :metabase_field)
timestamped
(types {:base_type :keyword
:field_type :keyword
:special_type :keyword})
(hydration-keys destination field origin)]
(hydration-keys destination field origin)
(types :base_type :keyword, :field_type :keyword, :special_type :keyword)
timestamped]
(pre-insert [_ field]
(let [defaults {:active true
......
......@@ -10,8 +10,7 @@
(defentity FieldValues
[(table :metabase_fieldvalues)
timestamped
(types {:human_readable_values :json
:values :json})]
(types :human_readable_values :json, :values :json)]
(post-select [_ field-values]
(update-in field-values [:human_readable_values] #(or % {}))))
......
......@@ -16,8 +16,8 @@
(defentity ForeignKey
[(table :metabase_foreignkey)
timestamped
(types {:relationship :keyword})]
(types :relationship :keyword)
timestamped]
(post-select [_ {:keys [origin_id destination_id] :as fk}]
(assoc fk
......
......@@ -4,7 +4,8 @@
[clojure.walk :refer [macroexpand-all]]
[korma.core :as k]
[medley.core :as m]
[metabase.config :as config]))
[metabase.config :as config]
[metabase.util :as u]))
;;; ## ---------------------------------------- ENTITIES ----------------------------------------
......@@ -33,8 +34,6 @@
The output of this function is ignored.")
(internal-post-select [this instance])
(post-select [this instance]
"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.")
......@@ -48,21 +47,22 @@
(pre-cascade-delete [_ {database-id :id :as database}]
(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
"Return the second arg as-is."
[_ obj]
obj)
(defn- identity-second [_ obj] obj)
(def ^:private constantly-nil (constantly nil))
(def ^:const ^:private default-entity-method-implementations
{:pre-insert identity-second
:post-insert identity-second
:pre-update identity-second
:post-update '(constantly nil)
:post-select identity-second
:pre-cascade-delete '(constantly nil)
:internal-post-select identity-second})
{:pre-insert #'identity-second
:post-insert #'identity-second
:pre-update #'identity-second
:post-update #'constantly-nil
:post-select #'identity-second
:pre-cascade-delete #'constantly-nil})
(def ^:const ^:private type-fns
{:json {:in 'metabase.db.internal/write-json
......@@ -70,8 +70,16 @@
:keyword {:in `name
:out `keyword}})
(defn- resolve-type-fns [types-map]
(m/map-vals #(:out (type-fns %)) types-map))
(defmacro apply-type-fns [obj-binding direction entity-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]
(future
......@@ -86,15 +94,18 @@
(internal-post-select entity)
(post-select entity)))))
(defmacro make-internal-post-select [obj kvs]
`(cond-> ~obj
~@(mapcat (fn [[k f]]
[`(~k ~obj) `(update-in [~k] ~f)])
(seq kvs))))
(defn- update-updated-at [obj]
(assoc obj :updated_at (u/new-sql-timestamp)))
(defn- update-created-at-updated-at [obj]
(let [ts (u/new-sql-timestamp)]
(assoc obj :created_at ts, :updated_at ts)))
(defmacro macrolet-entity-map [entity & entity-forms]
`(macrolet [(~'default-fields [m# & fields#] `(assoc ~m# ::default-fields [~@(map keyword fields#)]))
(~'hydration-keys [m# & fields#] `(assoc ~m# :hydration-keys #{~@(map keyword fields#)}))]
`(macrolet [(~'default-fields [m# & fields#] `(assoc ~m# ::default-fields [~@(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))
~@entity-forms)))
......@@ -105,8 +116,7 @@
(every? list? methods)]}
(let [entity-symb (symbol (format "%sEntity" (name entity)))
internal-post-select-symb (symbol (format "internal-post-select-%s" (name entity)))
entity-map (eval `(macrolet-entity-map ~entity ~@entity-forms))
type-fns (resolve-type-fns (:metabase.db/types entity-map))]
entity-map (eval `(macrolet-entity-map ~entity ~@entity-forms))]
`(do
(defrecord ~entity-symb []
clojure.lang.IFn
......@@ -115,13 +125,20 @@
(extend ~entity-symb
IEntity ~(merge default-entity-method-implementations
{:internal-post-select `(fn [~'_ ~'obj]
~(macroexpand-1 `(make-internal-post-select ~'obj ~type-fns)))}
{:internal-pre-insert `(fn [~'_ obj#]
(-> (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 {}
(for [[method-name & impl] methods]
{(keyword method-name) `(fn ~@impl)}))))
(def ~entity ; ~(vary-meta entity assoc :const true)
(def ~entity
(~(symbol (format "map->%sEntity" (name entity))) ~entity-map)))))
......
......@@ -10,9 +10,7 @@
(defentity QueryExecution
[(table :query_queryexecution)
(default-fields id uuid version json_query raw_query status started_at finished_at running_time error result_rows)
(types {:json_query :json
:result_data :json
:status :keyword})]
(types :json_query :json, :result_data :json, :status :keyword)]
(post-select [_ {:keys [result_rows] :as query-execution}]
;; sadly we have 2 ways to reference the row count :(
......
......@@ -14,9 +14,9 @@
(defentity Table
[(table :metabase_table)
timestamped
(hydration-keys table)
(types {:entity_type :keyword})]
(types :entity_type :keyword)
timestamped]
(post-select [_ {:keys [id db db_id description] :as 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