Initial Kit project with PostgreSQL, HTMX, Hiccup, and Tailwind
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
|||||||
|
kit.git-config.edn
|
||||||
|
.DS_Store
|
||||||
|
.nrepl-port
|
||||||
|
hs_err_pid*.log
|
||||||
|
.cpcache/
|
||||||
|
target
|
||||||
|
log/
|
||||||
|
.shadow-cljs/
|
||||||
|
node_modules/
|
||||||
|
modules/
|
||||||
|
resources/public/css/output.css
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
*.iml
|
||||||
|
.idea/
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# clj-kondo
|
||||||
|
.clj-kondo/.cache/
|
||||||
|
.calva/output-window/
|
||||||
|
.lsp/.cache/
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# syntax = docker/dockerfile:1.2
|
||||||
|
FROM clojure:openjdk-17 AS build
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
COPY . /
|
||||||
|
|
||||||
|
RUN clj -Sforce -T:build all
|
||||||
|
|
||||||
|
FROM azul/zulu-openjdk-alpine:17
|
||||||
|
|
||||||
|
COPY --from=build /target/elprice-standalone.jar /elprice/elprice-standalone.jar
|
||||||
|
|
||||||
|
EXPOSE $PORT
|
||||||
|
|
||||||
|
ENTRYPOINT exec java $JAVA_OPTS -jar /elprice/elprice-standalone.jar
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
clean:
|
||||||
|
rm -rf target
|
||||||
|
|
||||||
|
run:
|
||||||
|
clj -M:dev
|
||||||
|
|
||||||
|
repl:
|
||||||
|
clj -M:dev:nrepl
|
||||||
|
|
||||||
|
test:
|
||||||
|
clj -M:test
|
||||||
|
|
||||||
|
uberjar:
|
||||||
|
clj -T:build all
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# elprice
|
||||||
|
|
||||||
|
Start a [REPL](#repls) in your editor or terminal of choice.
|
||||||
|
|
||||||
|
Start the server with:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(go)
|
||||||
|
```
|
||||||
|
|
||||||
|
The default API is available under http://localhost:3000/api
|
||||||
|
|
||||||
|
System configuration is available under `resources/system.edn`.
|
||||||
|
|
||||||
|
To reload changes:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(reset)
|
||||||
|
```
|
||||||
|
|
||||||
|
## REPLs
|
||||||
|
|
||||||
|
### Cursive
|
||||||
|
|
||||||
|
Configure a [REPL following the Cursive documentation](https://cursive-ide.com/userguide/repl.html). Using the default "Run with IntelliJ project classpath" option will let you select an alias from the ["Clojure deps" aliases selection](https://cursive-ide.com/userguide/deps.html#refreshing-deps-dependencies).
|
||||||
|
|
||||||
|
### CIDER
|
||||||
|
|
||||||
|
Use the `cider` alias for CIDER nREPL support (run `clj -M:dev:cider`). See the [CIDER docs](https://docs.cider.mx/cider/basics/up_and_running.html) for more help.
|
||||||
|
|
||||||
|
Note that this alias runs nREPL during development. To run nREPL in production (typically when the system starts), use the kit-nrepl library through the +nrepl profile as described in [the documentation](https://kit-clj.github.io/docs/profiles.html#profiles).
|
||||||
|
|
||||||
|
### Command Line
|
||||||
|
|
||||||
|
Run `clj -M:dev:nrepl` or `make repl`.
|
||||||
|
|
||||||
|
Note that, just like with [CIDER](#cider), this alias runs nREPL during development. To run nREPL in production (typically when the system starts), use the kit-nrepl library through the +nrepl profile as described in [the documentation](https://kit-clj.github.io/docs/profiles.html#profiles).
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{:min-bb-version "0.8.156"
|
||||||
|
:deps {failjure/failjure {:mvn/version "2.3.0"}}
|
||||||
|
:tasks {:requires ([babashka.fs :as fs]
|
||||||
|
[babashka.tasks :refer [shell]])
|
||||||
|
|
||||||
|
run {:doc "starts the app"
|
||||||
|
:task (if (fs/windows?)
|
||||||
|
(clojure {:dir "."} "-M:dev")
|
||||||
|
(shell {:dir "."} "clj -M:dev"))}
|
||||||
|
|
||||||
|
nrepl {:doc "starts the nREPL"
|
||||||
|
:task (clojure {:dir "."} "-M:dev:nrepl")}
|
||||||
|
|
||||||
|
cider {:doc "starts the cider"
|
||||||
|
:task (clojure {:dir "."} "-M:dev:cider")}
|
||||||
|
|
||||||
|
test {:doc "runs tests"
|
||||||
|
:task (clojure {:dir "."} "-M:test")}
|
||||||
|
|
||||||
|
uberjar {:doc "builds the uberjar"
|
||||||
|
:task (clojure {:dir "."} "-T:build all")}
|
||||||
|
|
||||||
|
format {:doc "Formats codebase"
|
||||||
|
:task (try
|
||||||
|
(shell {:dir "src"} "cljstyle fix")
|
||||||
|
(catch Exception _
|
||||||
|
(clojure {:dir "src"} "-M:dev -m cljstyle.main fix")))}}}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
(ns build
|
||||||
|
(:require [clojure.string :as string]
|
||||||
|
[clojure.tools.build.api :as b]))
|
||||||
|
|
||||||
|
(def lib 'pmagnus/elprice)
|
||||||
|
(def main-cls (string/join "." (filter some? [(namespace lib) (name lib) "core"])))
|
||||||
|
(def version (format "0.0.1-SNAPSHOT"))
|
||||||
|
(def target-dir "target")
|
||||||
|
(def class-dir (str target-dir "/" "classes"))
|
||||||
|
(def uber-file (format "%s/%s-standalone.jar" target-dir (name lib)))
|
||||||
|
(def basis (b/create-basis {:project "deps.edn"}))
|
||||||
|
|
||||||
|
(defn clean
|
||||||
|
"Delete the build target directory"
|
||||||
|
[_]
|
||||||
|
(println (str "Cleaning " target-dir))
|
||||||
|
(b/delete {:path target-dir}))
|
||||||
|
|
||||||
|
(defn prep [_]
|
||||||
|
(println "Writing Pom...")
|
||||||
|
(b/write-pom {:class-dir class-dir
|
||||||
|
:lib lib
|
||||||
|
:version version
|
||||||
|
:basis basis
|
||||||
|
:src-dirs ["src/clj"]})
|
||||||
|
(b/copy-dir {:src-dirs ["src/clj" "resources" "env/prod/resources" "env/prod/clj"]
|
||||||
|
:target-dir class-dir}))
|
||||||
|
|
||||||
|
(defn uber [_]
|
||||||
|
(println "Compiling Clojure...")
|
||||||
|
(b/compile-clj {:basis basis
|
||||||
|
:src-dirs ["src/clj" "resources" "env/prod/resources" "env/prod/clj"]
|
||||||
|
:class-dir class-dir})
|
||||||
|
(println "Making uberjar...")
|
||||||
|
(b/uber {:class-dir class-dir
|
||||||
|
:uber-file uber-file
|
||||||
|
:main main-cls
|
||||||
|
:basis basis}))
|
||||||
|
|
||||||
|
(defn all [_]
|
||||||
|
(do (clean nil) (prep nil) (uber nil)))
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{:paths ["src/clj"
|
||||||
|
"resources"]
|
||||||
|
|
||||||
|
:deps {org.clojure/clojure {:mvn/version "1.12.3"}
|
||||||
|
|
||||||
|
;; Routing
|
||||||
|
metosin/reitit {:mvn/version "0.9.2"}
|
||||||
|
|
||||||
|
;; Ring
|
||||||
|
metosin/ring-http-response {:mvn/version "0.9.5"}
|
||||||
|
ring/ring-core {:mvn/version "1.15.3"}
|
||||||
|
ring/ring-defaults {:mvn/version "0.7.0"}
|
||||||
|
|
||||||
|
;; Logging
|
||||||
|
ch.qos.logback/logback-classic {:mvn/version "1.5.20"}
|
||||||
|
|
||||||
|
;; Data coercion
|
||||||
|
luminus-transit/luminus-transit {:mvn/version "0.1.6"
|
||||||
|
:exclusions [com.cognitect/transit-clj]}
|
||||||
|
metosin/muuntaja {:mvn/version "0.6.11"}
|
||||||
|
|
||||||
|
;; kit Libs
|
||||||
|
io.github.kit-clj/kit-core {:mvn/version "1.0.9"}
|
||||||
|
io.github.kit-clj/kit-undertow {:mvn/version "1.0.10"}
|
||||||
|
io.github.kit-clj/kit-postgres {:mvn/version "1.0.7"}
|
||||||
|
io.github.kit-clj/kit-sql-conman {:mvn/version "1.10.5"}
|
||||||
|
io.github.kit-clj/kit-sql-migratus {:mvn/version "1.0.5"}
|
||||||
|
hiccup/hiccup {:mvn/version "2.0.0"}}
|
||||||
|
:aliases {:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.11"}}
|
||||||
|
:ns-default build}
|
||||||
|
|
||||||
|
|
||||||
|
:dev {:extra-deps {com.lambdaisland/classpath {:mvn/version "0.6.58"}
|
||||||
|
criterium/criterium {:mvn/version "0.4.6"}
|
||||||
|
expound/expound {:mvn/version "0.9.0"}
|
||||||
|
integrant/repl {:mvn/version "0.5.0"}
|
||||||
|
mvxcvi/cljstyle {:mvn/version "0.17.642"}
|
||||||
|
pjstadig/humane-test-output {:mvn/version "0.11.0"}
|
||||||
|
ring/ring-devel {:mvn/version "1.15.3"}
|
||||||
|
ring/ring-mock {:mvn/version "0.6.2"}
|
||||||
|
io.github.kit-clj/kit-generator {:mvn/version "0.2.5"}
|
||||||
|
org.clojure/tools.namespace {:mvn/version "1.5.0"}
|
||||||
|
}
|
||||||
|
:extra-paths ["env/dev/clj" "env/dev/resources" "test/clj"]}
|
||||||
|
:nrepl {:extra-deps {nrepl/nrepl {:mvn/version "1.5.1"}}
|
||||||
|
:main-opts ["-m" "nrepl.cmdline" "-i"]}
|
||||||
|
:cider {:extra-deps {nrepl/nrepl {:mvn/version "1.5.1"}
|
||||||
|
cider/cider-nrepl {:mvn/version "0.58.0"}}
|
||||||
|
:main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]" "-i"]}
|
||||||
|
|
||||||
|
:test {:extra-deps {criterium/criterium {:mvn/version "0.4.6"}
|
||||||
|
expound/expound {:mvn/version "0.9.0"}
|
||||||
|
integrant/repl {:mvn/version "0.5.0"}
|
||||||
|
io.github.cognitect-labs/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git"
|
||||||
|
:git/tag "v0.5.1"
|
||||||
|
:git/sha "dfb30dd"}
|
||||||
|
pjstadig/humane-test-output {:mvn/version "0.11.0"}
|
||||||
|
ring/ring-devel {:mvn/version "1.15.3"}
|
||||||
|
ring/ring-mock {:mvn/version "0.6.2"}
|
||||||
|
io.github.kit-clj/kit-generator {:mvn/version "0.2.5"}
|
||||||
|
org.clojure/tools.namespace {:mvn/version "1.5.0"}
|
||||||
|
peridot/peridot {:mvn/version "0.5.4"}
|
||||||
|
org.clj-commons/byte-streams {:mvn/version "0.3.4"}
|
||||||
|
com.lambdaisland/classpath {:mvn/version "0.6.58"}}
|
||||||
|
:exec-fn cognitect.test-runner.api/test
|
||||||
|
:extra-paths ["env/dev/clj" "env/dev/resources" "env/test/resources" "test/clj"]
|
||||||
|
:main-opts ["-e" "(require 'pjstadig.humane-test-output) (pjstadig.humane-test-output/activate!)"
|
||||||
|
"-m" "cognitect.test-runner"]}}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: elprice
|
||||||
|
POSTGRES_USER: elprice
|
||||||
|
POSTGRES_PASSWORD: elprice
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
(ns pmagnus.elprice.dev-middleware)
|
||||||
|
|
||||||
|
(defn wrap-dev [handler _opts]
|
||||||
|
(-> handler
|
||||||
|
))
|
||||||
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
(ns pmagnus.elprice.env
|
||||||
|
(:require
|
||||||
|
[clojure.tools.logging :as log]
|
||||||
|
[pmagnus.elprice.dev-middleware :refer [wrap-dev]]))
|
||||||
|
|
||||||
|
(def defaults
|
||||||
|
{:init (fn []
|
||||||
|
(log/info "\n-=[elprice starting using the development or test profile]=-"))
|
||||||
|
:start (fn []
|
||||||
|
(log/info "\n-=[elprice started successfully using the development or test profile]=-"))
|
||||||
|
:stop (fn []
|
||||||
|
(log/info "\n-=[elprice has shut down successfully]=-"))
|
||||||
|
:middleware wrap-dev
|
||||||
|
:opts {:profile :dev}})
|
||||||
Vendored
+59
@@ -0,0 +1,59 @@
|
|||||||
|
(ns user
|
||||||
|
"Userspace functions you can run by default in your local REPL."
|
||||||
|
(:require
|
||||||
|
[clojure.pprint]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[clojure.tools.namespace.repl :as repl]
|
||||||
|
[criterium.core :as c] ;; benchmarking
|
||||||
|
[expound.alpha :as expound]
|
||||||
|
[integrant.core :as ig]
|
||||||
|
[integrant.repl :refer [clear go halt prep init reset reset-all]]
|
||||||
|
[integrant.repl.state :as state]
|
||||||
|
[kit.api :as kit]
|
||||||
|
[lambdaisland.classpath.watch-deps :as watch-deps] ;; hot loading for deps
|
||||||
|
[pmagnus.elprice.core :refer [start-app]]))
|
||||||
|
|
||||||
|
;; uncomment to enable hot loading for deps
|
||||||
|
(watch-deps/start! {:aliases [:dev :test]})
|
||||||
|
|
||||||
|
(alter-var-root #'s/*explain-out* (constantly expound/printer))
|
||||||
|
|
||||||
|
(add-tap (bound-fn* clojure.pprint/pprint))
|
||||||
|
|
||||||
|
(defn dev-prep!
|
||||||
|
[]
|
||||||
|
(integrant.repl/set-prep! (fn []
|
||||||
|
(-> (pmagnus.elprice.config/system-config {:profile :dev})
|
||||||
|
(ig/expand)))))
|
||||||
|
|
||||||
|
(defn test-prep!
|
||||||
|
[]
|
||||||
|
(integrant.repl/set-prep! (fn []
|
||||||
|
(-> (pmagnus.elprice.config/system-config {:profile :test})
|
||||||
|
(ig/expand)))))
|
||||||
|
|
||||||
|
;; Can change this to test-prep! if want to run tests as the test profile in your repl
|
||||||
|
;; You can run tests in the dev profile, too, but there are some differences between
|
||||||
|
;; the two profiles.
|
||||||
|
(dev-prep!)
|
||||||
|
|
||||||
|
(repl/set-refresh-dirs "src/clj")
|
||||||
|
|
||||||
|
(def refresh repl/refresh)
|
||||||
|
|
||||||
|
|
||||||
|
(defn reset-db []
|
||||||
|
(migratus.core/reset (:db.sql/migrations state/system)))
|
||||||
|
|
||||||
|
(defn rollback []
|
||||||
|
(migratus.core/rollback (:db.sql/migrations state/system)))
|
||||||
|
|
||||||
|
(defn migrate []
|
||||||
|
(migratus.core/migrate (:db.sql/migrations state/system)))
|
||||||
|
|
||||||
|
(def query-fn (:db.sql/query-fn state/system))
|
||||||
|
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(go)
|
||||||
|
(reset))
|
||||||
Vendored
+38
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration scan="true" scanPeriod="10 seconds">
|
||||||
|
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<!-- encoders are assigned the type
|
||||||
|
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||||
|
<encoder>
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>log/pmagnus.elprice.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>log/pmagnus.elprice.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
|
<maxFileSize>100MB</maxFileSize>
|
||||||
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
|
<!-- keep 30 days of history -->
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<logger name="org.eclipse.aether" level="warn" />
|
||||||
|
<logger name="io.methvin.watcher" level="warn" />
|
||||||
|
<logger name="org.eclipse.jgit" level="warn" />
|
||||||
|
<logger name="com.zaxxer.hikari" level="warn" />
|
||||||
|
<logger name="org.apache.http" level="warn" />
|
||||||
|
<logger name="org.xnio.nio" level="warn" />
|
||||||
|
<logger name="io.undertow" level="warn" />
|
||||||
|
<root level="DEBUG">
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
<appender-ref ref="FILE" />
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
(ns pmagnus.elprice.env
|
||||||
|
(:require [clojure.tools.logging :as log]))
|
||||||
|
|
||||||
|
(def defaults
|
||||||
|
{:init (fn []
|
||||||
|
(log/info "\n-=[elprice starting]=-"))
|
||||||
|
:start (fn []
|
||||||
|
(log/info "\n-=[elprice started successfully]=-"))
|
||||||
|
:stop (fn []
|
||||||
|
(log/info "\n-=[elprice has shut down successfully]=-"))
|
||||||
|
:middleware (fn [handler _] handler)
|
||||||
|
:opts {:profile :prod}})
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<logger name="com.zaxxer.hikari" level="error" />
|
||||||
|
<logger name="org.apache.http" level="error" />
|
||||||
|
<logger name="org.xnio.nio" level="error" />
|
||||||
|
<logger name="io.undertow" level="error" />
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<logger name="com.zaxxer.hikari" level="error" />
|
||||||
|
<logger name="org.apache.http" level="error" />
|
||||||
|
<logger name="org.xnio.nio" level="error" />
|
||||||
|
<logger name="io.undertow" level="error" />
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{:full-name "pmagnus/elprice"
|
||||||
|
:ns-name "pmagnus.elprice"
|
||||||
|
:sanitized "pmagnus/elprice"
|
||||||
|
:name "elprice"
|
||||||
|
:modules {:root "modules"
|
||||||
|
:repositories [{:url "https://github.com/kit-clj/modules.git"
|
||||||
|
:tag "master"
|
||||||
|
:name "kit-modules"}]}}
|
||||||
Generated
+1055
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "elprice",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Start a [REPL](#repls) in your editor or terminal of choice.",
|
||||||
|
"main": "index.js",
|
||||||
|
"directories": {
|
||||||
|
"test": "test"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"css:build": "npx @tailwindcss/cli -i resources/css/input.css -o resources/public/css/output.css --minify",
|
||||||
|
"css:watch": "npx @tailwindcss/cli -i resources/css/input.css -o resources/public/css/output.css --watch"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
|
"tailwindcss": "^4.1.18"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See https://github.com/yogthos/migratus
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- place your sql queries here
|
||||||
|
-- see https://www.hugsql.org/ for documentation
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{:system/env
|
||||||
|
#profile {:dev :dev
|
||||||
|
:test :test
|
||||||
|
:prod :prod}
|
||||||
|
|
||||||
|
:server/http
|
||||||
|
{:port #long #or [#env PORT 3000]
|
||||||
|
:host #or [#env HTTP_HOST "0.0.0.0"]
|
||||||
|
:handler #ig/ref :handler/ring}
|
||||||
|
|
||||||
|
:handler/ring
|
||||||
|
{:router #ig/ref :router/core
|
||||||
|
:api-path "/api"
|
||||||
|
:cookie-secret #or [#env COOKIE_SECRET "JJCILTOQLMYRBFRU"]
|
||||||
|
;; from ring.middleware.defaults. anti-forgery `false` by default because services may not require it
|
||||||
|
:site-defaults-config {:params {:urlencoded true
|
||||||
|
:multipart true
|
||||||
|
:nested true
|
||||||
|
:keywordize true}
|
||||||
|
:cookies true
|
||||||
|
:session {:flash true
|
||||||
|
:cookie-name "pmagnus.elprice"
|
||||||
|
:cookie-attrs {:max-age 86400
|
||||||
|
:http-only true
|
||||||
|
:same-site :strict}}
|
||||||
|
:security {:anti-forgery false
|
||||||
|
:xss-protection {:enable? true,
|
||||||
|
:mode :block}
|
||||||
|
:frame-options :sameorigin
|
||||||
|
:content-type-options :nosniff}
|
||||||
|
:static {:resources "public"}
|
||||||
|
:responses {:not-modified-responses true
|
||||||
|
:absolute-redirects true
|
||||||
|
:content-types true
|
||||||
|
:default-charset "utf-8"}}}
|
||||||
|
|
||||||
|
:reitit.routes/api
|
||||||
|
{:base-path "/api"
|
||||||
|
:env #ig/ref :system/env
|
||||||
|
:query-fn #ig/ref :db.sql/query-fn}
|
||||||
|
|
||||||
|
:router/routes
|
||||||
|
{:routes #ig/refset :reitit/routes}
|
||||||
|
|
||||||
|
:router/core
|
||||||
|
{:routes #ig/ref :router/routes
|
||||||
|
:env #ig/ref :system/env}
|
||||||
|
|
||||||
|
:db.sql/connection
|
||||||
|
#profile {:dev {:jdbc-url "jdbc:postgresql://localhost:5432/elprice?user=elprice&password=elprice"}
|
||||||
|
:test {:jdbc-url "jdbc:postgresql://localhost:5432/elprice?user=elprice&password=elprice"}
|
||||||
|
:prod {:jdbc-url #env JDBC_URL
|
||||||
|
:init-size 1
|
||||||
|
:min-idle 1
|
||||||
|
:max-idle 8
|
||||||
|
:max-active 32}}
|
||||||
|
|
||||||
|
:db.sql/query-fn
|
||||||
|
{:conn #ig/ref :db.sql/connection
|
||||||
|
:options {}
|
||||||
|
:filename "queries.sql"
|
||||||
|
:env #ig/ref :system/env}
|
||||||
|
|
||||||
|
:db.sql/migrations
|
||||||
|
{:store :database
|
||||||
|
:db {:datasource #ig/ref :db.sql/connection}
|
||||||
|
:migrate-on-init? true}
|
||||||
|
:reitit.routes/ui {:base-path "",
|
||||||
|
:env #ig/ref :system/env}}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
(ns pmagnus.elprice.config
|
||||||
|
(:require
|
||||||
|
[kit.config :as config]))
|
||||||
|
|
||||||
|
(def ^:const system-filename "system.edn")
|
||||||
|
|
||||||
|
(defn system-config
|
||||||
|
[options]
|
||||||
|
(config/read-config system-filename options))
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
(ns pmagnus.elprice.core
|
||||||
|
(:require
|
||||||
|
[clojure.tools.logging :as log]
|
||||||
|
[integrant.core :as ig]
|
||||||
|
[pmagnus.elprice.config :as config]
|
||||||
|
[pmagnus.elprice.env :refer [defaults]]
|
||||||
|
|
||||||
|
;; Edges
|
||||||
|
[kit.edge.db.sql.conman]
|
||||||
|
[kit.edge.db.sql.migratus]
|
||||||
|
[kit.edge.db.postgres]
|
||||||
|
[kit.edge.db.mysql]
|
||||||
|
[kit.edge.server.undertow]
|
||||||
|
[pmagnus.elprice.web.handler]
|
||||||
|
|
||||||
|
;; Routes
|
||||||
|
[pmagnus.elprice.web.routes.api]
|
||||||
|
[pmagnus.elprice.web.routes.ui])
|
||||||
|
(:gen-class))
|
||||||
|
|
||||||
|
;; log uncaught exceptions in threads
|
||||||
|
(Thread/setDefaultUncaughtExceptionHandler
|
||||||
|
(fn [thread ex]
|
||||||
|
(log/error {:what :uncaught-exception
|
||||||
|
:exception ex
|
||||||
|
:where (str "Uncaught exception on" (.getName thread))})))
|
||||||
|
|
||||||
|
(defonce system (atom nil))
|
||||||
|
|
||||||
|
(defn stop-app []
|
||||||
|
((or (:stop defaults) (fn [])))
|
||||||
|
(some-> (deref system) (ig/halt!)))
|
||||||
|
|
||||||
|
(defn start-app [& [params]]
|
||||||
|
((or (:start params) (:start defaults) (fn [])))
|
||||||
|
(->> (config/system-config (or (:opts params) (:opts defaults) {}))
|
||||||
|
(ig/expand)
|
||||||
|
(ig/init)
|
||||||
|
(reset! system)))
|
||||||
|
|
||||||
|
(defn -main [& _]
|
||||||
|
(start-app)
|
||||||
|
(.addShutdownHook (Runtime/getRuntime) (Thread. (fn [] (stop-app) (shutdown-agents)))))
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
(ns pmagnus.elprice.web.controllers.health
|
||||||
|
(:require
|
||||||
|
[ring.util.http-response :as http-response])
|
||||||
|
(:import
|
||||||
|
[java.util Date]))
|
||||||
|
|
||||||
|
(defn healthcheck!
|
||||||
|
[req]
|
||||||
|
(http-response/ok
|
||||||
|
{:time (str (Date. (System/currentTimeMillis)))
|
||||||
|
:up-since (str (Date. (.getStartTime (java.lang.management.ManagementFactory/getRuntimeMXBean))))
|
||||||
|
:app {:status "up"
|
||||||
|
:message ""}}))
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
(ns pmagnus.elprice.web.handler
|
||||||
|
(:require
|
||||||
|
[pmagnus.elprice.web.middleware.core :as middleware]
|
||||||
|
[integrant.core :as ig]
|
||||||
|
[ring.util.http-response :as http-response]
|
||||||
|
[reitit.ring :as ring]
|
||||||
|
[reitit.swagger-ui :as swagger-ui]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key :handler/ring
|
||||||
|
[_ {:keys [router api-path] :as opts}]
|
||||||
|
(ring/ring-handler
|
||||||
|
(router)
|
||||||
|
(ring/routes
|
||||||
|
;; Handle trailing slash in routes - add it + redirect to it
|
||||||
|
;; https://github.com/metosin/reitit/blob/master/doc/ring/slash_handler.md
|
||||||
|
(ring/redirect-trailing-slash-handler)
|
||||||
|
(ring/create-resource-handler {:path "/"})
|
||||||
|
(when (some? api-path)
|
||||||
|
(swagger-ui/create-swagger-ui-handler {:path api-path
|
||||||
|
:url (str api-path "/swagger.json")}))
|
||||||
|
(ring/create-default-handler
|
||||||
|
{:not-found
|
||||||
|
(constantly (-> {:status 404, :body "Page not found"}
|
||||||
|
(http-response/content-type "text/plain")))
|
||||||
|
:method-not-allowed
|
||||||
|
(constantly (-> {:status 405, :body "Not allowed"}
|
||||||
|
(http-response/content-type "text/plain")))
|
||||||
|
:not-acceptable
|
||||||
|
(constantly (-> {:status 406, :body "Not acceptable"}
|
||||||
|
(http-response/content-type "text/plain")))}))
|
||||||
|
{:middleware [(middleware/wrap-base opts)]}))
|
||||||
|
|
||||||
|
(defmethod ig/init-key :router/routes
|
||||||
|
[_ {:keys [routes]}]
|
||||||
|
(mapv (fn [route]
|
||||||
|
(if (fn? route)
|
||||||
|
(route)
|
||||||
|
route))
|
||||||
|
routes))
|
||||||
|
|
||||||
|
(defmethod ig/init-key :router/core
|
||||||
|
[_ {:keys [routes env] :as opts}]
|
||||||
|
(if (= env :dev)
|
||||||
|
#(ring/router ["" opts routes])
|
||||||
|
(constantly (ring/router ["" opts routes]))))
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
(ns pmagnus.elprice.web.htmx
|
||||||
|
(:require
|
||||||
|
[ring.util.http-response :as http-response]
|
||||||
|
[hiccup.core :as h]
|
||||||
|
[hiccup.page :as p]))
|
||||||
|
|
||||||
|
(defmacro page [opts & content]
|
||||||
|
`(-> (p/html5 ~opts
|
||||||
|
[:head
|
||||||
|
[:meta {:charset "UTF-8"}]
|
||||||
|
[:meta {:name "viewport"
|
||||||
|
:content "width=device-width, initial-scale=1, viewport-fit=cover"}]
|
||||||
|
[:link {:rel "stylesheet" :href "/css/output.css"}]
|
||||||
|
[:script {:src "https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js"
|
||||||
|
:defer true}]]
|
||||||
|
[:body {:class "bg-gray-50 min-h-screen"}
|
||||||
|
[:div {:class "mx-auto max-w-[390px] px-4 py-6"}
|
||||||
|
~@content]])
|
||||||
|
http-response/ok
|
||||||
|
(http-response/content-type "text/html")))
|
||||||
|
|
||||||
|
(defmacro fragment [opts & content]
|
||||||
|
`(-> (str (h/html ~opts ~@content))
|
||||||
|
http-response/ok
|
||||||
|
(http-response/content-type "text/html")))
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
(ns pmagnus.elprice.web.middleware.core
|
||||||
|
(:require
|
||||||
|
[pmagnus.elprice.env :as env]
|
||||||
|
[ring.middleware.defaults :as defaults]
|
||||||
|
[ring.middleware.session.cookie :as cookie]))
|
||||||
|
|
||||||
|
(defn wrap-base
|
||||||
|
[{:keys [metrics site-defaults-config cookie-secret] :as opts}]
|
||||||
|
(let [cookie-store (cookie/cookie-store {:key (.getBytes ^String cookie-secret)})]
|
||||||
|
(fn [handler]
|
||||||
|
(cond-> ((:middleware env/defaults) handler opts)
|
||||||
|
true (defaults/wrap-defaults
|
||||||
|
(assoc-in site-defaults-config [:session :store] cookie-store))
|
||||||
|
))))
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
(ns pmagnus.elprice.web.middleware.exception
|
||||||
|
(:require
|
||||||
|
[clojure.tools.logging :as log]
|
||||||
|
[reitit.ring.middleware.exception :as exception]))
|
||||||
|
|
||||||
|
(defn handler [message status exception request]
|
||||||
|
(when (>= status 500)
|
||||||
|
;; You can optionally use this to report error to an external service
|
||||||
|
(log/error exception))
|
||||||
|
{:status status
|
||||||
|
:body {:message message
|
||||||
|
:exception (.getClass exception)
|
||||||
|
:data (ex-data exception)
|
||||||
|
:uri (:uri request)}})
|
||||||
|
|
||||||
|
(def wrap-exception
|
||||||
|
(exception/create-exception-middleware
|
||||||
|
(merge
|
||||||
|
exception/default-handlers
|
||||||
|
{:system.exception/internal (partial handler "internal exception" 500)
|
||||||
|
:system.exception/business (partial handler "bad request" 400)
|
||||||
|
:system.exception/not-found (partial handler "not found" 404)
|
||||||
|
:system.exception/unauthorized (partial handler "unauthorized" 401)
|
||||||
|
:system.exception/forbidden (partial handler "forbidden" 403)
|
||||||
|
|
||||||
|
;; override the default handler
|
||||||
|
::exception/default (partial handler "default" 500)
|
||||||
|
|
||||||
|
;; print stack-traces for all exceptions
|
||||||
|
::exception/wrap (fn [handler e request]
|
||||||
|
(handler e request))})))
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
(ns pmagnus.elprice.web.middleware.formats
|
||||||
|
(:require
|
||||||
|
[luminus-transit.time :as time]
|
||||||
|
[muuntaja.core :as m]))
|
||||||
|
|
||||||
|
(def instance
|
||||||
|
(m/create
|
||||||
|
(-> m/default-options
|
||||||
|
(update-in
|
||||||
|
[:formats "application/transit+json" :decoder-opts]
|
||||||
|
(partial merge time/time-deserialization-handlers))
|
||||||
|
(update-in
|
||||||
|
[:formats "application/transit+json" :encoder-opts]
|
||||||
|
(partial merge time/time-serialization-handlers)))))
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
(ns pmagnus.elprice.web.routes.api
|
||||||
|
(:require
|
||||||
|
[pmagnus.elprice.web.controllers.health :as health]
|
||||||
|
[pmagnus.elprice.web.middleware.exception :as exception]
|
||||||
|
[pmagnus.elprice.web.middleware.formats :as formats]
|
||||||
|
[integrant.core :as ig]
|
||||||
|
[reitit.coercion.malli :as malli]
|
||||||
|
[reitit.ring.coercion :as coercion]
|
||||||
|
[reitit.ring.middleware.muuntaja :as muuntaja]
|
||||||
|
[reitit.ring.middleware.parameters :as parameters]
|
||||||
|
[reitit.swagger :as swagger]))
|
||||||
|
|
||||||
|
(def route-data
|
||||||
|
{:coercion malli/coercion
|
||||||
|
:muuntaja formats/instance
|
||||||
|
:swagger {:id ::api}
|
||||||
|
:middleware [;; query-params & form-params
|
||||||
|
parameters/parameters-middleware
|
||||||
|
;; content-negotiation
|
||||||
|
muuntaja/format-negotiate-middleware
|
||||||
|
;; encoding response body
|
||||||
|
muuntaja/format-response-middleware
|
||||||
|
;; exception handling
|
||||||
|
coercion/coerce-exceptions-middleware
|
||||||
|
;; decoding request body
|
||||||
|
muuntaja/format-request-middleware
|
||||||
|
;; coercing response bodys
|
||||||
|
coercion/coerce-response-middleware
|
||||||
|
;; coercing request parameters
|
||||||
|
coercion/coerce-request-middleware
|
||||||
|
;; exception handling
|
||||||
|
exception/wrap-exception]})
|
||||||
|
|
||||||
|
;; Routes
|
||||||
|
(defn api-routes [_opts]
|
||||||
|
[["/swagger.json"
|
||||||
|
{:get {:no-doc true
|
||||||
|
:swagger {:info {:title "pmagnus.elprice API"}}
|
||||||
|
:handler (swagger/create-swagger-handler)}}]
|
||||||
|
["/health"
|
||||||
|
;; note that use of the var is necessary
|
||||||
|
;; for reitit to reload routes without
|
||||||
|
;; restarting the system
|
||||||
|
{:get #'health/healthcheck!}]])
|
||||||
|
|
||||||
|
(derive :reitit.routes/api :reitit/routes)
|
||||||
|
|
||||||
|
(defmethod ig/init-key :reitit.routes/api
|
||||||
|
[_ {:keys [base-path]
|
||||||
|
:or {base-path ""}
|
||||||
|
:as opts}]
|
||||||
|
(fn [] [base-path route-data (api-routes opts)]))
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
(ns pmagnus.elprice.web.routes.ui
|
||||||
|
(:require
|
||||||
|
[pmagnus.elprice.web.middleware.exception :as exception]
|
||||||
|
[pmagnus.elprice.web.middleware.formats :as formats]
|
||||||
|
[pmagnus.elprice.web.routes.utils :as utils]
|
||||||
|
[pmagnus.elprice.web.htmx :refer [page fragment] :as htmx]
|
||||||
|
[integrant.core :as ig]
|
||||||
|
[reitit.ring.middleware.muuntaja :as muuntaja]
|
||||||
|
[reitit.ring.middleware.parameters :as parameters]))
|
||||||
|
|
||||||
|
(defn home [request]
|
||||||
|
(page {:lang "en"}
|
||||||
|
[:h1 {:class "text-2xl font-bold text-gray-900"}
|
||||||
|
"Welcome to elprice"]
|
||||||
|
[:p {:class "mt-2 text-gray-600"}
|
||||||
|
"Built with Kit, HTMX, Hiccup & Tailwind"]
|
||||||
|
[:button {:class "mt-4 rounded-lg bg-blue-600 px-4 py-2 text-white font-medium
|
||||||
|
active:bg-blue-700 transition-colors"
|
||||||
|
:hx-post "/clicked"
|
||||||
|
:hx-swap "outerHTML"}
|
||||||
|
"Click me!"]))
|
||||||
|
|
||||||
|
(defn clicked [request]
|
||||||
|
(fragment
|
||||||
|
[:div {:class "mt-4 rounded-lg bg-green-100 p-4 text-green-800"}
|
||||||
|
"Congratulations! You just clicked the button!"]))
|
||||||
|
|
||||||
|
;; Routes
|
||||||
|
(defn ui-routes [_opts]
|
||||||
|
[["/" {:get home}]
|
||||||
|
["/clicked" {:post clicked}]])
|
||||||
|
|
||||||
|
(def route-data
|
||||||
|
{:muuntaja formats/instance
|
||||||
|
:middleware
|
||||||
|
[;; Default middleware for ui
|
||||||
|
;; query-params & form-params
|
||||||
|
parameters/parameters-middleware
|
||||||
|
;; encoding response body
|
||||||
|
muuntaja/format-response-middleware
|
||||||
|
;; exception handling
|
||||||
|
exception/wrap-exception]})
|
||||||
|
|
||||||
|
(derive :reitit.routes/ui :reitit/routes)
|
||||||
|
|
||||||
|
(defmethod ig/init-key :reitit.routes/ui
|
||||||
|
[_ {:keys [base-path]
|
||||||
|
:or {base-path ""}
|
||||||
|
:as opts}]
|
||||||
|
(fn [] [base-path route-data (ui-routes opts)]))
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
(ns pmagnus.elprice.web.routes.utils)
|
||||||
|
|
||||||
|
(def route-data-path [:reitit.core/match :data])
|
||||||
|
|
||||||
|
(defn route-data
|
||||||
|
[req]
|
||||||
|
(get-in req route-data-path))
|
||||||
|
|
||||||
|
(defn route-data-key
|
||||||
|
[req k]
|
||||||
|
(get-in req (conj route-data-path k)))
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
content: ["./src/**/*.clj", "./resources/**/*.html"],
|
||||||
|
theme: { extend: {} },
|
||||||
|
plugins: []
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
(ns pmagnus.elprice.core-test
|
||||||
|
(:require
|
||||||
|
[pmagnus.elprice.test-utils :as utils]
|
||||||
|
[clojure.test :refer :all]))
|
||||||
|
|
||||||
|
(deftest example-test
|
||||||
|
(is (= 1 2)))
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
(ns pmagnus.elprice.test-utils
|
||||||
|
(:require
|
||||||
|
[pmagnus.elprice.core :as core]
|
||||||
|
[peridot.core :as p]
|
||||||
|
[byte-streams :as bs]
|
||||||
|
[integrant.repl.state :as state]))
|
||||||
|
|
||||||
|
(defn system-state
|
||||||
|
[]
|
||||||
|
(or @core/system state/system))
|
||||||
|
|
||||||
|
(defn system-fixture
|
||||||
|
[]
|
||||||
|
(fn [f]
|
||||||
|
(when (nil? (system-state))
|
||||||
|
(core/start-app {:opts {:profile :test}}))
|
||||||
|
(f)
|
||||||
|
(core/stop-app)))
|
||||||
|
|
||||||
|
(defn get-response [ctx]
|
||||||
|
(-> ctx
|
||||||
|
:response
|
||||||
|
(update :body (fnil bs/to-string ""))))
|
||||||
|
|
||||||
|
(defn GET [app path params headers]
|
||||||
|
(-> (p/session app)
|
||||||
|
(p/request path
|
||||||
|
:request-method :get
|
||||||
|
:content-type "application/edn"
|
||||||
|
:headers headers
|
||||||
|
:params params)
|
||||||
|
(get-response)))
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
(ns pmagnus.elprice.web.request-test
|
||||||
|
(:require [clojure.test :refer [deftest testing is use-fixtures]]
|
||||||
|
[pmagnus.elprice.test-utils :refer [system-state system-fixture GET]]))
|
||||||
|
|
||||||
|
(use-fixtures :once (system-fixture))
|
||||||
|
|
||||||
|
(deftest health-request-test []
|
||||||
|
(testing "happy path"
|
||||||
|
(let [handler (:handler/ring (system-state))
|
||||||
|
params {}
|
||||||
|
headers {}
|
||||||
|
response (GET handler "/api/health" params headers)]
|
||||||
|
(is (= 200 (:status response))))))
|
||||||
Reference in New Issue
Block a user