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
/.nrepl-port
/.prepl-port
*.db

View File

@@ -20,6 +20,7 @@ default, the server listens on port 3000.
* `reitit`: for handling routes
* `jetty`: web server
* `muuntaja`: JSON handling
* `next.jdbc`: database interface (SQLite)
## 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://ostash.dev/posts/2021-08-22-data-validation-in-clojure/>
* <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"]
[metosin/reitit "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
:target-path "target/%s"
:profiles {:uberjar {:aot :all

View File

@@ -4,7 +4,10 @@
[reitit.ring.middleware.muuntaja :as muuntaja]
[muuntaja.core :as m]
[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))
@@ -23,6 +26,38 @@
[: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 -------------------------------------------------------
(defn ahoy-handler
[_]
@@ -32,19 +67,18 @@
(defn get-items-handler
[_]
{:status 200
:body {:id 1
:name "Jack Sparrow"
:email "jack.sparrow@triangle.bm"}})
:body {:items (db-get-all-items)}})
(defn post-items-handler
(defn create-item-handler
[request]
(let [item (:body-params request)
id 1
name (:name item)
email (:email item)]
email (:email item)
id (db-store-item! name email)]
{:status 201
:headers {"Location" (str "/item/" id)}
:body {:id id, :name name, :email email}}))
:body (db-get-item id)}))
;; Custom middleware ----------------------------------------------
@@ -68,7 +102,7 @@
(ring/router
[["/ahoy" {:get ahoy-handler}]
["/items" {:get get-items-handler
:post {:handler post-items-handler
:post {:handler create-item-handler
:parameters {:body Item}}}]]
;; Middlewares:
;; - wrap-content-type-json: ensure POST routes are sent JSON payload
@@ -92,5 +126,8 @@
"HolidayPirates take-home assignement.
Goal: build a (very) small REST API that exposes to endpoints."
[& args]
(println "Initializing SQLite database...")
(init-db!)
(println "Starting server...")
(http-server/run-jetty app {:port 3000 :join? false})
(println "Ahoy! Yo-ho-ho API running on port 3000"))