diff --git a/components/command-processor-v2/src/ai/obney/grain/command_processor_v2/core.clj b/components/command-processor-v2/src/ai/obney/grain/command_processor_v2/core.clj index b1a5f29b..22cf6798 100644 --- a/components/command-processor-v2/src/ai/obney/grain/command_processor_v2/core.clj +++ b/components/command-processor-v2/src/ai/obney/grain/command_processor_v2/core.clj @@ -3,11 +3,18 @@ [ai.obney.grain.event-store-v3.interface :as event-store] [ai.obney.grain.command-processor-v2.interface.schemas :as command-schema] [ai.obney.grain.anomalies.interface :refer [anomaly?]] + [ai.obney.grain.time.interface :as time] [com.brunobonacci.mulog :as u] [cognitect.anomalies :as anom] [malli.core :as mc] [malli.error :as me])) +(defn ->command + [command] + (merge {:command/id (random-uuid) + :command/timestamp (time/now)} + command)) + (defn execute-command [handler {:keys [event-store tenant-id] :as context}] (let [result (try diff --git a/components/command-processor-v2/src/ai/obney/grain/command_processor_v2/interface.clj b/components/command-processor-v2/src/ai/obney/grain/command_processor_v2/interface.clj index b21a7cee..f3b8a266 100644 --- a/components/command-processor-v2/src/ai/obney/grain/command_processor_v2/interface.clj +++ b/components/command-processor-v2/src/ai/obney/grain/command_processor_v2/interface.clj @@ -1,6 +1,12 @@ (ns ai.obney.grain.command-processor-v2.interface (:require [ai.obney.grain.command-processor-v2.core :as core])) +(defn ->command + "Returns command with :command/id and :command/timestamp filled in + when not already present." + [command] + (core/->command command)) + (def command-registry* (atom {})) (defn register-command! diff --git a/components/command-processor-v2/test/ai/obney/grain/command_processor_v2/interface_test.clj b/components/command-processor-v2/test/ai/obney/grain/command_processor_v2/interface_test.clj index 052e5c9a..c688c710 100644 --- a/components/command-processor-v2/test/ai/obney/grain/command_processor_v2/interface_test.clj +++ b/components/command-processor-v2/test/ai/obney/grain/command_processor_v2/interface_test.clj @@ -58,13 +58,11 @@ ;; Test Helpers (defn make-command - [command-name & {:keys [id timestamp extra-data] - :or {id (uuid/v4) - timestamp (OffsetDateTime/now)}}] - (merge {:command/name command-name - :command/id id - :command/timestamp timestamp} - extra-data)) + [command-name & {:keys [id timestamp extra-data]}] + (cp/->command + (cond-> (merge {:command/name command-name} extra-data) + id (assoc :command/id id) + timestamp (assoc :command/timestamp timestamp)))) (defn make-context [command command-registry] @@ -134,6 +132,32 @@ (is (= {:status "completed"} (:command/result result))) (is (true? (:command-result/success result)))))) +(deftest ->command-test + (testing "->command fills in :command/id and :command/timestamp when absent" + (let [command (cp/->command {:command/name :example/create-counter + :name "Counter A"})] + (is (uuid? (:command/id command))) + (is (instance? OffsetDateTime (:command/timestamp command))) + (is (= :example/create-counter (:command/name command))) + (is (= "Counter A" (:name command))))) + + (testing "->command preserves a caller-supplied :command/id and :command/timestamp" + (let [id (uuid/v4) + ts (OffsetDateTime/now) + command (cp/->command {:command/name :example/create-counter + :command/id id + :command/timestamp ts})] + (is (= id (:command/id command))) + (is (= ts (:command/timestamp command))))) + + (testing "a command built with ->command round-trips through process-command" + (let [command (cp/->command {:command/name :test/create-resource}) + registry (make-registry {:test/create-resource successful-handler}) + context (make-context command registry) + result (cp/process-command context)] + (is (not (contains? result ::anom/category))) + (is (true? (:command-result/success result)))))) + (deftest test-command-with-events (testing "Events are persisted to event store when present in result" (let [command (make-command :test/create-with-events) @@ -508,8 +532,8 @@ parent-handler (fn [context] (let [child-ctx (assoc context :command-processor/skip-event-storage true) child-result (cp/process-command - (assoc child-ctx - :command (make-command :test/skip-storage-child)))] + (assoc child-ctx + :command (make-command :test/skip-storage-child)))] {:command-result/events (:command-result/events child-result) :command/result {:aggregated true}})) command (make-command :test/skip-storage) diff --git a/components/command-request-handler-v2/src/ai/obney/grain/command_request_handler_v2/core.clj b/components/command-request-handler-v2/src/ai/obney/grain/command_request_handler_v2/core.clj index 364827f0..f9859635 100644 --- a/components/command-request-handler-v2/src/ai/obney/grain/command_request_handler_v2/core.clj +++ b/components/command-request-handler-v2/src/ai/obney/grain/command_request_handler_v2/core.clj @@ -2,7 +2,6 @@ (:require [ai.obney.grain.anomalies.interface :refer [anomaly?]] [ai.obney.grain.command-processor-v2.interface.schemas :as command-schema] [ai.obney.grain.command-processor-v2.interface :as cp] - [ai.obney.grain.time.interface :as time] [clojure.core.async :as async] [com.brunobonacci.mulog :as u] [cognitect.anomalies :as anom] @@ -50,9 +49,9 @@ (defn decode-command [command] - (-> command - (assoc :command/id (random-uuid)) - (assoc :command/timestamp (time/now)))) + ;; Always (re)generate :command/id and :command/timestamp server-side, ignoring + ;; any client-supplied values, by dropping them before ->command fills them in. + (cp/->command (dissoc command :command/id :command/timestamp))) (defn prep-response [response] diff --git a/development/src/example_app_demo.clj b/development/src/example_app_demo.clj index c25699e6..ce0037b6 100644 --- a/development/src/example_app_demo.clj +++ b/development/src/example_app_demo.clj @@ -26,8 +26,7 @@ ;; (service/stop service) - "" - ) + "") (comment @@ -39,40 +38,36 @@ (try (cp/process-command (assoc context - :command {:command/name :example/create-counter - :command/timestamp (time/now) - :command/id (random-uuid) - :name "Counter A"})) + :command (cp/->command {:command/name :example/create-counter + :name "Counter A"}))) (catch Exception e (ex-data e))) (into [] (es/read event-store {:tenant-id tenant-id})) - (def counters - (->> (qp/process-query - (assoc context - :query {:query/name :example/counters - :query/timestamp (time/now) - :query/id (random-uuid)})) - :query/result)) + @(def counters + (->> (qp/process-query + (assoc context + :query {:query/name :example/counters + :query/timestamp (time/now) + :query/id (random-uuid)})) + :query/result)) - (def counter - (->> (qp/process-query - (assoc context - :query {:query/name :example/counter - :query/timestamp (time/now) - :query/id (random-uuid) - :counter-id (:counter/id (first counters))})) - :query/result)) + @(def counter + (->> (qp/process-query + (assoc context + :query {:query/name :example/counter + :query/timestamp (time/now) + :query/id (random-uuid) + :counter-id (:counter/id (first counters))})) + :query/result)) (cp/process-command (assoc context - :command {:command/name :example/increment-counter - :command/timestamp (time/now) - :command/id (random-uuid) - :counter-id (:counter/id counter)})) + :command (cp/->command {:command/name :example/increment-counter + :counter-id (:counter/id counter)}))) ;; Projects the :example/counters read model (read-model-processor-v2). @@ -83,8 +78,7 @@ (into [] (es/read event-store {:tenant-id tenant-id})) - "" - ) + "") (comment ;; Interact with the service via HTTP @@ -101,15 +95,15 @@ (catch Exception e (ex-data e))) ;; Get all counters - (def counters - (try - (:body - (http/post - "http://localhost:8080/query" - {:content-type :transit+json - :as :transit+json - :form-params {:query {:query/name :example/counters}}})) - (catch Exception e (ex-data e)))) + @(def counters + (try + (:body + (http/post + "http://localhost:8080/query" + {:content-type :transit+json + :as :transit+json + :form-params {:query {:query/name :example/counters}}})) + (catch Exception e (ex-data e)))) @@ -143,5 +137,4 @@ - "" - ) + "")