h-sql
Let's serve a list of comments with a comment form! First, we define a couple of prepared statements using Caqti, a library for talking to SQL databases:
module type DB = Caqti_lwt.CONNECTION
module R = Caqti_request
module T = Caqti_type
let list_comments =
let query =
R.collect T.unit T.(tup2 int string)
"SELECT id, text FROM comment" in
fun (module Db : DB) ->
let%lwt comments_or_error = Db.collect_list query () in
Caqti_lwt.or_fail comments_or_error
let add_comment =
let query =
R.exec T.string
"INSERT INTO comment (text) VALUES ($1)" in
fun text (module Db : DB) ->
let%lwt unit_or_error = Db.exec query text in
Caqti_lwt.or_fail unit_or_error
...then, a template for our CSRF-safe form, like in example
d-form:
let render comments request =
<html>
<body>
% comments |> List.iter (fun (_id, comment) ->
<p><%s comment %></p><% ); %>
<%s! Dream.form_tag ~action:"/" request %>
<input name="text">
</form>
</body>
</html>
...and finally, the Web app itself, which connects to db.sqlite:
let () =
Dream.run
@@ Dream.logger
@@ Dream.sql_pool "sqlite3:db.sqlite"
@@ Dream.sql_sessions
@@ Dream.router [
Dream.get "/" (fun request ->
let%lwt comments = Dream.sql list_comments request in
Dream.html (render comments request));
Dream.post "/" (fun request ->
match%lwt Dream.form request with
| `Ok ["text", text] ->
let%lwt () = Dream.sql (add_comment text) request in
Dream.redirect "/"
| _ ->
Dream.empty `Bad_Request);
]
@@ Dream.not_found
$ npm install esy && npx esy
$ npx esy start
Try visiting http://localhost:8080 and leaving some comments!
We take the opportunity to try out
Dream.sql_sessions, which
stores session data persistently in db.sqlite. See example
b-session for an introduction to session management.
Both the comments and the sessions survive server restarts.
db.sqlite was initialized with this schema, using the sqlite3 command:
CREATE TABLE comment (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text NOT NULL
);
CREATE TABLE dream_session (
id TEXT PRIMARY KEY,
label TEXT NOT NULL,
expires_at REAL NOT NULL,
payload TEXT NOT NULL
);
We also had to make an addition to our
dune file:
(executable
(name sql)
(libraries caqti-driver-sqlite3 dream)
(preprocess (pps lwt_ppx)))
(rule
(targets sql.ml)
(deps sql.eml.ml)
(action (run dream_eml %{deps} --workspace %{workspace_root})))
...and to
esy.json:
"dependencies": {
"@opam/caqti-driver-sqlite3": "*"
}
SQLite is good for small-to-medium sites and examples. For a larger site, microservices, or other needs, you can switch, for example, to PostgreSQL by...
- running PostgreSQL, typically in a Docker container;
- changing the connection URI to
postgres://user:password@host:port; - using
caqti-driver-postgres; - replacing
INTEGER PRIMARY KEY AUTOINCREMENTbySERIAL PRIMARY KEY.
A good program for examining databases locally is Beekeeper Studio. Dream might also integrate an optional hosted database UI in the future, and you could choose to serve it at some route.
See
Caqti_connect_sig.S.CONNECTIONfor Caqti's statement runners. These are the fields of the moduleDbin the example.Caqti_requestsets up prepared statements.Caqti_typeis used to specify the types of statement arguments and results.
Next steps:
