Persist items to the database

The database that was chosen here is SQLite, because it's dead simple to
setup and more than enough for this project.

Please note that I took some liberty with the assignment. I chose to use
a numeric field for the `id` column of an item. This leverages automatic
creation and incrementation of the id by SQLite itself.
This commit is contained in:
2026-01-31 01:59:32 +01:00
parent da6f8b4519
commit eded366570
4 changed files with 53 additions and 10 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ pom.xml.asc
/.lsp /.lsp
/.nrepl-port /.nrepl-port
/.prepl-port /.prepl-port
*.db

View File

@@ -20,6 +20,7 @@ default, the server listens on port 3000.
* `reitit`: for handling routes * `reitit`: for handling routes
* `jetty`: web server * `jetty`: web server
* `muuntaja`: JSON handling * `muuntaja`: JSON handling
* `next.jdbc`: database interface (SQLite)
## Documentation links ## Documentation links
@@ -29,3 +30,5 @@ The following links proved more than useful when working on this assignment:
* <https://github.com/metosin/reitit/blob/master/doc/ring/content_negotiation.md> * <https://github.com/metosin/reitit/blob/master/doc/ring/content_negotiation.md>
* <https://ostash.dev/posts/2021-08-22-data-validation-in-clojure/> * <https://ostash.dev/posts/2021-08-22-data-validation-in-clojure/>
* <https://clojurecivitas.github.io/malli/elements_of_malli.html> * <https://clojurecivitas.github.io/malli/elements_of_malli.html>
* <https://github.com/metosin/reitit/blob/master/doc/coercion/malli_coercion.md>
* <https://cljdoc.org/d/com.github.seancorfield/next.jdbc/1.3.1086/doc/getting-started>

View File

@@ -8,7 +8,9 @@
[ring/ring-jetty-adapter "1.15.3"] [ring/ring-jetty-adapter "1.15.3"]
[metosin/reitit "0.10.0"] [metosin/reitit "0.10.0"]
[metosin/reitit-malli "0.10.0"] [metosin/reitit-malli "0.10.0"]
[metosin/muuntaja "0.6.11"]] [metosin/muuntaja "0.6.11"]
[com.github.seancorfield/next.jdbc "1.3.1086"]
[org.xerial/sqlite-jdbc "3.51.1.0"]]
:main ^:skip-aot yohoho.core :main ^:skip-aot yohoho.core
:target-path "target/%s" :target-path "target/%s"
:profiles {:uberjar {:aot :all :profiles {:uberjar {:aot :all

View File

@@ -4,7 +4,10 @@
[reitit.ring.middleware.muuntaja :as muuntaja] [reitit.ring.middleware.muuntaja :as muuntaja]
[muuntaja.core :as m] [muuntaja.core :as m]
[reitit.coercion.malli :as malli] [reitit.coercion.malli :as malli]
[reitit.ring.coercion :as coercion]) [reitit.ring.coercion :as coercion]
[next.jdbc :as jdbc]
[next.jdbc.sql :as sql]
[next.jdbc.result-set :as rs])
(:gen-class)) (:gen-class))
@@ -23,6 +26,38 @@
[:email Email]]) [:email Email]])
;; Database stuff -------------------------------------------------
(def db {:dbtype "sqlite" :dbname "yohoho.db"})
(defn init-db!
"Create database structure if needed"
[]
(jdbc/execute!
db
["CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL)"]))
(defn db-store-item!
"Store a new item in database and returns it ID"
[name email]
(let [db-result (sql/insert! db :items {:name name, :email email})]
;; SQLite returns the generated ID of the new item like this:
;; {:last_insert_rowid() 42}
(get db-result (keyword "last_insert_rowid()"))))
(defn db-get-item
"Retrieve an item given its ID"
[id]
(sql/get-by-id db :items id
{:builder-fn rs/as-unqualified-lower-maps}))
(defn db-get-all-items
"Retrieve every item from the database"
[]
(sql/query db ["select * from items"] {:builder-fn rs/as-unqualified-lower-maps}))
;; Handlers ------------------------------------------------------- ;; Handlers -------------------------------------------------------
(defn ahoy-handler (defn ahoy-handler
[_] [_]
@@ -32,19 +67,18 @@
(defn get-items-handler (defn get-items-handler
[_] [_]
{:status 200 {:status 200
:body {:id 1 :body {:items (db-get-all-items)}})
:name "Jack Sparrow"
:email "jack.sparrow@triangle.bm"}})
(defn post-items-handler (defn create-item-handler
[request] [request]
(let [item (:body-params request) (let [item (:body-params request)
id 1
name (:name item) name (:name item)
email (:email item)] email (:email item)
id (db-store-item! name email)]
{:status 201 {:status 201
:headers {"Location" (str "/item/" id)} :headers {"Location" (str "/item/" id)}
:body {:id id, :name name, :email email}})) :body (db-get-item id)}))
;; Custom middleware ---------------------------------------------- ;; Custom middleware ----------------------------------------------
@@ -68,7 +102,7 @@
(ring/router (ring/router
[["/ahoy" {:get ahoy-handler}] [["/ahoy" {:get ahoy-handler}]
["/items" {:get get-items-handler ["/items" {:get get-items-handler
:post {:handler post-items-handler :post {:handler create-item-handler
:parameters {:body Item}}}]] :parameters {:body Item}}}]]
;; Middlewares: ;; Middlewares:
;; - wrap-content-type-json: ensure POST routes are sent JSON payload ;; - wrap-content-type-json: ensure POST routes are sent JSON payload
@@ -92,5 +126,8 @@
"HolidayPirates take-home assignement. "HolidayPirates take-home assignement.
Goal: build a (very) small REST API that exposes to endpoints." Goal: build a (very) small REST API that exposes to endpoints."
[& args] [& args]
(println "Initializing SQLite database...")
(init-db!)
(println "Starting server...")
(http-server/run-jetty app {:port 3000 :join? false}) (http-server/run-jetty app {:port 3000 :join? false})
(println "Ahoy! Yo-ho-ho API running on port 3000")) (println "Ahoy! Yo-ho-ho API running on port 3000"))