From 4d5d93da725cc2a715f52e48383da50b058dab5d Mon Sep 17 00:00:00 2001 From: Cam Saul <1455846+camsaul@users.noreply.github.com> Date: Mon, 29 Aug 2022 22:58:01 -0700 Subject: [PATCH] Namespaced keyword support (#21) * Namespaces should work with transforms * Add alias based on namespace so keywords work as well * Namespaced insert support * Support namespaces inside `update!` * Support namespaced keywords in delete * Lint fix --- src/toucan2/delete.clj | 3 +- src/toucan2/jdbc/result_set.clj | 36 +++++---- src/toucan2/model.clj | 38 ++++++++-- src/toucan2/query.clj | 22 +++++- src/toucan2/select.clj | 2 +- src/toucan2/update.clj | 3 +- test/toucan2/delete_test.clj | 13 ++++ test/toucan2/insert_test.clj | 28 +++++++ test/toucan2/model_test.clj | 35 +++++++++ test/toucan2/query_test.clj | 31 ++++++++ test/toucan2/select_test.clj | 79 +++++++++++++++++++- test/toucan2/tools/hydrate_test.clj | 2 - test/toucan2/tools/transformed_test.clj | 52 +++++++++++++ test/toucan2/tools/with_temp_test.clj | 1 - test/toucan2/update_test.clj | 22 ++++++ toucan1/test/toucan/models_test.clj | 58 +++++++------- toucan1/test/toucan/test_models/category.clj | 8 +- 17 files changed, 369 insertions(+), 64 deletions(-) diff --git a/src/toucan2/delete.clj b/src/toucan2/delete.clj index 1776b7b7..38546782 100644 --- a/src/toucan2/delete.clj +++ b/src/toucan2/delete.clj @@ -2,7 +2,6 @@ "Implementation of [[delete!]]." (:require [methodical.core :as m] - [toucan2.model :as model] [toucan2.pipeline :as pipeline] [toucan2.query :as query])) @@ -11,7 +10,7 @@ #_query clojure.lang.IPersistentMap] [query-type model parsed-args] (let [parsed-args (update parsed-args :query (fn [query] - (merge {:delete-from [(keyword (model/table-name model))]} + (merge {:delete-from (query/honeysql-table-and-alias model)} query)))] (next-method query-type model parsed-args))) diff --git a/src/toucan2/jdbc/result_set.clj b/src/toucan2/jdbc/result_set.clj index 040d0774..90426f05 100644 --- a/src/toucan2/jdbc/result_set.clj +++ b/src/toucan2/jdbc/result_set.clj @@ -5,6 +5,8 @@ [pretty.core :as pretty] [toucan2.instance :as instance] [toucan2.jdbc.row :as jdbc.row] + [toucan2.magic-map :as magic-map] + [toucan2.model :as model] [toucan2.protocols :as protocols] [toucan2.util :as u]) (:import @@ -53,7 +55,7 @@ [^ResultSetMetaData rsmeta] (range 1 (inc (.getColumnCount rsmeta)))) -(defn- row-instance [model #_key-xform col-name->thunk] +(defn- row-instance [model col-name->thunk] (let [row (jdbc.row/row col-name->thunk)] (u/with-debug-result ["Creating new instance of %s, which has key transform fn %s" model @@ -63,21 +65,29 @@ (defn row-thunk "Return a thunk that when called fetched the current row from the cursor and returns it as a [[row-instance]]." [^Connection conn model ^ResultSet rset] - (let [rsmeta (.getMetaData rset) - key-xform (instance/key-transform-fn model) + (let [rsmeta (.getMetaData rset) + ;; do case-insensitive lookup. + table->namespace (some-> (model/table-name->namespace model) (magic-map/magic-map u/lower-case-en)) + key-xform (instance/key-transform-fn model) ;; create a set of thunks to read each column. These thunks will call `read-column-thunk` to determine the ;; appropriate column-reading thunk the first time they are used. - col-name->thunk (into {} (for [^Long i (index-range rsmeta) - :let [col-name (key-xform (keyword (.getColumnName rsmeta i))) - ;; TODO -- add test to ensure we only resolve the read-column-thunk - ;; once even with multiple rows. - read-thunk (delay (read-column-thunk conn model rset rsmeta i)) - result-thunk (fn [] - (u/with-debug-result ["Realize column %s %s" i col-name] - (next.jdbc.rs/read-column-by-index (@read-thunk) rsmeta i)))]] - [col-name result-thunk]))] + col-name->thunk (into {} + (map (fn [^Long i] + (let [table-name (.getTableName rsmeta i) + col-name (.getColumnName rsmeta i) + table-ns-name (some-> (get table->namespace table-name) name) + col-key (key-xform (keyword table-ns-name col-name)) + ;; TODO -- add test to ensure we only resolve the read-column-thunk + ;; once even with multiple rows. + read-thunk (delay (read-column-thunk conn model rset rsmeta i)) + result-thunk (fn [] + (u/with-debug-result ["Realize column %s %s.%s as %s" + i table-name col-name col-key] + (next.jdbc.rs/read-column-by-index (@read-thunk) rsmeta i)))] + [col-key result-thunk]))) + (index-range rsmeta))] (fn row-instance-thunk [] - (row-instance model #_key-xform col-name->thunk)))) + (row-instance model col-name->thunk)))) (deftype ^:no-doc ReducibleResultSet [^Connection conn model ^ResultSet rset] clojure.lang.IReduceInit diff --git a/src/toucan2/model.clj b/src/toucan2/model.clj index cd662ab6..fa0d8da4 100644 --- a/src/toucan2/model.clj +++ b/src/toucan2/model.clj @@ -1,4 +1,5 @@ (ns toucan2.model + (:refer-clojure :exclude [namespace]) (:require [clojure.spec.alpha :as s] [methodical.core :as m] @@ -78,7 +79,7 @@ If an implementation returns a single keyword, the default `:around` method will automatically wrap it in a vector. It also validates that the ultimate result is a sequence of keywords, so it is safe to assume that calls to this will always return a sequence of keywords." - {:arglists '([model])} + {:arglists '([model₁])} u/dispatch-on-first-arg) ;;; if the PK comes back unwrapped, wrap it. @@ -96,10 +97,6 @@ {:model model, :result pk-or-pks}))) pks)) -(m/defmethod primary-keys :default - [_model] - [:id]) - ;;; TODO -- rename to `primary-key-values-map` (defn primary-key-values "Return a map of primary key values for a Toucan 2 `instance`." @@ -121,3 +118,34 @@ (if (= (count pk-keys) 1) (first pk-keys) (apply juxt pk-keys))))) + +(m/defmulti model->namespace + {:arglists '([model₁])} + u/dispatch-on-first-arg) + +(m/defmethod model->namespace :default + [_model] + nil) + +(defn table-name->namespace [model] + (not-empty + (into {} + (comp (filter (fn [[model _a-namespace]] + (not= (m/effective-primary-method table-name model) + (m/default-effective-method table-name)))) + (map (fn [[model a-namespace]] + [(table-name model) a-namespace]))) + (model->namespace model)))) + +(defn namespace [model] + (some + (fn [[a-model a-namespace]] + (when (isa? model a-model) + a-namespace)) + (model->namespace model))) + +(m/defmethod primary-keys :default + [model] + (if-let [model-namespace (namespace model)] + [(keyword (name model-namespace) "id")] + [:id])) diff --git a/src/toucan2/query.clj b/src/toucan2/query.clj index 6ff630a5..b2d75e18 100644 --- a/src/toucan2/query.clj +++ b/src/toucan2/query.clj @@ -25,8 +25,8 @@ ↓ Compiled query is executed with connection ```" - (:require + [better-cond.core :as b] [clojure.spec.alpha :as s] [honey.sql.helpers :as hsql.helpers] [methodical.core :as m] @@ -418,3 +418,23 @@ (m/defmethod build [:default :default clojure.lang.IPersistentMap] [_query-type model {:keys [kv-args query], :as _args}] (apply-kv-args model query kv-args)) + +(defn honeysql-table-and-alias + "Build an Honey SQL `[table]` or `[table alias]` (if the model has a [[toucan2.model/namespace]] form) for `model` for + use in something like a `:select` clause." + [model] + (b/cond + :let [table-id (keyword (model/table-name model)) + alias-id (model/namespace model) + alias-id (when alias-id + (keyword alias-id))] + alias-id + [table-id alias-id] + + :else + [table-id])) + +;; (defn- format-identifier [_ parts] +;; [(str/join \. (map hsql/format-entity parts))]) + +;; (hsql/register-fn! ::identifier #'format-identifier) diff --git a/src/toucan2/select.clj b/src/toucan2/select.clj index 3a82afb2..c661baf6 100644 --- a/src/toucan2/select.clj +++ b/src/toucan2/select.clj @@ -17,7 +17,7 @@ (merge {:select (or (not-empty columns) [:*])} (when model - {:from [[(keyword (model/table-name model))]]}) + {:from [(query/honeysql-table-and-alias model)]}) query))) (dissoc :columns))] (next-method query-type model parsed-args))) diff --git a/src/toucan2/update.clj b/src/toucan2/update.clj index 991e8d62..da4f1ac8 100644 --- a/src/toucan2/update.clj +++ b/src/toucan2/update.clj @@ -3,7 +3,6 @@ (:require [clojure.spec.alpha :as s] [methodical.core :as m] - [toucan2.model :as model] [toucan2.pipeline :as pipeline] [toucan2.query :as query] [toucan2.util :as u])) @@ -40,7 +39,7 @@ {:query-type query-type, :model model, :parsed-args parsed-args}))) (let [parsed-args (assoc parsed-args :kv-args (merge kv-args query) - :query {:update [(keyword (model/table-name model))] + :query {:update (query/honeysql-table-and-alias model) :set changes})] (next-method query-type model parsed-args))) diff --git a/test/toucan2/delete_test.clj b/test/toucan2/delete_test.clj index 6ab1c22e..f2d33abe 100644 --- a/test/toucan2/delete_test.clj +++ b/test/toucan2/delete_test.clj @@ -81,3 +81,16 @@ (test/with-discarded-table-changes :venues (is (= 0 (delete/delete! ::test/venues nil)))))) + +(derive ::venues.namespaced ::test/venues) + +(m/defmethod model/model->namespace ::venues.namespaced + [_model] + {::test/venues :venue}) + +(deftest namespaced-test + (test/with-discarded-table-changes :venues + (is (= 1 + (delete/delete! ::venues.namespaced :venue/id 3))) + (is (= nil + (select/select-one [::test/venues :id :name :category] :id 3))))) diff --git a/test/toucan2/insert_test.clj b/test/toucan2/insert_test.clj index e50532d7..c65f5fb4 100644 --- a/test/toucan2/insert_test.clj +++ b/test/toucan2/insert_test.clj @@ -263,3 +263,31 @@ :created-at (LocalDateTime/parse "2017-01-01T00:00") :updated-at (LocalDateTime/parse "2017-01-01T00:00")})]) (insert! ::test/venues ::named-rows))))))) + +(derive ::venues.namespaced ::test/venues) + +(m/defmethod model/model->namespace ::venues.namespaced + [_model] + {::test/venues :venue}) + +(deftest namespaced-test + (doseq [insert! [#'insert/insert! + #'insert/insert-returning-pks! + #'insert/insert-returning-instances!]] + (test/with-discarded-table-changes :venues + (testing insert! + (is (= (condp = insert! + #'insert/insert! 1 + #'insert/insert-returning-pks! [4] + #'insert/insert-returning-instances! [(instance/instance + ::venues.namespaced + {:venue/name "Grant & Green" + :venue/category "bar"})]) + (insert! [::venues.namespaced :venue/name :venue/category] + {:venue/name "Grant & Green", :venue/category "bar"}))) + (is (= (instance/instance + ::test/venues + {:id 4 + :name "Grant & Green" + :category "bar"}) + (select/select-one [::test/venues :id :name :category] :id 4))))))) diff --git a/test/toucan2/model_test.clj b/test/toucan2/model_test.clj index 36e51fd7..6ed66e63 100644 --- a/test/toucan2/model_test.clj +++ b/test/toucan2/model_test.clj @@ -63,3 +63,38 @@ ;;; [[model/default-connectable]] gets tested basically everywhere, because we define it for the models in ;;; [[toucan2.test]] and use it in almost every test namespace + +(derive ::venues.namespaced ::test/venues) + +(m/defmethod model/model->namespace ::venues.namespaced + [_model] + {::venues.namespaced :venue + ::test/categories :category}) + +(deftest model->namespace-test + (are [model expected] (= expected + (model/model->namespace model)) + ::venues.namespaced {::venues.namespaced :venue, ::test/categories :category} + :venues nil + nil nil)) + +(deftest table-name->namespace-test + (are [model expected] (= expected + (model/table-name->namespace model)) + ::venues.namespaced {"venues" :venue, "category" :category} + :venues nil + nil nil)) + +(derive ::venues.namespaced.child ::venues.namespaced) + +(deftest namespace-test + (are [model expected] (= expected + (model/namespace model)) + ::venues.namespaced :venue + ::venues.namespaced.child :venue + :venues nil + nil nil)) + +(deftest namespaced-default-primary-keys-test + (is (= [:venue/id] + (model/primary-keys ::venues.namespaced)))) diff --git a/test/toucan2/query_test.clj b/test/toucan2/query_test.clj index 7131db1b..e36b6e4b 100644 --- a/test/toucan2/query_test.clj +++ b/test/toucan2/query_test.clj @@ -100,3 +100,34 @@ ::venues.compound-pk [:in [[4 "BevMo"]]] [:and [:in :id [4]] [:in :name ["BevMo"]]] ::venues.compound-pk [:in [[4 "BevMo"] [5 "BevLess"]]] [:and [:in :id [4 5]] [:in :name ["BevMo" "BevLess"]]] ::venues.compound-pk [:between [4 "BevMo"] [5 "BevLess"]] [:and [:between :id 4 5] [:between :name "BevMo" "BevLess"]]))) + +(derive ::venues.namespaced ::test/venues) + +(m/defmethod model/model->namespace ::venues.namespaced + [_model] + {::test/venues :venue}) + +(deftest namespaced-toucan-pk-test + (is (= {:select [:*] + :from [[:venues :venue]] + :where [:= :venue/id 1]} + (query/build :toucan.query-type/select.instances + ::venues.namespaced + {:kv-args {:toucan/pk 1}, :query {}})))) + +(deftest honeysql-table-and-alias-test + (are [model expected] (= expected + (query/honeysql-table-and-alias model)) + ::test/venues [:venues] + ::venues.namespaced [:venues :venue] + "venues" [:venues])) + +;; (deftest identitfier-test +;; (testing "Custom Honey SQL identifier clause" +;; (are [identifier quoted? expected] (= expected +;; (hsql/format {:select [:*], :from [[identifier]]} +;; {:quoted quoted?})) +;; [::query/identifier :wow] false ["SELECT * FROM wow"] +;; [::query/identifier :wow] true ["SELECT * FROM \"wow\""] +;; [::query/identifier :table :field] false ["SELECT * FROM table.field"] +;; [::query/identifier :table :field] true ["SELECT * FROM \"table\".\"field\""]))) diff --git a/test/toucan2/select_test.clj b/test/toucan2/select_test.clj index 8a06e94c..e32ad9ed 100644 --- a/test/toucan2/select_test.clj +++ b/test/toucan2/select_test.clj @@ -408,8 +408,14 @@ (m/defmethod query/build :after [#_query-type :toucan.query-type/select.* #_model ::venues.with-category #_query clojure.lang.IPersistentMap] - [_query-type _model built-query] - (assoc built-query :left-join [[:category :c] [:= :venues.category :c.name]])) + [_query-type model built-query] + (let [model-ns-str (some-> (model/namespace model) name) + venues-category (keyword + (str + (when model-ns-str + (str model-ns-str \.)) + "category"))] + (assoc built-query :left-join [:category [:= venues-category :category.name]]))) (deftest joined-model-test (is (= (instance/instance ::venues.with-category @@ -421,5 +427,70 @@ :slug "bar_01" :parent-category nil}) (select/select-one ::venues.with-category - {:left-join [[:category :c] [:= :venues.category :c.name]] - :order-by [[:id :asc]]})))) + {:order-by [[:id :asc]]})))) + +(derive ::venues.namespaced ::test/venues) + +(m/defmethod model/model->namespace ::venues.namespaced + [_model] + {::venues.namespaced :venue}) + +(deftest namespaced-test + (is (= {"venues" :venue} + (model/table-name->namespace ::venues.namespaced))) + (is (= {:select [:*] + :from [[:venues :venue]] + :order-by [[:id :asc]]} + (tools.compile/build + (select/select-one ::venues.namespaced {:order-by [[:id :asc]]})))) + (testing "When selecting a model with a namespace, keys should come back in that namespace." + (is (= (instance/instance ::venues.namespaced + {:venue/id 1 + :venue/name "Tempest" + :venue/category "bar" + :venue/created-at (LocalDateTime/parse "2017-01-01T00:00") + :venue/updated-at (LocalDateTime/parse "2017-01-01T00:00")}) + (select/select-one ::venues.namespaced {:order-by [[:id :asc]]}))))) + +(doto ::venues.namespaced.with-category + (derive ::venues.namespaced) + (derive ::venues.with-category)) + +(m/defmethod model/model->namespace ::venues.namespaced.with-category + [_model] + {::test/venues :venue + ::test/categories :category}) + +(deftest namespaced-with-joins-test + (is (= {:select [:*] + :from [[:venues :venue]] + :order-by [[:id :asc]] + :left-join [:category [:= :venue.category :category.name]]} + (tools.compile/build + (select/select-one ::venues.namespaced.with-category {:order-by [[:id :asc]]})))) + (is (= (toucan2.instance/instance + ::venues.namespaced.with-category + {:venue/id 1 + :venue/name "Tempest" + :venue/category "bar" + :venue/created-at (LocalDateTime/parse "2017-01-01T00:00") + :venue/updated-at (LocalDateTime/parse "2017-01-01T00:00") + :category/name "bar" + :category/slug "bar_01" + :category/parent-category nil}) + (select/select-one ::venues.namespaced.with-category {:order-by [[:id :asc]]})))) + +(deftest namespaced-with-joins-columns-test + (is (= :venue + (model/namespace ::venues.namespaced.with-category))) + (is (= {:select [:venue/id :venue/name :category/name] + :from [[:venues :venue]] + :order-by [[:id :asc]] + :left-join [:category [:= :venue.category :category.name]]} + (tools.compile/build + (select/select-one [::venues.namespaced.with-category :venue/id :venue/name :category/name] + {:order-by [[:id :asc]]})))) + (is (= (instance/instance ::venues.namespaced.with-category + {:venue/id 1, :venue/name "Tempest", :category/name "bar"}) + (select/select-one [::venues.namespaced.with-category :venue/id :venue/name :category/name] + {:order-by [[:id :asc]]})))) diff --git a/test/toucan2/tools/hydrate_test.clj b/test/toucan2/tools/hydrate_test.clj index e658960c..e3e28870 100644 --- a/test/toucan2/tools/hydrate_test.clj +++ b/test/toucan2/tools/hydrate_test.clj @@ -14,8 +14,6 @@ (set! *warn-on-reflection* true) -(use-fixtures :each test/do-db-types-fixture) - (derive ::venues ::test/venues) (derive ::venues.category-keyword ::venues) diff --git a/test/toucan2/tools/transformed_test.clj b/test/toucan2/tools/transformed_test.clj index d8802ddb..c20e6e75 100644 --- a/test/toucan2/tools/transformed_test.clj +++ b/test/toucan2/tools/transformed_test.clj @@ -5,12 +5,14 @@ [toucan2.delete :as delete] [toucan2.insert :as insert] [toucan2.instance :as instance] + [toucan2.model :as model] [toucan2.pipeline :as pipeline] [toucan2.protocols :as protocols] [toucan2.query :as query] [toucan2.save :as save] [toucan2.select :as select] [toucan2.test :as test] + [toucan2.tools.compile :as tools.compile] [toucan2.tools.identity-query :as identity-query] [toucan2.tools.transformed :as transformed] [toucan2.update :as update]) @@ -431,3 +433,53 @@ :created-at (LocalDateTime/parse "2017-01-01T00:00") :updated-at (LocalDateTime/parse "2017-01-01T00:00")}) (select/select-one ::venues.override-transforms :toucan/pk 1)))) + +(derive ::categories.namespaced.category-keyword ::test/categories) + +(transformed/deftransforms ::categories.namespaced.category-keyword + {:category/name {:in name, :out keyword}}) + +(derive ::venues.namespaced.category-keyword ::test/venues) + +(transformed/deftransforms ::venues.namespaced.category-keyword + {:venue/category {:in name, :out keyword}}) + +;;; workaround for https://github.com/camsaul/methodical/issues/97 +(doseq [varr [#'transformed/transforms]] + (m/prefer-method! varr ::venues.namespaced.category-keyword ::categories.namespaced.category-keyword)) + +(doto ::venues.namespaced.with-category + (derive ::venues.namespaced.category-keyword) + (derive ::categories.namespaced.category-keyword)) + +(m/defmethod model/table-name ::venues.namespaced.with-category + [_model] + (model/table-name ::test/venues)) + +(m/defmethod model/model->namespace ::venues.namespaced.with-category + [_model] + {::venues.namespaced.category-keyword :venue + ::categories.namespaced.category-keyword :category}) + +(deftest namespaced-test + (is (= {:select [:*] + :from [[:venues :venue]] + :left-join [:category [:= :venue.category :category.name]] + :order-by [[:id :asc]]} + (tools.compile/build + (select/select-one ::venues.namespaced.with-category + {:left-join [:category [:= :venue.category :category.name]] + :order-by [[:id :asc]]})))) + (is (= (instance/instance + ::venues.namespaced.with-category + {:venue/id 1 + :venue/name "Tempest" + :venue/category :bar + :venue/created-at (LocalDateTime/parse "2017-01-01T00:00") + :venue/updated-at (LocalDateTime/parse "2017-01-01T00:00") + :category/name :bar + :category/slug "bar_01" + :category/parent-category nil}) + (select/select-one ::venues.namespaced.with-category + {:left-join [:category [:= :venue.category :category.name]] + :order-by [[:id :asc]]})))) diff --git a/test/toucan2/tools/with_temp_test.clj b/test/toucan2/tools/with_temp_test.clj index bdb880f3..c8d74fc9 100644 --- a/test/toucan2/tools/with_temp_test.clj +++ b/test/toucan2/tools/with_temp_test.clj @@ -10,7 +10,6 @@ (use-fixtures :each - test/do-db-types-fixture (fn [thunk] (is (= 6 (select/count ::test/birds))) diff --git a/test/toucan2/update_test.clj b/test/toucan2/update_test.clj index 0099fd92..3a48d927 100644 --- a/test/toucan2/update_test.clj +++ b/test/toucan2/update_test.clj @@ -121,3 +121,25 @@ (test/with-discarded-table-changes :venues (is (= 0 (update/update! ::test/venues nil {:name "Taco Bell"})))))) + +(derive ::venues.namespaced ::test/venues) + +(m/defmethod model/model->namespace ::venues.namespaced + [_model] + {::test/venues :venue}) + +(deftest namespaced-test + (doseq [update! [#'update/update! + #'update/update-returning-pks!]] + (test/with-discarded-table-changes :venues + (testing update! + (is (= (condp = update! + #'update/update! 1 + #'update/update-returning-pks! [3]) + (update! ::venues.namespaced 3 {:venue/name "Grant & Green", :venue/category "bar"}))) + (is (= (instance/instance + ::test/venues + {:id 3 + :name "Grant & Green" + :category "bar"}) + (select/select-one [::test/venues :id :name :category] :id 3))))))) diff --git a/toucan1/test/toucan/models_test.clj b/toucan1/test/toucan/models_test.clj index b7f02c79..398064a1 100644 --- a/toucan1/test/toucan/models_test.clj +++ b/toucan1/test/toucan/models_test.clj @@ -219,23 +219,23 @@ ;; queue (deftest post-insert-test (test/with-discarded-table-changes Category - (reset! category/categories-awaiting-moderation (clojure.lang.PersistentQueue/EMPTY)) - (is (= (instance/instance Category {:id 5, :name "toucannery", :parent-category-id nil}) - (t1.db/insert! Category :name "toucannery"))) - (testing `category/categories-awaiting-moderation - (is (= [5] - @category/categories-awaiting-moderation))) - (testing "Should include columns added by after-insert" - (derive ::Category.post-insert Category) - (after-insert/define-after-insert ::Category.post-insert - [row] - (assoc row :after-insert? true)) - (is (= (instance/instance ::Category.post-insert - {:id 6, :name "aviary", :parent-category-id nil, :after-insert? true}) - (t1.db/insert! ::Category.post-insert :name "aviary"))) - (testing `category/categories-awaiting-moderation - (is (= [5 6] - @category/categories-awaiting-moderation)))))) + (binding [category/*categories-awaiting-moderation* (atom (clojure.lang.PersistentQueue/EMPTY))] + (is (= (instance/instance Category {:id 5, :name "toucannery", :parent-category-id nil}) + (t1.db/insert! Category :name "toucannery"))) + (testing `category/*categories-awaiting-moderation* + (is (= [5] + @category/*categories-awaiting-moderation*))) + (testing "Should include columns added by after-insert" + (derive ::Category.post-insert Category) + (after-insert/define-after-insert ::Category.post-insert + [row] + (assoc row :after-insert? true)) + (is (= (instance/instance ::Category.post-insert + {:id 6, :name "aviary", :parent-category-id nil, :after-insert? true}) + (t1.db/insert! ::Category.post-insert :name "aviary"))) + (testing `category/*categories-awaiting-moderation* + (is (= [5 6] + @category/*categories-awaiting-moderation*))))))) ;; (after-insert/define-after-insert ::PostInsert ;; [row] @@ -254,19 +254,19 @@ ;; queue (deftest post-update-test (test/with-discarded-table-changes Category - (reset! category/categories-recently-updated (clojure.lang.PersistentQueue/EMPTY)) - (is (= true - (t1.db/update! Category 2 :name "lobster"))) - (is (= 2 - (peek @category/categories-recently-updated)))) + (binding [category/*categories-recently-updated* (atom (clojure.lang.PersistentQueue/EMPTY))] + (is (= true + (t1.db/update! Category 2 :name "lobster"))) + (is (= 2 + (peek @category/*categories-recently-updated*))))) (test/with-discarded-table-changes Category - (reset! category/categories-recently-updated (clojure.lang.PersistentQueue/EMPTY)) - (is (= true - (t1.db/update! Category 1 :name "fine-dining"))) - (is (= true - (t1.db/update! Category 2 :name "steak-house"))) - (is (= [1 2] - @category/categories-recently-updated)))) + (binding [category/*categories-recently-updated* (atom (clojure.lang.PersistentQueue/EMPTY))] + (is (= true + (t1.db/update! Category 1 :name "fine-dining"))) + (is (= true + (t1.db/update! Category 2 :name "steak-house"))) + (is (= [1 2] + @category/*categories-recently-updated*))))) ;; (deftest do-post-update-test ;; (testing `t1.models/post-update diff --git a/toucan1/test/toucan/test_models/category.clj b/toucan1/test/toucan/test_models/category.clj index 0a8a3792..7b872f2d 100644 --- a/toucan1/test/toucan/test_models/category.clj +++ b/toucan1/test/toucan/test_models/category.clj @@ -36,20 +36,20 @@ (defn- delete-child-categories! [{:keys [id]}] (t1.db/delete! Category :parent-category-id id)) -(def categories-awaiting-moderation +(def ^:dynamic *categories-awaiting-moderation* "A poor man's message queue of newly added Category IDs that are \"awating moderation\"." (atom (clojure.lang.PersistentQueue/EMPTY))) (defn add-category-to-moderation-queue! [{:keys [id], :as new-category}] - (swap! categories-awaiting-moderation conj id) + (swap! *categories-awaiting-moderation* conj id) new-category) -(def categories-recently-updated +(def ^:dynamic *categories-recently-updated* "A simple queue of recently updated Category IDs." (atom (clojure.lang.PersistentQueue/EMPTY))) (defn add-category-to-updated-queue! [{:keys [id]}] - (swap! categories-recently-updated conj id)) + (swap! *categories-recently-updated* conj id)) (t1.models/define-methods-with-IModel-method-map Category