Adding and Listing Chores
When we consider what features and in which order we should build for the application, the key is to think what brings the most value to the end user of the application. This can be thought from the view point of asking "What would be the value of the feature if we would have to stop the development of the application after finishing this feature?".
A tempting feature to add would be functionality for authentication, but for an application that focuses on chores, that is something that can wait -- let's start with adding and listing chores.
As the table chores
refers to an user, a user is required by the database. For now, let's create an user to the database manually and use the id of the created user. Creating the user is done in the database console.
INSERT INTO users (name, address, email, chorecoins, password) VALUES ('test-name', 'test-address', 'test-email', 999, 'test-password');
Once the user is created, we can query for the id of the user.
SELECT id FROM users;
In our case, the query result is 1
-- that is, the id of the user that was entered to the database is 1
. When building the initial functionality for adding chores, we always set the id of the user who created the chore as 1
.
When adding functionality, we add the functionality as a slice that cuts though the layers in our architecture. That is, we create or modify services that are responsible for handling chores, we create or modify controllers that are responsible for handling chores, and we create the views that are used for handling the chores. This could be done in either direction, but in this case we work from bottom to top.
We first add the functionality for adding a chore, after which we add the functionality for listing chores.
Adding a chore
Service for adding a chore
Let's start with creating a service through which we can create and list chores. Let's call the service choreService.js
and place it in the folder services
. The chore service uses the sql
function from the database.js
, providing the functionality to add and list chores. To add a chore, we need an user id, a title, a description, amount of chorecoins that will be available for completing the chore, and the due date for the chore.
import { sql } from "../database/database.js";
const addChore = async (userId, title, description, chorecoins, dueDate) => {
await sql`INSERT INTO chores
(user_id, title, description, chorecoins, due_date)
VALUES (${userId}, ${title}, ${description}, ${chorecoins}, ${dueDate})`;
};
export { addChore };
Controller for adding a chore
We also create a file called choreController.js
that is placed to the folder controllers
that is inside the folder routes
. For the first version of the controller, we simply read the content from the request body and log them for further analysis. At this point, we do not even attempt to use the choreService.js
.
import * as choreService from "../../services/choreService.js";
const addChore = async ({ request, response }) => {
const body = request.body({ type: "form" });
const params = await body.value;
console.log(params);
response.redirect("/");
};
export { addChore };
Let's also map the addChore
function to the routes. Let's map POST request to the path /chores
to the addChore
function exposed by the choreController.js
. After adding the route, the file routes.js
looks as follows.
import { Router } from "../deps.js";
import * as mainController from "./controllers/mainController.js";
import * as choreController from "./controllers/choreController.js";
const router = new Router();
router.get("/", mainController.showMain);
router.post("/chores", choreController.addChore);
export { router };
At this point, when the application is running, we can send a POST request to the application and check out the logs.
curl -H "Content-Type: application/x-www-form-urlencoded" -X POST -d "title=Meow the lawn" http://localhost:7777/chores
Redirecting to /.%
The log on the server shows something like the following, effectively describing the URLSearchParams object that includes the request body.
URLSearchParams {
[Symbol(list)]: [ [ "title", "Meow the lawn" ] ],
[Symbol("url object")]: null,
[Symbol("[[webidl.brand]]")]: Symbol("[[webidl.brand]]")
}
View for adding a chore
Let's modify the main.eta
and add a form for adding new chores to it. The form will use POST
request and submit the form contents to the path /
. We use an input
field for the chore title, a textarea
for the chore description, an input
field with the type number
for chorecoins, and an input field with the type date
for due date.
After adding the form, the main.eta
looks as follows.
<% layout('./layouts/layout.eta') %>
<h1>Chores!</h1>
<h2>Add a chore!</h2>
<form method="POST" action="/chores">
Title:<br/>
<input type="text" name="title" /><br/>
Description:<br/>
<textarea name="description"></textarea><br/>
Chorecoins:<br/>
<input type="number" name="chorecoins" /><br/>
Due date:<br/>
<input type="date" name="due_date" /><br/>
<input type="submit" value="Add"/>
</form>
When we open the application, fill in data to the form, and submit the form, we see that the form data is logged to the server console.
URLSearchParams {
[Symbol(list)]: [
[ "title", "chore-title" ],
[ "description", "chore-description" ],
[ "chorecoins", "2" ],
[ "due_date", "2080-12-31" ]
],
[Symbol("url object")]: null,
[Symbol("[[webidl.brand]]")]: Symbol("[[webidl.brand]]")
}
As you might notice, if you use a relatively recent browser, the number input and the date input restrict the values that can be used. Moreover, the date input field might pop up a dialog through which the date can be selected.
Back to controller for adding a chore
Now, as the data can be sent from the form to the application, let's modify our application so that we can add data to the database. Effectively, this means changing the controller so that instead of logging the data, we pass the data to the database.
Our first attempt looks as follows. As you may observe, we set the id of the user creating the chore as 1.
import * as choreService from "../../services/choreService.js";
const addChore = async ({ request, response }) => {
const body = request.body({ type: "form" });
const params = await body.value;
await choreService.addChore(
1,
params.get("title"),
params.get("description"),
params.get("chorecoins"),
params.get("due_date"),
);
response.redirect("/");
};
export { addChore };
Now, when we fill in the form and submit it, we are redirected to the front page of the application. Was it this easy?
Let's look at the database and verify whether the data that we sent to the server actually was stored to the database. This is done by performing a simple SELECT
query in the database.
SELECT * FROM chores;
Apparently, everything worked well. There are multiple locations where something might have failed. Specifically, both chorecoins
and due_date
are strings when they are received by the server; however, it seems that they were stored to the database in the correct format.
Let's move on to listing the chores.
Listing chores
Service for listing chores
Let's start with a relatively simple functionality for listing the chores. We simply select all the rows from the table where due_date
is null or where due_date
is in the future. Current time can be retrieved in PostgreSQL using the function NOW()
.
import { sql } from "../database/database.js";
const addChore = async (userId, title, description, chorecoins, dueDate) => {
await sql`INSERT INTO chores
(user_id, title, description, chorecoins, due_date)
VALUES (${userId}, ${title}, ${description}, ${chorecoins}, ${dueDate})`;
};
const listChores = async () => {
const rows = await sql`SELECT * FROM chores
WHERE (due_date IS NULL OR due_date > NOW())`;
return rows;
};
export { addChore, listChores };
Controller for listing chores
Let's first modify the controller so that we have an endpoint that shows the chores retrieved from the database using choreService.js
. In the first version, we simply return the chores as a JSON document.
import * as choreService from "../../services/choreService.js";
const addChore = async ({ request, response }) => {
const body = request.body({ type: "form" });
const params = await body.value;
await choreService.addChore(
1,
params.get("title"),
params.get("description"),
params.get("chorecoins"),
params.get("due_date"),
);
response.redirect("/");
};
const listChores = async ({ response }) => {
response.body = await choreService.listChores();
};
export { addChore, listChores };
Let's map the function listChores
from choreController.js
to GET request made to the path /chores
.
import { Router } from "../deps.js";
import * as mainController from "./controllers/mainController.js";
import * as choreController from "./controllers/choreController.js";
const router = new Router();
router.get("/", mainController.showMain);
router.get("/chores", choreController.listChores);
router.post("/chores", choreController.addChore);
export { router };
Now, when we make a GET request to the path /chores
, we receive a list of chores as a response. In the example below, we have set the due date of the chore to the end of 2080.
curl http://localhost:7777/chores
[{"id":1,"user_id":1,"title":"New title","description":"New description","chorecoins":2,"due_date":"2080-12-31T00:00:00.000Z"}]%
Let's create a view for showing the chores, and modify the controller to render the content to the view after that.
Views for listing chores
Let's modify the main.eta
to further list chores if chores are available. For now, let's use an unordered list for listing the chores. For each chore, we show the title and the amount of chorecoins that one could get from completing the chore.
Current chores are listed only if there are chores available. Otherwise, a text "None available." is shown.
<% layout('./layouts/layout.eta') %>
<h1>Chores!</h1>
<h2>Current chores</h2>
<% if (it.chores && it.chores.length > 0) { %>
<ul>
<% it.chores.forEach(chore => { %>
<li><%= chore.title %> (<%= chore.chorecoins %> cc)</li>
<% }); %>
</ul>
<% } else { %>
<p>None available.</p>
<% } %>
<h2>Add a chore!</h2>
<form method="POST" action="/chores">
Title:<br/>
<input type="text" name="title" /><br/>
Description:<br/>
<textarea name="description"></textarea><br/>
Chorecoins:<br/>
<input type="number" name="chorecoins" /><br/>
Due date:<br/>
<input type="date" name="due_date" /><br/>
<input type="submit" value="Add"/>
</form>
Controller for listing chores again
Now that we have a template for listing the chores, let's render the chores to the template instead of responding as JSON. The change needed to the choreController.js
is relatively straightforward -- instead of responding with a list of chores, we render the chores.
import * as choreService from "../../services/choreService.js";
const addChore = async ({ request, response }) => {
const body = request.body({ type: "form" });
const params = await body.value;
await choreService.addChore(
1,
params.get("title"),
params.get("description"),
params.get("chorecoins"),
params.get("due_date"),
);
response.redirect("/");
};
const listChores = async ({ render }) => {
render("main.eta", { chores: await choreService.listChores() });
};
export { addChore, listChores };
Now, let's try out the application.
curl http://localhost:7777
<h1>Chores!</h1>
<h2>Current chores</h2>
<p>None available.</p>
<h2>Add a chore!</h2>
<form method="POST" action="/chores">
Title:<br/>
<input type="text" name="title" /><br/>
Description:<br/>
<textarea name="description"></textarea><br/>
Chorecoins:<br/>
<input type="number" name="chorecoins" /><br/>
Due date:<br/>
<input type="date" name="due_date" /><br/>
<input type="submit" value="Add"/>
</form>%
No chores available. Why?
After a moment of reflection, we notice that we used the wrong path. If we query http://localhost:7777/chores
, we do see a chore.
curl http://localhost:7777/chores
<h1>Chores!</h1>
<h2>Current chores</h2>
<ul>
<li>New title (2 cc)</li>
</ul>
<h2>Add a chore!</h2>
<form method="POST" action="/chores">
Title:<br/>
<input type="text" name="title" /><br/>
Description:<br/>
<textarea name="description"></textarea><br/>
Chorecoins:<br/>
<input type="number" name="chorecoins" /><br/>
Due date:<br/>
<input type="date" name="due_date" /><br/>
<input type="submit" value="Add"/>
</form>%
This issue is in part due to us using the main.eta
for showing the content. The file main.eta
is used for both the mainController.js
and the choreController.js
-- let's create a separate view template for the chores. Or, ..., let's rename the file main.eta
as chores.eta
, and create a new file called main.eta
.
The new main.eta
will be as follows -- that is, when users enter the application, they see a page that has a link to chores.
<% layout('./layouts/layout.eta') %>
<a href="/chores">See chores</a>
In addition to this, we need to change the choreController.js
marginally. First, let's redirect users to /chores
when they add a new chore. In addition, let's render the chores.eta
instead of the main.eta
.
import * as choreService from "../../services/choreService.js";
const addChore = async ({ request, response }) => {
// ...
response.redirect("/chores");
};
const listChores = async ({ render }) => {
render("chores.eta", { chores: await choreService.listChores() });
};
export { addChore, listChores };
Now, we have very basic functionality for adding and listing chores in place.