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"})

Posts in this Series