Almost all components that you need on runtime should be reachable via the passed around state. To achieve this it should be part of the :deps map in the state. Any other configuration what you need in runtime should be part of this map too.
The system configuration and start-up with the chainable set-up:
(defn ->system
  [app-cfg]
  (-> (config/config)
      (merge app-cfg)
      (rename-key :xiana/auth :auth)
      (rename-key :xiana/uploads :uploads)
      routes/reset
      session/init-backend
      sse/init
      db/start
      db/migrate!
      (scheduler/start actions/ping 10000)
      (scheduler/start actions/execute-scheduled-actions (* 60 1000))
      ws/start
      closeable-map))
(defn app-cfg
  [config]
  {:routes                  routes
   :router-interceptors     [(spa-index/wrap-default-spa-index "/re-frame")]
   :controller-interceptors (concat [(xiana-interceptors/muuntaja)
                                     cookies/interceptor
                                     xiana-interceptors/params
                                     (session/protected-interceptor "/api" "/login")
                                     xiana-interceptors/view
                                     xiana-interceptors/side-effect
                                     db/db-access]
                                    (:controller-interceptors config))})
(defn -main
  [& _args]
  (->system (app-cfg {})))
Database migration is based on the following principles:
The migration process requires a config file containing:
:xiana/postgresql {:port     5432
                   :dbname   "framework"
                   :host     "localhost"
                   :dbtype   "postgresql"
                   :user     "postgres"
                   :password "postgres"}
:xiana/migration {:store                :database
                  :migration-dir        ["resources/migrations"]
                  :init-in-transaction? false
                  :migration-table-name "migrations"}
The :migration-dir param is a vector of classpath relative paths containing database migrations scripts.
The xiana.db.migrate implements a cli for migrations framework.
If you add to deps.edn in :aliases section:
:migrate {:main-opts ["-m" "xiana.db.migrate"]}
you could access this cli from clojure command.
To see all commands and options available run:
clojure -M:migrate --help
Examples of commands:
# update the database to current version:
clojure -M:migrate migrate -c resources/config.edn
# rollback the last run migration script:
clojure -M:migrate rollback -c resources/config.edn
# rollback the database down until id script: 
clojure -M:migrate rollback -i 20220103163538 -c resources/config.edn
# create the migrations scripts pair: 
clojure -M:migrate create -d resources/migrations -n the-name-of-the-script
Typical use-case, and ordering looks like this:
{:router-interceptors     [app/route-override?]
 :controller-interceptors [(interceptors/muuntaja)
                           interceptors/params
                           session/interceptor
                           interceptors/view
                           interceptors/db-access
                           rbac/interceptor]}
Which means:
An interceptor is a map of three functions.
:enter Runs while we are going down from the request to it's action, in the order of executors
:leave Runs while we're going up from the action to the response.
:error Executed when any error thrown while executing the two other functions
The provided function should have one parameter, the application state, and should return the state.
{:enter (fn [state]
          (println "Enter: " state)
          (-> state
              (transform-somehow)
              (or-do-side-effects))
          :leave (fn [state]
                   (println "Leave: " state)
                   state)
          :error (fn [state]
                   (println "Error: " state)
                   ;; Here `state` should have previously thrown exception
                   ;; stored in `:error` key.
                   ;; you can do something useful with it (e.g. log it)
                   ;; and/or handle it by `dissoc`ing from the state.
                   ;; In that case remaining `leave` interceptors will be executed.
                   (assoc state :response {:status 500 :body "Error occurred while printing out state"}))}
The router and controller interceptors are executed in the exact same order (enter functions in order, leave 
functions in reversed order), but not in the same place of the execution flow.
The handler function executes interceptors in this order
In router interceptors, you are able to interfere with the routing mechanism. Controller interceptors can be interfered with via route definition.
The router and controller interceptors definition is part of the application startup. The system's dependency map should contain two sequence of interceptors like
{:router-interceptors     [...]
 :controller-interceptors [...]}
On route definition you can interfere with the default controller interceptors. With the route definition you are able to set up different controller interceptors other than the ones already defined with the app. There are three ways to do it:
... {:action       #(do something)
     :interceptors [...]}
will override all controller interceptors
... {:action       #(do something)
     :interceptors {:around [...]}}
will extend the defaults around
... {:action       #(do something)
     :interceptors {:inside [...]}}
will extend the defaults inside
... {:action       #(do something)
     :interceptors {:inside [...]
                    :around [...]}}
will extend the defaults inside and around
... {:action       #(do something)
     :interceptors {:except [...]}}
will skip the excepted interceptors from defaults
The execution flow will look like this
All interceptors in :except will be skipped.
Route definition is done via reitit's routing library. Route processing is done
with xiana.route namespace. At route definition you can define.
If any extra parameter is provided here, it's injected into
(-> state :request-data :match)
in routing step.
The action function in a single CRUD application is for defining a view, a database-query and optionally a side-effect function which will be executed in the following interceptor steps.
(defn action
  [state]
  (assoc state :view view/success
               :side-effect behaviour/update-sessions-and-db!
               :query model/fetch-query))
The database.core's interceptor extracts the datasource from the provided state parameter and the :query.
The query should be in honey SQL format, it will be sql-formatted on execution:
(defn fetch-query
  [state]
  (let [login (-> state :request :body-params :login)]
    (-> (select :*)
        (from :users)
        (where [:and
                :is_active
                [:or
                 [:= :email login]
                 [:= :username login]]]))))
The execution always has {:return-keys true} parameter and the result goes into
(-> state :response-data :db-data)
without any transformation.
A view is a function to prepare the final response and saving it into the state based on whatever happened before.
(defn success
  [state]
  (let [{:users/keys [id]} (-> state :response-data :db-data first)]
    (assoc state :response {:status  200
                            :headers {"Content-type" "Application/json"}
                            :body    {:view-type "login"
                                      :data      {:login   "succeed"
                                                  :user-id id}}})))
Conventionally, side-effects interceptor is placed after action and database-access, just right before view. At this point, we already have the result of database execution, so we are able to do some extra refinements, like sending notifications, updating the application state, filtering or mapping the result and so on.
Adding to the previous examples:
(defn update-sessions-and-db!
  "Creates and adds a new session to the server's store for the user that wants to sign-in.
   Avoids duplication by firstly removing the session that is related to this user (if it exists).
   After the session addition, it updates the user's last-login value in the database."
  [state]
  (if (valid-credentials? state)
    (let [new-session-id (str (UUID/randomUUID))
          session-backend (-> state :deps :session-backend)
          {:users/keys [id] :as user} (-> state :response-data :db-data first)]
      (remove-from-session-store! session-backend id)
      (xiana-sessions/add! session-backend new-session-id user)
      (update-user-last-login! state id)
      (assoc-in state [:response :headers "Session-id"] new-session-id))
    (throw (ex-info "Missing session data"
                    {:xiana/response
                     {:status 401
                      :body   "You don't have rights to do this"}}))))
Session interceptor interchanges session data between the session-backend and the app state.
On :enter it loads the session by its session-id, into (-> state :session-data)
The session-id can be provided either in headers, cookies, or as query-param. When session-id is found nowhere or is an invalid UUID, or the session is not stored in the storage, then the response will be:
{:status 401
 :body   "Invalid or missing session"}
On the :leave branch, updates session storage with the data from (-> state :session-data)
To get the benefits of tiny RBAC library you need to provide the resource and the action for your endpoint in router definition:
[["/api"
  ["/image" {:delete {:action     delete-action
                      :permission :image/delete}}]]]
and add your role-set into your app's dependencies:
(defn ->system
  [app-cfg]
  (-> (config/config)
      (merge app-cfg)
      xiana.rbac/init
      ws/start))
On :enter, the interceptor performs the permission check. It determines if the action allowed for the user found
in (-> state :session-data :user). If access to the resource/action isn't permitted, then the response is:
{:status 403
 :body   "Forbidden"}
If a permission is found, then it goes into (-> state :request-data :user-permissions) as a parameter for data
ownership processing.
On :leave, executes the restriction function found in (-> state :request-data :restriction-fn). The restriction-fn
should look like this:
(defn restriction-fn
  [state]
  (let [user-permissions (get-in state [:request-data :user-permissions])]
    (cond
      (user-permissions :image/all) state
      (user-permissions :image/own) (let [session-id (get-in state [:request :headers "session-id"])
                                          session-backend (-> state :deps :session-backend)
                                          user-id (:users/id (session/fetch session-backend session-id))]
                                      (update state :query sql/merge-where [:= :owner.id user-id])))))
The rbac interceptor must be placed between the action and the db-access interceptors in the interceptor chain.
To use an endpoint to serve a WebSockets connection, you can define it on route-definition alongside the restfull action:
(def routes
  [[...]
   ["/ws" {:ws-action websocket/echo
           :action    restfull/hello}]])
In :ws-action function you can provide the reactive functions in (-> state :response-data :channel)
(:require
  ...
  [xiana.websockets :refer [router string->]]
  ...)
(defonce channels (atom {}))
(def routing
  (partial router routes string->))
(defn chat-action
  [state]
  (assoc-in state [:response-data :channel]
            {:on-receive (fn [ch msg]
                           (routing (update state :request-data
                                            merge {:ch         ch
                                                   :income-msg msg
                                                   :fallback   views/fallback
                                                   :channels   channels})))
             :on-open    (fn [ch]
                           (routing (update state :request-data
                                            merge {:ch         ch
                                                   :channels   channels
                                                   :income-msg "/welcome"})))
             :on-ping    (fn [ch data])
             :on-close   (fn [ch status] (swap! channels dissoc ch))
             :init       (fn [ch])}))
The creation of the actual channel happens in Xiana's handler. All provided reactive functions have the entire state to work with.
xiana.websockets offers a router function, which supports Xiana concepts. You can define a reitit route and use it
inside WebSockets reactive functions. With Xiana state
and support of interceptors, with interceptor override. You
can define a fallback function, to handle missing actions.
(def routes
  (r/router [["/login" {:action       behave/login
                        :interceptors {:inside [interceptors/side-effect
                                                interceptors/db-access]}
                        :hide         true}]]              ;; xiana.websockets/router will not log the message 
            {:data {:default-interceptors [(interceptors/message "Incoming message...")]}}))
For route matching Xiana provides a couple of modes:
extract from string
The first word of given message as actionable symbol
from JSON
The given message parsed as JSON, and :action is the actionable symbol
from EDN
The given message parsed as EDN, and :action is the actionable symbol
Probe
It tries to decode the message as JSON, then as EDN, then as string.
You can also define your own matching, and use it as a parameter to xiana.websockets/router
Xiana contains a simple SSE solution over http-kit server's Channel
protocol.
Initialization is done by calling xiana.sse/init. Clients can subscribe by routing to xiana.sse/sse-action. Messages
are sent with xiana.sse/put! function.
(ns app.core
  (:require
    [xiana.config :as config]
    [xiana.sse :as sse]
    [xiana.route :as route]
    [xiana.webserver :as ws]))
(def routes
  [["/sse" {:action sse/sse-action}]
   ["/broadcast" {:action (fn [state]
                            (sse/put! state {:message "This is not a drill!"})
                            state)}]])
(defn ->system
  [app-cfg]
  (-> (config/config)
      (merge app-cfg)
      sse/init
      ws/start))
(def app-cfg
  {:routes routes})
(defn -main
  [& _args]
  (->system app-cfg))
To repeatedly execute a function, you can use the xiana.scheduler/start function. Below is an implementation of SSE
ping:
(ns app.core
  (:require
    [xiana.scheduler :as scheduler]
    [clojure.core.async :as async]))
(defn ping [deps]
  (let [channel (get-in deps [:events-channel :channel])]
    (async/>!! channel {:type      :ping
                        :id        (str (UUID/randomUUID))
                        :timestamp (.getTime (Date.))})))
(defn ->system
  [app-cfg]
  (-> (config/config)
      (merge app-cfg)
      ...
      sse/init
      (scheduler/start ping 10000)
      ...))
Can you improve this documentation? These fine people already did:
Krisztian Gulyas, Stas Makarov, g-krisztian & Marius A. RabenarivoEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs | 
| ← | Move to previous article | 
| → | Move to next article | 
| Ctrl+/ | Jump to the search field |