├── components ├── env │ ├── resources │ │ └── env │ │ │ └── .keep │ ├── Readme.md │ ├── deps.edn │ └── src │ │ └── clojure │ │ └── realworld │ │ └── env │ │ ├── interface.clj │ │ └── core.clj ├── log │ ├── resources │ │ └── log │ │ │ └── .keep │ ├── Readme.md │ ├── deps.edn │ └── src │ │ └── clojure │ │ └── realworld │ │ └── log │ │ ├── core.clj │ │ ├── interface.clj │ │ └── config.clj ├── spec │ ├── resources │ │ └── spec │ │ │ └── .keep │ ├── Readme.md │ ├── deps.edn │ └── src │ │ └── clojure │ │ └── realworld │ │ └── spec │ │ ├── interface.clj │ │ └── core.clj ├── tag │ ├── resources │ │ └── tag │ │ │ └── .keep │ ├── Readme.md │ ├── src │ │ └── clojure │ │ │ └── realworld │ │ │ └── tag │ │ │ ├── interface.clj │ │ │ └── core.clj │ ├── deps.edn │ └── test │ │ └── clojure │ │ └── realworld │ │ └── tag │ │ └── core_test.clj ├── user │ ├── resources │ │ └── user │ │ │ └── .keep │ ├── Readme.md │ ├── src │ │ └── clojure │ │ │ └── realworld │ │ │ └── user │ │ │ ├── interface │ │ │ └── spec.clj │ │ │ ├── interface.clj │ │ │ ├── store.clj │ │ │ ├── spec.clj │ │ │ └── core.clj │ ├── deps.edn │ └── test │ │ └── clojure │ │ └── realworld │ │ └── user │ │ ├── store_test.clj │ │ └── core_test.clj ├── article │ ├── resources │ │ └── article │ │ │ └── .keep │ ├── Readme.md │ ├── src │ │ └── clojure │ │ │ └── realworld │ │ │ └── article │ │ │ ├── interface │ │ │ └── spec.clj │ │ │ ├── interface.clj │ │ │ ├── spec.clj │ │ │ ├── core.clj │ │ │ └── store.clj │ ├── deps.edn │ └── test │ │ └── clojure │ │ └── realworld │ │ └── article │ │ ├── core_test.clj │ │ └── store_test.clj ├── comment │ ├── resources │ │ └── comment │ │ │ └── .keep │ ├── Readme.md │ ├── src │ │ └── clojure │ │ │ └── realworld │ │ │ └── comment │ │ │ ├── interface │ │ │ └── spec.clj │ │ │ ├── interface.clj │ │ │ ├── spec.clj │ │ │ ├── store.clj │ │ │ └── core.clj │ ├── deps.edn │ └── test │ │ └── clojure │ │ └── realworld │ │ └── comment │ │ ├── store_test.clj │ │ └── core_test.clj ├── profile │ ├── resources │ │ └── profile │ │ │ └── .keep │ ├── Readme.md │ ├── src │ │ └── clojure │ │ │ └── realworld │ │ │ └── profile │ │ │ ├── interface │ │ │ └── spec.clj │ │ │ ├── interface.clj │ │ │ ├── spec.clj │ │ │ ├── store.clj │ │ │ └── core.clj │ ├── deps.edn │ └── test │ │ └── clojure │ │ └── realworld │ │ └── profile │ │ ├── store_test.clj │ │ └── core_test.clj └── database │ ├── resources │ └── database │ │ └── .keep │ ├── Readme.md │ ├── deps.edn │ └── src │ └── clojure │ └── realworld │ └── database │ ├── interface.clj │ ├── core.clj │ └── schema.clj ├── bases └── rest-api │ ├── resources │ └── rest_api │ │ └── .keep │ ├── Readme.md │ ├── deps.edn │ ├── src │ └── clojure │ │ └── realworld │ │ └── rest_api │ │ ├── main.clj │ │ ├── middleware.clj │ │ ├── api.clj │ │ └── handler.clj │ └── test │ └── clojure │ └── realworld │ └── rest_api │ └── handler_test.clj ├── logo.png ├── .vscode ├── extensions.json └── settings.json ├── env.edn ├── .media ├── how-to │ ├── 05_base.png │ ├── 01_workspace.png │ ├── 03_components.png │ ├── 02_dev_project.png │ ├── 06_empty_project.png │ ├── 10_repl_config.png │ ├── 07_filled_project.png │ ├── 08_workspace_info.png │ ├── 09_workspace_info_after_commit.png │ └── 04_components_added_to_development_project.png ├── readme │ ├── 01_rest_api.png │ ├── 02_polylith_info.png │ └── 03_calva_jack_in.png └── gitpod │ ├── Gitpod-to-REPL.png │ └── Gitpod-Polylith-RealWorld.png ├── development └── src │ └── dev │ ├── server.clj │ ├── furkan.clj │ ├── getting_started.clj │ └── hello_repl.clj ├── .gitignore ├── workspace.edn ├── api-tests └── run-api-tests.sh ├── .idea ├── clojure-deps.xml └── runConfigurations │ └── REPL.xml ├── .gitpod.yml ├── .gitpod └── Dockerfile ├── LICENSE ├── projects └── realworld-backend │ └── deps.edn ├── deps.edn ├── gitpod.md ├── .circleci └── config.yml ├── how-to.md └── readme.md /components/env/resources/env/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/log/resources/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/spec/resources/spec/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/tag/resources/tag/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/user/resources/user/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bases/rest-api/resources/rest_api/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/article/resources/article/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/comment/resources/comment/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/profile/resources/profile/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/database/resources/database/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/spec/Readme.md: -------------------------------------------------------------------------------- 1 | # spec component 2 | 3 | Component containing specs. -------------------------------------------------------------------------------- /bases/rest-api/Readme.md: -------------------------------------------------------------------------------- 1 | # rest-api base 2 | 3 | Base containing rest api endpoints and handlers. 4 | -------------------------------------------------------------------------------- /components/log/Readme.md: -------------------------------------------------------------------------------- 1 | # log component 2 | 3 | Component handling functions related to logging. 4 | -------------------------------------------------------------------------------- /components/tag/Readme.md: -------------------------------------------------------------------------------- 1 | # tag component 2 | 3 | Component handling functions related to tag domain. 4 | -------------------------------------------------------------------------------- /components/user/Readme.md: -------------------------------------------------------------------------------- 1 | # user component 2 | 3 | Component handling functions related to user domain. 4 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/logo.png -------------------------------------------------------------------------------- /components/article/Readme.md: -------------------------------------------------------------------------------- 1 | # article component 2 | 3 | Component handling functions related to article domain. 4 | -------------------------------------------------------------------------------- /components/comment/Readme.md: -------------------------------------------------------------------------------- 1 | # comment component 2 | 3 | Component handling functions related to comment domain. 4 | -------------------------------------------------------------------------------- /components/database/Readme.md: -------------------------------------------------------------------------------- 1 | # database component 2 | 3 | Component containing functions related to database. 4 | -------------------------------------------------------------------------------- /components/env/Readme.md: -------------------------------------------------------------------------------- 1 | # env component 2 | 3 | Component containing functionality to read environment variables. -------------------------------------------------------------------------------- /components/profile/Readme.md: -------------------------------------------------------------------------------- 1 | # profile component 2 | 3 | Component handling functions related to profile domain. 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "betterthantomorrow.calva", 4 | "djblue.portal" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /env.edn: -------------------------------------------------------------------------------- 1 | {:allowed-origins "http://localhost:3000,http://localhost:4100" 2 | :environment "LOCAL" 3 | :database "database.db"} 4 | -------------------------------------------------------------------------------- /.media/how-to/05_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/05_base.png -------------------------------------------------------------------------------- /.media/readme/01_rest_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/readme/01_rest_api.png -------------------------------------------------------------------------------- /components/env/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {} 3 | :aliases {:test {:extra-paths [] 4 | :extra-deps []}}} 5 | -------------------------------------------------------------------------------- /.media/how-to/01_workspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/01_workspace.png -------------------------------------------------------------------------------- /.media/how-to/03_components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/03_components.png -------------------------------------------------------------------------------- /.media/gitpod/Gitpod-to-REPL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/gitpod/Gitpod-to-REPL.png -------------------------------------------------------------------------------- /.media/how-to/02_dev_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/02_dev_project.png -------------------------------------------------------------------------------- /.media/how-to/06_empty_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/06_empty_project.png -------------------------------------------------------------------------------- /.media/how-to/10_repl_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/10_repl_config.png -------------------------------------------------------------------------------- /.media/readme/02_polylith_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/readme/02_polylith_info.png -------------------------------------------------------------------------------- /.media/readme/03_calva_jack_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/readme/03_calva_jack_in.png -------------------------------------------------------------------------------- /.media/how-to/07_filled_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/07_filled_project.png -------------------------------------------------------------------------------- /.media/how-to/08_workspace_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/08_workspace_info.png -------------------------------------------------------------------------------- /.media/gitpod/Gitpod-Polylith-RealWorld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/gitpod/Gitpod-Polylith-RealWorld.png -------------------------------------------------------------------------------- /components/env/src/clojure/realworld/env/interface.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.env.interface 2 | (:require [clojure.realworld.env.core :as core])) 3 | 4 | (def env core/env) 5 | -------------------------------------------------------------------------------- /.media/how-to/09_workspace_info_after_commit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/09_workspace_info_after_commit.png -------------------------------------------------------------------------------- /components/log/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {com.taoensso/timbre {:mvn/version "6.0.4"}} 3 | :aliases {:test {:extra-paths [] 4 | :extra-deps []}}} 5 | -------------------------------------------------------------------------------- /components/spec/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {metosin/spec-tools {:mvn/version "0.10.5"}} 3 | :aliases {:test {:extra-paths [] 4 | :extra-deps []}}} 5 | -------------------------------------------------------------------------------- /components/tag/src/clojure/realworld/tag/interface.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.tag.interface 2 | (:require [clojure.realworld.tag.core :as core])) 3 | 4 | (defn all-tags [] 5 | (core/all-tags)) 6 | -------------------------------------------------------------------------------- /.media/how-to/04_components_added_to_development_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/04_components_added_to_development_project.png -------------------------------------------------------------------------------- /components/profile/src/clojure/realworld/profile/interface/spec.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.profile.interface.spec 2 | (:require [clojure.realworld.profile.spec :as spec])) 3 | 4 | (def profile spec/profile) 5 | -------------------------------------------------------------------------------- /components/comment/src/clojure/realworld/comment/interface/spec.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.comment.interface.spec 2 | (:require [clojure.realworld.comment.spec :as spec])) 3 | 4 | (def id spec/id) 5 | 6 | (def add-comment spec/add-comment) 7 | -------------------------------------------------------------------------------- /components/tag/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {com.github.seancorfield/honeysql {:mvn/version "2.4.980"} 3 | org.clojure/java.jdbc {:mvn/version "0.7.12"}} 4 | :aliases {:test {:extra-paths ["test"] 5 | :extra-deps []}}} 6 | -------------------------------------------------------------------------------- /components/article/src/clojure/realworld/article/interface/spec.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.article.interface.spec 2 | (:require [clojure.realworld.article.spec :as spec])) 3 | 4 | (def create-article spec/create-article) 5 | 6 | (def update-article spec/update-article) 7 | -------------------------------------------------------------------------------- /development/src/dev/server.clj: -------------------------------------------------------------------------------- 1 | (ns dev.server 2 | (:require [clojure.realworld.rest-api.main :as main])) 3 | 4 | (defn start! [port] 5 | (main/start! port)) 6 | 7 | (defn stop! [] 8 | (main/stop!)) 9 | 10 | (comment 11 | (start! 6003) 12 | (stop!) 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /components/database/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {com.github.seancorfield/honeysql {:mvn/version "2.4.980"} 3 | org.clojure/java.jdbc {:mvn/version "0.7.12"} 4 | org.xerial/sqlite-jdbc {:mvn/version "3.41.0.0"}} 5 | :aliases {:test {:extra-paths [] 6 | :extra-deps []}}} 7 | -------------------------------------------------------------------------------- /components/profile/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {com.github.seancorfield/honeysql {:mvn/version "2.4.980"} 3 | metosin/spec-tools {:mvn/version "0.10.5"} 4 | org.clojure/java.jdbc {:mvn/version "0.7.12"}} 5 | :aliases {:test {:extra-paths ["test"] 6 | :extra-deps []}}} 7 | -------------------------------------------------------------------------------- /components/log/src/clojure/realworld/log/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.log.core 2 | (:require [taoensso.timbre :as timbre])) 3 | 4 | (defmacro info [args] 5 | `(timbre/log! :info :p ~args)) 6 | 7 | (defmacro warn [args] 8 | `(timbre/log! :warn :p ~args)) 9 | 10 | (defmacro error [args] 11 | `(timbre/log! :error :p ~args)) 12 | -------------------------------------------------------------------------------- /components/user/src/clojure/realworld/user/interface/spec.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.user.interface.spec 2 | (:require [clojure.realworld.user.spec :as spec])) 3 | 4 | (def login spec/login) 5 | 6 | (def register spec/register) 7 | 8 | (def update-user spec/update-user) 9 | 10 | (def user spec/user) 11 | 12 | (def visible-user spec/visible-user) 13 | -------------------------------------------------------------------------------- /development/src/dev/furkan.clj: -------------------------------------------------------------------------------- 1 | (ns dev.furkan 2 | (:require [portal.api :as p])) 3 | 4 | (def portal-atom (atom nil)) 5 | 6 | (defn launch-portal [] 7 | (let [portal (p/open (or @portal-atom {:launcher :vs-code}))] 8 | (reset! portal-atom portal) 9 | (add-tap #'p/submit) 10 | portal)) 11 | 12 | (comment 13 | (launch-portal) 14 | 15 | ) 16 | -------------------------------------------------------------------------------- /components/comment/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {clj-time/clj-time {:mvn/version "0.15.2"} 3 | com.github.seancorfield/honeysql {:mvn/version "2.4.980"} 4 | metosin/spec-tools {:mvn/version "0.10.5"} 5 | org.clojure/java.jdbc {:mvn/version "0.7.12"}} 6 | :aliases {:test {:extra-paths ["test"] 7 | :extra-deps []}}} 8 | -------------------------------------------------------------------------------- /components/spec/src/clojure/realworld/spec/interface.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.spec.interface 2 | (:require [clojure.realworld.spec.core :as core])) 3 | 4 | (def username? core/username?) 5 | 6 | (def non-empty-string? core/non-empty-string?) 7 | 8 | (def email? core/email?) 9 | 10 | (def uri-string? core/uri-string?) 11 | 12 | (def slug? core/slug?) 13 | 14 | (def password? core/password?) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/classes 2 | **/target 3 | **/.cpcache 4 | **/.DS_Store 5 | 6 | # IntelliJ-related 7 | *.iws 8 | *.ipr 9 | *.iml 10 | .idea/* 11 | !.idea/runConfigurations/REPL.xml 12 | !.idea/clojure-deps.xml 13 | 14 | # REPL-related 15 | .nrepl-port 16 | 17 | # Project specific 18 | database.db 19 | test.db 20 | 21 | # VSCode-related 22 | **/.calva/* 23 | .clj-kondo/* 24 | out/* 25 | .lsp/* 26 | **/.portal 27 | -------------------------------------------------------------------------------- /components/article/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {clj-time/clj-time {:mvn/version "0.15.2"} 3 | com.github.seancorfield/honeysql {:mvn/version "2.4.980"} 4 | metosin/spec-tools {:mvn/version "0.10.5"} 5 | org.clojure/java.jdbc {:mvn/version "0.7.12"} 6 | slugger/slugger {:mvn/version "1.0.1"}} 7 | :aliases {:test {:extra-paths ["test"] 8 | :extra-deps []}}} 9 | -------------------------------------------------------------------------------- /bases/rest-api/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {compojure/compojure {:mvn/version "1.7.0"} 3 | org.clojure/data.json {:mvn/version "2.4.0"} 4 | ring/ring-json {:mvn/version "0.5.1"} 5 | ring/ring-jetty-adapter {:mvn/version "1.9.6"} 6 | ring-logger-timbre/ring-logger-timbre {:mvn/version "0.7.6"}} 7 | :aliases {:test {:extra-paths ["test"] 8 | :extra-deps []}}} 9 | -------------------------------------------------------------------------------- /workspace.edn: -------------------------------------------------------------------------------- 1 | {:top-namespace "clojure.realworld" 2 | :interface-ns "interface" 3 | :default-profile-name "default" 4 | :compact-views #{} 5 | :vcs {:name "git" 6 | :auto-add true} 7 | :tag-patterns {:stable "stable-*" 8 | :release "v[0-9]*"} 9 | :projects {"development" {:alias "dev"} 10 | "realworld-backend" {:alias "rb"}}} 11 | -------------------------------------------------------------------------------- /components/log/src/clojure/realworld/log/interface.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.log.interface 2 | (:require [clojure.realworld.log.config :as config] 3 | [clojure.realworld.log.core :as core])) 4 | 5 | (defn init [] 6 | (config/init)) 7 | 8 | (defmacro info [& args] 9 | `(core/info ~args)) 10 | 11 | (defmacro warn [& args] 12 | `(core/warn ~args)) 13 | 14 | (defmacro error [& args] 15 | `(core/error ~args)) 16 | -------------------------------------------------------------------------------- /components/profile/src/clojure/realworld/profile/interface.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.profile.interface 2 | (:require [clojure.realworld.profile.core :as core])) 3 | 4 | (defn fetch-profile [auth-user username] 5 | (core/fetch-profile auth-user username)) 6 | 7 | (defn follow! [auth-user username] 8 | (core/follow! auth-user username)) 9 | 10 | (defn unfollow! [auth-user username] 11 | (core/unfollow! auth-user username)) 12 | -------------------------------------------------------------------------------- /components/comment/src/clojure/realworld/comment/interface.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.comment.interface 2 | (:require [clojure.realworld.comment.core :as core])) 3 | 4 | (defn article-comments [auth-user slug] 5 | (core/article-comments auth-user slug)) 6 | 7 | (defn add-comment! [auth-user slug comment] 8 | (core/add-comment! auth-user slug comment)) 9 | 10 | (defn delete-comment! [auth-user id] 11 | (core/delete-comment! auth-user id)) 12 | -------------------------------------------------------------------------------- /components/tag/src/clojure/realworld/tag/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.tag.core 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.realworld.database.interface :as database] 4 | [honey.sql :as sql])) 5 | 6 | (defn all-tags [] 7 | (let [query {:select [:name] 8 | :from [:tag]} 9 | result (jdbc/query (database/db) (sql/format query)) 10 | res {:tags (mapv :name result)}] 11 | [true res])) 12 | -------------------------------------------------------------------------------- /components/user/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {clj-jwt/clj-jwt {:mvn/version "0.1.1"} 3 | clj-time/clj-time {:mvn/version "0.15.2"} 4 | crypto-password/crypto-password {:mvn/version "0.3.0"} 5 | com.github.seancorfield/honeysql {:mvn/version "2.4.980"} 6 | metosin/spec-tools {:mvn/version "0.10.5"} 7 | org.clojure/java.jdbc {:mvn/version "0.7.12"}} 8 | :aliases {:test {:extra-paths ["test"] 9 | :extra-deps []}}} 10 | -------------------------------------------------------------------------------- /components/profile/src/clojure/realworld/profile/spec.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.profile.spec 2 | (:require [clojure.realworld.spec.interface :as spec] 3 | [spec-tools.data-spec :as ds])) 4 | 5 | (def profile 6 | (ds/spec {:name :core/profile 7 | :spec {:username spec/username? 8 | :following boolean? 9 | (ds/opt :image) (ds/maybe spec/uri-string?) 10 | (ds/opt :bio) (ds/maybe spec/non-empty-string?)}})) 11 | -------------------------------------------------------------------------------- /components/database/src/clojure/realworld/database/interface.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.database.interface 2 | (:require [clojure.realworld.database.core :as core] 3 | [clojure.realworld.database.schema :as schema])) 4 | 5 | (defn db 6 | ([path] 7 | (core/db path)) 8 | ([] 9 | (core/db))) 10 | 11 | (defn db-exists? [] 12 | (core/db-exists?)) 13 | 14 | (defn generate-db [db] 15 | (schema/generate-db db)) 16 | 17 | (defn drop-db [db] 18 | (schema/drop-db db)) 19 | 20 | (defn valid-schema? [db] 21 | (schema/valid-schema? db)) 22 | -------------------------------------------------------------------------------- /api-tests/run-api-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | 4 | SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" 5 | 6 | APIURL=${APIURL:-https://conduit.productionready.io/api} 7 | USERNAME=${USERNAME:-u`date +%s`} 8 | EMAIL=${EMAIL:-$USERNAME@mail.com} 9 | PASSWORD=${PASSWORD:-password} 10 | 11 | npx newman run $SCRIPTDIR/Conduit.postman_collection.json \ 12 | --delay-request 500 \ 13 | --global-var "APIURL=$APIURL" \ 14 | --global-var "USERNAME=$USERNAME" \ 15 | --global-var "EMAIL=$EMAIL" \ 16 | --global-var "PASSWORD=$PASSWORD" -------------------------------------------------------------------------------- /.idea/clojure-deps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /components/database/src/clojure/realworld/database/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.database.core 2 | (:require [clojure.java.io :as io] 3 | [clojure.realworld.env.interface :as env])) 4 | 5 | (defn- db-path [] 6 | (if (contains? env/env :database) 7 | (env/env :database) 8 | "database.db")) 9 | 10 | (defn db 11 | ([path] 12 | {:classname "org.sqlite.JDBC" 13 | :subprotocol "sqlite" 14 | :subname path}) 15 | ([] 16 | (db (db-path)))) 17 | 18 | (defn db-exists? [] 19 | (let [db-file (io/file "database.db")] 20 | (.exists db-file))) 21 | -------------------------------------------------------------------------------- /components/user/src/clojure/realworld/user/interface.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.user.interface 2 | (:require [clojure.realworld.user.core :as core] 3 | [clojure.realworld.user.store :as store])) 4 | 5 | (defn login! [login-input] 6 | (core/login! login-input)) 7 | 8 | (defn register! [register-input] 9 | (core/register! register-input)) 10 | 11 | (defn user-by-token [token] 12 | (core/user-by-token token)) 13 | 14 | (defn update-user! [auth-user user-input] 15 | (core/update-user! auth-user user-input)) 16 | 17 | (defn find-by-username-or-id [username-or-id] 18 | (store/find-by-username-or-id username-or-id)) 19 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod/Dockerfile 3 | 4 | vscode: 5 | extensions: 6 | - betterthantomorrow.calva 7 | - djblue.portal 8 | 9 | ports: 10 | - port: 5900 11 | onOpen: ignore 12 | - port: 6080 13 | onOpen: ignore 14 | - port: 6003 15 | onOpen: ignore 16 | 17 | tasks: 18 | - name: Polylith Real World nREPL Server 19 | command: clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version,"1.0.0"},cider/cider-nrepl {:mvn/version,"0.28.5"}}}' -M:dev:test:default -m nrepl.cmdline --middleware "[cider.nrepl/cider-middleware]" 20 | - name: Open Getting Started 21 | command: code development/src/dev/getting_started.clj 22 | - name: poly shell 23 | command: poly shell 24 | -------------------------------------------------------------------------------- /.idea/runConfigurations/REPL.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.gitpod/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-java-17 2 | 3 | ENV PATH="/home/gitpod/bin:/workspace/clojure-polylith-realworld-example-app/.gitpod:${PATH}" 4 | ENV JAVA_HOME="/home/gitpod/.sdkman/candidates/java/current" 5 | ENV JAVA_TOOL_OPTIONS="-Xmx3489m" 6 | 7 | RUN mkdir /home/gitpod/bin 8 | RUN wget -P /home/gitpod https://github.com/polyfy/polylith/releases/download/stable-master/poly-0.2.18-SNAPSHOT.jar 9 | RUN bash -c cat <<'EOF' > /home/gitpod/bin/poly && chmod +x /home/gitpod/bin/poly 10 | #!/bin/bash 11 | 12 | java $JVM_OPTS "-jar" "/home/gitpod/poly-0.2.18-SNAPSHOT.jar" $@ 13 | 14 | EOF 15 | RUN curl -sL https://raw.githubusercontent.com/borkdude/deps.clj/master/install > install_clojure && chmod +x install_clojure && ./install_clojure --dir /home/gitpod/bin --as-clj && clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version,"1.0.0"},cider/cider-nrepl {:mvn/version,"0.28.5"}}}' -P -------------------------------------------------------------------------------- /components/article/src/clojure/realworld/article/interface.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.article.interface 2 | (:require [clojure.realworld.article.core :as core])) 3 | 4 | (defn article [auth-user slug] 5 | (core/article auth-user slug)) 6 | 7 | (defn create-article! [auth-user article-input] 8 | (core/create-article! auth-user article-input)) 9 | 10 | (defn update-article! [auth-user slug article-input] 11 | (core/update-article! auth-user slug article-input)) 12 | 13 | (defn delete-article! [auth-user slug] 14 | (core/delete-article! auth-user slug)) 15 | 16 | (defn favorite-article! [auth-user slug] 17 | (core/favorite-article! auth-user slug)) 18 | 19 | (defn unfavorite-article! [auth-user slug] 20 | (core/unfavorite-article! auth-user slug)) 21 | 22 | (defn feed [auth-user limit offset] 23 | (core/feed auth-user limit offset)) 24 | 25 | (defn articles [auth-user limit offset author tag favorited] 26 | (core/articles auth-user limit offset author tag favorited)) 27 | -------------------------------------------------------------------------------- /bases/rest-api/src/clojure/realworld/rest_api/main.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.rest-api.main 2 | (:require [clojure.realworld.log.interface :as log] 3 | [clojure.realworld.rest-api.api :as api] 4 | [ring.adapter.jetty :refer [run-jetty]]) 5 | (:gen-class)) 6 | 7 | (def ^:private server-ref (atom nil)) 8 | 9 | (defn start! 10 | [port] 11 | (if-let [_server @server-ref] 12 | (log/warn "Server already running? (stop!) it first.") 13 | (do 14 | (log/info "Starting server on port: " port) 15 | (api/init) 16 | (reset! server-ref 17 | (run-jetty api/app 18 | {:port port 19 | :join? false}))))) 20 | 21 | (defn stop! [] 22 | (if-let [server @server-ref] 23 | (do (api/destroy) 24 | (.stop server) 25 | (reset! server-ref nil)) 26 | (log/warn "No server"))) 27 | 28 | (defn -main [& _args] 29 | (start! (Integer/valueOf 30 | (or (System/getenv "port") 31 | "6003") 32 | 10))) 33 | -------------------------------------------------------------------------------- /components/comment/src/clojure/realworld/comment/spec.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.comment.spec 2 | (:require [clojure.realworld.spec.interface :as spec] 3 | [clojure.realworld.profile.interface.spec :as profile-spec] 4 | [spec-tools.core :as st] 5 | [spec-tools.data-spec :as ds])) 6 | 7 | (def id 8 | (st/spec {:spec pos-int? 9 | :type :long 10 | :description "A long spec that defines a comment id which is a positive integer"})) 11 | 12 | (def add-comment 13 | (ds/spec {:name :core/add-comment 14 | :spec {:body spec/non-empty-string?}})) 15 | 16 | (def comment-spec 17 | (ds/spec {:name :core/comment 18 | :spec {:id pos-int? 19 | :updatedAt string? 20 | :createdAt string? 21 | :body spec/non-empty-string? 22 | :author profile-spec/profile}})) 23 | 24 | (def visible-comment 25 | (ds/spec {:name :core/visible-comment 26 | :spec {:comment comment-spec}})) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Furkan Bayraktar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/comment/src/clojure/realworld/comment/store.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.comment.store 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.realworld.database.interface :as database] 4 | [honey.sql :as sql])) 5 | 6 | (defn comments [article-id] 7 | (let [query {:select [:*] 8 | :from [:comment] 9 | :where [:= :articleId article-id]}] 10 | (jdbc/query (database/db) (sql/format query) {:identifiers identity}))) 11 | 12 | (defn find-by-id [id] 13 | (let [query {:select [:*] 14 | :from [:comment] 15 | :where [:= :id id]} 16 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})] 17 | (first results))) 18 | 19 | (defn add-comment! [comment-input] 20 | (let [result (jdbc/insert! (database/db) :comment 21 | comment-input 22 | {:entities identity})] 23 | (-> result first first val))) 24 | 25 | (defn delete-comment! [id] 26 | (let [query {:delete-from :comment 27 | :where [:= :id id]}] 28 | (jdbc/execute! (database/db) (sql/format query)) 29 | nil)) 30 | -------------------------------------------------------------------------------- /components/profile/src/clojure/realworld/profile/store.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.profile.store 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.realworld.database.interface :as database] 4 | [honey.sql :as sql])) 5 | 6 | (defn following? [user-id followed-user-id] 7 | (let [query {:select [:*] 8 | :from [:userFollows] 9 | :where [:and [:= :userId user-id] 10 | [:= :followedUserId followed-user-id]]} 11 | results (jdbc/query (database/db) (sql/format query))] 12 | (-> results first nil? not))) 13 | 14 | (defn follow! [user-id followed-user-id] 15 | (when-not (following? user-id followed-user-id) 16 | (jdbc/insert! (database/db) :userFollows {:userId user-id 17 | :followedUserId followed-user-id}))) 18 | 19 | (defn unfollow! [user-id followed-user-id] 20 | (when (following? user-id followed-user-id) 21 | (let [query {:delete-from :userFollows 22 | :where [:and [:= :userId user-id] 23 | [:= :followedUserId followed-user-id]]}] 24 | (jdbc/execute! (database/db) (sql/format query))))) 25 | -------------------------------------------------------------------------------- /components/tag/test/clojure/realworld/tag/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.tag.core-test 2 | (:require [clojure.test :refer [deftest is use-fixtures]] 3 | [clojure.realworld.database.interface :as database] 4 | [clojure.realworld.tag.core :as core] 5 | [clojure.java.jdbc :as jdbc])) 6 | 7 | (defn test-db 8 | ([] {:classname "org.sqlite.JDBC" 9 | :subprotocol "sqlite" 10 | :subname "test.db"}) 11 | ([_] (test-db))) 12 | 13 | (defn prepare-for-tests [f] 14 | (with-redefs [database/db test-db] 15 | (let [db (test-db)] 16 | (database/generate-db db) 17 | (f) 18 | (database/drop-db db)))) 19 | 20 | (use-fixtures :each prepare-for-tests) 21 | 22 | (deftest all-tags--return-tags-response 23 | (let [_ (jdbc/insert-multi! (test-db) :tag [{:name "tag1"} 24 | {:name "tag2"} 25 | {:name "tag3"} 26 | {:name "tag4"} 27 | {:name "tag5"}]) 28 | [ok? res] (core/all-tags)] 29 | (is (true? ok?)) 30 | (is (= {:tags ["tag1" "tag2" "tag3" "tag4" "tag5"]} res)))) 31 | -------------------------------------------------------------------------------- /components/user/src/clojure/realworld/user/store.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.user.store 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.realworld.database.interface :as database] 4 | [clojure.realworld.user.spec :as spec] 5 | [clojure.spec.alpha :as s] 6 | [honey.sql :as sql])) 7 | 8 | (defn find-by [key value] 9 | (let [query {:select [:*] 10 | :from [:user] 11 | :where [:= key value]} 12 | results (jdbc/query (database/db) (sql/format query))] 13 | (first results))) 14 | 15 | (defn find-by-email [email] 16 | (find-by :email email)) 17 | 18 | (defn find-by-username [username] 19 | (find-by :username username)) 20 | 21 | (defn find-by-id [id] 22 | (find-by :id id)) 23 | 24 | (defn find-by-username-or-id [username-or-id] 25 | (if (s/valid? spec/id username-or-id) 26 | (find-by-id username-or-id) 27 | (find-by-username username-or-id))) 28 | 29 | (defn insert-user! [user-input] 30 | (jdbc/insert! (database/db) :user user-input)) 31 | 32 | (defn update-user! [id user-input] 33 | (let [query {:update :user 34 | :set user-input 35 | :where [:= :id id]}] 36 | (jdbc/execute! (database/db) (sql/format query)))) 37 | -------------------------------------------------------------------------------- /components/env/src/clojure/realworld/env/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.env.core 2 | (:require [clojure.edn :as edn] 3 | [clojure.java.io :as io] 4 | [clojure.string :as str])) 5 | 6 | (defn keywordize [s] 7 | (when s 8 | (-> (str/lower-case s) 9 | (str/replace "_" "-") 10 | (str/replace "." "-") 11 | (keyword)))) 12 | 13 | (defn read-system-env [] 14 | (->> (System/getenv) 15 | (map (fn [[k v]] [(keywordize k) v])) 16 | (into {}))) 17 | 18 | (defn read-system-props [] 19 | (->> (System/getProperties) 20 | (map (fn [[k v]] [(keywordize k) v])) 21 | (into {}))) 22 | 23 | (defn slurp-file [f] 24 | (when-let [f (io/file f)] 25 | (when (.exists f) 26 | (slurp f)))) 27 | 28 | (defn read-env-file [f] 29 | (when-let [content (slurp-file f)] 30 | (try 31 | (let [parsed-content (edn/read-string content)] 32 | (if (map? parsed-content) 33 | parsed-content 34 | {})) 35 | (catch Exception _ 36 | {})))) 37 | 38 | (def env 39 | (let [env-file (read-env-file "env.edn") 40 | system-props (read-system-props) 41 | system-env (read-system-env)] 42 | (merge system-props 43 | system-env 44 | env-file))) 45 | -------------------------------------------------------------------------------- /components/profile/src/clojure/realworld/profile/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.profile.core 2 | (:require [clojure.realworld.profile.store :as store] 3 | [clojure.realworld.user.interface :as user])) 4 | 5 | (defn- create-profile [user following?] 6 | (let [profile (assoc (select-keys user [:username :bio :image]) 7 | :following following?)] 8 | {:profile profile})) 9 | 10 | (defn fetch-profile [auth-user username-or-id] 11 | (let [user (user/find-by-username-or-id username-or-id)] 12 | (if (nil? user) 13 | [false {:errors {:username ["Cannot find a profile with given username."]}}] 14 | (let [following? (if (nil? auth-user) 15 | false 16 | (store/following? (:id auth-user) (:id user)))] 17 | [true (create-profile user following?)])))) 18 | 19 | (defn follow! [auth-user username] 20 | (if-let [user (user/find-by-username-or-id username)] 21 | (do 22 | (store/follow! (:id auth-user) (:id user)) 23 | [true (create-profile user true)]) 24 | [false {:errors {:username ["Cannot find a profile with given username."]}}])) 25 | 26 | (defn unfollow! [auth-user username] 27 | (if-let [user (user/find-by-username-or-id username)] 28 | (do 29 | (store/unfollow! (:id auth-user) (:id user)) 30 | [true (create-profile user false)]) 31 | [false {:errors {:username ["Cannot find a profile with given username."]}}])) 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://calva.io/connect-sequences/ for more info about Calva REPL Connect Sequences 3 | "calva.replConnectSequences": [ 4 | { 5 | "projectType": "deps.edn", 6 | "afterCLJReplJackInCode": "(require '[dev.server] :reload) (in-ns 'dev.server) (start! 6003)", 7 | "name": "Polylith RealWorld Server REPL (start)", 8 | "autoSelectForJackIn": true, 9 | "projectRootPath": ["."], 10 | "menuSelections": { 11 | "cljAliases": ["dev", "test"] 12 | }, 13 | }, 14 | { 15 | "projectType": "deps.edn", 16 | "name": "Polylith RealWorld Server REPL (connect)", 17 | "afterCLJReplJackInCode": "(require '[dev.server] :reload) (in-ns 'dev.server) (start! 6003)", 18 | "autoSelectForConnect": true, 19 | "projectRootPath": ["."], 20 | } 21 | ], 22 | 23 | // When autoConnectRepl is set to `true`, you can start the REPL yourself and keep it running 24 | // using the command produced by: **Calva: Copy Jack-in Command Line to Clipboard**. This will 25 | // ensure that Calva's dependencies are met and Calva will connect to your REPL automatically 26 | // when the project is opened. 27 | "calva.autoConnectRepl": true, 28 | 29 | // Set this to `true` to make Calva show the Jack-in process in the Terminal pane. 30 | "calva.autoOpenJackInTerminal": false, 31 | // Enable this for a some startup messages from Calva. 32 | "calva.showCalvaSaysOnStart": false, 33 | 34 | // Calm down the hover eagerness a bit 35 | "editor.hover.delay": 1500 36 | } 37 | -------------------------------------------------------------------------------- /components/article/src/clojure/realworld/article/spec.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.article.spec 2 | (:require [clojure.realworld.spec.interface :as spec] 3 | [clojure.realworld.profile.interface.spec :as profile-spec] 4 | [spec-tools.data-spec :as ds])) 5 | 6 | (def create-article 7 | (ds/spec {:name :core/create-article 8 | :spec {:title spec/non-empty-string? 9 | :description spec/non-empty-string? 10 | :body spec/non-empty-string? 11 | (ds/opt :tagList) [spec/non-empty-string?]}})) 12 | 13 | (def update-article 14 | (ds/spec {:name :core/update-article 15 | :spec {(ds/opt :title) spec/non-empty-string? 16 | (ds/opt :description) spec/non-empty-string? 17 | (ds/opt :body) spec/non-empty-string?}})) 18 | 19 | (def article 20 | (ds/spec {:name :core/article 21 | :spec {:id pos-int? 22 | :slug spec/slug? 23 | :title spec/non-empty-string? 24 | :description spec/non-empty-string? 25 | :body spec/non-empty-string? 26 | :updatedAt string? 27 | :createdAt string? 28 | :favorited boolean? 29 | :favoritesCount nat-int? 30 | :author profile-spec/profile 31 | (ds/opt :tagList) [spec/non-empty-string?]}})) 32 | 33 | (def visible-article 34 | (ds/spec {:name :core/visible-article 35 | :spec {:article article}})) 36 | -------------------------------------------------------------------------------- /components/user/src/clojure/realworld/user/spec.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.user.spec 2 | (:require [clojure.realworld.spec.interface :as spec] 3 | [spec-tools.core :as st] 4 | [spec-tools.data-spec :as ds])) 5 | 6 | (def id 7 | (st/spec {:spec pos-int? 8 | :type :long 9 | :description "A long spec that defines a user id which is a positive integer"})) 10 | 11 | (def login 12 | (ds/spec {:name :core/login 13 | :spec {:email spec/email? 14 | :password spec/password?}})) 15 | 16 | (def register 17 | (ds/spec {:name :core/register 18 | :spec {:username spec/username? 19 | :email spec/email? 20 | :password spec/password?}})) 21 | 22 | (def update-user 23 | (ds/spec {:name :core/update-user 24 | :spec {:email spec/email? 25 | :username spec/username? 26 | :password spec/password? 27 | :image (ds/maybe spec/uri-string?) 28 | :bio (ds/maybe spec/non-empty-string?)} 29 | :keys-default ds/opt})) 30 | 31 | (def user-base 32 | {:id id 33 | :email spec/email? 34 | :username spec/username? 35 | (ds/opt :image) (ds/maybe spec/uri-string?) 36 | (ds/opt :bio) (ds/maybe spec/non-empty-string?)}) 37 | 38 | (def user 39 | (ds/spec {:name :core/user 40 | :spec user-base})) 41 | 42 | (def visible-user 43 | (ds/spec {:name :core/visible-user 44 | :spec {:user (assoc user-base 45 | (ds/opt :token) spec/non-empty-string?)}})) 46 | -------------------------------------------------------------------------------- /projects/realworld-backend/deps.edn: -------------------------------------------------------------------------------- 1 | {:ring {:init clojure.realworld.rest-api.api/init 2 | :destroy clojure.realworld.rest-api.api/destroy 3 | :handler clojure.realworld.rest-api.api/app 4 | :port 6003} 5 | 6 | :mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"} 7 | "clojars" {:url "https://clojars.org/repo"}} 8 | 9 | :deps {poly/article {:local/root "../../components/article"} 10 | poly/comment {:local/root "../../components/comment"} 11 | poly/database {:local/root "../../components/database"} 12 | poly/env {:local/root "../../components/env"} 13 | poly/log {:local/root "../../components/log"} 14 | poly/profile {:local/root "../../components/profile"} 15 | poly/spec {:local/root "../../components/spec"} 16 | poly/tag {:local/root "../../components/tag"} 17 | poly/user {:local/root "../../components/user"} 18 | poly/rest-api {:local/root "../../bases/rest-api"} 19 | 20 | org.clojure/clojure {:mvn/version "1.11.1"}} 21 | 22 | :aliases {:test {:extra-paths [] 23 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}}} 24 | 25 | :uberjar {:main clojure.realworld.rest-api.main} 26 | 27 | :ring {:extra-deps {org.slf4j/slf4j-nop {:mvn/version "2.0.3"} 28 | furkan3ayraktar/polylith-clj-deps-ring {:git/url "https://github.com/furkan3ayraktar/polylith-clj-deps-ring.git" 29 | :sha "7bb68846bb8a200a486a2886f1af95984538ec25" 30 | :deps/root "projects/core"}} 31 | 32 | :main-opts ["-m" "polylith.clj-deps-ring.cli.main" "start"]}}} 33 | -------------------------------------------------------------------------------- /bases/rest-api/src/clojure/realworld/rest_api/middleware.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.rest-api.middleware 2 | (:require [clojure.string :as str] 3 | [clojure.realworld.log.interface :as log] 4 | [clojure.realworld.user.interface :as user] 5 | [clojure.realworld.env.interface :as env])) 6 | 7 | (defn wrap-auth-user [handler] 8 | (fn [req] 9 | (let [authorization (get (:headers req) "authorization") 10 | token (when authorization (-> (str/split authorization #" ") last))] 11 | (if-not (str/blank? token) 12 | (let [[ok? user] (user/user-by-token token)] 13 | (if ok? 14 | (handler (assoc req :auth-user (:user user))) 15 | (handler req))) 16 | (handler req))))) 17 | 18 | (defn wrap-authorization [handler] 19 | (fn [req] 20 | (if (:auth-user req) 21 | (handler req) 22 | {:status 401 23 | :body {:errors {:authorization "Authorization required."}}}))) 24 | 25 | (defn wrap-exceptions [handler] 26 | (fn [req] 27 | (try 28 | (handler req) 29 | (catch Exception e 30 | (let [message (str "An unknown exception occurred.")] 31 | (log/error e message) 32 | {:status 500 33 | :body {:errors {:other [message]}}}))))) 34 | 35 | (defn create-access-control-header [origin] 36 | (let [allowed-origins (or (env/env :allowed-origins) "") 37 | origins (str/split allowed-origins #",") 38 | allowed-origin (some #{origin} origins)] 39 | {"Access-Control-Allow-Origin" allowed-origin 40 | "Access-Control-Allow-Methods" "POST, GET, PUT, OPTIONS, DELETE" 41 | "Access-Control-Max-Age" "3600" 42 | "Access-Control-Allow-Headers" "Authorization, Content-Type, x-requested-with"})) 43 | 44 | (defn wrap-cors [handler] 45 | (fn [req] 46 | (let [origin (get (:headers req) "origin") 47 | response (handler req)] 48 | (assoc response :headers (merge (:headers response) (create-access-control-header origin)))))) 49 | -------------------------------------------------------------------------------- /components/comment/src/clojure/realworld/comment/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.comment.core 2 | (:require [clj-time.core :as t] 3 | [clojure.realworld.article.interface :as article] 4 | [clojure.realworld.comment.store :as store] 5 | [clojure.realworld.profile.interface :as profile])) 6 | 7 | (defn comment->visible-comment [auth-user comment] 8 | (let [user-id (:userId comment) 9 | [_ author] (profile/fetch-profile auth-user user-id)] 10 | (-> comment 11 | (dissoc :userId :articleId) 12 | (assoc :author (:profile author))))) 13 | 14 | (defn article-comments [auth-user slug] 15 | (let [[ok? {:keys [article]}] (article/article auth-user slug)] 16 | (if ok? 17 | (let [article-id (:id article) 18 | comments (store/comments article-id) 19 | res (mapv #(comment->visible-comment auth-user %) comments)] 20 | [true {:comments res}]) 21 | [false {:errors {:slug ["Cannot find an article with given slug."]}}]))) 22 | 23 | (defn add-comment! [auth-user slug {:keys [body]}] 24 | (let [[ok? {:keys [article]}] (article/article auth-user slug)] 25 | (if ok? 26 | (let [now (t/now) 27 | comment {:body body 28 | :articleId (:id article) 29 | :userId (:id auth-user) 30 | :createdAt now 31 | :updatedAt now} 32 | comment-id (store/add-comment! comment)] 33 | (if comment-id 34 | (let [added-comment (store/find-by-id comment-id) 35 | res (comment->visible-comment auth-user added-comment)] 36 | [true {:comment res}]) 37 | [false {:errors {:other ["Cannot insert comment into db."]}}])) 38 | [false {:errors {:slug ["Cannot find an article with given slug."]}}]))) 39 | 40 | (defn delete-comment! [auth-user id] 41 | (if-let [comment (store/find-by-id id)] 42 | (if (= (:id auth-user) (:userId comment)) 43 | [true (store/delete-comment! id)] 44 | [false {:errors {:authorization ["You need to be author of this comment to delete it."]}}]) 45 | [false {:errors {:id ["Cannot find a comment with given id."]}}])) 46 | -------------------------------------------------------------------------------- /components/user/test/clojure/realworld/user/store_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.user.store-test 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.realworld.database.interface :as database] 4 | [clojure.realworld.user.store :as store] 5 | [clojure.test :refer [deftest is use-fixtures]])) 6 | 7 | (defn test-db 8 | ([] {:classname "org.sqlite.JDBC" 9 | :subprotocol "sqlite" 10 | :subname "test.db"}) 11 | ([_] (test-db))) 12 | 13 | (defn prepare-for-tests [f] 14 | (with-redefs [database/db test-db] 15 | (let [db (test-db)] 16 | (database/generate-db db) 17 | (f) 18 | (database/drop-db db)))) 19 | 20 | (use-fixtures :each prepare-for-tests) 21 | 22 | (deftest find-by-key--test 23 | (let [_ (jdbc/insert! (test-db) :user {:email "test@test.com" 24 | :username "username"}) 25 | res1 (store/find-by :email "test@test.com") 26 | user {:bio nil 27 | :email "test@test.com" 28 | :id 1 29 | :image nil 30 | :password nil 31 | :username "username"} 32 | res2 (store/find-by :username "username")] 33 | (is (= user res1)) 34 | (is (= user res2)))) 35 | 36 | (deftest insert-user!--test 37 | (let [user {:bio "bio" 38 | :email "test@test.com" 39 | :image "image" 40 | :password "password" 41 | :username "username"} 42 | _ (store/insert-user! user) 43 | res (store/find-by-email "test@test.com")] 44 | (is (= (assoc user :id 1) res)))) 45 | 46 | (deftest update-user!--test 47 | (let [_ (store/insert-user! {:bio "bio" 48 | :email "test@test.com" 49 | :image "image" 50 | :password "password" 51 | :username "username"}) 52 | user {:bio "updated-bio" 53 | :email "updated-test@test.com" 54 | :image "updated-image" 55 | :password "updated-password" 56 | :username "updated-username"} 57 | _ (store/update-user! 1 user) 58 | res (store/find-by-email "updated-test@test.com")] 59 | (is (= (assoc user :id 1) res)))) 60 | -------------------------------------------------------------------------------- /components/profile/test/clojure/realworld/profile/store_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.profile.store-test 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.realworld.database.interface :as database] 4 | [clojure.realworld.profile.store :as store] 5 | [clojure.test :refer [deftest is use-fixtures]])) 6 | 7 | (defn test-db 8 | ([] {:classname "org.sqlite.JDBC" 9 | :subprotocol "sqlite" 10 | :subname "test.db"}) 11 | ([_] (test-db))) 12 | 13 | (defn prepare-for-tests [f] 14 | (with-redefs [database/db test-db] 15 | (let [db (test-db)] 16 | (database/generate-db db) 17 | (f) 18 | (database/drop-db db)))) 19 | 20 | (use-fixtures :each prepare-for-tests) 21 | 22 | (deftest following?--following--return-true 23 | (let [_ (jdbc/insert! (test-db) :userFollows {:userId 1 24 | :followedUserId 2}) 25 | res (store/following? 1 2)] 26 | (is (true? res)))) 27 | 28 | (deftest following?--not-following--return-false 29 | (let [res (store/following? 1 2)] 30 | (is (false? res)))) 31 | 32 | (deftest follow!--currently-not-following--insert-user-follows 33 | (let [before-following? (store/following? 1 2) 34 | _ (store/follow! 1 2) 35 | after-following? (store/following? 1 2)] 36 | (is (false? before-following?)) 37 | (is (true? after-following?)))) 38 | 39 | (deftest follow!--currently-following--do-nothing 40 | (let [_ (store/follow! 1 2) 41 | before-following? (store/following? 1 2) 42 | _ (store/follow! 1 2) 43 | after-following? (store/following? 1 2)] 44 | (is (true? before-following?)) 45 | (is (true? after-following?)))) 46 | 47 | (deftest unfollow!--currently-following--delete-user-follows 48 | (let [_ (store/follow! 1 2) 49 | before-following? (store/following? 1 2) 50 | _ (store/unfollow! 1 2) 51 | after-following? (store/following? 1 2)] 52 | (is (true? before-following?)) 53 | (is (false? after-following?)))) 54 | 55 | (deftest unfollow!--currently-not-following--do-nothing 56 | (let [before-following? (store/following? 1 2) 57 | _ (store/unfollow! 1 2) 58 | after-following? (store/following? 1 2)] 59 | (is (false? before-following?)) 60 | (is (false? after-following?)))) 61 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"} 2 | "clojars" {:url "https://clojars.org/repo"}} 3 | 4 | :aliases {:dev {:extra-paths ["development/src"] 5 | 6 | :extra-deps {; Components 7 | poly/article {:local/root "components/article"} 8 | poly/comment {:local/root "components/comment"} 9 | poly/database {:local/root "components/database"} 10 | poly/env {:local/root "components/env"} 11 | poly/log {:local/root "components/log"} 12 | poly/profile {:local/root "components/profile"} 13 | poly/spec {:local/root "components/spec"} 14 | poly/tag {:local/root "components/tag"} 15 | poly/user {:local/root "components/user"} 16 | 17 | ; Bases 18 | poly/rest-api {:local/root "bases/rest-api"} 19 | 20 | ; Development dependencies 21 | djblue/portal {:mvn/version "0.35.1"} 22 | clj-http/clj-http {:mvn/version "3.12.3"} 23 | org.clojure/clojure {:mvn/version "1.11.1"} 24 | org.slf4j/slf4j-nop {:mvn/version "2.0.3"}}} 25 | 26 | :test {:extra-paths ["components/article/test" 27 | "components/comment/test" 28 | "components/profile/test" 29 | "components/tag/test" 30 | "components/user/test" 31 | 32 | "bases/rest-api/test"] 33 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}}} 34 | 35 | :poly {:main-opts ["-m" "polylith.clj.core.poly-cli.core"] 36 | :extra-deps {polylith/clj-poly {:mvn/version "0.2.18-SNAPSHOT"}}} 37 | 38 | :build {:deps {io.github.clojure/tools.build {:mvn/version "0.9.3"} 39 | io.github.seancorfield/build-clj {:git/tag "v0.9.2" 40 | :git/sha "9c9f078"} 41 | org.clojure/tools.deps {:mvn/version "0.16.1281"}} 42 | :paths [] 43 | :ns-default build}}} 44 | -------------------------------------------------------------------------------- /gitpod.md: -------------------------------------------------------------------------------- 1 | # Hack on Real World Polylith in your Browser 2 | 3 | Using [Gitpod](https://www.gitpod.io) you can explore a [Polylith](https://polylith.gitbook.io/polylith/) implementation of [Real World](https://www.realworld.how/) from the Clojure REPL, without downloading or installing anything at all. 4 | 5 | ![Gitpod-Polylith-RealWorld-example](.media/gitpod/Gitpod-Polylith-RealWorld.png) 6 | 7 | Don't click on this link just yet: 8 | https://gitpod.io/#https://github.com/furkan3ayraktar/clojure-polylith-realworld-example-app 9 | 10 | The link will take you to a full blown [VS Code](https://code.visualstudio.com/) running in your browser. The VS Code instance will have [Clojure](https://clojure.org) development support (through [Calva](https://calva.io)). The first time you use it, it may be quite a long wait. But then (after some little more waiting) you will be in the editor, connected to the REPL of the Polylith Real World server! 11 | 12 | ## Prerequisites 13 | 14 | * A Github account. 15 | * Curiosity 16 | 17 | That's it. 18 | 19 | ## From here to the Polylith REPL 20 | 21 | When you click the link you will first need to sign in to Gitpod using your Github account, then create the workspace, then wait, then wait a bit again. Then you will have the REPL under your fingertips. The process looks like so: 22 | 23 | ![Three click workflow of using the Gitpodified link to the repo, siging up/in to Gitpod, creating a workspace, and BOOM, a REPL into the running RealWorld Polylith example](.media/gitpod/Gitpod-to-REPL.png) 24 | 25 | The Clojure file that opens will have further instructions and suggestions for what you can try at the REPL. There's also a file `src/hello_repl.clj` available for anyone unfamiliar with Calva and/or Clojure to start with. 26 | 27 | Now you can click that link above. 😄 (That said, see below about [considering forking first](#fork-first).) 28 | 29 | Happy coding! ❤️ 30 | 31 | ## Fork first? 32 | 33 | If you just click the link above, things will work, but you will not be able to immediately push your work to your own repo. If you find that you want to do that, use your git skills to retarget the local (albeit Gitpod hosted) repo to a repo of your own. 34 | 35 | It could be a good idea to fork this repo first, of course. 36 | 37 | ## Gitpod Free hours are limited 38 | 39 | You may find it so fun to play with Polylith in the REPL that you run out of Gitpod hours. Then it is time to either: 40 | 41 | 1. Upgrade to a paid Gitpod plan 42 | 2. Run this repository locally 43 | * Check [the readme of this repository](readme.md) for instructions of how to run this example locally. (It is just a few steps more than running this on Gitpod.) 44 | 45 | -------------------------------------------------------------------------------- /components/comment/test/clojure/realworld/comment/store_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.comment.store-test 2 | (:require [clj-time.core :as t] 3 | [clojure.java.jdbc :as jdbc] 4 | [clojure.realworld.comment.store :as store] 5 | [clojure.realworld.database.interface :as database] 6 | [clojure.test :refer [deftest is use-fixtures]])) 7 | 8 | (defn test-db 9 | ([] {:classname "org.sqlite.JDBC" 10 | :subprotocol "sqlite" 11 | :subname "test.db"}) 12 | ([_] (test-db))) 13 | 14 | (defn prepare-for-tests [f] 15 | (with-redefs [database/db test-db] 16 | (let [db (test-db)] 17 | (database/generate-db db) 18 | (f) 19 | (database/drop-db db)))) 20 | 21 | (use-fixtures :each prepare-for-tests) 22 | 23 | (deftest comments--no-comments--return-empty-vector 24 | (let [res (store/comments 1)] 25 | (is (= [] res)))) 26 | 27 | (deftest comments--some-comments--return-all-comments 28 | (let [_ (jdbc/insert-multi! (test-db) :comment [{:articleId 1 :body "body1"} 29 | {:articleId 1 :body "body2"} 30 | {:articleId 1 :body "body3"} 31 | {:articleId 1 :body "body4"}]) 32 | res (store/comments 1)] 33 | (is (= 4 (count res))))) 34 | 35 | (deftest find-by-id--comment-exists--return-comment 36 | (let [_ (jdbc/insert! (test-db) :comment {:body "body"}) 37 | comment (store/find-by-id 1)] 38 | (is (= "body" (:body comment))))) 39 | 40 | (deftest find-by-id--comment-does-not-exist--return-nil 41 | (let [comment (store/find-by-id 1)] 42 | (is (nil? comment)))) 43 | 44 | (deftest add-comment!--test 45 | (let [now (t/now) 46 | comment {:body "body" 47 | :createdAt now 48 | :updatedAt now 49 | :userId 1 50 | :articleId 1} 51 | res (store/add-comment! comment) 52 | added (store/find-by-id 1)] 53 | (is (= (assoc comment :id 1 54 | :createdAt (-> comment :createdAt str) 55 | :updatedAt (-> comment :updatedAt str)) 56 | added)) 57 | (is (= 1 res)))) 58 | 59 | (deftest delete-comment!--test 60 | (let [now (t/now) 61 | _ (store/add-comment! {:body "body" 62 | :createdAt now 63 | :updatedAt now 64 | :userId 1 65 | :articleId 1}) 66 | comment-before (store/find-by-id 1) 67 | _ (store/delete-comment! 1) 68 | comment-after (store/find-by-id 1)] 69 | (is (not (nil? comment-before))) 70 | (is (nil? comment-after)))) 71 | -------------------------------------------------------------------------------- /components/spec/src/clojure/realworld/spec/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.spec.core 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [clojure.string :as str] 5 | [spec-tools.core :as st]) 6 | (:import (java.util UUID))) 7 | 8 | (def ^:private email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$") 9 | (def ^:private uri-regex #"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)") 10 | (def ^:private slug-regex #"^[a-z0-9]+(?:-[a-z0-9]+)*$") 11 | 12 | (def non-empty-string? 13 | (st/spec {:spec (s/and string? #(not (str/blank? %))) 14 | :type :string 15 | :description "Non empty string spec. Checks with clojure.string/blank?"})) 16 | 17 | (def username? 18 | (st/spec {:spec non-empty-string? 19 | :type :string 20 | :description "A non empty string spec with a special username (UUID) generator." 21 | :gen #(gen/fmap (fn [_] (str (UUID/randomUUID))) 22 | (gen/string-alphanumeric))})) 23 | 24 | (def email? 25 | (st/spec {:spec (s/and string? #(re-matches email-regex %)) 26 | :type :string 27 | :description "A string spec that conforms to email-regex." 28 | :gen #(gen/fmap (fn [[s1 s2]] (str s1 "@" s2 ".com")) 29 | (gen/tuple (gen/string-alphanumeric) (gen/string-alphanumeric)))})) 30 | 31 | (def uri-string? 32 | (st/spec {:spec (s/and string? #(re-matches uri-regex %)) 33 | :type :string 34 | :description "A string spec that conforms to uri-regex." 35 | :gen #(gen/fmap (fn [[c1 c2]] 36 | (let [s1 (apply str c1) 37 | s2 (apply str c2)] 38 | (str "http://" s1 "." (subs s2 0 (if (< 3 (count s2)) 3 (count s2)))))) 39 | (gen/tuple (gen/vector (gen/char-alpha) 2 100) (gen/vector (gen/char-alpha) 2 5)))})) 40 | 41 | (def slug? 42 | (st/spec {:spec (s/and string? #(re-matches slug-regex %)) 43 | :type :string 44 | :description "A string spec that conforms to slug-regex." 45 | :gen #(gen/fmap (fn [[c1 c2]] 46 | (let [s1 (str/lower-case (apply str c1)) 47 | s2 (str/lower-case (apply str c2))] 48 | (str s1 "-" s2))) 49 | (gen/tuple (gen/vector (gen/char-alpha) 2 10) (gen/vector (gen/char-alpha) 2 10)))})) 50 | 51 | (def password? 52 | (st/spec {:spec (s/and string? #(<= 8 (count %))) 53 | :type :string 54 | :description "A string spec with more than or equal to 8 characters."})) 55 | -------------------------------------------------------------------------------- /bases/rest-api/src/clojure/realworld/rest_api/api.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.rest-api.api 2 | (:require [clojure.realworld.database.interface :as database] 3 | [clojure.realworld.rest-api.handler :as h] 4 | [clojure.realworld.rest-api.middleware :as m] 5 | [clojure.realworld.log.interface :as log] 6 | [compojure.core :refer [routes wrap-routes defroutes GET POST PUT DELETE ANY OPTIONS]] 7 | [ring.logger.timbre :as logger] 8 | [ring.middleware.json :as js] 9 | [ring.middleware.keyword-params :as kp] 10 | [ring.middleware.multipart-params :as mp] 11 | [ring.middleware.nested-params :as np] 12 | [ring.middleware.params :as pr])) 13 | 14 | (defroutes public-routes 15 | (OPTIONS "/**" [] h/options) 16 | (GET "/api/health" [] h/health) 17 | (POST "/api/users/login" [] h/login) 18 | (POST "/api/users" [] h/register) 19 | (GET "/api/profiles/:username" [] h/profile) 20 | (GET "/api/articles" [] h/articles) 21 | (GET "/api/articles/:slug" [] h/article) 22 | (GET "/api/articles/:slug/comments" [] h/comments) 23 | (GET "/api/tags" [] h/tags)) 24 | 25 | (defroutes private-routes 26 | (GET "/api/user" [] h/current-user) 27 | (PUT "/api/user" [] h/update-user) 28 | (POST "/api/profiles/:username/follow" [] h/follow-profile) 29 | (DELETE "/api/profiles/:username/follow" [] h/unfollow-profile) 30 | (GET "/api/articles/feed" [] h/feed) 31 | (POST "/api/articles" [] h/create-article) 32 | (PUT "/api/articles/:slug" [] h/update-article) 33 | (DELETE "/api/articles/:slug" [] h/delete-article) 34 | (POST "/api/articles/:slug/comments" [] h/add-comment) 35 | (DELETE "/api/articles/:slug/comments/:id" [] h/delete-comment) 36 | (POST "/api/articles/:slug/favorite" [] h/favorite-article) 37 | (DELETE "/api/articles/:slug/favorite" [] h/unfavorite-article)) 38 | 39 | (defroutes other-routes 40 | (ANY "/**" [] h/other)) 41 | 42 | (def ^:private app-routes 43 | (routes 44 | (-> private-routes 45 | (wrap-routes m/wrap-authorization) 46 | (wrap-routes m/wrap-auth-user)) 47 | (-> public-routes 48 | (wrap-routes m/wrap-auth-user)) 49 | other-routes)) 50 | 51 | (def app 52 | (-> app-routes 53 | logger/wrap-with-logger 54 | kp/wrap-keyword-params 55 | pr/wrap-params 56 | mp/wrap-multipart-params 57 | js/wrap-json-params 58 | np/wrap-nested-params 59 | m/wrap-exceptions 60 | js/wrap-json-response 61 | m/wrap-cors)) 62 | 63 | (defn init [] 64 | (try 65 | (log/init) 66 | (let [db (database/db)] 67 | (if (database/db-exists?) 68 | (if (database/valid-schema? db) 69 | (log/info "Database schema is valid.") 70 | (do 71 | (log/warn "Please fix database schema and restart") 72 | (System/exit 1))) 73 | (do 74 | (log/info "Generating database.") 75 | (database/generate-db db) 76 | (log/info "Database generated.")))) 77 | (log/info "Initialized server.") 78 | (catch Exception e 79 | (log/error e "Could not start server.")))) 80 | 81 | (defn destroy [] 82 | (log/info "Destroyed server.")) 83 | -------------------------------------------------------------------------------- /components/log/src/clojure/realworld/log/config.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.log.config 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :as str] 4 | [clojure.realworld.env.interface :as env] 5 | [taoensso.timbre :as timbre]) 6 | (:import (java.util Calendar) 7 | (java.text SimpleDateFormat) 8 | (java.io File IOException))) 9 | 10 | (defn- rename-old-create-new-log [^File log ^File old-log] 11 | (.renameTo log old-log) 12 | (.createNewFile log)) 13 | 14 | (defn- shift-log-period [log path prev-cal] 15 | (let [postfix (-> "yyyy-MM-dd" SimpleDateFormat. (.format (.getTime prev-cal))) 16 | last-index-of-dot (str/last-index-of path ".") 17 | file-name (if (>= last-index-of-dot 0) (subs path 0 last-index-of-dot) path) 18 | extension (if (>= last-index-of-dot 0) (subs path (+ 1 last-index-of-dot)) "") 19 | old-path (format "%s.%s.%s" file-name postfix extension) 20 | old-log (io/file old-path)] 21 | (if (.exists old-log) 22 | (loop [index 0] 23 | (let [index-path (format "%s.%d" old-path index) 24 | index-log (io/file index-path)] 25 | (if (.exists index-log) 26 | (recur (+ index 1)) 27 | (rename-old-create-new-log log index-log)))) 28 | (rename-old-create-new-log log old-log)))) 29 | 30 | (defn- log-cal [date] (let [now (Calendar/getInstance)] (.setTime now date) now)) 31 | 32 | (defn- prev-period-end-cal [date pattern] 33 | (let [cal (log-cal date) 34 | offset (case pattern 35 | :daily 1 36 | :weekly (.get cal Calendar/DAY_OF_WEEK) 37 | :monthly (.get cal Calendar/DAY_OF_MONTH) 38 | 0)] 39 | (.add cal Calendar/DAY_OF_MONTH (* -1 offset)) 40 | (.set cal Calendar/HOUR_OF_DAY 23) 41 | (.set cal Calendar/MINUTE 59) 42 | (.set cal Calendar/SECOND 59) 43 | (.set cal Calendar/MILLISECOND 999) 44 | cal)) 45 | 46 | (defn- rolling-appender 47 | "Returns a Rolling file appender. Opts: 48 | :path - logfile path. 49 | :pattern - frequency of rotation, e/o {:daily :weekly :monthly}." 50 | [& [{:keys [path pattern] 51 | :or {path "./timbre-rolling.log" 52 | pattern :daily}}]] 53 | 54 | {:enabled? true 55 | :async? false 56 | :min-level nil 57 | :rate-limit nil 58 | :output-fn :inherit 59 | :fn 60 | (fn [data] 61 | (let [{:keys [instant output_]} data 62 | output-str (force output_) 63 | prev-cal (prev-period-end-cal instant pattern)] 64 | (when-let [log (io/file path)] 65 | (try 66 | (when-not (.exists log) 67 | (io/make-parents log)) 68 | (if (.exists log) 69 | (when (<= (.lastModified log) (.getTimeInMillis prev-cal)) 70 | (shift-log-period log path prev-cal)) 71 | (.createNewFile log)) 72 | (spit path (with-out-str (println output-str)) :append true) 73 | (catch IOException _)))))}) 74 | 75 | (defn init [] 76 | (if (= "LOCAL" (env/env :environment)) 77 | (timbre/info "Initialized logging. Using console to print logs.") 78 | (do 79 | (timbre/set-config! {:level :info 80 | :appenders {:rolling-file-adapter (rolling-appender {:path "/var/log/tomcat8/backend.log"})}}) 81 | (timbre/info "Initialized logging. Using /var/log/tomcat8/backend.log path for rolling file adapter.")))) 82 | -------------------------------------------------------------------------------- /components/user/src/clojure/realworld/user/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.user.core 2 | (:require [clojure.realworld.user.store :as store] 3 | [crypto.password.pbkdf2 :as crypto] 4 | [clj-jwt.core :as jwt] 5 | [clj-time.coerce :as c] 6 | [clj-time.core :as t] 7 | [clojure.realworld.env.interface :as env])) 8 | 9 | (defn- token-secret [] 10 | (if (contains? env/env :secret) 11 | (env/env :secret) 12 | "some-default-secret-do-not-use-it")) 13 | 14 | (defn- generate-token [email username] 15 | (let [now (t/now) 16 | claim {:sub username 17 | :iss email 18 | :exp (t/plus now (t/days 7)) 19 | :iat now}] 20 | (-> claim jwt/jwt (jwt/sign :HS256 (token-secret)) jwt/to-str))) 21 | 22 | (defn- jwt-str->jwt [jwt-string] 23 | (try 24 | (jwt/str->jwt jwt-string) 25 | (catch Exception _ 26 | nil))) 27 | 28 | (defn- expired? [jwt] 29 | (if-let [exp (-> jwt :claims :exp)] 30 | (try 31 | (t/before? (c/from-long (* 1000 exp)) (t/now)) 32 | (catch Exception _ 33 | true)) 34 | true)) 35 | 36 | (defn- token->claims [jwt-string] 37 | (when-let [jwt (jwt-str->jwt jwt-string)] 38 | (when (and (jwt/verify jwt (token-secret)) 39 | (not (expired? jwt))) 40 | (:claims jwt)))) 41 | 42 | (defn encrypt-password [password] 43 | (-> password crypto/encrypt str)) 44 | 45 | (defn user->visible-user [user token] 46 | {:user (-> user 47 | (assoc :token token) 48 | (dissoc :password))}) 49 | 50 | (defn login! [{:keys [email password]}] 51 | (if-let [user (store/find-by-email email)] 52 | (if (crypto/check password (:password user)) 53 | (let [new-token (generate-token email (:username user))] 54 | [true (user->visible-user user new-token)]) 55 | [false {:errors {:password ["Invalid password."]}}]) 56 | [false {:errors {:email ["Invalid email."]}}])) 57 | 58 | (defn register! [{:keys [username email password]}] 59 | (if-let [_ (store/find-by-email email)] 60 | [false {:errors {:email ["A user exists with given email."]}}] 61 | (if-let [_ (store/find-by-username username)] 62 | [false {:errors {:username ["A user exists with given username."]}}] 63 | (let [new-token (generate-token email username) 64 | user-input {:email email 65 | :username username 66 | :password (encrypt-password password)} 67 | _ (store/insert-user! user-input)] 68 | (if-let [user (store/find-by-email email)] 69 | [true (user->visible-user user new-token)] 70 | [false {:errors {:other ["Cannot insert user into db."]}}]))))) 71 | 72 | (defn user-by-token [token] 73 | (let [claims (token->claims token) 74 | username (:sub claims) 75 | user (store/find-by-username username)] 76 | (if user 77 | [true (user->visible-user user token)] 78 | [false {:errors {:token ["Cannot find a user with associated token."]}}]))) 79 | 80 | (defn update-user! [auth-user {:keys [username email password image bio]}] 81 | (if (and (not (nil? email)) 82 | (not= email (:email auth-user)) 83 | (not (nil? (store/find-by-email email)))) 84 | [false {:errors {:email ["A user exists with given email."]}}] 85 | (if (and (not (nil? username)) 86 | (not= username (:username auth-user)) 87 | (not (nil? (store/find-by-username username)))) 88 | [false {:errors {:username ["A user exists with given username."]}}] 89 | (let [email-to-use (if email email (:email auth-user)) 90 | optional-map (filter #(-> % val nil? not) 91 | {:password (when password (encrypt-password password)) 92 | :email (when email email) 93 | :username (when username username)}) 94 | user-input (merge {:image image 95 | :bio bio} 96 | optional-map) 97 | _ (store/update-user! (:id auth-user) user-input)] 98 | (if-let [updated-user (store/find-by-email email-to-use)] 99 | [true (user->visible-user updated-user (:token auth-user))] 100 | [false {:errors {:other ["Cannot update user."]}}]))))) 101 | -------------------------------------------------------------------------------- /development/src/dev/getting_started.clj: -------------------------------------------------------------------------------- 1 | (ns dev.getting-started 2 | (:require [clj-http.client :as http])) 3 | 4 | ;; Welcome to the REPL for: 5 | ;; Getting Started with the Polylith Real World example! 6 | ;; 7 | ;; Please have some little patience while the wheels spin up. You'll 8 | ;; know things are ready when you see timestamped log messages in 9 | ;; the repl/output pane that will split open to the right of this file -> 10 | ;; (We often refer to this as “The REPL/Output Window”.) 11 | ;; 12 | ;; If you are unfamiliar with Calva or Clojure, please open the file 13 | ;; development/src/dev/hello_repl.clj 14 | ;; To confirm that you know Calva well enough to continue this session, 15 | ;; 1. Load this file in the REPL 16 | ;; You should see a greeting printed in the output window 17 | ;; 2. Evaluate this string below 18 | ;; You should see the result (the string itself) appear both 19 | ;; inline in this pane and printed to the output window 20 | 21 | "Hello Polylith Real World!" 22 | 23 | ;; Worked? Great! Let's continue. 24 | ;; Didn't work as described? Please don't hesitate to file an issue. 25 | ;; 26 | ;; The guide is designed to run in Rich Comment Forms like the one 27 | ;; below. You are supposed to read instructions and evaluate the 28 | ;; top level forms in the RCF. If there is no instruction to evaluate 29 | ;; a top level form, consider it an implied instruction to do so. 30 | ;; Please don't hesitate to experiment with the code! 31 | 32 | (comment 33 | ;; The server is running, we should be able to grab some articles 34 | 35 | ;; This is the API base URL 36 | (def base-url "http://localhost:6003/api") 37 | 38 | ;; Fetch some articles 39 | (def articles (http/get (str base-url "/articles") {:as :auto})) 40 | 41 | ;; Look at them 42 | (:body articles) 43 | 44 | ;; Oh, no articles? We need to add one! 45 | 46 | ;; First we need to create a user, this is the API payload. 47 | (def register-payload {:user {:username "Eager Polylith Explorer" 48 | :email "polylith-is-cool@example.com" 49 | :password "battery-staple-horse"}}) 50 | 51 | ;; Creating the user 52 | ;; We hold on to the token so that we can create articles later 53 | (def jwt-token (let [register-response (http/post (str base-url "/users") 54 | {:form-params register-payload 55 | :content-type :json 56 | :as :auto})] 57 | (->> register-response :body :user :token))) 58 | ;; Consider evaluating `jwt-token` above to “save” the token in the 59 | ;; repl/output window. 60 | 61 | ;; Now our first article 62 | (def article-payload {:article {:title "Polylith Real World example article" 63 | :description "My first article. And it is about Polylith of course!" 64 | :body "Yada, yada, yada. Too lazy right now" 65 | :tagList ["polylith" "clojure" "realworld"]}}) 66 | 67 | (def article-request-headers {"Authorization" (str "Token " jwt-token)}) 68 | 69 | ;; Happy with the copy? Let's post it! 70 | (http/post (str base-url "/articles") 71 | {:form-params article-payload 72 | :headers article-request-headers 73 | :content-type :json}) 74 | 75 | ;; We define the request so that we can examine it later 76 | (def article-request *1) 77 | 78 | 79 | (def articles-again (http/get (str base-url "/articles") {:as :auto})) 80 | (:body articles-again) 81 | (->> articles-again :body :articlesCount) 82 | 83 | :rcf) 84 | 85 | ;; ======== The poly tool ======== 86 | ;; We have started the tool in a terminal, open the terminal pane 87 | ;; and you should find a terminal named “poly shell” 88 | ;; Commands you can try from the poly shell: 89 | ;; * `check` - checks the integrity of the project 90 | ;; * `help` 91 | ;; * `info` 92 | ;; * `help info` 93 | ;; * `test :all` 94 | ;; * `help create` 95 | ;; 96 | ;; NB: Don't prepend your poly tool commands with `poly`, that's 97 | ;; for when you run the `poly` command from the operating system 98 | ;; shell (bash etcetera). 99 | ;; Learn about the poly tool here: https://polylith.gitbook.io/poly/ 100 | 101 | (comment 102 | 103 | ;; TODO: @furkan3ayraktar and @tengstrand and @Misophistful, please fill in the blank space! 104 | 105 | :rcf) 106 | 107 | 108 | "Hello dear Polylith Real World explorer! You loaded this file in the REPL." -------------------------------------------------------------------------------- /components/database/src/clojure/realworld/database/schema.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.database.schema 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.realworld.log.interface :as log] 4 | [honey.sql :as sql])) 5 | 6 | (def user 7 | (jdbc/create-table-ddl :user 8 | [[:id :integer :primary :key :autoincrement] 9 | [:email :text :unique] 10 | [:username :text :unique] 11 | [:password :text] 12 | [:image :text] 13 | [:bio :text]] 14 | {:entities identity})) 15 | 16 | (def user-follows 17 | (jdbc/create-table-ddl :userFollows 18 | [[:userId :integer "references user(id)"] 19 | [:followedUserId :integer "references user(id)"]] 20 | {:entities identity})) 21 | 22 | (def article 23 | (jdbc/create-table-ddl :article 24 | [[:id :integer :primary :key :autoincrement] 25 | [:slug :text :unique] 26 | [:title :text] 27 | [:description :text] 28 | [:body :text] 29 | [:createdAt :datetime] 30 | [:updatedAt :datetime] 31 | [:userId :integer "references user(id)"]] 32 | {:entities identity})) 33 | 34 | (def tag 35 | (jdbc/create-table-ddl :tag 36 | [[:id :integer :primary :key :autoincrement] 37 | [:name :text :unique]] 38 | {:entities identity})) 39 | 40 | (def article-tags 41 | (jdbc/create-table-ddl :articleTags 42 | [[:articleId :integer "references article(id)"] 43 | [:tagId :integer "references tag(id)"]] 44 | {:entities identity})) 45 | 46 | (def favorite-articles 47 | (jdbc/create-table-ddl :favoriteArticles 48 | [[:articleId :integer "references article(id)"] 49 | [:userId :integer "references user(id)"]] 50 | {:entities identity})) 51 | 52 | (def comment-table 53 | (jdbc/create-table-ddl :comment 54 | [[:id :integer :primary :key :autoincrement] 55 | [:body :text] 56 | [:articleId :integer "references article(id)"] 57 | [:userId :integer "references user(id)"] 58 | [:createdAt :datetime] 59 | [:updatedAt :datetime]] 60 | {:entities identity})) 61 | 62 | (defn generate-db [db] 63 | (jdbc/db-do-commands db 64 | [user 65 | user-follows 66 | article 67 | tag 68 | article-tags 69 | favorite-articles 70 | comment-table])) 71 | 72 | (defn drop-db [db] 73 | (jdbc/db-do-commands db 74 | [(jdbc/drop-table-ddl :user) 75 | (jdbc/drop-table-ddl :userFollows) 76 | (jdbc/drop-table-ddl :article) 77 | (jdbc/drop-table-ddl :tag) 78 | (jdbc/drop-table-ddl :articleTags) 79 | (jdbc/drop-table-ddl :favoriteArticles) 80 | (jdbc/drop-table-ddl :comment)])) 81 | 82 | (defn table->schema-item [{:keys [tbl_name sql]}] 83 | [(keyword tbl_name) sql]) 84 | 85 | (defn valid-schema? [db] 86 | (let [query {:select [:*] 87 | :from [:sqlite_master] 88 | :where [:= :type "table"]} 89 | tables (jdbc/query db (sql/format query) {:identifiers identity}) 90 | current-schema (select-keys (into {} (map table->schema-item tables)) 91 | [:user :userFollows :article :tag :articleTags :favoriteArticles :comment]) 92 | valid-schema {:user user 93 | :userFollows user-follows 94 | :article article 95 | :tag tag 96 | :articleTags article-tags 97 | :favoriteArticles favorite-articles 98 | :comment comment-table}] 99 | (if (= valid-schema current-schema) 100 | true 101 | (do 102 | (log/warn "Current schema is invalid. Please correct it and restart the server.") 103 | false)))) 104 | 105 | -------------------------------------------------------------------------------- /components/comment/test/clojure/realworld/comment/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.comment.core-test 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.realworld.comment.core :as core] 4 | [clojure.realworld.comment.spec :as spec] 5 | [clojure.realworld.database.interface :as database] 6 | [clojure.realworld.user.interface.spec :as user-spec] 7 | [clojure.spec.alpha :as s] 8 | [clojure.spec.gen.alpha :as gen] 9 | [clojure.test :refer [deftest is use-fixtures]])) 10 | 11 | (defn test-db 12 | ([] {:classname "org.sqlite.JDBC" 13 | :subprotocol "sqlite" 14 | :subname "test.db"}) 15 | ([_] (test-db))) 16 | 17 | (def ^:private auth-user 18 | (assoc (gen/generate (s/gen user-spec/user)) :id 1)) 19 | 20 | (defn prepare-for-tests [f] 21 | (with-redefs [database/db test-db] 22 | (let [db (test-db)] 23 | (database/generate-db db) 24 | (jdbc/insert! db :user auth-user) 25 | (f) 26 | (database/drop-db db)))) 27 | 28 | (use-fixtures :each prepare-for-tests) 29 | 30 | (deftest article-comments--article-not-found--return-negative-response 31 | (let [[ok? res] (core/article-comments auth-user "slug")] 32 | (is (false? ok?)) 33 | (is (= {:errors {:slug ["Cannot find an article with given slug."]}} res)))) 34 | 35 | (deftest article-comments--no-comments-found--return-positive-response-with-empty-vector 36 | (let [_ (jdbc/insert! (test-db) :article {:slug "slug"}) 37 | [ok? res] (core/article-comments auth-user "slug")] 38 | (is (true? ok?)) 39 | (is (= {:comments []} res)))) 40 | 41 | (deftest article-comments--comments-found--return-positive-response 42 | (let [_ (jdbc/insert! (test-db) :article {:slug "slug"}) 43 | _ (jdbc/insert-multi! (test-db) :user (map-indexed #(assoc %2 :id (+ 2 %1)) (gen/sample (s/gen user-spec/user) 2))) 44 | _ (jdbc/insert-multi! (test-db) :comment [{:body "body1" :articleId 1 :userId 1} 45 | {:body "body2" :articleId 1 :userId 2} 46 | {:body "body3" :articleId 1 :userId 2} 47 | {:body "body4" :articleId 1 :userId 3}]) 48 | [ok? res] (core/article-comments auth-user "slug")] 49 | (is (true? ok?)) 50 | (is (= 4 (-> res :comments count))))) 51 | 52 | (deftest article-comments--comments-found-without-auth--return-positive-response 53 | (let [_ (jdbc/insert! (test-db) :article {:slug "slug"}) 54 | _ (jdbc/insert-multi! (test-db) :user (map-indexed #(assoc %2 :id (+ 2 %1)) (gen/sample (s/gen user-spec/user) 2))) 55 | _ (jdbc/insert-multi! (test-db) :comment [{:body "body1" :articleId 1 :userId 1} 56 | {:body "body2" :articleId 1 :userId 2} 57 | {:body "body3" :articleId 1 :userId 2} 58 | {:body "body4" :articleId 1 :userId 3}]) 59 | [ok? res] (core/article-comments nil "slug")] 60 | (is (true? ok?)) 61 | (is (= 4 (-> res :comments count))))) 62 | 63 | (deftest add-comment!--test 64 | (let [_ (jdbc/insert! (test-db) :article {:slug "slug"}) 65 | inputs (gen/sample (s/gen spec/add-comment) 20) 66 | results (map #(core/add-comment! auth-user "slug" %) inputs)] 67 | (is (every? true? (map first results))) 68 | (is (every? #(s/valid? spec/visible-comment (second %)) results)))) 69 | 70 | (deftest delete-comment!--comment-not-found--return-negative-response 71 | (let [[ok? res] (core/delete-comment! auth-user 1)] 72 | (is (false? ok?)) 73 | (is (= {:errors {:id ["Cannot find a comment with given id."]}} res)))) 74 | 75 | (deftest delete-comment!--comment-is-not-owned-by-user--return-negative-response 76 | (let [_ (jdbc/insert! (test-db) :article {:slug "slug"}) 77 | initial (gen/generate (s/gen spec/add-comment)) 78 | [_ comment] (core/add-comment! auth-user "slug" initial) 79 | [ok? res] (core/delete-comment! (assoc auth-user :id 2) 80 | (-> comment :comment :id))] 81 | (is (false? ok?)) 82 | (is (= {:errors {:authorization ["You need to be author of this comment to delete it."]}} res)))) 83 | 84 | (deftest delete-comment!--input-is-ok--delete-comment-and-return-positive-response 85 | (let [_ (jdbc/insert! (test-db) :article {:slug "slug"}) 86 | initial-inputs (gen/sample (s/gen spec/add-comment) 20) 87 | create-res (map #(core/add-comment! auth-user "slug" %) initial-inputs) 88 | update-res (map #(core/delete-comment! auth-user (-> % second :comment :id)) create-res)] 89 | (is (every? #(= [true nil] %) update-res)))) 90 | -------------------------------------------------------------------------------- /components/profile/test/clojure/realworld/profile/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.profile.core-test 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.realworld.database.interface :as database] 4 | [clojure.realworld.profile.core :as core] 5 | [clojure.realworld.user.interface.spec :as user-spec] 6 | [clojure.spec.gen.alpha :as gen] 7 | [clojure.spec.alpha :as s] 8 | [clojure.test :refer [deftest is use-fixtures]])) 9 | 10 | (defn- test-db 11 | ([] {:classname "org.sqlite.JDBC" 12 | :subprotocol "sqlite" 13 | :subname "test.db"}) 14 | ([_] (test-db))) 15 | 16 | (def ^:private auth-user 17 | (assoc (gen/generate (s/gen user-spec/user)) :id 1)) 18 | 19 | (defn prepare-for-tests [f] 20 | (with-redefs [database/db test-db] 21 | (let [db (test-db)] 22 | (database/generate-db db) 23 | (jdbc/insert! db :user auth-user) 24 | (f) 25 | (database/drop-db db)))) 26 | 27 | (use-fixtures :each prepare-for-tests) 28 | 29 | (deftest fetch-profile--profile-not-found--return-negative-result 30 | (let [[ok? res] (core/fetch-profile auth-user "username")] 31 | (is (false? ok?)) 32 | (is (= {:errors {:username ["Cannot find a profile with given username."]}} res)))) 33 | 34 | (deftest fetch-profile--not-logged-in--return-positive-result-with-false-following 35 | (let [_ (jdbc/insert! (database/db) :user {:username "username" 36 | :bio "bio" 37 | :image "image"}) 38 | [ok? res] (core/fetch-profile nil "username")] 39 | (is (true? ok?)) 40 | (is (= {:profile {:username "username" 41 | :bio "bio" 42 | :image "image" 43 | :following false}} 44 | res)))) 45 | 46 | (deftest fetch-profile--logged-in-not-following--return-positive-result-with-false-following 47 | (let [_ (jdbc/insert! (database/db) :user {:username "username" 48 | :bio "bio" 49 | :image "image"}) 50 | [ok? res] (core/fetch-profile auth-user "username")] 51 | (is (true? ok?)) 52 | (is (= {:profile {:username "username" 53 | :bio "bio" 54 | :image "image" 55 | :following false}} 56 | res)))) 57 | 58 | (deftest fetch-profile--logged-in-following--return-positive-result-with-true-following 59 | (let [_ (jdbc/insert! (database/db) :user {:username "username" 60 | :bio "bio" 61 | :image "image"}) 62 | _ (jdbc/insert! (database/db) :userFollows {:userId 1 :followedUserId 2}) 63 | [ok? res] (core/fetch-profile auth-user "username")] 64 | (is (true? ok?)) 65 | (is (= {:profile {:username "username" 66 | :bio "bio" 67 | :image "image" 68 | :following true}} 69 | res)))) 70 | 71 | (deftest follow!--profile-not-found--return-negative-result 72 | (let [[ok? res] (core/follow! auth-user "username")] 73 | (is (false? ok?)) 74 | (is (= {:errors {:username ["Cannot find a profile with given username."]}} res)))) 75 | 76 | (deftest follow!--profile-found--return-positive-result 77 | (let [_ (jdbc/insert! (database/db) :user {:username "username" 78 | :bio "bio" 79 | :image "image"}) 80 | [ok? res] (core/follow! auth-user "username")] 81 | (is (true? ok?)) 82 | (is (= {:profile {:username "username" 83 | :bio "bio" 84 | :image "image" 85 | :following true}} 86 | res)))) 87 | 88 | (deftest unfollow!--profile-not-found--return-negative-result 89 | (let [[ok? res] (core/unfollow! auth-user "username")] 90 | (is (false? ok?)) 91 | (is (= {:errors {:username ["Cannot find a profile with given username."]}} res)))) 92 | 93 | (deftest unfollow!--logged-in-and-profile-found--return-positive-result 94 | (let [_ (jdbc/insert! (database/db) :user {:username "username" 95 | :bio "bio" 96 | :image "image"}) 97 | _ (jdbc/insert! (database/db) :userFollows {:userId 1 :followedUserId 2}) 98 | [ok? res] (core/unfollow! auth-user "username")] 99 | (is (true? ok?)) 100 | (is (= {:profile {:username "username" 101 | :bio "bio" 102 | :image "image" 103 | :following false}} 104 | res)))) 105 | -------------------------------------------------------------------------------- /components/article/src/clojure/realworld/article/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.article.core 2 | (:require [clj-time.coerce :as c] 3 | [clj-time.core :as t] 4 | [clojure.realworld.article.store :as store] 5 | [clojure.realworld.profile.interface :as profile] 6 | [slugger.core :as slugger])) 7 | 8 | (defn article->visible-article [article auth-user] 9 | (let [user-id (:userId article) 10 | article-id (:id article) 11 | [_ author] (profile/fetch-profile auth-user user-id) 12 | favorited? (if auth-user (store/favorited? (:id auth-user) article-id) false) 13 | favorites-count (store/favorites-count article-id) 14 | tags (store/article-tags article-id)] 15 | (assoc (dissoc article :userId) 16 | :favorited favorited? 17 | :favoritesCount favorites-count 18 | :author (:profile author) 19 | :tagList tags))) 20 | 21 | (defn- create-slug [title now] 22 | (when title 23 | (let [slug (slugger/->slug title)] 24 | (if (store/find-by-slug slug) 25 | (str slug "-" (c/to-long now)) 26 | slug)))) 27 | 28 | (defn article [auth-user slug] 29 | (if-let [article (store/find-by-slug slug)] 30 | [true {:article (article->visible-article article auth-user)}] 31 | [false {:errors {:slug ["Cannot find an article with given slug."]}}])) 32 | 33 | (defn create-article! [auth-user {:keys [title description body tagList]}] 34 | (let [user-id (:id auth-user) 35 | now (t/now) 36 | slug (create-slug title now) 37 | article-input {:slug slug 38 | :title title 39 | :description description 40 | :body body 41 | :createdAt now 42 | :updatedAt now 43 | :userId user-id} 44 | _ (store/insert-article! article-input)] 45 | (if-let [article (store/find-by-slug slug)] 46 | (let [article-id (:id article) 47 | _ (store/update-tags! tagList) 48 | _ (store/add-tags-to-article! article-id tagList)] 49 | [true {:article (article->visible-article article auth-user)}]) 50 | [false {:errors {:other ["Cannot insert article into db."]}}]))) 51 | 52 | (defn update-article! [auth-user slug {:keys [title description body]}] 53 | (if-let [article (store/find-by-slug slug)] 54 | (if (= (:id auth-user) (:userId article)) 55 | (let [now (t/now) 56 | slug (create-slug title now) 57 | article-input (into {} (filter #(-> % val nil? not) 58 | {:slug slug 59 | :title title 60 | :description description 61 | :body body 62 | :updatedAt now})) 63 | _ (store/update-article! (:id article) article-input)] 64 | (if-let [updated-article (store/find-by-slug (if slug slug (:slug article)))] 65 | [true {:article (article->visible-article updated-article auth-user)}] 66 | [false {:errors {:other ["Cannot insert article into db."]}}])) 67 | [false {:errors {:authorization ["You need to be author of this article to update it."]}}]) 68 | [false {:errors {:slug ["Cannot find an article with given slug."]}}])) 69 | 70 | (defn delete-article! [auth-user slug] 71 | (if-let [article (store/find-by-slug slug)] 72 | (if (= (:id auth-user) (:userId article)) 73 | [true (store/delete-article! (:id article))] 74 | [false {:errors {:authorization ["You need to be author of this article to delete it."]}}]) 75 | [false {:errors {:slug ["Cannot find an article with given slug."]}}])) 76 | 77 | (defn favorite-article! [auth-user slug] 78 | (if-let [article (store/find-by-slug slug)] 79 | (do 80 | (store/favorite! (:id auth-user) (:id article)) 81 | [true {:article (article->visible-article article auth-user)}]) 82 | [false {:errors {:slug ["Cannot find an article with given slug."]}}])) 83 | 84 | (defn unfavorite-article! [auth-user slug] 85 | (if-let [article (store/find-by-slug slug)] 86 | (do 87 | (store/unfavorite! (:id auth-user) (:id article)) 88 | [true {:article (article->visible-article article auth-user)}]) 89 | [false {:errors {:slug ["Cannot find an article with given slug."]}}])) 90 | 91 | (defn feed [auth-user limit offset] 92 | (let [limit (or limit 20) 93 | offset (or offset 0) 94 | user-id (:id auth-user) 95 | articles (store/feed user-id limit offset) 96 | visible-articles (mapv #(article->visible-article % auth-user) articles) 97 | res {:articles visible-articles 98 | :articlesCount (count visible-articles)}] 99 | [true res])) 100 | 101 | (defn articles [auth-user limit offset author tag favorited] 102 | (let [limit (or limit 20) 103 | offset (or offset 0) 104 | articles (store/articles limit offset author tag favorited) 105 | visible-articles (mapv #(article->visible-article % auth-user) articles) 106 | res {:articles visible-articles 107 | :articlesCount (count visible-articles)}] 108 | [true res])) 109 | -------------------------------------------------------------------------------- /components/user/test/clojure/realworld/user/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.user.core-test 2 | (:require [clj-time.core :as t] 3 | [clojure.java.jdbc :as jdbc] 4 | [clojure.realworld.database.interface :as database] 5 | [clojure.realworld.user.core :as core] 6 | [clojure.realworld.user.spec :as spec] 7 | [clojure.spec.alpha :as s] 8 | [clojure.spec.gen.alpha :as gen] 9 | [clojure.test :refer :all])) 10 | 11 | (defn- test-db 12 | ([] {:classname "org.sqlite.JDBC" 13 | :subprotocol "sqlite" 14 | :subname "test.db"}) 15 | ([_] (test-db))) 16 | 17 | (defn prepare-for-tests [f] 18 | (with-redefs [database/db test-db] 19 | (let [db (test-db)] 20 | (database/generate-db db) 21 | (f) 22 | (database/drop-db db)))) 23 | 24 | (use-fixtures :each prepare-for-tests) 25 | 26 | (deftest login--user-not-found--return-negative-result 27 | (let [[ok? res] (core/login! {:email "test@test.com" :password "password"})] 28 | (is (false? ok?)) 29 | (is (= {:errors {:email ["Invalid email."]}} res)))) 30 | 31 | (deftest login--invalid-password--return-negative-result 32 | (let [_ (jdbc/insert! (test-db) :user {:email "test@test.com" 33 | :password (core/encrypt-password "password")}) 34 | [ok? res] (core/login! {:email "test@test.com" :password "invalid-password"})] 35 | (is (false? ok?)) 36 | (is (= {:errors {:password ["Invalid password."]}} res)))) 37 | 38 | (deftest login--valid-input--return-positive-result 39 | (let [_ (jdbc/insert! (test-db) :user {:email "test@test.com" 40 | :username "username" 41 | :password (core/encrypt-password "password")}) 42 | [ok? res] (core/login! {:email "test@test.com" :password "password"})] 43 | (is (true? ok?)) 44 | (is (true? (s/valid? spec/visible-user res))))) 45 | 46 | (deftest register!--user-exists-with-given-email--return-negative-result 47 | (let [_ (jdbc/insert! (test-db) :user {:email "test@test.com"}) 48 | [ok? res] (core/register! {:email "test@test.com"})] 49 | (is (false? ok?)) 50 | (is (= {:errors {:email ["A user exists with given email."]}} res)))) 51 | 52 | (deftest register!--user-exists-with-given-username--return-negative-result 53 | (let [_ (jdbc/insert! (test-db) :user {:username "username"}) 54 | [ok? res] (core/register! {:email "test@test.com" :username "username"})] 55 | (is (false? ok?)) 56 | (is (= {:errors {:username ["A user exists with given username."]}} res)))) 57 | 58 | (deftest register!--valid-input--return-positive-result 59 | (let [input (gen/generate (s/gen spec/register)) 60 | [ok? res] (core/register! input)] 61 | (is (true? ok?)) 62 | (is (s/valid? spec/visible-user res)) 63 | (is (not (nil? (-> res :user :token)))))) 64 | 65 | (deftest user-by-token--user-not-found--return-negative-result 66 | (let [[ok? res] (core/user-by-token "token")] 67 | (is (false? ok?)) 68 | (is (= {:errors {:token ["Cannot find a user with associated token."]}} res)))) 69 | 70 | (deftest user-by-token--user-found--return-positive-result 71 | (let [email "test@test.com" 72 | username "username" 73 | token (#'clojure.realworld.user.core/generate-token email username) 74 | _ (jdbc/insert! (test-db) :user {:email email :username username}) 75 | [ok? res] (core/user-by-token token)] 76 | (is (true? ok?)) 77 | (is (s/valid? spec/visible-user res)))) 78 | 79 | (deftest user-by-token--expired-token--return-negative-result 80 | (let [email "test@test.com" 81 | username "username" 82 | now (t/now) 83 | ten-days-ago (t/minus now (t/days 10)) 84 | token (with-redefs [clj-time.core/now (fn [] ten-days-ago)] 85 | (#'clojure.realworld.user.core/generate-token email username)) 86 | _ (jdbc/insert! (test-db) :user {:email email :username username}) 87 | [ok? res] (core/user-by-token token)] 88 | (is (false? ok?)) 89 | (is (= {:errors {:token ["Cannot find a user with associated token."]}} res)))) 90 | 91 | (deftest update-user!--user-exists-with-given-email--return-negative-result 92 | (let [_ (jdbc/insert! (test-db) :user {:email "test1@test.com"}) 93 | auth-user (jdbc/insert! (test-db) :user {:email "test2@test.com"}) 94 | [ok? res] (core/update-user! auth-user {:email "test1@test.com"})] 95 | (is (false? ok?)) 96 | (is (= {:errors {:email ["A user exists with given email."]}} res)))) 97 | 98 | (deftest update-user!--user-exists-with-given-username--return-negative-result 99 | (let [_ (jdbc/insert! (test-db) :user {:username "username"}) 100 | auth-user (jdbc/insert! (test-db) :user {:email "test2@test.com"}) 101 | [ok? res] (core/update-user! auth-user {:email "test@test.com" :username "username"})] 102 | (is (false? ok?)) 103 | (is (= {:errors {:username ["A user exists with given username."]}} res)))) 104 | 105 | (deftest update-user!--valid-input--return-positive-result 106 | (let [initial-inputs (gen/sample (s/gen spec/register) 20) 107 | users (map #(-> (core/register! %) second :user) initial-inputs) 108 | inputs (gen/sample (s/gen spec/update-user) 20) 109 | results (map-indexed #(core/update-user! (nth users %1) %2) inputs)] 110 | (is (every? true? (map first results))) 111 | (is (every? #(s/valid? spec/visible-user (second %)) results)) 112 | (is (= (map #(dissoc % :password) inputs) 113 | (map-indexed #(select-keys (-> %2 second :user) 114 | (keys (nth inputs %1))) 115 | results))))) 116 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | 3 | jobs: 4 | check: 5 | docker: 6 | - image: cimg/clojure:1.11.1 7 | working_directory: ~/realworld-example 8 | steps: 9 | - checkout 10 | - run: 11 | name: Get rid of erroneous git config 12 | command: | 13 | rm -rf ~/.gitconfig 14 | - restore_cache: 15 | keys: 16 | - realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }} 17 | - realworld-example- 18 | - run: 19 | name: Create polylith config if it does not exist 20 | command: mkdir -p ~/.polylith && echo "{}" > ~/.polylith/config.edn 21 | - run: 22 | name: Force clojure to resolve dependencies 23 | command: clojure -A:dev:test -P 24 | - run: 25 | name: Check Polylith workspace 26 | command: clojure -M:poly check 27 | - save_cache: 28 | key: realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }} 29 | paths: 30 | - ~/.polylith 31 | - ~/.m2 32 | - ~/.gitlibs 33 | - ~/.clojure 34 | - ~/realworld-example/.cpcache 35 | - persist_to_workspace: 36 | root: . 37 | paths: 38 | - . 39 | 40 | info: 41 | docker: 42 | - image: cimg/clojure:1.11.1 43 | working_directory: ~/realworld-example 44 | steps: 45 | - attach_workspace: 46 | at: ~/realworld-example 47 | - restore_cache: 48 | keys: 49 | - realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }} 50 | - run: 51 | name: Add global git user email 52 | command: git config --global user.email "circleci@polyfy.com" 53 | - run: 54 | name: Add global git user name 55 | command: git config --global user.name "CircleCI" 56 | - run: 57 | name: Run ws command for Polylith workspace 58 | command: clojure -M:poly ws 59 | - run: 60 | name: Run info command for Polylith workspace 61 | command: clojure -M:poly info 62 | - run: 63 | name: Run deps command for Polylith workspace 64 | command: clojure -M:poly deps 65 | - run: 66 | name: Run libs command for Polylith workspace 67 | command: clojure -M:poly libs 68 | 69 | test: 70 | docker: 71 | - image: cimg/clojure:1.11.1 72 | working_directory: ~/realworld-example 73 | steps: 74 | - attach_workspace: 75 | at: ~/realworld-example 76 | - restore_cache: 77 | keys: 78 | - realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }} 79 | - run: 80 | name: Add global git user email 81 | command: git config --global user.email "circleci@polyfy.com" 82 | - run: 83 | name: Add global git user name 84 | command: git config --global user.name "CircleCI" 85 | - run: 86 | name: Run tests for Polylith workspace 87 | command: clojure -M:poly test :project 88 | 89 | api-test: 90 | docker: 91 | - image: cimg/clojure:1.11.1-node 92 | working_directory: ~/realworld-example 93 | steps: 94 | - attach_workspace: 95 | at: ~/realworld-example 96 | - restore_cache: 97 | keys: 98 | - realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }} 99 | - run: 100 | name: Run realworld backend 101 | command: clojure -M:ring 102 | working_directory: projects/realworld-backend 103 | background: true 104 | - run: 105 | name: Wait for backend to initialize 106 | command: wget --retry-connrefused --waitretry=10 --read-timeout=20 --timeout=15 -t 30 http://localhost:6003/api/health 107 | - run: 108 | name: Run api tests 109 | command: APIURL=http://localhost:6003/api ./run-api-tests.sh 110 | working_directory: ~/realworld-example/api-tests 111 | 112 | build-uberjar: 113 | docker: 114 | - image: cimg/clojure:1.11.1 115 | working_directory: ~/realworld-example 116 | steps: 117 | - attach_workspace: 118 | at: ~/realworld-example 119 | - restore_cache: 120 | keys: 121 | - realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }} 122 | - run: 123 | name: Build realworld-backed project 124 | command: clojure -T:build uberjar :project realworld-backend 125 | - run: 126 | name: Copy artifact to artifacts folder 127 | command: | 128 | mkdir -p artifacts 129 | 130 | cp projects/realworld-backend/target/realworld-backend.jar artifacts/. 131 | - store_artifacts: 132 | path: ./artifacts 133 | 134 | mark-as-stable: 135 | docker: 136 | - image: cimg/clojure:1.11.1 137 | working_directory: ~/realworld-example 138 | steps: 139 | - attach_workspace: 140 | at: ~/realworld-example 141 | - add_ssh_keys: 142 | fingerprints: 143 | - "3d:7a:60:40:26:8e:a8:4e:9c:22:fe:77:3f:84:fb:ec" 144 | - restore_cache: 145 | keys: 146 | - realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }} 147 | - run: 148 | name: Add github.com to known hosts 149 | command: mkdir -p ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts 150 | - run: 151 | name: Add global git user email 152 | command: git config --global user.email "circleci@polyfy.com" 153 | - run: 154 | name: Add global git user name 155 | command: git config --global user.name "CircleCI" 156 | - run: 157 | name: Add git tag to mark this stable point 158 | command: git tag -f -a "stable-$CIRCLE_BRANCH" -m "[skip ci] Added Stable Polylith tag" 159 | - run: 160 | name: Push the new tag 161 | command: git push origin $CIRCLE_BRANCH --tags --force 162 | 163 | workflows: 164 | version: 2 165 | validate-test-build: 166 | jobs: 167 | - check 168 | - info: 169 | requires: 170 | - check 171 | - test: 172 | requires: 173 | - check 174 | - api-test: 175 | requires: 176 | - test 177 | - build-uberjar: 178 | requires: 179 | - test 180 | - mark-as-stable: 181 | requires: 182 | - api-test 183 | - build-uberjar 184 | filters: 185 | branches: 186 | only: 187 | - master 188 | -------------------------------------------------------------------------------- /components/article/src/clojure/realworld/article/store.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.article.store 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.realworld.database.interface :as database] 4 | [clojure.string :as str] 5 | [honey.sql :as sql])) 6 | 7 | (defn find-by-slug [slug] 8 | (let [query {:select [:*] 9 | :from [:article] 10 | :where [:= :slug slug]} 11 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})] 12 | (first results))) 13 | 14 | (defn insert-article! [article-input] 15 | (jdbc/insert! (database/db) :article article-input {:entities identity})) 16 | 17 | (defn tags-with-names [tag-names] 18 | (let [query {:select [:*] 19 | :from [:tag] 20 | :where [:in :name tag-names]} 21 | results (jdbc/query (database/db) (sql/format query))] 22 | results)) 23 | 24 | (defn add-tags-to-article! [article-id tag-names] 25 | (when-not (empty? tag-names) 26 | (let [tags (tags-with-names tag-names) 27 | inputs (mapv #(hash-map :articleId article-id 28 | :tagId (:id %)) 29 | tags)] 30 | (jdbc/insert-multi! (database/db) :articleTags inputs)))) 31 | 32 | (defn article-tags [article-id] 33 | (let [query {:select [:name] 34 | :from [:articleTags] 35 | :join [:tag [:= :tagId :tag.id]] 36 | :where [:= :articleId article-id]} 37 | results (jdbc/query (database/db) (sql/format query))] 38 | (mapv :name results))) 39 | 40 | (defn insert-tag! [name] 41 | (let [tag (first (tags-with-names [name]))] 42 | (when-not tag 43 | (jdbc/insert! (database/db) :tag {:name name})))) 44 | 45 | (defn update-tags! [tag-names] 46 | (when-not (empty? tag-names) 47 | (doseq [name tag-names] 48 | (insert-tag! name)))) 49 | 50 | (defn favorited? [user-id article-id] 51 | (let [query {:select [:%count.*] 52 | :from [:favoriteArticles] 53 | :where [:and [:= :articleId article-id] 54 | [:= :userId user-id]]} 55 | result (jdbc/query (database/db) (sql/format query))] 56 | (< 0 (-> result first first val)))) 57 | 58 | (defn favorites-count [article-id] 59 | (let [query {:select [:%count.*] 60 | :from [:favoriteArticles] 61 | :where [:= :articleId article-id]} 62 | result (jdbc/query (database/db) (sql/format query))] 63 | (-> result first first val))) 64 | 65 | (defn update-article! [id article-input] 66 | (let [query {:update :article 67 | :set article-input 68 | :where [:= :id id]}] 69 | (jdbc/execute! (database/db) (sql/format query)))) 70 | 71 | (defn delete-article! [id] 72 | (let [query-1 {:delete-from :articleTags 73 | :where [:= :articleId id]} 74 | query-2 {:delete-from :favoriteArticles 75 | :where [:= :articleId id]} 76 | query-3 {:delete-from :article 77 | :where [:= :id id]}] 78 | (jdbc/with-db-transaction [trans-conn (database/db)] 79 | (jdbc/execute! trans-conn (sql/format query-1)) 80 | (jdbc/execute! trans-conn (sql/format query-2)) 81 | (jdbc/execute! trans-conn (sql/format query-3))) 82 | nil)) 83 | 84 | (defn favorite! [user-id article-id] 85 | (when-not (favorited? user-id article-id) 86 | (jdbc/insert! (database/db) :favoriteArticles {:articleId article-id 87 | :userId user-id}))) 88 | 89 | (defn unfavorite! [user-id article-id] 90 | (when (favorited? user-id article-id) 91 | (let [query {:delete-from :favoriteArticles 92 | :where [:and [:= :articleId article-id] 93 | [:= :userId user-id]]}] 94 | (jdbc/execute! (database/db) (sql/format query))))) 95 | 96 | (defn feed [user-id limit offset] 97 | (let [query {:select [:a.*] 98 | :from [[:article :a]] 99 | :join [[:userFollows :u] [:= :a.userId :u.followedUserId]] 100 | :where [:= :u.userId user-id] 101 | :order-by [[:a.updatedAt :desc] [:a.id :desc]] 102 | :limit limit 103 | :offset offset} 104 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})] 105 | results)) 106 | 107 | (defn articles-by-tag [limit offset tag] 108 | (let [query {:select [:a.*] 109 | :from [[:article :a]] 110 | :join [[:articleTags :at] [:= :a.id :at.articleId] 111 | [:tag :t] [:= :at.tagId :t.id]] 112 | :where [:= :t.name tag] 113 | :order-by [[:updatedAt :desc] [:id :desc]] 114 | :limit limit 115 | :offset offset} 116 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})] 117 | results)) 118 | 119 | (defn articles-by-author [limit offset author] 120 | (let [query {:select [:a.*] 121 | :from [[:article :a]] 122 | :join [[:user :u] [:= :a.userId :u.id]] 123 | :where [:= :u.username author] 124 | :order-by [[:updatedAt :desc] [:id :desc]] 125 | :limit limit 126 | :offset offset} 127 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})] 128 | results)) 129 | 130 | (defn articles-by-favorited [limit offset favorited] 131 | (let [query {:select [:a.*] 132 | :from [[:article :a]] 133 | :join [[:favoriteArticles :fa] [:= :a.id :fa.articleId] 134 | [:user :u] [:= :fa.userId :u.id]] 135 | :where [:= :u.username favorited] 136 | :order-by [[:updatedAt :desc] [:id :desc]] 137 | :limit limit 138 | :offset offset} 139 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})] 140 | results)) 141 | 142 | (defn all-articles [limit offset] 143 | (let [query {:select [:*] 144 | :from [:article] 145 | :order-by [[:updatedAt :desc] [:id :desc]] 146 | :limit limit 147 | :offset offset} 148 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})] 149 | results)) 150 | 151 | (defn articles [limit offset author tag favorited] 152 | (if-not (str/blank? author) 153 | (articles-by-author limit offset author) 154 | (if-not (str/blank? tag) 155 | (articles-by-tag limit offset tag) 156 | (if-not (str/blank? favorited) 157 | (articles-by-favorited limit offset favorited) 158 | (all-articles limit offset))))) 159 | -------------------------------------------------------------------------------- /how-to.md: -------------------------------------------------------------------------------- 1 | ## How to create this workspace from scratch 2 | Workspace structure follows the Polylith Architecture ideas. The Polylith tool makes it easy to create a workspace, add components, and test and validate the workspace. Here, you can find all the steps required to create this workspace from scratch. 3 | 4 | ###### Install Clojure 5 | If you do not have Clojure installed on your machine already, you can install it through [Homebrew](https://brew.sh/) on MacOS by running: 6 | 7 | ```sh 8 | brew install clojure/tools/clojure 9 | ``` 10 | 11 | Refer to [this page on clojure.org](https://clojure.org/guides/getting_started) for more options on installing. 12 | 13 | ###### Install Polylith Tool 14 | Polylith provides a command line tool that you can install on your machine to enhance your development experience. You can install it through Homebrew on MacOS by running: 15 | 16 | ```sh 17 | brew install polyfy/polylith/poly 18 | ``` 19 | 20 | For more installation options, please have a look at [Polylith tool documentation](https://polylith.gitbook.io/poly/install/install). 21 | 22 | ###### Create a workspace 23 | - `` poly create workspace name:realworld-app top-ns:clojure.realworld `` 24 | - This will create your workspace under a directory named `` realworld-app `` 25 | - Your code will end up in a package named `` clojure.realworld `` 26 | - Inside your workspace, you'll find the structure as in the image below. 27 | - If you look inside these directories, you'll see that bases, components, and projects are empty. 28 | 29 | ![workspace](.media/how-to/01_workspace.png) 30 | 31 | Once the workspace is created, navigate to the workspace directory: `` cd realworld-backend `` 32 | 33 | ###### Open workspace in IDE 34 | You can open the workspace with your favorite IDE. It will look like the following at this stage if you open it with [Intellij IDEA](https://www.jetbrains.com/idea/) with [Cursive](https://cursive-ide.com) plugin: 35 | 36 | ![dev-project](.media/how-to/02_dev_project.png) 37 | 38 | ###### Create components 39 | - `` poly create component name:article `` 40 | - `` poly create component name:comment `` 41 | - `` poly create component name:database `` 42 | - `` poly create component name:env `` 43 | - `` poly create component name:log `` 44 | - `` poly create component name:profile `` 45 | - `` poly create component name:spec `` 46 | - `` poly create component name:tag `` 47 | - `` poly create component name:user `` 48 | 49 | These command above will create components under `` components `` directory. 50 | 51 | ![components](.media/how-to/03_components.png) 52 | 53 | However, our components are not yet added to development project's `` deps.edn ``. In order to start working with them, you need to add them to `` deps.edn `` in the root directory as following: 54 | 55 | ![components-added](.media/how-to/04_components_added_to_development_project.png) 56 | 57 | notice that we added the components both under the `:dev` and `:test` alias. Once you do this and load dev and test aliases, you can start working with your components. 58 | 59 | ###### Create base 60 | - `` poly create base name:rest-api `` 61 | 62 | This command will create a base named `` rest-api `` under bases directory. 63 | Same as components, you should add the base to the `deps.edn` file in the workspace root. It will look like this: 64 | 65 | ![base](.media/how-to/05_base.png) 66 | 67 | ###### Add code to components and the base 68 | You can take the code from the [repository](https://github.com/furkan3ayraktar/clojure-polylith-realworld-example-app) to populate the components and the base. 69 | You should also add the necessary dependencies to the `` deps.edn `` file in the workspace root. 70 | 71 | Once your code is ready, you can move on to the next step to create a project. 72 | 73 | ###### Create a project 74 | - `` poly create project name:realworld-backend `` 75 | 76 | This command will create a new directory under `` projects/realworld-backend ``. If you look into that directory, you will see that there is only a single file, named `` deps.edn `` and it will look like this: 77 | 78 | ![empty-project](.media/how-to/06_empty_project.png) 79 | 80 | This is where you will place the configuration for your project. In Polylith, a project is a configuration which includes a single base, a set of components and library dependencies. Since our project is a very simple one with one single artifact, we'll include our only base and all of our components in this configuration. After adding those, it will look like this: 81 | 82 | ![filled-project](.media/how-to/07_filled_project.png) 83 | 84 | As you can notice, we also have some extra configuration necessarry for our specific project, such as, ring configuration and two special aliases (`` :aot `` and `` :uberjar ``) for creating aot compiled uberjar artifact. 85 | 86 | At this stage, you should have a copy of this repository. 87 | 88 | ###### Workspace info 89 | ```sh 90 | poly info 91 | ``` 92 | 93 | This command will print out the information about the current workspace. You can find documentation about it in the [Polylith tool documentation](https://polylith.gitbook.io/poly/). It should print an output like this: 94 | 95 | 96 | 97 | Here the asterisk symbol points the changed components and bases since the last stable point. 98 | 99 | ###### Validating intergrity 100 | In order to validate the integratiy of the Polylith workspace, run the following command: 101 | 102 | ```sh 103 | poly check 104 | ``` 105 | 106 | This command should output `` OK `` as message if everything is okay. Otherwise, it will print out errors and warnings found in the workspace. 107 | 108 | ###### Running tests 109 | ```sh 110 | poly test 111 | ``` 112 | 113 | This command will run all the tests since the last stable point. Since this is a newly created workspace, last stable point will be since the beginning. 114 | 115 | ###### Adding a stable point 116 | Once you are ready with your changes and the check and test commands run without any issues, you can commit your changes to your git repository. After commiting, you can add a git tag with `` stable- `` prefix. This will tell Polylith to take that commit as the last stable point next time you run any Polylith commands. 117 | 118 | ```sh 119 | git tag -f -a "stable-master" -m "Stable point" 120 | ``` 121 | 122 | After adding tag, you can run info command again and get an output similar to this: 123 | 124 | ![workspace-info-after-commit](.media/how-to/09_workspace_info_after_commit.png) 125 | 126 | #### Sample REPL run configuration for Intellij IDEA with Cursive 127 | Polylith works out-of-the-box with Intellij IDEA + Cursive setup. Here is how my REPL run configuration looks like: 128 | 129 | ![repl-config](.media/how-to/10_repl_config.png) 130 | 131 | The only thing that is different from default Cursive REPL configuration is, I selected Run with Deps option and added two aliases (``dev,test``) that comes from the Polylith workspace `` deps.edn ``. 132 | -------------------------------------------------------------------------------- /bases/rest-api/src/clojure/realworld/rest_api/handler.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.rest-api.handler 2 | (:require [clojure.edn :as edn] 3 | [clojure.realworld.article.interface :as article] 4 | [clojure.realworld.article.interface.spec :as article-spec] 5 | [clojure.realworld.comment.interface :as comment-comp] 6 | [clojure.realworld.comment.interface.spec :as comment-spec] 7 | [clojure.realworld.spec.interface :as spec] 8 | [clojure.realworld.profile.interface :as profile] 9 | [clojure.realworld.tag.interface :as tag] 10 | [clojure.realworld.user.interface :as user] 11 | [clojure.realworld.user.interface.spec :as user-spec] 12 | [clojure.spec.alpha :as s] 13 | [clojure.realworld.env.interface :as env])) 14 | 15 | (defn- parse-query-param [param] 16 | (if (string? param) 17 | (try 18 | (edn/read-string param) 19 | (catch Exception _ 20 | param)) 21 | param)) 22 | 23 | (defn- handle 24 | ([status body] 25 | {:status (or status 404) 26 | :body body}) 27 | ([status] 28 | (handle status nil))) 29 | 30 | (defn options [_] 31 | (handle 200)) 32 | 33 | (defn health [_] 34 | (handle 200 {:environment (env/env :environment)})) 35 | 36 | (defn other [_] 37 | (handle 404 {:errors {:other ["Route not found."]}})) 38 | 39 | (defn login [req] 40 | (let [user (-> req :params :user)] 41 | (if (s/valid? user-spec/login user) 42 | (let [[ok? res] (user/login! user)] 43 | (handle (if ok? 200 404) res)) 44 | (handle 422 {:errors {:body ["Invalid request body."]}})))) 45 | 46 | (defn register [req] 47 | (let [user (-> req :params :user)] 48 | (if (s/valid? user-spec/register user) 49 | (let [[ok? res] (user/register! user)] 50 | (handle (if ok? 200 404) res)) 51 | (handle 422 {:errors {:body ["Invalid request body."]}})))) 52 | 53 | (defn current-user [req] 54 | (let [auth-user (-> req :auth-user)] 55 | (handle 200 {:user auth-user}))) 56 | 57 | (defn update-user [req] 58 | (let [auth-user (-> req :auth-user) 59 | user (-> req :params :user)] 60 | (if (s/valid? user-spec/update-user user) 61 | (let [[ok? res] (user/update-user! auth-user user)] 62 | (handle (if ok? 200 404) res)) 63 | (handle 422 {:errors {:body ["Invalid request body."]}})))) 64 | 65 | (defn profile [req] 66 | (let [auth-user (-> req :auth-user) 67 | username (-> req :params :username)] 68 | (if (s/valid? spec/username? username) 69 | (let [[ok? res] (profile/fetch-profile auth-user username)] 70 | (handle (if ok? 200 404) res)) 71 | (handle 422 {:errors {:username ["Invalid username."]}})))) 72 | 73 | (defn follow-profile [req] 74 | (let [auth-user (-> req :auth-user) 75 | username (-> req :params :username)] 76 | (if (s/valid? spec/username? username) 77 | (let [[ok? res] (profile/follow! auth-user username)] 78 | (handle (if ok? 200 404) res)) 79 | (handle 422 {:errors {:username ["Invalid username."]}})))) 80 | 81 | (defn unfollow-profile [req] 82 | (let [auth-user (-> req :auth-user) 83 | username (-> req :params :username)] 84 | (if (s/valid? spec/username? username) 85 | (let [[ok? res] (profile/unfollow! auth-user username)] 86 | (handle (if ok? 200 404) res)) 87 | (handle 422 {:errors {:username ["Invalid username."]}})))) 88 | 89 | (defn articles [req] 90 | (let [auth-user (-> req :auth-user) 91 | limit (parse-query-param (-> req :params :limit)) 92 | offset (parse-query-param (-> req :params :offset)) 93 | author (-> req :params :author) 94 | tag (-> req :params :tag) 95 | favorited (-> req :params :favorited) 96 | [ok? res] (article/articles auth-user 97 | (if (pos-int? limit) limit nil) 98 | (if (nat-int? offset) offset nil) 99 | (if (string? author) author nil) 100 | (if (string? tag) tag nil) 101 | (if (string? favorited) favorited nil))] 102 | (handle (if ok? 200 404) res))) 103 | 104 | (defn article [req] 105 | (let [auth-user (-> req :auth-user) 106 | slug (-> req :params :slug)] 107 | (if (s/valid? spec/slug? slug) 108 | (let [[ok? res] (article/article auth-user slug)] 109 | (handle (if ok? 200 404) res)) 110 | (handle 422 {:errors {:slug ["Invalid slug."]}})))) 111 | 112 | (defn comments [req] 113 | (let [auth-user (-> req :auth-user) 114 | slug (-> req :params :slug)] 115 | (if (s/valid? spec/slug? slug) 116 | (let [[ok? res] (comment-comp/article-comments auth-user slug)] 117 | (handle (if ok? 200 404) res)) 118 | (handle 422 {:errors {:slug ["Invalid slug."]}})))) 119 | 120 | (defn tags [_] 121 | (let [[ok? res] (tag/all-tags)] 122 | (handle (if ok? 200 404) res))) 123 | 124 | (defn feed [req] 125 | (let [auth-user (-> req :auth-user) 126 | limit (parse-query-param (-> req :params :limit)) 127 | offset (parse-query-param (-> req :params :offset)) 128 | [ok? res] (article/feed auth-user 129 | (if (pos-int? limit) limit nil) 130 | (if (nat-int? offset) offset nil))] 131 | (handle (if ok? 200 404) res))) 132 | 133 | (defn create-article [req] 134 | (let [auth-user (-> req :auth-user) 135 | article (-> req :params :article)] 136 | (if (s/valid? article-spec/create-article article) 137 | (let [[ok? res] (article/create-article! auth-user article)] 138 | (handle (if ok? 200 404) res)) 139 | (handle 422 {:errors {:body ["Invalid request body."]}})))) 140 | 141 | (defn update-article [req] 142 | (let [auth-user (-> req :auth-user) 143 | slug (-> req :params :slug) 144 | article (-> req :params :article)] 145 | (if (and (s/valid? article-spec/update-article article) 146 | (s/valid? spec/slug? slug)) 147 | (let [[ok? res] (article/update-article! auth-user slug article)] 148 | (handle (if ok? 200 404) res)) 149 | (handle 422 {:errors {:body ["Invalid request body."]}})))) 150 | 151 | (defn delete-article [req] 152 | (let [auth-user (-> req :auth-user) 153 | slug (-> req :params :slug)] 154 | (if (s/valid? spec/slug? slug) 155 | (let [[ok? res] (article/delete-article! auth-user slug)] 156 | (handle (if ok? 200 404) res)) 157 | (handle 422 {:errors {:slug ["Invalid slug."]}})))) 158 | 159 | (defn add-comment [req] 160 | (let [auth-user (-> req :auth-user) 161 | slug (-> req :params :slug) 162 | comment (-> req :params :comment)] 163 | (if (and (s/valid? spec/slug? slug) 164 | (s/valid? comment-spec/add-comment comment)) 165 | (let [[ok? res] (comment-comp/add-comment! auth-user slug comment)] 166 | (handle (if ok? 200 404) res)) 167 | (handle 422 {:errors {:body ["Invalid request body."]}})))) 168 | 169 | (defn delete-comment [req] 170 | (let [auth-user (-> req :auth-user) 171 | id (parse-query-param (-> req :params :id))] 172 | (if (s/valid? comment-spec/id id) 173 | (let [[ok? res] (comment-comp/delete-comment! auth-user id)] 174 | (handle (if ok? 200 404) res)) 175 | (handle 422 {:errors {:id ["Invalid comment id."]}})))) 176 | 177 | (defn favorite-article [req] 178 | (let [auth-user (-> req :auth-user) 179 | slug (-> req :params :slug)] 180 | (if (s/valid? spec/slug? slug) 181 | (let [[ok? res] (article/favorite-article! auth-user slug)] 182 | (handle (if ok? 200 404) res)) 183 | (handle 422 {:errors {:slug ["Invalid slug."]}})))) 184 | 185 | (defn unfavorite-article [req] 186 | (let [auth-user (-> req :auth-user) 187 | slug (-> req :params :slug)] 188 | (if (s/valid? spec/slug? slug) 189 | (let [[ok? res] (article/unfavorite-article! auth-user slug)] 190 | (handle (if ok? 200 404) res)) 191 | (handle 422 {:errors {:slug ["Invalid slug."]}})))) 192 | -------------------------------------------------------------------------------- /development/src/dev/hello_repl.clj: -------------------------------------------------------------------------------- 1 | (ns dev.hello-repl) 2 | 3 | ;; == Some VS Code knowledge required == 4 | ;; This tutorial assumes you know a few things about 5 | ;; VS Code. Please check out this page if you are new 6 | ;; to the editor: https://code.visualstudio.com/docs 7 | ;; 8 | ;; == Keyboard Shortcuts Notation used in this tutorial == 9 | ;; We use a notation for keyboard shortcuts, where 10 | ;; `+` means the keys are pressed at the same time 11 | ;; and ` ` separates any keyboard presses in the sequence. 12 | ;; `Ctrl+Alt+C Enter` means to press Ctrl, Alt, and C 13 | ;; all at the same time, then release the keys and 14 | ;; then press Enter. (The Alt key is named Option or 15 | ;; Opt, on some machines) 16 | ;; 17 | ;; Let's start by loading this file in the REPL. 18 | ;; Please press `Ctrl+Alt+C Enter` 19 | 20 | "Welcome to the Getting Started REPL! 💜" 21 | 22 | ;; Once you see a message in the output/REPL window -> 23 | ;; saying that this file is loaded, you can start by 24 | ;; placing the cursor anywhere on the line with the 25 | ;; string above and press: `Alt+Enter` 26 | 27 | ;; Did it? Great! 28 | ;; See that `=> "Welcome ...` at the end of the line? 29 | ;; That's the result of the evaluation you just 30 | ;; performed. You just used the Clojure REPL! 31 | ;; 🎉 Congratulations! 🎂 32 | 33 | (comment 34 | ;; You can evaluate the string below in the same way 35 | ;; Place the cursor anywhere on the line with the 36 | ;; string and press `Alt+Enter`. 37 | 38 | "Hello World!" 39 | 40 | ;; This works because this is a 'Rich Comment Form' 41 | ;; which is where we Clojurians often develop new 42 | ;; code. You'll sometimes see it abbreviated as RCF. 43 | ;; See also: https://calva.io/rich-comments/ 44 | 45 | ;; Evaluate the following form too (you can 46 | ;; place the cursor anywhere on any of the two lines): 47 | 48 | (repeat 7 49 | "I am using the REPL! 💪") 50 | 51 | ;; Only `=> ("I am using the REPL! 💪"` is displayed 52 | ;; inline. You can see the full result, and also copy 53 | ;; it, if you hover the evaluated expression. Or press 54 | ;; `Ctrl+K Ctrl/Cmd+I`. 55 | 56 | ;; Let's get into the mood for real. 😂 57 | ;; Place the cursor on any of the five code lines below: 58 | ;; `Alt+Enter`, then `Cmd+K Cmd+I`. 59 | 60 | (map (fn [s] 61 | (if (re-find #" [REPL]$" s) 62 | (str "Give me " s "! ~•~ " (last s) "!") 63 | s)) 64 | ["an R" "an E" "a P" "an L" "What do you get?" "REPL!"]) 65 | 66 | ;; Clear the inline display with `Esc`. The inline 67 | ;; results are also cleared when you edit the file. 68 | 69 | ;; Which brings us to a VERY IMPORTANT THING: 70 | ;; By default, Calva will be a Guardian of the Parens. 71 | ;; This means that the backspace and delete buttons 72 | ;; will not delete balanced brackets. Please go ahead 73 | ;; and try to delete a bracket in the expression above. 74 | ;; See? 75 | 76 | ;; TO DELETE A BALANCED BRACKET: 77 | ;; press `alt/option+backspace` or `alt/option+delete` 78 | 79 | ;; You might notice that the output/REPL window -> 80 | ;; is also displaying the results. Depending on your 81 | ;; preferences you might want to close that window or move 82 | ;; it to the same editor group (unsplit) as the files you 83 | ;; edit. But don't do that just yet, get a feel how how 84 | ;; it works having it in a split pane first. 85 | 86 | ;; BTW. That output/REPL window -> 87 | ;; You can evaluate code from its prompt too. 88 | ;; But the cool peeps do not do that very often. 89 | ;; Because the REPL lives in the files with the application 90 | ;; code! And because Rich Comment Forms (RCF). 91 | ;; It is Interactive Programming, and it is 💪. 92 | 93 | :rcf) ; <- This is a convenient way to keep the closing 94 | ; paren of a Rich comment form from folding 95 | ; when the code is formatted. 96 | 97 | 98 | ;; About commands and shortcuts: 99 | ;; Please read https://calva.io/finding-commands/ 100 | ;; (It's very short.) 101 | ;; When we refer to commands by their name, use 102 | ;; the VS Code Command Palette to search for them 103 | ;; if you don't know the keyboard shortcut. 104 | ;; All Calva commands are prefixed with ”Calva”. 105 | 106 | ;; == Evaluating definitions == 107 | ;; Alt+Enter is the Calva default keyboard shortcut 108 | ;; to evaluate the current ”top level” forms. Top 109 | ;; level meaning the outermost ”container” of forms, 110 | ;; which is the file. This function definition is on 111 | ;; the top level. Please evaluate it! 112 | 113 | (defn greet 114 | "I'll greet you" 115 | [s] 116 | (str "Hello " s "!")) 117 | 118 | ;; Forms inside `(comment ...)` are also considered 119 | ;; to be top level. This makes it easy to experiment 120 | ;; with code. 121 | 122 | (comment 123 | (greet "World") 124 | :rcf) 125 | 126 | ;; Anything printed to stdout is not shown inline. 127 | 128 | (comment 129 | (println (greet "World")) 130 | :rcf) 131 | 132 | ;; You should see the result of the evaluation, nil, 133 | ;; inline, and ”Hello World!” followed by the result 134 | ;; printed to the output window. 135 | 136 | ;; Maybe you wonder what a ”form” is? Loosely defined 137 | ;; it is about the same as an S-expression: 138 | ;; https://en.wikipedia.org/wiki/S-expression 139 | ;; That is, either a ”word” or something enclosed in 140 | ;; brackets of some type, parens (), hard brackets [], 141 | ;; curlies {}, or quotes "". This whole thing is a 142 | ;; form: 143 | 144 | (str 23 (apply + [2 3]) (:foo {:foo "foo"})) 145 | 146 | ;; So is `str`, `23`, "foo", `(apply + [2 3])`, 147 | ;; `{:foo "foo"}`, `+`, `[2 3]`, `apply`, and also 148 | ;; `(:foo {:foo "foo"})`. 149 | 150 | ;; Calva has a concept of ”current form”, to let you 151 | ;; evaluate forms that are not at the top level. The 152 | ;; ”current form” is determined by where the cursor is. 153 | ;; Calva has a command that will let you easily 154 | ;; experiment with which form is considered current: 155 | ;; * Calva: Expand Selection 156 | ;; Using this command, starting from no selection, will 157 | ;; select the current form. 158 | 159 | ;; == Evaluating the Current Form == 160 | ;; Ctrl+Enter evaluates the ”current” form 161 | ;; Try it with the cursor at different places in this 162 | ;; code snippet: 163 | 164 | (comment 165 | (str 23 (apply + [2 3]) (:foo {:foo "foo"})) 166 | 167 | ;; You might discover that Calva regards words in 168 | ;; strings as forms. Don't panic if `foo` causes 169 | ;; `foo` is undefined until you top-level eval it. 170 | ;; an evaluation error. It is not defined, since 171 | ;; it shouldn't be. You can define it, of course, 172 | ;; just for fun and learning: Top level eval the 173 | ;; following definition. 174 | 175 | (def foo 176 | [1 2 :three :four]) 177 | 178 | ;; Then *evaluate current form* with the cursor 179 | ;; inside the string "foo" above. 180 | ;; Whatever you ask Calva to send to the REPL, 181 | ;; Calva will send to the REPL. 182 | 183 | :rcf) 184 | 185 | ;; == Rich Comments Support == 186 | ;; Repeating an important concept: Forms inside 187 | ;; `(comment ...)` are also considered top level 188 | ;; by Calva. Alt+Enter at different places below 189 | ;; to get a feel for it. 190 | 191 | (comment 192 | "I ♥️ Clojure" 193 | 194 | (greet "World") 195 | 196 | foo 197 | 198 | (range 10) 199 | 200 | ;; https://calva.io/rich-comments/ 201 | :rcf) 202 | 203 | 204 | ;; Also try the commands *Show Hover*, 205 | ;; *Show Definition Preview Hover* 206 | ;; *Go to Definition* 207 | 208 | (comment 209 | (println (greet "side effect")) 210 | (+ (* 2 2) 211 | 2) 212 | 213 | :rcf) 214 | 215 | 216 | ;; == You Control what is Evaluated == 217 | ;; Please note that Calva never evaluates your code 218 | ;; unless you explicitly ask for it. So, you will 219 | ;; have to load files you open yourself. Make it a 220 | ;; habit to do this, because sometimes things don't 221 | ;; work when your file is not loaded. 222 | 223 | ;; Try it with this file: `Ctrl+Alt+C Enter`. 224 | ;; The result of loading a file is whatever is the 225 | ;; last top level form in the file. 226 | 227 | ;; == Editing Code == 228 | ;; A note about editing Clojure in Calva: 229 | ;; If you edit and experiment with the examples you 230 | ;; will notice that Calva auto-indents your code. 231 | ;; You can re-indent, and format, code at will, using 232 | ;; the `Tab` key. It will format the current enclosing 233 | ;; form. Try it at the numbered places in this piece 234 | ;; of code, starting at `; 1`: 235 | 236 | (comment ; 3 237 | (defn- divisible 238 | "Is `n` divisible by `d`?" 239 | [n d] 240 | (zero? (mod n d) 241 | ) 242 | 243 | ) 244 | 245 | (defn fizz-buzz [n] ; 2 246 | (cond ; 1 247 | (divisible n (* 5 3)) "FizzBuzz" 248 | (divisible n 5) "Buzz" 249 | (divisible n 3) "Fizz" 250 | :else n)) 251 | :rcf) 252 | 253 | ;; === Paredit `strict` mode is on === 254 | ;; Calva supports structural editing (editing that 255 | ;; considers forms rather than lines) using a system 256 | ;; called Paredit. By default Paredit tries to protect 257 | ;; from accidentally deleting brackets and unbalancing 258 | ;; the structure of forms. To override the protection, 259 | ;; use `Alt+Backspace` or `Alt+delete`. 260 | 261 | (comment 262 | (defn strict-greet 263 | "Try to remove brackets and string quotes 264 | using Backspace or Delete. Try the same 265 | with the Alt key pressed." 266 | [name] 267 | (str "Strictly yours, " name "!")) 268 | 269 | (strict-greet "dear Paredit fan") 270 | :rcf) 271 | 272 | ;; (Restore with *Undo* if needed.) 273 | ;; See also: 274 | ;; https://calva.io/paredit 275 | 276 | ;;;;;;;;;;;;;;;;;;;;;; 🤘 🎸 🎉 ;;;;;;;;;;;;;;;;;;;;; 277 | 278 | ;; Done? Awesome. Please continue with the Polylith 279 | ;; `getting_started.clj` 280 | 281 | ;; Learn much more about Calva at https://calva.io 282 | ;; Also: There is a more comprehensive guide to Calva 283 | ;; as well as to Paredit and even Clojure, built in 284 | 285 | 286 | ;; This string is the last expression in this file 287 | 288 | "hello_repl.clj is loaded, and ready with some things for you to try." 289 | 290 | ;; It is what you'll see printed in the Output 291 | ;; window when you load the file. 292 | 293 | ;; This guide downloaded from: 294 | ;; https://github.com/BetterThanTomorrow/dram 295 | ;; Please consider contributing. 296 | -------------------------------------------------------------------------------- /components/article/test/clojure/realworld/article/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.article.core-test 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.realworld.database.interface :as database] 4 | [clojure.realworld.article.core :as core] 5 | [clojure.realworld.article.spec :as spec] 6 | [clojure.realworld.user.interface.spec :as user-spec] 7 | [clojure.spec.alpha :as s] 8 | [clojure.spec.gen.alpha :as gen] 9 | [clojure.test :refer [deftest is use-fixtures]])) 10 | 11 | (defn- test-db 12 | ([] {:classname "org.sqlite.JDBC" 13 | :subprotocol "sqlite" 14 | :subname "test.db"}) 15 | ([_] (test-db))) 16 | 17 | (defn prepare-for-tests [f] 18 | (with-redefs [database/db test-db] 19 | (let [db (test-db)] 20 | (database/generate-db db) 21 | (f) 22 | (database/drop-db db)))) 23 | 24 | (use-fixtures :each prepare-for-tests) 25 | 26 | (deftest article--article-not-found--return-negative-result 27 | (let [[ok? res] (core/article nil "wrong-slug")] 28 | (is (false? ok?)) 29 | (is (= {:errors {:slug ["Cannot find an article with given slug."]}} res)))) 30 | 31 | (deftest article--article-found--return-positive-result 32 | (let [input {:title "title" :slug "this-is-slug"} 33 | _ (jdbc/insert! (test-db) :article input) 34 | [ok? res] (core/article nil "this-is-slug")] 35 | (is (true? ok?)) 36 | (is (= 1 (-> res :article :id))) 37 | (is (= "title" (-> res :article :title))) 38 | (is (= "this-is-slug" (-> res :article :slug))))) 39 | 40 | (deftest create-article--test 41 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 42 | _ (jdbc/insert! (test-db) :user auth-user) 43 | inputs (gen/sample (s/gen spec/create-article) 20) 44 | results (map #(core/create-article! auth-user %) inputs)] 45 | (is (every? true? (map first results))) 46 | (is (every? #(s/valid? spec/visible-article (second %)) results)))) 47 | 48 | (deftest update-article--article-not-found--return-negative-response 49 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 50 | _ (jdbc/insert! (test-db) :user auth-user) 51 | input (gen/generate (s/gen spec/update-article)) 52 | [ok? res] (core/update-article! auth-user "slug" input)] 53 | (is (false? ok?)) 54 | (is (= {:errors {:slug ["Cannot find an article with given slug."]}} res)))) 55 | 56 | (deftest update-article--article-is-not-owned-by-user--return-negative-response 57 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 58 | _ (jdbc/insert! (test-db) :user auth-user) 59 | initial (gen/generate (s/gen spec/create-article)) 60 | [_ article] (core/create-article! auth-user initial) 61 | input (gen/generate (s/gen spec/update-article)) 62 | [ok? res] (core/update-article! (assoc auth-user :id 2) 63 | (-> article :article :slug) 64 | input)] 65 | (is (false? ok?)) 66 | (is (= {:errors {:authorization ["You need to be author of this article to update it."]}} res)))) 67 | 68 | (deftest update-article--input-is-ok--update-article-and-return-positive-response 69 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 70 | _ (jdbc/insert! (test-db) :user auth-user) 71 | initial-inputs (gen/sample (s/gen spec/create-article) 20) 72 | create-res (map #(core/create-article! auth-user %) initial-inputs) 73 | inputs (gen/sample (s/gen spec/update-article) 20) 74 | update-res (map-indexed #(core/update-article! auth-user 75 | (-> (nth create-res %1) second :article :slug) 76 | %2) 77 | inputs)] 78 | (is (every? true? (map first update-res))) 79 | (is (every? #(s/valid? spec/visible-article (second %)) update-res)))) 80 | 81 | (deftest delete-article--article-not-found--return-negative-response 82 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 83 | _ (jdbc/insert! (test-db) :user auth-user) 84 | [ok? res] (core/delete-article! auth-user "slug")] 85 | (is (false? ok?)) 86 | (is (= {:errors {:slug ["Cannot find an article with given slug."]}} res)))) 87 | 88 | (deftest delete-article--article-is-not-owned-by-user--return-negative-response 89 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 90 | _ (jdbc/insert! (test-db) :user auth-user) 91 | initial (gen/generate (s/gen spec/create-article)) 92 | [_ article] (core/create-article! auth-user initial) 93 | [ok? res] (core/delete-article! (assoc auth-user :id 2) 94 | (-> article :article :slug))] 95 | (is (false? ok?)) 96 | (is (= {:errors {:authorization ["You need to be author of this article to delete it."]}} res)))) 97 | 98 | (deftest delete-article--input-is-ok--delete-article-and-return-positive-response 99 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 100 | _ (jdbc/insert! (test-db) :user auth-user) 101 | initial-inputs (gen/sample (s/gen spec/create-article) 20) 102 | create-res (map #(core/create-article! auth-user %) initial-inputs) 103 | update-res (map #(core/delete-article! auth-user (-> % second :article :slug)) create-res)] 104 | (is (every? #(= [true nil] %) update-res)))) 105 | 106 | (deftest favorite-article!--profile-not-found--return-negative-result 107 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 108 | _ (jdbc/insert! (test-db) :user auth-user) 109 | [ok? res] (core/favorite-article! auth-user "slug")] 110 | (is (false? ok?)) 111 | (is (= {:errors {:slug ["Cannot find an article with given slug."]}} res)))) 112 | 113 | (deftest favorite-article!--profile-found--return-positive-result 114 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 115 | _ (jdbc/insert! (test-db) :user auth-user) 116 | [_ article] (core/create-article! auth-user (gen/generate (s/gen spec/create-article))) 117 | [ok? res] (core/favorite-article! auth-user (-> article :article :slug))] 118 | (is (true? ok?)) 119 | (is (= 1 (-> res :article :favoritesCount))) 120 | (is (true? (-> res :article :favorited))))) 121 | 122 | (deftest unfavorite-article!--profile-not-found--return-negative-result 123 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 124 | _ (jdbc/insert! (test-db) :user auth-user) 125 | [ok? res] (core/unfavorite-article! auth-user "slug")] 126 | (is (false? ok?)) 127 | (is (= {:errors {:slug ["Cannot find an article with given slug."]}} res)))) 128 | 129 | (deftest unfavorite-article!--logged-in-and-profile-found--return-positive-result 130 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 131 | _ (jdbc/insert! (test-db) :user auth-user) 132 | [_ article] (core/create-article! auth-user (gen/generate (s/gen spec/create-article))) 133 | _ (core/favorite-article! auth-user (-> article :article :slug)) 134 | [ok? res] (core/unfavorite-article! auth-user (-> article :article :slug))] 135 | (is (true? ok?)) 136 | (is (= 0 (-> res :article :favoritesCount))) 137 | (is (false? (-> res :article :favorited))))) 138 | 139 | (deftest feed--no-articles-found--return-response-with-empty-vector 140 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 141 | _ (jdbc/insert! (test-db) :user auth-user) 142 | [ok? res] (core/feed auth-user 10 0)] 143 | (is (true? ok?)) 144 | (is (= {:articles [] 145 | :articlesCount 0} 146 | res)))) 147 | 148 | (deftest feed--articles-found--return-response 149 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 150 | other-user (assoc (gen/generate (s/gen user-spec/user)) :id 2) 151 | _ (jdbc/insert-multi! (test-db) :user [auth-user other-user]) 152 | _ (jdbc/insert! (test-db) :userFollows {:userId 1 :followedUserId 2}) 153 | articles (gen/sample (s/gen spec/create-article) 20) 154 | _ (doseq [a articles] 155 | (core/create-article! other-user a)) 156 | [ok? res] (core/feed auth-user 10 0)] 157 | (is (true? ok?)) 158 | (is (= 10 (:articlesCount res))) 159 | (is (= 10 (-> res :articles count))) 160 | (is (= (map :title (take 10 (reverse articles))) 161 | (map :title (:articles res)))))) 162 | 163 | (deftest feed--multiple-followed-users--return-response 164 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 165 | other-user (assoc (gen/generate (s/gen user-spec/user)) :id 2) 166 | other-user-2 (assoc (gen/generate (s/gen user-spec/user)) :id 3) 167 | _ (jdbc/insert-multi! (test-db) :user [auth-user other-user]) 168 | _ (jdbc/insert-multi! (test-db) :userFollows [{:userId 1 :followedUserId 2} 169 | {:userId 1 :followedUserId 3}]) 170 | articles (gen/sample (s/gen spec/create-article) 20) 171 | _ (doseq [a (take 10 articles)] 172 | (core/create-article! other-user a)) 173 | _ (doseq [a (take 10 (drop 10 articles))] 174 | (core/create-article! other-user-2 a)) 175 | [ok? res] (core/feed auth-user 10 0)] 176 | (is (true? ok?)) 177 | (is (= 10 (:articlesCount res))) 178 | (is (= 10 (-> res :articles count))) 179 | (is (= (map :title (take 10 (reverse articles))) 180 | (map :title (:articles res)))))) 181 | 182 | (deftest feed--no-limit-provided--return-response-with-limit-20 183 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 184 | other-user (assoc (gen/generate (s/gen user-spec/user)) :id 2) 185 | _ (jdbc/insert-multi! (test-db) :user [auth-user other-user]) 186 | _ (jdbc/insert! (test-db) :userFollows {:userId 1 :followedUserId 2}) 187 | articles (gen/sample (s/gen spec/create-article) 20) 188 | _ (doseq [a articles] 189 | (core/create-article! other-user a)) 190 | [ok? res] (core/feed auth-user nil nil)] 191 | (is (true? ok?)) 192 | (is (= 20 (:articlesCount res))) 193 | (is (= 20 (-> res :articles count))) 194 | (is (= (map :title (take 20 (reverse articles))) 195 | (map :title (:articles res)))))) 196 | 197 | (deftest feed--offset-provided--return-response-with-limit-20 198 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 199 | other-user (assoc (gen/generate (s/gen user-spec/user)) :id 2) 200 | _ (jdbc/insert-multi! (test-db) :user [auth-user other-user]) 201 | _ (jdbc/insert! (test-db) :userFollows {:userId 1 :followedUserId 2}) 202 | articles (gen/sample (s/gen spec/create-article) 20) 203 | _ (doseq [a articles] 204 | (core/create-article! other-user a)) 205 | [ok? res] (core/feed auth-user nil 5)] 206 | (is (true? ok?)) 207 | (is (= 15 (:articlesCount res))) 208 | (is (= 15 (-> res :articles count))) 209 | (is (= (map :title (take 15 (drop 5 (reverse articles)))) 210 | (map :title (:articles res)))))) 211 | 212 | (deftest articles--no-articles-found--return-response-with-empty-vector 213 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 214 | _ (jdbc/insert! (test-db) :user auth-user) 215 | [ok? res] (core/articles auth-user 10 0 nil nil nil)] 216 | (is (true? ok?)) 217 | (is (= {:articles [] 218 | :articlesCount 0} 219 | res)))) 220 | 221 | (deftest articles--articles-found--return-response 222 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1) 223 | other-user (assoc (gen/generate (s/gen user-spec/user)) :id 2) 224 | _ (jdbc/insert-multi! (test-db) :user [auth-user other-user]) 225 | articles (gen/sample (s/gen spec/create-article) 20) 226 | _ (doseq [a articles] 227 | (core/create-article! other-user a)) 228 | [ok? res] (core/articles auth-user 10 0 nil nil nil)] 229 | (is (true? ok?)) 230 | (is (= 10 (:articlesCount res))) 231 | (is (= 10 (-> res :articles count))) 232 | (is (= (map :title (take 10 (reverse articles))) 233 | (map :title (:articles res)))))) 234 | -------------------------------------------------------------------------------- /components/article/test/clojure/realworld/article/store_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.article.store-test 2 | (:require [clj-time.core :as t] 3 | [clojure.java.jdbc :as jdbc] 4 | [clojure.realworld.database.interface :as database] 5 | [clojure.realworld.article.store :as store] 6 | [clojure.test :refer [deftest is use-fixtures]])) 7 | 8 | (defn test-db 9 | ([] {:classname "org.sqlite.JDBC" 10 | :subprotocol "sqlite" 11 | :subname "test.db"}) 12 | ([_] (test-db))) 13 | 14 | (defn prepare-for-tests [f] 15 | (with-redefs [database/db test-db] 16 | (let [db (test-db)] 17 | (database/generate-db db) 18 | (f) 19 | (database/drop-db db)))) 20 | 21 | (use-fixtures :each prepare-for-tests) 22 | 23 | (deftest find-by-slug--test 24 | (let [_ (jdbc/insert! (test-db) :article {:slug "this-is-slug"}) 25 | res1 (store/find-by-slug "this-is-slug") 26 | article {:body nil 27 | :createdAt nil 28 | :description nil 29 | :id 1 30 | :slug "this-is-slug" 31 | :title nil 32 | :updatedAt nil 33 | :userId nil} 34 | res2 (store/find-by-slug "wrong-slug")] 35 | (is (= article res1)) 36 | (is (nil? res2)))) 37 | 38 | (deftest insert-article!--test 39 | (let [now (t/now) 40 | article {:slug "slug" 41 | :title "title" 42 | :description "description" 43 | :body "body" 44 | :createdAt now 45 | :updatedAt now 46 | :userId 1} 47 | _ (store/insert-article! article) 48 | res (store/find-by-slug "slug")] 49 | (is (= (assoc article :id 1 50 | :createdAt (-> article :createdAt str) 51 | :updatedAt (-> article :updatedAt str)) 52 | res)))) 53 | 54 | (deftest tags-with-names--test 55 | (let [_ (jdbc/insert-multi! (test-db) :tag [{:name "tag1"} 56 | {:name "tag2"} 57 | {:name "tag3"} 58 | {:name "tag4"} 59 | {:name "tag5"} 60 | {:name "tag6"}]) 61 | res (store/tags-with-names ["tag1" "tag2" "tag3"])] 62 | (is (= [{:id 1 63 | :name "tag1"} 64 | {:id 2 65 | :name "tag2"} 66 | {:id 3 67 | :name "tag3"}] 68 | res)))) 69 | 70 | (deftest add-tags-to-article!--test 71 | (let [_ (jdbc/insert-multi! (test-db) :tag [{:name "tag1"} 72 | {:name "tag2"} 73 | {:name "tag3"} 74 | {:name "tag4"} 75 | {:name "tag5"} 76 | {:name "tag6"}]) 77 | _ (store/add-tags-to-article! 1 ["tag1" "tag2" "tag3"]) 78 | res (store/article-tags 1)] 79 | (is (= ["tag1" "tag2" "tag3"] res)))) 80 | 81 | (deftest article-tags--test 82 | (let [_ (jdbc/insert-multi! (test-db) :tag [{:name "tag1"} 83 | {:name "tag2"} 84 | {:name "tag3"} 85 | {:name "tag4"} 86 | {:name "tag5"} 87 | {:name "tag6"}]) 88 | _ (jdbc/insert-multi! (test-db) :articleTags [{:articleId 1 :tagId 1} 89 | {:articleId 1 :tagId 2} 90 | {:articleId 1 :tagId 3} 91 | {:articleId 2 :tagId 3} 92 | {:articleId 2 :tagId 4} 93 | {:articleId 2 :tagId 5}]) 94 | res (store/article-tags 1)] 95 | (is (= ["tag1" "tag2" "tag3"] res)))) 96 | 97 | (deftest insert-tag!--tag-exists--do-nothing 98 | (let [_ (jdbc/insert! (test-db) :tag {:name "tag1"}) 99 | res (store/insert-tag! "tag1")] 100 | (is (nil? res)))) 101 | 102 | (deftest insert-tag!--tag-does-not-exist--insert-tag 103 | (let [_ (store/insert-tag! "tag1") 104 | tags (store/tags-with-names ["tag1"])] 105 | (is (= [{:id 1 106 | :name "tag1"}] 107 | tags)))) 108 | 109 | (deftest favorited?--not-favorited--return-false 110 | (let [favorited? (store/favorited? 1 1)] 111 | (is (false? favorited?)))) 112 | 113 | (deftest favorited?--favorited--return-true 114 | (let [_ (jdbc/insert! (test-db) :favoriteArticles {:articleId 1 :userId 1}) 115 | favorited? (store/favorited? 1 1)] 116 | (is (true? favorited?)))) 117 | 118 | (deftest favorites-count--not-favorited--return-0 119 | (let [favorites-count (store/favorites-count 1)] 120 | (is (= 0 favorites-count)))) 121 | 122 | (deftest favorites-count--favorited--return-favorites-count 123 | (let [_ (jdbc/insert-multi! (test-db) :favoriteArticles [{:articleId 1 :userId 1} 124 | {:articleId 1 :userId 2} 125 | {:articleId 1 :userId 3}]) 126 | favorites-count (store/favorites-count 1)] 127 | (is (= 3 favorites-count)))) 128 | 129 | (deftest update-article!--test 130 | (let [now (t/now) 131 | _ (store/insert-article! {:slug "slug" 132 | :title "title" 133 | :description "description" 134 | :body "body" 135 | :createdAt now 136 | :updatedAt now 137 | :userId 1}) 138 | update-time (t/now) 139 | article {:slug "updated-slug" 140 | :title "updated-title" 141 | :description "description" 142 | :body "body" 143 | :updatedAt update-time} 144 | _ (store/update-article! 1 article) 145 | res (store/find-by-slug "updated-slug")] 146 | (is (= (assoc article :id 1 147 | :createdAt (str now) 148 | :updatedAt (str update-time) 149 | :userId 1) 150 | res)))) 151 | 152 | (deftest delete-article!--test 153 | (let [now (t/now) 154 | _ (store/insert-article! {:slug "slug" 155 | :title "title" 156 | :description "description" 157 | :body "body" 158 | :createdAt now 159 | :updatedAt now 160 | :userId 1}) 161 | _ (jdbc/insert-multi! (test-db) :tag [{:name "tag1"} {:name "tag2"} {:name "tag3"}]) 162 | _ (jdbc/insert-multi! (test-db) :articleTags [{:tagId 1 :articleId 1} 163 | {:tagId 2 :articleId 1} 164 | {:tagId 3 :articleId 1}]) 165 | _ (jdbc/insert-multi! (test-db) :favoriteArticles [{:userId 1 :articleId 1} 166 | {:userId 2 :articleId 1} 167 | {:userId 3 :articleId 1}]) 168 | article-before (store/find-by-slug "slug") 169 | favorites-count-before (store/favorites-count 1) 170 | tags-before (store/article-tags 1) 171 | _ (store/delete-article! 1) 172 | article-after (store/find-by-slug "slug") 173 | favorites-count-after (store/favorites-count 1) 174 | tags-after (store/article-tags 1)] 175 | (is (not (nil? article-before))) 176 | (is (= 3 favorites-count-before)) 177 | (is (= ["tag1" "tag2" "tag3"] tags-before)) 178 | (is (nil? article-after)) 179 | (is (= 0 favorites-count-after)) 180 | (is (= [] tags-after)))) 181 | 182 | (deftest favorite!--currently-not-favorited--insert-user-favorite 183 | (let [before-favorited? (store/favorited? 1 2) 184 | _ (store/favorite! 1 2) 185 | after-favorited? (store/favorited? 1 2)] 186 | (is (false? before-favorited?)) 187 | (is (true? after-favorited?)))) 188 | 189 | (deftest favorite!--currently-favorited--do-nothing 190 | (let [_ (store/favorite! 1 2) 191 | before-favorited? (store/favorited? 1 2) 192 | _ (store/favorite! 1 2) 193 | after-favorited? (store/favorited? 1 2)] 194 | (is (true? before-favorited?)) 195 | (is (true? after-favorited?)))) 196 | 197 | (deftest unfavorite!--currently-favorited--delete-user-favorite 198 | (let [_ (store/favorite! 1 2) 199 | before-favorited? (store/favorited? 1 2) 200 | _ (store/unfavorite! 1 2) 201 | after-favorited? (store/favorited? 1 2)] 202 | (is (true? before-favorited?)) 203 | (is (false? after-favorited?)))) 204 | 205 | (deftest unfavorite!--currently-not-favorited--do-nothing 206 | (let [before-favorited? (store/favorited? 1 2) 207 | _ (store/unfavorite! 1 2) 208 | after-favorited? (store/favorited? 1 2)] 209 | (is (false? before-favorited?)) 210 | (is (false? after-favorited?)))) 211 | 212 | (deftest feed--no-articles-found--return-empty-vector 213 | (let [res (store/feed 1 10 0)] 214 | (is (= [] res)))) 215 | 216 | (deftest feed--some-articles-found--return-articles 217 | (let [_ (jdbc/insert-multi! (test-db) :article [{:slug "slug1" :userId 1} 218 | {:slug "slug2" :userId 1} 219 | {:slug "slug3" :userId 1} 220 | {:slug "slug4" :userId 2} 221 | {:slug "slug5" :userId 2} 222 | {:slug "slug6" :userId 3}]) 223 | _ (jdbc/insert-multi! (test-db) :userFollows [{:userId 4 :followedUserId 1} 224 | {:userId 4 :followedUserId 3}]) 225 | res (store/feed 4 10 0)] 226 | (is (= ["slug6" "slug3" "slug2" "slug1"] 227 | (mapv :slug res))))) 228 | 229 | (deftest feed--more-than-10-articles-found--return-10-articles 230 | (let [articles (mapv #(hash-map :slug (str "slug" %) :userId 1) (range 30)) 231 | _ (jdbc/insert-multi! (test-db) :article articles) 232 | _ (jdbc/insert-multi! (test-db) :userFollows [{:userId 2 :followedUserId 1}]) 233 | res1 (store/feed 2 10 0) 234 | res2 (store/feed 2 10 10)] 235 | (is (= (take 10 (reverse articles)) 236 | (mapv #(select-keys % [:slug :userId]) res1))) 237 | (is (= (take 10 (drop 10 (reverse articles))) 238 | (mapv #(select-keys % [:slug :userId]) res2))))) 239 | 240 | (deftest articles-by-tag--some-articles-found--return-articles 241 | (let [_ (jdbc/insert-multi! (test-db) :article [{:slug "slug1"} 242 | {:slug "slug2"} 243 | {:slug "slug3"} 244 | {:slug "slug4"} 245 | {:slug "slug5"} 246 | {:slug "slug6"}]) 247 | _ (jdbc/insert-multi! (test-db) :tag [{:name "tag1"} 248 | {:name "tag2"}]) 249 | _ (jdbc/insert-multi! (test-db) :articleTags [{:articleId 1 :tagId 1} 250 | {:articleId 1 :tagId 2} 251 | {:articleId 2 :tagId 2} 252 | {:articleId 3 :tagId 2} 253 | {:articleId 4 :tagId 1} 254 | {:articleId 6 :tagId 1}]) 255 | res1 (store/articles-by-tag 10 0 "tag1") 256 | res2 (store/articles-by-tag 10 0 "tag2")] 257 | (is (= ["slug6" "slug4" "slug1"] 258 | (mapv :slug res1))) 259 | (is (= ["slug3" "slug2" "slug1"] 260 | (mapv :slug res2))))) 261 | 262 | (deftest articles-by-author--some-articles-found--return-articles 263 | (let [_ (jdbc/insert-multi! (test-db) :user [{:username "username1"} 264 | {:username "username2"}]) 265 | _ (jdbc/insert-multi! (test-db) :article [{:slug "slug1" :userId 1} 266 | {:slug "slug2" :userId 1} 267 | {:slug "slug3" :userId 2} 268 | {:slug "slug4" :userId 2} 269 | {:slug "slug5" :userId 1} 270 | {:slug "slug6" :userId 2}]) 271 | res1 (store/articles-by-author 10 0 "username1") 272 | res2 (store/articles-by-author 10 0 "username2") 273 | res3 (store/articles-by-author 10 0 "username3")] 274 | (is (= ["slug5" "slug2" "slug1"] 275 | (mapv :slug res1))) 276 | (is (= ["slug6" "slug4" "slug3"] 277 | (mapv :slug res2))) 278 | (is (empty? res3)))) 279 | 280 | (deftest articles-by-favorited--some-articles-found--return-articles 281 | (let [_ (jdbc/insert-multi! (test-db) :user [{:username "username1"} 282 | {:username "username2"}]) 283 | _ (jdbc/insert-multi! (test-db) :article [{:slug "slug1" :userId 1} 284 | {:slug "slug2" :userId 1} 285 | {:slug "slug3" :userId 2} 286 | {:slug "slug4" :userId 2} 287 | {:slug "slug5" :userId 1} 288 | {:slug "slug6" :userId 2}]) 289 | _ (jdbc/insert-multi! (test-db) :favoriteArticles [{:articleId 1 :userId 1} 290 | {:articleId 2 :userId 1} 291 | {:articleId 3 :userId 2} 292 | {:articleId 6 :userId 1}]) 293 | res1 (store/articles-by-favorited 10 0 "username1") 294 | res2 (store/articles-by-favorited 10 0 "username2") 295 | res3 (store/articles-by-favorited 10 0 "username3")] 296 | (is (= ["slug6" "slug2" "slug1"] 297 | (mapv :slug res1))) 298 | (is (= ["slug3"] 299 | (mapv :slug res2))) 300 | (is (empty? res3)))) 301 | 302 | (deftest articles--no-other-filters--return-articles 303 | (let [_ (jdbc/insert-multi! (test-db) :article [{:slug "slug1"} 304 | {:slug "slug2"} 305 | {:slug "slug3"} 306 | {:slug "slug4"} 307 | {:slug "slug5"} 308 | {:slug "slug6"}]) 309 | res (store/articles 10 0 nil nil nil)] 310 | (is (= ["slug6" "slug5" "slug4" "slug3" "slug2" "slug1"] 311 | (mapv :slug res))))) 312 | -------------------------------------------------------------------------------- /bases/rest-api/test/clojure/realworld/rest_api/handler_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.realworld.rest-api.handler-test 2 | (:require [clojure.test :refer [deftest is use-fixtures]] 3 | [clojure.realworld.article.interface :as article] 4 | [clojure.realworld.article.interface.spec :as article-spec] 5 | [clojure.realworld.rest-api.handler :as handler] 6 | [clojure.realworld.comment.interface :as comment-comp] 7 | [clojure.realworld.comment.interface.spec :as comment-spec] 8 | [clojure.realworld.profile.interface :as profile] 9 | [clojure.realworld.spec.interface :as spec] 10 | [clojure.realworld.tag.interface :as tag] 11 | [clojure.realworld.user.interface :as user] 12 | [clojure.realworld.user.interface.spec :as user-spec] 13 | [clojure.spec.alpha :as s] 14 | [clojure.spec.gen.alpha :as gen])) 15 | 16 | (defn prepare-for-tests [f] 17 | (with-redefs [user/login! (fn [_] [true {}]) 18 | user/register! (fn [_] [true {}]) 19 | user/user-by-token (fn [_] [true {}]) 20 | user/update-user! (fn [_ _] [true {}]) 21 | profile/fetch-profile (fn [_ _] [true {}]) 22 | profile/follow! (fn [_ _] [true {}]) 23 | profile/unfollow! (fn [_ _] [true {}]) 24 | article/article (fn [_ _] [true {}]) 25 | article/create-article! (fn [_ _] [true {}]) 26 | article/update-article! (fn [_ _ _] [true {}]) 27 | article/delete-article! (fn [_ _] [true {}]) 28 | article/favorite-article! (fn [_ _] [true {}]) 29 | article/unfavorite-article! (fn [_ _] [true {}]) 30 | article/feed (fn [_ limit offset] [true {:limit limit :offset offset}]) 31 | article/articles (fn [_ limit offset author tag favorited] 32 | [true {:limit limit :offset offset 33 | :author author :tag tag :favorited favorited}]) 34 | tag/all-tags (fn [] [true {:tags []}]) 35 | comment-comp/article-comments (fn [_ _] [true {:comments []}]) 36 | comment-comp/add-comment! (fn [_ _ _] [true {}]) 37 | comment-comp/delete-comment! (fn [_ _] [true {}])] 38 | (f))) 39 | 40 | (use-fixtures :each prepare-for-tests) 41 | 42 | (deftest login--invalid-input--return-422 43 | (let [res (handler/login {})] 44 | (is (= {:status 422 45 | :body {:errors {:body ["Invalid request body."]}}} 46 | res)))) 47 | 48 | (deftest login--valid-input--return-200 49 | (let [res (handler/login {:params {:user (gen/generate (s/gen user-spec/login))}})] 50 | (is (= {:status 200 51 | :body {}} 52 | res)))) 53 | 54 | (deftest register--invalid-input--return-422 55 | (let [res (handler/register {})] 56 | (is (= {:status 422 57 | :body {:errors {:body ["Invalid request body."]}}} 58 | res)))) 59 | 60 | (deftest register--valid-input--return-200 61 | (let [res (handler/register {:params {:user (gen/generate (s/gen user-spec/register))}})] 62 | (is (= {:status 200 63 | :body {}} 64 | res)))) 65 | 66 | (deftest current-user--valid-input--return-200 67 | (let [auth-user (gen/generate (s/gen user-spec/user)) 68 | res (handler/current-user {:auth-user auth-user})] 69 | (is (= {:status 200 70 | :body {:user auth-user}} 71 | res)))) 72 | 73 | (deftest update-user--invalid-input--return-422 74 | (let [res (handler/update-user {})] 75 | (is (= {:status 422 76 | :body {:errors {:body ["Invalid request body."]}}} 77 | res)))) 78 | 79 | (deftest update-user--valid-input--return-200 80 | (let [res (handler/update-user {:auth-user (gen/generate (s/gen user-spec/user)) 81 | :params {:user (gen/generate (s/gen user-spec/update-user))}})] 82 | (is (= {:status 200 83 | :body {}} 84 | res)))) 85 | 86 | (deftest profile--invalid-input--return-422 87 | (let [res (handler/profile {})] 88 | (is (= {:status 422 89 | :body {:errors {:username ["Invalid username."]}}} 90 | res)))) 91 | 92 | (deftest profile--valid-input--return-200 93 | (let [res (handler/profile {:auth-user (gen/generate (s/gen user-spec/user)) 94 | :params {:username (gen/generate (s/gen spec/username?))}})] 95 | (is (= {:status 200 96 | :body {}} 97 | res)))) 98 | 99 | (deftest follow--invalid-input--return-422 100 | (let [res (handler/follow-profile {})] 101 | (is (= {:status 422 102 | :body {:errors {:username ["Invalid username."]}}} 103 | res)))) 104 | 105 | (deftest follow--valid-input--return-200 106 | (let [res (handler/follow-profile {:auth-user (gen/generate (s/gen user-spec/user)) 107 | :params {:username (gen/generate (s/gen spec/username?))}})] 108 | (is (= {:status 200 109 | :body {}} 110 | res)))) 111 | 112 | (deftest unfollow--invalid-input--return-422 113 | (let [res (handler/unfollow-profile {})] 114 | (is (= {:status 422 115 | :body {:errors {:username ["Invalid username."]}}} 116 | res)))) 117 | 118 | (deftest unfollow--valid-input--return-200 119 | (let [res (handler/unfollow-profile {:auth-user (gen/generate (s/gen user-spec/user)) 120 | :params {:username (gen/generate (s/gen spec/username?))}})] 121 | (is (= {:status 200 122 | :body {}} 123 | res)))) 124 | 125 | (deftest article--invalid-input--return-422 126 | (let [res (handler/article {})] 127 | (is (= {:status 422 128 | :body {:errors {:slug ["Invalid slug."]}}} 129 | res)))) 130 | 131 | (deftest article--valid-input--return-200 132 | (let [res (handler/article {:params {:slug "this-is-slug"}})] 133 | (is (= {:status 200 134 | :body {}} 135 | res)))) 136 | 137 | (deftest create-article--invalid-input--return-422 138 | (let [res (handler/create-article {})] 139 | (is (= {:status 422 140 | :body {:errors {:body ["Invalid request body."]}}} 141 | res)))) 142 | 143 | (deftest create-article--valid-input--return-200 144 | (let [res (handler/create-article {:auth-user (gen/generate (s/gen user-spec/user)) 145 | :params {:article (gen/generate (s/gen article-spec/create-article))}})] 146 | (is (= {:status 200 147 | :body {}} 148 | res)))) 149 | 150 | (deftest update-article--invalid-body--return-422 151 | (let [res (handler/update-article {:params {:slug "this-is-slug"}})] 152 | (is (= {:status 422 153 | :body {:errors {:body ["Invalid request body."]}}} 154 | res)))) 155 | 156 | (deftest update-article--invalid-slug--return-422 157 | (let [res (handler/update-article {:params {:article (gen/generate (s/gen article-spec/update-article))}})] 158 | (is (= {:status 422 159 | :body {:errors {:body ["Invalid request body."]}}} 160 | res)))) 161 | 162 | (deftest update-article--valid-input--return-200 163 | (let [res (handler/update-article {:auth-user (gen/generate (s/gen user-spec/user)) 164 | :params {:slug "this-is-slug" 165 | :article (gen/generate (s/gen article-spec/update-article))}})] 166 | (is (= {:status 200 167 | :body {}} 168 | res)))) 169 | 170 | (deftest delete-article--invalid-input--return-422 171 | (let [res (handler/delete-article {})] 172 | (is (= {:status 422 173 | :body {:errors {:slug ["Invalid slug."]}}} 174 | res)))) 175 | 176 | (deftest delete-article--valid-input--return-200 177 | (let [res (handler/delete-article {:auth-user (gen/generate (s/gen user-spec/user)) 178 | :params {:slug "this-is-slug"}})] 179 | (is (= {:status 200 180 | :body {}} 181 | res)))) 182 | 183 | (deftest favorite-article--invalid-input--return-422 184 | (let [res (handler/favorite-article {})] 185 | (is (= {:status 422 186 | :body {:errors {:slug ["Invalid slug."]}}} 187 | res)))) 188 | 189 | (deftest favorite-article--valid-input--return-200 190 | (let [res (handler/favorite-article {:auth-user (gen/generate (s/gen user-spec/user)) 191 | :params {:slug "this-is-slug"}})] 192 | (is (= {:status 200 193 | :body {}} 194 | res)))) 195 | 196 | (deftest unfavorite-article--invalid-input--return-422 197 | (let [res (handler/unfavorite-article {})] 198 | (is (= {:status 422 199 | :body {:errors {:slug ["Invalid slug."]}}} 200 | res)))) 201 | 202 | (deftest unfavorite-article--valid-input--return-200 203 | (let [res (handler/unfavorite-article {:auth-user (gen/generate (s/gen user-spec/user)) 204 | :params {:slug "this-is-slug"}})] 205 | (is (= {:status 200 206 | :body {}} 207 | res)))) 208 | 209 | (deftest tags--return-200 210 | (let [res (handler/tags {})] 211 | (is (= {:status 200 212 | :body {:tags []}} 213 | res)))) 214 | 215 | (deftest comments--invalid-input--return-422 216 | (let [res (handler/comments {})] 217 | (is (= {:status 422 218 | :body {:errors {:slug ["Invalid slug."]}}} 219 | res)))) 220 | 221 | (deftest comments--valid-input--return-200 222 | (let [res (handler/comments {:auth-user (gen/generate (s/gen user-spec/user)) 223 | :params {:slug "this-is-slug"}})] 224 | (is (= {:status 200 225 | :body {:comments []}} 226 | res)))) 227 | 228 | (deftest delete-comment--invalid-id-string--return-422 229 | (let [res (handler/delete-comment {:id "asd"})] 230 | (is (= {:status 422 231 | :body {:errors {:id ["Invalid comment id."]}}} 232 | res)))) 233 | 234 | (deftest delete-comment--nil-id-string--return-422 235 | (let [res (handler/delete-comment {})] 236 | (is (= {:status 422 237 | :body {:errors {:id ["Invalid comment id."]}}} 238 | res)))) 239 | 240 | (deftest delete-comment--valid-string-input--return-200 241 | (let [res (handler/delete-comment {:auth-user (gen/generate (s/gen user-spec/user)) 242 | :params {:id "1"}})] 243 | (is (= {:status 200 244 | :body {}} 245 | res)))) 246 | 247 | (deftest delete-comment--valid-int-input--return-200 248 | (let [res (handler/delete-comment {:auth-user (gen/generate (s/gen user-spec/user)) 249 | :params {:id 1}})] 250 | (is (= {:status 200 251 | :body {}} 252 | res)))) 253 | 254 | (deftest add-comment--invalid-slug--return-422 255 | (let [res (handler/add-comment {:auth-user (gen/generate (s/gen user-spec/user)) 256 | :params {:comment (gen/generate (s/gen comment-spec/add-comment))}})] 257 | (is (= {:status 422 258 | :body {:errors {:body ["Invalid request body."]}}} 259 | res)))) 260 | 261 | (deftest add-comment--invalid-comment--return-422 262 | (let [res (handler/add-comment {:auth-user (gen/generate (s/gen user-spec/user)) 263 | :params {:slug "this-is-slug"}})] 264 | (is (= {:status 422 265 | :body {:errors {:body ["Invalid request body."]}}} 266 | res)))) 267 | 268 | (deftest add-comment--valid-input--return-200 269 | (let [res (handler/add-comment {:auth-user (gen/generate (s/gen user-spec/user)) 270 | :params {:slug "this-is-slug" 271 | :comment (gen/generate (s/gen comment-spec/add-comment))}})] 272 | (is (= {:status 200 273 | :body {}} 274 | res)))) 275 | 276 | (deftest feed--invalid-limit--return-200 277 | (let [res (handler/feed {:auth-user (gen/generate (s/gen user-spec/user)) 278 | :params {:limit "invalid-limit" 279 | :offset 0}})] 280 | (is (= {:status 200 281 | :body {:limit nil 282 | :offset 0}} 283 | res)))) 284 | 285 | (deftest feed--invalid-offset--return-200 286 | (let [res (handler/feed {:auth-user (gen/generate (s/gen user-spec/user)) 287 | :params {:offset "invalid-offset" 288 | :limit 10}})] 289 | (is (= {:status 200 290 | :body {:limit 10 291 | :offset nil}} 292 | res)))) 293 | 294 | (deftest feed--string-offset--return-200 295 | (let [res (handler/feed {:auth-user (gen/generate (s/gen user-spec/user)) 296 | :params {:offset "5" 297 | :limit 10}})] 298 | (is (= {:status 200 299 | :body {:limit 10 300 | :offset 5}} 301 | res)))) 302 | 303 | (deftest feed--string-limit--return-200 304 | (let [res (handler/feed {:auth-user (gen/generate (s/gen user-spec/user)) 305 | :params {:offset 5 306 | :limit "10"}})] 307 | (is (= {:status 200 308 | :body {:limit 10 309 | :offset 5}} 310 | res)))) 311 | 312 | (deftest feed--valid-input--return-200 313 | (let [res (handler/feed {:auth-user (gen/generate (s/gen user-spec/user)) 314 | :params {:offset 5 315 | :limit 10}})] 316 | (is (= {:status 200 317 | :body {:limit 10 318 | :offset 5}} 319 | res)))) 320 | 321 | (deftest feed--no-limit-and-offset--return-200 322 | (let [res (handler/feed {:auth-user (gen/generate (s/gen user-spec/user)) 323 | :params {}})] 324 | (is (= {:status 200 325 | :body {:limit nil 326 | :offset nil}} 327 | res)))) 328 | 329 | (deftest articles--invalid-limit--return-200 330 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user)) 331 | :params {:limit "invalid-limit" 332 | :offset 0}})] 333 | (is (= {:status 200 334 | :body {:limit nil 335 | :offset 0 336 | :tag nil 337 | :author nil 338 | :favorited nil}} 339 | res)))) 340 | 341 | (deftest articles--invalid-offset--return-200 342 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user)) 343 | :params {:offset "invalid-offset" 344 | :limit 10}})] 345 | (is (= {:status 200 346 | :body {:limit 10 347 | :offset nil 348 | :tag nil 349 | :author nil 350 | :favorited nil}} 351 | res)))) 352 | 353 | (deftest articles--string-offset--return-200 354 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user)) 355 | :params {:offset "5" 356 | :limit 10}})] 357 | (is (= {:status 200 358 | :body {:limit 10 359 | :offset 5 360 | :tag nil 361 | :author nil 362 | :favorited nil}} 363 | res)))) 364 | 365 | (deftest articles--string-limit--return-200 366 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user)) 367 | :params {:offset 5 368 | :limit "10"}})] 369 | (is (= {:status 200 370 | :body {:limit 10 371 | :offset 5 372 | :tag nil 373 | :author nil 374 | :favorited nil}} 375 | res)))) 376 | 377 | (deftest articles--invalid-tag--return-200 378 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user)) 379 | :params {:offset 5 380 | :limit 10 381 | :tag 10}})] 382 | (is (= {:status 200 383 | :body {:limit 10 384 | :offset 5 385 | :tag nil 386 | :author nil 387 | :favorited nil}} 388 | res)))) 389 | 390 | (deftest articles--invalid-author--return-200 391 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user)) 392 | :params {:offset 5 393 | :limit 10 394 | :author 10}})] 395 | (is (= {:status 200 396 | :body {:limit 10 397 | :offset 5 398 | :tag nil 399 | :author nil 400 | :favorited nil}} 401 | res)))) 402 | 403 | (deftest articles--invalid-favorited--return-200 404 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user)) 405 | :params {:offset 5 406 | :limit 10 407 | :favorited 10}})] 408 | (is (= {:status 200 409 | :body {:limit 10 410 | :offset 5 411 | :tag nil 412 | :author nil 413 | :favorited nil}} 414 | res)))) 415 | 416 | (deftest articles--valid-filters--return-200 417 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user)) 418 | :params {:offset 5 419 | :limit 10 420 | :author "author" 421 | :tag "tag" 422 | :favorited "favorited"}})] 423 | (is (= {:status 200 424 | :body {:limit 10 425 | :offset 5 426 | :tag "tag" 427 | :author "author" 428 | :favorited "favorited"}} 429 | res)))) 430 | 431 | (deftest articles--valid-input--return-200 432 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user)) 433 | :params {:offset 5 434 | :limit 10}})] 435 | (is (= {:status 200 436 | :body {:limit 10 437 | :offset 5 438 | :tag nil 439 | :author nil 440 | :favorited nil}} 441 | res)))) 442 | 443 | (deftest articles--no-limit-and-offset--return-200 444 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user)) 445 | :params {}})] 446 | (is (= {:status 200 447 | :body {:limit nil 448 | :offset nil 449 | :tag nil 450 | :author nil 451 | :favorited nil}} 452 | res)))) 453 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ![RealWorld Example App](logo.png) 2 | 3 | A full-fledged [RealWorld](https://github.com/gothinkster/realworld) server (CRUD, auth, advanced patterns, etc) built with [Clojure](https://clojure.org), [Polylith](https://polylith.gitbook.io/), and [Ring](https://github.com/ring-clojure/ring), including CRUD operations, authentication, routing, pagination, and more. 4 | 5 | #### Build Status 6 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/furkan3ayraktar/clojure-polylith-realworld-example-app/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/furkan3ayraktar/clojure-polylith-realworld-example-app/tree/master) 7 | 8 | ## Two quick ways to try this project 9 | 10 | You can try both! 11 | 12 | 1. On your own machine, see below 13 | 2. In your browser using Gitpod, see [Hack on Real World Polylith in your Browser](gitpod.md) 14 | 15 | There are many other ways too. But especially if your experience with Clojure is somewhat limited, the Gitpod variant is definitely an easy and quick one. You will have the development environment up and running in less than two minutes, without installing anything at all. 16 | 17 | ## Start a REPL in VSCode / Calva 18 | 19 | 1. Fork & clone this repo 20 | 2. Open the project in VSCode 21 | * [Install the Calva extension](https://calva.io/getting-started/#install-vs-code-and-calva), if you don't have it installed alreaady. 22 | 3. Press F1 and select `Calva: Start a Project REPL and Connect (aka Jack-In)` 23 | > 24 | 25 | Calva will start the Polylith REPL, connect it to the VSCode, and start the RealWorld server at port 6003 for you 💫 26 | 27 | > Check [.vscode/settings.json](.vscode/settings.json) file to see what Calva does under the hood. 28 | 29 | ### Test it with a RealWorld Frontend 30 | 31 | A sweet way to put the server to some tests is to fork the [re-frame RealWorld frontend by Jacek Schae](https://github.com/jacekschae/conduit) and modify it to run against this server by editing the definition of `api-url` in `src/conduit/events.cljs` to be: 32 | 33 | ```clojure 34 | (def api-url "http://localhost:6003/api") 35 | ``` 36 | 37 | Then start the frontend and open http://localhost:3000/ in a web browser. 38 | 39 | ## Put the `poly` command to your service 40 | 41 | 1. Install the [Polylith tool](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/install) 42 | 2. Get ”at a glance" info about the project: 43 | ```sh 44 | $ poly info 45 | ``` 46 | > For detailed information, see [Workspace Info](#workspace-info) section below. 47 | 48 | > All Polylith commands can be run by starting a poly shell session by running `poly` command without any arguments on your terminal. 49 | 50 | ## Start it in your Clojure REPL 51 | 52 | 1. Fork & clone this repo 53 | 1. Open the project in your favorite Clojure editor, start the project, and connect the REPL. From a development perspective it is a regular `deps.edn` project. Just make sure to include the `:dev` and `:test` aliases, and you should be good. 54 | 1. In the `dev.server` namespace, evaluate: 55 | ```clojure 56 | (start! 6003) 57 | ``` 58 | 59 | Now the Polylith RealWorld backend is up on port 6003! 60 | 61 | ## Table of Contents 62 | 63 | - [Getting Started](#two-quick-ways-to-try-this-project) 64 | - [General Structure](#general-structure) 65 | - [Project](#project) 66 | - [Base](#base) 67 | - [Components](#components) 68 | - [Environment Variables](#environment-variables) 69 | - [Database](#database) 70 | - [Workspace Info](#workspace-info) 71 | - [Check Workspace Integrity](#check-workspace-integrity) 72 | - [Running Tests](#running-tests) 73 | - [Stable Points](#stable-points) 74 | - [Continuous Integration](#continuous-integration) 75 | - [How to create this workspace from scratch](#how-to-create-this-workspace-from-scratch) 76 | - [Steps required to use IntelliJ IDEA / Cursive](#steps-required-to-use-intellij-idea--cursive) 77 | 78 | 79 | ### General Structure 80 | This project is structured according to Polylith Architecture principles. 81 | If you are not familiar with Polylith Architecture, please refer to its [documentation](https://polylith.gitbook.io/polylith) for further and deeper understanding. 82 | 83 | The workspace is the root directory in a Polylith codebase, and is where we work with all our building blocks and projects. 84 | A workspace is usually version controlled in a monorepo, and contains all the building blocks, 85 | supplementary development sources, and projects. The subdirectories of the workspace looks like this: 86 | ``` 87 | ▾ bases 88 | ▸ rest-api 89 | ▾ components 90 | ▸ article 91 | ▸ comment 92 | ▸ database 93 | ▸ env 94 | ▸ log 95 | ▸ profile 96 | ▸ spec 97 | ▸ tag 98 | ▸ user 99 | ▸ development 100 | ▾ projects 101 | ▸ realworld-backend 102 | ``` 103 | 104 | Components are the main building blocks in Polylith. Bases are another kind of building blocks where the difference from components is that they expose a public API to the outside world. Both bases and components are encapsulated blocks of code that can be assembled together into services, libraries or tools. Components communicate to each other through their 'interfaces'. 105 | The base in each project, glue components together via their 'interfaces' and expose the business logic via a public API, in this project's case, a REST API. 106 | 107 | There is only one base and one project in this workspace to keep it simple. The project named 'realworld-backend' bundles the base, components and libraries together. The development project makes it delightful to develop from one single place. You can run a REPL within the development project, start the Ring server for debugging or refactor the components easily by using your favorite IDE. 108 | 109 | The Polylith tool also helps you run the tests incrementally. If you run the `` poly test `` command from the root directory, it will detect changes made since the last stable point in time, and only run tests for the recent changes. [Check out Polylith tool](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/testing) for further information about incremental testing or simply write `` poly help `` to see available commands. 110 | 111 | ##### Project 112 | Projects in the Polylith architecture are configurations for deployable artifacts. 113 | There is only one project in this workspace, which is called `` realworld-backend ``. 114 | Projects are a way to define a base, a set of components and libraries to deliver within a bundle. 115 | Since we only need to deliver one bundle for realworld backend, we have only one project. 116 | 117 | If you look at the directory `` projects/realworld-backend ``, you will see a standard ``deps.edn`` file. 118 | The magic here is the project's `` deps.edn `` file which refers to the sources, resources and tests of actual components and bases. 119 | A project only has it's `` deps.edn `` file to define project specific configuration and external dependencies. 120 | All the code and resources in a project come from the components and the base, which creates the project. 121 | 122 | ##### Base 123 | Bases in Polylith architecture are the building blocks that expose a public API to the outside world and `` rest-api `` is the only base in our workspace. 124 | As hinted in its name, it exposes its functionality via a RESTful API. 125 | In order to achieve this, it uses Ring and [Compojure](https://github.com/weavejester/compojure). 126 | There are four namespaces under the `` src `` directory of `` bases/rest-api ``: 127 | - `` api.clj `` 128 | - `` handler.clj `` 129 | - `` main.clj `` 130 | - `` middleware.clj`` 131 | 132 | The `` api.clj `` namespace contains route definitions for compojure and init function for Ring. The REST API looks like this: 133 | 134 | ![rest-api](.media/readme/01_rest_api.png) 135 | 136 | These routes are defined with compojure with this piece of code: 137 | ```clojure 138 | (defroutes public-routes 139 | (OPTIONS "/**" [] h/options) 140 | (POST "/api/users/login" [] h/login) 141 | (POST "/api/users" [] h/register) 142 | (GET "/api/profiles/:username" [] h/profile) 143 | (GET "/api/articles" [] h/articles) 144 | (GET "/api/articles/:slug" [] h/article) 145 | (GET "/api/articles/:slug/comments" [] h/comments) 146 | (GET "/api/tag" [] h/tags)) 147 | 148 | (defroutes private-routes 149 | (GET "/api/user" [] h/current-user) 150 | (PUT "/api/user" [] h/update-user) 151 | (POST "/api/profiles/:username/follow" [] h/follow-profile) 152 | (DELETE "/api/profiles/:username/follow" [] h/unfollow-profile) 153 | (GET "/api/articles/feed" [] h/feed) 154 | (POST "/api/articles" [] h/create-article) 155 | (PUT "/api/articles/:slug" [] h/update-article) 156 | (DELETE "/api/articles/:slug" [] h/delete-article) 157 | (POST "/api/articles/:slug/comments" [] h/add-comment) 158 | (DELETE "/api/articles/:slug/comments/:id" [] h/delete-comment) 159 | (POST "/api/articles/:slug/favorite" [] h/favorite-article) 160 | (DELETE "/api/articles/:slug/favorite" [] h/unfavorite-article)) 161 | ``` 162 | 163 | The `` middleware.clj `` namespace contains several useful middleware definitions for Ring, 164 | like adding CORS headers, wrapping exceptions and authorization. Middlewares in Ring are functions that are called before or after the execution of your handlers. For example, for authorization we can have a simple middleware like this: 165 | ```clojure 166 | (defn wrap-authorization [handler] 167 | (fn [req] 168 | (if (:auth-user req) 169 | (handler req) 170 | {:status 401 171 | :body {:errors {:authorization "Authorization required."}}}))) 172 | ``` 173 | This middleware will check every request that it wraps and return an authorization error if it can't find `` :auth-user `` in the request. Otherwise, it will execute the handler. 174 | 175 | The `` main.clj `` namespace contains a main function to expose the REST API via a [Jetty](https://www.eclipse.org/jetty/) server. If you look at the project configuration at `` projects/realworld-backend/deps.edn `` you'll notice that there are two aliases named `` :aot `` and `` :uberjar ``. With the help of those two aliases and `` main.clj ``, we can create an uberjar which is a single jar file that can be run directly on any machine that has Java runtime. Once the jar file is run, the main function defined under `` main.clj `` will be triggered and it will start the server. 176 | 177 | Finally, the `` handler.clj `` namespace is the place where we define our handlers. Since `` rest-api `` is the only place where our project exposes its functionality, its handler needs to call functions in different components via their `` interfaces ``. If you check out the `` :require `` statements on top of the namespace, you'll see this: 178 | ```clojure 179 | (ns clojure.realworld.rest-api.handler 180 | (:require [clojure.realworld.article.interface :as article] 181 | [clojure.realworld.comment.interface :as comment-comp] 182 | [clojure.realworld.spec.interface :as spec] 183 | [clojure.realworld.profile.interface :as profile] 184 | [clojure.realworld.tag.interface :as tag] 185 | [clojure.realworld.user.interface :as user] 186 | [clojure.spec.alpha :as s])) 187 | ``` 188 | Following the rules of the Polylith architecture means that `` handler.clj `` doesn't depend on anything except the interfaces of different components. An example handler for profile request can be written like this: 189 | ```clojure 190 | (defn profile [req] 191 | (let [auth-user (-> req :auth-user) 192 | username (-> req :params :username)] 193 | (if (s/valid? spec/username? username) 194 | (let [[ok? res] (profile/profile auth-user username)] 195 | (handler (if ok? 200 404) res)) 196 | (handler 422 {:errors {:username ["Invalid username."]}})))) 197 | ``` 198 | 199 | ##### Components 200 | Components are the main building blocks in a Polylith architecture. In this workspace, there are nine different components. 201 | Let's take a deeper look at one of the interfaces, like `` profile ``. 202 | The interface of the `` profile `` component is split into two different files. One of them contains the function interfaces and the other one contains the exposed specs. 203 | ```clojure 204 | (ns clojure.realworld.profile.interface 205 | (:require [clojure.realworld.profile.core :as core])) 206 | 207 | (defn fetch-profile [auth-user username] 208 | (core/fetch-profile auth-user username)) 209 | 210 | (defn follow! [auth-user username] 211 | (core/follow! auth-user username)) 212 | 213 | (defn unfollow! [auth-user username] 214 | (core/unfollow! auth-user username)) 215 | ``` 216 | 217 | ```clojure 218 | (ns clojure.realworld.profile.interface.spec 219 | (:require [clojure.realworld.profile.spec :as spec])) 220 | 221 | (def profile spec/profile) 222 | ``` 223 | 224 | As you can see, the interfaces are just passing through to the real implemantation encapsulated in the component. 225 | 226 | One example of using these interfaces can be found under `` handler.clj `` namespace of `` rest-api `` base. 227 | ```clojure 228 | (ns clojure.realworld.rest-api.handler 229 | (:require ;;... 230 | [clojure.realworld.profile.interface :as profile] 231 | ;;...)) 232 | 233 | ;;... 234 | 235 | (defn follow-profile [req] 236 | (let [auth-user (-> req :auth-user) 237 | username (-> req :params :username)] 238 | (if (s/valid? spec/username? username) 239 | (let [[ok? res] (profile/follow! auth-user username)] 240 | (handler (if ok? 200 404) res)) 241 | (handler 422 {:errors {:username ["Invalid username."]}})))) 242 | 243 | ;;... 244 | ``` 245 | 246 | The function `` profile/follow! `` is called via the `` profile `` interface, which delegates 247 | to the `follow!` function that lives in the `core` namespace inside the `profile` component: 248 | ```clojure 249 | (defn follow! [auth-user username] 250 | (if-let [user (user/find-by-username-or-id username)] 251 | (do 252 | (store/follow! (:id auth-user) (:id user)) 253 | [true (create-profile user true)]) 254 | [false {:errors {:username ["Cannot find a profile with given username."]}}])) 255 | ``` 256 | Here is another function call to the `` user `` component from `` profile `` component. 257 | This is how the `` user ``s interface looks like: 258 | ```clojure 259 | (ns clojure.realworld.user.interface 260 | (:require [clojure.realworld.user.core :as core] 261 | [clojure.realworld.user.store :as store])) 262 | 263 | (defn login! [login-input] 264 | (core/login! login-input)) 265 | 266 | (defn register! [register-input] 267 | (core/register! register-input)) 268 | 269 | (defn user-by-token [token] 270 | (core/user-by-token token)) 271 | 272 | (defn update-user! [auth-user user-input] 273 | (core/update-user! auth-user user-input)) 274 | 275 | (defn find-by-username-or-id [username-or-id] 276 | (store/find-by-username-or-id username-or-id)) 277 | ``` 278 | `` profile `` uses `` find-by-username-or-id `` function from `` user `` component. This is how different components talk to each other within the workspace. 279 | It's only possible to call component functions via their `` interface.clj ``. 280 | 281 | In the code example above, we can see that the interface functions redirect each function call to an actual implementation inside the component. 282 | By having an interface and an implementation of that interface, it is easy to compile/test/build (as well as develop) components in isolation. 283 | This separation gives it ability to detect/test/build only changed parts of the workspace. 284 | It also gives the developer a better development experience locally, with support for IDE refactoring via the development project. 285 | You can read more about interfaces and their benefits [here](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/interface). 286 | 287 | `` article ``, `` comment ``, `` profile ``, `` tag ``, and `` user `` components define functionality to endpoints required for the RealWorld backend. 288 | The other components, `` database ``, `` env ``, `` spec `` and `` log ``, are created to encapsulate some other common code in the workspace. 289 | `` spec `` component contains some basic spec definitions that are used in different components. 290 | 291 | Similarly, the `` log `` component creates a wrapper around the logging library [timbre](https://github.com/ptaoussanis/timbre). 292 | This is included in the workspace to demonstrate how to create wrapper components around external libraries. 293 | This gives you an opportunity to declare your own interface for an external library and if you decide to use another external library, 294 | you can just switch to another component implementing the same interface without affecting other components. 295 | 296 | The `` database `` component is another type of common functionality component. It contains schema definitions for the sqlite database and functions to apply that schema. If you check Ring initializer function in `` api.clj `` namespace of `` rest-api `` base, you'll see this: 297 | ```clojure 298 | (defn init [] 299 | (try 300 | (log/init) 301 | (let [db (database/db)] 302 | (if (database/valid-schema? db) 303 | (log/info "Database schema is valid.") 304 | (if (database/db-exists?) 305 | (log/warn "Please fix database schema and restart") 306 | (do 307 | (log/info "Generating database.") 308 | (database/generate-db db) 309 | (log/info "Database generated."))))) 310 | (log/info "Initialized server.") 311 | (catch Exception e 312 | (log/error e "Could not start server.")))) 313 | ``` 314 | Here, we use helper functions from the `` database `` component's `` interface.clj `` to check if an sqlite database exists in the current path and if it exists, to check the validity of the schema. 315 | The interface for the `` database `` component looks like this: 316 | ```clojure 317 | (ns clojure.realworld.database.interface 318 | (:require [clojure.realworld.database.core :as core] 319 | [clojure.realworld.database.schema :as schema])) 320 | 321 | (defn db 322 | ([path] 323 | (core/db path)) 324 | ([] 325 | (core/db))) 326 | 327 | (defn db-exists? [] 328 | (core/db-exists?)) 329 | 330 | (defn generate-db [db] 331 | (schema/generate-db db)) 332 | 333 | (defn drop-db [db] 334 | (schema/drop-db db)) 335 | 336 | (defn valid-schema? [db] 337 | (schema/valid-schema? db)) 338 | ``` 339 | 340 | ### Environment Variables 341 | The following environment variables are used in the project. 342 | You can define these variables under the `env.edn` file for local development. 343 | 344 | + `` :allowed-origins `` 345 | + Comma separated string of origins. Used to whitelist origins for CORS. 346 | + `` :environment `` 347 | + Defines current environment. Currently used for logging. If set to LOCAL, logs printed to console. 348 | + `` :database `` 349 | + Defaults to database.db. If provided, it will be the name of the file that contains the SQLite database. 350 | + `` :secret `` 351 | + Secret for JWT token. 352 | 353 | ### Database 354 | The project uses an SQLite database to make it easy to run. 355 | It can easily be changed to another SQL database, by editing the database connection and changing to a real jdbc dependency. 356 | There is an existing database under the development project, ready to be used. If you want to start from scratch, you can delete `database.db and start the server again. 357 | It will generate a database with correct schema on start. The project also checks if the schema is valid or not, and prints out proper logs for each case. 358 | 359 | ### Workspace info 360 | Run the following command from the root directory to print out workspace information and changes since the last stable point in time: 361 | `` poly info `` 362 | 363 | This command will print an output like below. Here you can see that changed components are marked with a * symbol. 364 | Refer to the [Polylith tool documentation](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/reference/commands#info) for more detailed information about this command and other commands that Polylith provides. 365 | 366 | 367 | 368 | ### Check workspace integrity 369 | In order to guarantee workspace integrity, which means all components refer to each other through their interfaces. 370 | The Polylith tool provides you with the `` poly check `` command that will check the entire workspace and print out errors and/or warnings, if any. 371 | 372 | ### Running tests 373 | Run the following command from the root directory: 374 | `` poly test `` 375 | 376 | This command will run all the tests for changed components and other components that are affected by the current changes. 377 | You can read more about the test command [here](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/reference/commands#test) and [here](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/testing). 378 | 379 | ### Stable points in time 380 | Once you check the integrity of your workspace and see that all tests are green, you can commit your changes to your git repository and add (or move if there is one already) a git tag that starts with ``stable-`` prefix. 381 | The Polylith tool with use this point in time to to calculate what changes has been made. 382 | You can easily add this logic to your continuous integration pipeline as a way to automate it. 383 | Read more about stable points [here](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/tagging) where you can find 384 | an example of how to implement the stable logic with the CI in the section below. 385 | 386 | ### Continuous integration 387 | This repository has a [CircleCI](https://circleci.com) configuration to demonstrate how to use the Polylith tool to incrementally run tests and build artifacts. 388 | The CircleCI configuration file is located at `` .circleci/config.yml ``. 389 | 390 | The CircleCI workflow for this project consists of six steps to demonstrate different commands from the Polylith tool. 391 | You can achieve the same result with fewer steps once you have learned the commands. The current steps are: 392 | 393 | - check 394 | - This job runs the check command from Polylith as follows: ```clojure -M:poly check```. If there are any errors in the Polylith workspace, it returns with a non-zero exit code and the CircleCI workflow stops at this stage. 395 | If there are any warnings printed by Polylith, it will be visible in the job's output. 396 | - info 397 | - Prints useful information about the current state of the workspace. This job runs the following commands, one after another: 398 | - ```clojure -M:poly ws``` 399 | - Prints the current workspace as data in [edn format](https://github.com/edn-format/edn). 400 | - ```clojure -M:poly info``` 401 | - Prints workspace information. 402 | - ```clojure -M:poly deps``` 403 | - Prints the dependency information 404 | - ```clojure -M:poly libs``` 405 | - Prints all libraries that are used in the workspace. 406 | - After this job is done, all this information will be available in the jobs output for debugging purposes if needed. You can read more about available commands [here](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/reference/commands). 407 | - test 408 | - This job runs all the tests for all the bricks and projects that are directly or indirectly changed since the last stable point in time. 409 | Polylith supports incremental testing out of the box by using stable point marks in the git history. 410 | It runs the following command: ```clojure -M:poly test :project```. 411 | If any of the tests fail, it will exit with a non-zero exit code and the CircleCI workflow stops at this stage. 412 | Information about the passed/failed tests will be printed in the job's output. 413 | - api-test 414 | - Runs end-to-end API tests using a [Postman](https://www.postman.com) collection defined under the `` api-tests `` directory. 415 | Before running the tests, start the backend service by executing the `` clojure -M:ring `` statement under `` projects/realworld-backend `` directory. 416 | - build-uberjar 417 | - This job creates an aot compiled uberjar for the realworld-backend project. Created artifact can be found in the artifacts section of this job's output. 418 | - mark-as-stable 419 | - This job only runs for the commits made to the master branch. 420 | It adds (or moves if there is already one) the `stable-master` tag to the repository. 421 | At this point in the workflow, it is proven that the Polylith workspace is valid and that all the tests have passed. 422 | It is safe to mark this commit as stable. It does that by running the following commands one after another: 423 | - ```git tag -f -a "stable-$CIRCLE_BRANCH" -m "[skip ci] Added Stable Polylith tag"``` 424 | - Creates or moves the tag 425 | - ```git push origin $CIRCLE_BRANCH --tags --force``` 426 | - Pushed tag back to the git repository 427 | 428 | ### How to create this workspace from scratch 429 | You can find necessary steps to create this workspace with Polylith plugin [here](how-to.md). 430 | 431 | ### Steps required to use IntelliJ IDEA / Cursive 432 | You can find necessary steps to make this project work in [Intellij IDEA](https://www.jetbrains.com/idea/) / [Cursive](https://cursive-ide.com) plugin [here](https://cursive-ide.com/userguide/polylith.html). 433 | 434 | ### Note about deps.edn vs Leiningen 435 | 436 | > This version uses [tools.deps](https://github.com/clojure/tools.deps). There is also an older version of this project that uses [Leiningen](https://leiningen.org/) on the [leiningen branch](https://github.com/furkan3ayraktar/clojure-polylith-realworld-example-app/tree/leiningen). 437 | 438 | ## License 439 | 440 | Distributed under the [The MIT License](https://opensource.org/licenses/MIT), the same as [RealWorld](https://github.com/gothinkster/realworld) project. 441 | --------------------------------------------------------------------------------