mirror of
https://github.com/aantron/dream.git
synced 2025-10-26 00:02:55 -04:00
Write example/h-sql
This commit is contained in:
parent
b478cb422c
commit
dc30e79a09
BIN
docs/asset/sql.png
Normal file
BIN
docs/asset/sql.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@ -426,7 +426,7 @@ ul ul li {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p + .odoc-spec {
|
p + .odoc-spec {
|
||||||
margin-top: 36px;
|
margin-top: 48px;
|
||||||
}
|
}
|
||||||
.odoc-spec + p {
|
.odoc-spec + p {
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
|
|||||||
@ -33,10 +33,10 @@ list below and jump to whatever interests you!
|
|||||||
prevention.
|
prevention.
|
||||||
- [**`e-json`**](e-json#files) — sends and receives JSON
|
- [**`e-json`**](e-json#files) — sends and receives JSON
|
||||||
securely.
|
securely.
|
||||||
- [**`f-static`**](f-static) — serves static files from a
|
- [**`f-static`**](f-static#files) — serves static files from
|
||||||
local directory.
|
a local directory.
|
||||||
- [**`g-upload`**](g-upload) — receives file uploads.
|
- [**`g-upload`**](g-upload#files) — receives file uploads.
|
||||||
- [**`h-sql`**](h-sql) — finally CRUD!
|
- [**`h-sql`**](h-sql#files) — queries an SQL database.
|
||||||
- [**`i-graphql`**](i-graphql)
|
- [**`i-graphql`**](i-graphql)
|
||||||
- [**`j-stream`**](j-stream)
|
- [**`j-stream`**](j-stream)
|
||||||
- [**`k-websocket`**](k-websocket)
|
- [**`k-websocket`**](k-websocket)
|
||||||
|
|||||||
@ -65,7 +65,7 @@ There are two other session back ends, which are persistent:
|
|||||||
[`Dream.run`](https://aantron.github.io/dream/#val-run) so that it doesn't
|
[`Dream.run`](https://aantron.github.io/dream/#val-run) so that it doesn't
|
||||||
generate a random key each time.
|
generate a random key each time.
|
||||||
- [`Dream.sql_sessions`](https://aantron.github.io/dream/#val-sql_sessions)
|
- [`Dream.sql_sessions`](https://aantron.github.io/dream/#val-sql_sessions)
|
||||||
stores sessions in a database. You can try it after example
|
stores sessions in a database. It's used in example
|
||||||
[**`h-sql`**](../h-sql#files).
|
[**`h-sql`**](../h-sql#files).
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
163
example/h-sql/README.md
Normal file
163
example/h-sql/README.md
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# `h-sql`
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Let's serve a list of comments with a comment form!
|
||||||
|
|
||||||
|
```ocaml
|
||||||
|
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
|
||||||
|
|
||||||
|
let render comments request =
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
% comments |> List.iter (fun (_id, comment) ->
|
||||||
|
<p><%s comment %></p><% ); %>
|
||||||
|
<%s! Dream.Tag.form ~action:"/" request %>
|
||||||
|
<input name="text">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
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.respond (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
|
||||||
|
let%lwt comments = Dream.sql list_comments request in
|
||||||
|
Dream.respond (render comments request)
|
||||||
|
| _ ->
|
||||||
|
Dream.empty `Bad_Request);
|
||||||
|
|
||||||
|
]
|
||||||
|
@@ Dream.not_found
|
||||||
|
```
|
||||||
|
|
||||||
|
<pre><code><b>$ dune exec --root . ./sql.exe</b></code></pre>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Try visiting [http://localhost:8080](http://localhost:8080) and leaving some
|
||||||
|
comments!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Several things are going on in this example. It...
|
||||||
|
|
||||||
|
- sets up the boilerplate for two SQL queries, `list_comments` and
|
||||||
|
`add_comment` with
|
||||||
|
[Caqti](https://paurkedal.github.io/ocaml-caqti/caqti/Caqti_connect_sig/module-type-S/module-type-CONNECTION/index.html);
|
||||||
|
- defines a template, `render`, for our app's main page;
|
||||||
|
- sets up a
|
||||||
|
[pool of SQL connections](https://aantron.github.io/dream/#val-sql_pool) to
|
||||||
|
`db.sqlite`; and
|
||||||
|
- sets up two routes, one for displaying the comment list, and one for
|
||||||
|
receiving new comments from our CSRF-safe form (example
|
||||||
|
[**`d-form`**](../d-form#files)).
|
||||||
|
|
||||||
|
We also take the opportunity to try out
|
||||||
|
[`Dream.sql_sessions`](https://aantron.github.io/dream/#val-sql_sessions), which
|
||||||
|
stores session data persistently in `db.sqlite`! See example
|
||||||
|
[**`b-session`**](../b-session#files) for an introduction to session management.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
`db.sqlite` was initialized with this schema, using the `sqlite3` command:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE comment (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
text NOT NULL);
|
||||||
|
|
||||||
|
CREATE TABLE dream_session (
|
||||||
|
key TEXT NOT NULL PRIMARY KEY,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
expires_at REAL NOT NULL,
|
||||||
|
payload TEXT NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
We also had to make an addition to our Dune file:
|
||||||
|
|
||||||
|
<pre>(executable
|
||||||
|
(name sql)
|
||||||
|
(libraries <b>caqti-driver-sqlite3</b> dream)
|
||||||
|
(preprocess (pps lwt_ppx)))
|
||||||
|
|
||||||
|
(rule
|
||||||
|
(targets sql.ml)
|
||||||
|
(deps sql.eml.ml)
|
||||||
|
(action (run dream_eml %{deps} --workspace %{workspace_root})))
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<!-- TODO Recommend a redirect for better refresh behavior. -->
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
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](https://hub.docker.com/_/postgres/);
|
||||||
|
- changing the connection URI to `postgres://user:password@host:port`;
|
||||||
|
- using `caqti-driver-postgres`;
|
||||||
|
- replacing `INTEGER PRIMARY KEY AUTOINCREMENT` by `SERIAL PRIMARY KEY`.
|
||||||
|
|
||||||
|
A good program for examining the database locally is
|
||||||
|
[Beekeeper Studio](https://www.beekeeperstudio.io/). Dream might also integrate
|
||||||
|
an optional hosted database UI in the future, and you could choose to serve it
|
||||||
|
at some route.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
See
|
||||||
|
|
||||||
|
- [`Caqti_connect_sig.S.CONNECTION`](https://paurkedal.github.io/ocaml-caqti/caqti/Caqti_connect_sig/module-type-S/module-type-CONNECTION/index.html)
|
||||||
|
for Caqti's statement runners. These are the fields of the module `Db` in the
|
||||||
|
example.
|
||||||
|
- [`Caqti_request`](https://paurkedal.github.io/ocaml-caqti/caqti/Caqti_request/)
|
||||||
|
sets up prepared statements.
|
||||||
|
- [`Caqti_type`](https://paurkedal.github.io/ocaml-caqti/caqti/Caqti_type/) is
|
||||||
|
used to specify the types of statement arguments and results.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
**Next steps:**
|
||||||
|
|
||||||
|
- [**`i-graphql`**](../i-graphql#files) handles GraphQL queries and serves
|
||||||
|
GraphiQL.
|
||||||
|
- [**`j-stream`**](../j-stream#files) streams response bodies to clients.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
[Up to the tutorial index](../#readme)
|
||||||
BIN
example/h-sql/db.sqlite
Normal file
BIN
example/h-sql/db.sqlite
Normal file
Binary file not shown.
9
example/h-sql/dune
Normal file
9
example/h-sql/dune
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
(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})))
|
||||||
1
example/h-sql/dune-project
Normal file
1
example/h-sql/dune-project
Normal file
@ -0,0 +1 @@
|
|||||||
|
(lang dune 2.0)
|
||||||
53
example/h-sql/sql.eml.ml
Normal file
53
example/h-sql/sql.eml.ml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
let render comments request =
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
% comments |> List.iter (fun (_id, comment) ->
|
||||||
|
<p><%s comment %></p><% ); %>
|
||||||
|
<%s! Dream.Tag.form ~action:"/" request %>
|
||||||
|
<input name="text">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
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.respond (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
|
||||||
|
let%lwt comments = Dream.sql list_comments request in
|
||||||
|
Dream.respond (render comments request)
|
||||||
|
| _ ->
|
||||||
|
Dream.empty `Bad_Request);
|
||||||
|
|
||||||
|
]
|
||||||
|
@@ Dream.not_found
|
||||||
@ -706,7 +706,7 @@ val origin_referer_check : middleware
|
|||||||
- their value must match [Host:]
|
- their value must match [Host:]
|
||||||
|
|
||||||
Responds with [400 Bad Request] if the check fails. See example
|
Responds with [400 Bad Request] if the check fails. See example
|
||||||
{{:https://github.com/aantron/dream/tree/master/example/e-json#files}
|
{{:https://github.com/aantron/dream/tree/master/example/e-json#security}
|
||||||
[e-json]}.
|
[e-json]}.
|
||||||
|
|
||||||
Implements the
|
Implements the
|
||||||
@ -1408,9 +1408,12 @@ val graphiql : string -> handler
|
|||||||
|
|
||||||
Dream provides thin convenience functions over
|
Dream provides thin convenience functions over
|
||||||
{{:https://github.com/paurkedal/ocaml-caqti/#readme} Caqti}, an SQL
|
{{:https://github.com/paurkedal/ocaml-caqti/#readme} Caqti}, an SQL
|
||||||
interface with several back ends. Dream installs the core
|
interface with several back ends. See example
|
||||||
{{:https://opam.ocaml.org/packages/caqti/} [caqti]} package, but you should
|
{{:https://github.com/aantron/dream/tree/master/example/h-sql#files}
|
||||||
also install at least one of:
|
[h-sql]}.
|
||||||
|
|
||||||
|
Dream installs the core {{:https://opam.ocaml.org/packages/caqti/} [caqti]}
|
||||||
|
package, but you should also install at least one of:
|
||||||
|
|
||||||
- {{:https://opam.ocaml.org/packages/caqti-driver-sqlite3/}
|
- {{:https://opam.ocaml.org/packages/caqti-driver-sqlite3/}
|
||||||
[caqti-driver-sqlite3]}
|
[caqti-driver-sqlite3]}
|
||||||
@ -1435,7 +1438,9 @@ val sql_pool : ?size:int -> string -> middleware
|
|||||||
|
|
||||||
(* TODO Work out the example. *)
|
(* TODO Work out the example. *)
|
||||||
val sql : (Caqti_lwt.connection -> 'a promise) -> request -> 'a promise
|
val sql : (Caqti_lwt.connection -> 'a promise) -> request -> 'a promise
|
||||||
(** Runs the callback with a connection from the SQL pool.
|
(** Runs the callback with a connection from the SQL pool. See example
|
||||||
|
{{:https://github.com/aantron/dream/tree/master/example/h-sql#files}
|
||||||
|
[h-sql]}.
|
||||||
|
|
||||||
{[
|
{[
|
||||||
let () =
|
let () =
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user