diff --git a/docs/asset/sql.png b/docs/asset/sql.png new file mode 100644 index 0000000..a464918 Binary files /dev/null and b/docs/asset/sql.png differ diff --git a/docs/web/site/docs.css b/docs/web/site/docs.css index ed47e55..f01555a 100644 --- a/docs/web/site/docs.css +++ b/docs/web/site/docs.css @@ -426,7 +426,7 @@ ul ul li { } p + .odoc-spec { - margin-top: 36px; + margin-top: 48px; } .odoc-spec + p { padding-top: 24px; diff --git a/example/README.md b/example/README.md index a1de8a7..2da8e31 100644 --- a/example/README.md +++ b/example/README.md @@ -33,10 +33,10 @@ list below and jump to whatever interests you! prevention. - [**`e-json`**](e-json#files)  —  sends and receives JSON securely. -- [**`f-static`**](f-static)  —  serves static files from a - local directory. -- [**`g-upload`**](g-upload)  —  receives file uploads. -- [**`h-sql`**](h-sql)  —  finally CRUD! +- [**`f-static`**](f-static#files)  —  serves static files from + a local directory. +- [**`g-upload`**](g-upload#files)  —  receives file uploads. +- [**`h-sql`**](h-sql#files)  —  queries an SQL database. - [**`i-graphql`**](i-graphql) - [**`j-stream`**](j-stream) - [**`k-websocket`**](k-websocket) diff --git a/example/b-session/README.md b/example/b-session/README.md index 6d8f8bd..06acc13 100644 --- a/example/b-session/README.md +++ b/example/b-session/README.md @@ -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 generate a random key each time. - [`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).
diff --git a/example/h-sql/README.md b/example/h-sql/README.md new file mode 100644 index 0000000..2e59629 --- /dev/null +++ b/example/h-sql/README.md @@ -0,0 +1,163 @@ +# `h-sql` + +
+ +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 = + + +% comments |> List.iter (fun (_id, comment) -> +

<%s comment %>

<% ); %> + <%s! Dream.Tag.form ~action:"/" request %> + + + + + +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 +``` + +
$ dune exec --root . ./sql.exe
+ +
+ +Try visiting [http://localhost:8080](http://localhost:8080) and leaving some +comments! + +![Comments](https://raw.githubusercontent.com/aantron/dream/master/docs/asset/sql.png) + +
+ +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. + +
+ +`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: + +
(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})))
+
+ + + +
+ +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. + +
+ +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. + +
+
+ +**Next steps:** + +- [**`i-graphql`**](../i-graphql#files) handles GraphQL queries and serves + GraphiQL. +- [**`j-stream`**](../j-stream#files) streams response bodies to clients. + +
+ +[Up to the tutorial index](../#readme) diff --git a/example/h-sql/db.sqlite b/example/h-sql/db.sqlite new file mode 100644 index 0000000..e43886e Binary files /dev/null and b/example/h-sql/db.sqlite differ diff --git a/example/h-sql/dune b/example/h-sql/dune new file mode 100644 index 0000000..e87930e --- /dev/null +++ b/example/h-sql/dune @@ -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}))) diff --git a/example/h-sql/dune-project b/example/h-sql/dune-project new file mode 100644 index 0000000..929c696 --- /dev/null +++ b/example/h-sql/dune-project @@ -0,0 +1 @@ +(lang dune 2.0) diff --git a/example/h-sql/sql.eml.ml b/example/h-sql/sql.eml.ml new file mode 100644 index 0000000..307f824 --- /dev/null +++ b/example/h-sql/sql.eml.ml @@ -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 = + + +% comments |> List.iter (fun (_id, comment) -> +

<%s comment %>

<% ); %> + <%s! Dream.Tag.form ~action:"/" request %> + + + + + +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 diff --git a/src/dream.mli b/src/dream.mli index af1030d..df5040b 100644 --- a/src/dream.mli +++ b/src/dream.mli @@ -706,7 +706,7 @@ val origin_referer_check : middleware - their value must match [Host:] 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]}. Implements the @@ -1408,9 +1408,12 @@ val graphiql : string -> handler Dream provides thin convenience functions over {{:https://github.com/paurkedal/ocaml-caqti/#readme} Caqti}, an SQL - interface with several back ends. Dream installs the core - {{:https://opam.ocaml.org/packages/caqti/} [caqti]} package, but you should - also install at least one of: + interface with several back ends. See example + {{:https://github.com/aantron/dream/tree/master/example/h-sql#files} + [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/} [caqti-driver-sqlite3]} @@ -1435,7 +1438,9 @@ val sql_pool : ?size:int -> string -> middleware (* TODO Work out the example. *) 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 () =