Skip to content

Commit

Permalink
adding a proposed solution for Lab 3
Browse files Browse the repository at this point in the history
  • Loading branch information
LucaMannella committed Apr 9, 2024
1 parent 94c5e62 commit 0f0cfac
Show file tree
Hide file tree
Showing 13 changed files with 2,887 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Laboratories solutions (under development) for the course of _Web Applications I

- Lab 01: [Getting Started with Node.js](https://github.com/polito-webapp1/lab-2024/tree/main/lab01-node)
- Lab 02: [Database Integration](https://github.com/polito-webapp1/lab-2024/tree/main/lab02-node-database)
- Lab 03:
- Lab 03: [APIs with Express](https://github.com/polito-webapp1/lab-2024/tree/main/lab03-express)
- Lab 04:
- Lab 05:
- Lab 06:
Expand Down
165 changes: 165 additions & 0 deletions lab03-express/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Lab 03 - APIs with Express

This repository contains a proposed solution for the third laboratory of the courses. Specifically, this README includes an overview of the files contained in the `solution` folder and a description of each API offered by the server.

## File overview

- `server.mjs`: the main file of the server. It defines all the API endpoints and behavior. It interacts with the database and returns to the client the desired data.
- `db.mjs`: it opens the database. It has to be imported (e.g., by `dao-film.mjs`) to interact with the db.
- `dao-films.mjs`: it contains all the method for interacting with the database (specifically, to interact with the `film` table).
- `films.mjs`: the same data model for Film objects used in the previous labs.
- `test-api.http`: this file can be used for testing the API with a dedicated Visual Studio Code extension.

## List of APIs offered by the server

### Film Management

#### Get all films

HTTP method: `GET` URL: `/api/films`

- Description: Get the full list of films or the films that match the query filter parameter
- Request body: _None_
- Request query parameter: _filter_ name of the filter to apply (filter-all, filter-favorite, filter-best, filter-lastmonth, filter-unseen)
- Response: `200 OK` (success)
- Response body: Array of objects, each describing one film:

``` json
[
{
"id": 1,
"title": "Pulp Fiction",
"favorite": true,
"watchDate": "2023-03-11",
"rating": 5,
"userId": 1
},
{
"id": 2,
"title": "21 Grams",
"favorite": true,
"watchDate": "2023-03-17",
"rating": 4,
"userId": 1
},
...
]
```

- Error responses: `500 Internal Server Error` (generic error)

#### Get film by id

HTTP method: `GET` URL: `/api/films/:id`

- Description: Get the film corresponding to the id
- Request body: _None_
- Response: `200 OK` (success)
- Response body: One object describing the required film:

``` JSON
[
{
"id": 2,
"title": "21 Grams",
"favorite": true,
"watchDate": "2023-03-17",
"rating": 4,
"userId": 1
}
]
```

- Error responses: `500 Internal Server Error` (generic error), `404 Not Found` (not present or unavailable)

#### Add a new film

HTTP method: `POST` URL: `/api/films`

- Description: Add a new film to the films of a specified user
- Request body: description of the object to add

``` JSON
{
"title": "21 Grams",
"favorite": true,
"watchDate": "2023-03-17",
"rating": 4,
"userId": 1
}
```

- Response: `200 OK` (success)
- Response body: the entire representation of the newly-added film

- Error responses: `404 Not Found` (not present or unavailable), `422 Unprocessable Entity` (invalid input), `503 Service Unavailable` (database error)

#### Update an existing film

HTTP method: `PUT` URL: `/api/films/:id`

- Description: Update values of an existing film, except the id
- Request body: description of the object to update

``` JSON
{
"title": "The Matrix",
"favorite": true,
"watchDate": "2023-03-31",
"rating": 5,
"userId": 1
}
```

- Response: `200 OK` (success)
- Response body: the entire representation of the newly-added film

- Error responses: `404 Not Found` (not present or unavailable), `422 Unprocessable Entity` (invalid input), `503 Service Unavailable` (database error)

#### Delete an existing film

HTTP method: `DELETE` URL: `/api/films/:id`

- Description: Delete an existing film
- Request body: _None_

- Response: `200 OK` (success)
- Response body: _None_

- Error responses: `404 Not Found` (not present or unavailable), `503 Service Unavailable` (database error)

#### Update whether a film is favorite

HTTP method: `PUT` URL: `/api/films/:id/favorite`

- Description: Update favorite value of an existing film
- Request body: value of the favorite property

``` JSON
{
"favorite": true,
}
```

- Response: `200 OK` (success)
- Response body: the object as represented in the database

- Error responses: `404 Not Found` (not present or unavailable), `422 Unprocessable Entity` (invalid input), `503 Service Unavailable` (database error)

#### Update the rating of an existing film

HTTP method: `PUT` URL: `/api/films/:id/rating`

- Description: Update the rating of an existing film
- Request body: value of the rating property

``` JSON
{
"rating": 5,
}
```

- Response: `200 OK` (success)
- Response body: the object as represented in the database

- Error responses: `404 Not Found` (not present or unavailable), `422 Unprocessable Entity` (invalid input), `503 Service Unavailable` (database error)
Binary file added lab03-express/films.db
Binary file not shown.
Binary file added lab03-express/lab03-express.pdf
Binary file not shown.
24 changes: 24 additions & 0 deletions lab03-express/solution/Film.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 01UDFOV Applicazioni Web I / 01TXYOV Web Applications I
* Model from lab 1 - 2024
*/

import dayjs from "dayjs";

export default function Film(id, title, isFavorite = false, watchDate = null, rating = null, userId = 1) {
this.id = id;
this.title = title;
this.favorite = isFavorite;
this.rating = rating;
// saved as dayjs object only if watchDate is truthy
this.watchDate = watchDate && dayjs(watchDate);
this.userId = userId;

// customize toJSON method to return the object with date only, no time
this.toJSON = () => {
return {
...this,
watchDate: this.watchDate ? this.watchDate.format("YYYY-MM-DD") : null,
};
};
}
130 changes: 130 additions & 0 deletions lab03-express/solution/dao-films.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/* Data Access Object (DAO) module for accessing films data */

import dayjs from "dayjs";
import db from "./db.mjs";
import Film from "./Film.mjs";


const filters = {
'filter-favorite': {label: 'Favorites', filterFunction: film => film.favorite},
'filter-best': {label: 'Best Rated', filterFunction: film => film.rating >= 5},
'filter-lastmonth': {label: 'Seen Last Month', filterFunction: film => isSeenLastMonth(film)},
'filter-unseen': {label: 'Unseen', filterFunction: film => !film.watchDate}
};

const isSeenLastMonth = (film) => {
if ('watchDate' in film && film.watchDate) { // Accessing watchDate only if defined
const diff = film.watchDate.diff(dayjs(), 'month');
const isLastMonth = diff <= 0 && diff > -1; // last month
return isLastMonth;
}
};

function mapRowsToFilms(rows) {
// Note: the parameters must follow the same order specified in the constructor.
return rows.map(row => new Film(row.id, row.title, row.isFavorite === 1, row.watchDate, row.rating, row.userId));
}


// NOTE: all functions return error messages as json object { error: <string> }
export default function FilmDao() {

// This function retrieves the whole list of films from the database.
this.getFilms = (filter) => {
return new Promise((resolve, reject) => {
const query = 'SELECT * FROM films';
db.all(query, (err, rows) => {
if (err) {
reject(err);
} else {
const films = mapRowsToFilms(rows);

if (filters.hasOwnProperty(filter))
resolve(films.filter(filters[filter].filterFunction));
else // if an invalid filter is specified, all the films are returned.
resolve(films);
}
});
});
};

// This function retrieves a film given its id and the associated user id.
this.getFilm = (id) => {
return new Promise((resolve, reject) => {
const query = 'SELECT * FROM films WHERE id=?';
db.get(query, [id], (err, row) => {
if (err) {
reject(err);
}
if (row === undefined) {
resolve({error: 'Film not found.'});
} else {
resolve(mapRowsToFilms([row])[0]);
}
});
});
};


/**
* This function adds a new film in the database.
* The film id is added automatically by the DB, and it is returned as this.lastID.
*/
this.addFilm = (film) => {
return new Promise((resolve, reject) => {
const query = 'INSERT INTO films (title, isFavorite, rating, watchDate, userId) VALUES(?, ?, ?, ?, ?)';
const watchDate = film.watchDate ? film.watchDate.format("YYYY-MM-DD") : null;
let rating;
if (!film.rating || film.rating < 1 || film.rating > 5)
rating = null;
else
rating = film.rating;

db.run(query, [film.title, film.favorite, rating, watchDate, film.userId], function (err) {
if (err) {
reject(err);
}
film.id = this.lastID;
resolve(film);
});
});
};

// This function updates an existing film given its id and the new properties.
this.updateFilm = (id, film) => {
return new Promise((resolve, reject) => {
const query = 'UPDATE films SET title = ?, isFavorite = ?, rating = ?, watchDate = ? WHERE id = ?';
const watchDate = film.watchDate ? film.watchDate.format("YYYY-MM-DD") : null;
let rating;
if (!film.rating || film.rating < 1 || film.rating > 5)
rating = null;
else
rating = film.rating;

db.run(query, [film.title, film.favorite, rating, watchDate, id], function (err) {
if (err) {
reject(err);
}
if (this.changes !== 1) {
resolve({error: 'Film not found.'});
} else {
resolve(film);
}
});
});
};

// This function deletes an existing film given its id.
this.deleteFilm = (id) => {
return new Promise((resolve, reject) => {
const query = 'DELETE FROM films WHERE id = ?';
db.run(query, [id], function (err) {
if (err) {
reject(err);
} else
resolve(this.changes);
});
});
};

}
27 changes: 27 additions & 0 deletions lab03-express/solution/dao-users.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* Data Access Object (DAO) module for accessing users data */

import db from "./db.mjs";


// NOTE: all functions return error messages as json object { error: <string> }
export default function UserDao() {

// This function retrieves one user by id
this.getUser = (id) => {
return new Promise((resolve, reject) => {
const query = 'SELECT * FROM users WHERE id=?';
db.get(query, [id], (err, row) => {
if (err) {
reject(err);
}
if (row === undefined) {
resolve({error: 'User not found.'});
} else {
resolve(row);
}
});
});
};


}
11 changes: 11 additions & 0 deletions lab03-express/solution/db.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** DB access module **/

import sqlite3 from "sqlite3";

// Opening the database
// NOTE: if you are running the solution from the root folder this path should be: `./lab03-express/solution/films.db`
const db = new sqlite3.Database('films.db', (err) => {
if (err) throw err;
});

export default db;
Binary file added lab03-express/solution/films.db
Binary file not shown.
Loading

0 comments on commit 0f0cfac

Please sign in to comment.