diff --git a/README.md b/README.md index c3d0fcd..80b70aa 100644 --- a/README.md +++ b/README.md @@ -1 +1,9 @@ -# datomic-tutorials +# Datomic Tutorials + +## Overview + +A repository of examples on how to use Datomic and learn in a practical way from the Datomic team. Follow along at [tutorials.datomic.com](https://tutorials.datomic.com/). + +## Contact + +More questions? [Find out how to reach the community.](https://www.datomic.com/contact.html) \ No newline at end of file diff --git a/todo-app/part-1/todo-app/README.md b/todo-app/part-1/todo-app/README.md index e1418b9..04527ce 100644 --- a/todo-app/part-1/todo-app/README.md +++ b/todo-app/part-1/todo-app/README.md @@ -42,13 +42,13 @@ Java(TM) SE Runtime Environment Oracle GraalVM 21.0.4+8.1 (build 21.0.4+8-LTS-jv Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 21.0.4+8.1 (build 21.0.4+8-LTS-jvmci-23.1-b41, mixed mode, sharing) ``` -It should work with Java 8, 11, 17 and 21 +It should work with Java 8, 11, 17 and 21. -### Before you start +### Before You Start -This tutorial expects that you're familiar with Clojure and progamming at REPL. If you are totally new to Clojure, you should seek out some more introductory content before coming back here. Clojure is the absolute most important requirement to use Datomic effectively, the better you get at the language the better you'll get at using Datomic. +This tutorial expects that you're familiar with Clojure and progamming at REPL. If you are totally new to Clojure, you should seek out some more introductory content before coming back here. Clojure is the absolute most important requirement to use Datomic effectively, the better you get at the language the better you'll get at using Datomic. -Here is a suggestion of content: +Some suggestions to learn more about Clojure: - [Learn Clojure](https://clojure.org/guides/learn/clojure) - [Clojure destructuring](https://clojure.org/guides/destructuring) @@ -57,9 +57,9 @@ Here is a suggestion of content: - [Clojure for](https://clojuredocs.org/clojure.core/for) - [Programming at REPL](https://clojure.org/guides/repl/introduction) -The tutorial can be followed through the REPL in the terminal, if you have your [editor](https://clojure.org/guides/editors) environment to interact with a Clojure REPL run the REPL examples inside your editor. +You can follow the tutorial through the REPL in the terminal. If you have your [editor](https://clojure.org/guides/editors) environment set up to interact with a Clojure REPL, run the REPL examples inside your editor. -We highly recommend to spend time to setup your [editor](https://clojure.org/guides/editors) environment to interact with a Clojure REPL. If you are able to do the following you should be good to go. +We highly recommend spending time to set up your [editor](https://clojure.org/guides/editors) environment to interact with a Clojure REPL. You should be good to go if you can do the following: - Start a REPL and connect it to your editor. - Evaluate Clojure code from a file(namespace) to the REPL. @@ -67,7 +67,7 @@ We highly recommend to spend time to setup your [editor](https://clojure.org/gui ### Glossary -If at any point some word it’s not clear or was left out from clear explanation the official glossary offers a semantically description. +If at any point you're not clear about the meaning of a term, the official glossary offers semantic descriptions. [![img](https://docs.datomic.com/impl/favicon.ico) Glossary | Datomic](https://docs.datomic.com/glossary.html?search=da#datom) @@ -175,20 +175,20 @@ Let's make sure we have the dependencies we need, add the following to `deps.edn ``` For now, we'll focus in the `com.datomic/peer` library. The rest will come handy when building the browser UI. -In the terminal that we created the project structure run +In the terminal that we created the project structure, run: ```shell clj ``` -and a REPL will appear, that means that the dependencies are downloaded correctly +A REPL will appear, which means that the dependencies are downloaded correctly. ```shell Clojure 1.12.0 ;; the version might be different in your machine. user=> ``` -Alternatively you can start the REPL in your editor, **this is the recommended way**. +Alternatively, you can start the REPL in your editor. **This is the recommended way.** #### Schema @@ -196,7 +196,7 @@ Let's think about a schema that will help us get something working. > Lists has many items -That's it, we don't need anything else. Just `List` and `Item` entities. +That's it, we don't need anything else! Just `List` and `Item` entities. ```clojure (def schema @@ -228,7 +228,7 @@ We'll do `:list/items` a [cardinality](https://docs.datomic.com/schema/schema-re The Datomic Peer API names databases with a URI that includes the protocol name, storage connection information, and a database name. The complete URI for a database named "todo" on the transactor you started in the previous step is `datomic:dev://localhost:4334/todo`. -Inside `src/todo_db.clj` add this code. +Inside `src/todo_db.clj`, add this code. ```clojure (ns todo-db @@ -269,7 +269,7 @@ Inside `src/todo_db.clj` add this code. (def conn (d/connect db-uri)) ``` -Load the file to the REPL and then [navigate](https://clojure.org/guides/repl/navigating_namespaces) to the `todo-db` namespace in the REPL. Once there run the following code in the REPL. +Load the file to the REPL and then [navigate](https://clojure.org/guides/repl/navigating_namespaces) to the `todo-db` namespace in the REPL. Once there, run the following code in the REPL: ```clojure ;; REPL @@ -283,6 +283,7 @@ Load the file to the REPL and then [navigate](https://clojure.org/guides/repl/na todo-db> ``` Now we need to load the code that we have in the file to the REPL. Every time we change some code in the file we will need to run the `use` function to load the latest changes. + > If you are using your editor REPL, you can ignore this and make sure to load the code to the REPL. ```clojure @@ -292,6 +293,7 @@ Now we need to load the code that we have in the file to the REPL. Every time we The result should be something like this, two important things occurred. 1. The database is created `(d/create-database db-uri)` 2. Connection is stablished `(def conn (d/connect db-uri))` + ```clojure ;; Result [main] INFO datomic.process-monitor - {:event :metrics/initializing, :metricsCallback clojure.core/identity, :phase :begin, :pid 40809, :tid 1} @@ -339,9 +341,9 @@ Transact the schema with [d/transact](https://docs.datomic.com/clojure/index.htm -9223300668110598104 17592186045419}} ``` -> Datomic supports online schema evolution, meaning you can modify schema while the system is running. No DB downtime to transact schema changes. As we just did, to add novelty to the schema is like transacting other data. +> Datomic supports online schema evolution, meaning you can modify schema while the system is running. No DB downtime to transact schema changes. Adding novelty to the schema, as we just did, is like transacting other data. -With the schema transacted we are able to store some lists and items. For that we'll create a function `new-list` that receives a *list-name* and returns a [datom](https://docs.datomic.com/glossary.html#datom) that we'll use to transact a new List. +With the schema transacted, we are able to store some lists and items. For that, we'll create a function `new-list` that receives a *list-name* and returns a [datom](https://docs.datomic.com/glossary.html#datom) that we'll use to transact a new List. Add the `new-list` function to `todo_db.clj` file and save. ```clojure @@ -362,7 +364,8 @@ To have the `new-list` function available in the REPL, it's necessary to reload #### Transact a New List -Now transact a new list with name "life" +Now, transact a new list with name "life": + ```clojure ;; REPL @(d/transact conn [(new-list "life")]) @@ -377,14 +380,15 @@ Now transact a new list with name "life" ``` Let's breakdown the result, in Datomic is called `tx-report`. + - `:db-before` = database value before the transaction - `:db-after` = database value after the transaction - `:tx-data` = datoms produced by the transaction - `:tempids` = tempid resolution, from the string we chose to the actual value in the database. -The tx-report enables straight forward comparisons between before/after data is transacted, and we can use the db-before to make queries to the past. +The `tx-report` enables straight forward comparisons between before/after data is transacted, and we can use the db-before to make queries to the past. -To showcase a quick example of that, create another list `learn`, but this time we are going to save the result in a var and then access to the result values. +To showcase a quick example of that, create another list called `learn`—but this time we are going to save the result in a var and then access to the result values. ```clojure ;; REPL @@ -414,15 +418,16 @@ To showcase a quick example of that, create another list `learn`, but this time #{} ``` -With the result of `(def tx-report-learn @(d/transact conn [(new-list "learn")])`, you can run the same query with `(:db-after tx-report-learn)` or `(:db-before tx-report-learn)`, the first returns the eid of the list in the database, the second is empty because at that point in time the list didn't existed. This is a simple example of the **out-of-the-box** support for making queries at different moments of time, we'll do more later in the tutorial. +With the result of `(def tx-report-learn @(d/transact conn [(new-list "learn")])`, you can run the same query with `(:db-after tx-report-learn)` or `(:db-before tx-report-learn)`, the first returns the _eid_ of the list in the database, the second is empty because at that point in time the list didn't exist. This is a simple example of the **out-of-the-box** support for making queries at different moments of time. We'll do more later in the tutorial. -*learn more about [Datomic time model](https://docs.datomic.com/whatis/data-model.html#time-model)* +*Learn more about [Datomic time model](https://docs.datomic.com/whatis/data-model.html#time-model)* #### Transact New Items -With a list transacted, let's start adding some items. We'll follow the same pattern: create a function `new-item` that receives a *list-name* and the todo string. We are also including the [db](https://docs.datomic.com/glossary.html#database) as we want to get the eid of the list to make the correct relationship. +With a list transacted, let's start adding some items. We'll follow the same pattern: create a function `new-item` that receives a *list-name* and the todo string. We are also including the [db](https://docs.datomic.com/glossary.html#database) since we want to get the _eid_ of the list to make the correct relationship. + +Add `new-item` function to `todo_db.clj`: -Add `new-item` function to `todo_db.clj` ```clojure (defn new-item [db list-name item-text] {:db/id (d/entid db [:list/name list-name]) @@ -431,9 +436,9 @@ Add `new-item` function to `todo_db.clj` :item/status :item.status/waiting}]}) ``` -In this function, we are making use of [map forms](https://docs.datomic.com/transactions/transaction-data-reference.html#map-forms) as a shorthand for set of additions. +In this function, we are using [map forms](https://docs.datomic.com/transactions/transaction-data-reference.html#map-forms) as a shorthand for set of additions. -The tempid ("item.temp") is for the item entity because the list is persisted in the database and we must use the eid, otherwise it will create a new list. To get the eid of an entity we can use `d/entid` function that receives a db and a [lookup ref](https://docs.datomic.com/schema/identity.html#lookup-refs). +The tempid ("item.temp") is for the item entity because the list persists in the database. Use the _eid_, otherwise it will create a new list. To get the _eid_ of an entity, use the `d/entid` function, which receives a db and a [lookup ref](https://docs.datomic.com/schema/identity.html#lookup-refs). Make sure to **load the file to the REPL**, using `(use 'todo-db :reload)`. Then run the following in the REPL. @@ -449,7 +454,8 @@ Make sure to **load the file to the REPL**, using `(use 'todo-db :reload)`. Then :tx-data [#datom[13194139534320 50 #inst "2025-02-26T19:08:17.539-00:00" 13194139534320 true] #datom[17592186045421 73 17592186045425 13194139534320 true] #datom[17592186045425 75 "travel" 13194139534320 true] #datom[17592186045425 74 17592186045417 13194139534320 true]], :tempids {"item.temp.travel" 17592186045425}} ``` -let's populate it with more data to run some exploration queries. + +Let's populate it with more data to run some exploration queries. ```clojure ;; REPL @@ -459,7 +465,7 @@ let's populate it with more data to run some exploration queries. (d/transact conn)) ``` -It fails. The error below is the exception. Is telling us that we are trying to persist two datoms with the same eid. +It fails. The error below is the exception. Is telling us that we are trying to persist two datoms with the same _eid_. ```clojure {:cause ":db.error/datoms-conflict Two datoms in the same transaction conflict\n{:d1 [17592186045427 :item/text \"play drums\" 13194139534322 true],\n :d2 [17592186045427 :item/text \"scuba dive\" 13194139534322 true]}\n" @@ -470,7 +476,7 @@ It fails. The error below is the exception. Is telling us that we are trying to :tempids {"item.temp" 17592186045427}}} ;; Here is the tempid ``` -Current *new-item* function +Current *new-item* function: ```clojure (defn new-item [db list-name item-text] @@ -480,7 +486,7 @@ Current *new-item* function :item/status :item.status/waiting}]}) ``` -We are generating many items in the same transaction and the tempid needs to be different for each item, otherwise Datomic resolves to the same entity. Let's fix it. +We are generating many items in the same transaction and the tempid needs to be different for each item; otherwise Datomic resolves to the same entity. Let's fix that. Change the function in `todo-db.clj` ```clojure @@ -492,7 +498,7 @@ Change the function in `todo-db.clj` :item/status :item.status/waiting}]})) ``` -Load the file to the REPL and then run +Load the file to the REPL and then run: ```clojure ;; REPL @@ -561,7 +567,7 @@ Add items to the `learn` list. Datomic uses [Datalog](https://docs.datomic.com/whatis/supported-ops.html#datalog) as query engine. A query finds [values ](https://docs.datomic.com/glossary.html#value)in a [database ](https://docs.datomic.com/glossary.html#database) subject to the given constraints, and is specified as [edn](https://docs.datomic.com/glossary.html#edn). Queries are modeled following the same pattern of a Datom `[e a v t]`, if we understand this structure queries can become very powerful. -Currently we transacted 2 lists and a few items. Let's start with some simple queries to get things going. +Currently, we transacted 2 lists and a few items. Let's start with some simple queries to get things going. - **Get lists and their names** @@ -594,7 +600,9 @@ Currently we transacted 2 lists and a few items. Let's start with some simple qu #{["life" 17592186045425] ["life" 17592186045429] ["life" 17592186045428] ["life" 17592186045427] ["learn" 17592186045431] ["learn" 17592186045432] ["learn" 17592186045433] ["learn" 17592186045434]} ``` - What's with that result? A set of vectors repeating the list name? similar to Clojure, at first glance looks different and it's because it is a different way to interact with a database. Let's break it down, first thing we see is the repeating of list name e.g `["life" 17592186045425]` and the same case for "*life*". Our schema is defined as `:db.cardinality/many` on the `:list/items` attribute, in other words we are allowing many items being referenced by `:list/items`, that makes the result make sense, it's telling us that the list "life" has many items, each one being a reference. + What's with that result? A set of vectors repeating the list name? It's similar to Clojure, but at first glance looks different. This is because it's a different way to interact with a database. Let's break that down. + + The first thing we see is the repeating of list name e.g `["life" 17592186045425]` and the same case for "*life*". Our schema is defined as `:db.cardinality/many` on the `:list/items` attribute, in other words we are allowing many items being referenced by `:list/items`. That makes the result make sense. It's telling us that the list "life" has many items, each one a reference. - **Get lists + items different version** @@ -613,7 +621,7 @@ Currently we transacted 2 lists and a few items. Let's start with some simple qu ["life" [17592186045425 17592186045429 17592186045428 17592186045427]]] ``` - The difference is `(vec ?items)` which is grouping all of the items of a list. Also the items are numbers and that is because we are just pulling the reference number to the entity (eid), if we want to get the attributes of the Item we need to ask that explicitly. + The difference is `(vec ?items)`, which is grouping all of the items of a list. The items are also numbers, which is because we are just pulling the reference number to the entity (_eid_). If we want to get the attributes of the Item, we need to ask for that explicitly. **Get lists + items using pull** @@ -639,17 +647,18 @@ Currently we transacted 2 lists and a few items. Let's start with some simple qu #:item{:text "cook rissotto"}]}]] ``` -In this query [pull](https://docs.datomic.com/query/query-pull.html) is very handy as we want to make an association of datoms related to the same entity. +In this query, [pull](https://docs.datomic.com/query/query-pull.html) is very handy because we want to make an association of datoms related to the same entity. -#### Connecting the dots +#### Connecting the Dots So far we have: -- An initial schema. -- A function to create new lists. -- A function to create new items. -- Some queries to fetch the state of the database. -With some embellishment of the code, we can have this initial `src/todo_db.clj` file +- An initial schema +- A function to create new lists +- A function to create new items +- Some queries to fetch the state of the database + +With some embellishment of the code, we can have this initial `src/todo_db.clj` file: ```clojure (ns todo-db @@ -702,9 +711,10 @@ With some embellishment of the code, we can have this initial `src/todo_db.clj` :item/status :item.status/waiting}]})) ``` -That's it for now, in [part 2](../../part-2/todo-app/README.md) we will do CRUD for Lists and Items. We'll create a UI and render the todo lists and items, that will be served by a Pedestal HTTP server and the HTML and CSS by Hiccup. +That's it for now! In [part 2](../../part-2/todo-app/README.md), we will do CRUD for Lists and Items. We'll create a UI and render the todo lists and items, served by a Pedestal HTTP server with HTML and CSS by Hiccup. ## References + - [Datomic - docs](https://docs.datomic.com/datomic-overview.html) - [Datomic - pull](https://docs.datomic.com/query/query-pull.html) - [Datomic - defining a schema](https://docs.datomic.com/schema/schema-reference.html#defining-schema) diff --git a/todo-app/part-2/todo-app/README.md b/todo-app/part-2/todo-app/README.md index a0638c6..1d50480 100644 --- a/todo-app/part-2/todo-app/README.md +++ b/todo-app/part-2/todo-app/README.md @@ -7,11 +7,11 @@ In [Part 1](../../part-1/todo-app/README.md), we completed the following: - Transact new data - Explore query API in the REPL -This part of the tutorial is shorter and more pragmatic than the previous one. You will build a simple HTML & CSS website that will render the List and Items that are in Datomic database. +This part of the tutorial is shorter and more pragmatic than the previous one. You will build a simple HTML & CSS website that will render the List and Items that are in the Datomic database. -### Building the app +## Building the App -#### Project structure +### Project Structure Create the file inside `src/server.clj`. The full project structure will look like this. ``` @@ -22,25 +22,25 @@ Create the file inside `src/server.clj`. The full project structure will look li └── todo_db.clj ``` -If you come from [part 1](../part-1), in the same terminal where you created the project run the following command or create the file inside your text editor. +If you came from [part 1](../part-1), run the following command in the same terminal where you created the project, or create the file inside your text editor. ```shell touch src/server.clj ``` -#### Create a simple UI +### Create a Simple UI The next step is to display the database's data in the browser with a simple UI. To render data in the browser, we'll use two more libraries, [Pedestal](http://pedestal.io/pedestal/0.7/index.html), for HTTP server, and [Hiccup](https://github.com/weavejester/hiccup), for HTML rendering. ![](assets/part2.png) -Essentially we need to make a query that will provide us with the data needed to convey the image above. +Essentially, we need to make a query that will provide us with the data needed to convey the image above. First, create the HTTP server and the [Hiccup](https://github.com/weavejester/hiccup/wiki/Syntax) skeleton, then we will populate it with the results from querying Datomic. **Pedestal routes** -Write the following functions inside `src/server.clj` +Write the following functions inside `src/server.clj`: ```clojure (ns server @@ -82,7 +82,7 @@ Write the following functions inside `src/server.clj` *more about [Pedestal routes](http://pedestal.io/pedestal/0.7/guides/defining-routes.html)* -Run `(start-server)` in the REPL +Run `(start-server)` in the REPL: ```clojure ;;REPL @@ -94,11 +94,11 @@ Run `(start-server)` in the REPL #:io.pedestal.http{:port 8890, :service-fn #function[io.pedestal.http.impl.servlet-interceptor/interceptor-service-fn/fn--23980], :host "localhost", :secure-headers {:content-security-policy-settings "object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http:;"}, :type :jetty, :start-fn #function[io.pedestal.http.jetty/server/fn--24550], :interceptors [#Interceptor{:name :io.pedestal.http.tracing/tracing} #Interceptor{:name :io.pedestal.http/log-request} #Interceptor{:name :io.pedestal.http/not-found} #Interceptor{:name :io.pedestal.http.ring-middlewares/content-type-interceptor} #Interceptor{:name :io.pedestal.http.route/query-params} #Interceptor{:name :io.pedestal.http.route/method-param} #Interceptor{:name :io.pedestal.http.secure-headers/secure-headers} #Interceptor{:name :io.pedestal.http.route/router} #Interceptor{:name :io.pedestal.http.route/path-params-decoder}], :routes ({:path "/", :method :get, :path-re #"/\Q\E", :path-parts [""], :interceptors [#Interceptor{}], :route-name :home, :path-params []}), :servlet #object[io.pedestal.http.servlet.FnServlet 0x56a6e7f3 "io.pedestal.http.servlet.FnServlet@56a6e7f3"], :server #object[org.eclipse.jetty.server.Server 0x71994ca0 "Server@71994ca0{STARTED}[11.0.20,sto=0]"], :join? false, :stop-fn #function[io.pedestal.http.jetty/server/fn--24552]} ``` -navigate to http://localhost:8890/ in the browser, we should see something like this. +Navigate to http://localhost:8890/ in the browser, we should see something like this: ![](assets/hello-world.png) -Now build the HTML (hiccup) skeleton. +Now build the HTML (Hiccup) skeleton. ```clojure (ns server @@ -166,9 +166,9 @@ Now build the HTML (hiccup) skeleton. [:span {:class "badge text-bg-light"} item-status]]])]]]])]]))) ``` -We define `db-example` because we are not going to query Datomic yet (patience). It is great exercise to model how you want the result to be, that will guide you towards how to create the query and even show the strengths or areas of improvement in the schema model. For those familiar with basic HTML we are creating a table. To add the values, loop over the `db-example` which is a vector of maps, each map is a "List", inside the list we have `:list/items` and then loop again to render all the items for each list. +We define `db-example` because we are not going to query Datomic yet (patience!). It is great exercise to model how you want the result to be, that will guide you towards how to create the query and even show the strengths or areas of improvement in the schema model. For those familiar with basic HTML we are creating a table. To add the values, loop over the `db-example` which is a vector of maps, with each map a "List." Inside the list, we have `:list/items`. Then loop again to render all the items for each list. -Let's glue all things together in one file `src/server.clj` +Let's glue all things together in one file `src/server.clj`: ```clojure (ns server @@ -257,18 +257,18 @@ Let's glue all things together in one file `src/server.clj` (start-server)) ``` -Load the file to the REPL and then restart the server +Load the file to the REPL and then restart the server. ```clojure ;; REPL (restart-server) ``` -now let's got to our browser and navigate to http://localhost:8890/ , we should see something like this. +Now, let's got to our browser and navigate to http://localhost:8890/ , we should see something like this: ![](assets/part2.png) -Awesome, we have our beautiful UI working, we don't need anything else for now. Next step is to fetch the data from Datomic instead of db-example. Let's go back to the `src/todo_db.clj` file and take a look at the query we use previously to fetch lists and items. +Awesome, we have our beautiful UI working. We don't need anything else for now. The next step is to fetch the data from Datomic instead of `db-example`. Let's go back to the `src/todo_db.clj` file and take a look at the query we used previously to fetch lists and items. ```clojure (d/q '[:find (pull ?list [:list/name {:list/items [:item/text]}]) @@ -293,7 +293,7 @@ Awesome, we have our beautiful UI working, we don't need anything else for now. #:item{:text "cook rissotto",}]}]] ``` -It's actually very close to what we want, instead of vectors of maps we have vector of vectors. We want to tell Datomic to bind the results into a collection, we can make use of the [binding forms](https://docs.datomic.com/query/query-data-reference.html#binding-forms), particularly the collection one `[?a ...]` , this tells Datomic to return the results as a collection of the results, it's a way to flatten the results. +It's actually very close to what we want. Instead of vectors of maps, we have a vector of vectors. We want to tell Datomic to bind the results into a collection. For this, use the [binding forms](https://docs.datomic.com/query/query-data-reference.html#binding-forms), particularly the collection one `[?a ...]`. This tells Datomic to return the results as a collection of the results, as a way to flatten the results. ```clojure (d/q '[:find [(pull ?list [:list/name {:list/items [:item/text]}]) ...] @@ -301,7 +301,7 @@ It's actually very close to what we want, instead of vectors of maps we have vec :where [?list :list/name ?list-name]] (d/db conn)) ``` -> pull is a very powerful API, it's straight forward to pull nested data without the need of joins, Datalog makes the navigation of relationships seamlessly, it has cleaner and simple semantics, codebases tend to be be more expressive and easier to understand. +> `pull` is a very powerful API. It's straightforward to pull nested data without the need of joins. Datalog makes the navigation of relationships seamless. With cleaner and simple semantics, codebases tend to be be more expressive and easier to understand. ```clojure ;; => @@ -319,9 +319,9 @@ It's actually very close to what we want, instead of vectors of maps we have vec #:item{:text "cook rissotto",}]}] ``` -The result is in the form we need, now it's matter of making the query and passing the result to the render function. Before that we create a `lists-page` function that will execute the query and place it inside `todo_db.clj`. We will also add the `:db/id` in our query result because that's the id we will use to update or retract datoms and also include the item status, `:item/status`. +The result is in the form we need. Now, it's matter of making the query and passing the result to the render function. Before that, we create a `lists-page` function that will execute the query and place it inside `todo_db.clj`. We will also add the `:db/id` in our query result because that's the _id_ we will use to update or retract datoms and also include the item status, `:item/status`. -Add the query to `src/todo_db.clj` save and load the file to the REPL +Add the query to `src/todo_db.clj` save and load the file to the REPL: ```clojure (defn lists-page [db] @@ -331,9 +331,9 @@ Add the query to `src/todo_db.clj` save and load the file to the REPL db)) ``` -*more about :db/ident [here](https://docs.datomic.com/schema/identity.html#idents)* +*More about :db/ident [here](https://docs.datomic.com/schema/identity.html#idents)* -In the `server.clj` file we make some modifications to our `all-lists-page` render function. +In the `server.clj` file, we make some modifications to our `all-lists-page` render function. ```clojure (ns server-experiment @@ -414,15 +414,17 @@ In the `server.clj` file we make some modifications to our `all-lists-page` rend (start-server)) ``` -the `todo-db/list-page` receives a db as parameter, to get the current db in Datomic we call `d/db` which receives a connection and for this project we define the connection inside the `todo-db/conn`. With the query result we just change the value we were passing to the `for` and we should be able to get the same output. -Load the file to the REPL and call `(restart-server)` +The `todo-db/list-page` receives a db as parameter, to get the current db in Datomic we call `d/db` which receives a connection and for this project we define the connection inside the `todo-db/conn`. With the query result, we just change the value we were passing to the `for` and we should be able to get the same output. + +Load the file to the REPL and call `(restart-server)`: + ```clojure ;;REPL (restart-server) ``` -go to http://localhost:8890/ hit reload and you should see the the items with the status too, all served by Datomic. +Go to http://localhost:8890/ hit reload and you should see the the items with the status too, all served by Datomic. -### Resources +## Resources - [Pedestal routes](http://pedestal.io/pedestal/0.7/guides/defining-routes.html) - [Hiccup basic syntax](https://github.com/weavejester/hiccup/wiki/Syntax) diff --git a/todo-app/part-3/todo-app/README.md b/todo-app/part-3/todo-app/README.md new file mode 100644 index 0000000..201d7c1 --- /dev/null +++ b/todo-app/part-3/todo-app/README.md @@ -0,0 +1,763 @@ +# Building a TODO List App with Clojure + Datomic Pro - [Part 3] + +In [Part 2](../../part-2/todo-app/README.md), we completed the following: + +- Serve a website with Pedestal and Hiccup +- Create a query to load the List and Items + +Part 3 is about CRUD. You will explore the _t/transact_ API to create, update and delete entities (such as List and Items). These are the actions that we want to support: + +- Create list +- Delete list +- Add item to list +- Delete item +- Update item status + +### Building the App + +#### Create and Delete Lists + +To start with, run the REPL in your editor or in the terminal. + +```shell +clj +``` + +```shell +;; REPL +user> +``` + +As we are using native HTML, we will use `forms` to make requests to the server. The form does a POST to `/lists` and it will include `new-list` argument with the name of the list. + +```clojure +(def new-list-form + [:form {:action "/lists" :method "POST" :class "row row-cols-md-auto form-inline"} + [:div {:class "col"} [:input {:type "text" :name "new-list" :class "form-control"}]] + [:div {:class "col"} [:input {:type "submit" :value "new list" :class "btn btn-secondary btn-s"}]]]) +``` + +Add the function to `src/server.clj`: + +```clojure +(ns server + (:require + [datomic.api :as d] ;; new + [hiccup.page :as hp] + [hiccup2.core :as h] + [io.pedestal.http :as http] + [io.pedestal.http.route :as route] + [todo-db :as todo-db])) ;; new + +(defn gen-page-head + "Include bootstrap css" + [title] + [:head + [:title title] + (hp/include-css "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css")]) + +(def new-list-form ;; new + [:form {:action "/lists" :method "POST" :class "row row-cols-md-auto form-inline"} + [:div {:class "col"} [:input {:type "text" :name "new-list" :class "form-control"}]] + [:div {:class "col"} [:input {:type "submit" :value "new list" :class "btn btn-secondary btn-s"}]]]) + +(defn all-lists-page + "Renders the TODO list page" + [_request] + (str + (h/html + (gen-page-head "TODO App with Clojure + Datomic") + [:div {:class "container"} + [:div {:class "bg-light rounded-3" :style "padding: 20px"} + [:h1 {:style "color: #1cb14a"} "Lists"] + new-list-form] ;; new + [:div {:class "row row-cols-2"} + (for [list (todo-db/lists-page (d/db todo-db/conn)) ;; Important line + :let [list-name (:list/name list)]] + [:div {:class "col card"} + [:div {:class "card-body"} + [:h4 {:class "card-title"} list-name] + [:table {:class "table mb-4"} + [:thead + [:tr + [:th {:scope "col"} "Item"] + [:th {:scope "col"} "Status"]]] + [:tbody + (for [item (:list/items list) + :let [item-text (get item :item/text) + item-status (get-in item [:item/status :db/ident])]] ;; new + [:tr + [:td item-text] + [:td + [:span {:class "badge text-bg-light"} item-status]]])]]]])]]))) + +(defn html-200 [body] + {:status 200 + :headers {"Content-Type" "text/html; charset=utf-8"} + :body body}) + +(def routes + (route/expand-routes + #{["/" :get (comp html-200 all-lists-page) :route-name :home]})) + +(defn create-server [] + (http/create-server + {::http/routes routes + ::http/secure-headers {:content-security-policy-settings "object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http:;"} + ::http/type :jetty + ::http/port 8890 + ::http/join? false})) + +(defonce server (atom nil)) + +(defn start-server [] + (reset! server (-> (create-server) http/start))) + +(defn stop-server [] + (swap! server http/stop)) + +(defn restart-server [] + (stop-server) + (start-server)) +``` + +Load the file to the REPL and `(start-server)`: + +```clojure +;; Result +(start-server) +``` + +Navigate to http://localhost:8890/ in the browser, where you should see the new list text box area. + +![](assets/new-list.png) + +Receive the request in Pedestal routes and create a new list with the name received by the form. Add `[io.pedestal.http.body-params :as body-params]`in the `:require`. That namespace contains a function to parse `form-params` and it's used inside the routes. + +```clojure +(def routes + (route/expand-routes + #{["/" :get (comp html-200 all-lists-page) :route-name :home] + ["/lists" :post [(body-params/body-params) new-list] :route-name :new-list]})) ;; new +``` + +*Learn more about [Pedestal interceptors](https://pedestal.io/pedestal/0.7/guides/defining-routes.html#_interceptors)* + +Interceptors are functions that can be composed. They receive the request context and the last function is a handler that will return the final response. In this case, it's `new-list`, so let's create it. + +```clojure +(def html-302-response + {:status 302 + :headers {"Content-Type" "text/html; charset=utf-8" "Location" "/"} + :body ""}) + +(defn new-list [{:keys [form-params] :as _request}] + (let [list-name (:new-list form-params)] + (d/transact todo-db/conn [(todo-db/new-list (d/db todo-db/conn) list-name)]) + html-302-response)) +``` + +The function receives the the context from the Pedestal request and access to the form-params. It reads the `:new-list` key that contains the name inputted in the UI. Then it proceeds to transact to Datomic. The 302 status is returned because we want to comeback to the initial page and not to the path where the POST was made. + +Add the function to `src/server.clj`: + +```clojure +(ns server + (:require + [datomic.api :as d] + [hiccup.page :as hp] + [hiccup2.core :as h] + [io.pedestal.http.body-params :as body-params] ;; new + [io.pedestal.http :as http] + [io.pedestal.http.route :as route] + [todo-db :as todo-db])) + +(defn gen-page-head + "Include bootstrap css" + [title] + [:head + [:title title] + (hp/include-css "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css")]) + +(def new-list-form + [:form {:action "/lists" :method "POST" :class "row row-cols-md-auto form-inline"} + [:div {:class "col"} [:input {:type "text" :name "new-list" :class "form-control"}]] + [:div {:class "col"} [:input {:type "submit" :value "new list" :class "btn btn-secondary btn-s"}]]]) + +(defn all-lists-page + "Renders the TODO list page" + [_request] + (str + (h/html + (gen-page-head "TODO App with Clojure + Datomic") + [:div {:class "container"} + [:div {:class "bg-light rounded-3" :style "padding: 20px"} + [:h1 {:style "color: #1cb14a"} "Lists"] + new-list-form] + [:div {:class "row row-cols-2"} + (for [list (todo-db/lists-page (d/db todo-db/conn)) ;; Important line + :let [list-name (:list/name list)]] + [:div {:class "col card"} + [:div {:class "card-body"} + [:h4 {:class "card-title"} list-name] + [:table {:class "table mb-4"} + [:thead + [:tr + [:th {:scope "col"} "Item"] + [:th {:scope "col"} "Status"]]] + [:tbody + (for [item (:list/items list) + :let [item-text (get item :item/text) + item-status (get-in item [:item/status :db/ident])]] ;; new + [:tr + [:td item-text] + [:td + [:span {:class "badge text-bg-light"} item-status]]])]]]])]]))) + +(def html-302-response ;; new + {:status 302 + :headers {"Content-Type" "text/html; charset=utf-8" "Location" "/"} + :body ""}) + +(defn html-200 [body] + {:status 200 + :headers {"Content-Type" "text/html; charset=utf-8"} + :body body}) + +(defn new-list [{:keys [form-params] :as _request}] ;;new + (let [list-name (:new-list form-params)] + (d/transact todo-db/conn [(todo-db/new-list list-name)]) + html-302-response)) + +(def routes + (route/expand-routes + #{["/" :get (comp html-200 all-lists-page) :route-name :home] + ["/lists" :post [(body-params/body-params) new-list] :route-name :new-list]})) ;; new + +(defn create-server [] + (http/create-server + {::http/routes routes + ::http/secure-headers {:content-security-policy-settings "object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http:;"} + ::http/type :jetty + ::http/port 8890 + ::http/join? false})) + +(defonce server (atom nil)) + +(defn start-server [] + (reset! server (-> (create-server) http/start))) + +(defn stop-server [] + (swap! server http/stop)) + +(defn restart-server [] + (stop-server) + (start-server)) +``` + +Load the file to the REPL and `(restart-server)`: + +```clojure +;; REPL +(restart-server) +``` + +Now, let's got to http://localhost:8890/, where we should see something like this: + +GIF of creating a new list + +Great, let's move on to `retraction` to represent the deletion of a list. For that we will use the [:retractEntity](https://docs.datomic.com/transactions/transaction-functions.html#dbfn-retractentity) function. It will receive the _listeid_ and it retracts all the attribute values where the given entity _id_ is either the entity or value, effectively retracting the entity's own data and any references to the entity as well. + +```clojure +(defn retract-list [listeid] + [:db/retractEntity listeid]) +``` +It's all about Datoms. Pass it to `d/transact`: + +```clojure +(defn retract-list [{:keys [path-params] :as _request}] + (let [listeid (Long/parseLong (:list-id path-params))] ;; Datomic expects a Long and the path-params are received as string + (d/transact todo-db/conn [(todo-db/retract-list listeid)]) + html-302-response)) +``` + +Then add the route and call the function: + +```clojure +(def routes + (route/expand-routes + #{["/" :get (comp html-200 all-lists-page) :route-name :home] + ["/lists" :post [(body-params/body-params) new-list] :route-name :new-list] + ["/lists/:list-id/retract" :post [(body-params/body-params) retract-list] :route-name :retract-list]})) ;; new +``` + +### Create and Delete Items + +It's the same idea for the items. Add a transaction to add the new Datoms and a retraction to remove the item. Here is a list of the things that we will do: + +- Create a `new-item` function in the server.clj file. +- Add the `lists/:list-id/items/:item-id` route and call `new-item` handler function. +- Add Hiccup form to create a new item. +- Create a `retract-item` function in the server.clj file. +- Add the `lists/:list-id/items/:item-id/delete` route and call `retract-item` handler function. + +Let's start with the creation of new items. + +```clojure +(defn new-item [{:keys [path-params form-params] :as _request}] + (let [listeid (Long/parseLong (:list-id path-params)) + item-text (:new-item form-params)] + (d/transact todo-db/conn [(todo-db/new-item (d/db todo-db/conn) listeid item-text)]) + html-302-response)) +``` + +In the `path-params`, we have access to the `listeid` and the current `todo-db/new-item` function expects the name. Go to `src/todo_db.clj` and modify the _new-item_ function to receive the `listeid` instead of the name. + +From: + +```clojure +(defn new-item [db list-name item-text] + (let [minify (clojure.string/replace item-text #" " "-")] + {:db/id (d/entid db [:list/name list-name]) + :list/items [{:db/id (str "item.temp." minify) ;; IMPORTANT line + :item/text item-text + :item/status :item.status/waiting}]})) +``` + +To: + +```clojure +(defn new-item [db listeid item-text] ;; list-name -> listeid + (let [minify (clojure.string/replace item-text #" " "-")] + {:db/id listeid ;; new + :list/items [{:db/id (str "item.temp." minify) ;; IMPORTANT line + :item/text item-text + :item/status :item.status/waiting}]})) +``` + +Make sure to load the changes of `todo_db.clj` to the REPL. Now let's move to `src/server.clj` and add the route. + +```clojure +(def routes + (route/expand-routes + #{["/" :get (comp html-200 all-lists-page) :route-name :home] + ["/lists" :post [(body-params/body-params) new-list] :route-name :new-list] + ["/lists/:list-id/retract" :post [(body-params/body-params) retract-list] :route-name :retract-list] + ["/lists/:list-id/items" :post [(body-params/body-params) new-item] :route-name :new-item]})) ;; new +``` + +To finish the creation of the item, add the new item form. + +```clojure +(defn new-item-form [list-id] + [:form {:action (str "/lists/" list-id "/items") + :method "POST" + :class "row row-cols-sm-auto form-inline"} + [:div {:class "col"} [:input {:type "text" :name "new-item" :class "form-control"}]] + [:div {:class "col"} [:input {:type "submit" :value "add" :class "btn btn-light btn-s"}]]]) +``` + +Place it below the `[:h4 {:class "card-title"} list-name]`: + +```clojure +(defn all-lists-page + "Renders the TODO list page" + [_request] + (str + (h/html + (gen-page-head "TODO App with Clojure + Datomic") + [:div {:class "container"} + [:div {:class "bg-light rounded-3" :style "padding: 20px"} + [:h1 {:style "color: #1cb14a"} "Lists"] + new-list-form] + [:div {:class "row row-cols-2"} + (for [list (todo-db/lists-page (d/db todo-db/conn)) ;; Important line + :let [list-name (:list/name list) + list-id (:db/id list)]] + [:div {:class "col card"} + [:div {:class "card-body"} + [:form {:action (str "/lists/" list-id "/retract") :method "POST" :class ""} + [:input {:type "submit" :value "" :class "btn-close"}]] + [:h4 {:class "card-title"} list-name] + (new-item-form list-id) ;; new + [:table {:class "table mb-4"} + [:thead + [:tr + [:th {:scope "col"} "Item"] + [:th {:scope "col"} "Status"]]] + [:tbody + (for [item (:list/items list) + :let [item-text (get item :item/text) + item-status (get-in item [:item/status :db/ident])]] ;; new + [:tr + [:td item-text] + [:td + [:span {:class "badge text-bg-light"} item-status]]])]]]])]]))) +``` + +Load the file to the REPL and `(restart-server)`: + +```clojure +;; REPL +(restart-server) +``` + +Go to http://localhost:8890/: + +ADD GIF adding a new item + +##### Retract Item + +Go to `src/todo_db.clj` and add the `retract-item` function. + +```clojure +(defn retract-item [itemeid] + [:db/retractEntity itemeid]) +``` + +Call it from `src/server.clj`: + +```clojure +(defn retract-item [{:keys [path-params] :as _request}] + (let [itemeid (Long/parseLong (:item-id path-params))] + (d/transact todo-db/conn [(todo-db/retract-item itemeid)]) + html-302-response)) +``` + +Now, add the function call in the routes: + +```clojure +(def routes + (route/expand-routes + #{["/" :get (comp html-200 all-lists-page) :route-name :home] + ["/lists" :post [(body-params/body-params) new-list] :route-name :new-list] + ["/lists/:list-id/retract" :post [(body-params/body-params) retract-list] :route-name :retract-list] + ["/lists/:list-id/items" :post [(body-params/body-params) new-item] :route-name :new-item] + ["/lists/:list-id/items/:item-id/retract" :post [(body-params/body-params) retract-item] :route-name :retract-item]})) +``` + +Finally, add the Hiccup form to retract. Add one more column to the table named `actions`. Put the retract button here for the transition to a different status. + +```clojure +(defn all-lists-page + "Renders the TODO list page" + [_request] + (str + (h/html + (gen-page-head "TODO App with Clojure + Datomic") + [:div {:class "container"} + [:div {:class "bg-light rounded-3" :style "padding: 20px"} + [:h1 {:style "color: #1cb14a"} "Lists"] + new-list-form] + [:div {:class "row row-cols-2"} + (for [list (todo-db/lists-page (d/db todo-db/conn)) + :let [list-name (:list/name list) + list-id (:db/id list)]] + [:div {:class "col card"} + [:div {:class "card-body"} + [:form {:action (str "/lists/" list-id "/retract") :method "POST" :class ""} + [:input {:type "submit" :value "" :class "btn-close"}]] + [:h4 {:class "card-title"} list-name] + (new-item-form list-id) + [:table {:class "table mb-4"} + [:thead + [:tr + [:th {:width "40%" :scope "col"} "Item"] + [:th {:width "20%" :scope "col"} "Status"] + [:th {:width "40%" :scope "col"} "Actions"]]] + [:tbody + (for [item (:list/items list) + :let [item-text (get item :item/text) + item-status (get-in item [:item/status :db/ident]) + item-id (get item :db/id)]] ;;;;;;;;;; new ;;;;;;;;;; + [:tr + [:td item-text] + [:td + [:span {:class "badge text-bg-light"} item-status]] + [:td ;;;;;;;;;;;;; new ;;;;;;;;;;;;;;;; + [:div {:class "row"} + [:div {:class "col-sm-2 align-items-right"} + [:form {:action (str "/lists/" list-id "/items/" item-id "/retract") :method "POST"} + [:input {:type "submit" :value "X" :class "btn btn-sm btn-danger"}]]]]]])]]]])]]))) +``` + +Load the file to the REPL and `(restart-server)`: + +```clojure +;; REPL +(restart-server) +``` + +Go to http://localhost:8890/ and refresh. + +ADD GIF DELETING ITEM + +#### Update Item Status + +This section is about the transition of the Item status. We have three options: `waiting`, `doing` and `done`. We will show a dropdown button that allows the user to choose the desired status and then make the request to the server. The server will receive the request, then call the handler, then make the transaction to the database and return 302 to make the refresh of the main page. + +Go to `src/todo_db.clj` and add the `transition-item` function: + +```clojure +(def namespace-status (comp keyword (partial str "item.status/"))) + +(defn transition-item [itemeid to-status] + [:db/add itemeid :item/status (namespace-status to-status)]) +``` + +When we receive the status in the HTTP request, it comes as string and without the `item.status` namespace. To adapt it so that it aligns with our defined schema enums, we need to add the `item.status` namespace and convert it to a keyword. The `namespace-status` function does that. + +Load the file to the REPL and move to `src/server.clj`. Now create the `transition-item` handler function: + +```clojure +(defn transition-item [{:keys [path-params form-params] :as _request}] + (let [itemeid (Long/parseLong (:item-id path-params)) + new-status (:new-status form-params)] + (d/transact todo-db/conn [(todo-db/transition-item itemeid new-status)]) + html-302-response)) +``` + +Add the Pedestal route: + +```clojure +(def routes + (route/expand-routes + #{["/" :get (comp html-200 all-lists-page) :route-name :home] + ["/lists" :post [(body-params/body-params) new-list] :route-name :new-list] + ["/lists/:list-id/retract" :post [(body-params/body-params) retract-list] :route-name :retract-list] + ["/lists/:list-id/items" :post [(body-params/body-params) new-item] :route-name :new-item] + ["/lists/:list-id/items/:item-id/retract" :post [(body-params/body-params) retract-item] :route-name :retract-item] + ["/lists/:list-id/items/:item-id/transition" :post [(body-params/body-params) transition-item] :route-name :transition-item]})) +``` + +Finally, add the form in the Hiccup code: + +```clojure +(defn todo-statuses-form [list-name todo] + [:form {:action (str "/lists/" list-name "/todos/" (:db/id todo) "/transition") + :method "POST" + :class "row row-cols-lg-auto g-3 align-items-center"} + [:div {:class "col"} + [:select {:class "form-select" :id "floatingSelectGrid" :name "new-status"} + [:option {:selected true} (get-in todo [:todo/status :db/ident])] + [:option {:value "waiting"} "waiting"] + [:option {:value "doing"} "doing"] + [:option {:value "done"} "done"]]] + [:div {:class "col"} + [:input {:type "submit" :value "ok" :class "btn btn-sm btn-secondary "}]]]) +``` + +Then add it ito `all-lists-page`: + +```clojure +(defn all-lists-page + "Renders the TODO list page" + [_request] + (str + (h/html + (gen-page-head "TODO App with Clojure + Datomic") + [:div {:class "container"} + [:div {:class "bg-light rounded-3" :style "padding: 20px"} + [:h1 {:style "color: #1cb14a"} "Lists"] + new-list-form] + [:div {:class "row row-cols-2"} + (for [list (todo-db/lists-page (d/db todo-db/conn)) + :let [list-name (:list/name list) + list-id (:db/id list)]] + [:div {:class "col card"} + [:div {:class "card-body"} + [:form {:action (str "/lists/" list-id "/retract") :method "POST" :class ""} + [:input {:type "submit" :value "" :class "btn-close"}]] + [:h4 {:class "card-title"} list-name] + (new-item-form list-id) + [:table {:class "table mb-4"} + [:thead + [:tr + [:th {:width "40%" :scope "col"} "Item"] + [:th {:width "20%" :scope "col"} "Status"] + [:th {:width "40%" :scope "col"} "Actions"]]] + [:tbody + (for [item (:list/items list) + :let [item-text (get item :item/text) + item-status (get-in item [:item/status :db/ident]) + item-id (get item :db/id)]] + [:tr + [:td item-text] + [:td + [:span {:class "badge text-bg-light"} item-status]] + [:td + [:div {:class "row"} + [:div {:class "col-sm"} ;;;;;;;;;; new ;;;;;;;;;;; + (transition-item-form list-id item)] + [:div {:class "col-sm-2 align-items-right"} + [:form {:action (str "/lists/" list-id "/items/" item-id "/retract") :method "POST"} + [:input {:type "submit" :value "X" :class "btn btn-sm btn-danger"}]]]]]])]]]])]]))) +``` + +Load the file to the REPL and `(restart-server)`, then go to http://localhost:8890/ and refresh. + +ADD GIF of UPDATE + +### Final Code + +You can see the code in [src/server.clj](src/server.clj) and [src/todo_db](src/todo_db). You can also see the final version of both files here: + +```clojure +(ns server + (:require + [datomic.api :as d] + [hiccup.page :as hp] + [hiccup2.core :as h] + [io.pedestal.http.body-params :as body-params] + [io.pedestal.http :as http] + [io.pedestal.http.route :as route] + [todo-db :as todo-db])) + +(defn gen-page-head + "Include bootstrap css" + [title] + [:head + [:title title] + (hp/include-css "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css")]) + +(def new-list-form + [:form {:action "/lists" :method "POST" :class "row row-cols-md-auto form-inline"} + [:div {:class "col"} [:input {:type "text" :name "new-list" :class "form-control"}]] + [:div {:class "col"} [:input {:type "submit" :value "new list" :class "btn btn-secondary btn-s"}]]]) + +(defn new-item-form [list-id] + [:form {:action (str "/lists/" list-id "/items") :method "POST" :class "row row-cols-sm-auto form-inline"} + [:div {:class "col"} [:input {:type "text" :name "new-item" :class "form-control"}]] + [:div {:class "col"} [:input {:type "submit" :value "add" :class "btn btn-light btn-s"}]]]) + +(defn transition-item-form [list-id item] + [:form {:action (str "/lists/" list-id "/items/" (:db/id item) "/transition") + :method "POST" + :class "row row-cols-lg-auto g-3 align-items-center"} + [:div {:class "col"} + [:select {:class "form-select" :id "floatingSelectGrid" :name "new-status"} + [:option {:selected true} (get-in item [:item/status :db/ident])] + [:option {:value "waiting"} "waiting"] + [:option {:value "doing"} "doing"] + [:option {:value "done"} "done"]]] + [:div {:class "col"} + [:input {:type "submit" :value "ok" :class "btn btn-sm btn-secondary "}]]]) + +(defn all-lists-page + "Renders the TODO list page" + [_request] + (str + (h/html + (gen-page-head "TODO App with Clojure + Datomic") + [:div {:class "container"} + [:div {:class "bg-light rounded-3" :style "padding: 20px"} + [:h1 {:style "color: #1cb14a"} "Lists"] + new-list-form] + [:div {:class "row row-cols-2"} + (for [list (todo-db/lists-page (d/db todo-db/conn)) + :let [list-name (:list/name list) + list-id (:db/id list)]] + [:div {:class "col card"} + [:div {:class "card-body"} + [:form {:action (str "/lists/" list-id "/retract") :method "POST" :class ""} + [:input {:type "submit" :value "" :class "btn-close"}]] + [:h4 {:class "card-title"} list-name] + (new-item-form list-id) + [:table {:class "table mb-4"} + [:thead + [:tr + [:th {:width "40%" :scope "col"} "Item"] + [:th {:width "20%" :scope "col"} "Status"] + [:th {:width "40%" :scope "col"} "Actions"]]] + [:tbody + (for [item (:list/items list) + :let [item-text (get item :item/text) + item-status (get-in item [:item/status :db/ident]) + item-id (get item :db/id)]] + [:tr + [:td item-text] + [:td + [:span {:class "badge text-bg-light"} item-status]] + [:td + [:div {:class "row"} + [:div {:class "col-sm"} + (transition-item-form list-id item)] + [:div {:class "col-sm-2 align-items-right"} + [:form {:action (str "/lists/" list-id "/items/" item-id "/retract") :method "POST"} + [:input {:type "submit" :value "X" :class "btn btn-sm btn-danger"}]]]]]])]]]])]]))) + +(def html-302-response + {:status 302 + :headers {"Content-Type" "text/html; charset=utf-8" "Location" "/"} + :body ""}) + +(defn html-200 [body] + {:status 200 + :headers {"Content-Type" "text/html; charset=utf-8"} + :body body}) + +(defn new-list [{:keys [form-params] :as _request}] + (let [list-name (:new-list form-params)] + (d/transact todo-db/conn [(todo-db/new-list list-name)]) + html-302-response)) + +(defn retract-list [{:keys [path-params] :as _request}] + (let [listeid (Long/parseLong (:list-id path-params))] + (d/transact todo-db/conn [(todo-db/retract-list listeid)]) + html-302-response)) + +(defn new-item [{:keys [path-params form-params] :as _request}] + (let [listeid (Long/parseLong (:list-id path-params)) + item-text (:new-item form-params)] + (d/transact todo-db/conn [(todo-db/new-item (d/db todo-db/conn) listeid item-text)]) + html-302-response)) + +(defn retract-item [{:keys [path-params] :as _request}] + (let [itemeid (Long/parseLong (:item-id path-params))] + (d/transact todo-db/conn [(todo-db/retract-item itemeid)]) + html-302-response)) + +(defn transition-item [{:keys [path-params form-params] :as _request}] + (let [itemeid (Long/parseLong (:item-id path-params)) + new-status (:new-status form-params)] + (d/transact todo-db/conn [(todo-db/transition-item itemeid new-status)]) + html-302-response)) + +(def routes + (route/expand-routes + #{["/" :get (comp html-200 all-lists-page) :route-name :home] + ["/lists" :post [(body-params/body-params) new-list] :route-name :new-list] + ["/lists/:list-id/retract" :post [(body-params/body-params) retract-list] :route-name :retract-list] + ["/lists/:list-id/items" :post [(body-params/body-params) new-item] :route-name :new-item] + ["/lists/:list-id/items/:item-id/retract" :post [(body-params/body-params) retract-item] :route-name :retract-item] + ["/lists/:list-id/items/:item-id/transition" :post [(body-params/body-params) transition-item] :route-name :transition-item]})) + +(defn create-server [] + (http/create-server + {::http/routes routes + ::http/secure-headers {:content-security-policy-settings "object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http:;"} + ::http/type :jetty + ::http/port 8890 + ::http/join? false})) + +(defonce server (atom nil)) + +(defn start-server [] + (reset! server (-> (create-server) http/start))) + +(defn stop-server [] + (swap! server http/stop)) + +(defn restart-server [] + (stop-server) + (start-server)) +``` + +#### Retract Item + +### Resources + +- [Pedestal routes](http://pedestal.io/pedestal/0.7/guides/defining-routes.html) +- [Hiccup basic syntax](https://github.com/weavejester/hiccup/wiki/Syntax) +- [Datomic - :db/ident](https://docs.datomic.com/schema/identity.html#idents) +- [Datomic - pull](https://docs.datomic.com/query/query-pull.html) \ No newline at end of file diff --git a/todo-app/part-3/todo-app/assets/new-list.png b/todo-app/part-3/todo-app/assets/new-list.png new file mode 100644 index 0000000..8b83d3d Binary files /dev/null and b/todo-app/part-3/todo-app/assets/new-list.png differ diff --git a/todo-app/part-3/todo-app/deps.edn b/todo-app/part-3/todo-app/deps.edn new file mode 100644 index 0000000..4aead06 --- /dev/null +++ b/todo-app/part-3/todo-app/deps.edn @@ -0,0 +1,5 @@ +{ :deps {org.clojure/clojure {:mvn/version "1.12.0"} + com.datomic/peer {:mvn/version "1.0.7260"} + io.pedestal/pedestal.jetty {:mvn/version "0.7.1"} + org.slf4j/slf4j-simple {:mvn/version "2.0.10"} + hiccup/hiccup {:mvn/version "2.0.0-RC3"}}} \ No newline at end of file diff --git a/todo-app/part-3/todo-app/src/server.clj b/todo-app/part-3/todo-app/src/server.clj new file mode 100644 index 0000000..6838c8f --- /dev/null +++ b/todo-app/part-3/todo-app/src/server.clj @@ -0,0 +1,76 @@ +(ns server + (:require + [datomic.api :as d] + [hiccup.page :as hp] + [hiccup2.core :as h] + [io.pedestal.http :as http] + [io.pedestal.http.route :as route] + [todo-db-2 :as todo-db])) + +(defn gen-page-head + "Include bootstrap css and js" + [title] + [:head + [:title title] + (hp/include-css "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css") + (hp/include-js "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js")]) + +(defn all-lists-page + "Renders the TODO list page" + [_request] + (str + (h/html + (gen-page-head "Datolister") + [:div {:class "container" :style "padding-top: 50px"} + [:h1 "Lists"] + [:div {:class "row row-cols-2"} + (for [list (todo-db/lists-page (d/db todo-db/conn)) ;; Important line + :let [list-name (:list/name list)]] + [:div {:class "col card"} + [:div {:class "card-body"} + [:h4 {:class "card-title"} list-name] + [:table {:class "table mb-4"} + [:thead + [:tr + [:th {:scope "col"} "Item"] + [:th {:scope "col"} "Status"] + [:th {:scope "col"} "Actions"]]] + [:tbody + (for [item (:list/items list) + :let [item-text (get item :item/text) + item-status (get-in item [:item/status :db/ident])]] + [:tr + [:td item-text] + [:td + [:span {:class "badge text-bg-light"} item-status]] + [:td + [:div {:class "row row-cols-auto row-cols-sm"}]]])]]]])]]))) + +(defn html-200 [body] + {:status 200 + :headers {"Content-Type" "text/html; charset=utf-8"} + :body body}) + +(def routes + (route/expand-routes + #{["/" :get (comp html-200 all-lists-page) :route-name :home]})) + +(defn create-server [] + (http/create-server + {::http/routes routes + ::http/secure-headers {:content-security-policy-settings "object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http:;"} + ::http/type :jetty + ::http/port 8890 + ::http/join? false})) + +(defonce server (atom nil)) + +(defn start-server [] + (reset! server (-> (create-server) http/start))) + +(defn stop-server [] + (swap! server http/stop)) + +(defn restart-server [] + (stop-server) + (start-server)) \ No newline at end of file diff --git a/todo-app/part-3/todo-app/src/todo_db.clj b/todo-app/part-3/todo-app/src/todo_db.clj new file mode 100644 index 0000000..3f81000 --- /dev/null +++ b/todo-app/part-3/todo-app/src/todo_db.clj @@ -0,0 +1,61 @@ +(ns todo-db + (:require + [clojure.string :as string] + [datomic.api :as d])) + +(def schema + [{:db/ident :list/name + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/unique :db.unique/identity + :db/doc "List name"} + {:db/ident :list/items + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/many + :db/doc "List items reference"} + {:db/ident :item/status + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one + :db/doc "Item Status"} + {:db/ident :item/text + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/doc "Item text"} + {:db/ident :item.status/todo} + {:db/ident :item.status/doing} + {:db/ident :item.status/done}]) + +(def db-uri "datomic:dev://localhost:4334/todo") + +;; INFO: Creates the database, if it does not exist returns false +(d/create-database db-uri) ;; it requires a running transactor + +;; INFO: to delete a database use `d/delete-database` +(comment (d/delete-database db-uri)) + +;; INFO: Establish connection to the database +(def conn (d/connect db-uri)) + +(comment @(d/transact conn schema)) + +(defn ensure-schema + "verify that schema is transacted" + [conn] + (or (-> conn d/db (d/entid :list/name)) + @(d/transact conn schema))) + +(defn new-list [list-name] + [:db/add "list.id" :list/name list-name]) + +(defn new-item [db list-name item-text] + (let [minify (string/replace item-text #" " "-")] + {:db/id (d/entid db [:list/name list-name]) + :list/items [{:db/id (str "item.temp." minify) + :item/text item-text + :item/status :item.status/todo}]})) + +(defn lists-page [db] + (d/q '[:find [(pull ?list [:db/id :list/name {:list/items [:db/id :item/text {:item/status [:db/ident]}]}]) ...] + :in $ + :where [?list :list/name ?list-name]] + db)) \ No newline at end of file