From 22971d801f096bbf8e6062a11daca77a9b75110b Mon Sep 17 00:00:00 2001 From: Charlotte Allen Date: Mon, 6 Jan 2020 11:27:28 -0800 Subject: [PATCH] Add database stuff --- env/dev/clj/user.clj | 33 ++++++- env/dev/resources/logback.xml | 1 + env/prod/resources/logback.xml | 1 + env/test/resources/logback.xml | 1 + project.clj | 3 + resources/docs/docs.md | 15 ++++ .../20200106192245-add-users-table.down.sql | 1 + .../20200106192245-add-users-table.up.sql | 9 ++ resources/sql/queries.sql | 21 +++++ src/clj/shapey_shifty/core.clj | 19 +++- src/clj/shapey_shifty/db/core.clj | 86 +++++++++++++++++++ src/clj/shapey_shifty/routes/home.clj | 1 + test/clj/shapey_shifty/test/db/core.clj | 38 ++++++++ 13 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 resources/migrations/20200106192245-add-users-table.down.sql create mode 100644 resources/migrations/20200106192245-add-users-table.up.sql create mode 100644 resources/sql/queries.sql create mode 100644 src/clj/shapey_shifty/db/core.clj create mode 100644 test/clj/shapey_shifty/test/db/core.clj diff --git a/env/dev/clj/user.clj b/env/dev/clj/user.clj index 65cd7a8..e9024d9 100644 --- a/env/dev/clj/user.clj +++ b/env/dev/clj/user.clj @@ -6,7 +6,10 @@ [clojure.spec.alpha :as s] [expound.alpha :as expound] [mount.core :as mount] - [shapey-shifty.core :refer [start-app]])) + [shapey-shifty.core :refer [start-app]] + [shapey-shifty.db.core] + [conman.core :as conman] + [luminus-migrations.core :as migrations])) (alter-var-root #'s/*explain-out* (constantly expound/printer)) @@ -29,4 +32,32 @@ (stop) (start)) +(defn restart-db + "Restarts database." + [] + (mount/stop #'shapey-shifty.db.core/*db*) + (mount/start #'shapey-shifty.db.core/*db*) + (binding [*ns* 'shapey-shifty.db.core] + (conman/bind-connection shapey-shifty.db.core/*db* "sql/queries.sql"))) + +(defn reset-db + "Resets database." + [] + (migrations/migrate ["reset"] (select-keys env [:database-url]))) + +(defn migrate + "Migrates database up for all outstanding migrations." + [] + (migrations/migrate ["migrate"] (select-keys env [:database-url]))) + +(defn rollback + "Rollback latest database migration." + [] + (migrations/migrate ["rollback"] (select-keys env [:database-url]))) + +(defn create-migration + "Create a new up and down migration file with a generated timestamp and `name`." + [name] + (migrations/create name (select-keys env [:database-url]))) + diff --git a/env/dev/resources/logback.xml b/env/dev/resources/logback.xml index 8219609..db59388 100644 --- a/env/dev/resources/logback.xml +++ b/env/dev/resources/logback.xml @@ -26,6 +26,7 @@ + diff --git a/env/prod/resources/logback.xml b/env/prod/resources/logback.xml index 9975a5b..2e68a51 100644 --- a/env/prod/resources/logback.xml +++ b/env/prod/resources/logback.xml @@ -18,6 +18,7 @@ + diff --git a/env/test/resources/logback.xml b/env/test/resources/logback.xml index 8219609..db59388 100644 --- a/env/test/resources/logback.xml +++ b/env/test/resources/logback.xml @@ -26,6 +26,7 @@ + diff --git a/project.clj b/project.clj index ff82eb6..32197ec 100644 --- a/project.clj +++ b/project.clj @@ -6,10 +6,12 @@ :dependencies [[ch.qos.logback/logback-classic "1.2.3"] [cheshire "5.9.0"] [clojure.java-time "0.3.2"] + [conman "0.8.4"] [cprop "0.1.15"] [expound "0.8.3"] [funcool/struct "1.4.0"] [luminus-jetty "0.1.7"] + [luminus-migrations "0.6.6"] [luminus-transit "0.1.2"] [luminus/ring-ttl-session "0.3.3"] [markdown-clj "1.10.1"] @@ -21,6 +23,7 @@ [org.clojure/clojure "1.10.1"] [org.clojure/tools.cli "0.4.2"] [org.clojure/tools.logging "0.5.0"] + [org.postgresql/postgresql "42.2.9"] [org.webjars.npm/bulma "0.8.0"] [org.webjars.npm/material-icons "0.3.1"] [org.webjars/webjars-locator "0.38"] diff --git a/resources/docs/docs.md b/resources/docs/docs.md index a96b3ad..57a8602 100644 --- a/resources/docs/docs.md +++ b/resources/docs/docs.md @@ -91,6 +91,21 @@ the `env/dev/clj/` source path. learn more about middleware » +
+ +#### 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"})))))