Adding Crux to a Fulcro template
Adding Crux database to a Fulcro template project. To set up Emacs for Clojure and ClojureScript development with Cider see e.g.: My Emacs configuration
New Fulco project from template
1lein new fulcro yourprojectname
Your Fulcro project is now in the subdirectory yourprojectname
.
Initialize development environment (once)
1npm install # only need to do this once
Add Crux to deps.edn
In file deps.edn
add juxt/crux
to existing dependencies and emacs stuff:
1{:paths ["src/main" "resources"]
2
3 :deps {bidi {:mvn/version "2.1.5"}
4 bk/ring-gzip {:mvn/version "0.3.0"}
5 com.taoensso/timbre {:mvn/version "4.10.0"}
6 com.wsscode/pathom {:mvn/version "2.2.12"}
7 fulcrologic/fulcro {:mvn/version "2.8.8"}
8 fulcrologic/fulcro-incubator {:mvn/version "0.0.32"}
9 garden {:mvn/version "1.3.6"}
10 hiccup {:mvn/version "1.0.5"}
11 juxt/crux {:mvn/version "19.04-1.0.3-alpha"}
12 http-kit {:mvn/version "2.3.0"}
13 ; clj-time {:mvn/version "0.15.1"}
14 mount {:mvn/version "0.1.14"}
15 org.clojure/clojure {:mvn/version "1.10.1-beta2"}
16 org.clojure/core.async {:mvn/version "0.4.490"}
17 ring/ring-core {:mvn/version "1.7.1"}
18 ring/ring-defaults {:mvn/version "0.3.2"}}
19
20 :aliases {:clj-tests {:extra-paths ["src/test"]
21 :main-opts ["-m" "kaocha.runner"]
22 :extra-deps {lambdaisland/kaocha {:mvn/version "0.0-389"}}}
23
24 ;; See https://github.com/clojure-emacs/cider-nrepl/blob/master/deps.edn for Emacs support
25 :dev {:extra-paths ["src/test" "src/dev" "src/workspaces"]
26 :jvm-opts ["-XX:-OmitStackTraceInFastThrow"]
27 :extra-deps {org.clojure/clojurescript {:mvn/version "1.10.520"}
28 fulcrologic/fulcro-spec {:mvn/version "3.0.0"}
29 thheller/shadow-cljs {:mvn/version "2.8.25"}
30 binaryage/devtools {:mvn/version "0.9.10"}
31 nubank/workspaces {:mvn/version "1.0.3"},
32 fulcrologic/fulcro-inspect {:mvn/version "2.2.4"}
33 org.clojure/tools.namespace {:mvn/version "0.3.0-alpha4"}
34 org.clojure/tools.nrepl {:mvn/version "0.2.13"}
35 cider/cider-nrepl {:mvn/version "0.21.0"}}}
36 :cider-clj {:extra-deps {org.clojure/clojure {:mvn/version "1.9.0"}}
37 :main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]"]}
38
39 :cider-cljs {:extra-deps {org.clojure/clojure {:mvn/version "1.9.0"}
40 org.clojure/clojurescript {:mvn/version "1.10.339"}
41 cider/piggieback {:mvn/version "0.3.9"}}
42 :main-opts ["-m" "nrepl.cmdline" "--middleware"
43 "[cider.nrepl/cider-middleware,cider.piggieback/wrap-cljs-repl]"]}}}
Add Crux configuration to defaults.edn
To file src/main/config/defaults.edn
add the following section:
1;;TODO Check validity of these parameters; they don't work in production uberjar!
2;; See: https://juxt.pro/crux/docs/configuration.html
3 :crux.api/config {:kv-backend "crux.kv.memdb.MemKv"
4 :db-dir "data/db-dir-1"}
Add file db_server.clj
Add file src/main/yourprojectname/server_components/db_server.clj
with following contents:
1(ns yourprojectname.server-components.db-server
2 (:require
3 [yourprojectname.server-components.config :refer [config]]
4 [mount.core :refer [defstate]]
5 [clojure.pprint :refer [pprint]]
6 [taoensso.timbre :as log]
7 [crux.api :as crux]))
8
9(defstate db-server
10 :start (let [cfg (::crux/config config)]
11 (log/info "Starting Database Server with config " (with-out-str (pprint cfg)))
12 (crux/start-standalone-system cfg))
13 :stop (.close db-server))
Change file http_server.clj
Change require in file src/main/yourprojectname/server_components/http_server.clj
to include yourprojectname.server-components.db-server
namespace:
1(:require
2 [yourprojectname.server-components.config :refer [config]]
3 [yourprojectname.server-components.middleware :refer [middleware]]
4 [yourprojectname.server-components.db-server]
5 [mount.core :refer [defstate]]
6 [clojure.pprint :refer [pprint]]
7 [org.httpkit.server :as http-kit]
8 [taoensso.timbre :as log])
Replace user.clj file
Replace file src/main/yourprojectname/model/user.clj
with following contents:
1(ns yourprojectname.model.user
2 (:require
3 [com.wsscode.pathom.connect :as pc]
4 [yourprojectname.server-components.pathom-wrappers :refer [defmutation defresolver]]
5 [yourprojectname.server-components.db-server :refer [db-server]]
6 [taoensso.timbre :as log]
7 #_[clj-time.core :as time]
8 #_[clj-time.format :as ftime]
9 [crux.api :as crux]))
10
11#_(def built-in-formatter (ftime/formatters :date-time))
12
13(defn dump-db []
14 (let [q (crux/q (crux/db db-server)
15 '{:find [i]
16 :where [[i :crux.db/id _]]})]
17 (map (fn [e] (crux/entity (crux/db db-server) (first e))) q)))
18
19#_(def user-database (atom {}))
20;; Example contents dump.
21;; @user-database
22#_{"e996f209-0810-4b29-ab5d-530582769ccd"
23 #:user{:id "e996f209-0810-4b29-ab5d-530582769ccd",
24 :name "User e996f209-0810-4b29-ab5d-530582769ccd"},
25 "3622054c-6dd9-4d60-a686-581dd95b51eb"
26 #:user{:id "3622054c-6dd9-4d60-a686-581dd95b51eb",
27 :name "User 3622054c-6dd9-4d60-a686-581dd95b51eb"},
28 "9d75d157-ec7e-4b0b-8c70-d615cb3152a8"
29 #:user{:id "9d75d157-ec7e-4b0b-8c70-d615cb3152a8",
30 :name "User 9d75d157-ec7e-4b0b-8c70-d615cb3152a8"},
31 "c008fa6a-c348-4386-82c1-f04d04dcf65f"
32 #:user{:id "c008fa6a-c348-4386-82c1-f04d04dcf65f",
33 :name "User c008fa6a-c348-4386-82c1-f04d04dcf65f"}}
34
35(defresolver all-users-resolver
36 "Resolve queries for :all-users."
37 [env input]
38 {;;GIVEN nothing
39 ::pc/output [{:all-users [:user/id]}]}
40
41 ;; I can output all users. NOTE: only ID is needed...other resolvers resolve the rest.
42 #_(log/info "All users. Database contains: " @user-database)
43 (let [q (crux/q (crux/db db-server)
44 '{:find [d]
45 :where [[_ :user/id d]]})]
46 {:all-users (mapv (fn [id] {:user/id (first id)})
47 #_(keys @user-database)
48 q)}))
49
50(defresolver user-resolver
51 "Resolve details of a single user. (See pathom docs for adding batching)"
52 [env {:user/keys [id]}]
53 {::pc/input #{:user/id} ; GIVEN a user ID
54 ::pc/output [:user/name]} ; I can produce a user's details
55
56 ;; Look up the user (e.g. in a database), and return what you promised.
57 #_(when (contains? @user-database id)
58 (get @user-database id))
59 (let [kid (keyword id)
60 q (crux/entity (crux/db db-server) kid)]
61 (into {} (filter (fn [e] (= (namespace (key e)) "user")) q))))
62
63(defresolver user-address-resolver
64 "Resolve address details for a user. Note the address data could be stored on the user in the database or elsewhere."
65 [env {:user/keys [id]}]
66 {::pc/input #{:user/id} ; GIVEN a user ID
67 ::pc/output [:address/id :address/street :address/city :address/state :address/postal-code]}
68
69 ;;TODO Address with user in database (get it here).
70 ;; I can produce address details
71 (log/info "Resolving address for " id)
72 #_{:address/id "fake-id"
73 :address/street "111 Main St."
74 :address/city "Nowhere"
75 :address/state "WI"
76 :address/postal-code "99999"}
77
78 (let [kid (keyword id)
79 q (crux/entity (crux/db db-server) kid)]
80 (into {} (filter (fn [e] (= (namespace (key e)) "address")) q))))
81
82(defmutation upsert-user
83 "Add/save a user. Required parameters are:
84
85 :user/id - The ID of the user
86 :user/name - The name of the user
87
88 Returns a User (e.g. :user/id) which can resolve to a mutation join return graph.
89 "
90 [{:keys [config ring/request]} {:user/keys [id name]}]
91 {::pc/params #{:user/id :user/name}
92 ::pc/output [:user/id]}
93
94 (log/debug "Upsert user with server config that has keys: " (keys config))
95 (log/debug "Ring request that has keys: " (keys request))
96 (log/debug "UPSERT-USER: " id " " name)
97 (when (and id name)
98 ;;TODO Add user to database; example given below:
99 (let [kid (keyword id)]
100 (crux/submit-tx db-server
101 [[:crux.tx/put kid ; id for Kafka
102 {:crux.db/id kid ; id for Crux
103 :user/id id
104 :user/name name
105 :address/id "fake-id"
106 :address/street (str (int (rand 1000)) " Main Street")
107 :address/city (nth ["New York" "Los Angeles"
108 "Chicago" "Houston"
109 "Phoenix" "Philadelphia"
110 "San Antonio" "San Diego"
111 "Dallas" "San Jose"] (int (rand 10)))
112 :address/state "WI"
113 :address/postal-code "99999"}]]))
114 #_(swap! user-database assoc id {:user/id id
115 :user/name name})
116 ;; Returning the user id allows the UI to query for the result. In
117 ;; this case we're "virtually" adding an address for them!
118 {:user/id id}))
Start headless REPL
Start a headless REPL.
1npx shadow-cljs server
Jot down the port on which the nREPL server started.
Connect to REPL (for Clojure)
In Emacs use M-x cider-connect
to connect to the REPL. Normally you can use the default (localhost) and also press ENTER for the port number (which automatically finds the port number). If it doesn't work then use the jotted down port number.
Start the server in the Clojure REPL
1(start)
The page can be found at: http://localhost:3000.
ClojureScript build
Navigate to http://localhost:9630 and enable main
build; wait until it completes; reload page at http://localhost:3000/.
Create a second connection to the REPL (for ClojureScript)
Under Emacs use CIDER
-> ClojureScript
-> Connect to a Clojurescript REPL
with defaults for hosts and port; answer yes to =A session with the same parameters exists (…). You can connect a sibling instead. Proceed?=; choose shadow-select
as the REPL type and main
for build.
Initial app state
To view initial app state (e.g. in ClojureScript REPL) use the following (optionally with cljs.pprint/print
to get a nicely formatted version):
1(fulcro.client.primitives/get-initial-state yourprojectname.ui.root/Root {})
Current app state
To view current app state (e.g. in ClojureScript REPL) use the following (optionally with cljs.pprint/print
to get a nicely formatted version):
1@(fulcro.client.primitives/app-state (get @yourprojectname.client/SPA :reconciler))
Screendump (example)
After playing around with the demo project and adding some users, one gets e.g.:
Database dump (example)
From your Clojure REPL prompt, do a dump-db to get the database contents matching the above screendump example.
1user> (yourprojectname.model.user/dump-db)
2({:crux.db/id :5dd9c16d-42bb-46bf-a5b7-be4728d7cd92,
3 :user/id "5dd9c16d-42bb-46bf-a5b7-be4728d7cd92",
4 :user/name "User 5dd9c16d-42bb-46bf-a5b7-be4728d7cd92",
5 :address/id "fake-id",
6 :address/street "846 Main Street",
7 :address/city "Phoenix",
8 :address/state "WI",
9 :address/postal-code "99999"}
10 {:crux.db/id :8614d58e-ad02-4dd4-8c17-740f981d38de,
11 :user/id "8614d58e-ad02-4dd4-8c17-740f981d38de",
12 :user/name "User 8614d58e-ad02-4dd4-8c17-740f981d38de",
13 :address/id "fake-id",
14 :address/street "403 Main Street",
15 :address/city "San Diego",
16 :address/state "WI",
17 :address/postal-code "99999"}
18 {:crux.db/id :389ae1ee-0197-4236-a79c-df906a64fbbe,
19 :user/id "389ae1ee-0197-4236-a79c-df906a64fbbe",
20 :user/name "User 389ae1ee-0197-4236-a79c-df906a64fbbe",
21 :address/id "fake-id",
22 :address/street "822 Main Street",
23 :address/city "Los Angeles",
24 :address/state "WI",
25 :address/postal-code "99999"}
26 {:crux.db/id :b7a3146b-8ac0-4b7b-94b6-f3a2c400dbfa,
27 :user/id "b7a3146b-8ac0-4b7b-94b6-f3a2c400dbfa",
28 :user/name "User b7a3146b-8ac0-4b7b-94b6-f3a2c400dbfa",
29 :address/id "fake-id",
30 :address/street "654 Main Street",
31 :address/city "Dallas",
32 :address/state "WI",
33 :address/postal-code "99999"}
34 {:crux.db/id :2f72197e-8829-47c3-9d52-dd54bac5ed1c,
35 :user/id "2f72197e-8829-47c3-9d52-dd54bac5ed1c",
36 :user/name "User 2f72197e-8829-47c3-9d52-dd54bac5ed1c",
37 :address/id "fake-id",
38 :address/street "652 Main Street",
39 :address/city "Philadelphia",
40 :address/state "WI",
41 :address/postal-code "99999"})