Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: realworld-example #12

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{:hooks
{:analyze-call
{next.jdbc/with-transaction
hooks.com.github.seancorfield.next-jdbc/with-transaction}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
(ns hooks.com.github.seancorfield.next-jdbc
(:require [clj-kondo.hooks-api :as api]))

(defn with-transaction
"Expands (with-transaction [tx expr opts] body)
to (let [tx expr] opts body) pre clj-kondo examples."
[{:keys [:node]}]
(let [[binding-vec & body] (rest (:children node))
[sym val opts] (:children binding-vec)]
(when-not (and sym val)
(throw (ex-info "No sym and val provided" {})))
(let [new-node (api/list-node
(list*
(api/token-node 'let)
(api/vector-node [sym val])
opts
body))]
{:node new-node})))
27 changes: 27 additions & 0 deletions examples/realworld/dev/user.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
(ns user
(:require [clj-dev.core :as d]
[potemkin :as p]))

(p/import-vars
[d start pause suspend resume stop restart watch system config go halt reset reset-all])
(d/init
{;; By default only watch and namespace reload and refresh works
;; Paths to target for refresh & tests
:paths ["src" "test" "dev" "resources"]
;; Whether to auto-start i.e. when calling clj-dev/init, call clj-dev/start
:start-on-init? false

;; File patterns to trigger reload on.
:watch-pattern #"[^.].*(\.clj|\.edn)$"
;; time stamp format, set to nil if you don't want have timestamp.
:watch-timestamp "[hh:mm:ss]"

;; Integrant file configuration path within :paths.
:integrant-file-path "conduit/config.edn" ;; string
;; integrant profiles.
:integrant-profiles [:duct.profile/dev :duct.profile/local] ;; vector
;; Whether duct framework should be considered.
:integrant-with-duct? true})



16 changes: 16 additions & 0 deletions examples/realworld/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: '3'

# 1 service = 1 container. For example, a service, a server, a client, a database...
services:
database:
image: 'postgres:latest'
ports:
- 5432:5432
# volumes:
# - type: bind
# source: ./resources/db/sql/
# target: /docker-entrypoint-initdb.d
environment:
POSTGRES_DB: tami # Main Database
POSTGRES_USER: postgres # The PostgreSQL user (useful to connect to the database)
POSTGRES_PASSWORD: password # The PostgreSQL password (useful to connect to the database)
35 changes: 35 additions & 0 deletions examples/realworld/project.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
(defproject conduit-example "0.1.0-SNAPSHOT"
:description "Implementation of https://github.com/gothinkster/realworld"
:url "https://github.com/tami5/clj-duct-reitit/blob/master/realworld"
:license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.10.0"]
[org.clojure/core.async "1.5.648"]

;; Database
[duct/module.sql "0.6.1" :exceptions [hikari-cp duct/database.sql.hikaricp]]
[duct/database.sql.hikaricp "0.4.1-SNAPTSHOT"]
[hikari-cp "2.13.0"]
[org.postgresql/postgresql "42.3.1"]
[com.github.seancorfield/next.jdbc "1.2.761"]
[com.github.seancorfield/honeysql "2.2.840"]

;; Logging
[duct/module.logging "0.5.0"]
[log4j/log4j "1.2.17" :exclusions [javax.mail/mail javax.jms/jms com.sun.jmdk/jmxtools com.sun.jmx/jmxri]]
[org.slf4j/slf4j-log4j12 "1.7.1"]
[com.taoensso/timbre "5.1.2"]

[duct/module.reitit "0.3.1-SNAPSHOT"]
[duct/middleware.buddy "0.2.0"]
[buddy/buddy-hashers "1.8.1"]
[metosin/malli "0.7.5"]]

:profiles {:repl {:source-paths ["dev"]
:prep-tasks ^:replace ["javac" "compile"]}
:uberjar {:aot :all}
:dev {:dependencies [[org.xerial/sqlite-jdbc "3.36.0.3"]]}}

:plugins [[duct/lein-duct "0.12.3"]
[cider/cider-nrepl "0.27.3"]]
:resource-paths ["resources" "target/resources"]
:prep-tasks ["javac" "compile" ["run" ":duct/compiler"]])
55 changes: 55 additions & 0 deletions examples/realworld/resources/conduit/config.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{; Modules (Configuration Transformers :D)
:duct.module/reitit {}
:duct.module/sql {}
:duct.module/logging {}

; Base System Configuration
:duct.profile/base
{:duct.core/project-ns conduit

:duct.reitit/routes
[["/api/users"
{:post {:handler user/register
:parameters {:body :request.user/register}}}]

["/api/users/login"
{:post {:handler user/login
:parameters {:body :request.user/login}}}]]

:duct.reitit/environment
{:db #ig/ref :duct.database.sql/hikaricp
:jwt-secret "SECRET:D"}

:duct.reitit/coercion {:enable true :coercer malli}
:duct.reitit/muuntaja false

:duct.migrator/ragtime
{:migrations #ig/ref :duct.migrator.ragtime/resources
:logger #ig/ref :duct/logger}

:duct.migrator.ragtime/resources
{:path "conduit/migrations"}}

; Development Environment Changes
:duct.profile/dev
{:duct.database.sql/hikaricp
{:jdbc-url "jdbc:postgresql://localhost/postgres?user=postgres&password=password"
:logger #ig/ref :duct/logger}
:duct.logger/timbre
{:level :info
:appenders {:duct.logger.timbre/brief #ig/ref :duct.logger.timbre/brief}}

:duct.reitit/logging
{:level :report
:enable true
:logger #ig/ref :duct/logger
:exceptions? true
:pretty? false
:coercions? false
:requests? true}

:duct.logger.timbre/brief {}}

; Production Environment Changes
:duct.profile/prod {}}

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS users
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE,
username TEXT UNIQUE,
password TEXT,
bio TEXT,
image TEXT
)
22 changes: 22 additions & 0 deletions examples/realworld/src/conduit/db.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
(ns conduit.db
(:require [duct.database.sql]
[buddy.hashers :as hashers]
[honey.sql :as sql]
[next.jdbc :as jdbc]
[next.jdbc.result-set :as rs]
[duct.reitit.util :refer [spy]]))

(def ^:private default-options
{:return-keys true
:builder-fn rs/as-unqualified-kebab-maps})

(defn execute-one! [sql-map {{:keys [datasource]} :spec} & [options]]
(jdbc/execute-one! datasource
(sql/format sql-map)
(merge default-options options)))

(defn execute! [sql-map {{:keys [datasource]} :spec} & [options]]
(jdbc/execute! datasource
(sql/format sql-map)
(merge default-options options)))

23 changes: 23 additions & 0 deletions examples/realworld/src/conduit/db/user.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
(ns conduit.db.user
(:require [duct.database.sql]
[buddy.hashers :as hashers]
[conduit.db :refer [execute-one!]]))

(defprotocol User
(register [spec user])
(login [spec user]))

(extend-protocol User
duct.database.sql.Boundary

(register [spec user]
(let [keys [:username :email :bio :image :password]
user (update (select-keys user keys) :password hashers/encrypt)
query {:insert-into :users :values [user]}]
(execute-one! query spec)))

(login [spec {:keys [email password]}]
(let [query {:select [:*] :from [:users] :where [:= :email email]}
user (execute-one! query spec)]
(when (and user (hashers/check password (:password user)))
(dissoc user :password)))))
22 changes: 22 additions & 0 deletions examples/realworld/src/conduit/handler/user.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
(ns conduit.handler.user
(:require [conduit.db.user :as user]
[ring.util.response :as rs]
[buddy.sign.jwt :as jwt]
[clj-dev.utils :refer [spy]]))

(defn ^:private wrap-with-token [user jwt-secret]
(->> (jwt/sign {:user-id (:id user)} jwt-secret)
(assoc user :token)
(hash-map :user)))

(defn login [{:keys [db jwt-secret parameters]}]
(if-let [user (user/login db (get-in parameters [:body :user]))]
(rs/response (wrap-with-token user jwt-secret))
(rs/bad-request "Wrong email or password.")))

(defn register [{:keys [db jwt-secret parameters]}]
(try (-> (user/register db (get-in parameters [:body :user]))
(wrap-with-token jwt-secret)
(rs/response))
(catch Exception e
(rs/bad-request (ex-message e)))))
13 changes: 13 additions & 0 deletions examples/realworld/src/conduit/main.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
(ns conduit.main
(:gen-class)
(:require [duct.core :as duct]))

(duct/load-hierarchy)

(defn -main [& args]
(let [keys (or (duct/parse-keys args) [:duct/daemon])
profiles [:duct.profile/prod]]
(-> (duct/resource "conduit/config.edn")
(duct/read-config)
(duct/exec-config profiles keys))
(System/exit 0)))
83 changes: 83 additions & 0 deletions examples/realworld/src/conduit/spec.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
(ns conduit.spec
(:require [conduit.spec.util :refer [schemas-from-feilds optional-keys closed-schema set-malli-registry!]]))

(def ^:private fields
{;; User Schema
:user/username :string
:user/email :string
:user/password :string
:user/bio :string
:user/token :string
:user/image :string
;; Profile Schema
:profile/bio :user/bio
:profile/following :boolean
:profile/username :user/username
:profile/image :user/image
;; Article Schema
:article/slug :string
:article/title :string
:article/description :string
:article/body :string
:article/author :schema/profile
:article/tag-list [:vector :string]
:article/created-at :string
:article/updated-at :string
:article/favorited :boolean
:article/favorites-count :int
;; Comment Schema
:comment/id :int
:comment/author :schema/profile
:comment/body :string
:comment/created-at :string
:comment/updated-at :string})

(def ^:private actions
{; User-Specific actions fields
:actions.user/login [:map
[:email :user/email]
[:password :user/password]]
:actions.user/register [:map
[:email :user/email]
[:username :user/username]
[:password :user/password]]
; Article-Specific actions fields
:actions.article/create [:map
[:body :article/body]
[:description :article/description]
[:title :article/title]
[:tag-list {:optional true} :article/tag-list]]
:actions.article/update [:map
[:title {:optional true} :article/title]
[:body {:optional true} :article/body]
[:description {:optional true} :article/description]]
; Comment-Specific actions fields
:actions.comment/create [:map [:body :comment/body]]})

(def ^:private requests
{; User-Specific Requests
:request.user/register [:map [:user :actions.user/register]]
:request.user/update [:map [:user :schema/user]]
:request.user/login [:map [:user :actions.user/login]]
; Article-Specific Requests
:request.article/create [:map [:article :actions.article/create]]
:request.article/update [:map [:article :actions.article/update]]
; Comment-Specific Requests
:request.comment/create [:map [:comment :actions.comment/create]]})

(def ^:private responses
#:response{:article [:map [:article :schema/article]]
:articles [:map [:articles [:vec :schema/article] [:count :int]]]
:user [:map [:user :schema/user]]
:comment [:map [:comment :schema/comment]]
:comments [:map [:comments [:vec :schema/comment]]]
:tags [:map [:tags [:vec :string]]]})

(def ^:private schema
(let [{:keys [comment article profile user]} (schemas-from-feilds fields)]
#:schema{:user (-> user optional-keys closed-schema)
:comment (-> comment optional-keys closed-schema)
:article (-> article optional-keys closed-schema)
:profile (-> profile optional-keys closed-schema)}))

(set-malli-registry! fields schema actions requests responses)
41 changes: 41 additions & 0 deletions examples/realworld/src/conduit/spec/util.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
(ns conduit.spec.util
(:require [malli.registry :as mr]
[malli.core :as m]))

(defn- inherit-to-type
"Take a map of keys and spec and returns malli schema
wrapped with 'type and where each memeber has unqualifed key and the actual
key."
[schema mu-type]
(->> schema
(mapv (fn [[k _]] [(keyword (name k)) k]))
(cons mu-type)
(vec)))

(defn schemas-from-feilds
"Given a namespace and qualified map of fields
Return <ns>/<schema> and malli map"
[fields]
(let [organize-by-namespace #(assoc-in %1 [(keyword (namespace %2)) %2] %3)
transfrom-to-malli #(assoc %1 %2 (inherit-to-type %3 :map))]
(->> fields
(reduce-kv organize-by-namespace {})
(reduce-kv transfrom-to-malli {}))))

(defn optional-keys
"Same as malli.util/optional-keys but without checking if keys are valid.
Return the malli map with all keys are optional."
[m]
(mapv #(if (vector? %)
(vec (concat [(first %)] [{:optional true}] (rest %)))
%) m))

(defn closed-schema
"Same as malli.util/closed-schema but non-recursive and without checking if
keys are valid. Return the closed malli map "
[m]
(vec (concat [(first m)] [{:closed true}] (rest m))))

(defn set-malli-registry! [& additions]
(mr/set-default-registry!
(apply merge (cons (m/default-schemas) additions))))
12 changes: 12 additions & 0 deletions examples/realworld/src/log4j.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
log4j.rootLogger=ERROR, stdout

# log4j.appender.R=org.apache.log4j.RollingFileAppender
# log4j.appender.R.layout=org.apache.log4j.PatternLayout
# log4j.appender.R.layout.ConversionPattern=[%d][%p][%c] %m%n
# log4j.appender.R.File=./log/logger1.log
# log4j.appender.R.MaxFileSize=100KB
# log4j.appender.R.MaxBackupIndex=20

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.SimpleLayout
log4j.appender.stdout.Target=System.out
Loading