|
79 | 79 | Options:
|
80 | 80 | -c, --cider Start an NREPL server with CIDER middleware
|
81 | 81 | --init Create a blank duct.edn config file
|
| 82 | + -k, --keys KEYS Limit --main to start only the supplied keys |
82 | 83 | -p, --profiles PROFILES A concatenated list of profile keys
|
83 | 84 | -n, --nrepl Start an NREPL server
|
84 | 85 | -m, --main Start the application
|
@@ -869,9 +870,13 @@ command-line argument or environment variable to override this default.
|
869 | 870 | ----
|
870 | 871 | user=> (reset)
|
871 | 872 | :reloading ()
|
| 873 | +:user/added (db sql) |
872 | 874 | :resumed
|
873 | 875 | ----
|
874 | 876 |
|
| 877 | +NOTE: The `:user/added` message informs you about convenience functions |
| 878 | +that have been added to the REPL environment in the user namespace. |
| 879 | + |
875 | 880 | The SQL module adds a database connection pool under the key
|
876 | 881 | `:duct.database.sql/hikaricp`, which derives from the more general
|
877 | 882 | `:duct.database/sql` key. We can use this connection pool as a
|
@@ -983,3 +988,120 @@ This will run any component with a key that derives from
|
983 | 988 | NOTE: `:duct/logger` is often defined as an optional dependency, via a
|
984 | 989 | *refset*. Without explicitly specifying this as one of the keys, the
|
985 | 990 | migrator will run without logging.
|
| 991 | + |
| 992 | +=== Database-Driven Todos |
| 993 | + |
| 994 | +Now that we have a database table and a web server, it's time to put the |
| 995 | +two together. The database we pass to the index function can be used to |
| 996 | +populate an unordered list. We'll change the index function accordingly. |
| 997 | + |
| 998 | +.src/todo/routes.clj |
| 999 | +[,clojure] |
| 1000 | +---- |
| 1001 | +(ns todo.routes |
| 1002 | + (:require [next.jdbc :as jdbc])) |
| 1003 | +
|
| 1004 | +(def list-todos "SELECT * FROM todo") |
| 1005 | +
|
| 1006 | +(defn index [{:keys [db]}] |
| 1007 | + (fn [_request] |
| 1008 | + [:html {:lang "en"} |
| 1009 | + [:head [:title "Todo"]] |
| 1010 | + [:body |
| 1011 | + [:ul (for [rs (jdbc/execute! db [list-todos])] |
| 1012 | + [:li (:todo/description rs)])]]])) |
| 1013 | +---- |
| 1014 | + |
| 1015 | +TIP: It's often a good idea to factor out each SQL string into its own |
| 1016 | +var. This allows them to be treated almost like function calls when |
| 1017 | +combined with `execute!`. |
| 1018 | + |
| 1019 | +We can reset via the REPL and add some test data with the `sql` |
| 1020 | +convenience function. |
| 1021 | + |
| 1022 | +[,shell] |
| 1023 | +---- |
| 1024 | +user=> (reset) |
| 1025 | +:reloading (todo.routes) |
| 1026 | +:resumed |
| 1027 | +user=> (sql "INSERT INTO todo (description) VALUES ('Test One')") |
| 1028 | +[#:next.jdbc{:update-count 1}] |
| 1029 | +user=> (sql "INSERT INTO todo (description) VALUES ('Test Two')") |
| 1030 | +[#:next.jdbc{:update-count 1}] |
| 1031 | +---- |
| 1032 | + |
| 1033 | +If you visit http://localhost:3000/ you'll be able to see the todo items |
| 1034 | +that were added to the database table. |
| 1035 | + |
| 1036 | +The next step is to allow for new todo items to be added through the web |
| 1037 | +interface. This is a little more involved, as we'll need a HTML form and |
| 1038 | +a route to respond to the form's POST. |
| 1039 | + |
| 1040 | +First, we add a new handler, `new-todo`, to the configuration to handle |
| 1041 | +the POST. |
| 1042 | + |
| 1043 | +.duct.edn |
| 1044 | +[,clojure] |
| 1045 | +---- |
| 1046 | +{:vars {jdbc-url {:default "jdbc:sqlite:todo.db"}} |
| 1047 | + :system |
| 1048 | + {:duct.module/logging {} |
| 1049 | + :duct.module/sql {} |
| 1050 | + :duct.module/web |
| 1051 | + {:features #{:site} |
| 1052 | + :handler-opts {:db #ig/ref :duct.database/sql} |
| 1053 | + :routes [["/" {:get :todo.routes/index |
| 1054 | + :post :todo.routes/new-todo}]]}}} |
| 1055 | +---- |
| 1056 | + |
| 1057 | +Then we need incorporate the POST handler and the form into the |
| 1058 | +codebase. |
| 1059 | + |
| 1060 | +.src/todo/routes.clj |
| 1061 | +[,clojure] |
| 1062 | +---- |
| 1063 | +(ns todo.routes |
| 1064 | + (:require [next.jdbc :as jdbc] |
| 1065 | + [ring.middleware.anti-forgery :as af])) |
| 1066 | +
|
| 1067 | +(def list-todos "SELECT * FROM todo") |
| 1068 | +(def insert-todo "INSERT INTO todo (description) VALUES (?)") |
| 1069 | +
|
| 1070 | +(defn- create-todo-form [] |
| 1071 | + [:form {:action "/" :method "post"} |
| 1072 | + [:input {:type "hidden" |
| 1073 | + :name "__anti-forgery-token" |
| 1074 | + :value af/*anti-forgery-token*}] |
| 1075 | + [:input {:type "text", :name "description"}] |
| 1076 | + [:input {:type "submit", :value "Create"}]]) |
| 1077 | +
|
| 1078 | +(defn index [{:keys [db]}] |
| 1079 | + (fn [_request] |
| 1080 | + [:html {:lang "en"} |
| 1081 | + [:head [:title "Todo"]] |
| 1082 | + [:body |
| 1083 | + [:ul |
| 1084 | + (for [rs (jdbc/execute! db [list-todos])] |
| 1085 | + [:li (:todo/description rs)]) |
| 1086 | + [:li (create-todo-form)]]]])) |
| 1087 | +
|
| 1088 | +(defn new-todo [{:keys [db]}] |
| 1089 | + (fn [{{:keys [description]} :params}] |
| 1090 | + (jdbc/execute! db [insert-todo description]) |
| 1091 | + {:status 303, :headers {"Location" "/"}})) |
| 1092 | +---- |
| 1093 | + |
| 1094 | +There are two new additions here. The `create-todo-form` function |
| 1095 | +creates a form for making new todo list items. You may notice that it |
| 1096 | +includes a hidden field for setting an anti-forgery token. This prevents |
| 1097 | +a type of attack known as a |
| 1098 | +https://en.wikipedia.org/wiki/Cross-site_request_forgery[Cross-site |
| 1099 | +request forgery]. |
| 1100 | + |
| 1101 | +The second addition is the `new-todo` function. This inserts a new row |
| 1102 | +into the todo table, then returns a "`303 See Other`" response that will |
| 1103 | +redirect the browser back to the index page. |
| 1104 | + |
| 1105 | +If we reset via the REPL and check http://localhost:3000/, you should |
| 1106 | +see a text input box at the bottom of the todo list, allowing more todo |
| 1107 | +items to be added. |
0 commit comments