diff --git a/.gitignore b/.gitignore
index 7e5ff3d1..588f83e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -121,3 +121,5 @@ pom.xml.asc
.clj-kondo/*
!.clj-kondo/config.edn
*.iml
+/node_modules/
+/package.json
diff --git a/README.md b/README.md
index 2d0b2d40..1552fc4d 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,10 @@
-![Xiana logo](resources/images/Xiana.png)
+![Xiana logo](doc/resources/images/Xiana.png)
+
# Xiana framework
Xiana is a lightweight web-application framework written in Clojure, for Clojure. The goal is to be simple, fast, and
-most importantly - a welcoming platform for web programmers with different backgrounds who want to experience the wonders
-of functional programming!
+most importantly - a welcoming platform for web programmers with different backgrounds who want to experience the
+wonders of functional programming!
It's easy to install, fun to experiment with, and a powerful tool to produce monolithic web applications.
@@ -16,7 +17,9 @@ Xiana has its own Leiningen template, so you can create a skeleton project with
```shell
lein new xiana app
```
-[getting-started](./doc/getting-started.md) explains how to use this to create a very simple app with a db, a backend offering an API, and a frontend that displays something from the database.
+
+[getting-started](./doc/getting-started.md) explains how to use this to create a very simple app with a db, a backend
+offering an API, and a frontend that displays something from the database.
### As a dependency
@@ -28,8 +31,6 @@ Add it to your project as a dependency from clojars:
- First check out the [conventions](./doc/conventions.md).
- To start working with xiana, read the [tutorials](./doc/tutorials.md).
-- A hands-on approach in the [how-to](./doc/How-To.md)s.
-- Check the available [modules](./doc/modules.md), and [interceptors](./doc/interceptors.md).
- To contribute, see the [contribution](./doc/contribution.md) docs.
### Examples
@@ -45,5 +46,7 @@ Visit [examples folder](examples), to see how you can perform
## References
### Concept of interceptors
+
http://pedestal.io/reference/interceptors
+
https://github.com/metosin/sieppari
diff --git a/deps.edn b/deps.edn
index 0bfe8185..efc48263 100644
--- a/deps.edn
+++ b/deps.edn
@@ -39,13 +39,20 @@
{:extra-deps {clj-kondo/clj-kondo {:mvn/version "2022.05.31"}}
:main-opts ["-m" "clj-kondo.main" "--lint" "src"]}
- :codox {:extra-deps {codox/codox {:mvn/version "0.10.8"}}
+ :codox {:extra-deps {codox/codox {:mvn/version "0.10.8"}}
:extra-paths ["resources"]
- :exec-fn codox.main/generate-docs
- :exec-args {:output-path "docs/new/"
- :themes [:default :xiana]
- :source-paths ["src"]
- :doc-files ["doc/getting-started.md", "doc/How-To.md", "doc/Development-Guide.md"]}}
+ :exec-fn codox.main/generate-docs
+ :exec-args {:output-path "docs/new/"
+ :themes [:default :xiana]
+ :metadata {:doc/format :markdown}
+ :source-uri "https://github.com/Flexiana/framework/blob/{git-commit}/{filepath}#L{line}"
+ :source-paths ["src"]
+ :doc-files ["doc/welcome.md"
+ "doc/conventions.md"
+ "doc/tutorials.md"
+ "doc/how-to.md"
+ "doc/contribution.md"
+ "doc/getting-started.md"]}}
:kibit
{:extra-deps {tvaughan/kibit-runner {:mvn/version "1.0.1"}}
diff --git a/doc/Development-Guide.md b/doc/Development-Guide.md
deleted file mode 100644
index 68e12396..00000000
--- a/doc/Development-Guide.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Development guide
-TODO fill this with everything a developer starting to work on Xiana might need to know
-
-* Ticket System
-* Coding standards
-* Submitting a PR
-* Making a release
-
-
-## Generating API Docs
-
-This is done automatically using *Codox*
-
-To generate or update the current version run the script:
-
-```shell
-script/build-docs.sh
-```
-
-This runs the following:
-
-```shell
-clj -X:codox
-mv docs/new docs/{{version-number}}
-```
-
-It also updates the index.html file to point to the new version.
diff --git a/doc/contribution.md b/doc/contribution.md
index d9f218f9..0b72a75c 100644
--- a/doc/contribution.md
+++ b/doc/contribution.md
@@ -1,9 +1,37 @@
+
+
# Contribution
+- [Ticket system](#ticket-system)
+- [Coding standards](#coding-standards)
+- [Submitting a PR](#submitting-a-pr)
- [Development dependencies](#development-dependencies)
- [Setup](#setup)
- [Deps](#deps)
- [Releasing](#releasing)
+- [Generating API docs](#generating-api-docs)
+
+## Ticket system
+
+We're using GitHub [issues](https://github.com/Flexiana/framework/issues) for tracking and discussing ideas and
+requests.
+
+## Coding standards
+
+Please follow `clj-style` and `kondo` instructions. `Kibit` isn't a showstopper, but PRs are more welcome if they don't
+break `kibit`.
+
+## Submitting a PR
+
+Before you submit a PR be sure:
+
+- You've updated the documentation and the [CHANGELOG](../CHANGELOG.md)
+- the PR has an issue in GitHub, with a good description
+- you have added tests
+- you provided an example project for a new feature
+
+All PRs need at least two approvals and pls
+follow [Semantic Versioning 2.0.0](https://semver.org/#semantic-versioning-200)
## Development dependencies
@@ -31,12 +59,8 @@
| nilenso/honeysql-postgres | 0.2.6 | PostGreSQL |
| org.postgresql/postgresql | 42.2.2 | PostGreSQL |
| crypto-password/crypto-password | 0.2.1 | Security |
-
-#### Optional
-
-| Name | Version | Provide |
-|---------------------|---------|---------|
-| clj-kondo/clj-kondo | RELEASE | Tests |
+| clj-kondo/clj-kondo | RELEASE | Tests |
+| npx | RELEASE | Documentation |
## Setup
@@ -84,4 +108,38 @@ clj -M:install
- Be sure all examples has the same framework version as it is in `release.edn` as dependency
- Execute `./example-tests.sh` script. It will install the actual version of xiana, and go through the examples folder
- for `check-style` and `lein test`.
\ No newline at end of file
+ for `check-style` and `lein test`.
+
+## Generating API Docs
+
+This is done with [mermaid-cli](https://github.com/mermaid-js/mermaid-cli) and a forked version
+of [Codox](https://github.com/Flexiana/codox).
+
+We're using mermaid-cli to render UML-diagrams in markdown files, see the `doc/conventions_template.md` for example.
+These files need to be added to the `/script/build-docs.sh` . For using it you need to have `npx`.
+
+Codox is forked because markdown anchors aren't converted to HTML anchors in the official release. To use it you need
+
+```shell
+git clone git@github.com:Flexiana/codox.git
+cd codox/codox
+lein install
+```
+
+it before generating the documentation.
+
+To generate or update the current version run the script:
+
+```shell
+./script/build-docs.sh
+```
+
+This runs the following:
+
+```shell
+npx -py @mermaid-js/mermaid-cli mmdc -i doc/conventions_template.md -o doc/conventions.md
+clj -X:codox
+mv docs/new docs/{{version-number}}
+```
+
+It also updates the index.html file to point to the new version.
\ No newline at end of file
diff --git a/doc/conventions-1.svg b/doc/conventions-1.svg
new file mode 100644
index 00000000..4e7c6022
--- /dev/null
+++ b/doc/conventions-1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/doc/conventions-2.svg b/doc/conventions-2.svg
new file mode 100644
index 00000000..9ee4bc7c
--- /dev/null
+++ b/doc/conventions-2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/doc/conventions-3.svg b/doc/conventions-3.svg
new file mode 100644
index 00000000..3141e7d6
--- /dev/null
+++ b/doc/conventions-3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/doc/conventions.md b/doc/conventions.md
index 8c6d50dc..104c5926 100644
--- a/doc/conventions.md
+++ b/doc/conventions.md
@@ -1,21 +1,29 @@
+
+
# Conventions
+- [Overview](#overview)
- [State](#state)
- [Action](#action)
- [Handler](#handler)
- [Dependencies](#dependencies)
- [Interceptors](#interceptors)
+- [Interceptors error handling](#interceptors-error-handling)
+
+## Overview
+The diagram bellow gives you an overview how a request is processed in Xiana based applications.
+![diagram](./conventions-1.svg)
## State
-A state record. It is created for each HTTP request and represents the current state of the application. It contains:
+State is created for each HTTP request and represents the current state of the application. It contains:
-- the application's dependencies
+- the application's dependencies and configuration
- request
- request-data
- response
-This structure is very volatile, will be updated quite often on the application's life cycle.
+This structure is very volatile, and will be updated quite often on the application's life cycle.
The main modules that update the state are:
@@ -25,7 +33,7 @@ The main modules that update the state are:
- Interceptors:
- Add, consumes or remove information from the state map. More details in [Interceptors](#interceptors) section.
+ Add, consume or remove information from the state map. More details in [Interceptors](#interceptors) section.
- Actions:
@@ -39,7 +47,7 @@ The state is renewed on every request.
The action conventionally is the control point of the application flow. This is the place were you can define how the
rest of your execution flow would behave. Here you can provide the database query, restriction function, the view, and
-the additional side effect functions are you want to execute.
+the additional side effect functions that you want to execute.
Actions are defined in the routes vector
@@ -49,20 +57,35 @@ Actions are defined in the routes vector
## Handler
-Xiana's handler does all the processing. It runs on every request and does the following. It creates the state for every
-request, matches the appropriate route, executes the interceptors, handles interceptor overrides, and not-found cases.
+Xiana's handler creates the state for every request, matches the appropriate route, executes the interceptors, handles
+interceptor overrides, and not-found cases.
It handles websocket requests too.
-### Routing
+## Routing
Routing means selecting the actions to execute depending on the request URL, and HTTP method.
## Dependencies
-Modules can depend on external resources, configurations, as well as on other modules. These dependencies are added to
+Modules can depend on external resources and configurations, as well as on other modules. These dependencies are added to
the state on state creation, and defined on application startup.
## Interceptors
-An interceptor is a pair of unary functions. Each function must recieve and return a state map. You can look at it as on an analogy to AOP's around aspect, or as on a pair of middlewares. They work mostly the same way as [pedestal](http://pedestal.io/reference/interceptors) and [sieppari](https://github.com/metosin/sieppari) interceptors.
-Xiana provides a set of base [interceptors](interceptors.md), for the most common use cases.
+An interceptor is a pair of unary functions. Each function must recieve and return a state map. Look at it as an analogy
+to AOP's around aspect, or as on a pair of middlewares. They work mostly the same way
+as [pedestal](http://pedestal.io/reference/interceptors) and [sieppari](https://github.com/metosin/sieppari)
+interceptors.
+Xiana provides a set of base interceptors, for the most common use cases.
+
+This figure shows how interceptors are executed ideally:
+![diagram](./conventions-2.svg)
+## Interceptors error handling:
+
+The interceptor executor handles the exceptional states like sieppari does. If an exception happens, it tries to handle
+first in the same interceptor. If it has an `:error` handler, it will call it, otherwise it'll search for `:error`
+handlers from the beginning of the interceptor queue. When an `:error` function found, and matched with the given
+exception, the executor calls the queue of `:leave` functions in reserved order from where the handler was found.
+
+This diagram shows how the error cases are handled:
+![diagram](./conventions-3.svg)
\ No newline at end of file
diff --git a/doc/conventions_template.md b/doc/conventions_template.md
new file mode 100644
index 00000000..76841c49
--- /dev/null
+++ b/doc/conventions_template.md
@@ -0,0 +1,165 @@
+
+
+# Conventions
+
+- [Overview](#overview)
+- [State](#state)
+- [Action](#action)
+- [Handler](#handler)
+- [Dependencies](#dependencies)
+- [Interceptors](#interceptors)
+- [Interceptors error handling](#interceptors-error-handling)
+
+## Overview
+
+The diagram bellow gives you an overview how a request is processed in Xiana based applications.
+
+```mermaid
+ sequenceDiagram
+ Web server->>Handler:Request
+ Note over Handler: Enhancing app state with request
+ Handler->>Router interceptors:State
+ Note over Router interceptors: Enter functions
+ Router interceptors->>Router: State
+ Note over Router:Matching, routing
+ Router->>Router interceptors:State
+ Note over Router interceptors: Leave functions in reverted order
+ Router interceptors->>Controller interceptors: State
+ Note over Controller interceptors: Enter functions
+ Controller interceptors->>Action: State
+ Note over Action:Executing controller
+ Action->>Controller interceptors:State
+ Note over Controller interceptors: Leave functions in reverted order
+ Controller interceptors->>Handler:State
+ Note over Handler:Extracting response
+ Handler->>Web server:Response
+```
+
+## State
+
+State is created for each HTTP request and represents the current state of the application. It contains:
+
+- the application's dependencies and configuration
+- request
+- request-data
+- response
+
+This structure is very volatile, and will be updated quite often on the application's life cycle.
+
+The main modules that update the state are:
+
+- Routes:
+
+ Add information from the matched route to the state map
+
+- Interceptors:
+
+ Add, consume or remove information from the state map. More details in [Interceptors](#interceptors) section.
+
+- Actions:
+
+ In actions, you are able to interfere with the :leave parts of the interceptors.
+
+At the last step of execution the handler extracts the response value from the state.
+
+The state is renewed on every request.
+
+## Action
+
+The action conventionally is the control point of the application flow. This is the place were you can define how the
+rest of your execution flow would behave. Here you can provide the database query, restriction function, the view, and
+the additional side effect functions that you want to execute.
+
+Actions are defined in the routes vector
+
+```clojure
+["/" {:get {:action #(do something)}}]
+```
+
+## Handler
+
+Xiana's handler creates the state for every request, matches the appropriate route, executes the interceptors, handles
+interceptor overrides, and not-found cases.
+It handles websocket requests too.
+
+## Routing
+
+Routing means selecting the actions to execute depending on the request URL, and HTTP method.
+
+## Dependencies
+
+Modules can depend on external resources and configurations, as well as on other modules. These dependencies are added to
+the state on state creation, and defined on application startup.
+
+## Interceptors
+
+An interceptor is a pair of unary functions. Each function must recieve and return a state map. Look at it as an analogy
+to AOP's around aspect, or as on a pair of middlewares. They work mostly the same way
+as [pedestal](http://pedestal.io/reference/interceptors) and [sieppari](https://github.com/metosin/sieppari)
+interceptors.
+Xiana provides a set of base interceptors, for the most common use cases.
+
+This figure shows how interceptors are executed ideally:
+
+```mermaid
+ sequenceDiagram
+ Handler->>Interceptor_I:State
+ Note over Interceptor_I: :enter
+ Interceptor_I->>Interceptor_II:State
+ Note over Interceptor_II: :enter
+ Interceptor_II->>Interceptor_III:State
+ Note over Interceptor_III: :enter
+ Interceptor_III->>Action:State
+ Note over Action: Do something
+ Action->>Interceptor_III:State
+ Note over Interceptor_III::leave
+ Interceptor_III->>Interceptor_II:State
+ Note over Interceptor_II::leave
+ Interceptor_II->>Interceptor_I:State
+ Note over Interceptor_I::leave
+ Interceptor_I->>Handler:State
+```
+
+## Interceptors error handling:
+
+The interceptor executor handles the exceptional states like sieppari does. If an exception happens, it tries to handle
+first in the same interceptor. If it has an `:error` handler, it will call it, otherwise it'll search for `:error`
+handlers from the beginning of the interceptor queue. When an `:error` function found, and matched with the given
+exception, the executor calls the queue of `:leave` functions in reserved order from where the handler was found.
+
+This diagram shows how the error cases are handled:
+
+```mermaid
+sequenceDiagram
+Handler->>Interceptor_I:State
+Note over Interceptor_I: :enter
+Interceptor_I->>Interceptor_II:State
+Note over Interceptor_II: :enter
+Interceptor_II->>Interceptor_III:State
+Note over Interceptor_III: :enter - Throwns an exception
+alt Exception handled by thower
+ Note over Interceptor_III: :error - Handles exception
+ Interceptor_III->>Interceptor_II:State
+ Note over Interceptor_II::leave
+ Interceptor_II->>Interceptor_I:State
+ Note over Interceptor_I::leave
+ Interceptor_I->>Handler:State
+
+else Exception not handled by thrower
+ Note over Interceptor_III: :error - Doesn't handles exception
+ Interceptor_III->>Interceptor_I:State
+ alt Interceptor_I handles the exception
+ Note over Interceptor_I: :error - Handles exception
+ Interceptor_I->>Handler:State
+ else Interceptor_II handles the exception
+ Note over Interceptor_I: :error - Doesn't handles exception
+ Interceptor_I->>Interceptor_II:State
+ Note over Interceptor_II: :error - Handles exception
+ Interceptor_II->>Interceptor_I:State
+ Note over Interceptor_I::leave
+ Interceptor_I->>Handler:State
+ end
+end
+ Note over Handler:Extract response
+participant Action
+```
diff --git a/doc/decisions/0001-record-architecture-decisions.md b/doc/decisions/0001-record-architecture-decisions.md
deleted file mode 100644
index 97f4ba88..00000000
--- a/doc/decisions/0001-record-architecture-decisions.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# 1. Record architecture decisions
-
-Date: 2021-01-12
-
-## Status
-
-Accepted
-
-## Context
-
-We need to record the architectural decisions made on this project.
-
-## Decision
-
-We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
-
-## Consequences
-
-See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).
diff --git a/doc/decisions/0002-database-basic-architecture.md b/doc/decisions/0002-database-basic-architecture.md
deleted file mode 100644
index 92c555c4..00000000
--- a/doc/decisions/0002-database-basic-architecture.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# 2. Database basic architecture
-
-Date: 2021-01-14
-
-## Status
-
-Proposed
-
-## Context
-
-1. The framework rationale requires a separation of concern between "code
-is code, data is data" and because of that some libraries were not
-able to proceed to be used in the application.
-
-2. The framework also wants to be a complete solution which means that
-our users should not be required to learn too many libraries in order
-to be productive. Instead, the framework should provide layers of
-indirection to wrap such functionalities and provide centralized
-control over the features.
-
-## Decision
-
-- Usage of `yogthos/config` to handle config files
-- Usage of `stuartsierra/components` to handle dependency management
-- Usage of `honeysql` to handle SQL interactions
-- Build a framework layer to handle migrations in `honeysql` style
-- Build framework layer to wrap honeysql, honeysql-plugins, next-jdbc,
- hikari-cp and possible others lirbaries.
-
-## Consequences
-
-- [Positive] Framework is more aligned with its goals
-- [Positive] Framework already have a `SQL` layer to be used
-- [Negative] Choosing to wrap underlying libraries adds the work to
- keep our library in sync with new features they release
-- [Negative] We were not able to use `integrant` as desired in the
- beginning because it violates the first rationale.
-- [Negative] We were not able to use `duct` as desired because it
- violates the first rationale
diff --git a/doc/decisions/0003-controller-architecture.md b/doc/decisions/0003-controller-architecture.md
deleted file mode 100644
index 7234ea24..00000000
--- a/doc/decisions/0003-controller-architecture.md
+++ /dev/null
@@ -1,153 +0,0 @@
-# 3. Controller basic architecture
-
-Date: 2021-01-27
-
-## Staus
-
-Proposed
-
-## Context
- 1. The Controller is responsible for the lifecycle of the framework. It reflects the starting point and the means of controll of all the needed actions that the framework must do (load deps, render views, generate routes etc).
- 2. The framework requires a simple and easy controll to the end user.
- 3. In turn the interaction of the user with the controller should be simple understandable and destructured in smaller steps that would provide a better grain of detail in the controll of the application, validation and error management etc.
- 4. The framework rational requires a functional approach but in a way that would be easy for new users and in a way that it would use user's previous concept knowledge from similar frameworks in other programming languages.
-
-## Decisions
-### 1. Controller being `monadic`
-#### Explanation:
-The Controller would be a composition of different functions in steps as was created in the initial draft by Lukáš Rychtecký.
-#### Why:
-This would provide the user with the ability to controll each
-step on it's creation.
-##### Example:
-
-``` clojure
-(defn controller
- []
- (-> {}
- (set-routes)
- (set-template)
- (set-response )
- (set-middlewares )
- (set-actions )
- (set-deps)
- (set-session))
- )
-```
-### 2. Controller use a State map.
-#### Explanation:
-The Controller would be 'feeded' with an initial state-map that the user would provide. The state-map would hold the data that would be needed to build the Controller. This approach is similar to the State monad but it would return only the result of the configuration and discard the initial state.
-#### Why:
-This helps in two ways:
-1. We can enforce a specific stucture in the definition of the state-map to the user. This would give a clearer understanding on the 'ingredients' that a controller would need to created to the user, as it would be a map which its keys would represent the different stages of the controller.
-2. We would be able to validate the state-map and its data.
-##### Example:
-###### state map:
-``` clojure
-(def state {:http-request {:method :get
- :url "/hi"
- :request {:action :select :params {}}}
- :response {:headers (list {"Content-Type" "txt/html"}
- {"charset" "UTF-8"})
- :body nil}
- :request-data {:model Foo
- :template (comp temp)
- :middlewares []}
- :session-data {}
- :deps {}})
-```
-
-###### state map schema:
-``` clojure
-;; mali schema for the State Map
-(def State-map
- [:map
- [:http-request [:map
- [:method [:enum :get :post :put]]
- [:url [:vector]]
- [:request [:action [:enum :select :update :delet :insert]]]]]
- [:response [:map
- [:headers [:vector]]
- [:body [:or [:string] [:vector] [:nil]]]
- ]]
- [:request-data [:map
- [:model ;;malli schema
- [:vector]]
- [:template ;; hiccup or static
- [:or [:vector]
- [:string]]]
- [:middlewares ;; list of functions to used for the middleware
- [:vector]]]]
- [:deps [:map]]
- [:session [:map]]])
-```
-
-###### controller with state-map:
-
-``` clojure
-(defn controller
- [state-map]
- (-> {}
- (set-routes state-map)
- (set-template state-map)
- (set-response state-map)
- (set-middlewares state-map)
- (set-actions state-map)
- (set-deps state-map)
- (set-session state-map))
- )
-```
-
-### Using a metosin/reitit wrapper
-#### Explanation:
-The controller would be wraped in a thin reitit wrapper with initial approach.
-#### Why:
-1. Elevates the use of reitit and its already implemented functionality and safeguards.
-2. Availability to perform validation on map to gurantee the correct definition of each stage.
-##### Exapmle:
-###### controller result map
-
-``` clojure
-{:route ["/hi" :get],
- :template #function[poc.core/temp],
- :response
- {:headers ({"Content-Type" "txt/html"} {"charset" "UTF-8"}), :body nil},
- :middlewares [],
- :actions {:action :select, :params {}},
- :deps {},
- :session nil}
-```
-###### result map schema
-
-``` clojure
-;; internal schema of an instance of a state-map
-(def instance-map
- [:map
- [:route [:vector]]
- [:respones [:map [:headers [:list]
- :body [:or [:string] [:vector]]]]]
- [:template [:or [:string] [:vector]]]
- [:actions [:vector [:and [:enum :select :delete :update :insert] [:map]]]]
- [:deps [:map]]
- [:middlewares [:list]]
- [:session [:map]]])
-;;
-```
-###### controller build and routes
-
-``` clojure
-(defn build-controller
- [ctrl]
- )
-
-(defn generate-routes
- [ctrl]
- (ring/ring-handler
- (ring/router
- ["/api"
- (:route ctrl)]
- {:data {:coercion reitit.coercion.spec/coercion
- :middleware [rrc/coerce-exceptions-middleware
- rrc/coerce-request-middleware
- rrc/coerce-response-middleware]}})))
-```
diff --git a/doc/getting-started.md b/doc/getting-started.md
index 7601db94..c0831057 100755
--- a/doc/getting-started.md
+++ b/doc/getting-started.md
@@ -1,3 +1,5 @@
+
+
# How to make a Todo app using Xiana
## Requirements
@@ -27,13 +29,15 @@ lein shadow npm-deps
### 3. Run dockerized database
-You need to have docker and docker-compose installed on your machine for the following command to work. Run the following from the root directory of your project.
+You need to have docker and docker-compose installed on your machine for the following command to work. Run the
+following from the root directory of your project.
```bash
docker-compose up -d
```
-This should spin up a PostgreSQL database on port 5433. Name of the DB is `todo_app` You can verify that the database is running by connecting to it. Value of `username` and `password` is *`postgres`.*
+This should spin up a PostgreSQL database on port 5433. Name of the DB is `todo_app` You can verify that the database is
+running by connecting to it. Value of `username` and `password` is *`postgres`.*
```bash
docker exec -it todo-app_db_1 psql -U postgres -d todo_app
@@ -43,7 +47,8 @@ which should open a PostgreSQL shell if successful.
### 4. Populate database with data
-On application start, the framework will look for database migrations located in the configured location. By default, this location is set to *resources/migrations* directory*.*
+On application start, the framework will look for database migrations located in the configured location. By default,
+this location is set to *resources/migrations* directory*.*
It is possible to create migrations by running from the project directory
@@ -51,7 +56,8 @@ It is possible to create migrations by running from the project directory
lein migrate create todos
```
-This will create two migration files inside the *resources/migrations* directory. Both file names will be prefixed by the timestamp value of the file creation.
+This will create two migration files inside the *resources/migrations* directory. Both file names will be prefixed by
+the timestamp value of the file creation.
Put the following SQL code inside the *todos-up.sql* file
@@ -65,7 +71,9 @@ CREATE TABLE todos
done boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now()
);
+
--;;
+
INSERT INTO todos (label) values ('example label from DB');
```
@@ -80,18 +88,19 @@ DROP TABLE todos;
1. Run Clojure REPL and load contents of file *dev/user.clj* into it.
2. Execute function *start-dev-system*
3. Your **Todo App** should now be running. You can verify it by visiting URL
-*[http://localhost:3000/re-frame](http://localhost:3000/re-frame)* in your browser.
+ *[http://localhost:3000/re-frame](http://localhost:3000/re-frame)* in your browser.
### 6. Add endpoint to backend API
-Routes definition can be found in the *src/backend/todo_app/core.clj* file. Replace the routes definition with following code.
+Routes definition can be found in the *src/backend/todo_app/core.clj* file. Replace the routes definition with following
+code.
```clojure
(def routes
[["/api/todos" {:action #'fetch}]])
```
-define a function to be called when the endpoint is hit.
+define a function to be called when the endpoint is hit.
```clojure
(defn fetch
@@ -99,7 +108,8 @@ define a function to be called when the endpoint is hit.
state)
```
-Now you need to reload the changed files to REPL and restart the application by executing *start-dev-system* function once again.
+Now you need to reload the changed files to REPL and restart the application by executing *start-dev-system* function
+once again.
Now opening [http://localhost:3000/api/todos](http://localhost:3000/api/todos) in your browser returns a blank page.
### 7. Hello World!
@@ -110,14 +120,14 @@ Change the implementation of *fetch* function to display "Hello World!" every ti
(defn fetch
[state]
(assoc state :response {:status 200
- :body "Hello World!"}))
+ :body "Hello World!"}))
```
Reload the modified function and restart the application.
### 8. Return contents of the todos table
-Change the implementation of function *fetch* once again and create a new *view* function.
+Change the implementation of function *fetch* once again and create a new *view* function.
Function:
@@ -128,15 +138,15 @@ Function:
(defn view
[{{db-data :db-data} :response-data :as state}]
(assoc state :response {:status 200
- :body (mapv :todos/label db-data)}))
+ :body (mapv :todos/label db-data)}))
(defn fetch
[state]
(assoc state
- :view view
- :query {:select [:*] :from [:todos]}))
+ :view view
+ :query {:select [:*] :from [:todos]}))
```
-Again, reload the modified function and restart the application.
+Again, reload the modified function and restart the application.
After running following curl command from your shell, live data from your database should appear on your screen.
@@ -146,7 +156,7 @@ curl http://localhost:3000/api/todos
## Create a page in the app to display data returned by the endpoint
-### 1. Add frontend dependencies
+### 1. Add frontend dependencies
Add following dependencies into *project.clj* file.
@@ -154,18 +164,19 @@ Add following dependencies into *project.clj* file.
(defproject
...
:dependencies [
- ...
- [cljs-ajax "0.8.4"]
- [day8.re-frame/http-fx "0.2.3"]
- ...
- ]
+ ...
+ [cljs-ajax "0.8.4"]
+ [day8.re-frame/http-fx "0.2.3"]
+ ...
+ ]
...
)
```
### 2. Set initial value of re-frame database
-File *src/frontend/todo_app/db.cljs* contains initial value of re-frame databse. Inside of this file replace value of *default-db* to following:
+File *src/frontend/todo_app/db.cljs* contains initial value of re-frame databse. Inside of this file replace value of *
+default-db* to the following:
```clojure
(def default-db
@@ -179,39 +190,39 @@ Replace contents of *src/frontend/todo_app/events.cljs* file with the following
```clojure
(ns todo-app.events
(:require
- [re-frame.core :as re-frame]
- [ajax.core :as ajax]
- [day8.re-frame.http-fx]
- [todo-app.db :as db]
- ))
+ [re-frame.core :as re-frame]
+ [ajax.core :as ajax]
+ [day8.re-frame.http-fx]
+ [todo-app.db :as db]
+ ))
(defn url [tail] (str "http://localhost:3000" tail))
(re-frame/reg-event-db
- ::initialize-db
- (fn [_ _]
- db/default-db))
+ ::initialize-db
+ (fn [_ _]
+ db/default-db))
(re-frame/reg-event-db
- ::add-todos->db
- (fn [db [_ response]]
- (assoc db :todos response)))
+ ::add-todos->db
+ (fn [db [_ response]]
+ (assoc db :todos response)))
(re-frame/reg-event-db
- ::failure
- (fn [db _]
- (js/console.error "Something is wrong!")
- db))
+ ::failure
+ (fn [db _]
+ (js/console.error "Something is wrong!")
+ db))
(re-frame/reg-event-fx
- ::fetch-todos!
- (fn [_ [_]]
- (js/console.info "Fetching todos!")
- {:http-xhrio {:uri (url "/api/todos")
- :response-format (ajax/json-response-format {:keywords? true})
- :format (ajax/json-request-format)
- :on-success [::add-todos->db]
- :on-failure [::failure]}}))
+ ::fetch-todos!
+ (fn [_ [_]]
+ (js/console.info "Fetching todos!")
+ {:http-xhrio {:uri (url "/api/todos")
+ :response-format (ajax/json-response-format {:keywords? true})
+ :format (ajax/json-request-format)
+ :on-success [::add-todos->db]
+ :on-failure [::failure]}}))
```
### 4. Define re-frame subscription
@@ -221,7 +232,7 @@ Replace contents of *src/frontend/todo_app/subs.cljs* file with the following co
```clojure
(ns todo-app.subs
(:require
- [re-frame.core :as re-frame]))
+ [re-frame.core :as re-frame]))
(re-frame/reg-sub
::todos
@@ -236,16 +247,16 @@ Replace contents of *src/frontend/todo_app/views.cljs* file with the following c
```clojure
(ns todo-app.views
(:require
- [re-frame.core :as re-frame]
- [todo-app.events :as events]
- [todo-app.subs :as subs]))
+ [re-frame.core :as re-frame]
+ [todo-app.events :as events]
+ [todo-app.subs :as subs]))
(defn main-panel []
(re-frame/dispatch [::events/fetch-todos!])
- (let [todos (re-frame/subscribe [::subs/todos])]
- [:div
- (map #(identity [:ul %])
- @todos)]))
+ (let [todos (re-frame/subscribe [::subs/todos])]
+ [:div
+ (map #(identity [:ul %])
+ @todos)]))
```
### 6. Define frontend routes
@@ -262,9 +273,11 @@ Update *routes* definition in file *src/backend/todo_app/core.clj* to following:
### 7. At this point the app should be running
-Again, reload the modified code and restart the application. After opening [http://localhost:3000/todos](http://localhost:3000/todos) in your browser, it returns a page containing the data from our database that looks like this:
+Again, reload the modified code and restart the application. After
+opening [http://localhost:3000/todos](http://localhost:3000/todos) in your browser, it returns a page containing the
+data from our database that looks like this:
-![success](../resources/images/success.png)
+![success](./resources/images/success.png)
## Create another endpoint to add a new entry to the DB
diff --git a/doc/How-To.md b/doc/how-to.md
similarity index 69%
rename from doc/How-To.md
rename to doc/how-to.md
index 4bd37526..a90cc5b1 100644
--- a/doc/How-To.md
+++ b/doc/how-to.md
@@ -1,305 +1,175 @@
+
+
# How to
-- [Login implementation](#login-implementation)
-- [Logout implementation](#logout-implementation)
-- [Session management](#session-management)
+- [Defining new interceptors](#defining-new-interceptors)
+ - [Interceptor example](#interceptor-example)
+- [Providing default interceptors](#providing-default-interceptors)
+- [Interceptor overriding](#interceptor-overriding)
+- [Role based access and data ownership control](#role-based-access-and-data-ownership-control)
- [Access and data ownership control](#access-and-data-ownership-control)
- [Role set definition](#role-set-definition)
- [Provide resource/action at routing](#provide-resourceaction-at-routing)
- [Application start-up](#application-start-up)
- [Access control](#access-control)
- [Data ownership](#data-ownership)
- - [All together](#all-together)
+- [Login implementation](#login-implementation)
+- [Logout implementation](#logout-implementation)
-## Login implementation
+## Defining new interceptors
-Xiana framework does not have any login or logout functions, as every application has its own user management logic.
-Though Xiana offers all the tools to easily implement them. One of the default interceptors is the session interceptor.
-If included, it can validate a request only if the session already exists in session storage. To log in a user, simply
-add its session data to the storage. (TODO: where? What is the exact key to modify?). All sessions should have a unique
-UUID as session-id. The active session lives under `(-> state :session-data)`. On every request, before reaching the
-action defined by the route, the interceptor checks `[:headers :session-id]` among other things. Which is the id of the
-current session. The session is then loaded in session storage. If the id is not found, the execution flow is
-interrupted with the response:
+The interceptor is a map, can have three functions like:
-```clojure
-{:status 401
- :body "Invalid or missing session"}
-```
+`:enter` Runs while we are going down from the request to it's action, in the order of executors
-To implement login, you need to [use the session interceptor](tutorials.md#interceptor-overriding) in
+`:leave` Runs while we're going up from the action to the response.
-```clojure
-(let [;; Create a unique ID
- session-id (UUID/randomUUID)]
- ;; Store a new session in session storage
- (add! session-storage session-id {:session-id session-id})
- ;; Make sure session-id is part of the response
- (assoc-in state [:response :headers :session-id] (str session-id)))
-```
+`:error` Executed when any error thrown while executing the two other functions
-or use the `guest-session` interceptor, which creates a guest session for unknown, or missing sessions.
+and a `:name` can be defined. All keys are optional, and if it missing it's replaced by `identity`.
-For role-based access control, you need to store the actual user in your session data. First, you'll have to query it
-from the database. It is best placed in models/user namespace. Here's an example:
+The provided functions are should have one parameter, the application state, and should return the modified state.
-```clojure
-(defn fetch-query
- [state]
- (let [login (-> state :request :body-params :login)]
- (-> (select :*)
- (from :users)
- (where [:and
- :is_active
- [:or
- [:= :email login]
- [:= :username login]]]))))
-```
-
-To execute it, place `db-access` interceptor in the interceptors list. It injects the query result into the state. If
-you already have this injected, you can modify your create session function like this:
+### Interceptor example
```clojure
-(let [;; Get user from database result
- user (-> state :response-data :db-data first)
- ;; Create session
- session-id (UUID/randomUUID)]
- ;; Store the new session in session storage. Notice the addition of user.
- (add! session-storage session-id (assoc user :session-id session-id))
- ;; Make sure session-id is part of the response
- (assoc-in state [:response :headers :session-id] (str session-id)))
-```
-
-Be sure to remove user's password and any other sensitive information before storing it:
-```clojure
-(let [;; Get user from database result
- user (-> state
- :response-data
- :db-data
- first
- ;; Remove password for session storage
- (dissoc :users/password))
- ;; Create session id
- session-id (UUID/randomUUID)]
- ;; Store the new session in session storage
- (add! session-storage session-id (assoc user :session-id session-id))
- ;; Make sure session-id is part of the response
- (assoc-in state [:response :headers :session-id] (str session-id)))
+{:name :sample-interceptor
+ :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"}))}
```
-Next, we check if the credentials are correct, so we use an `if` statement.
+## Providing default interceptors
-```clojure
-(if (valid-credentials?)
- (let [;; Get user from database result
- user (-> state
- :response-data
- :db-data
- first
- ;; Remove password for session storage
- (dissoc :users/password))
- ;; Create session ID
- session-id (UUID/randomUUID)]
- ;; Store the new session in session storage
- (add! session-storage session-id (assoc user :session-id session-id))
- ;; Make sure session-id is part of the response
- (assoc-in state [:response :headers :session-id] (str session-id)))
- (throw (ex-info "Missing session data"
- {:xiana/response
- {:body "Login failed"
- :status 401}})))
-```
-
-Xiana provides `xiana.hash` to check user credentials:
+The router and controller interceptors definition is part of the application startup. The system's dependency map should
+contain two sequence of interceptors like
```clojure
-(defn- valid-credentials?
- "It checks that the password provided by the user matches the encrypted password from the database."
- [state]
- (let [user-provided-pass (-> state :request :body-params :password)
- db-stored-pass (-> state :response-data :db-data first :users/password)]
- (and user-provided-pass
- db-stored-pass
- (hash/check state user-provided-pass db-stored-pass))))
+(def app-cfg
+ {:routes routes
+ :router-interceptors [...]
+ :controller-interceptors [...]
+ :web-socket-interceptors [...]})
```
-The login logic is done, but where to place it?
+## Interceptor overriding
-Do you remember the [side effect interceptor](interceptors.md#side-effect)? It's running after we have the query result
-from the database, and before the final response is rendered with the [view interceptor](interceptors.md#view). The
-place for the function defined above is in the interceptor chain. How does it go there? Let's see
-an [action](conventions.md#action)
+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:
```clojure
-(defn action
- [state]
- (assoc state :side-effect side-effects/login))
+... {:action #(do something)
+ :interceptors [...]}
```
-This is the place for injecting the database query, too:
+will override all controller interceptors
```clojure
-(defn action
- [state]
- (assoc state :side-effect side-effects/login
- :query model/fetch-query))
+... {:action #(do something)
+ :interceptors {:around [...]}}
```
-But some tiny thing is still missing. The definition of the response in the all-ok case. A happy path response.
+will extend the defaults around
```clojure
-(defn login-success
- [state]
- (let [id (-> state :response-data :db-data first :users/id)]
- (-> state
- (assoc-in [:response :body]
- {:view-type "login"
- :data {:login "succeed"
- :user-id id}})
- (assoc-in [:response :status] 200))))
+... {:action #(do something)
+ :interceptors {:inside [...]}}
```
-And finally the [view](tutorials.md#view) is injected in the action function:
+will extend the defaults inside
```clojure
-(defn action
- [state]
- (assoc state :side-effect side-effects/login
- :view view/login-success
- :query model/fetch-query))
+... {:action #(do something)
+ :interceptors {:inside [...]
+ :around [...]}}
```
-## Logout implementation
-
-To do a logout is much easier than a login implementation. The `session-interceptor` does half of the work, and if you
-have a running session, then it will not complain. The only thing you should do is to remove the actual session from
-the `state`
-and from session storage. Something like this:
+will extend the defaults inside and around
```clojure
-(defn logout
- [state]
- (let [session-store (get-in state [:deps :session-backend])
- session-id (get-in state [:session-data :session-id])]
- (session/delete! session-store session-id)
- (dissoc state :session-data)))
+... {:action #(do something)
+ :interceptors {:except [...]}}
```
-Add the `ok` response
+will skip the excepted interceptors from defaults
-```clojure
-(defn logout-view
- [state]
- (-> state
- (assoc-in [:response :body]
- {:view-type "logout"
- :data {:logout "succeed"}})
- (assoc-in [:response :status] 200)))
-```
+## Role based access and data ownership control
-and use it:
+To get the benefits of [tiny RBAC](https://github.com/Flexiana/tiny-rbac) library you need to provide the resource and
+the action for your endpoint in [router](#routes) definition:
```clojure
-(defn logout
- [state]
- (let [session-store (get-in state [:deps :session-backend])
- session-id (get-in state [:session-data :session-id])]
- (session/delete! session-store session-id)
- (-> state
- (dissoc :session-data)
- (assoc :view views/logout-view))))
+[["/api"
+ ["/image" {:delete {:action delete-action
+ :permission :image/delete}}]]]
```
-## Session management
-
-Session management is done via two components
-
-- session backend, which can be
- - in-memory
- - persistent
-- session interceptors
-
-### In memory session backend
-
-Basically it's an atom backed session protocol implementation, allows you to `fetch` `add!` `delete!` `dump`
-and `erase!` session data, or the whole session storage. It doesn't require any additional configuration, and this is
-the default set up for handling session storage. All stored session data is wiped out on system restart.
-
-### Persistent session backend
-
-Instead of atom, it uses a postgresql table to store session data. Has the same protocol as in-memory. Configuration is
-necessary to use it.
-
-- it's necessary to have a table in postgres:
+and add your role-set into your app's [dependencies](#dependencies-and-configuration):
-```postgres-sql
-CREATE TABLE sessions (
- session_data json not null,
- session_id uuid primary key,
- modified_at timestamp DEFAULT CURRENT_TIMESTAMP
-);
+```clojure
+(defn ->system
+ [app-cfg]
+ (-> (config/config app-cfg)
+ xiana.rbac/init
+ ws/start))
```
-- you need to define the session's configuration in you `config.edn` files:
+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:
```clojure
- :xiana/session-backend {:storage :database
- :session-table-name :sessions}
+{:status 403
+ :body "Forbidden"}
```
-- in case of
- - missing `:storage` key, `in-memory` session backend will be used
- - missing `:session-table-name` key, `:sessions` table will be used
+If a permission is found, then it goes into `(-> state :request-data :user-permissions)` as a parameter for data
+ownership processing.
-- the database connection can be configured in three ways:
+On `:leave`, executes the restriction function found in `(-> state :request-data :restriction-fn)`. The `restriction-fn`
+should look like this:
- In resolution order
- - via additional configuration
- ```clojure
- :xiana/session-backend {:storage :database
- :session-table-name :sessions
- :port 5433
- :dbname "app-db"
- :host "localhost"
- :dbtype "postgresql"
- :user "db-user"
- :password "db-password"}
- ```
- - using the same datasource as the application use:
-
- Just init the backend after the database connection
- ```clojure
- (defn ->system
- [app-cfg]
- (-> (config/config app-cfg)
- routes/reset
- db-core/connect
- db-core/migrate!
- session/init-backend
- ws/start))
- ```
- - Creating new datasource
-
- If no datasource is provided on initialization, the `init-backend` function merges the database config with the
- session backend configuration, and creates a new datasource from the result.
-
-### Session interceptors
+```clojure
+(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])))))
+```
-[See interceptors](interceptors.md#session)
+The rbac interceptor must be placed between the [action](#action) and the [db-access](#database-access) interceptors in
+the interceptor chain.
## Access and data ownership control
-[RBAC](tutorials.md#role-based-access-and-data-ownership-control) is a handy way to restrict user actions on different
+[RBAC](./tutorials.md#role-based-access-and-data-ownership-control) is a handy way to restrict user actions on different
resources. It's a role-based access control and helps you to implement data ownership control. The `rbac/interceptor`
-should be placed [inside](tutorials.md#interceptor-overriding) [db-access](interceptors.md#db-access).
+should be placed [inside](./tutorials.md#interceptor-overriding) [db-access](./interceptors.md#db-access).
### Role set definition
For [tiny-RBAC](https://github.com/Flexiana/tiny-rbac) you should provide
a [role-set](https://github.com/Flexiana/tiny-rbac#builder). It's a map which defines the application resources, the
actions on it, the roles with the different granted actions, and restrictions for data ownership control. This map must
-be placed in [deps](conventions.md#dependencies).
+be placed in [deps](./conventions.md#dependencies).
Here's an example role-set for an image service:
@@ -467,7 +337,208 @@ And finally, the only missing piece of code: the model, and the action
(defn delete-image
[state]
+ (-> state
+ (assoc :query (delete-query state))
+ (assoc-in [:request-data :restriction-fn] restriction-fn)))
+```
+
+## Login implementation
+
+Xiana framework does not have any login or logout functions, as every application has its own user management logic.
+Though Xiana offers all the tools to easily implement them. One of the default interceptors is the session interceptor.
+If included, it can validate a request only if the session already exists in session storage. To log in a user, simply
+add its session data to the storage. (TODO: where? What is the exact key to modify?). All sessions should have a unique
+UUID as session-id. The active session lives under `(-> state :session-data)`. On every request, before reaching the
+action defined by the route, the interceptor checks `[:headers :session-id]` among other things. Which is the id of the
+current session. The session is then loaded in session storage. If the id is not found, the execution flow is
+interrupted with the response:
+
+```clojure
+{:status 401
+ :body "Invalid or missing session"}
+```
+
+To implement login, you need to [use the session interceptor](tutorials.md#interceptor-overriding) in
+
+```clojure
+(let [;; Create a unique ID
+ session-id (UUID/randomUUID)]
+ ;; Store a new session in session storage
+ (add! session-storage session-id {:session-id session-id})
+ ;; Make sure session-id is part of the response
+ (assoc-in state [:response :headers :session-id] (str session-id)))
+```
+
+or use the `guest-session` interceptor, which creates a guest session for unknown, or missing sessions.
+
+For role-based access control, you need to store the actual user in your session data. First, you'll have to query it
+from the database. It is best placed in models/user namespace. Here's an example:
+
+```clojure
+(defn fetch-query
+ [state]
+ (let [login (-> state :request :body-params :login)]
+ (-> (select :*)
+ (from :users)
+ (where [:and
+ :is_active
+ [:or
+ [:= :email login]
+ [:= :username login]]]))))
+```
+
+To execute it, place `db-access` interceptor in the interceptors list. It injects the query result into the state. If
+you already have this injected, you can modify your create session function like this:
+
+```clojure
+(let [;; Get user from database result
+ user (-> state :response-data :db-data first)
+ ;; Create session
+ session-id (UUID/randomUUID)]
+ ;; Store the new session in session storage. Notice the addition of user.
+ (add! session-storage session-id (assoc user :session-id session-id))
+ ;; Make sure session-id is part of the response
+ (assoc-in state [:response :headers :session-id] (str session-id)))
+```
+
+Be sure to remove user's password and any other sensitive information before storing it:
+
+```clojure
+(let [;; Get user from database result
+ user (-> state
+ :response-data
+ :db-data
+ first
+ ;; Remove password for session storage
+ (dissoc :users/password))
+ ;; Create session id
+ session-id (UUID/randomUUID)]
+ ;; Store the new session in session storage
+ (add! session-storage session-id (assoc user :session-id session-id))
+ ;; Make sure session-id is part of the response
+ (assoc-in state [:response :headers :session-id] (str session-id)))
+```
+
+Next, we check if the credentials are correct, so we use an `if` statement.
+
+```clojure
+(if (valid-credentials?)
+ (let [;; Get user from database result
+ user (-> state
+ :response-data
+ :db-data
+ first
+ ;; Remove password for session storage
+ (dissoc :users/password))
+ ;; Create session ID
+ session-id (UUID/randomUUID)]
+ ;; Store the new session in session storage
+ (add! session-storage session-id (assoc user :session-id session-id))
+ ;; Make sure session-id is part of the response
+ (assoc-in state [:response :headers :session-id] (str session-id)))
+ (throw (ex-info "Missing session data"
+ {:xiana/response
+ {:body "Login failed"
+ :status 401}})))
+```
+
+Xiana provides `xiana.hash` to check user credentials:
+
+```clojure
+(defn- valid-credentials?
+ "It checks that the password provided by the user matches the encrypted password from the database."
+ [state]
+ (let [user-provided-pass (-> state :request :body-params :password)
+ db-stored-pass (-> state :response-data :db-data first :users/password)]
+ (and user-provided-pass
+ db-stored-pass
+ (hash/check state user-provided-pass db-stored-pass))))
+```
+
+The login logic is done, but where to place it?
+
+Do you remember the [side effect interceptor](./interceptors.md#side-effect)? It's running after we have the query
+result
+from the database, and before the final response is rendered with the [view interceptor](./interceptors.md#view). The
+place for the function defined above is in the interceptor chain. How does it go there? Let's see
+an [action](./conventions.md#action)
+
+```clojure
+(defn action
+ [state]
+ (assoc state :side-effect side-effects/login))
+```
+
+This is the place for injecting the database query, too:
+
+```clojure
+(defn action
+ [state]
+ (assoc state :side-effect side-effects/login
+ :query model/fetch-query))
+```
+
+But some tiny thing is still missing. The definition of the response in the all-ok case. A happy path response.
+
+```clojure
+(defn login-success
+ [state]
+ (let [id (-> state :response-data :db-data first :users/id)]
(-> state
- (assoc :query (delete-query state))
- (assoc-in [:request-data :restriction-fn] restriction-fn)))
+ (assoc-in [:response :body]
+ {:view-type "login"
+ :data {:login "succeed"
+ :user-id id}})
+ (assoc-in [:response :status] 200))))
+```
+
+And finally the [view](tutorials.md#view) is injected in the action function:
+
+```clojure
+(defn action
+ [state]
+ (assoc state :side-effect side-effects/login
+ :view view/login-success
+ :query model/fetch-query))
+```
+
+## Logout implementation
+
+To do a logout is much easier than a login implementation. The `session-interceptor` does half of the work, and if you
+have a running session, then it will not complain. The only thing you should do is to remove the actual session from
+the `state`
+and from session storage. Something like this:
+
+```clojure
+(defn logout
+ [state]
+ (let [session-store (get-in state [:deps :session-backend])
+ session-id (get-in state [:session-data :session-id])]
+ (session/delete! session-store session-id)
+ (dissoc state :session-data)))
+```
+
+Add the `ok` response
+
+```clojure
+(defn logout-view
+ [state]
+ (-> state
+ (assoc-in [:response :body]
+ {:view-type "logout"
+ :data {:logout "succeed"}})
+ (assoc-in [:response :status] 200)))
+```
+
+and use it:
+
+```clojure
+(defn logout
+ [state]
+ (let [session-store (get-in state [:deps :session-backend])
+ session-id (get-in state [:session-data :session-id])]
+ (session/delete! session-store session-id)
+ (-> state
+ (dissoc :session-data)
+ (assoc :view views/logout-view))))
```
diff --git a/doc/interceptors.md b/doc/interceptors.md
index 38e222a9..48015d54 100644
--- a/doc/interceptors.md
+++ b/doc/interceptors.md
@@ -1,3 +1,5 @@
+
+
# Interceptors implemented in Xiana
- [log](#log)
diff --git a/doc/modules.md b/doc/modules.md
index 864f4b2b..9f8c422a 100644
--- a/doc/modules.md
+++ b/doc/modules.md
@@ -1,59 +1,54 @@
+
+
# Modules
-- [Backend](#backend)
- - [Auth](#auth)
- - [Config](#config)
- - [Database](#database)
- - [Database/main](#databasemain)
- - [Database/seed](#databaseseed)
- - [Database/core](#databasecore)
- - [Migrations](#migrations)
- - [Interceptor](#interceptor)
- - [Interceptor/core](#interceptorcore)
- - [Interceptor/muuntaja](#interceptormuuntaja)
- - [Interceptor/wrap](#interceptorwrap)
- - [Mail](#mail)
- - [RBAC](#rbac-1)
- - [Route](#route)
- - [Scheduler](#scheduler)
- - [SSE](#sse)
- - [Session](#session)
- - [State](#state-1)
- - [Webserver](#webserver)
-
-## Backend
-
-### Auth
+- [Auth](#auth)
+- [Config](#config)
+- [Database](#database)
+ - [Database/main](#databasemain)
+ - [Database/core](#databasecore)
+- [Migrations](#migrations)
+- [Interceptor](#interceptor)
+ - [Interceptor/core](#interceptorcore)
+ - [Interceptor/muuntaja](#interceptormuuntaja)
+ - [Interceptor/wrap](#interceptorwrap)
+- [Mail](#mail)
+- [RBAC](#rbac-1)
+- [Route](#route)
+- [Scheduler](#scheduler)
+- [SSE](#sse)
+- [Session](#session)
+- [State](#state)
+- [Webserver](#webserver)
+
+
+
+## Auth
Auth.core is a package to deal with password encoding, and validation.
-### Config
+## Config
config/core provides functions to deal with environment variables and config.edn files.
-### Database
+## Database
Database handling functions.
-#### Database/main
+## Database/main
Migratus wrapper to get rid of lein migratus plugin. As well as support profile dependent configuration of migratus.
-#### Database/seed
-
-Migratus wrapper to support profile based separation of data seeding
-
-
-#### Database/core
+## Database/core
Start function, which gets the data source based on given configuration. Query executor functions and the db-access
interceptor.
-### Interceptor
+## Interceptor
Some default interceptors and helpers.
-#### Interceptor/core
+## Interceptor/core
Collection of interceptors, like:
@@ -66,49 +61,50 @@ Collection of interceptors, like:
- session-user-role
- muuntaja
-#### Interceptor/muuntaja
+## Interceptor/muuntaja
Default settings, and format instance of content negotiation.
-#### Interceptor/queue
+## Interceptor/queue
The queue is the interceptor executor. It contains all functions necessary to the handler to deal with interceptors and
interceptor overrides.
-#### Interceptor/wrap
+## Interceptor/wrap
With interceptor wrapper you can use any kind of interceptors and middlewares with xiana provided flow.
-### Mail
+## Mail
Helps you to send an email message based on configuration.
-### RBAC
+## RBAC
Wrapper package for tiny-RBAC lib. It initializes role-set and gathers permissions from the actual state. It contains an
interceptor too, to deal with permissions and restrictions.
-### Route
+## Route
Contains all functions to deal with route dependent functionality. Uses reitit matcher and router. Collects request-data
for processing it via controller interceptors and action.
-### Scheduler
+## Scheduler
Repeated function execution. The function gets `:deps` as parameter.
-### SSE
+## SSE
Server-sent events implementation based on HTTP-kit's Channel protocol.
-### Session
+## Session
-Contains Session protocol definition, an in-memory, and persistent (postgres) session backend, and the session interceptor.
+Contains Session protocol definition, an in-memory, and persistent (postgres) session backend, and the session
+interceptor.
-### State
+## State
In handler-fn it creates the initial state for request execution.
-### Webserver
+## Webserver
Starts a jetty server with the default handler, provides the dependencies to the handler function.
diff --git a/resources/images/.gitkeep b/doc/resources/images/.gitkeep
similarity index 100%
rename from resources/images/.gitkeep
rename to doc/resources/images/.gitkeep
diff --git a/resources/images/Xiana.png b/doc/resources/images/Xiana.png
similarity index 100%
rename from resources/images/Xiana.png
rename to doc/resources/images/Xiana.png
diff --git a/resources/images/around-and-inside.png b/doc/resources/images/around-and-inside.png
similarity index 100%
rename from resources/images/around-and-inside.png
rename to doc/resources/images/around-and-inside.png
diff --git a/resources/images/around.png b/doc/resources/images/around.png
similarity index 100%
rename from resources/images/around.png
rename to doc/resources/images/around.png
diff --git a/resources/images/flow.png b/doc/resources/images/flow.png
similarity index 100%
rename from resources/images/flow.png
rename to doc/resources/images/flow.png
diff --git a/resources/images/inside.png b/doc/resources/images/inside.png
similarity index 100%
rename from resources/images/inside.png
rename to doc/resources/images/inside.png
diff --git a/resources/images/override.png b/doc/resources/images/override.png
similarity index 100%
rename from resources/images/override.png
rename to doc/resources/images/override.png
diff --git a/resources/images/success.png b/doc/resources/images/success.png
similarity index 100%
rename from resources/images/success.png
rename to doc/resources/images/success.png
diff --git a/doc/tutorials.md b/doc/tutorials.md
index 99881d86..3ea7c957 100644
--- a/doc/tutorials.md
+++ b/doc/tutorials.md
@@ -1,64 +1,90 @@
+
+
# Tutorials
-- [Dependencies and configuration](#dependencies-and-configuration)
-- [Database migration](#database-migration)
-- [Database seed with data](#database-seed-with-data)
-- [Interceptors typical use-case, and ordering](#interceptors-typical-use-case-and-ordering)
-- [Defining new interceptors](#defining-new-interceptors)
- - [Interceptor example](#interceptor-example)
+- [Application startup](#application-startup)
+- [Configuration](#configuration)
+- [Dependencies](#dependencies)
- [Router and controller interceptors](#router-and-controller-interceptors)
-- [Providing default interceptors](#providing-default-interceptors)
-- [Interceptor overriding](#interceptor-overriding)
- [Routes](#routes)
- [Action](#action)
+- [Database migration](#database-migration)
- [Database-access](#database-access)
- [View](#view)
- [Side-effects](#side-effects)
- [Session management](#session-management)
-- [Role based access and data ownership control](#role-based-access-and-data-ownership-control)
+ - [In memory backend](#in-memory-session-backend)
+ - [Persistent backend](#persistent-session-backend)
+ - [Session interceptors](#session-interceptors)
- [WebSockets](#websockets)
- [WebSockets routing](#websockets-routing)
- [Route matching](#route-matching)
- [Server-Sent Events (SSE)](#server-sent-events-sse)
- [Scheduler](#scheduler)
-## Dependencies and configuration
+## Application startup
+
+Starting up an application takes several well-defined steps:
+
+- reading the configuration
+- setting up dependencies
+- spinning up a web-server
+
+## Configuration
+
+Apps built with Xiana are configurable in several ways. It uses [yogthos/config](https://github.com/yogthos/config) to
+resolve basic configuration from `config.edn`, `.lein-env`, `.boot-env` files, environment variables and system
+properties. Additionally
+
+- Xiana looks for an `.edn` file pointed with `:xiana-config` variable for overrides
+- You can define a key to read from any other (already defined) value, and/or pass a default value.
+
+In practice this means you can define a config value like this:
+
+```clojure
+:xiana/test {:test-value-1 "$property"
+ :test-value-2 "$foo | baz"}
+```
+
+and this will be resolved as
+
+```clojure
+:xiana/test {:test-value-1 "the value of 'property' key, or nil"
+ :test-value-2 "the value of `foo` key or \"baz\""}
+```
+
+The value of property key can come from the config files, environment variables, or from system properties.
-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.
+## Dependencies
+
+Database connection, external APIs or session storage, the route definition, setting up scheduled executors or
+doing migrations are our dependencies. These dependencies 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:
```clojure
(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))})
+ (-> (config/config app-cfg) ;Read config
+ routes/reset ;set up routing
+ db/start ;set up database connection
+ db/migrate! ;running migrations
+ session/init-backend ;initialize session storage
+ (scheduler/start actions/ping 10000) ;starting a scheduler
+ ws/start)) ;spinning up the webserver
+
+(def app-cfg
+ {:routes routes ;injecting route definition
+ :router-interceptors [] ;definition of router interceptors
+ :controller-interceptors [(xiana-interceptors/muuntaja) ;definition of controller interceptors
+ cookies/interceptor
+ xiana-interceptors/params
+ session/interceptor
+ xiana-interceptors/view
+ xiana-interceptors/side-effect
+ db/db-access]})
(defn -main
[& _args]
@@ -179,17 +205,17 @@ The provided function should have one parameter, the application state, and shou
(-> 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"}))}
+ :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"}))}
```
#### Router and controller interceptors
@@ -207,135 +233,167 @@ The handler function executes interceptors in this order
6. controller interceptors :leave functions in reversed order
In router interceptors, you are able to interfere with the routing mechanism. Controller interceptors can be interfered
-with via route definition.
-
-## Providing default interceptors
+with via route definition. There is an option to define interceptors around creating WebSocket channels, these
+interceptors are executed around the `:ws-action` instead of `:action`.
-The router and controller interceptors definition is part of the application startup. The system's dependency map should
-contain two sequence of interceptors like
+## Routes
-```clojure
-{:router-interceptors [...]
- :controller-interceptors [...]}
-```
+Route definition is done via [reitit's routing](https://github.com/metosin/reitit) library. Route processing is done
+with `xiana.route` namespace. At route definition you can define.
-## Interceptor overriding
+- The [action](#action) that should be executed
+- [Interceptor overriding](how-to.md#interceptor-overriding)
+- The required permission for [rbac](how-to.md#role-based-access-and-data-ownership-control)
+- [WebSockets](#websockets) action definition
-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:
+If any extra parameter is provided here, it's injected into
```clojure
-... {:action #(do something)
- :interceptors [...]}
+(-> state :request-data :match)
```
-will override all controller interceptors
-
-```clojure
-... {:action #(do something)
- :interceptors {:around [...]}}
-```
+in routing step.
-will extend the defaults around
+Example route definition:
```clojure
-... {:action #(do something)
- :interceptors {:inside [...]}}
+["/api" {}
+ ["/login" {:post {:action #'user-controllers/login ;Login controller
+ :interceptors {:except [session/interceptor]}}}] ;the user doesn't have a valid session yet
+ ["/posts" {:get {:action #'posts-controllers/fetch ;controller definition for fetching posts
+ :permission :posts/read} ;set up the permission for fetching posts
+ :put {:action #'posts-controllers/add
+ :permission :posts/create}
+ :post {:action #'posts-controllers/update-post
+ :permission :posts/update}
+ :delete {:action #'posts-controllers/delete-post
+ :permission :posts/delete}}]
+ ["/notifications" {:get {:ws-action #'websockets/notifications ;websocket controller for sending notifications
+ :action #'notifications/fetch ;REST endpoint for fetching notifications
+ :permission :notifications/read}}] ;inject permission
+ ["/style" {:get {:action #'style/fetch
+ :organization :who}}]] ;this is not a usual key, the value will go to
+;(-> state :request-data :match :organization)
+;at the routing process
```
-will extend the defaults inside
+## Action
+
+The action function (controller) in a
+single [CRUD application](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete#RESTful_APIs) is for defining
+a [view](#view), a [database-query](#database-access) (model) and optionally a [side-effect](#side-effects) function
+which will be executed in the following interceptor steps.
```clojure
-... {:action #(do something)
- :interceptors {:inside [...]
- :around [...]}}
+(defn action
+ [state]
+ (assoc state :view view/success
+ :side-effect behaviour/update-sessions-and-db!
+ :query model/fetch-query))
```
-will extend the defaults inside and around
+## Database migration
-```clojure
-... {:action #(do something)
- :interceptors {:except [...]}}
-```
+Database migration is based on the following principles:
-will skip the excepted interceptors from defaults
+1. The migration process is based on a stack of immutable changes. If at some point you want to change the schema or the
+ content of the database you don't change the previous scripts but add new scripts at the top of the stack.
+2. There should be a single standard resources/migrations migration directory
+3. If a specific platform (dev, stage, test, etc) needs additional scripts, specific directories should be created and
+ in config set the appropriate migrations-dir as a vector containing the standard directory and the auxiliary
+ directory.
+4. The order in which scripts are executed depends only on the script id and not on the directory where the script is
+ located
-The execution flow will look like this
+### Configure migration
-1. router interceptors :enters in order
-2. router interceptors :leaves in reversed order
-3. routing
-4. around interceptors :enters in order
-5. controller interceptors :enters in order
-6. inside interceptors :enters in order
-7. action
-8. inside interceptors :leaves in reversed order
-9. controller interceptors :leaves in reversed order
-10. around interceptors :leaves in reversed order
+The migration process requires a config file containing:
-All interceptors in :except will be skipped.
+```clojure
+: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"}
+```
-## Routes
+The :migration-dir param is a vector of classpath relative paths containing database migrations scripts.
-Route definition is done via [reitit's routing](https://github.com/metosin/reitit) library. Route processing is done
-with `xiana.route` namespace. At route definition you can define.
+### Usage
-- The [action](#action) that should be executed
-- [Interceptor overriding](#interceptor-overriding)
-- The required permission for [rbac](#role-based-access-and-data-ownership-control)
-- [WebSocket](#websocket) action definition
+The `xiana.db.migrate` implements a cli for migrations framework.
-If any extra parameter is provided here, it's injected into
+If you add to `deps.edn` in `:aliases` section:
```clojure
-(-> state :request-data :match)
+:migrate {:main-opts ["-m" "xiana.db.migrate"]}
```
-in routing step.
+you could access this cli from clojure command.
-## Action
+To see all commands and options available run:
+
+```shell
+clojure -M:migrate --help
+```
-The action function in a single
-[CRUD application](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete#RESTful_APIs) is for defining a
-[view](#view), a [database-query](#database-access) and optionally a [side-effect](#side-effects) function which will be
-executed in the following interceptor steps.
+Examples of commands:
-```clojure
-(defn action
- [state]
- (assoc state :view view/success
- :side-effect behaviour/update-sessions-and-db!
- :query model/fetch-query))
+```shell
+# 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
```
-## Database-access
+## Database access
+
+The `xiana.db/db-access` executes queries from `:query` (for single, non-transactional database access)
+and `:db-queries` in this order against the datasource extracted from state. The result will be available
+in `(-> state :response-data :db-data)` which is always a sequence.
-The `database.core`'s interceptor extracts the datasource from the provided state parameter and the :query.
+`db-queries` is still a map, contains `:queries` and `:transaction?` keys. If `:transaction?` is set to `true`,
+all `queries` in `db-queries` will be executed in one transaction.
The query should be in [honey SQL](https://github.com/nilenso/honeysql-postgres) format, it will be sql-formatted on
execution:
```clojure
-(defn fetch-query
- [state]
- (let [login (-> state :request :body-params :login)]
- (-> (select :*)
- (from :users)
- (where [:and
- :is_active
- [:or
- [:= :email login]
- [:= :username 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
+is equal to
```clojure
-(-> state :response-data :db-data)
+{:select [:*]
+ :from [:users]
+ :where [:and
+ :is-active
+ [:or
+ [:= :email login]
+ [:= :user-name login]]]}
```
-without any transformation.
+Both examples above are leads to
+
+```postgres-sql
+["SELECT * FROM users WHERE is_active AND (email = ? OR user_name = ?)" login login]
+```
## View
@@ -345,21 +403,22 @@ A view is a function to prepare the final response and saving it into the state
(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}}})))
+ (assoc state :response {:status 200
+ :body {:view-type "login"
+ :data {:login "succeed"
+ :user-id id}}})))
```
## Side-effects
-Conventionally, side-effects interceptor is placed after [action](#action) and [database-access](#database-access), just
+Conventionally, side-effects interceptor is placed after [action](#action) and [database-access](#database-access),
+just
right before [view](#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:
+This example shows you, how can you react on a login request. This stores the user data in the actual session on
+successful login, or injects the `Unauthorized` response into the state.
```clojure
(defn update-sessions-and-db!
@@ -370,10 +429,8 @@ Adding to the previous examples:
(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)
+ user (-> state :response-data :db-data first)]
(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
@@ -383,70 +440,99 @@ Adding to the previous examples:
## Session management
-Session interceptor interchanges session data between the session-backend and the app state.
+Session management has two mayor components
-On `:enter` it loads the session by its session-id, into `(-> state :session-data)`
+- session backend
+- session interceptors
-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:
+The session backend can be in-memory or persisted using a json storage in postgres database.
-```clojure
-{:status 401
- :body "Invalid or missing session"}
-```
+### In memory session backend
-On the `:leave` branch, updates session storage with the data from `(-> state :session-data)`
+Basically it's an atom backed session protocol implementation, allows you to `fetch` `add!` `delete!` `dump`
+and `erase!` session data or the whole session storage. It doesn't require any additional configuration, and this is
+the default set up for handling session storage. All stored session data is wiped out on system restart.
-## Role based access and data ownership control
+### Persistent session backend
-To get the benefits of [tiny RBAC](https://github.com/Flexiana/tiny-rbac) library you need to provide the resource and
-the action for your endpoint in [router](#routes) definition:
+Instead of atom, it uses a postgresql table to store session data. Has the same protocol as in-memory. Configuration is
+necessary to use it.
-```clojure
-[["/api"
- ["/image" {:delete {:action delete-action
- :permission :image/delete}}]]]
+- it's necessary to have a table in postgres:
+
+```postgres-sql
+CREATE TABLE sessions (
+ session_data json not null,
+ session_id uuid primary key,
+ modified_at timestamp DEFAULT CURRENT_TIMESTAMP
+);
```
-and add your role-set into your app's [dependencies](#dependencies-and-configuration):
+- you need to define the session's configuration in you `config.edn` files:
```clojure
-(defn ->system
- [app-cfg]
- (-> (config/config)
- (merge app-cfg)
- xiana.rbac/init
- ws/start))
+ :xiana/session-backend {:storage :database
+ :session-table-name :sessions}
```
-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:
+- in case of
+ - missing `:storage` key, `in-memory` session backend will be used
+ - missing `:session-table-name` key, `:sessions` table will be used
-```clojure
-{:status 403
- :body "Forbidden"}
-```
+- the database connection can be configured in three ways:
+
+ - via additional configuration
-If a permission is found, then it goes into `(-> state :request-data :user-permissions)` as a parameter for data
-ownership processing.
+ ```clojure
+ :xiana/session-backend {:storage :database
+ :session-table-name :sessions
+ :port 5433
+ :dbname "app-db"
+ :host "localhost"
+ :dbtype "postgresql"
+ :user "db-user"
+ :password "db-password"}
+ ```
-On `:leave`, executes the restriction function found in `(-> state :request-data :restriction-fn)`. The `restriction-fn`
-should look like this:
+ - using the same datasource as the application use:
+
+ Just init the backend after the database connection
+ ```clojure
+ (defn ->system
+ [app-cfg]
+ (-> (config/config app-cfg)
+ routes/reset
+ db-core/connect
+ db-core/migrate!
+ session/init-backend
+ ws/start))
+ ```
+
+ - Creating new datasource
+
+ If no datasource is provided on initialization, the `init-backend` function merges the database config with the
+ session backend configuration, and creates a new datasource from the result.
+
+### Session interceptors
+
+The session interceptors interchanges session data between the session-backend and the app state.
+
+The `xiana.session/interceptor` throws an exception when no valid session-id can be found in the headers, cookies or as
+query parameter.
+
+The `xiana.session/guest-session-interceptor` creates a guest session if the session-id is missing, or invalid, which
+means:
```clojure
-(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])))))
+{:session-id (UUID/randomUUID)
+ :users/role :guest
+ :users/id (UUID/randomUUID)}
```
-The rbac interceptor must be placed between the [action](#action) and the [db-access](#database-access) interceptors in
-the [interceptor chain](#interceptors-typical-use-case-and-ordering).
+will be injected to the session data.
+
+Both interceptors fetching already stored session data into the state at `:enter`, and on `:leave` updates session
+storage with the data from `(-> state :session-data)`
## WebSockets
@@ -499,7 +585,7 @@ have the entire [state](conventions.md#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](conventions.md#state)
-and support of [interceptors](conventions.md#interceptors), with [interceptor override](#interceptor-overriding). You
+and support of [interceptors](conventions.md#interceptors), with [interceptor override](how-to.md#interceptor-overriding). You
can define a [fallback function](#websockets), to handle missing actions.
```clojure
@@ -529,26 +615,18 @@ For route matching Xiana provides a couple of modes:
- Probe
- It tries to decode the message as JSON, then as EDN, then as string.
+ It tries to decode the message as JSON, EDN or string in corresponding order.
You can also define your own matching, and use it as a parameter to `xiana.websockets/router`
## Server-Sent Events (SSE)
-Xiana contains a simple SSE solution over [http-kit](https://github.com/http-kit/http-kit) server's `Channel`
-protocol.
+Xiana contains a simple SSE solution over WebSockets 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.
+Initialization is done by calling `xiana.sse/init`. Clients can subscribe by a route
+with `xiana.sse/sse-action` as `:ws-action`. Messages are sent with `xiana.sse/put!` function.
```clojure
-(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]
@@ -557,17 +635,14 @@ are sent with `xiana.sse/put!` function.
(defn ->system
[app-cfg]
- (-> (config/config)
- (merge app-cfg)
+ (-> (config/config app-cfg)
+ (route/reset routes)
sse/init
ws/start))
-(def app-cfg
- {:routes routes})
-
(defn -main
[& _args]
- (->system app-cfg))
+ (->system {}))
```
## Scheduler
diff --git a/doc/welcome.md b/doc/welcome.md
new file mode 100644
index 00000000..12a283a4
--- /dev/null
+++ b/doc/welcome.md
@@ -0,0 +1,55 @@
+
+
+# Xiana framework
+
+Xiana is a lightweight web-application framework written in Clojure, for Clojure. The goal is to be simple, fast, and
+most importantly - a welcoming platform for web programmers with different backgrounds who want to experience the
+wonders
+of functional programming!
+
+It's easy to install, fun to experiment with, and a powerful tool to produce monolithic web applications.
+
+## Installation
+
+### From template
+
+Xiana has its own Leiningen template, so you can create a skeleton project with
+
+```shell
+lein new xiana app
+```
+
+### As a dependency
+
+Add it to your project as a dependency from clojars:
+
+[![Clojars Project](https://img.shields.io/clojars/v/com.flexiana/framework.svg)](https://clojars.org/com.flexiana/framework)
+
+## Getting started
+
+This [document](./getting-started.md) explains how to use Xiana to create a very simple app with a db, a backend
+offering an API, and a frontend that displays something from the database.
+
+## Docs
+
+- First check out the [conventions](./conventions.md).
+- To start working with xiana, read the [tutorials](./tutorials.md).
+- To contribute, see the [contribution](./contribution.md) docs.
+
+### Examples
+
+Visit [examples folder](https://github.com/Flexiana/framework/tree/main/examples), to see how you can perform
+
+- Access and data ownership control
+- Request coercion and response validation
+- Session handling with varying interceptors
+- Chat platform with WebSockets
+- Event based resource handling
+
+## References
+
+### Concept of interceptors
+
+[Pedestal](http://pedestal.io/reference/interceptors)
+
+[Sieppari](https://github.com/metosin/sieppari)
diff --git a/docs/0.5.0-rc2/contribution.html b/docs/0.5.0-rc2/contribution.html
new file mode 100644
index 00000000..6578272d
--- /dev/null
+++ b/docs/0.5.0-rc2/contribution.html
@@ -0,0 +1,108 @@
+
+
$ git clone git@github.com:Flexiana/framework.git; cd framework
+$ ./script/auto.sh -y all
+
+
The first command will clone Flexiana/framework repository and jump to its directory. The second command calls auto.sh script to perform the following sequence of steps:
+
+
Download the necessary docker images
+
Instantiate the database container
+
Import the initial SQL schema: ./docker/sql-scripts/init.sql
+
Populate the new schema with ‘fake’ data from: ./docker/sql-scripts/test.sql
+
Call clj -X:test that will download the necessary Clojure dependencies and executes unitary tests.
+
+
See ./script/auto.sh help for more advanced options.
+
Remember it’s necessary to have docker & docker-compose installed in your host machine. Docker daemon should be running. The chain of commands fails otherwise. It should also be noted that after the first installation everything will be cached preventing unnecessary rework, it’s possible to run only the tests, if your development environment is already up, increasing the overall productivity.
+
./script/auto.sh -y tests
+
+
Releasing
+
Install locally
+
clj -M:install
+
+
Deploying a release
+
+
Set up a new version number in release.edn eg: "0.5.0-rc2"
+
Make a git TAG with v prefix, like v0.5.0-rc2
+
Push it and wait for deployment to clojars
+
+
Executing example’s tests
+
+
Be sure all examples has the same framework version as it is in release.edn as dependency
+
Execute ./example-tests.sh script. It will install the actual version of xiana, and go through the examples folder for check-style and lein test.
+
+
Generating API Docs
+
This is done with using mermaid-cli and a forked version of Codox.
+
We’re using mermaid-cli for render UML-diagrams in markdown files, see the doc/conventions_template.md for example. These files need to be added to the /script/build-docs.sh . For using it you need to have npx.
+
Codox is forked because markdown anchors aren’t converted to HTML anchors in the official release. For use, you need to
It also updates the index.html file to point to the new version.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/conventions-1.svg b/docs/0.5.0-rc2/conventions-1.svg
new file mode 100644
index 00000000..ee25487a
--- /dev/null
+++ b/docs/0.5.0-rc2/conventions-1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/conventions-2.svg b/docs/0.5.0-rc2/conventions-2.svg
new file mode 100644
index 00000000..9b2d93b6
--- /dev/null
+++ b/docs/0.5.0-rc2/conventions-2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/conventions-3.svg b/docs/0.5.0-rc2/conventions-3.svg
new file mode 100644
index 00000000..20ac2909
--- /dev/null
+++ b/docs/0.5.0-rc2/conventions-3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/conventions.html b/docs/0.5.0-rc2/conventions.html
new file mode 100644
index 00000000..f86b761d
--- /dev/null
+++ b/docs/0.5.0-rc2/conventions.html
@@ -0,0 +1,60 @@
+
+Conventions
The diagram bellow gives you an overview, how a request is processed in Xiana based applications.
+
+
State
+
State is created for each HTTP request and represents the current state of the application. It contains:
+
+
the application’s dependencies and configuration
+
request
+
request-data
+
response
+
+
This structure is very volatile, will be updated quite often on the application’s life cycle.
+
The main modules that update the state are:
+
+
Routes:
+
+
Add information from the matched route to the state map
+
+
Interceptors:
+
+
Add, consumes or remove information from the state map. More details in Interceptors section.
+
+
Actions:
+
+
In actions, you are able to interfere with the :leave parts of the interceptors.
+
At the last step of execution the handler extracts the response value from the state.
+
The state is renewed on every request.
+
Action
+
The action conventionally is the control point of the application flow. This is the place were you can define how the rest of your execution flow would behave. Here you can provide the database query, restriction function, the view, and the additional side effect functions are you want to execute.
+
Actions are defined in the routes vector
+
["/" {:get {:action #(do something)}}]
+
+
Handler
+
Xiana’s handler creates the state for every request, matches the appropriate route, executes the interceptors, handles interceptor overrides, and not-found cases. It handles websocket requests too.
+
Routing
+
Routing means selecting the actions to execute depending on the request URL, and HTTP method.
+
Dependencies
+
Modules can depend on external resources, configurations, as well as on other modules. These dependencies are added to the state on state creation, and defined on application startup.
+
Interceptors
+
An interceptor is a pair of unary functions. Each function must recieve and return a state map. You can look at it as on an analogy to AOP’s around aspect, or as on a pair of middlewares. They work mostly the same way as pedestal and sieppari interceptors. Xiana provides a set of base interceptors, for the most common use cases.
+
This figure shows how interceptors are executed ideally:
+
+
Interceptors error handling:
+
The interceptor executor handles the exceptional states like sieppari does. If an exception happens, it tries to handle first in the same interceptor. If it has and :error handler, it will call it, otherwise it’ll search for :error handlers for the beginning of the interceptor queue. When and :error function found, and matched with the given exception, the executor calls the queue :leave functions in reserved order, where the handler has been found.
This will create a directory todo-app containing project scaffold.
+
2. Satisfy dependencies
+
Run the following command from the newly created project directory to fetch frontend dependencies
+
lein shadow npm-deps
+
+
3. Run dockerized database
+
You need to have docker and docker-compose installed on your machine for the following command to work. Run the following from the root directory of your project.
+
docker-compose up -d
+
+
This should spin up a PostgreSQL database on port 5433. Name of the DB is todo_app You can verify that the database is running by connecting to it. Value of username and password is postgres.
which should open a PostgreSQL shell if successful.
+
4. Populate database with data
+
On application start, the framework will look for database migrations located in the configured location. By default, this location is set to resources/migrations directory*.*
+
It is possible to create migrations by running from the project directory
+
lein migrate create todos
+
+
This will create two migration files inside the resources/migrations directory. Both file names will be prefixed by the timestamp value of the file creation.
+
Put the following SQL code inside the todos-up.sql file
+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+--;;
+CREATE TABLE todos
+(
+ id uuid NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY,
+ label varchar(2048) NOT NULL,
+ done boolean NOT NULL DEFAULT false,
+ created_at timestamptz NOT NULL DEFAULT now()
+);
+
+--;;
+
+INSERT INTO todos (label) values ('example label from DB');
+
+
and this one into the todos-down.sql file.
+
DROP TABLE todos;
+
+
5. Start the application
+
+
Run Clojure REPL and load contents of file dev/user.clj into it.
define a function to be called when the endpoint is hit.
+
(defn fetch
+ [state]
+ state)
+
+
Now you need to reload the changed files to REPL and restart the application by executing start-dev-system function once again. Now opening http://localhost:3000/api/todos in your browser returns a blank page.
+
7. Hello World!
+
Change the implementation of fetch function to display “Hello World!” every time our endpoint is hit.
Again, reload the modified code and restart the application. After opening http://localhost:3000/todos in your browser, it returns a page containing the data from our database that looks like this:
+
+
Create another endpoint to add a new entry to the DB
+
Left as an exercise for the reader.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/how-to.html b/docs/0.5.0-rc2/how-to.html
new file mode 100644
index 00000000..27fa2978
--- /dev/null
+++ b/docs/0.5.0-rc2/how-to.html
@@ -0,0 +1,383 @@
+
+How to
The interceptor is a map, can have three functions like:
+
: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
+
and a :name can be defined. All keys are optional, and if it missing it’s replaced by identity.
+
The provided functions are should have one parameter, the application state, and should return the modified state.
+
Interceptor example
+
+{:name :sample-interceptor
+ :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"}))}
+
+
Providing default interceptors
+
The router and controller interceptors definition is part of the application startup. The system’s dependency map should contain two sequence of interceptors like
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:
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.
+
Access and data ownership control
+
RBAC is a handy way to restrict user actions on different resources. It’s a role-based access control and helps you to implement data ownership control. The rbac/interceptor should be placed insidedb-access.
+
Role set definition
+
For tiny-RBAC you should provide a role-set. It’s a map which defines the application resources, the actions on it, the roles with the different granted actions, and restrictions for data ownership control. This map must be placed in deps.
user’s role is in (-> state :session-data :users/role)
+
+
If the :permission key is missing, all requests are going to be granted. If role-set or :users/role is missing, all requests are going to be denied.
+
When rbac/interceptor:enter is executed, it checks if the user has any permission on the pre-defined resource/action pair. If there is any, it collects all of them (including inherited permissions) into a set of format: :resource/restriction.
+
For example:
+
:image/own
+
+
means the given user is granted the permission to do the given action on :own:image resource. This will help you to implement data ownership functions. This set is associated in (-> state :request-data :user-permissions)
+
If user cannot perform the given action on the given resource (neither by inheritance nor by direct permission), the interceptor will interrupt the execution flow with the response:
+
{:status 403
+ :body "Forbidden"}
+
+
Data ownership
+
Data ownership control is about restricting database results only to the elements on which the user is able to perform the given action. In the context of the example above, it means :members are able to delete only the owned :images. At this point, you can use the result of the access control from the state. Continuing with the same example.
To achieve this, you can simply provide a restriction function into (-> state :request-data :restriction-fn) The user-permissions is a set, so it can be easily used for making conditions:
Xiana framework does not have any login or logout functions, as every application has its own user management logic. Though Xiana offers all the tools to easily implement them. One of the default interceptors is the session interceptor. If included, it can validate a request only if the session already exists in session storage. To log in a user, simply add its session data to the storage. (TODO: where? What is the exact key to modify?). All sessions should have a unique UUID as session-id. The active session lives under (-> state :session-data). On every request, before reaching the action defined by the route, the interceptor checks [:headers :session-id] among other things. Which is the id of the current session. The session is then loaded in session storage. If the id is not found, the execution flow is interrupted with the response:
+
{:status 401
+ :body "Invalid or missing session"}
+
(let [;; Create a unique ID
+ session-id (UUID/randomUUID)]
+ ;; Store a new session in session storage
+ (add! session-storage session-id {:session-id session-id})
+ ;; Make sure session-id is part of the response
+ (assoc-in state [:response :headers :session-id] (str session-id)))
+
+
or use the guest-session interceptor, which creates a guest session for unknown, or missing sessions.
+
For role-based access control, you need to store the actual user in your session data. First, you’ll have to query it from the database. It is best placed in models/user namespace. Here’s an example:
To execute it, place db-access interceptor in the interceptors list. It injects the query result into the state. If you already have this injected, you can modify your create session function like this:
+
(let [;; Get user from database result
+ user (-> state :response-data :db-data first)
+ ;; Create session
+ session-id (UUID/randomUUID)]
+ ;; Store the new session in session storage. Notice the addition of user.
+ (add! session-storage session-id (assoc user :session-id session-id))
+ ;; Make sure session-id is part of the response
+ (assoc-in state [:response :headers :session-id] (str session-id)))
+
+
Be sure to remove user’s password and any other sensitive information before storing it:
+
(let [;; Get user from database result
+ user (-> state
+ :response-data
+ :db-data
+ first
+ ;; Remove password for session storage
+ (dissoc :users/password))
+ ;; Create session id
+ session-id (UUID/randomUUID)]
+ ;; Store the new session in session storage
+ (add! session-storage session-id (assoc user :session-id session-id))
+ ;; Make sure session-id is part of the response
+ (assoc-in state [:response :headers :session-id] (str session-id)))
+
+
Next, we check if the credentials are correct, so we use an if statement.
+
(if (valid-credentials?)
+ (let [;; Get user from database result
+ user (-> state
+ :response-data
+ :db-data
+ first
+ ;; Remove password for session storage
+ (dissoc :users/password))
+ ;; Create session ID
+ session-id (UUID/randomUUID)]
+ ;; Store the new session in session storage
+ (add! session-storage session-id (assoc user :session-id session-id))
+ ;; Make sure session-id is part of the response
+ (assoc-in state [:response :headers :session-id] (str session-id)))
+ (throw (ex-info "Missing session data"
+ {:xiana/response
+ {:body "Login failed"
+ :status 401}})))
+
+
Xiana provides xiana.hash to check user credentials:
+
(defn- valid-credentials?
+ "It checks that the password provided by the user matches the encrypted password from the database."
+ [state]
+ (let [user-provided-pass (-> state :request :body-params :password)
+ db-stored-pass (-> state :response-data :db-data first :users/password)]
+ (and user-provided-pass
+ db-stored-pass
+ (hash/check state user-provided-pass db-stored-pass))))
+
+
The login logic is done, but where to place it?
+
Do you remember the side effect interceptor? It’s running after we have the query result from the database, and before the final response is rendered with the view interceptor. The place for the function defined above is in the interceptor chain. How does it go there? Let’s see an action
+
(defn action
+ [state]
+ (assoc state :side-effect side-effects/login))
+
+
This is the place for injecting the database query, too:
To do a logout is much easier than a login implementation. The session-interceptor does half of the work, and if you have a running session, then it will not complain. The only thing you should do is to remove the actual session from the state and from session storage. Something like this:
+
(defn logout
+ [state]
+ (let [session-store (get-in state [:deps :session-backend])
+ session-id (get-in state [:session-data :session-id])]
+ (session/delete! session-store session-id)
+ (dissoc state :session-data)))
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/index.html b/docs/0.5.0-rc2/index.html
new file mode 100644
index 00000000..76b2d704
--- /dev/null
+++ b/docs/0.5.0-rc2/index.html
@@ -0,0 +1,3 @@
+
+
Starting up an application takes several well-defined steps:
+
+
reading the configuration
+
setting up dependencies
+
spinning up a web-server
+
+
Configuration
+
Apps built with Xiana are configurable in several ways. It uses yogthos/config to resolve basic configuration from config.edn, .lein-env, .boot-env files, environment variables and system properties. Additionally
+
+
Xiana looks for an .edn file pointed with :xiana-config variable for overrides
+
You can define a key to read from any other (already defined) value, and/or pass a default value.
+
+
In practice this means you can define a config value like this:
:xiana/test {:test-value-1 "the value of 'property' key, or nil"
+ :test-value-2 "the value of `foo` key or \"baz\""}
+
+
The value of property key can come from the config files, environment variables, or from system properties.
+
Dependencies
+
Database connection, external APIs or session storage, the route definition, setting up scheduled executors or doing migrations are our dependencies. These dependencies 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:
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. Router interceptors are executed around Xiana’s router, controller interceptors executed around the defined action.
+
+
router interceptors :enter functions in order
+
router interceptors :leave functions in reversed order
+
routing, and matching
+
controller interceptors :enter functions in order
+
action
+
controller interceptors :leave functions in reversed order
+
+
In router interceptors, you are able to interfere with the routing mechanism. Controller interceptors can be interfered with via route definition. There is an option to define interceptors around creating WebSocket channels, these interceptors are executed around the :ws-action instead of :action.
+
Routes
+
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.
+
Example route definition:
+
["/api" {}
+ ["/login" {:post {:action #'user-controllers/login ;Login controller
+ :interceptors {:except [session/interceptor]}}}] ;the user doesn't have a valid session yet
+ ["/posts" {:get {:action #'posts-controllers/fetch ;controller definition for fetching posts
+ :permission :posts/read} ;set up the permission for fetching posts
+ :put {:action #'posts-controllers/add
+ :permission :posts/create}
+ :post {:action #'posts-controllers/update-post
+ :permission :posts/update}
+ :delete {:action #'posts-controllers/delete-post
+ :permission :posts/delete}}]
+ ["/notifications" {:get {:ws-action #'websockets/notifications ;websocket controller for sending notifications
+ :action #'notifications/fetch ;REST endpoint for fetching notifications
+ :permission :notifications/read}}] ;inject permission
+ ["/style" {:get {:action #'style/fetch
+ :organization :who}}]] ;this is not a usual key, the value will go to
+;(-> state :request-data :match :organization)
+;at the routing process
+
+
Action
+
The action function (controller) in a single CRUD application is for defining a view, a database-query (model) and optionally a side-effect function which will be executed in the following interceptor steps.
Database migration is based on the following principles:
+
+
The migration process is based on a stack of immutable changes. If at some point you want to change the schema or the content of the database you don’t change the previous scripts but add new scripts at the top of the stack.
+
There should be a single standard resources/migrations migration directory
+
If a specific platform (dev, stage, test, etc) needs additional scripts, specific directories should be created and in config set the appropriate migrations-dir as a vector containing the standard directory and the auxiliary directory.
+
The order in which scripts are executed depends only on the script id and not on the directory where the script is located
+
+
Configure migration
+
The migration process requires a config file containing:
The :migration-dir param is a vector of classpath relative paths containing database migrations scripts.
+
Usage
+
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
+
+
Database access
+
The xiana.db/db-access executes queries from :query (for single, non-transactional database access) and :db-queries in this order against the datasource extracted from state. The result will be available in (-> state :response-data :db-data) which is always a sequence.
+
db-queries is still a map, contains :queries and :transaction? keys. If :transaction? is set to true, all queries in db-queries will be executed in one transaction.
+
The query should be in honey SQL format, it will be sql-formatted on execution:
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.
+
This example shows you, how can you react on a login request. This stores the user data in the actual session on successful login, or injects the Unauthorized response into the state.
+
(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)
+ user (-> state :response-data :db-data first)]
+ (xiana-sessions/add! session-backend new-session-id user)
+ (assoc-in state [:response :headers "Session-id"] new-session-id))
+ (assoc state :response {:status 401
+ :body "Unauthorized"})))
+
+
Session management
+
Session management has two mayor components
+
+
session backend
+
session interceptors
+
+
The session backend can be in-memory or persisted using a json storage in postgres database.
+
In memory session backend
+
Basically it’s an atom backed session protocol implementation, allows you to fetchadd!delete!dump and erase! session data or the whole session storage. It doesn’t require any additional configuration, and this is the default set up for handling session storage. All stored session data is wiped out on system restart.
+
Persistent session backend
+
Instead of atom, it uses a postgresql table to store session data. Has the same protocol as in-memory. Configuration is necessary to use it.
Just init the backend after the database connection clojure (defn ->system [app-cfg] (-> (config/config app-cfg) routes/reset db-core/connect db-core/migrate! session/init-backend ws/start))
+
- Creating new datasource
+
+
If no datasource is provided on initialization, the init-backend function merges the database config with the session backend configuration, and creates a new datasource from the result.
+
Session interceptors
+
The session interceptors interchanges session data between the session-backend and the app state.
+
The xiana.session/interceptor throws an exception when no valid session-id can be found in the headers, cookies or as query parameter.
+
The xiana.session/guest-session-interceptor creates a guest session if the session-id is missing, or invalid, which means:
Both interceptors fetching already stored session data into the state at :enter, and on :leave updates session storage with the data from (-> state :session-data)
+
WebSockets
+
To use an endpoint to serve a WebSockets connection, you can define it on route-definition alongside the restfull action:
The creation of the actual channel happens in Xiana’s handler. All provided reactive functions have the entire state to work with.
+
WebSockets routing
+
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...")]}}))
+
+
Route matching
+
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, EDN or string in corresponding order.
+
You can also define your own matching, and use it as a parameter to xiana.websockets/router
+
Server-Sent Events (SSE)
+
Xiana contains a simple SSE solution over WebSockets protocol.
+
Initialization is done by calling xiana.sse/init. Clients can subscribe by a route with xiana.sse/sse-action as :ws-action. Messages are sent with xiana.sse/put! function.
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/welcome.html b/docs/0.5.0-rc2/welcome.html
new file mode 100644
index 00000000..08ea4306
--- /dev/null
+++ b/docs/0.5.0-rc2/welcome.html
@@ -0,0 +1,36 @@
+
+Xiana framework
Xiana is a lightweight web-application framework written in Clojure, for Clojure. The goal is to be simple, fast, and most importantly - a welcoming platform for web programmers with different backgrounds who want to experience the wonders of functional programming!
+
It’s easy to install, fun to experiment with, and a powerful tool to produce monolithic web applications.
+
Installation
+
From template
+
Xiana has its own Leiningen template, so you can create a skeleton project with
+
lein new xiana app
+
+
As a dependency
+
Add it to your project as a dependency from clojars:
+
+
Getting started
+
This document explains how to use Xiana to create a very simple app with a db, a backend offering an API, and a frontend that displays something from the database.
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.coercion.html b/docs/0.5.0-rc2/xiana.coercion.html
new file mode 100644
index 00000000..c6d6b301
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.coercion.html
@@ -0,0 +1,30 @@
+
+xiana.coercion documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.commons.html b/docs/0.5.0-rc2/xiana.commons.html
new file mode 100644
index 00000000..9b1a9d20
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.commons.html
@@ -0,0 +1,7 @@
+
+xiana.commons documentation
Same as clojure.core/merge, except that it recursively applies itself to every nested map.
+
map-keys
(map-keys f m)
Do mapping on map’s keys
+
rename-key
(rename-key m from to)
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.config.html b/docs/0.5.0-rc2/xiana.config.html
new file mode 100644
index 00000000..b0d0359b
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.config.html
@@ -0,0 +1,6 @@
+
+xiana.config documentation
You can pass path to the config file with the :xiana-config key. It’s useful for choosing an environment different from the current one (e.g. test or production while in the dev repl).
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.cookies.html b/docs/0.5.0-rc2/xiana.cookies.html
new file mode 100644
index 00000000..30149d80
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.cookies.html
@@ -0,0 +1,5 @@
+
+xiana.cookies documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.db.html b/docs/0.5.0-rc2/xiana.db.html
new file mode 100644
index 00000000..a13cdca4
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.db.html
@@ -0,0 +1,12 @@
+
+xiana.db documentation
Adds :datasource key to the :xiana/postgresql config section and duplicates :xiana/postgresql under the top-level :db key.
+
db-access
Database access interceptor, works from :query and from db-queries keys Enter: nil. Leave: Fetch and execute a given query using the chosen database driver, if succeeds associate its results into state response data. The query must be a sql-map, e.g:
+
{:select [:*] :from [:users]}
+
+
execute
(execute datasource sql-map)
Gets datasource, parse the given sql-map (query) and execute it using jdbc/execute!, and returns the modified keys
+
map->db
(map->db m__7972__auto__)
Factory function for class xiana.db.db, taking a map of keywords to field values.
+
migrate!
(migrate! config)(migrate! config count)
Tries to migrate the database maximum 10 times.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.db.migrate.html b/docs/0.5.0-rc2/xiana.db.migrate.html
new file mode 100644
index 00000000..3f5a8772
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.db.migrate.html
@@ -0,0 +1,5 @@
+
+xiana.db.migrate documentation
Validate command line arguments. Either return a map indicating the program should exit (with an error message, and optional ok status), or a map indicating the action the program should take and the options provided.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.handler.html b/docs/0.5.0-rc2/xiana.handler.html
new file mode 100644
index 00000000..a0152247
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.handler.html
@@ -0,0 +1,24 @@
+
+xiana.handler documentation
Returns handler function for server, which do the routing, and executes interceptors and given action.
+
Execution order:
+
+
router interceptors: enters in order
+
router interceptors leaves in reversed order
+
+
routing
+
+
+
around interceptors enters in order
+
controller interceptors enters in order
+
inside interceptors enters in order
+
+
action
+
+
+
inside interceptors leaves in reversed order
+
controller interceptors leaves in reversed order
+
around interceptors leaves in reversed order
+
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.hash.html b/docs/0.5.0-rc2/xiana.hash.html
new file mode 100644
index 00000000..1c917fc6
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.hash.html
@@ -0,0 +1,8 @@
+
+xiana.hash documentation
Cryptography helper for creating, and resolving passwords. Supported algorithms are bcrypr, pbkdf2, and scrypt. The required algorithm should be in
+
(-> state :deps :auth :hash-algorithm)
+
+
check
multimethod
Validating password.
+
make
multimethod
Creating an encrypted version for store password.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.interceptor.error.html b/docs/0.5.0-rc2/xiana.interceptor.error.html
new file mode 100644
index 00000000..a0e7845e
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.interceptor.error.html
@@ -0,0 +1,5 @@
+
+xiana.interceptor.error documentation
Universal error handler for :xiana/response errors
+
response
Handles the exception if there’s ex-info exception with non-empty :xiana/response key.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.interceptor.html b/docs/0.5.0-rc2/xiana.interceptor.html
new file mode 100644
index 00000000..5612f412
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.interceptor.html
@@ -0,0 +1,30 @@
+
+xiana.interceptor documentation
Enter: Print ‘Enter:’ followed by the complete state map.
+
Leave: Print ‘Leave:’ followed by the complete state map.
+
+
message
(message msg)
This interceptor creates a function that prints predefined message.
+
+
Enter: Print an arbitrary message.
+
Leave: Print an arbitrary message.
+
+
muuntaja
(muuntaja)(muuntaja interceptor)
Muuntaja encoder/decoder interceptor.
+
params
Update the request map with parsed url-encoded parameters. Adds the following keys to the request map:
+
:query-params - a map of parameters from the query string
+
:form-params - a map of parameters from the body
+
:params - a merged map of all types of parameter
+
prune-get-request-bodies
This interceptor removes bodies from GET requests on Enter.
+
side-effect
Side-effect interceptor.
+
+
Enter: nil.
+
Leave: apply :side-effect state key to state if it exists.
+
+
view
View interceptor
+
+
Enter: nil
+
Leave: apply :view state key to state if it exists and :response is absent.
+
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.interceptor.muuntaja.html b/docs/0.5.0-rc2/xiana.interceptor.muuntaja.html
new file mode 100644
index 00000000..430d1ca6
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.interceptor.muuntaja.html
@@ -0,0 +1,5 @@
+
+xiana.interceptor.muuntaja documentation
Request/response format interceptor based on muuntaja
+
interceptor
Define muuntaja’s default interceptor.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.interceptor.queue.html b/docs/0.5.0-rc2/xiana.interceptor.queue.html
new file mode 100644
index 00000000..30b1c630
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.interceptor.queue.html
@@ -0,0 +1,5 @@
+
+xiana.interceptor.queue documentation
Interceptor executor. Collects and executes interceptors and the given action in between.
+
execute
(execute state default-interceptors)
Execute the interceptors queue and invoke the action procedure between its enter-leave stacks.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.interceptor.wrap.html b/docs/0.5.0-rc2/xiana.interceptor.wrap.html
new file mode 100644
index 00000000..e394308c
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.interceptor.wrap.html
@@ -0,0 +1,6 @@
+
+xiana.interceptor.wrap documentation
Parse middleware function to interceptor leave lambda function.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.mail.html b/docs/0.5.0-rc2/xiana.mail.html
new file mode 100644
index 00000000..f5273dc4
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.mail.html
@@ -0,0 +1,4 @@
+
+xiana.mail documentation
(send-email! {mail-config :xiana/emails} {:keys [to cc bcc subject body attachments]})
Sending a mail with ‘postal.core’
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.rbac.html b/docs/0.5.0-rc2/xiana.rbac.html
new file mode 100644
index 00000000..df1b6137
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.rbac.html
@@ -0,0 +1,9 @@
+
+xiana.rbac documentation
On enter it validates if the resource is restricted, and available at the current state (actual user with a role) If it’s not restricted does nothing, if the given user has no rights, it throws ex-info with data
+
{:status 403 :body "Forbidden"}
+
+
On leave executes restriction function if any.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.route.helpers.html b/docs/0.5.0-rc2/xiana.route.helpers.html
new file mode 100644
index 00000000..1c32198b
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.route.helpers.html
@@ -0,0 +1,6 @@
+
+xiana.route.helpers documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.route.html b/docs/0.5.0-rc2/xiana.route.html
new file mode 100644
index 00000000..41efcd4d
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.route.html
@@ -0,0 +1,6 @@
+
+xiana.route documentation
Do the routing, and inject request data to the xiana state
+
match
(match {request :request, :as state})
Associate router match template data into the state. Return the wrapped state container.
+
reset
(reset config)
Update routes.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.scheduler.html b/docs/0.5.0-rc2/xiana.scheduler.html
new file mode 100644
index 00000000..a556547a
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.scheduler.html
@@ -0,0 +1,5 @@
+
+xiana.scheduler documentation
Scheduler creates a channel for all scheduled jobs, and calls it in a go-loop after a given timeout
+
start
(start deps action interval-msecs)
Starts to repeat an action with given timeout
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.session.html b/docs/0.5.0-rc2/xiana.session.html
new file mode 100644
index 00000000..66d75a6d
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.session.html
@@ -0,0 +1,23 @@
+
+xiana.session documentation
it’ll be use postgres else it will be an in-memory storage
+
interceptor
:enter: Fetches and injects previously stored session from session backend, into
+
(-> state :session-data)
+
+
:leave: Stores session data from state
+
When session is missing from session backend it’ll return with
+
{:body {:message "Invalid or missing session"}
+ :status 401}
+
+
on :enter
+
Session
protocol
members
add!
(add! _ k v)
add an element (side effect)
+
delete!
(delete! _ k)
delete an element (side effect)
+
dump
(dump _)
fetch all elements (no side effect)
+
erase!
(erase! _)
erase all elements (side effect)
+
fetch
(fetch _ k)
fetch an element (no side effect)
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.sse.html b/docs/0.5.0-rc2/xiana.sse.html
new file mode 100644
index 00000000..d54bdc1c
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.sse.html
@@ -0,0 +1,12 @@
+
+xiana.sse documentation
SSE aka Server sent event solution based on websockets.
+
->closable-events-channel
(->closable-events-channel channel clients)
Positional factory function for class xiana.sse.closable-events-channel.
+
close-channel
Close a client’s channel
+
init
(init config)
Opens the SSE input channel and injects it into the config map
+
map->closable-events-channel
(map->closable-events-channel m__7972__auto__)
Factory function for class xiana.sse.closable-events-channel, taking a map of keywords to field values.
+
put!
(put! state message)
Puts a message to SSE input channel for broadcast it to all clients
+
put->session
(put->session deps session-id message)
Puts a message directly addressed to one client, by its session id
+
sse-action
(sse-action state)
A web-socket action to subscribe a client to SSE channel
+
stop-heartbeat-loop
(stop-heartbeat-loop state)
Closes input channel for sending messages
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.state.html b/docs/0.5.0-rc2/xiana.state.html
new file mode 100644
index 00000000..cab6bd87
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.state.html
@@ -0,0 +1,4 @@
+
+xiana.state documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.webserver.html b/docs/0.5.0-rc2/xiana.webserver.html
new file mode 100644
index 00000000..c08a7bac
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.webserver.html
@@ -0,0 +1,7 @@
+
+xiana.webserver documentation
Positional factory function for class xiana.webserver.webserver.
+
map->webserver
(map->webserver m__7972__auto__)
Factory function for class xiana.webserver.webserver, taking a map of keywords to field values.
+
start
(start dependencies)
Start web server.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.websockets.html b/docs/0.5.0-rc2/xiana.websockets.html
new file mode 100644
index 00000000..617e099c
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.websockets.html
@@ -0,0 +1,10 @@
+
+xiana.websockets documentation
Router for webSockets. Parameters: routes: reitit routes msg->uri: function makes routing base from message. If missing tries to solve message as json, edn and string state: xiana state record
\ No newline at end of file
diff --git a/docs/0.5.0-rc2/xiana.websockets.router-helpers.html b/docs/0.5.0-rc2/xiana.websockets.router-helpers.html
new file mode 100644
index 00000000..a076afe3
--- /dev/null
+++ b/docs/0.5.0-rc2/xiana.websockets.router-helpers.html
@@ -0,0 +1,12 @@
+
+xiana.websockets.router-helpers documentation
EDN to uri, converts edn string to map, extract :action key
+
json->
(json-> j)
JSON to uri, converts json string to map, extract :action key
+
probe->
(probe-> e)
Tries to solve the routing, in order:
+
+
json->
+
edn->
+
string->
+
+
string->
(string-> s)
String to uri, uses the first word as action key
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/contribution.html b/docs/0.5.0-rc3/contribution.html
new file mode 100644
index 00000000..c724e290
--- /dev/null
+++ b/docs/0.5.0-rc3/contribution.html
@@ -0,0 +1,107 @@
+
+Contribution
$ git clone git@github.com:Flexiana/framework.git; cd framework
+$ ./script/auto.sh -y all
+
+
The first command will clone Flexiana/framework repository and jump to its directory. The second command calls auto.sh script to perform the following sequence of steps:
+
+
Download the necessary docker images
+
Instantiate the database container
+
Import the initial SQL schema: ./docker/sql-scripts/init.sql
+
Populate the new schema with ‘fake’ data from: ./docker/sql-scripts/test.sql
+
Call clj -X:test that will download the necessary Clojure dependencies and executes unitary tests.
+
+
See ./script/auto.sh help for more advanced options.
+
Remember it’s necessary to have docker & docker-compose installed in your host machine. Docker daemon should be running. The chain of commands fails otherwise. It should also be noted that after the first installation everything will be cached preventing unnecessary rework, it’s possible to run only the tests, if your development environment is already up, increasing the overall productivity.
+
./script/auto.sh -y tests
+
+
Releasing
+
Install locally
+
clj -M:install
+
+
Deploying a release
+
+
Set up a new version number in release.edn eg: "0.5.0-rc2"
+
Make a git TAG with v prefix, like v0.5.0-rc2
+
Push it and wait for deployment to clojars
+
+
Executing example’s tests
+
+
Be sure all examples has the same framework version as it is in release.edn as dependency
+
Execute ./example-tests.sh script. It will install the actual version of xiana, and go through the examples folder for check-style and lein test.
We’re using mermaid-cli to render UML-diagrams in markdown files, see the doc/conventions_template.md for example. These files need to be added to the /script/build-docs.sh . For using it you need to have npx.
+
Codox is forked because markdown anchors aren’t converted to HTML anchors in the official release. To use it you need
It also updates the index.html file to point to the new version.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/conventions-1.svg b/docs/0.5.0-rc3/conventions-1.svg
new file mode 100644
index 00000000..4e7c6022
--- /dev/null
+++ b/docs/0.5.0-rc3/conventions-1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/conventions-2.svg b/docs/0.5.0-rc3/conventions-2.svg
new file mode 100644
index 00000000..9ee4bc7c
--- /dev/null
+++ b/docs/0.5.0-rc3/conventions-2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/conventions-3.svg b/docs/0.5.0-rc3/conventions-3.svg
new file mode 100644
index 00000000..3141e7d6
--- /dev/null
+++ b/docs/0.5.0-rc3/conventions-3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/conventions.html b/docs/0.5.0-rc3/conventions.html
new file mode 100644
index 00000000..1988093d
--- /dev/null
+++ b/docs/0.5.0-rc3/conventions.html
@@ -0,0 +1,57 @@
+
+Conventions
The diagram bellow gives you an overview how a request is processed in Xiana based applications.
+
State
+
State is created for each HTTP request and represents the current state of the application. It contains:
+
+
the application’s dependencies and configuration
+
request
+
request-data
+
response
+
+
This structure is very volatile, and will be updated quite often on the application’s life cycle.
+
The main modules that update the state are:
+
+
Routes:
+
+
Add information from the matched route to the state map
+
+
Interceptors:
+
+
Add, consume or remove information from the state map. More details in Interceptors section.
+
+
Actions:
+
+
In actions, you are able to interfere with the :leave parts of the interceptors.
+
At the last step of execution the handler extracts the response value from the state.
+
The state is renewed on every request.
+
Action
+
The action conventionally is the control point of the application flow. This is the place were you can define how the rest of your execution flow would behave. Here you can provide the database query, restriction function, the view, and the additional side effect functions that you want to execute.
+
Actions are defined in the routes vector
+
["/" {:get {:action #(do something)}}]
+
+
Handler
+
Xiana’s handler creates the state for every request, matches the appropriate route, executes the interceptors, handles interceptor overrides, and not-found cases. It handles websocket requests too.
+
Routing
+
Routing means selecting the actions to execute depending on the request URL, and HTTP method.
+
Dependencies
+
Modules can depend on external resources and configurations, as well as on other modules. These dependencies are added to the state on state creation, and defined on application startup.
+
Interceptors
+
An interceptor is a pair of unary functions. Each function must recieve and return a state map. Look at it as an analogy to AOP’s around aspect, or as on a pair of middlewares. They work mostly the same way as pedestal and sieppari interceptors. Xiana provides a set of base interceptors, for the most common use cases.
+
This figure shows how interceptors are executed ideally:
+
Interceptors error handling:
+
The interceptor executor handles the exceptional states like sieppari does. If an exception happens, it tries to handle first in the same interceptor. If it has an :error handler, it will call it, otherwise it’ll search for :error handlers from the beginning of the interceptor queue. When an :error function found, and matched with the given exception, the executor calls the queue of :leave functions in reserved order from where the handler was found.
+
This diagram shows how the error cases are handled:
This will create a directory todo-app containing project scaffold.
+
2. Satisfy dependencies
+
Run the following command from the newly created project directory to fetch frontend dependencies
+
lein shadow npm-deps
+
+
3. Run dockerized database
+
You need to have docker and docker-compose installed on your machine for the following command to work. Run the following from the root directory of your project.
+
docker-compose up -d
+
+
This should spin up a PostgreSQL database on port 5433. Name of the DB is todo_app You can verify that the database is running by connecting to it. Value of username and password is postgres.
which should open a PostgreSQL shell if successful.
+
4. Populate database with data
+
On application start, the framework will look for database migrations located in the configured location. By default, this location is set to resources/migrations directory*.*
+
It is possible to create migrations by running from the project directory
+
lein migrate create todos
+
+
This will create two migration files inside the resources/migrations directory. Both file names will be prefixed by the timestamp value of the file creation.
+
Put the following SQL code inside the todos-up.sql file
+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+--;;
+CREATE TABLE todos
+(
+ id uuid NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY,
+ label varchar(2048) NOT NULL,
+ done boolean NOT NULL DEFAULT false,
+ created_at timestamptz NOT NULL DEFAULT now()
+);
+
+--;;
+
+INSERT INTO todos (label) values ('example label from DB');
+
+
and this one into the todos-down.sql file.
+
DROP TABLE todos;
+
+
5. Start the application
+
+
Run Clojure REPL and load contents of file dev/user.clj into it.
define a function to be called when the endpoint is hit.
+
(defn fetch
+ [state]
+ state)
+
+
Now you need to reload the changed files to REPL and restart the application by executing start-dev-system function once again. Now opening http://localhost:3000/api/todos in your browser returns a blank page.
+
7. Hello World!
+
Change the implementation of fetch function to display “Hello World!” every time our endpoint is hit.
Again, reload the modified code and restart the application. After opening http://localhost:3000/todos in your browser, it returns a page containing the data from our database that looks like this:
+
+
Create another endpoint to add a new entry to the DB
+
Left as an exercise for the reader.
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/how-to.html b/docs/0.5.0-rc3/how-to.html
new file mode 100644
index 00000000..27fa2978
--- /dev/null
+++ b/docs/0.5.0-rc3/how-to.html
@@ -0,0 +1,383 @@
+
+How to
The interceptor is a map, can have three functions like:
+
: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
+
and a :name can be defined. All keys are optional, and if it missing it’s replaced by identity.
+
The provided functions are should have one parameter, the application state, and should return the modified state.
+
Interceptor example
+
+{:name :sample-interceptor
+ :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"}))}
+
+
Providing default interceptors
+
The router and controller interceptors definition is part of the application startup. The system’s dependency map should contain two sequence of interceptors like
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:
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.
+
Access and data ownership control
+
RBAC is a handy way to restrict user actions on different resources. It’s a role-based access control and helps you to implement data ownership control. The rbac/interceptor should be placed insidedb-access.
+
Role set definition
+
For tiny-RBAC you should provide a role-set. It’s a map which defines the application resources, the actions on it, the roles with the different granted actions, and restrictions for data ownership control. This map must be placed in deps.
user’s role is in (-> state :session-data :users/role)
+
+
If the :permission key is missing, all requests are going to be granted. If role-set or :users/role is missing, all requests are going to be denied.
+
When rbac/interceptor:enter is executed, it checks if the user has any permission on the pre-defined resource/action pair. If there is any, it collects all of them (including inherited permissions) into a set of format: :resource/restriction.
+
For example:
+
:image/own
+
+
means the given user is granted the permission to do the given action on :own:image resource. This will help you to implement data ownership functions. This set is associated in (-> state :request-data :user-permissions)
+
If user cannot perform the given action on the given resource (neither by inheritance nor by direct permission), the interceptor will interrupt the execution flow with the response:
+
{:status 403
+ :body "Forbidden"}
+
+
Data ownership
+
Data ownership control is about restricting database results only to the elements on which the user is able to perform the given action. In the context of the example above, it means :members are able to delete only the owned :images. At this point, you can use the result of the access control from the state. Continuing with the same example.
To achieve this, you can simply provide a restriction function into (-> state :request-data :restriction-fn) The user-permissions is a set, so it can be easily used for making conditions:
Xiana framework does not have any login or logout functions, as every application has its own user management logic. Though Xiana offers all the tools to easily implement them. One of the default interceptors is the session interceptor. If included, it can validate a request only if the session already exists in session storage. To log in a user, simply add its session data to the storage. (TODO: where? What is the exact key to modify?). All sessions should have a unique UUID as session-id. The active session lives under (-> state :session-data). On every request, before reaching the action defined by the route, the interceptor checks [:headers :session-id] among other things. Which is the id of the current session. The session is then loaded in session storage. If the id is not found, the execution flow is interrupted with the response:
+
{:status 401
+ :body "Invalid or missing session"}
+
(let [;; Create a unique ID
+ session-id (UUID/randomUUID)]
+ ;; Store a new session in session storage
+ (add! session-storage session-id {:session-id session-id})
+ ;; Make sure session-id is part of the response
+ (assoc-in state [:response :headers :session-id] (str session-id)))
+
+
or use the guest-session interceptor, which creates a guest session for unknown, or missing sessions.
+
For role-based access control, you need to store the actual user in your session data. First, you’ll have to query it from the database. It is best placed in models/user namespace. Here’s an example:
To execute it, place db-access interceptor in the interceptors list. It injects the query result into the state. If you already have this injected, you can modify your create session function like this:
+
(let [;; Get user from database result
+ user (-> state :response-data :db-data first)
+ ;; Create session
+ session-id (UUID/randomUUID)]
+ ;; Store the new session in session storage. Notice the addition of user.
+ (add! session-storage session-id (assoc user :session-id session-id))
+ ;; Make sure session-id is part of the response
+ (assoc-in state [:response :headers :session-id] (str session-id)))
+
+
Be sure to remove user’s password and any other sensitive information before storing it:
+
(let [;; Get user from database result
+ user (-> state
+ :response-data
+ :db-data
+ first
+ ;; Remove password for session storage
+ (dissoc :users/password))
+ ;; Create session id
+ session-id (UUID/randomUUID)]
+ ;; Store the new session in session storage
+ (add! session-storage session-id (assoc user :session-id session-id))
+ ;; Make sure session-id is part of the response
+ (assoc-in state [:response :headers :session-id] (str session-id)))
+
+
Next, we check if the credentials are correct, so we use an if statement.
+
(if (valid-credentials?)
+ (let [;; Get user from database result
+ user (-> state
+ :response-data
+ :db-data
+ first
+ ;; Remove password for session storage
+ (dissoc :users/password))
+ ;; Create session ID
+ session-id (UUID/randomUUID)]
+ ;; Store the new session in session storage
+ (add! session-storage session-id (assoc user :session-id session-id))
+ ;; Make sure session-id is part of the response
+ (assoc-in state [:response :headers :session-id] (str session-id)))
+ (throw (ex-info "Missing session data"
+ {:xiana/response
+ {:body "Login failed"
+ :status 401}})))
+
+
Xiana provides xiana.hash to check user credentials:
+
(defn- valid-credentials?
+ "It checks that the password provided by the user matches the encrypted password from the database."
+ [state]
+ (let [user-provided-pass (-> state :request :body-params :password)
+ db-stored-pass (-> state :response-data :db-data first :users/password)]
+ (and user-provided-pass
+ db-stored-pass
+ (hash/check state user-provided-pass db-stored-pass))))
+
+
The login logic is done, but where to place it?
+
Do you remember the side effect interceptor? It’s running after we have the query result from the database, and before the final response is rendered with the view interceptor. The place for the function defined above is in the interceptor chain. How does it go there? Let’s see an action
+
(defn action
+ [state]
+ (assoc state :side-effect side-effects/login))
+
+
This is the place for injecting the database query, too:
To do a logout is much easier than a login implementation. The session-interceptor does half of the work, and if you have a running session, then it will not complain. The only thing you should do is to remove the actual session from the state and from session storage. Something like this:
+
(defn logout
+ [state]
+ (let [session-store (get-in state [:deps :session-backend])
+ session-id (get-in state [:session-data :session-id])]
+ (session/delete! session-store session-id)
+ (dissoc state :session-data)))
+
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/index.html b/docs/0.5.0-rc3/index.html
new file mode 100644
index 00000000..76b2d704
--- /dev/null
+++ b/docs/0.5.0-rc3/index.html
@@ -0,0 +1,3 @@
+
+
Starting up an application takes several well-defined steps:
+
+
reading the configuration
+
setting up dependencies
+
spinning up a web-server
+
+
Configuration
+
Apps built with Xiana are configurable in several ways. It uses yogthos/config to resolve basic configuration from config.edn, .lein-env, .boot-env files, environment variables and system properties. Additionally
+
+
Xiana looks for an .edn file pointed with :xiana-config variable for overrides
+
You can define a key to read from any other (already defined) value, and/or pass a default value.
+
+
In practice this means you can define a config value like this:
:xiana/test {:test-value-1 "the value of 'property' key, or nil"
+ :test-value-2 "the value of `foo` key or \"baz\""}
+
+
The value of property key can come from the config files, environment variables, or from system properties.
+
Dependencies
+
Database connection, external APIs or session storage, the route definition, setting up scheduled executors or doing migrations are our dependencies. These dependencies 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:
Database migration is based on the following principles:
+
+
The migration process is based on a stack of immutable changes. If at some point you want to change the schema or the content of the database you don’t change the previous scripts but add new scripts at the top of the stack.
+
There should be a single standard resources/migrations migration directory
+
If a specific environment (dev, stage, test, etc) needs additional scripts, specific directories should be created and in config set the appropriate migrations-dir as a vector containing the standard directory and the auxiliary directory.
+
The order in which scripts are executed depends only on the script id and not on the directory where the script is located
+
+
Configure migration
+
The migration process requires a config file containing:
The :migration-dir param is a vector of classpath relative paths containing database migrations scripts.
+
Usage
+
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
+
The router injects :request-data, and decides what action should be executed
+
Muuntaja does the request’s encoding
+
parameters injected via reitit
+
injecting session-data into the state
+
view does nothing on :enter
+
db-access does nothing on :enter
+
RBAC tests for permissions
+
execution of the given action
+
RBAC applies data ownership function
+
db-access executes the given query
+
rendering response map
+
updating session storage from state/session-data
+
Params do nothing on :leave
+
muuntaja converts the response body to the accepted format
+
+
Defining new interceptors
+
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.
+
Interceptor example
+
+{: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"}))}
+
+
Router and controller interceptors
+
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
+
+
router interceptors :enter functions in order
+
router interceptors :leave functions in reversed order
+
routing, and matching
+
controller interceptors :enter functions in order
+
action
+
controller interceptors :leave functions in reversed order
+
+
In router interceptors, you are able to interfere with the routing mechanism. Controller interceptors can be interfered with via route definition. There is an option to define interceptors around creating WebSocket channels, these interceptors are executed around the :ws-action instead of :action.
+
Routes
+
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.
+
Example route definition:
+
["/api" {}
+ ["/login" {:post {:action #'user-controllers/login ;Login controller
+ :interceptors {:except [session/interceptor]}}}] ;the user doesn't have a valid session yet
+ ["/posts" {:get {:action #'posts-controllers/fetch ;controller definition for fetching posts
+ :permission :posts/read} ;set up the permission for fetching posts
+ :put {:action #'posts-controllers/add
+ :permission :posts/create}
+ :post {:action #'posts-controllers/update-post
+ :permission :posts/update}
+ :delete {:action #'posts-controllers/delete-post
+ :permission :posts/delete}}]
+ ["/notifications" {:get {:ws-action #'websockets/notifications ;websocket controller for sending notifications
+ :action #'notifications/fetch ;REST endpoint for fetching notifications
+ :permission :notifications/read}}] ;inject permission
+ ["/style" {:get {:action #'style/fetch
+ :organization :who}}]] ;this is not a usual key, the value will go to
+;(-> state :request-data :match :organization)
+;at the routing process
+
+
Action
+
The action function (controller) in a single CRUD application is for defining a view, a database-query (model) and optionally a side-effect function which will be executed in the following interceptor steps.
Database migration is based on the following principles:
+
+
The migration process is based on a stack of immutable changes. If at some point you want to change the schema or the content of the database you don’t change the previous scripts but add new scripts at the top of the stack.
+
There should be a single standard resources/migrations migration directory
+
If a specific platform (dev, stage, test, etc) needs additional scripts, specific directories should be created and in config set the appropriate migrations-dir as a vector containing the standard directory and the auxiliary directory.
+
The order in which scripts are executed depends only on the script id and not on the directory where the script is located
+
+
Configure migration
+
The migration process requires a config file containing:
The :migration-dir param is a vector of classpath relative paths containing database migrations scripts.
+
Usage
+
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
+
+
Database access
+
The xiana.db/db-access executes queries from :query (for single, non-transactional database access) and :db-queries in this order against the datasource extracted from state. The result will be available in (-> state :response-data :db-data) which is always a sequence.
+
db-queries is still a map, contains :queries and :transaction? keys. If :transaction? is set to true, all queries in db-queries will be executed in one transaction.
+
The query should be in honey SQL format, it will be sql-formatted on execution:
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.
+
This example shows you, how can you react on a login request. This stores the user data in the actual session on successful login, or injects the Unauthorized response into the state.
+
(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)
+ user (-> state :response-data :db-data first)]
+ (xiana-sessions/add! session-backend new-session-id user)
+ (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 management
+
Session management has two mayor components
+
+
session backend
+
session interceptors
+
+
The session backend can be in-memory or persisted using a json storage in postgres database.
+
In memory session backend
+
Basically it’s an atom backed session protocol implementation, allows you to fetchadd!delete!dump and erase! session data or the whole session storage. It doesn’t require any additional configuration, and this is the default set up for handling session storage. All stored session data is wiped out on system restart.
+
Persistent session backend
+
Instead of atom, it uses a postgresql table to store session data. Has the same protocol as in-memory. Configuration is necessary to use it.
Just init the backend after the database connection clojure (defn ->system [app-cfg] (-> (config/config app-cfg) routes/reset db-core/connect db-core/migrate! session/init-backend ws/start))
+
- Creating new datasource
+
+
If no datasource is provided on initialization, the init-backend function merges the database config with the session backend configuration, and creates a new datasource from the result.
+
Session interceptors
+
The session interceptors interchanges session data between the session-backend and the app state.
+
The xiana.session/interceptor throws an exception when no valid session-id can be found in the headers, cookies or as query parameter.
+
The xiana.session/guest-session-interceptor creates a guest session if the session-id is missing, or invalid, which means:
Both interceptors fetching already stored session data into the state at :enter, and on :leave updates session storage with the data from (-> state :session-data)
+
WebSockets
+
To use an endpoint to serve a WebSockets connection, you can define it on route-definition alongside the restfull action:
The creation of the actual channel happens in Xiana’s handler. All provided reactive functions have the entire state to work with.
+
WebSockets routing
+
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...")]}}))
+
+
Route matching
+
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, EDN or string in corresponding order.
+
You can also define your own matching, and use it as a parameter to xiana.websockets/router
+
Server-Sent Events (SSE)
+
Xiana contains a simple SSE solution over WebSockets protocol.
+
Initialization is done by calling xiana.sse/init. Clients can subscribe by a route with xiana.sse/sse-action as :ws-action. Messages are sent with xiana.sse/put! function.
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/welcome.html b/docs/0.5.0-rc3/welcome.html
new file mode 100644
index 00000000..08ea4306
--- /dev/null
+++ b/docs/0.5.0-rc3/welcome.html
@@ -0,0 +1,36 @@
+
+Xiana framework
Xiana is a lightweight web-application framework written in Clojure, for Clojure. The goal is to be simple, fast, and most importantly - a welcoming platform for web programmers with different backgrounds who want to experience the wonders of functional programming!
+
It’s easy to install, fun to experiment with, and a powerful tool to produce monolithic web applications.
+
Installation
+
From template
+
Xiana has its own Leiningen template, so you can create a skeleton project with
+
lein new xiana app
+
+
As a dependency
+
Add it to your project as a dependency from clojars:
+
+
Getting started
+
This document explains how to use Xiana to create a very simple app with a db, a backend offering an API, and a frontend that displays something from the database.
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.coercion.html b/docs/0.5.0-rc3/xiana.coercion.html
new file mode 100644
index 00000000..1c2057f1
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.coercion.html
@@ -0,0 +1,30 @@
+
+xiana.coercion documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.commons.html b/docs/0.5.0-rc3/xiana.commons.html
new file mode 100644
index 00000000..8d34f94e
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.commons.html
@@ -0,0 +1,7 @@
+
+xiana.commons documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.config.html b/docs/0.5.0-rc3/xiana.config.html
new file mode 100644
index 00000000..be6c9bab
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.config.html
@@ -0,0 +1,6 @@
+
+xiana.config documentation
You can pass path to the config file with the :xiana-config key. It’s useful for choosing an environment different from the current one (e.g. test or production while in the dev repl).
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.cookies.html b/docs/0.5.0-rc3/xiana.cookies.html
new file mode 100644
index 00000000..81c23dd1
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.cookies.html
@@ -0,0 +1,5 @@
+
+xiana.cookies documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.db.html b/docs/0.5.0-rc3/xiana.db.html
new file mode 100644
index 00000000..0255f095
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.db.html
@@ -0,0 +1,12 @@
+
+xiana.db documentation
Database access interceptor, works from :query and from db-queries keys Enter: nil. Leave: Fetch and execute a given query using the chosen database driver, if succeeds associate its results into state response data. The query must be a sql-map, e.g:
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.db.migrate.html b/docs/0.5.0-rc3/xiana.db.migrate.html
new file mode 100644
index 00000000..32578c11
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.db.migrate.html
@@ -0,0 +1,5 @@
+
+xiana.db.migrate documentation
Validate command line arguments. Either return a map indicating the program should exit (with an error message, and optional ok status), or a map indicating the action the program should take and the options provided.
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.handler.html b/docs/0.5.0-rc3/xiana.handler.html
new file mode 100644
index 00000000..76d5f719
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.handler.html
@@ -0,0 +1,24 @@
+
+xiana.handler documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.hash.html b/docs/0.5.0-rc3/xiana.hash.html
new file mode 100644
index 00000000..e1add1b8
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.hash.html
@@ -0,0 +1,8 @@
+
+xiana.hash documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.interceptor.error.html b/docs/0.5.0-rc3/xiana.interceptor.error.html
new file mode 100644
index 00000000..a615a02f
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.interceptor.error.html
@@ -0,0 +1,5 @@
+
+xiana.interceptor.error documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.interceptor.html b/docs/0.5.0-rc3/xiana.interceptor.html
new file mode 100644
index 00000000..854f5cfd
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.interceptor.html
@@ -0,0 +1,30 @@
+
+xiana.interceptor documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.interceptor.muuntaja.html b/docs/0.5.0-rc3/xiana.interceptor.muuntaja.html
new file mode 100644
index 00000000..22c033b9
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.interceptor.muuntaja.html
@@ -0,0 +1,5 @@
+
+xiana.interceptor.muuntaja documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.interceptor.queue.html b/docs/0.5.0-rc3/xiana.interceptor.queue.html
new file mode 100644
index 00000000..401c8cc4
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.interceptor.queue.html
@@ -0,0 +1,5 @@
+
+xiana.interceptor.queue documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.interceptor.wrap.html b/docs/0.5.0-rc3/xiana.interceptor.wrap.html
new file mode 100644
index 00000000..dddaec5a
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.interceptor.wrap.html
@@ -0,0 +1,6 @@
+
+xiana.interceptor.wrap documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.mail.html b/docs/0.5.0-rc3/xiana.mail.html
new file mode 100644
index 00000000..38feab6a
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.mail.html
@@ -0,0 +1,4 @@
+
+xiana.mail documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.rbac.html b/docs/0.5.0-rc3/xiana.rbac.html
new file mode 100644
index 00000000..98c7f598
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.rbac.html
@@ -0,0 +1,9 @@
+
+xiana.rbac documentation
On enter it validates if the resource is restricted, and available at the current state (actual user with a role) If it’s not restricted does nothing, if the given user has no rights, it throws ex-info with data
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.route.helpers.html b/docs/0.5.0-rc3/xiana.route.helpers.html
new file mode 100644
index 00000000..5dd8d443
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.route.helpers.html
@@ -0,0 +1,6 @@
+
+xiana.route.helpers documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.route.html b/docs/0.5.0-rc3/xiana.route.html
new file mode 100644
index 00000000..5c36cba2
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.route.html
@@ -0,0 +1,6 @@
+
+xiana.route documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.scheduler.html b/docs/0.5.0-rc3/xiana.scheduler.html
new file mode 100644
index 00000000..6f49ffcb
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.scheduler.html
@@ -0,0 +1,5 @@
+
+xiana.scheduler documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.session.html b/docs/0.5.0-rc3/xiana.session.html
new file mode 100644
index 00000000..ede3ea96
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.session.html
@@ -0,0 +1,23 @@
+
+xiana.session documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.sse.html b/docs/0.5.0-rc3/xiana.sse.html
new file mode 100644
index 00000000..28307f00
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.sse.html
@@ -0,0 +1,12 @@
+
+xiana.sse documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.state.html b/docs/0.5.0-rc3/xiana.state.html
new file mode 100644
index 00000000..d0034117
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.state.html
@@ -0,0 +1,4 @@
+
+xiana.state documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.webserver.html b/docs/0.5.0-rc3/xiana.webserver.html
new file mode 100644
index 00000000..8b934bc8
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.webserver.html
@@ -0,0 +1,7 @@
+
+xiana.webserver documentation
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.websockets.html b/docs/0.5.0-rc3/xiana.websockets.html
new file mode 100644
index 00000000..959621b5
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.websockets.html
@@ -0,0 +1,10 @@
+
+xiana.websockets documentation
Router for webSockets. Parameters: routes: reitit routes msg->uri: function makes routing base from message. If missing tries to solve message as json, edn and string state: xiana state record
\ No newline at end of file
diff --git a/docs/0.5.0-rc3/xiana.websockets.router-helpers.html b/docs/0.5.0-rc3/xiana.websockets.router-helpers.html
new file mode 100644
index 00000000..062ded70
--- /dev/null
+++ b/docs/0.5.0-rc3/xiana.websockets.router-helpers.html
@@ -0,0 +1,12 @@
+
+xiana.websockets.router-helpers documentation