├── .clj-kondo └── config.edn ├── .cljstyle ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── clojars_deploy.clj └── workflows │ ├── clj-kondo.yml │ ├── deploy.yml │ ├── examples_test.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── README.md ├── config ├── dev │ └── config.edn ├── local │ └── config.edn ├── prod │ └── config.edn └── test │ └── config.edn ├── deps.edn ├── doc ├── Development-Guide.md ├── How-To.md ├── contribution.md ├── conventions.md ├── decisions │ ├── 0001-record-architecture-decisions.md │ ├── 0002-database-basic-architecture.md │ └── 0003-controller-architecture.md ├── getting-started.md ├── interceptors.md ├── intro.md ├── jwt.md ├── modules.md ├── swagger.md └── tutorials.md ├── docker ├── docker-compose.yml ├── postgres.dockerfile └── sql-scripts │ ├── init.sql │ └── test.sql ├── docs ├── 0.3.0 │ ├── Development-Guide.html │ ├── Getting-Started.html │ ├── How-To.html │ ├── css │ │ ├── default.css │ │ ├── highlight.css │ │ └── xiana.css │ ├── framework.acl.builder.builder-functions.html │ ├── framework.acl.builder.html │ ├── framework.acl.builder.permissions.html │ ├── framework.acl.builder.roles.html │ ├── framework.acl.core-functions.html │ ├── framework.acl.core.html │ ├── framework.app.view.css.tailwind.core.html │ ├── framework.app.view.css.tailwind.helpers.html │ ├── framework.app.view.css.tailwind.preparers.html │ ├── framework.app.view.css.tailwind.resolvers.html │ ├── framework.auth.hash.html │ ├── framework.coercion.core.html │ ├── framework.config.core.html │ ├── framework.cookies.core.html │ ├── framework.db.core.html │ ├── framework.db.main.html │ ├── framework.handler.core.html │ ├── framework.interceptor.core.html │ ├── framework.interceptor.muuntaja.html │ ├── framework.interceptor.queue.html │ ├── framework.interceptor.wrap.html │ ├── framework.mail.core.html │ ├── framework.rbac.core.html │ ├── framework.route.core.html │ ├── framework.route.helpers.html │ ├── framework.scheduler.core.html │ ├── framework.session.core.html │ ├── framework.sse.core.html │ ├── framework.state.core.html │ ├── framework.webserver.core.html │ ├── framework.websockets.core.html │ ├── index.html │ ├── js │ │ ├── highlight.min.js │ │ ├── jquery.min.js │ │ └── page_effects.js │ ├── xiana.commons.html │ └── xiana.core.html └── index.html ├── example-tests.sh ├── examples ├── .gitignore ├── acl │ ├── .gitignore │ ├── README.md │ ├── config │ │ ├── dev │ │ │ └── config.edn │ │ └── test │ │ │ └── config.edn │ ├── docker-compose.yml │ ├── project.clj │ ├── resources │ │ ├── migrations │ │ │ ├── 20210322112622-create-uuid-extension.down.sql │ │ │ ├── 20210322112622-create-uuid-extension.up.sql │ │ │ ├── 20210322112732-users-table.down.sql │ │ │ ├── 20210322112732-users-table.up.sql │ │ │ ├── 20210322112828-posts-table.down.sql │ │ │ ├── 20210322112828-posts-table.up.sql │ │ │ ├── 20210322112927-comments-table.down.sql │ │ │ ├── 20210322112927-comments-table.up.sql │ │ │ ├── 20210322113010-add-test-users.down.sql │ │ │ └── 20210322113010-add-test-users.up.sql │ │ └── public │ │ │ └── index.html │ ├── src │ │ ├── backend │ │ │ ├── acl.clj │ │ │ └── app │ │ │ │ ├── controller_behaviors │ │ │ │ └── .gitkeep │ │ │ │ ├── controllers │ │ │ │ ├── comments.clj │ │ │ │ ├── index.clj │ │ │ │ ├── posts.clj │ │ │ │ ├── re_frame.clj │ │ │ │ └── users.clj │ │ │ │ ├── db_migrations │ │ │ │ └── .gitkeep │ │ │ │ ├── interceptors │ │ │ │ ├── .gitkeep │ │ │ │ └── load_user.clj │ │ │ │ ├── models │ │ │ │ ├── .gitkeep │ │ │ │ ├── comments.clj │ │ │ │ ├── data_ownership.clj │ │ │ │ ├── posts.clj │ │ │ │ └── users.clj │ │ │ │ └── views │ │ │ │ ├── comments.clj │ │ │ │ ├── common.clj │ │ │ │ ├── layouts │ │ │ │ └── .gitkeep │ │ │ │ ├── posts.clj │ │ │ │ └── users.clj │ │ ├── frontend │ │ │ ├── acl │ │ │ │ ├── config.cljs │ │ │ │ ├── core.cljs │ │ │ │ ├── db.cljs │ │ │ │ ├── events.cljs │ │ │ │ ├── subs.cljs │ │ │ │ └── views.cljs │ │ │ └── deps.cljs │ │ └── shared │ │ │ ├── config.clj │ │ │ └── schema.clj │ └── test │ │ ├── acl_fixture.clj │ │ ├── comments_test.clj │ │ ├── helpers.clj │ │ ├── post_helpers.clj │ │ ├── posts_test.clj │ │ └── users_test.clj ├── cli-chat │ ├── .gitignore │ ├── README.md │ ├── config │ │ ├── dev │ │ │ └── config.edn │ │ └── test │ │ │ └── config.edn │ ├── docker-compose.yml │ ├── project.clj │ ├── resources │ │ ├── migrations │ │ │ ├── 20211029080516-users.down.sql │ │ │ └── 20211029080516-users.up.sql │ │ └── public │ │ │ └── index.html │ ├── src │ │ ├── backend │ │ │ └── cli_chat │ │ │ │ ├── controller_behaviors │ │ │ │ ├── .gitkeep │ │ │ │ └── chat.clj │ │ │ │ ├── controllers │ │ │ │ ├── chat.clj │ │ │ │ ├── index.clj │ │ │ │ └── re_frame.clj │ │ │ │ ├── core.clj │ │ │ │ ├── db_migrations │ │ │ │ └── .gitkeep │ │ │ │ ├── interceptors.clj │ │ │ │ ├── interceptors │ │ │ │ └── .gitkeep │ │ │ │ ├── models │ │ │ │ ├── .gitkeep │ │ │ │ └── users.clj │ │ │ │ └── views │ │ │ │ ├── chat.clj │ │ │ │ ├── common.clj │ │ │ │ └── layouts │ │ │ │ └── .gitkeep │ │ └── frontend │ │ │ ├── cli_chat │ │ │ ├── config.cljs │ │ │ ├── core.cljs │ │ │ ├── db.cljs │ │ │ ├── events.cljs │ │ │ ├── subs.cljs │ │ │ └── views.cljs │ │ │ └── deps.cljs │ └── test │ │ ├── cli_chat_fixture.clj │ │ └── cli_chat_test.clj ├── controllers │ ├── .gitignore │ ├── README.md │ ├── config │ │ ├── dev │ │ │ └── config.edn │ │ └── test │ │ │ └── config.edn │ ├── dev │ │ ├── state.clj │ │ └── user.clj │ ├── docker-compose.yml │ ├── project.clj │ ├── resources │ │ ├── migrations │ │ │ ├── 20210205095919-add-auth-tables.down.sql │ │ │ └── 20210205095919-add-auth-tables.up.sql │ │ └── public │ │ │ └── index.html │ ├── src │ │ ├── backend │ │ │ └── app │ │ │ │ ├── controllers │ │ │ │ ├── index.clj │ │ │ │ └── re_frame.clj │ │ │ │ ├── core.clj │ │ │ │ ├── interceptors.clj │ │ │ │ ├── my_domain_logic │ │ │ │ └── siege_machines.clj │ │ │ │ └── route.clj │ │ └── frontend │ │ │ ├── controllers │ │ │ ├── config.cljs │ │ │ ├── core.cljs │ │ │ ├── db.cljs │ │ │ ├── events.cljs │ │ │ ├── subs.cljs │ │ │ └── views.cljs │ │ │ └── deps.cljs │ └── test │ │ └── core_test.clj ├── frames │ ├── .gitignore │ ├── README.md │ ├── config │ │ ├── dev │ │ │ └── config.edn │ │ └── test │ │ │ └── config.edn │ ├── dev │ │ └── cljs │ │ │ └── user.cljs │ ├── docker-compose.yml │ ├── project.clj │ ├── resources │ │ └── public │ │ │ ├── assets │ │ │ └── Clojure-icon.png │ │ │ └── index.html │ ├── src │ │ ├── backend │ │ │ └── frames │ │ │ │ ├── controllers │ │ │ │ ├── index.clj │ │ │ │ └── status.clj │ │ │ │ └── core.clj │ │ └── frontend │ │ │ ├── deps.cljs │ │ │ └── donor │ │ │ ├── config.cljs │ │ │ ├── core.cljs │ │ │ ├── db.cljs │ │ │ ├── events.cljs │ │ │ ├── subs.cljs │ │ │ └── views.cljs │ └── test │ │ ├── asset_test.clj │ │ └── status_test.clj ├── jwt │ ├── .gitignore │ ├── Docker │ │ ├── db.Dockerfile │ │ └── init.sql │ ├── README.md │ ├── config │ │ ├── dev │ │ │ └── config.edn │ │ └── test │ │ │ └── config.edn │ ├── deps.edn │ ├── resources │ │ ├── _files │ │ │ ├── jwtRS256.key │ │ │ └── jwtRS256.key.pub │ │ └── public │ │ │ └── index.html │ ├── src │ │ └── backend │ │ │ └── app │ │ │ ├── controllers │ │ │ ├── index.clj │ │ │ ├── login.clj │ │ │ └── secret.clj │ │ │ └── core.clj │ └── test │ │ ├── app │ │ └── controllers │ │ │ ├── login_test.clj │ │ │ └── secret_test.clj │ │ ├── integration_test.clj │ │ └── jwt_fixture.clj ├── sessions │ ├── .gitignore │ ├── Docker │ │ ├── db.Dockerfile │ │ └── init.sql │ ├── README.md │ ├── config │ │ ├── dev │ │ │ └── config.edn │ │ └── test │ │ │ └── config.edn │ ├── docker-compose.yml │ ├── init.sql │ ├── postgres-start.sh │ ├── project.clj │ ├── resources │ │ ├── migrations │ │ │ ├── 20210205095919-add-auth-tables.down.sql │ │ │ └── 20210205095919-add-auth-tables.up.sql │ │ └── public │ │ │ └── index.html │ ├── src │ │ ├── backend │ │ │ └── app │ │ │ │ ├── controllers │ │ │ │ ├── index.clj │ │ │ │ ├── login.clj │ │ │ │ ├── logout.clj │ │ │ │ └── secret.clj │ │ │ │ ├── core.clj │ │ │ │ └── interceptors.clj │ │ └── frontend │ │ │ └── controllers │ │ │ ├── config.cljs │ │ │ ├── core.cljs │ │ │ ├── db.cljs │ │ │ ├── events.cljs │ │ │ ├── subs.cljs │ │ │ └── views.cljs │ └── test │ │ └── integration_test.clj └── state-events │ ├── .gitignore │ ├── README.md │ ├── config │ ├── dev │ │ └── config.edn │ └── test │ │ └── config.edn │ ├── dev │ ├── state.clj │ └── user.clj │ ├── docker-compose.yml │ ├── project.clj │ ├── resources │ ├── migrations │ │ ├── 20211118093611-events.down.sql │ │ └── 20211118093611-events.up.sql │ └── public │ │ ├── assets │ │ └── favicon.ico │ │ └── index.html │ ├── src │ ├── backend │ │ └── state_events │ │ │ ├── controller_behaviors │ │ │ ├── .gitkeep │ │ │ └── sse.clj │ │ │ ├── controllers │ │ │ ├── event.clj │ │ │ ├── index.clj │ │ │ └── re_frame.clj │ │ │ ├── core.clj │ │ │ ├── interceptors.clj │ │ │ ├── interceptors │ │ │ ├── .gitkeep │ │ │ └── event_process.clj │ │ │ ├── models │ │ │ ├── .gitkeep │ │ │ └── event.clj │ │ │ └── views │ │ │ ├── event.clj │ │ │ └── layouts │ │ │ └── .gitkeep │ ├── frontend │ │ └── state_events │ │ │ ├── config.cljs │ │ │ ├── core.cljs │ │ │ ├── db.cljs │ │ │ ├── effects.cljs │ │ │ ├── events.cljs │ │ │ ├── subs.cljs │ │ │ ├── views.cljs │ │ │ └── web_sockets.cljs │ └── shared │ │ ├── config.clj │ │ └── schema.clj │ └── test │ ├── integration │ └── state_events │ │ └── integration_test.clj │ ├── state_events_fixture.clj │ └── unit │ └── state_events │ └── event_process_test.clj ├── log └── .gitkeep ├── release.edn ├── resources ├── codox │ └── theme │ │ └── xiana │ │ ├── css │ │ └── xiana.css │ │ └── theme.edn ├── images │ ├── .gitkeep │ ├── Xiana.png │ ├── around-and-inside.png │ ├── around.png │ ├── flow.png │ ├── inside.png │ ├── override.png │ └── success.png └── javascript │ └── .gitkeep ├── script ├── .gitkeep ├── auto.sh ├── build-docs.sh ├── postgres-start.sh ├── project-version └── template-index.html ├── src └── xiana │ ├── coercion.clj │ ├── commons.clj │ ├── config.clj │ ├── cookies.clj │ ├── db.clj │ ├── db │ └── migrate.clj │ ├── handler.clj │ ├── hash.clj │ ├── interceptor.clj │ ├── interceptor │ ├── cors.clj │ ├── error.clj │ ├── kebab_camel.clj │ ├── muuntaja.clj │ ├── queue.clj │ └── wrap.clj │ ├── jwt.clj │ ├── jwt │ ├── action.clj │ └── interceptors.clj │ ├── logging.clj │ ├── mail.clj │ ├── rbac.clj │ ├── route.clj │ ├── route │ └── helpers.clj │ ├── scheduler.clj │ ├── session.clj │ ├── sse.clj │ ├── state.clj │ ├── swagger.clj │ ├── webserver.clj │ └── websockets.clj ├── test ├── resources │ ├── _files │ │ ├── jwtRS256.key │ │ └── jwtRS256.key.pub │ ├── init.sql │ └── multipart.csv ├── xiana │ ├── commons_test.clj │ ├── config_test.clj │ ├── hash_test.clj │ ├── interceptor │ │ ├── cors_test.clj │ │ ├── error_test.clj │ │ ├── kebab_camel_test.clj │ │ ├── multipart_test.clj │ │ ├── muuntaja_test.clj │ │ ├── queue_test.clj │ │ └── wrap_test.clj │ ├── interceptor_test.clj │ ├── jwt │ │ └── interceptors_test.clj │ ├── jwt_test.clj │ ├── rbac │ │ ├── integration_test.clj │ │ └── interceptor_test.clj │ ├── route │ │ └── helpers_test.clj │ ├── route_test.clj │ ├── session_test.clj │ ├── state_test.clj │ ├── swagger_test.clj │ ├── web_socket │ │ ├── integration_test.clj │ │ └── router_test.clj │ └── webserver_test.clj └── xiana_fixture.clj └── tests.edn /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {xiana.core/flow-> clojure.core/-> 2 | com.wsscode.pathom3.connect.operation/defresolver clojure.core/def} 3 | :linters 4 | {:unused-namespace {:level :error} 5 | :unresolved-var {:exclude [honeysql.helpers 6 | honeysql-postgres.helpers 7 | honeysql.core]} 8 | 9 | :consistent-alias 10 | {:level :error 11 | :aliases {taoensso.timbre log 12 | xiana.core xiana}}}} 13 | -------------------------------------------------------------------------------- /.cljstyle: -------------------------------------------------------------------------------- 1 | {:files {:ignore #{"checkouts" "target"} 2 | :extensions #{"clj" "edn"}} 3 | :rules {:whitespace {:remove-surrounding? true 4 | :remove-trailing? true 5 | :insert-missing? true} 6 | :blank-lines {:padding-lines 1 7 | :max-consecutive 1 8 | :insert-padding true 9 | :trim-consecutive? true} 10 | :types {:enabled? false} 11 | :functions {:enabled? false} 12 | :eof-new-line {:enabled? true} 13 | :namespaces {:enabled? true 14 | :break-libs true 15 | :indent-size 2}}} 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Title: 2 | 3 | ### Description 4 | A brief summary of the changes including a description of the new behaviour or the fixed bug. 5 | 6 | ### Tester info 7 | A brief description of any steps needed to demonstrate the new functionality or show the bug is fixed 8 | 9 | ### Completion Checklist 10 | 11 | - [ ] Add description of the changes made here to the changelog file 12 | - [ ] Update the documentation as necessary 13 | -------------------------------------------------------------------------------- /.github/clojars_deploy.clj: -------------------------------------------------------------------------------- 1 | ;; based on: https://github.com/mauricioszabo/clj-lib-deployer/blob/master/deploy-lein.bb 2 | ;; updated for clj-tools: g-krisztian 3 | 4 | (def release 5 | (edn/read-string (slurp "release.edn"))) 6 | 7 | (println release) 8 | 9 | (def project 10 | (format "%s/%s" (:group-id release) (:artifact-id release))) 11 | 12 | (println "Project: " project) 13 | 14 | (def version 15 | (:version release)) 16 | 17 | (println "Version: " version) 18 | 19 | (defn- can-deploy? [] 20 | (let [status (:status (curl/get (str "https://clojars.org/" project 21 | "/versions/" version) 22 | {:throw false}))] 23 | (= 404 status))) 24 | 25 | (defn- tag-name [] (System/getenv "TAG")) 26 | 27 | (defn- decode-base64 [string] 28 | (-> java.util.Base64 29 | .getDecoder 30 | (.decode string))) 31 | 32 | (defn run-shell-cmd [& args] 33 | (let [{:keys [exit out err] :as result} (apply shell/sh args)] 34 | (when-not (zero? exit) 35 | (println "ERROR running command\nSTDOUT:") 36 | (println out "\nSTDERR:") 37 | (println err) 38 | (throw (ex-info "Error while running shell command" {:status exit}))) 39 | result)) 40 | 41 | (defn- import-gpg! [] 42 | (let [secret (System/getenv "GPG_SECRET_KEYS") 43 | ownertrust (System/getenv "GPG_OWNERTRUST")] 44 | (when-not (and secret ownertrust) (throw (ex-info "Can't find GPG keys!" {}))) 45 | (run-shell-cmd "gpg" "--import" :in (decode-base64 secret)) 46 | (run-shell-cmd "gpg" "--import-ownertrust" :in (decode-base64 ownertrust)))) 47 | 48 | (defn deploy! [] 49 | (let [tag (not-empty (tag-name))] 50 | (when-not (can-deploy?) 51 | (throw (ex-info "Can't deploy this version - release version already exist on clojars" 52 | {:version version}))) 53 | 54 | (when (some-> tag (str/replace-first #"v" "") (not= version)) 55 | (throw (ex-info "Tag version mismatches with release.edn" 56 | {:tag-name tag 57 | :version version}))) 58 | 59 | (when tag 60 | (import-gpg!) 61 | (println "Deploying a release version") 62 | 63 | (run-shell-cmd "clojure" "-M:release" "--version" version) 64 | (println "Deploy was successful")))) 65 | 66 | (deploy!) 67 | -------------------------------------------------------------------------------- /.github/workflows/clj-kondo.yml: -------------------------------------------------------------------------------- 1 | name: clj-kondo checks 2 | 3 | on: [push] 4 | 5 | jobs: 6 | self-lint: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: DeLaGuardo/clojure-lint-action@master 13 | with: 14 | clj-kondo-args: --lint src # TODO: lint tests, examples 15 | github_token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Push to Clojars 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | push-to-clojars: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Setup Clojure 16 | uses: DeLaGuardo/setup-clojure@5.0 17 | with: 18 | cli: 1.11.1.1113 19 | lein: 2.9.8 20 | 21 | - name: Cache clojure dependencies 22 | uses: actions/cache@v3 23 | with: 24 | path: | 25 | ~/.m2/repository 26 | ~/.gitlibs 27 | ~/.deps.clj 28 | # List all files containing dependencies: 29 | key: cljdeps-${{ hashFiles('deps.edn') }} 30 | # key: cljdeps-${{ hashFiles('deps.edn', 'bb.edn') }} 31 | # key: cljdeps-${{ hashFiles('project.clj') }} 32 | # key: cljdeps-${{ hashFiles('build.boot') }} 33 | restore-keys: cljdeps- 34 | 35 | - name: Setup babashka 36 | run: | 37 | curl -L https://github.com/borkdude/babashka/releases/download/v0.2.5/babashka-0.2.5-linux-amd64.zip -o bb.zip 38 | unzip bb.zip 39 | chmod +x bb 40 | sudo mv bb /usr/bin 41 | 42 | - name: Set TAG env variable 43 | run: echo "TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 44 | 45 | - name: Deploy to Clojars 46 | env: 47 | CLOJARS_LOGIN: ${{ secrets.CLOJARS_LOGIN }} 48 | CLOJARS_USERNAME: ${{ secrets.CLOJARS_LOGIN }} 49 | CLOJARS_PASSWORD: ${{ secrets.CLOJARS_PASSWORD }} 50 | GPG_OWNERTRUST: ${{ secrets.GPG_OWNERTRUST }} 51 | GPG_SECRET_KEYS: ${{ secrets.GPG_SECRET_KEYS }} 52 | TAG: ${{ env.TAG }} 53 | run: /usr/bin/bb ./.github/clojars_deploy.clj 54 | -------------------------------------------------------------------------------- /.github/workflows/examples_test.yml: -------------------------------------------------------------------------------- 1 | name: Clojure CI 2 | 3 | on: 4 | push: 5 | branches: '!main' 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | check-examples: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Prepare java 17 | uses: actions/setup-java@v3 18 | with: 19 | distribution: 'corretto' 20 | java-version: '17' 21 | 22 | - name: Install clojure tools 23 | uses: DeLaGuardo/setup-clojure@9.4 24 | with: 25 | # Install just one or all simultaneously 26 | # The value must indicate a particular version of the tool, or use 'latest' 27 | # to always provision the latest version 28 | cli: 1.11.1.1155 # Clojure CLI based on tools.deps 29 | lein: 2.9.1 # Leiningen 30 | bb: 0.7.8 # Babashka 31 | 32 | - name: Cache clojure dependencies 33 | uses: actions/cache@v3 34 | with: 35 | path: | 36 | ~/.m2/repository 37 | ~/.gitlibs 38 | ~/.deps.clj 39 | # List all files containing dependencies: 40 | key: cljdeps-${{ hashFiles('deps.edn') }} 41 | # key: cljdeps-${{ hashFiles('deps.edn', 'bb.edn') }} 42 | # key: cljdeps-${{ hashFiles('project.clj') }} 43 | # key: cljdeps-${{ hashFiles('build.boot') }} 44 | restore-keys: cljdeps- 45 | 46 | - name: Checking examples 47 | run: ./example-tests.sh 48 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Clojure CI 2 | 3 | on: 4 | push: 5 | branches: '!main' 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Prepare java 17 | uses: actions/setup-java@v3 18 | with: 19 | distribution: 'corretto' 20 | java-version: '17' 21 | 22 | - name: Install clojure tools 23 | uses: DeLaGuardo/setup-clojure@9.4 24 | with: 25 | # Install just one or all simultaneously 26 | # The value must indicate a particular version of the tool, or use 'latest' 27 | # to always provision the latest version 28 | cli: 1.11.1.1155 # Clojure CLI based on tools.deps 29 | lein: 2.9.1 # Leiningen 30 | bb: 0.7.8 # Babashka 31 | 32 | - name: Cache clojure dependencies 33 | uses: actions/cache@v3 34 | with: 35 | path: | 36 | ~/.m2/repository 37 | ~/.gitlibs 38 | ~/.deps.clj 39 | # List all files containing dependencies: 40 | key: cljdeps-${{ hashFiles('deps.edn') }} 41 | # key: cljdeps-${{ hashFiles('deps.edn', 'bb.edn') }} 42 | # key: cljdeps-${{ hashFiles('project.clj') }} 43 | # key: cljdeps-${{ hashFiles('build.boot') }} 44 | restore-keys: cljdeps- 45 | 46 | - name: Run cljstyle 47 | run: clojure -M:format check 48 | 49 | - name: Run kondo 50 | run: clojure -M:kondo 51 | 52 | - name: Run kibit 53 | continue-on-error: true 54 | run: clojure -M:kibit -- --reporter markdown 55 | 56 | - name: Run tests 57 | run: clojure -M:test 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cmake build folder 2 | build 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | *.core 10 | *.data 11 | 12 | # Precompiled Headers 13 | *.gch 14 | *.pch 15 | 16 | # Compiled Dynamic libraries 17 | *.so 18 | *.dylib 19 | *.dll 20 | 21 | # Fortran module files 22 | *.mod 23 | *.smod 24 | 25 | # Compiled Static libraries 26 | *.lai 27 | *.la 28 | *.a 29 | *.lib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | 36 | # python precompiled files 37 | *.pyc 38 | 39 | # python dirs 40 | venv 41 | dist 42 | htmlcov 43 | instance 44 | .pytest_cache 45 | .coverage 46 | *.egg-info 47 | __pycache__ 48 | 49 | # OS generated files 50 | .DS_Store* 51 | ehthumbs.db 52 | Icon? 53 | Thumbs.db 54 | *.lock 55 | package-lock.json 56 | 57 | # Emacs 58 | *~ 59 | \#*\# 60 | /.emacs.desktop 61 | /.emacs.desktop.lock 62 | *.elc 63 | auto-save-list 64 | tramp 65 | .\#* 66 | 67 | # Etags 68 | TAGS 69 | 70 | # Gtags 71 | GPATH 72 | GRTAGS 73 | GTAGS 74 | 75 | # Org-mode 76 | #*.org 77 | .org-id-locations 78 | *_archive 79 | 80 | # flymake-mode 81 | *_flymake.* 82 | 83 | # reftex files 84 | *.rel 85 | 86 | # projectile 87 | .projectile 88 | 89 | # dir-locals 90 | .dir-locals.el 91 | 92 | # editorconfig 93 | .editorconfig 94 | 95 | # Dirs 96 | .backup 97 | 98 | # Autoloads 99 | *-autoloads.el 100 | 101 | # Clojure 102 | /target 103 | /classes 104 | /checkouts 105 | profiles.clj 106 | pom.xml 107 | pom.xml.asc 108 | *.jar 109 | *.class 110 | /.lein-* 111 | /.nrepl-port 112 | /.dir-locals.el 113 | /profiles.clj 114 | /dev/resources/local.edn 115 | /dev/src/local.clj 116 | .eastwood 117 | .idea/ 118 | .cpcache/ 119 | .lsp/ 120 | !/.lein-git-deps/ 121 | .clj-kondo/* 122 | !.clj-kondo/config.edn 123 | *.iml 124 | doc/swagger.org 125 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ## 0.5.0-rc6 4 | 5 | - kebab camel interceptor for all request type #263 6 | - dev/reset migratus #264 7 | - Add swagger documentation #266 8 | - Feature/swagger toplevel description #268 9 | - Bump hiccup/hiccup to version 2.0.0-RC2 #269 10 | 11 | ## 0.5.0-rc5 12 | 13 | - Bump info.sunng/ring-jetty9-adapter ring adapter library 14 | - Refactor swagger 15 | 16 | ## 0.5.0-rc4 17 | 18 | - added swagger-ui 19 | - Fix `xiana.db/in-transaction` to support any `java.sql.Connection` 20 | - Add support for custom datasource 21 | - JWT signing support for authentication and content exchange via interceptors. 22 | 23 | ## 0.5.0-rc3 24 | 25 | - Use clj-tools for 26 | - test 27 | - build 28 | - install 29 | - release 30 | - Remove `project.clj` 31 | - Fix kibit complaints 32 | - Added new `prune-get-request-bodies` interceptor 33 | 34 | Breaking changes: 35 | 36 | - refactor database migration system, use patched migratus (Flexiana/migratus), removed seed module and rewrite data 37 | migration tool 38 | 39 | ## 0.5-rc2 40 | 41 | Breaking changes: 42 | 43 | - removed monands (`funcool.cats` library), related code and docs 44 | removed `xiana.interceptor.wrap/interceptor` 45 | - changed interceptors behaviour making them similar to pedestal and sieppari 46 | - changed error handling 47 | - addded deps.edn 48 | - a bunch of other small fixes. 49 | 50 | ## 0.5-rc1 51 | 52 | Breaking change: rename almost all namespaces from `framework..` to `xiana..`). 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Xiana logo](resources/images/Xiana.png) 2 | # Xiana framework 3 | 4 | Xiana is a lightweight web-application framework written in Clojure, for Clojure. The goal is to be simple, fast, and 5 | most importantly - a welcoming platform for web programmers with different backgrounds who want to experience the wonders 6 | of functional programming! 7 | 8 | It's easy to install, fun to experiment with, and a powerful tool to produce monolithic web applications. 9 | 10 | ## Installation 11 | 12 | ### From template 13 | 14 | Xiana has its own Leiningen template, so you can create a skeleton project with 15 | 16 | ```shell 17 | lein new xiana app 18 | ``` 19 | 20 | It also has a deps.edn template. Instructions for using it are [here](https://github.com/Flexiana/templates) 21 | 22 | [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. 23 | 24 | ### As a dependency 25 | 26 | Add it to your project as a dependency from clojars: 27 | 28 | [![Clojars Project](https://img.shields.io/clojars/v/com.flexiana/framework.svg)](https://clojars.org/com.flexiana/framework) 29 | 30 | ## Docs 31 | 32 | - First check out the [conventions](./doc/conventions.md). 33 | - To start working with xiana, read the [tutorials](./doc/tutorials.md). 34 | - A hands-on approach in the [how-to](./doc/How-To.md)s. 35 | - Check the available [modules](./doc/modules.md), and [interceptors](./doc/interceptors.md). 36 | - To contribute, see the [contribution](./doc/contribution.md) docs. 37 | 38 | ### Examples 39 | 40 | Visit [examples folder](examples), to see how you can perform 41 | 42 | - [Access and data ownership control](examples/acl/README.md) 43 | - [Request coercion and response validation](examples/controllers/README.md) 44 | - [Session handling with varying interceptors](examples/sessions/README.md) 45 | - [Chat platform with WebSocket](examples/cli-chat/README.md) 46 | - [Event based resource handling](examples/state-events/README.md) 47 | 48 | ## References 49 | 50 | ### Concept of interceptors 51 | * http://pedestal.io/reference/interceptors 52 | * https://github.com/metosin/sieppari 53 | -------------------------------------------------------------------------------- /config/dev/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/postgresql {:port 5432 2 | :dbname "framework" 3 | :host "localhost" 4 | :dbtype "postgresql" 5 | :user "postgres" 6 | :password "postgres"} 7 | 8 | :xiana/hikari-pool-params {:auto-commit true 9 | :read-only false 10 | :connection-timeout 30000 11 | :validation-timeout 5000 12 | :idle-timeout 600000 13 | :max-lifetime 1800000 14 | :minimum-idle 10 15 | :maximum-pool-size 10 16 | :pool-name "db-pool" 17 | :register-mbeans false} 18 | 19 | :xiana/web-server {:port 3000 20 | :join? false} 21 | :xiana/swagger {:uri-path "/swagger/swagger.json" 22 | :path :swagger.json 23 | :data {:coercion (reitit.coercion.malli/create 24 | {:error-keys #{:coercion :in :schema :value :errors :humanized} 25 | :compile malli.util/closed-schema 26 | :strip-extra-keys true 27 | :default-values true 28 | :options nil}) 29 | :middleware [reitit.swagger/swagger-feature]}} 30 | :xiana/swagger-ui {:uri-path "/swagger/swagger-ui"} 31 | :xiana/migration {:store :database 32 | :migration-dir "resources/migrations" 33 | :init-in-transaction? false 34 | :migration-table-name "migrations"} 35 | :xiana/emails {:host "" 36 | :user "" 37 | :pass "" 38 | :tls true 39 | :port 587 40 | :from ""} 41 | :xiana/auth {:hash-algorithm :bcrypt ; Available values: :bcrypt, :scrypt, and :pbkdf2 42 | :bcrypt-settings {:work-factor 11} 43 | :scrypt-settings {:cpu-cost 32768 ; Must be a power of 2 44 | :memory-cost 8 45 | :parallelization 1} 46 | :pbkdf2-settings {:type :sha1 ; Available values: :sha1 and :sha256 47 | :iterations 100000}}} 48 | -------------------------------------------------------------------------------- /config/local/config.edn: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /config/prod/config.edn: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /config/test/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/postgresql {:image-name "postgres:14-alpine" 2 | :port 5432 3 | :dbname "framework" 4 | :host "localhost" 5 | :dbtype "postgresql" 6 | :user "postgres" 7 | :password "postgres"} 8 | 9 | 10 | :xiana/hikari-pool-params {:auto-commit true 11 | :read-only false 12 | :connection-timeout 30000 13 | :validation-timeout 5000 14 | :idle-timeout 600000 15 | :max-lifetime 1800000 16 | :minimum-idle 10 17 | :maximum-pool-size 10 18 | :pool-name "db-pool" 19 | :register-mbeans false} 20 | 21 | :xiana/web-server {:port 3333 22 | :join? false} 23 | :xiana/migration {:store :database 24 | :migration-dir "resources/migrations" 25 | :init-in-transaction? false 26 | :migration-table-name "migrations"} 27 | :xiana/emails {:host "" 28 | :user "" 29 | :pass "" 30 | :tls true 31 | :port 587 32 | :from ""} 33 | :xiana/auth {:hash-algorithm :bcrypt ; Available values: :bcrypt, :scrypt, and :pbkdf2 34 | :bcrypt-settings {:work-factor 11} 35 | :scrypt-settings {:cpu-cost 32768 ; Must be a power of 2 36 | :memory-cost 8 37 | :parallelization 1} 38 | :pbkdf2-settings {:type :sha1 ; Available values: :sha1 and :sha256 39 | :iterations 100000}} 40 | :xiana/test {:test-value-1 "$property" 41 | :test-value-2 "$something-else | default"}} 42 | -------------------------------------------------------------------------------- /doc/Development-Guide.md: -------------------------------------------------------------------------------- 1 | # Development guide 2 | TODO fill this with everything a developer starting to work on Xiana might need to know 3 | 4 | * Ticket System 5 | * Coding standards 6 | * Submitting a PR 7 | * Making a release 8 | 9 | 10 | ## Generating API Docs 11 | 12 | This is done automatically using *Codox* 13 | 14 | To generate or update the current version run the script: 15 | 16 | ```shell 17 | script/build-docs.sh 18 | ``` 19 | 20 | This runs the following: 21 | 22 | ```shell 23 | clj -X:codox 24 | mv docs/new docs/{{version-number}} 25 | ``` 26 | 27 | It also updates the index.html file to point to the new version. 28 | -------------------------------------------------------------------------------- /doc/conventions.md: -------------------------------------------------------------------------------- 1 | # Conventions 2 | 3 | - [State](#state) 4 | - [Action](#action) 5 | - [Handler](#handler) 6 | - [Dependencies](#dependencies) 7 | - [Interceptors](#interceptors) 8 | 9 | ## State 10 | 11 | A state record. It is created for each HTTP request and represents the current state of the application. It contains: 12 | 13 | - the application's dependencies 14 | - request 15 | - request-data 16 | - response 17 | 18 | This structure is very volatile, will be updated quite often on the application's life cycle. 19 | 20 | The main modules that update the state are: 21 | 22 | - Routes: 23 | 24 | Add information from the matched route to the state map 25 | 26 | - Interceptors: 27 | 28 | Add, consumes or remove information from the state map. More details in [Interceptors](#interceptors) section. 29 | 30 | - Actions: 31 | 32 | In actions, you are able to interfere with the :leave parts of the interceptors. 33 | 34 | At the last step of execution the handler extracts the response value from the state. 35 | 36 | The state is renewed on every request. 37 | 38 | ## Action 39 | 40 | The action conventionally is the control point of the application flow. This is the place were you can define how the 41 | rest of your execution flow would behave. Here you can provide the database query, restriction function, the view, and 42 | the additional side effect functions are you want to execute. 43 | 44 | Actions are defined in the routes vector 45 | 46 | ```clojure 47 | ["/" {:get {:action #(do something)}}] 48 | ``` 49 | 50 | ## Handler 51 | 52 | Xiana's handler does all the processing. It runs on every request and does the following. It creates the state for every 53 | request, matches the appropriate route, executes the interceptors, handles interceptor overrides, and not-found cases. 54 | It handles websocket requests too. 55 | 56 | ### Routing 57 | 58 | Routing means selecting the actions to execute depending on the request URL, and HTTP method. 59 | 60 | ## Dependencies 61 | 62 | Modules can depend on external resources, configurations, as well as on other modules. These dependencies are added to 63 | the state on state creation, and defined on application startup. 64 | 65 | ## Interceptors 66 | 67 | 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. 68 | Xiana provides a set of base [interceptors](interceptors.md), for the most common use cases. 69 | -------------------------------------------------------------------------------- /doc/decisions/0001-record-architecture-decisions.md: -------------------------------------------------------------------------------- 1 | # 1. Record architecture decisions 2 | 3 | Date: 2021-01-12 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to record the architectural decisions made on this project. 12 | 13 | ## Decision 14 | 15 | We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). 16 | 17 | ## Consequences 18 | 19 | See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). 20 | -------------------------------------------------------------------------------- /doc/decisions/0002-database-basic-architecture.md: -------------------------------------------------------------------------------- 1 | # 2. Database basic architecture 2 | 3 | Date: 2021-01-14 4 | 5 | ## Status 6 | 7 | Proposed 8 | 9 | ## Context 10 | 11 | 1. The framework rationale requires a separation of concern between "code 12 | is code, data is data" and because of that some libraries were not 13 | able to proceed to be used in the application. 14 | 15 | 2. The framework also wants to be a complete solution which means that 16 | our users should not be required to learn too many libraries in order 17 | to be productive. Instead, the framework should provide layers of 18 | indirection to wrap such functionalities and provide centralized 19 | control over the features. 20 | 21 | ## Decision 22 | 23 | - Usage of `yogthos/config` to handle config files 24 | - Usage of `stuartsierra/components` to handle dependency management 25 | - Usage of `honeysql` to handle SQL interactions 26 | - Build a framework layer to handle migrations in `honeysql` style 27 | - Build framework layer to wrap honeysql, honeysql-plugins, next-jdbc, 28 | hikari-cp and possible others lirbaries. 29 | 30 | ## Consequences 31 | 32 | - [Positive] Framework is more aligned with its goals 33 | - [Positive] Framework already have a `SQL` layer to be used 34 | - [Negative] Choosing to wrap underlying libraries adds the work to 35 | keep our library in sync with new features they release 36 | - [Negative] We were not able to use `integrant` as desired in the 37 | beginning because it violates the first rationale. 38 | - [Negative] We were not able to use `duct` as desired because it 39 | violates the first rationale 40 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to Xiana 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | 5 | 6 | 7 | ## Email configuration 8 | 9 | Xiana has support for sending email messages. 10 | For this functionality to work, a `:xiana/emails` key has to be added to the application 11 | config e.g. `config/dev/config.edn`. 12 | `:from` is a required key. `:host :user :pass :tls :port` are optional, if they are not present, 13 | the app will default to the mail sender present on the host, but you can use any SMTP of your 14 | choice. 15 | 16 | Full configuration looks like this 17 | 18 | ```clojure 19 | :xiana/emails {:host "smtp.some.mail.com" 20 | :user "john.user" 21 | :pass "secret@password" 22 | :tls true 23 | :port 587 24 | :from "System Admin "} 25 | ``` 26 | 27 | ## Hashing 28 | 29 | The module `xiana.hash` provides secure Bcrypt, Scrypt and Pbkdf2 hashing for storing user passwords. 30 | If you'd like to define some of them, their keys are: `:bcrypt :scrypt :pbkdf2`. If you don't define one of them, Bcrypt will be defined by default. You can setup the key within `config` derectory as well as its corresponding optional configuration. The values by default for each algorithm are defined as follows: 31 | 32 | ```clojure 33 | :xiana/auth {:hash-algorithm :bcrypt ;; Available values: :bcrypt, :scrypt, and :pbkdf2 34 | :bcrypt-settings {:work-factor 11} 35 | :scrypt-settings {:cpu-cost 32768 ;; Must be a power of 2 36 | :memory-cost 8 37 | :parallelization 1} 38 | :pbkdf2-settings {:type :sha1 ;; Available values: :sha1 and :sha256 39 | :iterations 100000}} 40 | ``` 41 | 42 | The module has `make` (it generate a hashed password) and `check` (it verifies the encrypted password against the current string) functions to deal with hashing process. 43 | 44 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | postgres: 5 | build: 6 | context: . 7 | dockerfile: postgres.dockerfile 8 | # environment: 9 | # POSTGRES_DB: framework 10 | volumes: 11 | - ./sql-scripts:/sql 12 | # - db-data:/var/lib/postgresql/data 13 | ports: 14 | - "5432:5432" 15 | 16 | # volumes: 17 | # db-data: 18 | -------------------------------------------------------------------------------- /docker/postgres.dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:11.5-alpine 2 | 3 | # COPY sql-scripts/init.sql /docker-entrypoint-initdb.d 4 | -------------------------------------------------------------------------------- /docker/sql-scripts/init.sql: -------------------------------------------------------------------------------- 1 | --;; 2 | DROP DATABASE IF EXISTS framework; 3 | 4 | --;; 5 | CREATE DATABASE framework; 6 | 7 | --;; 8 | GRANT ALL PRIVILEGES ON DATABASE framework TO postgres; 9 | 10 | --;; 11 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 12 | -------------------------------------------------------------------------------- /docker/sql-scripts/test.sql: -------------------------------------------------------------------------------- 1 | --;; 2 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 3 | 4 | --;; 5 | DROP TABLE IF EXISTS 6 | users 7 | CASCADE; 8 | 9 | --;; 10 | CREATE TABLE users 11 | ( 12 | id uuid DEFAULT uuid_generate_v4() PRIMARY KEY, 13 | created_at timestamptz NOT NULL DEFAULT now(), 14 | last_login timestamptz, 15 | is_active boolean, 16 | email varchar(254) NOT NULL, 17 | role varchar(254), 18 | username text NOT NULL, 19 | password text, 20 | salt text, 21 | fullname text 22 | ); 23 | 24 | --;; 25 | INSERT INTO users (id, created_at, email, role, password, username, is_active) 26 | VALUES ('fd5e0d70-506a-45cc-84d5-b12b5e3e99d2', 27 | '2021-03-30 12:34:10.358157+02', 'admin@frankie.sw', 'admin', 28 | '$2a$11$ivfRMKD7dHMfqCWBiEQcaOknsJgDnK9zoSP/cXAVNQVYHc.M9SZJK', 29 | 'admin', true), 30 | -- 31 | ('31c2c58f-28cb-4013-8765-9240626a18a2', 32 | '2021-03-30 12:34:10.358157+02', 'frankie@frankie.sw', 'user', 33 | '$2a$11$ivf2RMKD7dHMfqCWBiEQcaOknsJgDnK9zoSP/cXAVNQVYHc.M9SZJK', 34 | 'frankie', true), 35 | -- 36 | ('8d05b2e1-6463-478a-ba30-35768738af29', 37 | '2021-03-30 12:34:10.358157+02', 'impostor@frankie.sw', 'interviewer', 38 | '$2a$11$ivfRMKD7dHMfqCWBiEQcaOknsJgDnK9zoSP/cXAVNQVYHc.M9SZJK', 39 | 'impostor', false); 40 | -------------------------------------------------------------------------------- /docs/0.3.0/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | github.com style (c) Vasily Polovnyov 3 | */ 4 | 5 | .hljs { 6 | display: block; 7 | overflow-x: auto; 8 | padding: 0.5em; 9 | color: #333; 10 | background: #f8f8f8; 11 | } 12 | 13 | .hljs-comment, 14 | .hljs-quote { 15 | color: #998; 16 | font-style: italic; 17 | } 18 | 19 | .hljs-keyword, 20 | .hljs-selector-tag, 21 | .hljs-subst { 22 | color: #333; 23 | font-weight: bold; 24 | } 25 | 26 | .hljs-number, 27 | .hljs-literal, 28 | .hljs-variable, 29 | .hljs-template-variable, 30 | .hljs-tag .hljs-attr { 31 | color: #008080; 32 | } 33 | 34 | .hljs-string, 35 | .hljs-doctag { 36 | color: #d14; 37 | } 38 | 39 | .hljs-title, 40 | .hljs-section, 41 | .hljs-selector-id { 42 | color: #900; 43 | font-weight: bold; 44 | } 45 | 46 | .hljs-subst { 47 | font-weight: normal; 48 | } 49 | 50 | .hljs-type, 51 | .hljs-class .hljs-title { 52 | color: #458; 53 | font-weight: bold; 54 | } 55 | 56 | .hljs-tag, 57 | .hljs-name, 58 | .hljs-attribute { 59 | color: #000080; 60 | font-weight: normal; 61 | } 62 | 63 | .hljs-regexp, 64 | .hljs-link { 65 | color: #009926; 66 | } 67 | 68 | .hljs-symbol, 69 | .hljs-bullet { 70 | color: #990073; 71 | } 72 | 73 | .hljs-built_in, 74 | .hljs-builtin-name { 75 | color: #0086b3; 76 | } 77 | 78 | .hljs-meta { 79 | color: #999; 80 | font-weight: bold; 81 | } 82 | 83 | .hljs-deletion { 84 | background: #fdd; 85 | } 86 | 87 | .hljs-addition { 88 | background: #dfd; 89 | } 90 | 91 | .hljs-emphasis { 92 | font-style: italic; 93 | } 94 | 95 | .hljs-strong { 96 | font-weight: bold; 97 | } 98 | -------------------------------------------------------------------------------- /docs/0.3.0/css/xiana.css: -------------------------------------------------------------------------------- 1 | .sidebar.primary { 2 | background: #f8cc00; 3 | width: 200px; 4 | } 5 | 6 | .sidebar.primary::-webkit-scrollbar { 7 | display: none; 8 | } 9 | 10 | .logo { 11 | padding-top: 10px; 12 | margin:0px; 13 | } 14 | 15 | .logo img { 16 | height: 65px; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | Page Redirection 10 | 11 | 12 | If you are not redirected automatically, follow this link to the current api. 13 | 14 | 15 | -------------------------------------------------------------------------------- /example-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | clojure -M:install --version $(bb script/project-version) 3 | for f in ./examples/*; do 4 | if [ -d "$f" ]; then 5 | cd $f 6 | pwd 7 | if test -f project.clj; then 8 | lein check-style src test 9 | else 10 | clojure -M:format check 11 | fi 12 | status=$? 13 | if ! test $status -eq 0; then 14 | exit $status 15 | fi 16 | if test -f project.clj; then 17 | lein test 18 | else 19 | clojure -M:test 20 | fi 21 | status=$? 22 | if ! test $status -eq 0; then 23 | exit $status 24 | fi 25 | cd $OLDPWD 26 | fi 27 | done 28 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | */.shadow-cljs/ 2 | */node_modules/ 3 | */resources/public/js/ 4 | -------------------------------------------------------------------------------- /examples/acl/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /.prepl-port 12 | .hgignore 13 | .hg/ 14 | -------------------------------------------------------------------------------- /examples/acl/README.md: -------------------------------------------------------------------------------- 1 | # acl 2 | 3 | API implementation for ACL example 4 | 5 | ## Usage 6 | 7 | ### Prepare 8 | 9 | start postgres with docker-compose: 10 | 11 | docker compose up -d 12 | 13 | ### Run application 14 | 15 | lein run 16 | 17 | ### Run tests 18 | 19 | lein test 20 | 21 | 22 | 23 | ### Try controller 24 | 25 | #### test user-ids: 26 | 27 | |role |user-id | 28 | |----------------|--------------------------------------| 29 | |guest | Optional | 30 | |member | 611d7f8a-456d-4f3c-802d-4d869dcd89bf | 31 | |admin | b651939c-96e6-4fbb-88fb-299e728e21c8 | 32 | |suspended_admin | b01fae53-d742-4990-ac01-edadeb4f2e8f | 33 | |staff | 75c0d9b2-2c23-41a7-93a1-d1b716cdfa6c | 34 | 35 | #### Test actions: 36 | 37 | to select acting user, add `-H "Authorization: {{user-id}}"` to curl parameters 38 | 39 | get all posts: 40 | 41 | curl http://localhost:3000/posts 42 | 43 | get post by id: 44 | 45 | curl http://localhost:3000/posts?id={{post-id}} 46 | 47 | create new post: 48 | 49 | curl -X PUT -d 'content=something to post' http://localhost:3000/posts 50 | 51 | update existing post: 52 | 53 | curl -X POST -d 'content=Update post to this' http://localhost:3000/posts?id={{post-id}} 54 | 55 | delete post: 56 | 57 | curl -X DELETE http://localhost:3000/posts?id={{post-id}} 58 | 59 | -------------------------------------------------------------------------------- /examples/acl/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | db: 4 | image: postgres:14-alpine 5 | environment: 6 | - POSTGRES_DB=acl 7 | - POSTGRES_USER=postgres 8 | - POSTGRES_PASSWORD=postgres 9 | volumes: 10 | - db-data:/var/lib/postgresql/data 11 | ports: 12 | - "5433:5432" 13 | 14 | volumes: 15 | db-data: 16 | -------------------------------------------------------------------------------- /examples/acl/project.clj: -------------------------------------------------------------------------------- 1 | (defproject acl "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :min-lein-version "2.0.0" 4 | :dependencies [[com.flexiana/framework "0.5.0-rc4"]] 5 | :plugins [] 6 | :main ^:skip-aot acl 7 | :uberjar-name "acl.jar" 8 | :source-paths ["src/backend" "src/backend/app" "src/frontend" "src/shared"] 9 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 10 | :profiles {:dev {:resource-paths ["config/dev"] 11 | :dependencies [[binaryage/devtools "1.0.5"]]} 12 | :local {:resource-paths ["config/local"]} 13 | :prod {:resource-paths ["config/prod"]} 14 | :test {:resource-paths ["config/test"] 15 | :dependencies [[clj-http "3.12.3"] 16 | [mvxcvi/cljstyle "0.15.0" 17 | :exclusions [org.clojure/clojure]]]}} 18 | :aliases {"check-style" ["with-profile" "+test" "run" "-m" "cljstyle.main" "check"] 19 | "ci" ["do" "clean," "cloverage," "lint," "uberjar"] 20 | "kondo" ["run" "-m" "clj-kondo.main" "--lint" "src" "test"] 21 | "lint" ["do" "kondo," "eastwood," "kibit"]}) 22 | -------------------------------------------------------------------------------- /examples/acl/resources/migrations/20210322112622-create-uuid-extension.down.sql: -------------------------------------------------------------------------------- 1 | DROP EXTENSION IF EXISTS "uuid-ossp"; -------------------------------------------------------------------------------- /examples/acl/resources/migrations/20210322112622-create-uuid-extension.up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -------------------------------------------------------------------------------- /examples/acl/resources/migrations/20210322112732-users-table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE users; -------------------------------------------------------------------------------- /examples/acl/resources/migrations/20210322112732-users-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | password character varying(128) NOT NULL, 4 | last_login timestamp with time zone, 5 | is_superuser boolean NOT NULL, 6 | username character varying(150) NOT NULL, 7 | first_name character varying(150) NOT NULL, 8 | last_name character varying(150) NOT NULL, 9 | email character varying(254) NOT NULL, 10 | is_staff boolean NOT NULL, 11 | is_active boolean NOT NULL, 12 | date_joined TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 13 | ); 14 | -------------------------------------------------------------------------------- /examples/acl/resources/migrations/20210322112828-posts-table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE posts; -------------------------------------------------------------------------------- /examples/acl/resources/migrations/20210322112828-posts-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE posts ( 2 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | user_id uuid NOT NULL, 4 | creation_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, 5 | content character varying NOT NULL 6 | ); -------------------------------------------------------------------------------- /examples/acl/resources/migrations/20210322112927-comments-table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE comments; -------------------------------------------------------------------------------- /examples/acl/resources/migrations/20210322112927-comments-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE comments ( 2 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | user_id uuid NOT NULL, 4 | post_id uuid NOT NULL, 5 | creation_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, 6 | content character varying NOT NULL 7 | ); -------------------------------------------------------------------------------- /examples/acl/resources/migrations/20210322113010-add-test-users.down.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM users WHERE users.id IN ( 2 | '611d7f8a-456d-4f3c-802d-4d869dcd89bf', 3 | 'b651939c-96e6-4fbb-88fb-299e728e21c8', 4 | 'b01fae53-d742-4990-ac01-edadeb4f2e8f', 5 | '75c0d9b2-2c23-41a7-93a1-d1b716cdfa6c'); -------------------------------------------------------------------------------- /examples/acl/resources/migrations/20210322113010-add-test-users.up.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO users 2 | (id, password, is_superuser, username, first_name, last_name, email, is_staff, is_active) 3 | VALUES 4 | ('611d7f8a-456d-4f3c-802d-4d869dcd89bf', 'not-null', FALSE, 'test_Customer', 'John', 'Smith', 'jsmith@test.com', FALSE, TRUE), 5 | ('b651939c-96e6-4fbb-88fb-299e728e21c8', 'not-null', TRUE, 'test_Admin', 'John', 'Doe', 'jdoe@test.com', FALSE, TRUE), 6 | ('b01fae53-d742-4990-ac01-edadeb4f2e8f', 'not-null', TRUE, 'test_Suspended_Admin', 'Jonatan', 'Fired', 'jfire@test.com', FALSE, FALSE), 7 | ('75c0d9b2-2c23-41a7-93a1-d1b716cdfa6c', 'not-null', FALSE, 'test_Staff', 'Alexander', 'Great', 'greata@test.com', FALSE, TRUE); -------------------------------------------------------------------------------- /examples/acl/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | acl 7 | 8 | 9 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/acl/src/backend/app/controller_behaviors/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/acl/src/backend/app/controller_behaviors/.gitkeep -------------------------------------------------------------------------------- /examples/acl/src/backend/app/controllers/comments.clj: -------------------------------------------------------------------------------- 1 | (ns controllers.comments 2 | (:require 3 | [models.comments :as model] 4 | [models.data-ownership :as owner] 5 | [views.comments :as views])) 6 | 7 | (defn fetch 8 | [state] 9 | (-> 10 | (assoc state :view views/comments) 11 | model/fetch-query 12 | owner/owner-fn)) 13 | 14 | (defn add 15 | [state] 16 | (model/add-query 17 | (assoc state 18 | :view 19 | views/comments))) 20 | 21 | (defn update-comment 22 | [state] 23 | (-> 24 | (assoc state :view views/comments) 25 | model/update-query 26 | owner/owner-fn)) 27 | 28 | (defn delete-comment 29 | [state] 30 | (-> 31 | (assoc state :view views/comments) 32 | model/delete-query 33 | owner/owner-fn)) 34 | -------------------------------------------------------------------------------- /examples/acl/src/backend/app/controllers/index.clj: -------------------------------------------------------------------------------- 1 | (ns controllers.index 2 | (:require 3 | [ring.util.response :as ring])) 4 | 5 | (defn handle-index 6 | [state] 7 | (assoc state 8 | :response 9 | (ring/response "Index page"))) 10 | -------------------------------------------------------------------------------- /examples/acl/src/backend/app/controllers/posts.clj: -------------------------------------------------------------------------------- 1 | (ns controllers.posts 2 | (:require 3 | [models.data-ownership :as owner] 4 | [models.posts :as model] 5 | [views.posts :as views])) 6 | 7 | (defn fetch 8 | [state] 9 | (-> 10 | (assoc state :view views/fetch-posts) 11 | model/fetch-query 12 | owner/owner-fn)) 13 | 14 | (defn add 15 | [state] 16 | (model/add-query 17 | (assoc state 18 | :view 19 | views/fetch-posts))) 20 | 21 | (defn update-post 22 | [state] 23 | (-> 24 | (assoc state :view views/fetch-posts) 25 | model/update-query 26 | owner/owner-fn)) 27 | 28 | (defn delete-post 29 | [state] 30 | (-> 31 | (assoc state :view views/fetch-posts) 32 | model/delete-query 33 | owner/owner-fn)) 34 | 35 | (defn fetch-by-ids 36 | [state] 37 | (-> 38 | (assoc state :view views/all-posts) 39 | model/fetch-by-ids-query 40 | owner/owner-fn)) 41 | 42 | (defn fetch-with-comments 43 | [state] 44 | (-> 45 | (assoc state :view views/fetch-post-with-comments) 46 | model/fetch-with-comments-query 47 | owner/owner-fn)) 48 | -------------------------------------------------------------------------------- /examples/acl/src/backend/app/controllers/re_frame.clj: -------------------------------------------------------------------------------- 1 | (ns controllers.re-frame 2 | (:require 3 | [ring.util.response :as ring])) 4 | 5 | (defn handle-index 6 | [state] 7 | (assoc state 8 | :response 9 | (-> "index.html" 10 | (ring/resource-response {:root "public"}) 11 | (ring/header "Content-Type" "text/html; charset=utf-8")))) 12 | -------------------------------------------------------------------------------- /examples/acl/src/backend/app/controllers/users.clj: -------------------------------------------------------------------------------- 1 | (ns controllers.users 2 | (:require 3 | [models.data-ownership :as owner] 4 | [models.users :as model] 5 | [views.users :as views])) 6 | 7 | (defn fetch 8 | [state] 9 | (model/fetch-query 10 | (assoc state 11 | :view 12 | views/fetch-users))) 13 | 14 | (defn add 15 | [state] 16 | (model/add-query 17 | (assoc state 18 | :view 19 | views/fetch-users))) 20 | 21 | (defn update-user 22 | [state] 23 | (-> 24 | (assoc state :view views/fetch-users) 25 | model/update-query 26 | owner/owner-fn)) 27 | 28 | (defn delete-user 29 | [state] 30 | (-> 31 | (assoc state :view views/fetch-users) 32 | model/delete-query 33 | owner/owner-fn)) 34 | 35 | (defn fetch-with-posts-comments 36 | [state] 37 | (-> 38 | (assoc state :view views/fetch-posts-comments) 39 | model/fetch-with-post-comments-query 40 | owner/owner-fn)) 41 | 42 | (defn fetch-with-posts 43 | [state] 44 | (-> 45 | (assoc state :view views/fetch-posts) 46 | model/fetch-with-post-query 47 | owner/owner-fn)) 48 | -------------------------------------------------------------------------------- /examples/acl/src/backend/app/db_migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/acl/src/backend/app/db_migrations/.gitkeep -------------------------------------------------------------------------------- /examples/acl/src/backend/app/interceptors/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/acl/src/backend/app/interceptors/.gitkeep -------------------------------------------------------------------------------- /examples/acl/src/backend/app/interceptors/load_user.clj: -------------------------------------------------------------------------------- 1 | (ns interceptors.load-user 2 | (:require 3 | [honeysql.helpers :refer [select from where]] 4 | [xiana.db :as db])) 5 | 6 | (defn ->role 7 | [user] 8 | (let [role (cond 9 | (not (:users/is_active user)) :guest 10 | (:users/is_superuser user) :superuser 11 | (:users/is_staff user) :staff 12 | (:users/is_active user) :member 13 | :else :guest)] 14 | (assoc user :users/role role))) 15 | 16 | (defn valid-user? 17 | [{:keys [request] :as state}] 18 | (let [user-id (parse-uuid (get-in request [:headers :authorization])) 19 | datasource (get-in state [:deps :db :datasource]) 20 | query (-> {} 21 | (select :*) 22 | (from :users) 23 | (where [:= :id user-id]))] 24 | (-> (db/execute datasource query) 25 | first 26 | ->role))) 27 | 28 | (def load-user! 29 | {:enter (fn [{:keys [request] :as state}] 30 | (let [guest-user {:users/role :guest 31 | :users/id (random-uuid)} 32 | session-id (get-in request [:headers :session-id] (random-uuid)) 33 | user (try (let [valid-user (valid-user? state)] 34 | (if (empty? valid-user) 35 | guest-user 36 | valid-user)) 37 | (catch Exception _ guest-user))] 38 | (assoc state :session-data (assoc user :session-id session-id))))}) 39 | -------------------------------------------------------------------------------- /examples/acl/src/backend/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/acl/src/backend/app/models/.gitkeep -------------------------------------------------------------------------------- /examples/acl/src/backend/app/models/comments.clj: -------------------------------------------------------------------------------- 1 | (ns models.comments 2 | (:require 3 | [honeysql.helpers :refer [select 4 | from 5 | where 6 | insert-into 7 | delete-from 8 | columns 9 | values 10 | sset] 11 | :as helpers])) 12 | 13 | (defn fetch-query 14 | [{{{id :id} :query-params} :request 15 | :as state}] 16 | (assoc state :query (cond-> 17 | (from (select :*) :comments) 18 | id (where [:= :id (parse-uuid id)])))) 19 | 20 | (defn add-query 21 | [{{user-id :users/id} :session-data 22 | {{content :content post-id :post_id} :body-params} :request 23 | :as state}] 24 | (let [pid (try (parse-uuid post-id) 25 | (catch Exception _ (random-uuid)))] 26 | (assoc state :query (-> (insert-into :comments) 27 | (columns :content :post_id :user_id) 28 | (values [[content pid user-id]]))))) 29 | 30 | (defn update-query 31 | [{{{id :id} :params} :request 32 | {{content :content} :body-params} :request 33 | :as state}] 34 | (assoc state :query (-> (helpers/update :comments) 35 | (where [:= :id (parse-uuid id)]) 36 | (sset {:content content})))) 37 | 38 | (defn delete-query 39 | [{{{id :id} :params} :request 40 | :as state}] 41 | (assoc state :query (cond-> 42 | (delete-from :comments) 43 | id (where [:= :id (parse-uuid id)])))) 44 | -------------------------------------------------------------------------------- /examples/acl/src/backend/app/models/data_ownership.clj: -------------------------------------------------------------------------------- 1 | (ns models.data-ownership 2 | (:require 3 | [honeysql.helpers :refer [merge-where]])) 4 | 5 | (defn owner-fn 6 | [state] 7 | (let [user-permissions (get-in state [:request-data :user-permissions]) 8 | query (:query state) 9 | user-id (get-in state [:session-data :users/id])] 10 | (assoc state :query 11 | (cond 12 | (user-permissions :comments/own) (merge-where query [:= :comments.user_id user-id]) 13 | (user-permissions :users/own) (merge-where query [:= :users.id user-id]) 14 | (user-permissions :posts/own) (merge-where query [:= :posts.user_id user-id]) 15 | :else query)))) 16 | -------------------------------------------------------------------------------- /examples/acl/src/backend/app/models/posts.clj: -------------------------------------------------------------------------------- 1 | (ns models.posts 2 | (:require 3 | [honeysql.helpers :refer [select 4 | from 5 | where 6 | insert-into 7 | values 8 | left-join 9 | sset 10 | delete-from] 11 | :as helpers])) 12 | 13 | (defn fetch-query 14 | [{{{id :id} :query-params} :request 15 | :as state}] 16 | (assoc state :query (cond-> 17 | (from (select :*) :posts) 18 | id (where [:= :id (parse-uuid id)])))) 19 | 20 | (defn add-query 21 | [{{user-id :users/id} :session-data 22 | :as state}] 23 | (let [content (or (get-in state [:request :params :content]) 24 | (get-in state [:request :body-params :content]))] 25 | (assoc state :query (values 26 | (insert-into :posts) 27 | [{:content content, :user_id user-id}])))) 28 | 29 | (defn update-query 30 | [{{{id :id} :params} :request 31 | :as state}] 32 | (let [content (or (get-in state [:request :params :content]) 33 | (get-in state [:request :body-params :content]))] 34 | (assoc state :query (-> (helpers/update :posts) 35 | (where [:= :id (parse-uuid id)]) 36 | (sset {:content content}))))) 37 | 38 | (defn delete-query 39 | [{{{id :id} :query-params} :request 40 | :as state}] 41 | (assoc state :query (cond-> 42 | (delete-from :posts) 43 | id (where [:= :id (parse-uuid id)])))) 44 | 45 | (defn fetch-by-ids-query 46 | [{{{ids :ids} :body-params} :request 47 | :as state}] 48 | (assoc state :query (cond-> 49 | (from (select :*) :posts) 50 | ids (where [:in :id (map #(parse-uuid %) [ids])]) 51 | (coll? ids) (where [:in :id (map #(parse-uuid %) ids)])))) 52 | 53 | (defn fetch-with-comments-query 54 | [{{{id :id} :query-params} :request 55 | :as state}] 56 | (assoc state :query (cond-> (-> (select :*) 57 | (from :posts) 58 | (left-join :comments [:= :posts.id :comments.post_id])) 59 | id (where [:= :posts.id (parse-uuid id)])))) 60 | -------------------------------------------------------------------------------- /examples/acl/src/backend/app/views/comments.clj: -------------------------------------------------------------------------------- 1 | (ns views.comments 2 | (:require 3 | [views.common :as common])) 4 | 5 | (defn ->comment-view 6 | [m] 7 | (select-keys m [:comments/id 8 | :comments/post_id 9 | :comments/user_id 10 | :comments/content 11 | :comments/creation_time])) 12 | 13 | (defn comments 14 | [{response-data :response-data :as state}] 15 | (common/response state {:view-type "comments" 16 | :data {:comments (->> response-data 17 | :db-data 18 | (map ->comment-view))}})) 19 | -------------------------------------------------------------------------------- /examples/acl/src/backend/app/views/common.clj: -------------------------------------------------------------------------------- 1 | (ns views.common) 2 | 3 | (defn response 4 | [state body] 5 | (-> 6 | state 7 | (assoc-in [:response :status] 200) 8 | (assoc-in [:response :headers "Content-type"] "Application/json") 9 | (assoc-in [:response :body] body))) 10 | 11 | (defn not-allowed 12 | [state] 13 | (assoc state :response {:status 401 :body "You don't have rights to do this"})) 14 | -------------------------------------------------------------------------------- /examples/acl/src/backend/app/views/layouts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/acl/src/backend/app/views/layouts/.gitkeep -------------------------------------------------------------------------------- /examples/acl/src/backend/app/views/posts.clj: -------------------------------------------------------------------------------- 1 | (ns views.posts 2 | (:require 3 | [views.comments :as comments] 4 | [views.common :as c])) 5 | 6 | (defn ->post-view 7 | [m] 8 | (select-keys m [:posts/id 9 | :posts/user_id 10 | :posts/content 11 | :posts/creation_time])) 12 | 13 | (defn post-view 14 | [{response-data :response-data :as state}] 15 | (c/response state {:view-type "Single post" 16 | :data {:posts (map ->post-view (:db-data response-data))}})) 17 | 18 | (defn all-posts 19 | [{response-data :response-data :as state}] 20 | (c/response state {:view-type "All posts" 21 | :data {:posts (map ->post-view (:db-data response-data))}})) 22 | 23 | (defn fetch-posts 24 | [{{{id :id} :query-params} :request 25 | :as state}] 26 | (if id 27 | (post-view state) 28 | (all-posts state))) 29 | 30 | (defn render-posts-with-comments 31 | [data] 32 | (->> data 33 | (group-by ->post-view) 34 | (map (fn [[k v]] 35 | [k (mapv comments/->comment-view v)])) 36 | (map (fn [[k v]] (assoc k :comments (remove #(every? nil? (vals %)) v)))))) 37 | 38 | (defn fetch-post-with-comments 39 | [{{{id :id} :query-params} :request 40 | response-data :response-data 41 | :as state}] 42 | (if id 43 | (c/response state {:view-type "Single post with comments" 44 | :data {:posts (render-posts-with-comments (:db-data response-data))}}) 45 | (c/response state {:view-type "Multiple posts with comments" 46 | :data {:posts (render-posts-with-comments (:db-data response-data))}}))) 47 | 48 | -------------------------------------------------------------------------------- /examples/acl/src/frontend/acl/config.cljs: -------------------------------------------------------------------------------- 1 | (ns acl.config) 2 | 3 | (def debug? 4 | ^boolean goog.DEBUG) 5 | -------------------------------------------------------------------------------- /examples/acl/src/frontend/acl/core.cljs: -------------------------------------------------------------------------------- 1 | (ns acl.core 2 | (:require 3 | [acl.config :as config] 4 | [acl.events :as events] 5 | [acl.views :as views] 6 | [re-frame.core :as re-frame] 7 | [reagent.dom :as rdom])) 8 | 9 | (defn dev-setup 10 | [] 11 | (when config/debug? 12 | (println "dev mode"))) 13 | 14 | (defn ^:dev/after-load mount-root 15 | [] 16 | (re-frame/clear-subscription-cache!) 17 | (let [root-el (.getElementById js/document "app")] 18 | (rdom/unmount-component-at-node root-el) 19 | (rdom/render [views/main-panel] root-el))) 20 | 21 | (defn init 22 | [] 23 | (re-frame/dispatch-sync [::events/initialize-db]) 24 | (dev-setup) 25 | (mount-root)) 26 | -------------------------------------------------------------------------------- /examples/acl/src/frontend/acl/db.cljs: -------------------------------------------------------------------------------- 1 | (ns acl.db) 2 | 3 | (def default-db 4 | {:name "re-frame"}) 5 | -------------------------------------------------------------------------------- /examples/acl/src/frontend/acl/events.cljs: -------------------------------------------------------------------------------- 1 | (ns acl.events 2 | (:require 3 | [acl.db :as db] 4 | [re-frame.core :as re-frame])) 5 | 6 | (re-frame/reg-event-db 7 | ::initialize-db 8 | (fn [_ _] 9 | db/default-db)) 10 | -------------------------------------------------------------------------------- /examples/acl/src/frontend/acl/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns acl.subs 2 | (:require 3 | [re-frame.core :as re-frame])) 4 | 5 | (re-frame/reg-sub 6 | ::name 7 | (fn [db] 8 | (get db :name))) 9 | -------------------------------------------------------------------------------- /examples/acl/src/frontend/acl/views.cljs: -------------------------------------------------------------------------------- 1 | (ns acl.views 2 | (:require 3 | [acl.subs :as subs] 4 | [re-frame.core :as re-frame])) 5 | 6 | (defn main-panel 7 | [] 8 | (let [name (re-frame/subscribe [::subs/name])] 9 | [:div 10 | [:h1 "Hello from " @name]])) 11 | -------------------------------------------------------------------------------- /examples/acl/src/frontend/deps.cljs: -------------------------------------------------------------------------------- 1 | {:npm-dev-deps {"shadow-cljs" "2.11.7"}} 2 | -------------------------------------------------------------------------------- /examples/acl/src/shared/config.clj: -------------------------------------------------------------------------------- 1 | (ns config) 2 | -------------------------------------------------------------------------------- /examples/acl/src/shared/schema.clj: -------------------------------------------------------------------------------- 1 | (ns schema) 2 | -------------------------------------------------------------------------------- /examples/acl/test/acl_fixture.clj: -------------------------------------------------------------------------------- 1 | (ns acl-fixture 2 | (:require 3 | [acl] 4 | [xiana.config :as config] 5 | [xiana.db :as db])) 6 | 7 | (defn std-system-fixture 8 | [f] 9 | (with-open [_ (-> (config/config) 10 | (merge acl/app-cfg) 11 | db/docker-postgres! 12 | acl/->system)] 13 | (f))) 14 | -------------------------------------------------------------------------------- /examples/acl/test/post_helpers.clj: -------------------------------------------------------------------------------- 1 | (ns post-helpers 2 | (:require 3 | [helpers] 4 | [jsonista.core :as j])) 5 | 6 | (defn init-db-with-two-posts 7 | [] 8 | (helpers/delete :posts) 9 | (helpers/put :posts {:content "Test post"}) 10 | (helpers/put :posts {:content "Second Test post"})) 11 | 12 | (defn post-ids 13 | [body] 14 | (map :posts/id (-> body 15 | (j/read-value j/keyword-keys-object-mapper) 16 | :data 17 | :posts))) 18 | 19 | (defn all-post-ids 20 | [] 21 | (-> (helpers/fetch :posts) 22 | :body 23 | post-ids)) 24 | 25 | (defn update-count 26 | [body] 27 | (-> body 28 | (j/read-value j/keyword-keys-object-mapper) 29 | (get-in [:data :posts]) 30 | count)) 31 | -------------------------------------------------------------------------------- /examples/cli-chat/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /.prepl-port 12 | .hgignore 13 | .hg/ 14 | .idea/ 15 | -------------------------------------------------------------------------------- /examples/cli-chat/README.md: -------------------------------------------------------------------------------- 1 | # cli-chat 2 | 3 | Websockets based chat server example implementation 4 | 5 | ## Usage 6 | 7 | ### Start dockerized PostgreSQL 8 | ```shell 9 | docker compose up -d 10 | ``` 11 | 12 | ### Log into psql console 13 | ```shell 14 | psql -U postgres -p 5433 -h localhost 15 | ``` 16 | 17 | ### Run the backend 18 | ```shell 19 | lein run 20 | ``` 21 | 22 | ### Try cli-chat 23 | 24 | Connect at least two times with [WebSocat](https://github.com/vi/websocat/releases) to `ws://localhost:3000/chat` 25 | -------------------------------------------------------------------------------- /examples/cli-chat/config/dev/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/postgresql {:port 5433 2 | :dbname "cli_chat" 3 | :host "localhost" 4 | :dbtype "postgresql" 5 | :user "postgres" 6 | :password "postgres"} 7 | 8 | :xiana/migration {:store :database 9 | :migration-dir "resources/migrations" 10 | :init-in-transaction? false 11 | :migration-table-name "migrations"} 12 | 13 | :xiana/web-server {:port 3000 14 | :join? false} 15 | 16 | :xiana/auth {:hash-algorithm :bcrypt ; Available values: :bcrypt, :scrypt, and :pbkdf2 17 | :bcrypt-settings {:work-factor 11} 18 | :scrypt-settings {:cpu-cost 32768 ; Must be a power of 2 19 | :memory-cost 8 20 | :parallelization 1} 21 | :pbkdf2-settings {:type :sha1 ; Available values: :sha1 and :sha256 22 | :iterations 100000}}} 23 | -------------------------------------------------------------------------------- /examples/cli-chat/config/test/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/postgresql {:image-name "postgres:14-alpine" 2 | :port 5433 3 | :dbname "cli_chat_test" 4 | :host "localhost" 5 | :dbtype "postgresql" 6 | :user "postgres" 7 | :password "postgres"} 8 | 9 | :xiana/migration {:store :database 10 | :migration-dir "resources/migrations" 11 | :init-in-transaction? false 12 | :migration-table-name "migrations"} 13 | 14 | :xiana/web-server {:port 3333 15 | :join? false} 16 | 17 | :xiana/auth {:hash-algorithm :bcrypt ; Available values: :bcrypt, :scrypt, and :pbkdf2 18 | :bcrypt-settings {:work-factor 11} 19 | :scrypt-settings {:cpu-cost 32768 ; Must be a power of 2 20 | :memory-cost 8 21 | :parallelization 1} 22 | :pbkdf2-settings {:type :sha1 ; Available values: :sha1 and :sha256 23 | :iterations 100000}}} 24 | 25 | -------------------------------------------------------------------------------- /examples/cli-chat/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | db: 4 | image: postgres:14-alpine 5 | environment: 6 | - POSTGRES_DB=cli_chat 7 | - POSTGRES_USER=postgres 8 | - POSTGRES_PASSWORD=postgres 9 | volumes: 10 | - db-data:/var/lib/postgresql/data 11 | ports: 12 | - "5433:5432" 13 | 14 | volumes: 15 | db-data: 16 | -------------------------------------------------------------------------------- /examples/cli-chat/project.clj: -------------------------------------------------------------------------------- 1 | (defproject cli-chat "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :min-lein-version "2.0.0" 4 | :dependencies [[com.flexiana/framework "0.5.0-rc4"]] 5 | :plugins [] 6 | :main ^:skip-aot cli-chat.core 7 | :uberjar-name "cli-chat.jar" 8 | :source-paths ["src/backend" "src/frontend" "src/shared"] 9 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 10 | :profiles {:dev {:resource-paths ["config/dev"] 11 | :dependencies [[binaryage/devtools "1.0.5"]]} 12 | :local {:resource-paths ["config/local"]} 13 | :prod {:resource-paths ["config/prod"]} 14 | :test {:resource-paths ["config/test"] 15 | :dependencies [[clj-http "3.12.3"] 16 | [http.async.client "1.3.1"] 17 | [mvxcvi/cljstyle "0.15.0" 18 | :exclusions [org.clojure/clojure]]]}} 19 | :shadow-cljs {:nrepl {:port 8777} 20 | :builds {:app {:target :browser 21 | :output-dir "resources/public/js/compiled" 22 | :asset-path "/js/compiled" 23 | :modules {:app {:init-fn cli-chat.core/init 24 | :preloads [devtools.preload]}}}}} 25 | :aliases {"check-style" ["with-profile" "+test" "run" "-m" "cljstyle.main" "check"] 26 | "ci" ["do" "clean," "cloverage," "lint," "uberjar"] 27 | "kondo" ["run" "-m" "clj-kondo.main" "--lint" "src" "test"] 28 | "lint" ["do" "kondo," "eastwood," "kibit"] 29 | "watch" ["with-profile" "dev" "do" 30 | ["shadow" "watch" "app" "browser-test" "karma-test"]] 31 | "release" ["with-profile" "prod" "do" 32 | ["shadow" "release" "app"]]} 33 | :migratus {:store :database 34 | :migration-dir "migrations" 35 | :db {:classname "com.mysql.jdbc.Driver" 36 | :subprotocol "postgres" 37 | :subname "//localhost:5433/cli_chat" 38 | :user "postgres" 39 | :password "postgres"}}) 40 | -------------------------------------------------------------------------------- /examples/cli-chat/resources/migrations/20211029080516-users.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE users; -------------------------------------------------------------------------------- /examples/cli-chat/resources/migrations/20211029080516-users.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id SERIAL PRIMARY KEY, 3 | name VARCHAR NOT NULL UNIQUE, 4 | passwd VARCHAR NOT NULL, 5 | role VARCHAR 6 | ); -------------------------------------------------------------------------------- /examples/cli-chat/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cli-chat 7 | 8 | 9 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/controller_behaviors/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/cli-chat/src/backend/cli_chat/controller_behaviors/.gitkeep -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/controllers/chat.clj: -------------------------------------------------------------------------------- 1 | (ns cli-chat.controllers.chat 2 | (:require 3 | [cli-chat.controller-behaviors.chat :as behave] 4 | [cli-chat.views.chat :as views] 5 | [reitit.core :as r] 6 | [xiana.db :as db] 7 | [xiana.interceptor :as interceptors] 8 | [xiana.websockets :refer [router string->]])) 9 | 10 | (defonce channels (atom {})) 11 | 12 | (def routes 13 | (r/router [["/help" {:action behave/help}] 14 | ["/welcome" {:action behave/welcome}] 15 | ["/me" {:action behave/me}] 16 | ["/to" {:action behave/to}] 17 | ["/login" {:action behave/login 18 | :interceptors {:inside [interceptors/side-effect 19 | db/db-access]} 20 | :hide true}] 21 | ["/sign-up" {:action behave/sign-up 22 | :interceptors {:inside [interceptors/side-effect 23 | db/db-access]} 24 | :hide true}]] 25 | {:data {:default-interceptors [(interceptors/message "Incoming message...")]}})) 26 | 27 | (def routing 28 | (partial router routes string->)) 29 | 30 | (defn chat-action 31 | [state] 32 | (assoc-in state [:response-data :channel] 33 | {:on-text (fn [ch msg] 34 | (routing (update state :request-data 35 | merge {:ch ch 36 | :income-msg msg 37 | :fallback views/fallback 38 | :channels channels}))) 39 | :on-connect (fn [ch] 40 | (routing (update state :request-data 41 | merge {:ch ch 42 | :channels channels 43 | :income-msg "/welcome"}))) 44 | :on-close (fn [ch _status _reason] (swap! channels dissoc ch))})) 45 | -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/controllers/index.clj: -------------------------------------------------------------------------------- 1 | (ns cli-chat.controllers.index 2 | (:require 3 | [ring.util.response :as ring])) 4 | 5 | (defn handle-index 6 | [state] 7 | (assoc state 8 | :response 9 | (ring/response "Index page"))) 10 | -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/controllers/re_frame.clj: -------------------------------------------------------------------------------- 1 | (ns cli-chat.controllers.re-frame 2 | (:require 3 | [ring.util.response :as ring])) 4 | 5 | (defn handle-index 6 | [state] 7 | (assoc state 8 | :response 9 | (-> "index.html" 10 | (ring/resource-response {:root "public"}) 11 | (ring/header "Content-Type" "text/html; charset=utf-8")))) 12 | -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/core.clj: -------------------------------------------------------------------------------- 1 | (ns cli-chat.core 2 | (:require 3 | [cli-chat.controllers.chat :refer [chat-action]] 4 | [cli-chat.controllers.index :as index] 5 | [cli-chat.controllers.re-frame :as re-frame] 6 | [piotr-yuxuan.closeable-map :refer [closeable-map]] 7 | [reitit.ring :as ring] 8 | [xiana.commons :refer [rename-key]] 9 | [xiana.config :as config] 10 | [xiana.db :as db] 11 | [xiana.interceptor :as interceptors] 12 | [xiana.rbac :as rbac] 13 | [xiana.route :as routes] 14 | [xiana.session :as session] 15 | [xiana.webserver :as ws])) 16 | 17 | (def routes 18 | [["/" {:action index/handle-index}] 19 | ["/re-frame" {:action re-frame/handle-index}] 20 | ["/assets/*" (ring/create-resource-handler {:path "/"})] 21 | ["/chat" {:ws-action chat-action}]]) 22 | 23 | (defn ->system 24 | [app-cfg] 25 | (-> (config/config) 26 | (merge app-cfg) 27 | routes/reset 28 | db/connect 29 | db/migrate! 30 | rbac/init 31 | (rename-key :xiana/auth :auth) 32 | session/init-backend 33 | ws/start 34 | closeable-map)) 35 | 36 | (def app-cfg 37 | {:routes routes 38 | :router-interceptors [] 39 | :web-socket-interceptors [interceptors/params 40 | session/guest-session-interceptor] 41 | :controller-interceptors [(interceptors/muuntaja) 42 | interceptors/params 43 | session/guest-session-interceptor 44 | interceptors/view 45 | interceptors/side-effect 46 | db/db-access 47 | rbac/interceptor]}) 48 | 49 | (defn -main 50 | [& _args] 51 | (->system app-cfg)) 52 | 53 | -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/db_migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/cli-chat/src/backend/cli_chat/db_migrations/.gitkeep -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/interceptors.clj: -------------------------------------------------------------------------------- 1 | (ns cli-chat.interceptors) 2 | 3 | (def sample-cli-chat-controller-interceptor 4 | {:enter (fn [{request :request {:keys [handler controller match]} :request-data :as state}] 5 | state) 6 | :leave (fn [{response :response :as state}] 7 | state)}) 8 | -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/interceptors/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/cli-chat/src/backend/cli_chat/interceptors/.gitkeep -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/cli-chat/src/backend/cli_chat/models/.gitkeep -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/models/users.clj: -------------------------------------------------------------------------------- 1 | (ns cli-chat.models.users) 2 | 3 | (defn get-user 4 | [user-name] 5 | {:select [:*] 6 | :from [:users] 7 | :where [:= :name user-name]}) 8 | -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/views/chat.clj: -------------------------------------------------------------------------------- 1 | (ns cli-chat.views.chat 2 | (:require 3 | [clojure.string :as str] 4 | [xiana.websockets :as ws])) 5 | 6 | (defn send-multi-line 7 | ([ch reply] 8 | (doseq [m (str/split-lines reply)] 9 | (ws/send! ch (str m "\n")))) 10 | ([{{req-ch :ch} :request-data 11 | {res-ch :ch 12 | reply :reply} :response-data}] 13 | (send-multi-line (or res-ch req-ch) reply))) 14 | 15 | (defn broadcast-to-others 16 | [{{ch :ch 17 | channels :channels} :request-data 18 | {reply :reply} :response-data}] 19 | (doseq [c (remove #(#{ch} (key %)) @channels)] 20 | (ws/send! (key c) reply))) 21 | 22 | (defn broadcast 23 | [{{ch :ch 24 | channels :channels 25 | income-msg :income-msg} :request-data 26 | :as state}] 27 | (let [username (get-in @channels [ch :users/name])] 28 | (update state :response-data merge {:reply-fn broadcast-to-others 29 | :reply (str username ": " income-msg)}))) 30 | 31 | (defn fallback 32 | [{{income-msg :income-msg} :request-data 33 | :as state}] 34 | (if (str/starts-with? income-msg "/") 35 | (update state :response-data merge {:reply-fn send-multi-line 36 | :reply (str "Invalid command: " income-msg)}) 37 | (broadcast state))) 38 | -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/views/common.clj: -------------------------------------------------------------------------------- 1 | (ns cli-chat.views.common) 2 | 3 | (defn response 4 | [state body] 5 | (-> 6 | state 7 | (assoc-in [:response :status] 200) 8 | (assoc-in [:response :headers "Content-type"] "Application/json") 9 | (assoc-in [:response :body] body))) 10 | 11 | (defn not-allowed 12 | [_] 13 | (throw (ex-info "You don't have rights to do this" 14 | {:status 401 :body "You don't have rights to do this"}))) 15 | -------------------------------------------------------------------------------- /examples/cli-chat/src/backend/cli_chat/views/layouts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/cli-chat/src/backend/cli_chat/views/layouts/.gitkeep -------------------------------------------------------------------------------- /examples/cli-chat/src/frontend/cli_chat/config.cljs: -------------------------------------------------------------------------------- 1 | (ns cli-chat.config) 2 | 3 | (def debug? 4 | ^boolean goog.DEBUG) 5 | -------------------------------------------------------------------------------- /examples/cli-chat/src/frontend/cli_chat/core.cljs: -------------------------------------------------------------------------------- 1 | (ns cli-chat.core 2 | (:require 3 | [cli-chat.config :as config] 4 | [cli-chat.events :as events] 5 | [cli-chat.views :as views] 6 | [re-frame.core :as re-frame] 7 | [reagent.dom :as rdom])) 8 | 9 | (defn dev-setup [] 10 | (when config/debug? 11 | (println "dev mode"))) 12 | 13 | (defn ^:dev/after-load mount-root [] 14 | (re-frame/clear-subscription-cache!) 15 | (let [root-el (.getElementById js/document "app")] 16 | (rdom/unmount-component-at-node root-el) 17 | (rdom/render [views/main-panel] root-el))) 18 | 19 | (defn init [] 20 | (re-frame/dispatch-sync [::events/initialize-db]) 21 | (dev-setup) 22 | (mount-root)) 23 | -------------------------------------------------------------------------------- /examples/cli-chat/src/frontend/cli_chat/db.cljs: -------------------------------------------------------------------------------- 1 | (ns cli-chat.db) 2 | 3 | (def default-db 4 | {:name "re-frame"}) 5 | -------------------------------------------------------------------------------- /examples/cli-chat/src/frontend/cli_chat/events.cljs: -------------------------------------------------------------------------------- 1 | (ns cli-chat.events 2 | (:require 3 | [cli-chat.db :as db] 4 | [re-frame.core :as re-frame])) 5 | 6 | (re-frame/reg-event-db 7 | ::initialize-db 8 | (fn [_ _] 9 | db/default-db)) 10 | -------------------------------------------------------------------------------- /examples/cli-chat/src/frontend/cli_chat/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns cli-chat.subs 2 | (:require 3 | [re-frame.core :as re-frame])) 4 | 5 | (re-frame/reg-sub 6 | ::name 7 | (fn [db] 8 | (get db :name))) 9 | -------------------------------------------------------------------------------- /examples/cli-chat/src/frontend/cli_chat/views.cljs: -------------------------------------------------------------------------------- 1 | (ns cli-chat.views 2 | (:require 3 | [cli-chat.subs :as subs] 4 | [re-frame.core :as re-frame])) 5 | 6 | (defn main-panel [] 7 | (let [name (re-frame/subscribe [::subs/name])] 8 | [:div 9 | [:h1 "Hello from " @name]])) 10 | -------------------------------------------------------------------------------- /examples/cli-chat/src/frontend/deps.cljs: -------------------------------------------------------------------------------- 1 | {:npm-dev-deps {"shadow-cljs" "2.11.7"}} 2 | -------------------------------------------------------------------------------- /examples/cli-chat/test/cli_chat_fixture.clj: -------------------------------------------------------------------------------- 1 | (ns cli-chat-fixture 2 | (:require 3 | [cli-chat.core :refer [->system]] 4 | [xiana.config :as config] 5 | [xiana.db :as db])) 6 | 7 | (defn std-system-fixture 8 | [app-cfg f] 9 | (with-open [_ (-> (config/config) 10 | (merge app-cfg) 11 | db/docker-postgres! 12 | ->system)] 13 | (f))) 14 | -------------------------------------------------------------------------------- /examples/controllers/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /.prepl-port 12 | .hgignore 13 | .hg/ 14 | .cpcache/ 15 | -------------------------------------------------------------------------------- /examples/controllers/README.md: -------------------------------------------------------------------------------- 1 | # controllers 2 | 3 | FIXME: description 4 | 5 | ## Usage 6 | 7 | ### Run the backend 8 | 9 | ```shell 10 | docker compose up -d 11 | 12 | lein run 13 | ``` 14 | 15 | ### Try controllers 16 | 17 | curl http://localhost:3000/ 18 | 19 | Unauthorized 20 | 21 | curl http://localhost:3000/wrong 22 | 23 | Unauthorized 24 | 25 | curl http://localhost:3000/re-frame 26 | 27 | Unauthorized 28 | 29 | curl -H "Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l" http://localhost:3000/re-frame 30 | 31 | ```html 32 | 33 | 34 | 35 | 36 | 37 | controllers 38 | 39 | 40 | 43 |
44 | 45 | 46 | 47 | ``` 48 | 49 | curl -H "Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l" http://localhost:3000/ 50 | 51 | Index page 52 | 53 | curl -H "Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l" http://localhost:3000/wrong 54 | 55 | Not Found 56 | 57 | curl -H "Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l" http://localhost:3000/api/siege-machines/1 58 | 59 | {"id":1,"name":"trebuchet"} 60 | 61 | curl -H "Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l" http://localhost:3000/api/siege-machines/apple 62 | 63 | Request coercion failed 64 | 65 | curl -H "Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l" http://localhost:3000/api/siege-machines/4 66 | 67 | Not found 68 | 69 | curl -H "Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l" http://localhost:3000/api/siege-machines/3 70 | 71 | Response validation failed 72 | 73 | ### Run controllers test 74 | 75 | Run tests with 76 | 77 | ```shell 78 | lein test 79 | ``` 80 | -------------------------------------------------------------------------------- /examples/controllers/config/dev/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/postgresql {:port 5432 2 | :dbname "controllers" 3 | :host "localhost" 4 | :dbtype "postgresql" 5 | :user "postgres" 6 | :password "postgres"} 7 | :xiana/migration {:store :database 8 | :migration-dir "resources/migrations" 9 | :init-in-transaction? false 10 | :migration-table-name "migrations"} 11 | :xiana/web-server {:port 3000 12 | :join? false}} 13 | -------------------------------------------------------------------------------- /examples/controllers/config/test/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/postgresql {:image-name "postgres:14-alpine" 2 | :port 5432 3 | :dbname "controllers" 4 | :host "localhost" 5 | :dbtype "postgresql" 6 | :user "postgres" 7 | :password "postgres"} 8 | 9 | :xiana/web-server {:port 3333 10 | :join? false}} 11 | -------------------------------------------------------------------------------- /examples/controllers/dev/state.clj: -------------------------------------------------------------------------------- 1 | (ns state 2 | (:require 3 | [clojure.tools.namespace.repl :refer [disable-reload!]] 4 | [piotr-yuxuan.closeable-map :refer [closeable-map]])) 5 | 6 | (disable-reload!) 7 | 8 | (defonce dev-sys (atom (closeable-map {}))) 9 | -------------------------------------------------------------------------------- /examples/controllers/dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:gen-class) 3 | (:require 4 | [clojure.tools.namespace.repl :refer [refresh-all]] 5 | [core :refer [->system app-cfg]] 6 | [piotr-yuxuan.closeable-map :refer [closeable-map]] 7 | [state :refer [dev-sys]])) 8 | 9 | (def dev-app-config app-cfg) 10 | 11 | (defn- stop-dev-system 12 | [] 13 | (when (:webserver @dev-sys) 14 | (.close @dev-sys) 15 | (refresh-all)) 16 | (reset! dev-sys (closeable-map {}))) 17 | 18 | (defn start-dev-system 19 | [] 20 | (stop-dev-system) 21 | (reset! dev-sys (->system dev-app-config))) 22 | 23 | (comment 24 | (start-dev-system)) 25 | -------------------------------------------------------------------------------- /examples/controllers/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | db: 4 | image: postgres:14-alpine 5 | environment: 6 | - POSTGRES_DB=controllers 7 | - POSTGRES_USER=postgres 8 | - POSTGRES_PASSWORD=postgres 9 | volumes: 10 | - db-data:/var/lib/postgresql/data 11 | ports: 12 | - "5432:5432" 13 | 14 | volumes: 15 | db-data: 16 | -------------------------------------------------------------------------------- /examples/controllers/project.clj: -------------------------------------------------------------------------------- 1 | (defproject controllers "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :min-lein-version "2.0.0" 4 | :dependencies [[com.flexiana/framework "0.5.0-rc4"] 5 | [org.clojure/clojurescript "1.11.4"]] 6 | :plugins [[lein-shadow "0.4.0"] 7 | [lein-shell "0.5.0"] 8 | [migratus-lein "0.7.3"]] 9 | :main ^:skip-aot core 10 | :uberjar-name "frames.jar" 11 | :source-paths ["src/backend/app" "src/backend/components" "src/frontend" "src/shared"] 12 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 13 | :profiles {:dev {:resource-paths ["config/dev" "dev"] 14 | :dependencies [[binaryage/devtools "1.0.5"] 15 | [org.clojure/tools.namespace "1.2.0"]]} 16 | :local {:resource-paths ["config/local"]} 17 | :prod {:resource-paths ["config/prod"]} 18 | :test {:resource-paths ["config/test"] 19 | :dependencies [[clj-http "3.12.3"] 20 | [mvxcvi/cljstyle "0.15.0" 21 | :exclusions [org.clojure/clojure]]]}} 22 | :jvm-opts ["-Dmalli.registry/type=custom"] 23 | :shadow-cljs {:nrepl {:port 8777} 24 | :builds {:app {:target :browser 25 | :output-dir "resources/public/js/compiled" 26 | :asset-path "/js/compiled" 27 | :modules {:app {:init-fn controllers.core/init 28 | :preloads [devtools.preload]}}}}} 29 | 30 | :aliases {"check-style" ["with-profile" "+test" "run" "-m" "cljstyle.main" "check"] 31 | "ci" ["do" "clean," "cloverage," "lint," "uberjar"] 32 | "kondo" ["run" "-m" "clj-kondo.main" "--lint" "src" "test"] 33 | "lint" ["do" "kondo," "eastwood," "kibit"] 34 | "watch" ["with-profile" "dev" "do" 35 | ["shadow" "watch" "app" "browser-test" "karma-test"]] 36 | "release" ["with-profile" "prod" "do" 37 | ["shadow" "release" "app"]]} 38 | :migratus {:store :database 39 | :migration-dir "migrations" 40 | :db {:classname "com.mysql.jdbc.Driver" 41 | :subprotocol "postgres" 42 | :subname "//localhost/controllers" 43 | :user "postgres" 44 | :password "postgres"}}) 45 | -------------------------------------------------------------------------------- /examples/controllers/resources/migrations/20210205095919-add-auth-tables.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.auth_group CASCADE; 2 | 3 | --;; 4 | 5 | DROP TABLE public.auth_group_permissions CASCADE; 6 | 7 | --;; 8 | 9 | DROP TABLE public.auth_permission CASCADE; 10 | 11 | --;; 12 | 13 | DROP TABLE public.auth_user CASCADE; 14 | 15 | --;; 16 | 17 | DROP TABLE public.auth_user_groups CASCADE; 18 | 19 | --;; 20 | 21 | DROP TABLE public.auth_user_user_permissions CASCADE; 22 | -------------------------------------------------------------------------------- /examples/controllers/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | controllers 7 | 8 | 9 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/controllers/src/backend/app/controllers/index.clj: -------------------------------------------------------------------------------- 1 | (ns controllers.index) 2 | 3 | (defn index-view 4 | [state] 5 | (assoc state 6 | :response 7 | {:status 200 8 | :headers {"Content-Type" "text/plain"} 9 | :body "Index page"})) 10 | 11 | (defn require-logged-in 12 | [{req :http-request :as state}] 13 | (if-let [authorization (get-in req [:headers "authorization"])] 14 | (assoc-in state [:session-data :authorization] authorization) 15 | (throw (ex-info "Unauthorized" {:status 401 :body "Unauthorized"})))) 16 | 17 | (defn something-else 18 | [state] 19 | state) 20 | 21 | ;; (defn comment-action [] 22 | ;; (controller-> 23 | ;; (validator/process-form) 24 | ;; (model/save-to-database) 25 | ;; (session/set-flash-message "Comment was saved") 26 | ;; (response/redirect :HERE) 27 | ;; 28 | ;; :invalid-comment (controller-> 29 | ;; (response/populate-form) 30 | ;; (view/set :error "Comment is invalid")) 31 | ;; )) 32 | 33 | (defn index 34 | [state] 35 | (index-view state)) 36 | -------------------------------------------------------------------------------- /examples/controllers/src/backend/app/controllers/re_frame.clj: -------------------------------------------------------------------------------- 1 | (ns controllers.re-frame 2 | (:require 3 | [ring.util.response :as response])) 4 | 5 | (defn index 6 | [state] 7 | (assoc state 8 | :response 9 | (-> "index.html" 10 | (response/resource-response {:root "public"}) 11 | (response/header "Content-Type" "text/html; charset=utf-8")))) 12 | -------------------------------------------------------------------------------- /examples/controllers/src/backend/app/core.clj: -------------------------------------------------------------------------------- 1 | (ns core 2 | (:require 3 | [interceptors] 4 | [piotr-yuxuan.closeable-map :refer [closeable-map]] 5 | [route] 6 | [xiana.coercion :as coercion] 7 | [xiana.config :as config] 8 | [xiana.interceptor :as xiana-interceptors] 9 | [xiana.interceptor.error] 10 | [xiana.route :as routes] 11 | [xiana.webserver :as ws])) 12 | 13 | (defn ->system 14 | [app-cfg] 15 | (-> (config/config) 16 | (merge app-cfg) 17 | routes/reset 18 | ws/start 19 | closeable-map)) 20 | 21 | (def app-cfg 22 | {:routes route/routes 23 | :controller-interceptors [(xiana-interceptors/muuntaja) 24 | xiana.interceptor.error/response 25 | interceptors/require-logged-in 26 | xiana-interceptors/params 27 | coercion/interceptor]}) 28 | 29 | (defn -main 30 | [& _args] 31 | (->system app-cfg)) 32 | -------------------------------------------------------------------------------- /examples/controllers/src/backend/app/interceptors.clj: -------------------------------------------------------------------------------- 1 | (ns interceptors) 2 | 3 | (def require-logged-in 4 | {:enter (fn [{request :request :as state}] 5 | (if-let [authorization (get-in request [:headers "authorization"])] 6 | (assoc-in state [:session-data :authorization] authorization) 7 | (throw (ex-info "Unauthorized" 8 | {:xiana/response 9 | {:status 401 :body "Unauthorized"}}))))}) 10 | -------------------------------------------------------------------------------- /examples/controllers/src/backend/app/my_domain_logic/siege_machines.clj: -------------------------------------------------------------------------------- 1 | (ns my-domain-logic.siege-machines) 2 | 3 | (def fetch-whole-db 4 | (constantly 5 | {:siege-machine [{:id 1 :name :trebuchet} 6 | {:id 2 :name :battering-ram :created #inst"2021-03-05T10"} 7 | {:id 3 :name "puppet on strings" :range "0 mm"}]})) 8 | 9 | (defn get-by-id 10 | [ctx] 11 | (let [data (fetch-whole-db (get-in ctx [:deps :db])) 12 | id (get-in ctx [:request :params :path :mydomain/id]) 13 | response (->> data 14 | :siege-machine 15 | (filter #(= id (:id %))) 16 | first)] 17 | (if response 18 | (assoc ctx :response {:status 200 19 | :body response}) 20 | (throw (ex-info "Not found" {:status 404 21 | :body "Not found"}))))) 22 | -------------------------------------------------------------------------------- /examples/controllers/src/backend/app/route.clj: -------------------------------------------------------------------------------- 1 | (ns route 2 | (:require 3 | [clojure.data.xml :as xml] 4 | [controllers.index :as index] 5 | [controllers.re-frame :as re-frame] 6 | [malli.core :as m] 7 | [malli.registry :as mr] 8 | [malli.util :as mu] 9 | [muuntaja.format.core] 10 | [my-domain-logic.siege-machines :as mydomain.siege-machines] 11 | [reitit.coercion.malli :as rcm] 12 | [reitit.ring :as ring] 13 | [xiana.handler :as handler]) 14 | (:import 15 | (clojure.data.xml 16 | Event) 17 | (clojure.lang 18 | Keyword))) 19 | 20 | (def registry 21 | (merge 22 | (m/default-schemas) 23 | (mu/schemas) 24 | {:mydomain/SiegeMachine [:map 25 | [:id int?] 26 | [:name keyword?] 27 | [:range {:optional true} int?] 28 | [:created {:optional true} inst?]] 29 | :mydomain/Infantry [:map 30 | [:id int?] 31 | [:name keyword?] 32 | [:attack {:optional true} int?]]})) 33 | 34 | (mr/set-default-registry! registry) 35 | 36 | (m/validate :mydomain/SiegeMachine {:id 1 :name :asd} {:registry registry}) 37 | (m/validate :mydomain/SiegeMachine {:id 1 :name :asd}) 38 | 39 | (extend-protocol xml/EventGeneration 40 | Keyword 41 | (gen-event [s] 42 | (Event. :chars nil nil (name s))) 43 | (next-events [_ next-items] 44 | next-items)) 45 | 46 | (defn xml-encoder 47 | [_options] 48 | (let [helper #(xml/emit-str 49 | (mapv (fn make-node 50 | [[f s]] 51 | (if (map? s) 52 | (xml/element f {} (map make-node (seq s))) 53 | (xml/element f {} s))) 54 | (seq %)))] 55 | (reify 56 | muuntaja.format.core/EncodeToBytes 57 | (encode-to-bytes [_ data charset] 58 | (.getBytes ^String (helper data) ^String charset))))) 59 | 60 | (def routes 61 | [["/" {:action index/index}] 62 | ["/re-frame" {:action re-frame/index}] 63 | ["" {:coercion (rcm/create {:registry registry})} 64 | ["/api/siege-machines/{mydomain/id}" {:hander handler/handler-fn 65 | :action mydomain.siege-machines/get-by-id 66 | :parameters {:path [:map [:mydomain/id int?]]} 67 | :responses {200 {:body :mydomain/SiegeMachine}}}]] 68 | ["/assets/*" (ring/create-resource-handler)]]) 69 | -------------------------------------------------------------------------------- /examples/controllers/src/frontend/controllers/config.cljs: -------------------------------------------------------------------------------- 1 | (ns controllers.config) 2 | 3 | (def debug? 4 | ^boolean goog.DEBUG) 5 | -------------------------------------------------------------------------------- /examples/controllers/src/frontend/controllers/core.cljs: -------------------------------------------------------------------------------- 1 | (ns controllers.core 2 | (:require 3 | [controllers.config :as config] 4 | [controllers.events :as events] 5 | [controllers.views :as views] 6 | [re-frame.core :as re-frame] 7 | [reagent.dom :as rdom])) 8 | 9 | (defn dev-setup 10 | [] 11 | (when config/debug? 12 | (println "dev mode"))) 13 | 14 | (defn ^:dev/after-load mount-root 15 | [] 16 | (re-frame/clear-subscription-cache!) 17 | (let [root-el (.getElementById js/document "app")] 18 | (rdom/unmount-component-at-node root-el) 19 | (rdom/render [views/main-panel] root-el))) 20 | 21 | (defn init 22 | [] 23 | (re-frame/dispatch-sync [::events/initialize-db]) 24 | (dev-setup) 25 | (mount-root)) 26 | -------------------------------------------------------------------------------- /examples/controllers/src/frontend/controllers/db.cljs: -------------------------------------------------------------------------------- 1 | (ns controllers.db) 2 | 3 | (def default-db 4 | {:name "re-frame"}) 5 | -------------------------------------------------------------------------------- /examples/controllers/src/frontend/controllers/events.cljs: -------------------------------------------------------------------------------- 1 | (ns controllers.events 2 | (:require 3 | [controllers.db :as db] 4 | [re-frame.core :as re-frame])) 5 | 6 | (re-frame/reg-event-db 7 | ::initialize-db 8 | (fn [_ _] 9 | db/default-db)) 10 | -------------------------------------------------------------------------------- /examples/controllers/src/frontend/controllers/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns controllers.subs 2 | (:require 3 | [re-frame.core :as re-frame])) 4 | 5 | (re-frame/reg-sub 6 | ::name 7 | (fn [db] 8 | (get db :name))) 9 | -------------------------------------------------------------------------------- /examples/controllers/src/frontend/controllers/views.cljs: -------------------------------------------------------------------------------- 1 | (ns controllers.views 2 | (:require 3 | [controllers.subs :as subs] 4 | [re-frame.core :as re-frame])) 5 | 6 | (defn main-panel 7 | [] 8 | (let [name (re-frame/subscribe [::subs/name])] 9 | [:div 10 | [:h1 "Hello from " @name]])) 11 | -------------------------------------------------------------------------------- /examples/controllers/src/frontend/deps.cljs: -------------------------------------------------------------------------------- 1 | {:npm-dev-deps {"shadow-cljs" "2.11.7"}} 2 | -------------------------------------------------------------------------------- /examples/frames/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /.prepl-port 12 | .hgignore 13 | .hg/ 14 | .cpcache/ 15 | -------------------------------------------------------------------------------- /examples/frames/README.md: -------------------------------------------------------------------------------- 1 | # frames 2 | 3 | ## Usage 4 | 5 | ### Start dockerized PostgreSQL 6 | 7 | docker compose up -d 8 | 9 | 10 | ### Compile front-end and run the application 11 | 12 | lein release 13 | 14 | lein run 15 | 16 | ### Open the app 17 | 18 | http://localhost:3000/ 19 | 20 | http://localhost:3000/status 21 | 22 | should response 23 | {"status":"OK"} 24 | -------------------------------------------------------------------------------- /examples/frames/config/dev/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/postgresql {:port 5432 2 | :dbname "frames" 3 | :host "localhost" 4 | :dbtype "postgresql" 5 | :user "postgres" 6 | :password "postgres"} 7 | 8 | :xiana/migration {:store :database 9 | :migration-dir "resources/migrations" 10 | :init-in-transaction? false 11 | :migration-table-name "migrations"} 12 | 13 | :xiana/web-server {:port 3000 14 | :join? false}} 15 | 16 | -------------------------------------------------------------------------------- /examples/frames/config/test/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/web-server {:port 3333 2 | :join? false}} 3 | 4 | -------------------------------------------------------------------------------- /examples/frames/dev/cljs/user.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs.user 2 | "Commonly used symbols for easy access in the ClojureScript REPL during 3 | development." 4 | (:require 5 | [cljs.repl :refer (Error->map apropos dir doc error->str ex-str ex-triage 6 | find-doc print-doc pst source)] 7 | [clojure.pprint :refer (pprint)] 8 | [clojure.string :as str])) 9 | -------------------------------------------------------------------------------- /examples/frames/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | db: 4 | image: postgres:14-alpine 5 | environment: 6 | - POSTGRES_DB=frames 7 | - POSTGRES_USER=postgres 8 | - POSTGRES_PASSWORD=postgres 9 | volumes: 10 | - db-data:/var/lib/postgresql/data 11 | ports: 12 | - "5432:5432" 13 | 14 | volumes: 15 | db-data: 16 | -------------------------------------------------------------------------------- /examples/frames/project.clj: -------------------------------------------------------------------------------- 1 | (defproject frames "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :min-lein-version "2.0.0" 4 | :dependencies [[com.flexiana/framework "0.5.0-rc4"]] 5 | :plugins [[lein-shadow "0.4.0"]] 6 | :main ^:skip-aot frames.core 7 | :uberjar-name "frames.jar" 8 | :source-paths ["src/backend/" "src/frontend"] 9 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 10 | :profiles {:dev {:resource-paths ["config/dev"] 11 | :dependencies [[binaryage/devtools "1.0.5"]]} 12 | :local {:resource-paths ["config/local" "resources"]} 13 | :prod {:resource-paths ["config/prod" "resources"]} 14 | :test {:resource-paths ["config/test" "resources"] 15 | :dependencies [[clj-http "3.12.3"] 16 | [mvxcvi/cljstyle "0.15.0" 17 | :exclusions [org.clojure/clojure]]]}} 18 | :shadow-cljs {:nrepl {:port 8777} 19 | 20 | :builds {:app {:target :browser 21 | :output-dir "resources/public/js/compiled" 22 | :asset-path "/js/compiled" 23 | :modules {:app {:init-fn donor.core/init 24 | :preloads [devtools.preload]}}}}} 25 | :aliases {"check-style" ["with-profile" "+test" "run" "-m" "cljstyle.main" "check"] 26 | "ci" ["do" "clean," "cloverage," "lint," "uberjar"] 27 | "kondo" ["run" "-m" "clj-kondo.main" "--lint" "src" "test"] 28 | "lint" ["do" "kondo," "eastwood," "kibit"] 29 | "watch" ["with-profile" "dev" "do" 30 | ["shadow" "watch" "app" "browser-test" "karma-test"]] 31 | "release" ["with-profile" "prod" "do" 32 | ["shadow" "release" "app"]]}) 33 | -------------------------------------------------------------------------------- /examples/frames/resources/public/assets/Clojure-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/frames/resources/public/assets/Clojure-icon.png -------------------------------------------------------------------------------- /examples/frames/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | donor 7 | 8 | 9 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/frames/src/backend/frames/controllers/index.clj: -------------------------------------------------------------------------------- 1 | (ns frames.controllers.index 2 | (:require 3 | [ring.util.response :as response])) 4 | 5 | (defn handle-index 6 | [state] 7 | (assoc state :response 8 | (-> "index.html" 9 | (response/resource-response {:root "public"}) 10 | (response/header "Content-Type" "text/html; charset=utf-8")))) 11 | -------------------------------------------------------------------------------- /examples/frames/src/backend/frames/controllers/status.clj: -------------------------------------------------------------------------------- 1 | (ns frames.controllers.status 2 | (:require 3 | [ring.util.response :as response])) 4 | 5 | (defn handle-status 6 | [state] 7 | (assoc state :response 8 | (response/response {:status "OK"}))) 9 | -------------------------------------------------------------------------------- /examples/frames/src/backend/frames/core.clj: -------------------------------------------------------------------------------- 1 | (ns frames.core 2 | (:require 3 | [frames.controllers.index :as index] 4 | [frames.controllers.status :as status] 5 | [piotr-yuxuan.closeable-map :refer [closeable-map]] 6 | [reitit.ring :as ring] 7 | [xiana.config :as config] 8 | [xiana.interceptor :as interceptor] 9 | [xiana.route :as router] 10 | [xiana.webserver :as ws])) 11 | 12 | (def routes 13 | [["/" {:get {:action index/handle-index}}] 14 | ["/status" {:get {:action status/handle-status}}] 15 | ["/assets/*" (ring/create-resource-handler {:path "/"})]]) 16 | 17 | (defn ->system 18 | [app-cfg] 19 | (-> (config/config) 20 | (merge app-cfg) 21 | router/reset 22 | ws/start 23 | (select-keys [:routes 24 | :webserver 25 | :controller-interceptors]) 26 | closeable-map)) 27 | 28 | (def app-cfg 29 | {:routes routes 30 | :controller-interceptors [(interceptor/muuntaja)]}) 31 | 32 | (defn -main 33 | [& args] 34 | (->system app-cfg)) 35 | -------------------------------------------------------------------------------- /examples/frames/src/frontend/deps.cljs: -------------------------------------------------------------------------------- 1 | {:npm-dev-deps {"shadow-cljs" "2.11.7"}} 2 | -------------------------------------------------------------------------------- /examples/frames/src/frontend/donor/config.cljs: -------------------------------------------------------------------------------- 1 | (ns donor.config) 2 | 3 | (def debug? 4 | ^boolean goog.DEBUG) 5 | -------------------------------------------------------------------------------- /examples/frames/src/frontend/donor/core.cljs: -------------------------------------------------------------------------------- 1 | (ns donor.core 2 | (:require 3 | [donor.config :as config] 4 | [donor.events :as events] 5 | [donor.views :as views] 6 | [re-frame.core :as re-frame] 7 | [reagent.dom :as rdom])) 8 | 9 | (defn dev-setup 10 | [] 11 | (when config/debug? 12 | (println "dev mode"))) 13 | 14 | (defn ^:dev/after-load mount-root 15 | [] 16 | (re-frame/clear-subscription-cache!) 17 | (let [root-el (.getElementById js/document "app")] 18 | (rdom/unmount-component-at-node root-el) 19 | (rdom/render [views/main-panel] root-el))) 20 | 21 | (defn init 22 | [] 23 | (re-frame/dispatch-sync [::events/initialize-db]) 24 | (dev-setup) 25 | (mount-root)) 26 | -------------------------------------------------------------------------------- /examples/frames/src/frontend/donor/db.cljs: -------------------------------------------------------------------------------- 1 | (ns donor.db) 2 | 3 | (def default-db 4 | {:name "re-frame"}) 5 | -------------------------------------------------------------------------------- /examples/frames/src/frontend/donor/events.cljs: -------------------------------------------------------------------------------- 1 | (ns donor.events 2 | (:require 3 | [donor.db :as db] 4 | [re-frame.core :as re-frame])) 5 | 6 | (re-frame/reg-event-db 7 | ::initialize-db 8 | (fn [_ _] 9 | db/default-db)) 10 | -------------------------------------------------------------------------------- /examples/frames/src/frontend/donor/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns donor.subs 2 | (:require 3 | [re-frame.core :as re-frame])) 4 | 5 | (re-frame/reg-sub 6 | ::name 7 | (fn [db] 8 | (get db :name))) 9 | -------------------------------------------------------------------------------- /examples/frames/src/frontend/donor/views.cljs: -------------------------------------------------------------------------------- 1 | (ns donor.views 2 | (:require 3 | [donor.subs :as subs] 4 | [re-frame.core :as re-frame])) 5 | 6 | (defn main-panel 7 | [] 8 | (let [name (re-frame/subscribe [::subs/name])] 9 | [:div 10 | [:h1 "Hello from " @name]])) 11 | -------------------------------------------------------------------------------- /examples/frames/test/asset_test.clj: -------------------------------------------------------------------------------- 1 | (ns asset-test 2 | (:require 3 | [clojure.test :refer :all] 4 | [frames.core :as frames] 5 | [xiana.handler :refer [handler-fn]])) 6 | 7 | (deftest status-test 8 | (with-open [deps (frames/->system frames/app-cfg)] 9 | (let [request {:uri "/assets/Clojure-icon.png" 10 | :request-method :get} 11 | handle (handler-fn deps) 12 | visit (update (handle request) :body slurp)] 13 | (is (= 200 (:status visit))) 14 | (is (= (slurp "resources/public/assets/Clojure-icon.png") 15 | (:body visit)))))) 16 | -------------------------------------------------------------------------------- /examples/frames/test/status_test.clj: -------------------------------------------------------------------------------- 1 | (ns status-test 2 | (:require 3 | [clojure.test :refer :all] 4 | [frames.core :as frames] 5 | [xiana.handler :refer [handler-fn]])) 6 | 7 | (deftest status-test 8 | (with-open [deps (frames/->system frames/app-cfg)] 9 | (let [request {:uri "/status" 10 | :request-method :get} 11 | handle (handler-fn deps) 12 | visit (update (handle request) :body slurp)] 13 | (is (= 200 (:status visit))) 14 | (is (= "{\"status\":\"OK\"}" (:body visit)))))) 15 | -------------------------------------------------------------------------------- /examples/jwt/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | /resources/js/public/js/compiled 5 | profiles.clj 6 | pom.xml 7 | pom.xml.asc 8 | *.jar 9 | *.class 10 | /.lein-* 11 | /.nrepl-port 12 | /.prepl-port 13 | .hgignore 14 | .hg/ 15 | /.shadow-cljs/ 16 | /node_modules/ 17 | /package.json 18 | /package-lock.json 19 | /shadow-cljs.edn 20 | -------------------------------------------------------------------------------- /examples/jwt/Docker/db.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:14-alpine 2 | COPY init.sql /docker-entrypoint-initdb.d/ -------------------------------------------------------------------------------- /examples/jwt/Docker/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE controllers; 2 | GRANT ALL PRIVILEGES ON DATABASE controllers TO postgres; 3 | -------------------------------------------------------------------------------- /examples/jwt/config/dev/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/session-backend {:storage :database 2 | :session-table-name :sessions 3 | :port 5432 4 | :dbname "sessions" 5 | :host "localhost" 6 | :dbtype "postgresql" 7 | :user "sessions" 8 | :password "postgres"} 9 | :xiana/web-server {:port 3000 10 | :join? false} 11 | :xiana/jwt {:auth 12 | {:alg :rs256 13 | :public-key "$jwt-public-key | resources/_files/jwtRS256.key.pub" 14 | :private-key "$jwt-private-key | resources/_files/jwtRS256.key" 15 | :in-claims {:iss "xiana-api" 16 | :aud "api-consumer" 17 | :leeway 0 18 | :max-age 1000} 19 | :out-claims {:exp 1000 20 | :iss "xiana-api" 21 | :aud "api-consumer" 22 | :nbf 0}}}} 23 | 24 | -------------------------------------------------------------------------------- /examples/jwt/config/test/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/session-backend {:storage :database 2 | :session-table-name :sessions 3 | :port 5432 4 | :dbname "sessions" 5 | :host "localhost" 6 | :dbtype "postgresql" 7 | :user "sessions" 8 | :password "postgres"} 9 | :xiana/web-server {:port 3333 10 | :join? false} 11 | :xiana/jwt {:auth 12 | {:alg :rs256 13 | :public-key "$jwt-public-key | resources/_files/jwtRS256.key.pub" 14 | :private-key "$jwt-private-key | resources/_files/jwtRS256.key" 15 | :in-claims {:iss "xiana-api" 16 | :aud "api-consumer" 17 | :leeway 0 18 | :max-age 40} 19 | :out-claims {:exp 1000 20 | :iss "xiana-api" 21 | :aud "api-consumer" 22 | :nbf 0}}}} 23 | -------------------------------------------------------------------------------- /examples/jwt/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/backend" "resources"] 2 | :deps {com.flexiana/framework {:mvn/version "0.5.0-rc4"}} 3 | :aliases 4 | {:run 5 | {:extra-paths ["config/dev"] 6 | :main-opts [-m "app.core"]} 7 | 8 | :dev 9 | {:extra-paths ["config/dev" "../.."] 10 | :extra-deps {com.flexiana/framework {:local/root "../.."}}} 11 | 12 | :format 13 | {:replace-deps {mvxcvi/cljstyle {:mvn/version "0.15.0"}} 14 | :main-opts ["-m" "cljstyle.main"]} 15 | 16 | :test 17 | {:extra-paths ["config/test" "test"] 18 | :extra-deps {clj-test-containers/clj-test-containers {:mvn/version "0.5.0"} 19 | clj-http/clj-http {:mvn/version "3.12.3"} 20 | http.async.client/http.async.client {:mvn/version "1.3.1"} 21 | com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner" 22 | :sha "4e7e1c0dfd5291fa2134df052443dc29695d8cbe"} 23 | org.testcontainers/testcontainers {:mvn/version "1.16.3"}} 24 | :main-opts ["-m" "cognitect.test-runner" "-d" "test"]}}} 25 | -------------------------------------------------------------------------------- /examples/jwt/resources/_files/jwtRS256.key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqteE1o/LJCNV+F3rMtOP 3 | rR2anM0cPFq9aaV9/QZRIcKZHbqVZaOZ2kAhrxUJiKV1NBdF9cDX+iou+/X92XAF 4 | Y65FbUZ5nmhpvy0qqKh1TUFbHKJa6V9pF9Ax1m4qyyyRqLC20pEegyGGX7gVczZi 5 | 1Psl1NdD1Q37VSALrOckvyPlP2NjSEYUBuT8MkK19COhSwn6oqiI8zjXDNdcKG27 6 | zj9wiUKAOPN9NMy806+1NcY8v1dijimDENyBwdNoFqG1cIF9I8+UZM4U9Mq5bZrf 7 | 7w44l2+QRlqn8W6mKKx/IkkZegeSSgS/We12fuhFelbfz7EXIc5cqXbvzo5KeKu2 8 | j3V33ZI/XcBlGRtfbYU7z0/i57FWBb2pL+nqOsKUVo1YWCsPAHJPdQHBcqA1i78a 9 | w1JqqnTfNkfwmOADD50H7P88oGOuJphlwYA43mTZ9RopPt98VBwTy3KjfCfMDEr0 10 | tpjBUY+Z7pZAZe7H3T26C6PzMXa54ZxifUUVISv+1Fmw2swUDzLvsYecQwLl3wc8 11 | W5Or4mQFEkLNqQUYWPGRePYpcQsPA51XA5wwPq76EXkAXDO4AUsRFSl2zwdjF5TF 12 | N/UAysSNkg9tRUGz3LvJ5vgvPTI6YruklvbdK9Z2+Hy8ZACQtTK4jxGO79MZaQZe 13 | mfGYi0sFmXqxIO01ea6roesCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /examples/jwt/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | controllers 7 | 8 | 9 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/jwt/src/backend/app/controllers/index.clj: -------------------------------------------------------------------------------- 1 | (ns app.controllers.index) 2 | 3 | (defn index 4 | [state] 5 | (let [user (:session-data state) 6 | body (if user 7 | (format "Index page, for %s" (:first-name user)) 8 | "Index page")] 9 | (assoc state 10 | :response 11 | {:status 200 12 | :headers {"Content-Type" "text/plain"} 13 | :body body}))) 14 | -------------------------------------------------------------------------------- /examples/jwt/src/backend/app/controllers/login.clj: -------------------------------------------------------------------------------- 1 | (ns app.controllers.login 2 | (:require 3 | [xiana.jwt :as jwt] 4 | [xiana.route.helpers :as helpers])) 5 | 6 | (def db 7 | [{:id 1 8 | :email "xiana@test.com" 9 | :first-name "Xiana" 10 | :last-name "Developer" 11 | :password "topsecret"}]) 12 | 13 | (defn find-user 14 | [email] 15 | (first (filter (fn [i] 16 | (= email (:email i))) db))) 17 | 18 | (defn missing-credentials 19 | [state] 20 | (assoc state :response {:status 401 21 | :body "Missing credentials"})) 22 | 23 | (defn login-controller 24 | [{request :request :as state}] 25 | (try (let [rbody (or (:body-params request) 26 | (throw (ex-message "Missing body"))) 27 | user (find-user (:email rbody))] 28 | (if (and user (= (:password user) (:password rbody))) 29 | (let [cfg (get-in state [:deps :xiana/jwt :auth]) 30 | jwt-token (jwt/sign :claims (dissoc user :password) cfg)] 31 | (assoc state :response {:status 200 :body {:auth-token jwt-token}})) 32 | (helpers/unauthorized state "Incorrect credentials"))) 33 | (catch Exception e 34 | (println e) 35 | (missing-credentials state)))) 36 | -------------------------------------------------------------------------------- /examples/jwt/src/backend/app/controllers/secret.clj: -------------------------------------------------------------------------------- 1 | (ns app.controllers.secret) 2 | 3 | (defn protected-controller 4 | [state] 5 | (assoc state 6 | :response {:status 200 7 | :headers {"Content-Type" "application/json"} 8 | :body (str "Hello " (get-in state [:session-data :jwt-authentication :first-name]) ". request content: " (get-in state [:request :body-params]))})) 9 | -------------------------------------------------------------------------------- /examples/jwt/src/backend/app/core.clj: -------------------------------------------------------------------------------- 1 | (ns app.core 2 | (:require 3 | [app.controllers.index :as index] 4 | [app.controllers.login :as login] 5 | [app.controllers.secret :as secret] 6 | [xiana.config :as config] 7 | [xiana.handler :as x-handler] 8 | [xiana.interceptor :as x-interceptors] 9 | [xiana.interceptor.error] 10 | [xiana.jwt :as jwt] 11 | [xiana.jwt.action :as jwt-a] 12 | [xiana.jwt.interceptors :as jwt-interceptors] 13 | [xiana.route :as x-routes] 14 | [xiana.webserver :as ws])) 15 | 16 | (def routes 17 | [["" {:handler x-handler/handler-fn}] 18 | ["/" {:action index/index}] 19 | ["/login" {:post 20 | {:action login/login-controller 21 | :interceptors {:except [jwt-interceptors/jwt-auth]}}}] 22 | ["/secret" {:post 23 | {:action secret/protected-controller}}] 24 | ["/token" {:get 25 | {:action #'jwt-a/refresh-token}}]]) 26 | 27 | (defn ->system 28 | [app-cfg] 29 | (-> (config/config app-cfg) 30 | jwt/init-from-file 31 | x-routes/reset 32 | ws/start)) 33 | 34 | (def app-cfg 35 | {:routes routes 36 | :controller-interceptors [(x-interceptors/muuntaja) 37 | xiana.interceptor.error/response 38 | x-interceptors/params 39 | jwt-interceptors/jwt-auth]}) 40 | 41 | (defn -main 42 | [& _args] 43 | (->system app-cfg)) 44 | -------------------------------------------------------------------------------- /examples/jwt/test/app/controllers/secret_test.clj: -------------------------------------------------------------------------------- 1 | (ns app.controllers.secret-test 2 | (:require 3 | [app.controllers.login :refer [login-controller]] 4 | [app.controllers.secret :refer [protected-controller]] 5 | [clojure.test :refer :all] 6 | [xiana.jwt :as jwt] 7 | [xiana.jwt.interceptors :as jwt-interceptors])) 8 | 9 | (defn auth-token 10 | [config] 11 | (let [state {:request {:body-params {:email "xiana@test.com" 12 | :password "topsecret"}} 13 | :deps config}] 14 | (-> (login-controller state) 15 | :response 16 | :body 17 | :auth-token))) 18 | 19 | (deftest protected-controller-test 20 | (let [interceptor (:enter jwt-interceptors/jwt-auth) 21 | private-key-file "resources/_files/jwtRS256.key" 22 | public-key-file "resources/_files/jwtRS256.key.pub" 23 | config (jwt/init-from-file 24 | {:xiana/jwt {:auth {:alg :rs256 25 | :public-key public-key-file 26 | :private-key private-key-file 27 | :in-claims {:iss "xiana-api" 28 | :aud "api-consumer" 29 | :leeway 0 30 | :max-age 40} 31 | :out-claims {:exp 1000 32 | :iss "xiana-api" 33 | :aud "api-consumer" 34 | :nbf 0}}}}) 35 | auth (auth-token config) 36 | request {:headers {:authorization (str "Bearer " auth)} 37 | :body-params {:hello "hello"}}] 38 | (is (= "Hello Xiana. request content: {:hello \"hello\"}" 39 | (-> {:request request 40 | :deps config} 41 | interceptor 42 | protected-controller 43 | :response 44 | :body))))) 45 | -------------------------------------------------------------------------------- /examples/jwt/test/jwt_fixture.clj: -------------------------------------------------------------------------------- 1 | (ns jwt-fixture 2 | (:require 3 | [app.core :refer [app-cfg ->system]] 4 | [piotr-yuxuan.closeable-map :refer [closeable-map]])) 5 | 6 | (defonce test-system (atom {})) 7 | 8 | (defn std-system-fixture 9 | [f] 10 | (with-open [_ (reset! test-system 11 | (-> app-cfg 12 | ->system 13 | closeable-map))] 14 | (f) 15 | (reset! test-system {}))) 16 | -------------------------------------------------------------------------------- /examples/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | /resources/js/public/js/compiled 5 | profiles.clj 6 | pom.xml 7 | pom.xml.asc 8 | *.jar 9 | *.class 10 | /.lein-* 11 | /.nrepl-port 12 | /.prepl-port 13 | .hgignore 14 | .hg/ 15 | /.shadow-cljs/ 16 | /node_modules/ 17 | /package.json 18 | /package-lock.json 19 | /shadow-cljs.edn 20 | -------------------------------------------------------------------------------- /examples/sessions/Docker/db.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:14-alpine 2 | COPY init.sql /docker-entrypoint-initdb.d/ -------------------------------------------------------------------------------- /examples/sessions/Docker/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE controllers; 2 | GRANT ALL PRIVILEGES ON DATABASE controllers TO postgres; 3 | -------------------------------------------------------------------------------- /examples/sessions/README.md: -------------------------------------------------------------------------------- 1 | # Sessions 2 | 3 | Example that showcase the use of session interceptors using Xiana. 4 | 5 | ## Advice to test: 6 | 7 | Observe what happens when the backend application is restarting. Check if already logged-in user is still logged in, or 8 | it's lost his/her session data? 9 | 10 | ## Spin up database 11 | 12 | ```bash 13 | docker-compose up -d 14 | ``` 15 | 16 | starts the database for persisting sessions. See `docker-compose.yml` and `init.sql` how the the database and 17 | the `sessions` table is setting up. 18 | 19 | ## Run the backend 20 | 21 | ```bash 22 | lein run 23 | ``` 24 | 25 | ## Run manual tests against the application 26 | 27 | There are 4 endpoints provided: 28 | 29 | - localhost:3000/ 30 | 31 | > if you don't have valid session it returns 32 | > 33 | > ```clojure 34 | > {:status 200, :body "Index page"} 35 | > ``` 36 | > 37 | >with valid session it returns 38 | > 39 | > ```clojure 40 | > {:status 200, :body "Index page, for Piotr"} 41 | > ``` 42 | 43 | - localhost:3000/secret 44 | 45 | > if you don't have valid session it returns 46 | > 47 | > ```clojure 48 | > {:status 401, :body "Invalid or missing session"} 49 | > ``` 50 | > 51 | >with valid session it returns 52 | > 53 | > ```clojure 54 | > {:status 200, :body "Hello Piotr"} 55 | > ``` 56 | 57 | - localhost:3000/login 58 | 59 | > request should look like: 60 | >```clojure 61 | > {:method :post 62 | > :body {:email "piotr@example.com" 63 | > :password "topsecret"}} 64 | >``` 65 | >returns: 66 | >```clojure 67 | >{:status 200 68 | > :body {:session-id {{session-id}} 69 | > :user {"first-name" "Piotr" 70 | > "id" 1 71 | > "email" "piotr@example.com" 72 | > "last-name" "Developer"}}} 73 | >``` 74 | > 75 | >Without the request body, or with wrong HTTP method it returns: 76 | > ```clojure 77 | > {:status 401 78 | > :body "Missing credentials"} 79 | >``` 80 | 81 | - localhost:3000/logout 82 | 83 | > if you have valid session it returns 84 | > 85 | > ```clojure 86 | > {:status 200 :body "Piotr logged out"} 87 | > ``` 88 | > 89 | > and it clears the session you had. 90 | 91 | You can provide the session-id from login response in the request's headers 92 | > ```clojure 93 | > {:headers {:session-id {{session-id}}}} 94 | >``` 95 | 96 | ## Run integration tests 97 | 98 | ```bash 99 | lein test 100 | ``` 101 | -------------------------------------------------------------------------------- /examples/sessions/config/dev/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/session-backend {:storage :database 2 | :session-table-name :sessions 3 | :port 5432 4 | :dbname "sessions" 5 | :host "localhost" 6 | :dbtype "postgresql" 7 | :user "sessions" 8 | :password "postgres"} 9 | :xiana/web-server {:port 3000 10 | :join? false}} 11 | 12 | -------------------------------------------------------------------------------- /examples/sessions/config/test/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/session-backend {:storage :database 2 | :session-table-name :sessions 3 | :port 5432 4 | :dbname "sessions" 5 | :host "localhost" 6 | :dbtype "postgresql" 7 | :user "sessions" 8 | :password "postgres"} 9 | :xiana/web-server {:port 3333 10 | :join? false}} 11 | -------------------------------------------------------------------------------- /examples/sessions/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | db: 4 | image: postgres:14-alpine 5 | volumes: 6 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql 7 | - db-data:/var/lib/postgresql/data 8 | ports: 9 | - "5432:5432" 10 | environment: 11 | - POSTGRES_DB=sessions 12 | - POSTGRES_USER=sessions 13 | - POSTGRES_PASSWORD=postgres 14 | 15 | volumes: 16 | db-data: 17 | -------------------------------------------------------------------------------- /examples/sessions/init.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 2 | 3 | --;; 4 | 5 | CREATE TABLE sessions ( 6 | session_data json not null, 7 | session_id uuid primary key, 8 | modified_at timestamp DEFAULT CURRENT_TIMESTAMP 9 | ); 10 | -------------------------------------------------------------------------------- /examples/sessions/postgres-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose -f docker-compose.yml up --no-start 3 | docker-compose -f docker-compose.yml start -------------------------------------------------------------------------------- /examples/sessions/project.clj: -------------------------------------------------------------------------------- 1 | (defproject sessions "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :min-lein-version "2.0.0" 4 | :dependencies [[com.flexiana/framework "0.5.0-rc4"]] 5 | :plugins [[lein-shadow "0.4.0"] 6 | [lein-shell "0.5.0"] 7 | [migratus-lein "0.7.3"]] 8 | :main ^:skip-aot app.core 9 | :uberjar-name "frames.jar" 10 | :source-paths ["src/backend/"] 11 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 12 | :profiles {:dev {:resource-paths ["config/dev"] 13 | :dependencies [[binaryage/devtools "1.0.5"]]} 14 | :frontend {:source-paths ["src/frontend"] 15 | :dependencies [[thheller/shadow-cljs "2.19.0"] 16 | [re-frame "1.2.0"]]} 17 | :local {:resource-paths ["config/local"]} 18 | :prod {:resource-paths ["config/prod"]} 19 | :test {:resource-paths ["config/test"] 20 | :dependencies [[clj-http "3.12.3"] 21 | [mvxcvi/cljstyle "0.15.0" 22 | :exclusions [org.clojure/clojure]]]}} 23 | :shadow-cljs {:nrepl {:port 8777} 24 | :builds {:app {:target :browser 25 | :output-dir "resources/public/js/compiled" 26 | :asset-path "/js/compiled" 27 | :modules {:app {:init-fn controllers.core/init 28 | :preloads [devtools.preload]}}}}} 29 | :aliases {"check-style" ["with-profile" "+test" "run" "-m" "cljstyle.main" "check"] 30 | "ci" ["do" "clean," "cloverage," "lint," "uberjar"] 31 | "kondo" ["run" "-m" "clj-kondo.main" "--lint" "src" "test"] 32 | "lint" ["do" "kondo," "eastwood," "kibit"] 33 | "watch" ["with-profile" "dev,frontend" "do" 34 | ["shadow" "watch" "app" "browser-test" "karma-test"]] 35 | "release" ["with-profile" "prod,frontend" "do" 36 | ["shadow" "release" "app"]]} 37 | :migratus {:store :database 38 | :migration-dir "migrations" 39 | :db {:classname "com.mysql.jdbc.Driver" 40 | :subprotocol "postgres" 41 | :subname "//localhost/controllers" 42 | :user "postgres" 43 | :password "postgres"}}) 44 | -------------------------------------------------------------------------------- /examples/sessions/resources/migrations/20210205095919-add-auth-tables.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.auth_group CASCADE; 2 | 3 | --;; 4 | 5 | DROP TABLE public.auth_group_permissions CASCADE; 6 | 7 | --;; 8 | 9 | DROP TABLE public.auth_permission CASCADE; 10 | 11 | --;; 12 | 13 | DROP TABLE public.auth_user CASCADE; 14 | 15 | --;; 16 | 17 | DROP TABLE public.auth_user_groups CASCADE; 18 | 19 | --;; 20 | 21 | DROP TABLE public.auth_user_user_permissions CASCADE; 22 | -------------------------------------------------------------------------------- /examples/sessions/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | controllers 7 | 8 | 9 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/sessions/src/backend/app/controllers/index.clj: -------------------------------------------------------------------------------- 1 | (ns app.controllers.index) 2 | 3 | (defn index 4 | [state] 5 | (let [user (get-in state [:session-data :user]) 6 | body (if user 7 | (format "Index page, for %s" (:first-name user)) 8 | "Index page")] 9 | (assoc state 10 | :response 11 | {:status 200 12 | :headers {"Content-Type" "text/plain"} 13 | :body body}))) 14 | -------------------------------------------------------------------------------- /examples/sessions/src/backend/app/controllers/login.clj: -------------------------------------------------------------------------------- 1 | (ns app.controllers.login 2 | (:require 3 | [jsonista.core :as j] 4 | [ring.util.request :refer [body-string]] 5 | [xiana.session :as session])) 6 | 7 | (def db 8 | [{:id 1 9 | :email "piotr@example.com" 10 | :first-name "Piotr" 11 | :last-name "Developer" 12 | :password "topsecret"}]) 13 | 14 | (defn find-user 15 | [email] 16 | (first (filter (fn [i] 17 | (= email (:email i))) db))) 18 | 19 | (defn missing-credentials 20 | [state] 21 | (assoc state :response {:status 401 22 | :body "Missing credentials"})) 23 | 24 | (defn login-controller 25 | [{request :request :as state}] 26 | (try (let [rbody (or (some-> request 27 | body-string 28 | (j/read-value j/keyword-keys-object-mapper)) 29 | (throw (ex-message "Missing body"))) 30 | user (find-user (:email rbody)) 31 | session-id (random-uuid) 32 | session-data {:session-id session-id 33 | :user (dissoc user :password)}] 34 | (if (and user (= (:password user) (:password rbody))) 35 | (let [session-backend (get-in state [:deps :session-backend])] 36 | (session/add! session-backend session-id session-data) 37 | (assoc state 38 | :response {:status 200 39 | :headers {"Content-Type" "application/json" 40 | "Session-id" (str session-id)} 41 | :body (j/write-value-as-string (update session-data :session-id str))})) 42 | (assoc state :response {:status 401 43 | :body "Incorrect credentials"}))) 44 | (catch Exception _ (missing-credentials state)))) 45 | -------------------------------------------------------------------------------- /examples/sessions/src/backend/app/controllers/logout.clj: -------------------------------------------------------------------------------- 1 | (ns app.controllers.logout) 2 | 3 | (defn logout-controller 4 | [state] 5 | (let [user (get-in state [:session-data :user])] 6 | (assoc state 7 | :response {:status 200 8 | :headers {"Content-Type" "application/json"} 9 | :body (str (:first-name user) " logged out")}))) 10 | -------------------------------------------------------------------------------- /examples/sessions/src/backend/app/controllers/secret.clj: -------------------------------------------------------------------------------- 1 | (ns app.controllers.secret) 2 | 3 | (defn protected-controller 4 | [state] 5 | (assoc state 6 | :response {:status 200 7 | :headers {"Content-Type" "application/json"} 8 | :body (str "Hello " (get-in state [:session-data :user :first-name]))})) 9 | -------------------------------------------------------------------------------- /examples/sessions/src/backend/app/core.clj: -------------------------------------------------------------------------------- 1 | (ns app.core 2 | (:require 3 | [app.controllers.index :as index] 4 | [app.controllers.login :as login] 5 | [app.controllers.logout :as logout] 6 | [app.controllers.secret :as secret] 7 | [app.interceptors :refer [inject-session? 8 | logout 9 | require-logged-in]] 10 | [piotr-yuxuan.closeable-map :refer [closeable-map]] 11 | [xiana.config :as config] 12 | [xiana.handler :as x-handler] 13 | [xiana.interceptor :as x-interceptors] 14 | [xiana.interceptor.error] 15 | [xiana.route :as x-routes] 16 | [xiana.session :as x-session] 17 | [xiana.webserver :as ws])) 18 | 19 | (def routes 20 | [["" {:handler x-handler/handler-fn}] 21 | ["/" {:action index/index 22 | :interceptors [x-interceptors/params 23 | inject-session?]}] 24 | ["/login" {:action login/login-controller 25 | :interceptors {:except [x-session/interceptor]}}] 26 | ["/logout" {:action logout/logout-controller 27 | :interceptors {:around [logout]}}] 28 | ["/secret" {:action secret/protected-controller 29 | :interceptors {:inside [require-logged-in]}}]]) 30 | 31 | (defn ->system 32 | [app-cfg] 33 | (-> (config/config) 34 | (merge app-cfg) 35 | x-routes/reset 36 | x-session/init-backend 37 | ws/start 38 | closeable-map)) 39 | 40 | (def app-cfg 41 | {:routes routes 42 | :controller-interceptors [(x-interceptors/muuntaja) 43 | xiana.interceptor.error/response 44 | x-interceptors/params 45 | x-session/interceptor]}) 46 | 47 | (defn -main 48 | [& _args] 49 | (->system app-cfg)) 50 | -------------------------------------------------------------------------------- /examples/sessions/src/backend/app/interceptors.clj: -------------------------------------------------------------------------------- 1 | (ns app.interceptors 2 | (:require 3 | [xiana.session :refer [delete! fetch]])) 4 | 5 | (def require-logged-in 6 | {:name ::require-logged-in 7 | :enter 8 | (fn [state] 9 | (if (get-in state [:session-data :user]) 10 | state 11 | (throw (ex-info "Unauthorized" {:xiana/response {:status 401 :body "Unauthorized"}}))))}) 12 | 13 | (def logout 14 | {:name ::logout 15 | :leave (fn [state] 16 | (let [sessions-backend (-> state 17 | :deps 18 | :session-backend) 19 | session-id (-> state 20 | :session-data 21 | :session-id)] 22 | (delete! sessions-backend session-id) 23 | (dissoc state :session-data)))}) 24 | 25 | (def inject-session? 26 | {:name ::inject-session? 27 | :enter (fn [{{headers :headers 28 | cookies :cookies 29 | query-params :query-params} :request 30 | :as state}] 31 | (try (let [session-backend (-> state :deps :session-backend) 32 | session-id (parse-uuid (or (some->> headers 33 | :session-id) 34 | (some->> cookies 35 | :session-id 36 | :value) 37 | (some->> query-params 38 | :SESSIONID))) 39 | session-data (fetch session-backend session-id)] 40 | (assoc state :session-data (assoc session-data :session-id session-id))) 41 | (catch Exception _ ; TODO: catch more specific exception or rethink that 42 | state)))}) 43 | -------------------------------------------------------------------------------- /examples/sessions/src/frontend/controllers/config.cljs: -------------------------------------------------------------------------------- 1 | (ns controllers.config) 2 | 3 | (def debug? 4 | ^boolean goog.DEBUG) 5 | -------------------------------------------------------------------------------- /examples/sessions/src/frontend/controllers/core.cljs: -------------------------------------------------------------------------------- 1 | (ns controllers.core 2 | (:require 3 | [controllers.config :as config] 4 | [controllers.events :as events] 5 | [controllers.views :as views] 6 | [re-frame.core :as re-frame] 7 | [reagent.dom :as rdom])) 8 | 9 | (defn dev-setup 10 | [] 11 | (when config/debug? 12 | (println "dev mode"))) 13 | 14 | (defn ^:dev/after-load mount-root 15 | [] 16 | (re-frame/clear-subscription-cache!) 17 | (let [root-el (.getElementById js/document "app")] 18 | (rdom/unmount-component-at-node root-el) 19 | (rdom/render [views/main-panel] root-el))) 20 | 21 | (defn init 22 | [] 23 | (re-frame/dispatch-sync [::events/initialize-db]) 24 | (dev-setup) 25 | (mount-root)) 26 | -------------------------------------------------------------------------------- /examples/sessions/src/frontend/controllers/db.cljs: -------------------------------------------------------------------------------- 1 | (ns controllers.db) 2 | 3 | (def default-db 4 | {:name "re-frame"}) 5 | -------------------------------------------------------------------------------- /examples/sessions/src/frontend/controllers/events.cljs: -------------------------------------------------------------------------------- 1 | (ns controllers.events 2 | (:require 3 | [controllers.db :as db] 4 | [re-frame.core :as re-frame])) 5 | 6 | (re-frame/reg-event-db 7 | ::initialize-db 8 | (fn [_ _] 9 | db/default-db)) 10 | -------------------------------------------------------------------------------- /examples/sessions/src/frontend/controllers/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns controllers.subs 2 | (:require 3 | [re-frame.core :as re-frame])) 4 | 5 | (re-frame/reg-sub 6 | ::name 7 | (fn [db] 8 | (get db :name))) 9 | -------------------------------------------------------------------------------- /examples/sessions/src/frontend/controllers/views.cljs: -------------------------------------------------------------------------------- 1 | (ns controllers.views 2 | (:require 3 | [controllers.subs :as subs] 4 | [re-frame.core :as re-frame])) 5 | 6 | (defn main-panel 7 | [] 8 | (let [name (re-frame/subscribe [::subs/name])] 9 | [:div 10 | [:h1 "Hello from " @name]])) 11 | -------------------------------------------------------------------------------- /examples/state-events/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /.prepl-port 12 | .hgignore 13 | .hg/ 14 | .idea/ 15 | /.shadow-cljs/ 16 | /node_modules/ 17 | /package.json 18 | /shadow-cljs.edn 19 | /resources/public/assets/js/compiled/ 20 | -------------------------------------------------------------------------------- /examples/state-events/README.md: -------------------------------------------------------------------------------- 1 | # state-events 2 | 3 | Example for event resourcing, and server sent event based frontend state management. 4 | 5 | ## Description 6 | 7 | The actual resource is the aggregate of the events. You can create a resource with `PUT` modify it with `POST` and mark 8 | as deleted with `DELETE` methods. Every events can be undone except creation, or the last change was from another 9 | session. Undo events can redoed in (with) the same session(-id). It possible to remove fields on by one (except resource 10 | id, and resource type), to clean up a resource. 11 | 12 | ## Start dockerized PostgreSQL 13 | 14 | docker-compose up -d 15 | 16 | ## Log into psql console 17 | 18 | psql -U postgres -p 5433 -h localhost 19 | 20 | ## Build frontend and run the backend 21 | 22 | lein release 23 | 24 | lein run 25 | 26 | ## Open re-frame app 27 | 28 | - open [the app](http://localhost:3000/re-frame) 29 | - Add new person, select it from the list above, try to apply different changes on it 30 | - Open another session in private browser, or another browser, to see if SSE works 31 | - check for cookies in the developer tools, it shoud have a session-id key with UUID value -------------------------------------------------------------------------------- /examples/state-events/config/dev/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/postgresql {:port 5433 2 | :dbname "state_events" 3 | :host "localhost" 4 | :dbtype "postgresql" 5 | :user "postgres" 6 | :password "postgres"} 7 | 8 | :xiana/migration {:store :database 9 | :migration-dir "migrations" 10 | :init-in-transaction? false 11 | :migration-table-name "migrations"} 12 | 13 | :xiana/web-server {:port 3000 14 | :join? false} 15 | 16 | :xiana/auth {:hash-algorithm :bcrypt ; Available values: :bcrypt, :scrypt, and :pbkdf2 17 | :bcrypt-settings {:work-factor 11} 18 | :scrypt-settings {:cpu-cost 32768 ; Must be a power of 2 19 | :memory-cost 8 20 | :parallelization 1} 21 | :pbkdf2-settings {:type :sha1 ; Available values: :sha1 and :sha256 22 | :iterations 100000}}} 23 | -------------------------------------------------------------------------------- /examples/state-events/config/test/config.edn: -------------------------------------------------------------------------------- 1 | {:xiana/postgresql {:image-name "postgres:11.5-alpine" 2 | :port 5433 3 | :dbname "state_events" 4 | :host "localhost" 5 | :dbtype "postgresql" 6 | :user "postgres" 7 | :password "postgres"} 8 | 9 | :xiana/migration {:store :database 10 | :migration-dir "migrations" 11 | :init-in-transaction? false 12 | :migration-table-name "migrations"} 13 | 14 | :xiana/web-server {:port 3333 15 | :join? false} 16 | 17 | :xiana/auth {:hash-algorithm :bcrypt ; Available values: :bcrypt, :scrypt, and :pbkdf2 18 | :bcrypt-settings {:work-factor 11} 19 | :scrypt-settings {:cpu-cost 32768 ; Must be a power of 2 20 | :memory-cost 8 21 | :parallelization 1} 22 | :pbkdf2-settings {:type :sha1 ; Available values: :sha1 and :sha256 23 | :iterations 100000}}} 24 | 25 | -------------------------------------------------------------------------------- /examples/state-events/dev/state.clj: -------------------------------------------------------------------------------- 1 | (ns state 2 | (:require 3 | [clojure.tools.namespace.repl :refer [disable-reload!]] 4 | [piotr-yuxuan.closeable-map :refer [closeable-map]])) 5 | 6 | (disable-reload!) 7 | 8 | (defonce dev-sys (atom (closeable-map {}))) 9 | -------------------------------------------------------------------------------- /examples/state-events/dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:gen-class) 3 | (:require 4 | [clojure.tools.namespace.repl :refer [refresh-all]] 5 | [piotr-yuxuan.closeable-map :refer [closeable-map]] 6 | [shadow.cljs.devtools.api :as shadow.api] 7 | [shadow.cljs.devtools.server :as shadow.server] 8 | [state :refer [dev-sys]] 9 | [state-events.core :refer [->system app-cfg]])) 10 | 11 | (def dev-app-config 12 | app-cfg) 13 | 14 | (defn- stop-dev-system 15 | [] 16 | (when (:webserver @dev-sys) (.close @dev-sys) (refresh-all)) 17 | (reset! dev-sys (closeable-map {}))) 18 | 19 | (defn start-dev-system 20 | [] 21 | (stop-dev-system) 22 | (shadow.server/start!) 23 | (shadow.api/watch :app) 24 | (reset! dev-sys (->system dev-app-config))) 25 | 26 | (comment 27 | (start-dev-system)) 28 | -------------------------------------------------------------------------------- /examples/state-events/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | db: 4 | image: postgres:11.5-alpine 5 | volumes: 6 | - db-data:/var/lib/postgresql/data 7 | ports: 8 | - "5433:5432" 9 | environment: 10 | - POSTGRES_DB=state_events 11 | - POSTGRES_USER=postgres 12 | - POSTGRES_PASSWORD=postgres 13 | 14 | volumes: 15 | db-data: 16 | -------------------------------------------------------------------------------- /examples/state-events/resources/migrations/20211118093611-events.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE EVENTS CASCADE; -------------------------------------------------------------------------------- /examples/state-events/resources/migrations/20211118093611-events.up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 2 | 3 | --;; 4 | 5 | create table EVENTS ( 6 | payload json not null, 7 | resource varchar, 8 | resource_id uuid, 9 | modified_at timestamp, 10 | action varchar, 11 | creator uuid 12 | ); -------------------------------------------------------------------------------- /examples/state-events/resources/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/state-events/resources/public/assets/favicon.ico -------------------------------------------------------------------------------- /examples/state-events/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | state-events 8 | 9 | 10 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/state-events/src/backend/state_events/controller_behaviors/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/state-events/src/backend/state_events/controller_behaviors/.gitkeep -------------------------------------------------------------------------------- /examples/state-events/src/backend/state_events/controller_behaviors/sse.clj: -------------------------------------------------------------------------------- 1 | (ns state-events.controller-behaviors.sse 2 | (:require 3 | [clojure.core.async :as async] 4 | [xiana.sse :as sse]) 5 | (:import 6 | (java.sql 7 | Timestamp) 8 | (java.util 9 | Date))) 10 | 11 | (defn prepare->json 12 | [m] 13 | (reduce 14 | (fn [acc [k v]] 15 | (into acc {k (cond (uuid? v) (str v) 16 | (instance? Timestamp v) (.getTime v) 17 | :else v)})) {} m)) 18 | 19 | (defn send-event! 20 | [state] 21 | (let [agg-event (get-in state [:response-data :event-aggregate])] 22 | (sse/put! state (prepare->json (assoc agg-event :type :modify)))) 23 | state) 24 | 25 | (defonce ^{:private true} ping-id (atom 0)) 26 | 27 | (defn ping [deps] 28 | (let [channel (get-in deps [:events-channel :channel])] 29 | (async/>!! channel {:type :ping :id (swap! ping-id inc) :timestamp (.getTime (Date.))}))) 30 | -------------------------------------------------------------------------------- /examples/state-events/src/backend/state_events/controllers/index.clj: -------------------------------------------------------------------------------- 1 | (ns state-events.controllers.index 2 | (:require 3 | [ring.util.response :as ring])) 4 | 5 | (defn handle-index 6 | [state] 7 | (assoc state 8 | :response 9 | (ring/response "Index page"))) 10 | -------------------------------------------------------------------------------- /examples/state-events/src/backend/state_events/controllers/re_frame.clj: -------------------------------------------------------------------------------- 1 | (ns state-events.controllers.re-frame 2 | (:require 3 | [ring.util.response :as ring])) 4 | 5 | (defn handle-index 6 | [state] 7 | (assoc state 8 | :response 9 | (-> "index.html" 10 | (ring/resource-response {:root "public"}) 11 | (ring/header "Content-Type" "text/html; charset=utf-8")))) 12 | -------------------------------------------------------------------------------- /examples/state-events/src/backend/state_events/interceptors.clj: -------------------------------------------------------------------------------- 1 | (ns state-events.interceptors 2 | (:require 3 | [clojure.walk :refer [keywordize-keys]] 4 | [ring.middleware.cookies :as cookies])) 5 | 6 | (def asset-router 7 | {:enter (fn [{{uri :uri} :request 8 | :as state}] 9 | (case uri 10 | "/favicon.ico" (assoc-in state [:request :uri] "/assets/favicon.ico") 11 | state))}) 12 | 13 | (def session-id->cookie 14 | {:leave (fn [state] 15 | (assoc-in 16 | (assoc-in 17 | state 18 | [:response :cookies :session-id] 19 | (get-in state [:session-data :session-id])) 20 | [:response :headers "access-control-expose-headers"] 21 | "Set-Cookie"))}) 22 | -------------------------------------------------------------------------------- /examples/state-events/src/backend/state_events/interceptors/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/state-events/src/backend/state_events/interceptors/.gitkeep -------------------------------------------------------------------------------- /examples/state-events/src/backend/state_events/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/state-events/src/backend/state_events/models/.gitkeep -------------------------------------------------------------------------------- /examples/state-events/src/backend/state_events/models/event.clj: -------------------------------------------------------------------------------- 1 | (ns state-events.models.event 2 | (:require 3 | [honeysql.format :as sqlf] 4 | [honeysql.helpers :as sqlh])) 5 | 6 | (defn add [state] 7 | (let [event (-> state :request-data :event) 8 | resource (:resource event) 9 | resource-id (:resource-id event)] 10 | (assoc-in state 11 | [:db-queries :queries] 12 | [(sqlh/values 13 | (sqlh/insert-into :events) 14 | [(update event :payload sqlf/value)]) 15 | (-> (sqlh/select :*) 16 | (sqlh/from :events) 17 | (sqlh/where [:and 18 | [:= :resource resource] 19 | [:= :resource-id resource-id]]))]))) 20 | 21 | (defn last-event 22 | [state] 23 | (let [event (-> state :request-data :event) 24 | resource (:resource event) 25 | resource-id (:resource-id event)] 26 | (-> (sqlh/select :*) 27 | (sqlh/from :events) 28 | (sqlh/where [:and 29 | [:= :events.resource resource] 30 | [:= :events.resource-id resource-id]]) 31 | (sqlh/order-by [:events/modified_at :desc]) 32 | (sqlh/limit 1)))) 33 | 34 | (defn fetch 35 | [state] 36 | (assoc state 37 | :query 38 | (sqlh/from (sqlh/select :*) :events))) 39 | -------------------------------------------------------------------------------- /examples/state-events/src/backend/state_events/views/event.clj: -------------------------------------------------------------------------------- 1 | (ns state-events.views.event 2 | (:require 3 | [clojure.string :as str] 4 | [clojure.walk :refer [keywordize-keys]] 5 | [state-events.interceptors.event-process :refer [event->agg]])) 6 | 7 | (def aggregate 8 | (fn [state] 9 | (assoc-in state 10 | [:response :body :data] 11 | (-> state :response-data :event-aggregate)))) 12 | 13 | (defn group-events 14 | [state] 15 | (->> state :response-data :db-data 16 | (group-by #(format "%s/%s" (str/join (:events/resource %)) (:events/resource_id %))) 17 | keywordize-keys)) 18 | 19 | (defn persons 20 | [state] 21 | (let [persons (group-events state) 22 | not-deleted (remove (fn [p] (#{"delete"} (-> p second last :events/action))) persons)] 23 | (assoc-in state [:response :body :data] 24 | (reduce (fn [acc [k v]] (into acc {k (:events/payload (event->agg v))})) 25 | {} not-deleted)))) 26 | 27 | (defn raw 28 | [state] 29 | (assoc-in state [:response :body :data] 30 | (group-events state))) 31 | 32 | -------------------------------------------------------------------------------- /examples/state-events/src/backend/state_events/views/layouts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/examples/state-events/src/backend/state_events/views/layouts/.gitkeep -------------------------------------------------------------------------------- /examples/state-events/src/frontend/state_events/config.cljs: -------------------------------------------------------------------------------- 1 | (ns state-events.config) 2 | 3 | (def debug? 4 | ^boolean goog.DEBUG) 5 | -------------------------------------------------------------------------------- /examples/state-events/src/frontend/state_events/core.cljs: -------------------------------------------------------------------------------- 1 | (ns state-events.core 2 | (:require 3 | [cljs.reader :as edn] 4 | [re-frame.core :as re-frame] 5 | [reagent.dom :as rdom] 6 | [state-events.config :as config] 7 | [state-events.effects :as effects] 8 | [state-events.events :as events] 9 | [state-events.subs :as subs] 10 | [state-events.views :as views] 11 | [state-events.web-sockets :as ws])) 12 | 13 | (defn dev-setup [] 14 | (when config/debug? 15 | (println "dev mode"))) 16 | 17 | (defn ^:dev/after-load mount-root [] 18 | (re-frame/clear-subscription-cache!) 19 | (let [root-el (.getElementById js/document "app")] 20 | (rdom/unmount-component-at-node root-el) 21 | (rdom/render [views/main-panel] root-el))) 22 | 23 | (defn init [] 24 | (re-frame/dispatch-sync [::events/initialize-db]) 25 | (ws/connect! (str "ws://" (.-host js/location) "/sse")) 26 | (dev-setup) 27 | (mount-root)) 28 | -------------------------------------------------------------------------------- /examples/state-events/src/frontend/state_events/db.cljs: -------------------------------------------------------------------------------- 1 | (ns state-events.db) 2 | 3 | (def default-db 4 | {:name "re-frame" 5 | :persons nil 6 | :selected {}}) 7 | -------------------------------------------------------------------------------- /examples/state-events/src/frontend/state_events/events.cljs: -------------------------------------------------------------------------------- 1 | (ns state-events.events 2 | (:require 3 | [ajax.core :refer [GET POST PUT]] 4 | [clojure.walk :refer [keywordize-keys]] 5 | [re-frame.core :as re-frame] 6 | [state-events.db :as db])) 7 | 8 | (re-frame/reg-event-db 9 | ::initialize-db 10 | (fn [_ _] 11 | (GET "/person" 12 | {:handler #(re-frame/dispatch [::update-db %])}) 13 | db/default-db)) 14 | 15 | (re-frame/reg-event-db 16 | ::update-db 17 | (fn [db [_ v]] 18 | (let [value (keywordize-keys v)] 19 | (assoc db :persons (:data value))))) 20 | -------------------------------------------------------------------------------- /examples/state-events/src/frontend/state_events/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns state-events.subs 2 | (:require 3 | [re-frame.core :as re-frame])) 4 | 5 | (re-frame/reg-sub 6 | :selected 7 | :selected) 8 | 9 | (re-frame/reg-sub 10 | :persons 11 | :persons) 12 | -------------------------------------------------------------------------------- /examples/state-events/src/frontend/state_events/web_sockets.cljs: -------------------------------------------------------------------------------- 1 | (ns state-events.web-sockets 2 | (:require 3 | [clojure.string :as str] 4 | [clojure.walk :refer [keywordize-keys]] 5 | [re-frame.core :as re-frame])) 6 | 7 | (defonce channel (atom nil)) 8 | 9 | (defn normalize-sse-message [m] 10 | (-> m 11 | keywordize-keys 12 | (update :type keyword))) 13 | 14 | (defn handle-response! [response] 15 | (let [data (-> (.parse js/JSON (str/replace-first (.-data response) #"data: " "")) 16 | (js->clj :keywordize-keys true) 17 | (update :type keyword))] 18 | (case (:type data) 19 | :ping (prn "ping") 20 | :modify (let [k (keyword (clojure.string/join (rest (:resource data))) 21 | (:resource_id data)) 22 | a (:action data)] 23 | (case a 24 | "delete" (re-frame/dispatch [:persons/delete k]) 25 | (re-frame/dispatch [:persons/modify k data]))) 26 | (constantly nil)))) 27 | 28 | (defn connect! [url] 29 | (if-let [chan (js/WebSocket. url)] 30 | (do 31 | (.log js/console "Connected!") 32 | (set! (.-onmessage chan) handle-response!) 33 | (reset! channel chan)) 34 | (throw (ex-info "Websocket Connection Failed!" 35 | {:url url})))) 36 | -------------------------------------------------------------------------------- /examples/state-events/src/shared/config.clj: -------------------------------------------------------------------------------- 1 | (ns config) 2 | -------------------------------------------------------------------------------- /examples/state-events/src/shared/schema.clj: -------------------------------------------------------------------------------- 1 | (ns schema) 2 | -------------------------------------------------------------------------------- /examples/state-events/test/state_events_fixture.clj: -------------------------------------------------------------------------------- 1 | (ns state-events-fixture 2 | (:require 3 | [state-events.core :refer [->system app-cfg]])) 4 | 5 | (defn std-system-fixture 6 | [f] 7 | (with-open [_ (->system app-cfg)] 8 | (f))) 9 | 10 | -------------------------------------------------------------------------------- /log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/log/.gitkeep -------------------------------------------------------------------------------- /release.edn: -------------------------------------------------------------------------------- 1 | {:group-id "com.flexiana" 2 | :artifact-id "framework" 3 | :version "0.5.0-rc6" 4 | :scm-url "https://github.com/Flexiana/framework"} 5 | -------------------------------------------------------------------------------- /resources/codox/theme/xiana/css/xiana.css: -------------------------------------------------------------------------------- 1 | .sidebar.primary { 2 | background: #f8cc00; 3 | width: 200px; 4 | } 5 | 6 | .sidebar.primary::-webkit-scrollbar { 7 | display: none; 8 | } 9 | 10 | .logo { 11 | padding-top: 10px; 12 | margin:0px; 13 | } 14 | 15 | .logo img { 16 | height: 65px; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /resources/codox/theme/xiana/theme.edn: -------------------------------------------------------------------------------- 1 | {:transforms [[:head] [:append [:link {:rel "stylesheet", :type "text/css" :href "css/xiana.css"}]] 2 | [:div.namespace-index :h1 :span.project-title] [:substitute [:div.logo [:img {:src "../../resources/images/Xiana.png"}]]] 3 | [:div.namespace-index :h5] [:substitute [:div]] 4 | [:div.namespace-index :div.doc] [:substitute [:div]]] 5 | 6 | :resources ["css/xiana.css"]} 7 | -------------------------------------------------------------------------------- /resources/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/resources/images/.gitkeep -------------------------------------------------------------------------------- /resources/images/Xiana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/resources/images/Xiana.png -------------------------------------------------------------------------------- /resources/images/around-and-inside.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/resources/images/around-and-inside.png -------------------------------------------------------------------------------- /resources/images/around.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/resources/images/around.png -------------------------------------------------------------------------------- /resources/images/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/resources/images/flow.png -------------------------------------------------------------------------------- /resources/images/inside.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/resources/images/inside.png -------------------------------------------------------------------------------- /resources/images/override.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/resources/images/override.png -------------------------------------------------------------------------------- /resources/images/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/resources/images/success.png -------------------------------------------------------------------------------- /resources/javascript/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/resources/javascript/.gitkeep -------------------------------------------------------------------------------- /script/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flexiana/framework/b2ff4ce47ad9f417ff26be37cd85592b7c11ca4d/script/.gitkeep -------------------------------------------------------------------------------- /script/build-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Running codox" 3 | clj -X:codox 4 | 5 | PROJECT_VERSION=$(script/project-version) 6 | VERSION_PATH="docs/$PROJECT_VERSION" 7 | echo $VERSION_PATH 8 | INDEX_HTML="docs/index.html" 9 | 10 | echo "Moving docs into $VERSION_PATH" 11 | if [ -d $VERSION_PATH ]; then 12 | rm -r $VERSION_PATH 13 | else 14 | echo "Not found" 15 | fi 16 | mv "docs/new" $VERSION_PATH 17 | 18 | echo "Generating new index.html" 19 | if [ -f $INDEX_HTML ]; then 20 | rm "docs/index.html" 21 | fi 22 | cat script/template-index.html | sed "s//$PROJECT_VERSION/" > docs/index.html 23 | -------------------------------------------------------------------------------- /script/postgres-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose -f docker-compose.yml up --no-start 3 | docker-compose -f docker-compose.yml start 4 | docker ps 5 | -------------------------------------------------------------------------------- /script/project-version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (print (:version (read-string (slurp "release.edn")))) 4 | -------------------------------------------------------------------------------- /script/template-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | Page Redirection 10 | 11 | 12 | If you are not redirected automatically, follow this link to the current api. 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/xiana/commons.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.commons) 2 | 3 | (defn ?assoc-in 4 | "Same as assoc-in, but skip the assoc if v is nil" 5 | [m [k & ks] v] 6 | (if v 7 | (if ks 8 | (update-in m [k] assoc-in ks v) 9 | (assoc m k v)) 10 | m)) 11 | 12 | (defn map-keys 13 | [f m] 14 | (zipmap (map f (keys m)) 15 | (vals m))) 16 | 17 | (defn rename-key 18 | [m from to] 19 | (-> m 20 | (assoc to (get m from)) 21 | (dissoc from))) 22 | 23 | (defn deep-merge 24 | "Same as clojure.core/merge, except that 25 | it recursively applies itself to every nested map." 26 | [& maps] 27 | (apply merge-with 28 | (fn [& args] 29 | (if (every? map? args) 30 | (apply deep-merge args) 31 | (last args))) 32 | maps)) 33 | -------------------------------------------------------------------------------- /src/xiana/config.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.config 2 | "Solves environment and config variables" 3 | (:require 4 | [clojure.edn :as edn] 5 | [clojure.java.io :as io] 6 | [clojure.string :as str] 7 | [config.core :refer [load-env]] 8 | [xiana.commons :refer [deep-merge]]) 9 | (:import 10 | (java.io 11 | PushbackReader))) 12 | 13 | (defn read-edn-file 14 | "Reads edn configuration file." 15 | [config] 16 | (if-let [edn-file (:framework-edn-config config)] 17 | (with-open [r (io/reader edn-file)] 18 | (deep-merge config (edn/read (PushbackReader. r)))) 19 | config)) 20 | 21 | (defn key-and-default [v] 22 | (let [[key-name default] (str/split v #"\|")] 23 | [(keyword (subs (str/trim key-name) 1)) (some-> default str/trim)])) 24 | 25 | (defn- inject-env-vars 26 | [config] 27 | (into {} (map (fn mapper [[k v]] 28 | (cond 29 | (map? v) [k (into {} (map mapper v))] 30 | (and (string? v) (str/starts-with? v "$")) (let [[_key default] (key-and-default v)] 31 | [k (get config _key default)]) 32 | :else [k v])) 33 | config))) 34 | 35 | (defn config 36 | "Returns a new config instance. 37 | 38 | You can pass path to the config file with the `:framework-edn-config` key. 39 | It's useful for choosing an environment different from the current one 40 | (e.g. `test` or `production` while in the `dev` repl)." 41 | ([] 42 | (config {})) 43 | ([config] 44 | (-> (load-env) 45 | (deep-merge config) 46 | read-edn-file 47 | inject-env-vars))) 48 | -------------------------------------------------------------------------------- /src/xiana/cookies.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.cookies 2 | "Cookie parser" 3 | (:require 4 | [clojure.walk :refer [keywordize-keys]] 5 | [ring.middleware.cookies :as cookies])) 6 | 7 | (def interceptor 8 | "Parses request and response cookies" 9 | (letfn [(move-cookies [{headers :headers :as req}] 10 | (cond-> req 11 | (not (get headers "cookie")) (assoc-in 12 | [:headers "cookie"] 13 | (:cookie headers)))) 14 | (parse-request-cookies [req] 15 | (-> req move-cookies cookies/cookies-request keywordize-keys))] 16 | {:name ::interceptor 17 | :enter (fn [state] 18 | (update state :request parse-request-cookies)) 19 | :leave (fn [state] 20 | (update state :response cookies/cookies-response))})) 21 | -------------------------------------------------------------------------------- /src/xiana/handler.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.handler 2 | "Provides the default handler function" 3 | (:require 4 | [ring.adapter.jetty9 :as jetty] 5 | [xiana.interceptor.queue :as interceptor.queue] 6 | [xiana.route :as route] 7 | [xiana.state :as state])) 8 | 9 | (defn handler-fn 10 | "Returns handler function for server, which do the routing, and executes interceptors and given action. 11 | 12 | Execution order: 13 | router interceptors: enters in order 14 | router interceptors leaves in reversed order 15 | routing 16 | around interceptors enters in order 17 | controller interceptors enters in order 18 | inside interceptors enters in order 19 | action 20 | inside interceptors leaves in reversed order 21 | controller interceptors leaves in reversed order 22 | around interceptors leaves in reversed order" 23 | [deps] 24 | (fn handle* 25 | ([http-request] 26 | (let [websocket? (jetty/ws-upgrade-request? http-request) 27 | state (state/make deps http-request) 28 | queue (list #(interceptor.queue/execute % (:router-interceptors deps)) 29 | #(route/match %) 30 | #(interceptor.queue/execute % (if websocket? 31 | (:web-socket-interceptors deps) 32 | (:controller-interceptors deps)))) 33 | result (reduce (fn [s f] (f s)) state queue) 34 | channel (get-in result [:response-data :channel])] 35 | (if (and websocket? channel) 36 | (jetty/ws-upgrade-response channel) 37 | (:response result)))) 38 | ([request respond _] 39 | (respond (handle* request))))) 40 | -------------------------------------------------------------------------------- /src/xiana/interceptor/cors.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.interceptor.cors) 2 | 3 | (defn cors-headers [origin] 4 | {"Access-Control-Allow-Origin" origin 5 | "Access-Control-Allow-Credentials" "true" 6 | "Access-Control-Allow-Methods" "GET,PUT,POST,DELETE" 7 | "Access-Control-Allow-Headers" "Origin, X-Requested-With, Content-Type, Accept"}) 8 | 9 | (defn preflight? 10 | "Returns true if the request is a preflight request" 11 | [request] 12 | (= (request :request-method) :options)) 13 | 14 | (def interceptor 15 | {:name ::cross-origin-headers 16 | :leave (fn [state] 17 | (let [request (:request state) 18 | headers (cors-headers (get-in state [:deps :cors-origin]))] 19 | (if (preflight? request) 20 | (update-in state [:response] 21 | merge 22 | {:status 200 23 | :headers headers 24 | :body "preflight complete"}) 25 | (update-in state [:response :headers] 26 | merge headers))))}) 27 | -------------------------------------------------------------------------------- /src/xiana/interceptor/error.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.interceptor.error) 2 | 3 | (def response 4 | "Handles the exception if there's `ex-info` exception with non-empty 5 | `:xiana/response` key." 6 | {:name ::error-response 7 | :error (fn [state] 8 | (if-let [resp (-> state :error ex-data :xiana/response)] 9 | (-> state 10 | (assoc :response resp) 11 | (dissoc :error)) 12 | state))}) 13 | -------------------------------------------------------------------------------- /src/xiana/interceptor/kebab_camel.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.interceptor.kebab-camel 2 | (:require 3 | [camel-snake-kebab.core :as csk] 4 | [camel-snake-kebab.extras :as cske] 5 | [clojure.core.memoize :as mem])) 6 | 7 | (def request-type-params [:params :body-params :query-params :path-params :form-params :multipart-params]) 8 | 9 | (def camel-to-kebab 10 | (fn [resp] 11 | (cske/transform-keys 12 | (mem/fifo csk/->kebab-case {} :fifo/threshold 512) resp))) 13 | 14 | (def kebab-to-camel 15 | (fn [resp] 16 | (cske/transform-keys 17 | (mem/fifo csk/->camelCase {} :fifo/threshold 512) resp))) 18 | 19 | (def interceptor 20 | "The purpose is to make Js request compatible with clojure, and response compatible with Javascript. 21 | :request - {:params { " 22 | {:name ::camel-to-kebab-case 23 | :enter (fn [state] 24 | (reduce 25 | (fn [state type-param] 26 | (update-in state [:request type-param] camel-to-kebab)) 27 | state 28 | request-type-params)) 29 | :leave (fn [state] 30 | (update-in state [:response :body] kebab-to-camel))}) 31 | -------------------------------------------------------------------------------- /src/xiana/interceptor/muuntaja.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.interceptor.muuntaja 2 | "Muuntaja interceptor encoder/decode.." 3 | (:require 4 | [clojure.data.xml :as xml] 5 | [clojure.string] 6 | [muuntaja.core] 7 | [muuntaja.format.core] 8 | [muuntaja.format.json :as json] 9 | [muuntaja.interceptor])) 10 | 11 | (defn xml-encoder 12 | "XML encoder." 13 | [_] 14 | (let [helper-fn 15 | #(xml/emit-str 16 | (mapv 17 | (fn make-node 18 | [[f s]] 19 | (xml/element f {} (if (map? s) (map make-node (seq s)) s))) 20 | (seq %)))] 21 | ;; implement EncodeToBytes protocol 22 | (reify 23 | muuntaja.format.core/EncodeToBytes 24 | (encode-to-bytes 25 | [_ data charset] 26 | (.getBytes ^String (helper-fn data) ^String charset))))) 27 | 28 | (def instance 29 | "Define muuntaja's default encoder/decoder instance." 30 | (muuntaja.core/create 31 | (-> muuntaja.core/default-options 32 | (assoc-in [:formats "application/upper-json"] 33 | {:decoder [json/decoder] 34 | :encoder [json/encoder 35 | {:encode-key-fn 36 | (comp clojure.string/upper-case name)}]}) 37 | (assoc-in [:formats "application/xml"] {:encoder [xml-encoder]}) 38 | (assoc-in [:formats "application/json" :decoder-opts :bigdecimals] true) 39 | (assoc-in [:formats "application/json" :encoder-opts :date-format] 40 | "yyyy-MM-dd")))) 41 | 42 | (def interceptor 43 | "Define muuntaja's default interceptor." 44 | (muuntaja.interceptor/format-interceptor instance)) 45 | -------------------------------------------------------------------------------- /src/xiana/interceptor/wrap.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.interceptor.wrap 2 | "To wrap any kind of middlewares and interceptors to xiana flow.") 3 | 4 | (defn- middleware-fn 5 | "Simple enter/leave middleware function generator." 6 | [m k] 7 | (fn [{r k :as state}] 8 | (assoc state k (m r)))) 9 | 10 | (defn middleware->enter 11 | "Parse middleware function to interceptor enter lambda function." 12 | ([middleware] 13 | (middleware->enter {} middleware)) 14 | ([interceptor middleware] 15 | (let [m (middleware identity) 16 | f (middleware-fn m :request)] 17 | (assoc interceptor :enter f)))) 18 | 19 | (defn middleware->leave 20 | "Parse middleware function to interceptor leave lambda function." 21 | ([middleware] 22 | (middleware->leave {} middleware)) 23 | ([interceptor middleware] 24 | (let [m (middleware identity) 25 | f (middleware-fn m :response)] 26 | (assoc interceptor :leave f)))) 27 | -------------------------------------------------------------------------------- /src/xiana/jwt/action.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.jwt.action 2 | (:require 3 | [xiana.jwt :as jwt])) 4 | 5 | (defn refresh-token 6 | [state] 7 | (let [jwt-authentication (get-in state [:session-data :jwt-authentication]) 8 | cfg (get-in state [:deps :xiana/jwt :auth]) 9 | jwt-token (jwt/sign :claims jwt-authentication cfg)] 10 | (assoc state :response {:status 200 :body {:auth-token jwt-token}}))) 11 | -------------------------------------------------------------------------------- /src/xiana/jwt/interceptors.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.jwt.interceptors 2 | (:require 3 | [clojure.string :as str] 4 | [xiana.jwt :as jwt] 5 | [xiana.route.helpers :as helpers]) 6 | (:import 7 | (clojure.lang 8 | ExceptionInfo))) 9 | 10 | (def jwt-auth 11 | {:name ::jwt-authentication 12 | :enter 13 | (fn [{request :request :as state}] 14 | (let [auth (get-in request [:headers :authorization])] 15 | (cond 16 | (= :options (:request-method request)) 17 | state 18 | 19 | auth 20 | (let [auth (-> request 21 | (get-in [:headers :authorization]) 22 | (str/split #" ") 23 | second) 24 | cfg (get-in state [:deps :xiana/jwt :auth])] 25 | (try 26 | (->> 27 | (jwt/verify-jwt :claims auth cfg) 28 | (assoc-in state [:session-data :jwt-authentication])) 29 | (catch ExceptionInfo e 30 | (assoc state :error e)))) 31 | 32 | (nil? auth) 33 | (assoc state :error (ex-info "Authorization header not provided" {:type :authorization}))))) 34 | :error 35 | (fn [state] 36 | (let [error (:error state) 37 | err-info (ex-data error)] 38 | (cond 39 | (= :exp (:cause err-info)) 40 | (helpers/unauthorized state "JWT Token expired.") 41 | (= :validation (:type err-info)) 42 | (helpers/unauthorized state "One or more Claims were invalid.") 43 | :else 44 | (helpers/unauthorized state "Signature could not be verified."))))}) 45 | 46 | (def jwt-content 47 | {:name ::jwt-content-exchange 48 | :enter 49 | (fn [{request :request :as state}] 50 | (if-let [body-params (:body-params request)] 51 | (let [cfg (get-in state [:deps :xiana/jwt :content])] 52 | (try 53 | (->> (jwt/verify-jwt :no-claims body-params cfg) 54 | (assoc-in state [:request :body-params])) 55 | (catch ExceptionInfo e 56 | (assoc state :error e)))) 57 | state)) 58 | :leave 59 | (fn [{response :response :as state}] 60 | (let [cfg (get-in state [:deps :xiana/jwt :content])] 61 | (->> (jwt/sign :no-claims (:body response) cfg) 62 | (assoc-in state [:state :response :body])))) 63 | :error 64 | (fn [state] 65 | (helpers/unauthorized state "Signature could not be verified"))}) 66 | -------------------------------------------------------------------------------- /src/xiana/logging.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.logging 2 | (:require 3 | [taoensso.timbre :as log])) 4 | 5 | (defn set-level 6 | [cfg] 7 | (when-let [level (-> cfg :logging/timbre-config :min-level)] 8 | (log/set-min-level! level)) 9 | cfg) 10 | -------------------------------------------------------------------------------- /src/xiana/mail.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.mail 2 | (:require 3 | [cuerdas.core :as cu] 4 | [postal.core :as pc])) 5 | 6 | (defn- make-body 7 | [body atts] 8 | (let [body-payload [:alternative 9 | {:type "text/plain" 10 | :content (cu/strip-tags body)} 11 | {:type "text/html" 12 | :content body}] 13 | attachments (if (string? atts) (vector atts) atts) 14 | file-map #(hash-map :type :attachment :content (java.io.File. %))] 15 | (vec (concat body-payload (map file-map attachments))))) 16 | 17 | (defn send-email! 18 | "Sending a mail with 'postal.core'" 19 | [{mail-config :xiana/emails} 20 | {:keys [to cc bcc subject body attachments]}] 21 | (pc/send-message mail-config 22 | {:from (:from mail-config) 23 | :to to 24 | :cc cc 25 | :bcc bcc 26 | :subject subject 27 | :body (make-body body attachments)})) 28 | -------------------------------------------------------------------------------- /src/xiana/rbac.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.rbac 2 | "Integrates tiny-RBAC library to the xiana flow" 3 | (:require 4 | [tiny-rbac.builder :as b] 5 | [tiny-rbac.core :as c])) 6 | 7 | (defn init 8 | "Initialize and validates a role-set" 9 | [config] 10 | (assoc config :role-set (b/init (:xiana/role-set config)))) 11 | 12 | (defn permissions 13 | "Gathers the necessary parameters from xiana state for permission resolution. 14 | Returns a set of keywords for data ownership check. 15 | The format of returned keywords: 16 | ':resource/restriction'" 17 | [state] 18 | (let [role-set (get-in state [:deps :role-set]) 19 | role (get-in state [:session-data :users/role]) 20 | permit (get-in state [:request-data :permission]) 21 | resource (keyword (namespace permit)) 22 | action (keyword (name permit)) 23 | permissions (c/permissions role-set role resource action)] 24 | (set (map #(keyword (str (name resource) "/" (name %))) permissions)))) 25 | 26 | (def interceptor 27 | "On enter it validates if the resource is restricted, 28 | and available at the current state (actual user with a role) 29 | If it's not restricted do nothing, 30 | if the given user has no rights, it throws ex-info with data {:status 403 :body \"Forbidden\"}. 31 | On leave executes restriction function if any." 32 | {:name ::rbac 33 | :enter (fn [state] 34 | (let [operation-restricted (get-in state [:request-data :permission]) 35 | permits (and operation-restricted (permissions state))] 36 | (cond 37 | (and operation-restricted (empty? permits)) (throw (ex-info "Forbidden" 38 | {:xiana/response {:status 403 :body "Forbidden"}})) 39 | operation-restricted (assoc-in state [:request-data :user-permissions] permits) 40 | :else state))) 41 | :leave (fn [state] 42 | (let [restriction-fn (get-in state [:request-data :restriction-fn] identity)] 43 | (restriction-fn state)))}) 44 | -------------------------------------------------------------------------------- /src/xiana/route.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.route 2 | "Do the routing, and inject request data to the xiana state" 3 | (:require 4 | [piotr-yuxuan.closeable-map] 5 | [reitit.coercion :as coercion] 6 | [reitit.core :as r] 7 | [ring.adapter.jetty9 :as jetty] 8 | [xiana.commons :refer [?assoc-in]] 9 | [xiana.route.helpers :as helpers])) 10 | 11 | (defn reset 12 | "Update routes." 13 | [config] 14 | (update config :routes r/router {:compile coercion/compile-request-coercers})) 15 | 16 | (defmacro -get-in-template 17 | "Simple macro to get the values from the match template." 18 | [t m k p] 19 | `(or (-> ~t :data ~p) 20 | (-> ~t ~k ~m ~p))) 21 | 22 | (defn- -update 23 | "Update state with router match template data." 24 | [match {request :request :as state}] 25 | (let [method (:request-method request) 26 | handler (-get-in-template match method :result :handler) 27 | action (-get-in-template match method :data :action) 28 | ws-action (-get-in-template match method :data :ws-action) 29 | permission (-get-in-template match method :data :permission) 30 | interceptors (-get-in-template match method :data :interceptors)] 31 | ;; associate the necessary route match information 32 | (-> state 33 | (?assoc-in [:request-data :method] method) 34 | (?assoc-in [:request-data :handler] handler) 35 | (?assoc-in [:request-data :interceptors] interceptors) 36 | (?assoc-in [:request-data :match] match) 37 | (?assoc-in [:request-data :permission] permission) 38 | (assoc-in [:request-data :action] 39 | (cond (and (jetty/ws-upgrade-request? request) ws-action) ws-action 40 | action action 41 | handler helpers/action 42 | :else helpers/not-found))))) 43 | 44 | (defn match 45 | "Associate router match template data into the state. 46 | Return the wrapped state container." 47 | [{request :request :as state}] 48 | (let [match (r/match-by-path (-> state :deps :routes) (:uri request))] 49 | (-update match state))) 50 | -------------------------------------------------------------------------------- /src/xiana/route/helpers.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.route.helpers 2 | "The default not found and action functions") 3 | 4 | (defn not-found 5 | "Default not-found response handler helper." 6 | [state] 7 | (assoc state :response {:status 404 :body "Not Found"})) 8 | 9 | (defn unauthorized 10 | "Default unauthorized response handler helper." 11 | [state msg] 12 | (assoc state :response {:status 401 :body msg})) 13 | 14 | (defn action 15 | "Default action response handler helper." 16 | [{request :request {handler :handler} :request-data :as state}] 17 | (try 18 | (assoc state :response (handler request)) 19 | (catch Exception e 20 | (-> state 21 | (assoc :error e) 22 | (assoc :response 23 | {:status 500 :body "Internal Server error"}))))) 24 | -------------------------------------------------------------------------------- /src/xiana/scheduler.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.scheduler 2 | (:require 3 | [clojure.core.async :as async :refer [chan timeout close! go-loop]] 4 | [clojure.core.async.impl.protocols :refer [closed?]] 5 | [taoensso.timbre :as log]) 6 | (:import 7 | (java.lang 8 | AutoCloseable))) 9 | 10 | (defrecord Closeable-channels-atom 11 | [channels] 12 | AutoCloseable 13 | (close [this] 14 | (doseq [c @(:channels this)] 15 | (swap! (:channels this) disj c) 16 | (close! c)))) 17 | 18 | (defonce channels 19 | (atom #{})) 20 | 21 | (defn start 22 | [deps action interval-msecs] 23 | (let [chan (chan)] 24 | (go-loop [chan chan] 25 | (async/Closeable-channels-atom channels)))) 36 | -------------------------------------------------------------------------------- /src/xiana/sse.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.sse 2 | (:require 3 | [clojure.core.async :as async :refer (message [data] 18 | (str "data: " (j/write-value-as-string data) EOL EOL)) 19 | 20 | (defn- clients->channels 21 | [clients] 22 | (reduce into (vals clients))) 23 | 24 | (defrecord closable-events-channel 25 | [channel clients] 26 | AutoCloseable 27 | (close [this] 28 | (.close! (:channel this)) 29 | (doseq [c (clients->channels @(:clients this))] 30 | (jetty/close! c)))) 31 | 32 | (defn init [config] 33 | (let [channel (async/chan 5) 34 | clients (atom {})] 35 | (go-loop [] 36 | (when-let [data (channels @clients)] 39 | (jetty/send! c (->message data))) 40 | (recur))) 41 | (assoc config :events-channel (->closable-events-channel 42 | channel 43 | clients)))) 44 | 45 | (defn server-event-channel [state] 46 | (let [clients (get-in state [:deps :events-channel :clients]) 47 | session-id (get-in state [:session-data :session-id])] 48 | {:on-connect (fn [ch] 49 | (swap! clients update session-id (fnil conj #{}) ch) 50 | (jetty/send! ch {:headers headers :body (j/write-value-as-string {})})) 51 | :on-text (fn [c m] (jetty/send! c m)) 52 | :on-close (fn [ch _status _reason] (swap! clients update session-id disj ch))})) 53 | 54 | (defn stop-heartbeat-loop 55 | [state] 56 | (when-let [channel (get-in state [:deps :events-channel :channel])] 57 | (async/close! channel))) 58 | 59 | (defn put! 60 | [state message] 61 | (let [events-channel (get-in state [:deps :events-channel :channel])] 62 | (async/put! events-channel message))) 63 | 64 | (defn put->session 65 | [deps session-id message] 66 | (let [clients (get-in deps [:events-channel :clients]) 67 | session-clients (get @clients session-id)] 68 | (doseq [c session-clients] (jetty/send! c (->message message))) 69 | (not-empty session-clients))) 70 | 71 | (defn sse-action 72 | [state] 73 | (assoc-in state [:response-data :channel] (server-event-channel state))) 74 | -------------------------------------------------------------------------------- /src/xiana/state.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.state) 2 | 3 | ;; state/context record definition 4 | (defrecord State 5 | [request request-data response session-data deps]) 6 | 7 | (defn make 8 | "Create an empty state structure." 9 | [deps request] 10 | (-> 11 | {:deps deps 12 | :request request 13 | :response {}} 14 | map->State (conj {}))) 15 | -------------------------------------------------------------------------------- /src/xiana/webserver.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.webserver 2 | "Lifecycle management of the webserver" 3 | (:require 4 | [ring.adapter.jetty9 :as jetty] 5 | [taoensso.timbre :as log] 6 | [xiana.handler :refer [handler-fn]]) 7 | (:import 8 | (java.lang 9 | AutoCloseable))) 10 | 11 | (defrecord webserver 12 | [options server] 13 | AutoCloseable 14 | (close [this] 15 | (log/info "Stop webserver" (:options this)) 16 | (jetty/stop-server (:server this)))) 17 | 18 | (defn- make 19 | "Web server instance." 20 | [dependencies] 21 | (let [options (:webserver dependencies (:xiana/web-server dependencies))] 22 | (map->webserver 23 | {:options options 24 | :server (jetty/run-jetty (handler-fn dependencies) options)}))) 25 | 26 | (defn start 27 | "Start web server." 28 | [dependencies] 29 | ;; stop the server 30 | (when-let [webserver (get-in dependencies [:webserver :server])] 31 | (webserver)) 32 | ;; get server options 33 | (when-let [server (make dependencies)] 34 | (log/info "Server started with options: " (:options server)) 35 | (assoc dependencies :webserver server))) 36 | -------------------------------------------------------------------------------- /src/xiana/websockets.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.websockets 2 | (:require 3 | [clojure.edn :as edn] 4 | [clojure.string :as str] 5 | [jsonista.core :as j] 6 | [reitit.core :as r] 7 | [ring.adapter.jetty9 :as jetty] 8 | [taoensso.timbre :as log] 9 | [xiana.interceptor.queue :as queue])) 10 | 11 | (def send! jetty/send!) 12 | (def close! jetty/close!) 13 | 14 | (defn string-> 15 | "String to 'uri', uses the first word as action key" 16 | [s] 17 | (first (str/split s #"\s"))) 18 | 19 | (defn edn-> 20 | "EDN to 'uri', converts edn string to map, extract :action key" 21 | [e] 22 | (:action (edn/read-string e))) 23 | 24 | (defn json-> 25 | "JSON to 'uri', converts json string to map, extract :action key" 26 | [j] 27 | (:action (j/read-value j j/keyword-keys-object-mapper))) 28 | 29 | (defn probe-> 30 | [e] 31 | (name 32 | (or (try (json-> e) 33 | (catch Exception _ nil)) 34 | (try (edn-> e) 35 | (catch Exception _ nil)) 36 | (try (string-> e) 37 | (catch Exception _ nil))))) 38 | 39 | (defn router 40 | "Router for webSockets. 41 | Parameters: 42 | routes: reitit routes 43 | msg->uri: function makes routing base from message. If missing tries to solve message as json, edn and string 44 | state: xiana state record" 45 | ([routes state] 46 | (router routes probe-> state)) 47 | ([routes msg->uri {{income-msg :income-msg 48 | fallback :fallback} :request-data 49 | :as state}] 50 | (when-not (str/blank? income-msg) 51 | (let [match (r/match-by-path routes (msg->uri income-msg)) 52 | action (get-in match [:data :action] fallback) 53 | interceptors (get-in match [:data :interceptors]) 54 | default-interceptors (get-in match [:data :default-interceptors]) 55 | _ (or (get-in match [:data :hide]) 56 | (log/info "Processing: " (str/trim income-msg))) 57 | update-state (-> state 58 | (update :request-data assoc 59 | :action action 60 | :interceptors interceptors) 61 | (queue/execute default-interceptors))] 62 | (when-let [reply-fn (get-in update-state [:response-data :reply-fn])] 63 | (reply-fn update-state)))))) 64 | -------------------------------------------------------------------------------- /test/resources/_files/jwtRS256.key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqteE1o/LJCNV+F3rMtOP 3 | rR2anM0cPFq9aaV9/QZRIcKZHbqVZaOZ2kAhrxUJiKV1NBdF9cDX+iou+/X92XAF 4 | Y65FbUZ5nmhpvy0qqKh1TUFbHKJa6V9pF9Ax1m4qyyyRqLC20pEegyGGX7gVczZi 5 | 1Psl1NdD1Q37VSALrOckvyPlP2NjSEYUBuT8MkK19COhSwn6oqiI8zjXDNdcKG27 6 | zj9wiUKAOPN9NMy806+1NcY8v1dijimDENyBwdNoFqG1cIF9I8+UZM4U9Mq5bZrf 7 | 7w44l2+QRlqn8W6mKKx/IkkZegeSSgS/We12fuhFelbfz7EXIc5cqXbvzo5KeKu2 8 | j3V33ZI/XcBlGRtfbYU7z0/i57FWBb2pL+nqOsKUVo1YWCsPAHJPdQHBcqA1i78a 9 | w1JqqnTfNkfwmOADD50H7P88oGOuJphlwYA43mTZ9RopPt98VBwTy3KjfCfMDEr0 10 | tpjBUY+Z7pZAZe7H3T26C6PzMXa54ZxifUUVISv+1Fmw2swUDzLvsYecQwLl3wc8 11 | W5Or4mQFEkLNqQUYWPGRePYpcQsPA51XA5wwPq76EXkAXDO4AUsRFSl2zwdjF5TF 12 | N/UAysSNkg9tRUGz3LvJ5vgvPTI6YruklvbdK9Z2+Hy8ZACQtTK4jxGO79MZaQZe 13 | mfGYi0sFmXqxIO01ea6roesCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /test/resources/init.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 2 | CREATE TABLE users (id uuid DEFAULT uuid_generate_v4() PRIMARY KEY, created_at timestamptz NOT NULL DEFAULT now(), last_login timestamptz, is_active boolean, email varchar(254) NOT NULL, role varchar(254), username text NOT NULL, password text, salt text, fullname text); 3 | INSERT INTO users (id, created_at, email, role, password, username, is_active) VALUES ('fd5e0d70-506a-45cc-84d5-b12b5e3e99d2', '2021-03-30 12:34:10.358157+02', 'admin@frankie.sw', 'admin', '$2a$11$ivfRMKD7dHMfqCWBiEQcaOknsJgDnK9zoSP/cXAVNQVYHc.M9SZJK', 'admin', true), ('31c2c58f-28cb-4013-8765-9240626a18a2', '2021-03-30 12:34:10.358157+02', 'frankie@frankie.sw', 'user', '$2a$11$ivfRMKD7dHMfqCWBiEQcaOknsJgDnK9zoSP/cXAVNQVYHc.M9SZJK', 'frankie', true), ('8d05b2e1-6463-478a-ba30-35768738af29', '2021-03-30 12:34:10.358157+02', 'impostor@frankie.sw', 'interviewer', '$2a$11$ivfRMKD7dHMfqCWBiEQcaOknsJgDnK9zoSP/cXAVNQVYHc.M9SZJK', 'impostor', false); 4 | -------------------------------------------------------------------------------- /test/resources/multipart.csv: -------------------------------------------------------------------------------- 1 | first_name,last_name,age 2 | John,Doe,25 3 | Jane,Doe,24 4 | Alice,Smith,22 5 | Bob,Johnson,30 6 | -------------------------------------------------------------------------------- /test/xiana/commons_test.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.commons-test 2 | (:require 3 | [clojure.test :refer [deftest are]] 4 | [xiana.commons :refer [deep-merge]])) 5 | 6 | (deftest deep-merge-test 7 | (are [res a b] (= res (deep-merge a b)) 8 | nil nil nil 9 | {:a 1} nil {:a 1} 10 | {:a 1} {:a 1} nil 11 | {:a 1 :b 2} {:a 1} {:b 2} 12 | {:a 2} {:a 1} {:a 2} 13 | {:a [2]} {:a [1]} {:a [2]} 14 | {:a nil} {:a 1} {:a nil} 15 | {:a {:m 1 :n 2}} {:a {:m 1}} {:a {:n 2}} 16 | {:a nil} {:a {:m 1}} {:a nil} 17 | {:a {:m 1}} {:a nil} {:a {:m 1}})) 18 | -------------------------------------------------------------------------------- /test/xiana/hash_test.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.hash-test 2 | (:require 3 | [clojure.test :refer [deftest is]] 4 | [xiana.hash :as hash])) 5 | 6 | (def password "myPersonalPassword!") 7 | 8 | (defn testing-ok 9 | [settings] 10 | (let [encrypted (hash/make settings password)] 11 | (is (true? (hash/check settings password encrypted))))) 12 | 13 | (defn testing-mistake 14 | [settings] 15 | (let [encrypted (hash/make settings password)] 16 | (is (false? (hash/check settings "myWrongPassword!" encrypted))))) 17 | 18 | (deftest test-full-functionality-bcrypt 19 | (let [fragment {:deps {:auth {:hash-algorithm :bcrypt}}}] 20 | (testing-mistake fragment) 21 | (testing-ok fragment))) 22 | 23 | (deftest test-full-functionality-script 24 | (let [fragment {:deps {:auth {:hash-algorithm :scrypt}}}] 25 | (testing-mistake fragment) 26 | (testing-ok fragment))) 27 | 28 | (deftest test-full-functionality-pbkdf2 29 | (let [fragment {:deps {:auth {:hash-algorithm :pbkdf2}}}] 30 | (testing-mistake fragment) 31 | (testing-ok fragment))) 32 | 33 | (deftest test-full-functionality-argon2 34 | (let [fragment {:deps {:auth {:hash-algorithm :argon2}}}] 35 | (testing-mistake fragment) 36 | (testing-ok fragment))) 37 | 38 | (deftest hash-behavior 39 | (let [pwd "not_nil" 40 | state {:deps {:auth {:hash-algorithm :bcrypt 41 | :bcrypt-settings {:work-factor 11}}}} 42 | hash1 (hash/make state pwd) 43 | hash2 (hash/make state pwd)] 44 | (is (false? (hash/check state hash1 hash2))) 45 | (is (not= hash1 hash2)) 46 | (is (true? (and (hash/check state pwd hash1) 47 | (hash/check state pwd hash2)))))) 48 | -------------------------------------------------------------------------------- /test/xiana/interceptor/cors_test.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.interceptor.cors-test 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | [xiana.interceptor.cors :refer [cors-headers interceptor]])) 5 | 6 | (deftest cors-interceptor-test 7 | (testing "cross-origin-headers interceptor" 8 | (let [leave (:leave interceptor) 9 | origin "http://localhost:3001"] 10 | 11 | (testing "when request is a preflight request" 12 | (let [state {:request {:request-method :options} 13 | :deps {:cors-origin origin}} 14 | expected (update-in state [:response] 15 | merge 16 | {:status 200 17 | :headers (cors-headers origin) 18 | :body "preflight complete"}) 19 | result (leave state)] 20 | (is (= expected result)))) 21 | 22 | (testing "when request is not a preflight request" 23 | (let [state {:request {:request-method :get} 24 | :deps {:cors-origin origin} 25 | :response {:headers {}}} 26 | expected (update-in state [:response :headers] 27 | merge (cors-headers origin)) 28 | result (leave state)] 29 | (is (= expected result))))))) 30 | -------------------------------------------------------------------------------- /test/xiana/interceptor/error_test.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.interceptor.error-test 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | [xiana.interceptor.error :refer [response]])) 5 | 6 | (defn make-error-state [response] 7 | {:error (ex-info "Test exception" {:xiana/response response})}) 8 | 9 | (deftest response-test 10 | (testing "Error response interceptor" 11 | (let [resp {:error "Test error"} 12 | error-state (make-error-state resp) 13 | f (:error response) 14 | result (f error-state)] 15 | (is (= {:response resp} result)) 16 | 17 | (testing "When :error is nil" 18 | (let [error-state {:error nil} 19 | result (f error-state)] 20 | (is (= {:error nil} result))))))) 21 | -------------------------------------------------------------------------------- /test/xiana/interceptor/kebab_camel_test.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.interceptor.kebab-camel-test 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | [xiana.interceptor.kebab-camel :as kc])) 5 | 6 | (deftest req->kebab-resp->camel-test 7 | (testing "Transforms keys of request params to kebab case" 8 | (let [state {:request {:params {:paramKey1 1 :paramKey2 2 :paramKey3 3} 9 | :body-params {:bodyParamKey1 1 :bodyParamKey2 2 :bodyParamKey3 3} 10 | :query-params {:queryParamKey1 1 :queryParamKey2 2 :queryParamKey3 3} 11 | :path-params {:pathParamKey1 1 :pathParamKey2 2 :pathParamKey3 3} 12 | :form-params {:formParamKey1 1 :formParamKey2 2 :formParamKey3 3} 13 | :multipart-params {:multipartParamKey1 1 :multipartParamKey2 2 :multipartParamKey3 3}}} 14 | expected {:request {:params {:param-key-1 1 :param-key-2 2 :param-key-3 3} 15 | :body-params {:body-param-key-1 1 :body-param-key-2 2 :body-param-key-3 3} 16 | :query-params {:query-param-key-1 1 :query-param-key-2 2 :query-param-key-3 3} 17 | :path-params {:path-param-key-1 1 :path-param-key-2 2 :path-param-key-3 3} 18 | :form-params {:form-param-key-1 1 :form-param-key-2 2 :form-param-key-3 3} 19 | :multipart-params {:multipart-param-key-1 1 :multipart-param-key-2 2 :multipart-param-key-3 3}}} 20 | enter (:enter kc/interceptor) 21 | result (enter state)] 22 | (is (= expected result)))) 23 | 24 | (testing "Transform keys of response body to Camel case" 25 | (let [state {:response {:body {:param-key-1 1 :param-key-2 2 :param-key-3 3}}} 26 | expected {:response {:body {:paramKey1 1 :paramKey2 2 :paramKey3 3}}} 27 | leave (:leave kc/interceptor) 28 | result (leave state)] 29 | (is (= expected result))))) 30 | 31 | -------------------------------------------------------------------------------- /test/xiana/interceptor/multipart_test.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.interceptor.multipart-test 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.test :refer [deftest is testing]] 5 | [peridot.multipart :as p] 6 | [ring.mock.request :as mock] 7 | [xiana.interceptor :as interceptor]) 8 | (:import 9 | (java.io 10 | File))) 11 | 12 | (defn state [] 13 | {:request 14 | (merge (mock/request :post "/upload") 15 | (p/build {:data (io/file "test/resources/multipart.csv")}))}) 16 | 17 | (deftest multipart-test 18 | (testing "Multipart support in the Framework interceptor chain" 19 | (let [f (:enter interceptor/params) 20 | r (f (state)) 21 | data-params (get-in r [:request :params :data])] 22 | (is (= "multipart.csv" (:filename data-params))) 23 | (is (= "text/csv" (:content-type data-params))) 24 | (is (instance? File (:tempfile data-params))) 25 | (is (pos? (:size data-params))) 26 | (is (= "first_name,last_name,age\nJohn,Doe,25\nJane,Doe,24\nAlice,Smith,22\nBob,Johnson,30\n" 27 | (slurp (:tempfile data-params))))))) 28 | -------------------------------------------------------------------------------- /test/xiana/interceptor/muuntaja_test.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.interceptor.muuntaja-test 2 | (:require 3 | [clojure.string :as str] 4 | [clojure.test :refer [deftest is]] 5 | [muuntaja.format.core :as format] 6 | [xiana.interceptor.muuntaja :as muuntaja])) 7 | 8 | (def data-sample [["note" "anything" "note"]]) 9 | 10 | (deftest contains-default-xlm 11 | (let [instance (muuntaja/xml-encoder '_) 12 | byte-array (format/encode-to-bytes instance {} "utf-8") 13 | XLM-string (str/join (map #(char (bit-and % 255)) byte-array)) 14 | expected ""] 15 | ;; verify if response is equal to the expected 16 | (is (= XLM-string expected)))) 17 | 18 | (deftest enconde-arbitrary-xml 19 | (let [instance (muuntaja/xml-encoder '_) 20 | byte-array (format/encode-to-bytes instance data-sample "utf-8") 21 | XLM-string (str/join (map #(char (bit-and % 255)) byte-array)) 22 | expected "anything"] 23 | ;; verify if response is equal to the expected 24 | (is (= XLM-string expected)))) 25 | -------------------------------------------------------------------------------- /test/xiana/route/helpers_test.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.route.helpers-test 2 | (:require 3 | [clojure.test :refer [deftest is]] 4 | [xiana.route.helpers :as helpers])) 5 | 6 | (defn test-handler 7 | "Sample test handler function for the tests." 8 | [_] 9 | {:status 200 :body "Ok"}) 10 | 11 | (def test-state 12 | "Sample test state." 13 | {:request {} 14 | :request-data {:handler test-handler}}) 15 | 16 | ;; test default not-found handler response 17 | (deftest contains-not-found-response 18 | (let [response (:response (helpers/not-found {})) 19 | expected {:status 404, :body "Not Found"}] 20 | ;; verify if the response and expected value are equal 21 | (is (= response expected)))) 22 | 23 | ;; test default action handler: error response 24 | (deftest contains-action-error-response 25 | (let [response (:response (helpers/action {})) 26 | expected {:status 500 :body "Internal Server error"}] 27 | ;; verify if the response and expected value are equal 28 | (is (= response expected)))) 29 | 30 | ;; test default action handler: ok response 31 | (deftest contains-action-ok-response 32 | (let [response (:response (helpers/action test-state)) 33 | expected {:status 200, :body "Ok"}] 34 | ;; verify if the response and expected value are equal 35 | (is (= response expected)))) 36 | -------------------------------------------------------------------------------- /test/xiana/session_test.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.session-test 2 | (:require 3 | [clojure.test :refer [deftest is]] 4 | [xiana.session :as session])) 5 | 6 | ;; initial session-instance instance 7 | (def session-instance (:session-backend (session/init-backend {}))) 8 | 9 | ;; test add and fetch reify implementations 10 | (deftest session-protocol-add!-fetch 11 | (let [user-id (random-uuid)] 12 | ;; add user id 13 | (session/add! session-instance :user-id {:id user-id}) 14 | ;; verify if user ids are equal 15 | (is (= {:id user-id} 16 | (session/fetch session-instance :user-id))))) 17 | 18 | ;; test delete! reify implementation 19 | (deftest session-protocol-delete! 20 | (let [user-id (session/fetch session-instance :user-id)] 21 | ;; remove user identification 22 | (when user-id 23 | (session/delete! session-instance :user-id)) 24 | ;; verify if was removed 25 | (is (nil? (session/fetch session-instance :user-id))))) 26 | 27 | ;; test erase reify implementation 28 | (deftest session-protocol-add!-erase! 29 | (let [user-id (random-uuid) 30 | session-id (random-uuid)] 31 | ;; add session instance values 32 | (session/add! session-instance :user-id {:id user-id}) 33 | (session/add! session-instance :session-id {:id session-id}) 34 | ;; verify if the values exists 35 | (is (= {:id user-id} (session/fetch session-instance :user-id))) 36 | (is (= {:id session-id} (session/fetch session-instance :session-id))) 37 | ;; erase and verify if session instance is empty 38 | (is (empty? (session/erase! session-instance))))) 39 | -------------------------------------------------------------------------------- /test/xiana/state_test.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.state-test 2 | (:require 3 | [clojure.test :refer [deftest is]] 4 | [xiana.state :as state])) 5 | 6 | (def state-initial-map 7 | {:deps {} 8 | :request {} 9 | :response {}}) 10 | 11 | ;; test empty state creation 12 | (deftest initial-state 13 | (let [result (state/make {} {}) 14 | expected (state/map->State state-initial-map)] 15 | ;; verify if the response and expected value are equal 16 | (is (= result expected)))) 17 | -------------------------------------------------------------------------------- /test/xiana/web_socket/router_test.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.web-socket.router-test 2 | (:require 3 | [clojure.test :refer [deftest is]] 4 | [jsonista.core :as j] 5 | [reitit.core :as r] 6 | [xiana.interceptor :as interceptors] 7 | [xiana.websockets :refer [router]])) 8 | 9 | (defn string-log [state] 10 | (assoc state :response-data {:reply-fn identity 11 | :reply "Log was called via string"})) 12 | 13 | (defn json-log [state] 14 | (assoc state :response-data {:reply-fn identity 15 | :reply "Log was called via json"})) 16 | 17 | (defn log-edn [state] 18 | (assoc state :response-data {:reply-fn identity 19 | :reply "Log was called via edn"})) 20 | 21 | (def routes 22 | (r/router [["/log-string" {:action string-log}] 23 | ["log-json" {:action json-log}] 24 | ["log-edn" {:action log-edn}]] 25 | {:data {:default-interceptors [(interceptors/message "Incoming message...")]}})) 26 | 27 | (def routing 28 | (partial router routes)) 29 | 30 | (deftest router-test 31 | (let [string-action "/log-string" 32 | json-action (j/write-value-as-string {:action :log-json}) 33 | edn-action "{:action :log-edn}"] 34 | (is (= "Log was called via string" 35 | (-> (routing {:request-data 36 | {:income-msg string-action}}) 37 | :response-data 38 | :reply))) 39 | (is (= "Log was called via json" 40 | (-> (routing {:request-data 41 | {:income-msg json-action}}) 42 | :response-data 43 | :reply))) 44 | (is (= "Log was called via edn" 45 | (-> (routing {:request-data 46 | {:income-msg edn-action}}) 47 | :response-data 48 | :reply))))) 49 | -------------------------------------------------------------------------------- /test/xiana/webserver_test.clj: -------------------------------------------------------------------------------- 1 | (ns xiana.webserver-test 2 | (:require 3 | [clojure.test :refer [deftest function? is]] 4 | [xiana.handler :refer [handler-fn]] 5 | [xiana.route :as route])) 6 | 7 | (def default-interceptors []) 8 | 9 | (def sample-request 10 | {:uri "/" :request-method :get}) 11 | 12 | (def sample-routes 13 | "Sample routes structure." 14 | {:routes [["/" {:action #(assoc % :response {:status 200, :body ":action"})}]]}) 15 | 16 | (deftest handler-fn-creation 17 | ;; test if handler-fn return 18 | (let [handler-fn (handler-fn {:controller-interceptors default-interceptors})] 19 | ;; check if return handler is a function 20 | (is (function? handler-fn)))) 21 | 22 | ;; test jetty handler function call 23 | (deftest call-handler-fn 24 | (let [f (handler-fn (route/reset sample-routes))] 25 | ;; verify if it's the right response 26 | (is (= (f sample-request) {:status 200, :body ":action"})))) 27 | -------------------------------------------------------------------------------- /test/xiana_fixture.clj: -------------------------------------------------------------------------------- 1 | (ns xiana-fixture 2 | (:require 3 | [piotr-yuxuan.closeable-map :refer [closeable-map]] 4 | [xiana.commons :refer [rename-key]] 5 | [xiana.config :as config] 6 | [xiana.db :as db-core] 7 | [xiana.rbac :as rbac] 8 | [xiana.route :as routes] 9 | [xiana.session :as session-backend] 10 | [xiana.sse :as sse] 11 | [xiana.webserver :as ws])) 12 | 13 | (defn ->system 14 | [app-cfg] 15 | (-> (config/config) 16 | (merge app-cfg) 17 | (rename-key :xiana/auth :auth) 18 | db-core/docker-postgres! 19 | db-core/connect 20 | db-core/migrate! 21 | session-backend/init-backend 22 | routes/reset 23 | rbac/init 24 | sse/init 25 | ws/start 26 | closeable-map)) 27 | 28 | (defn std-system-fixture 29 | [config f] 30 | (with-open [_ (->system config)] 31 | (f))) 32 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:plugins [:kaocha.plugin/cloverage]} 3 | --------------------------------------------------------------------------------