+
+#### Database configuration is required
+
+If you haven't already, then please follow the steps below to configure your database connection and run the necessary migrations.
+
+* Create the database for your application.
+* Update the connection URL in the `dev-config.edn` and `test-config.edn` files with your database name and login credentials.
+* Run `lein run migrate` in the root of the project to create the tables.
+* Let `mount` know to start the database connection by `require`-ing `shapey-shifty.db.core` in some other namespace.
+* Restart the application.
+
+
learn more about database access »
+
+
diff --git a/resources/migrations/20200106192245-add-users-table.down.sql b/resources/migrations/20200106192245-add-users-table.down.sql
new file mode 100644
index 0000000..cc1f647
--- /dev/null
+++ b/resources/migrations/20200106192245-add-users-table.down.sql
@@ -0,0 +1 @@
+DROP TABLE users;
diff --git a/resources/migrations/20200106192245-add-users-table.up.sql b/resources/migrations/20200106192245-add-users-table.up.sql
new file mode 100644
index 0000000..b9c31f1
--- /dev/null
+++ b/resources/migrations/20200106192245-add-users-table.up.sql
@@ -0,0 +1,9 @@
+CREATE TABLE users
+(id VARCHAR(20) PRIMARY KEY,
+ first_name VARCHAR(30),
+ last_name VARCHAR(30),
+ email VARCHAR(30),
+ admin BOOLEAN,
+ last_login TIMESTAMP,
+ is_active BOOLEAN,
+ pass VARCHAR(300));
diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql
new file mode 100644
index 0000000..28d2b8c
--- /dev/null
+++ b/resources/sql/queries.sql
@@ -0,0 +1,21 @@
+-- :name create-user! :! :n
+-- :doc creates a new user record
+INSERT INTO users
+(id, first_name, last_name, email, pass)
+VALUES (:id, :first_name, :last_name, :email, :pass)
+
+-- :name update-user! :! :n
+-- :doc updates an existing user record
+UPDATE users
+SET first_name = :first_name, last_name = :last_name, email = :email
+WHERE id = :id
+
+-- :name get-user :? :1
+-- :doc retrieves a user record given the id
+SELECT * FROM users
+WHERE id = :id
+
+-- :name delete-user! :! :n
+-- :doc deletes a user record given the id
+DELETE FROM users
+WHERE id = :id
diff --git a/src/clj/shapey_shifty/core.clj b/src/clj/shapey_shifty/core.clj
index 6a7113b..a32586c 100644
--- a/src/clj/shapey_shifty/core.clj
+++ b/src/clj/shapey_shifty/core.clj
@@ -3,6 +3,7 @@
[shapey-shifty.handler :as handler]
[shapey-shifty.nrepl :as nrepl]
[luminus.http-server :as http]
+ [luminus-migrations.core :as migrations]
[shapey-shifty.config :refer [env]]
[clojure.tools.cli :refer [parse-opts]]
[clojure.tools.logging :as log]
@@ -55,4 +56,20 @@
(.addShutdownHook (Runtime/getRuntime) (Thread. stop-app)))
(defn -main [& args]
- (start-app args))
+ (mount/start #'shapey-shifty.config/env)
+ (cond
+ (nil? (:database-url env))
+ (do
+ (log/error "Database configuration not found, :database-url environment variable must be set before running")
+ (System/exit 1))
+ (some #{"init"} args)
+ (do
+ (migrations/init (select-keys env [:database-url :init-script]))
+ (System/exit 0))
+ (migrations/migration? args)
+ (do
+ (migrations/migrate args (select-keys env [:database-url]))
+ (System/exit 0))
+ :else
+ (start-app args)))
+
diff --git a/src/clj/shapey_shifty/db/core.clj b/src/clj/shapey_shifty/db/core.clj
new file mode 100644
index 0000000..d23ab66
--- /dev/null
+++ b/src/clj/shapey_shifty/db/core.clj
@@ -0,0 +1,86 @@
+(ns shapey-shifty.db.core
+ (:require
+ [cheshire.core :refer [generate-string parse-string]]
+ [clojure.java.jdbc :as jdbc]
+ [clojure.tools.logging :as log]
+ [conman.core :as conman]
+ [java-time :as jt]
+ [java-time.pre-java8]
+ [shapey-shifty.config :refer [env]]
+ [mount.core :refer [defstate]])
+ (:import org.postgresql.util.PGobject
+ java.sql.Array
+ clojure.lang.IPersistentMap
+ clojure.lang.IPersistentVector
+ [java.sql
+ BatchUpdateException
+ PreparedStatement]))
+(defstate ^:dynamic *db*
+ :start (if-let [jdbc-url (env :database-url)]
+ (conman/connect! {:jdbc-url jdbc-url})
+ (do
+ (log/warn "database connection URL was not found, please set :database-url in your config, e.g: dev-config.edn")
+ *db*))
+ :stop (conman/disconnect! *db*))
+
+(conman/bind-connection *db* "sql/queries.sql")
+
+
+(extend-protocol jdbc/IResultSetReadColumn
+ java.sql.Timestamp
+ (result-set-read-column [v _2 _3]
+ (.toLocalDateTime v))
+ java.sql.Date
+ (result-set-read-column [v _2 _3]
+ (.toLocalDate v))
+ java.sql.Time
+ (result-set-read-column [v _2 _3]
+ (.toLocalTime v))
+ Array
+ (result-set-read-column [v _ _] (vec (.getArray v)))
+ PGobject
+ (result-set-read-column [pgobj _metadata _index]
+ (let [type (.getType pgobj)
+ value (.getValue pgobj)]
+ (case type
+ "json" (parse-string value true)
+ "jsonb" (parse-string value true)
+ "citext" (str value)
+ value))))
+
+(defn to-pg-json [value]
+ (doto (PGobject.)
+ (.setType "jsonb")
+ (.setValue (generate-string value))))
+
+(extend-type clojure.lang.IPersistentVector
+ jdbc/ISQLParameter
+ (set-parameter [v ^java.sql.PreparedStatement stmt ^long idx]
+ (let [conn (.getConnection stmt)
+ meta (.getParameterMetaData stmt)
+ type-name (.getParameterTypeName meta idx)]
+ (if-let [elem-type (when (= (first type-name) \_) (apply str (rest type-name)))]
+ (.setObject stmt idx (.createArrayOf conn elem-type (to-array v)))
+ (.setObject stmt idx (to-pg-json v))))))
+
+(extend-protocol jdbc/ISQLValue
+ java.util.Date
+ (sql-value [v]
+ (java.sql.Timestamp. (.getTime v)))
+ java.time.LocalTime
+ (sql-value [v]
+ (jt/sql-time v))
+ java.time.LocalDate
+ (sql-value [v]
+ (jt/sql-date v))
+ java.time.LocalDateTime
+ (sql-value [v]
+ (jt/sql-timestamp v))
+ java.time.ZonedDateTime
+ (sql-value [v]
+ (jt/sql-timestamp v))
+ IPersistentMap
+ (sql-value [value] (to-pg-json value))
+ IPersistentVector
+ (sql-value [value] (to-pg-json value)))
+
diff --git a/src/clj/shapey_shifty/routes/home.clj b/src/clj/shapey_shifty/routes/home.clj
index feb8865..012406e 100644
--- a/src/clj/shapey_shifty/routes/home.clj
+++ b/src/clj/shapey_shifty/routes/home.clj
@@ -1,6 +1,7 @@
(ns shapey-shifty.routes.home
(:require
[shapey-shifty.layout :as layout]
+ [shapey-shifty.db.core :as db]
[clojure.java.io :as io]
[shapey-shifty.middleware :as middleware]
[ring.util.response]
diff --git a/test/clj/shapey_shifty/test/db/core.clj b/test/clj/shapey_shifty/test/db/core.clj
new file mode 100644
index 0000000..dc06cd8
--- /dev/null
+++ b/test/clj/shapey_shifty/test/db/core.clj
@@ -0,0 +1,38 @@
+(ns shapey-shifty.test.db.core
+ (:require
+ [shapey-shifty.db.core :refer [*db*] :as db]
+ [java-time.pre-java8]
+ [luminus-migrations.core :as migrations]
+ [clojure.test :refer :all]
+ [clojure.java.jdbc :as jdbc]
+ [shapey-shifty.config :refer [env]]
+ [mount.core :as mount]))
+
+(use-fixtures
+ :once
+ (fn [f]
+ (mount/start
+ #'shapey-shifty.config/env
+ #'shapey-shifty.db.core/*db*)
+ (migrations/migrate ["migrate"] (select-keys env [:database-url]))
+ (f)))
+
+(deftest test-users
+ (jdbc/with-db-transaction [t-conn *db*]
+ (jdbc/db-set-rollback-only! t-conn)
+ (is (= 1 (db/create-user!
+ t-conn
+ {:id "1"
+ :first_name "Sam"
+ :last_name "Smith"
+ :email "sam.smith@example.com"
+ :pass "pass"})))
+ (is (= {:id "1"
+ :first_name "Sam"
+ :last_name "Smith"
+ :email "sam.smith@example.com"
+ :pass "pass"
+ :admin nil
+ :last_login nil
+ :is_active nil}
+ (db/get-user t-conn {:id "1"})))))