Integrazione con il database SQLite

Obiettivo

Estendere il programma film-library.js collegandolo al database locale films.db. La FilmLibrary dovrà offrire sei metodi asincroni che recuperano insiemi di film secondo diversi criteri, restituendo sempre una Promise risolta con un array di oggetti Film.

Procedura/Spiegazione passo-passo

  1. Import di SQLite3

  2. Definizione dei metodi di lettura

  3. Implementazioni richieste

    # Metodo Query SQL (semplificata) Parametri
    1 getAll() SELECT * FROM films -
    2 getFavorites() SELECT * FROM films WHERE favorite = 1 -
    3 getWatchedToday() SELECT * FROM films WHERE watchDate = DATE('now') -
    4 getWatchedBefore(date) SELECT * FROM films WHERE watchDate < ? date (ISO yyyy-mm-dd)
    5 getRatedAtLeast(rating) SELECT * FROM films WHERE rating >= ? rating (int)
    6 getFilmsByTitle(substr) `SELECT * FROM films WHERE title LIKE '%'
  4. Verifica del funzionamento

Codice o snippet

"use strict";
const dayjs = require("dayjs");
const sqlite = require ("sqlite3");

// Connessione condivisa
const db = new sqlite.Database('films.db', (err) => {
    if (err)
        throw err;
});

/* ===== Model ===== */
function Film(id, title, favorite = false, watchDate = null, rating = null, user = 1) {
    this.id = id;
    this.title = title;
    this.favorite = favorite;
    this.watchDate = watchDate ? dayjs(watchDate) : null;
    this.rating = rating;
    this.user = user;

    this.toString = () => {
        const dateStr = this.watchDate ? this.watchDate.format("MMMM D, YYYY") : "-";
        const scoreStr = this.rating ?? "-";
        return `Id: ${this.id}, Title: ${this.title}, Favorite: ${this.favorite}, Watch date: ${dateStr}, Score: ${scoreStr}, User: ${this.user}`;
    };
}

/* ===== Collection ===== */
function FilmLibrary() {
    this.list = [];

    /* Inserimento */
    this.addNewFilm = film => {
        this.list.push(film);
    }

    /* 1. Ordinamento per data */
    this.sortByDate = () => {
        return [...this.list].sort((a, b) => {
            if (a.watchDate === null && b.watchDate === null)
                return 0;
            if (a.watchDate === null)
                return 1;   // a non visto -> dopo
            if (b.watchDate === null)
                return -1;  // b non visto -> dopo
            return a.watchDate.isAfter(b.watchDate) ? 1 : -1;
        });
    };

    /* 2. Cancellazione per id */
    this.deleteFilm = id => {
        this.list = this.list.filter(f => f.id !== id);
    };

    /* 3. Reset di tutte le date di visione */
    this.resetWatchedFilms = () => {
        this.list.forEach(f => {
            f.watchDate = null
        });
    };

    /* 4. Selezione dei film valutati */
    this.getRated = () => {
        return this.list
            .filter(f => f.rating != null && f.rating > 0)
            .sort((a, b) => b.rating - a.rating); 
    };

    /* Utility: stampa completa della libreria */
    this.print = () => {
        this.list.forEach(f => console.log(f.toString()));
    }; 
    
    // 1. Tutti i film
    this.getAll = () => {
        return new Promise((resolve, reject) => {
            const sql = "SELECT * FROM films";
            db.all(sql, [], (err, rows) => {
                if (err)
                    reject(err);
                else
                    resolve(rows.map(r => new Film(r.id, r.title, !!r.favorite, r.watchDate, r.rating, r.user)));
            });
        });
    };

    // 2. Preferiti
    this.getFavorites = () => {
        return new Promise((resolve, reject) => {
            const sql = "SELECT * FROM films WHERE isFavorite = 1"; 
            db.all(sql, [], (err, rows) => {
                if (err)
                    reject(err);
                else
                    resolve(rows.map(r => new Film(r.id, r.title, true, r.watchDate, r.rating, r.user)));
            });
        });
    };

    // 3. Visti oggi
    this.getWatchedToday = () => {
        return new Promise((resolve, reject) => {
            const sql = "SELECT * FROM films WHERE DATE(watchDate) = DATE('now')";
            db.all(sql, [], (err, rows) => {
                if (err)
                    reject(err);
                else
                    resolve(rows.map(r => new Film(r.id, r.title, !!r.favorite, r.watchDate, r.rating, r.user)));
            });
        });
    };

    // 4. Visti prima di una data
    this.getWatchedBefore = (date) => {
        return new Promise((resolve, reject) => {
            const sql = "SELECT * FROM films WHERE watchDate < ?";
            db.all(sql, [date], (err, rows) => {
                if (err)
                    reject(err);
                else
                    resolve(rows.map(r => new Film(r.id, r.title, !!r.favorite, r.watchDate, r.rating, r.user)));
            });
        });
    };

    // 5. Valutazione minima
    this.getRatedAtLeast = (rating) => {
        return new Promise((resolve, reject) => {
            const sql = "SELECT * FROM films WHERE rating >= ?";
            db.all(sql, [rating], (err, rows) => {
                if (err)
                    reject(err);
                else
                    resolve(rows.map(r => new Film(r.id, r.title, !!r.favorite, r.watchDate, r.rating, r.user)));
            });
        });
    };

    // 6. Titolo contenente stringa
    this.getFilmsByTitle = (substr) => {
        return new Promise((resolve, reject) => {
            const sql = "SELECT * FROM films WHERE title LIKE '%' || ? || '%'";
            db.all(sql, [substr], (err, rows) => {
                if (err)
                    reject(err);
                else 
                    resolve(rows.map(r => new Film(r.id, r.title, !!r.favorite, r.watchDate, r.rating, r.user)));
            });
        });
    };
}

/* ===== Demo ===== */
async function demo() {
    const libary = new FilmLibrary();
    libary.addNewFilm(new Film(1, "Pulp Fiction", true, "2024-03-10", 5));
    libary.addNewFilm(new Film(2, "21 Grams", true, "2024-03-17", 4));
    libary.addNewFilm(new Film(3, "Star Wars", false, null, null));
    libary.addNewFilm(new Film(4, "Matrix", false, null, null));
    libary.addNewFilm(new Film(5, "Shrek", false, "2024-03-21", 3));

    console.log("***** List sorted by date *****");
    libary.print(libary.sortByDate());

    console.log("\\n>>> Delete film id=3");
    libary.deleteFilm(3);
    libary.print(libary.list);

    console.log("\\n>>> Reset watch dates");
    libary.resetWatchedFilms();
    libary.print(libary.list);

    console.log("\\n***** Films filtered - rated only *****");
    libary.print(libary.getRated());

    console.log(">> Tutti");
    (await libary.getAll()).forEach(f => console.log(f.toString()));
    
    console.log("\\n>> Preferiti");
    (await libary.getFavorites()).forEach(f => console.log(f.toString()));

    console.log("\\n>> Visti oggi");
    (await libary.getWatchedToday()).forEach(f => console.log(f.toString()));

    console.log("\\n>> Visti prima del 2024");
    (await libary.getWatchedBefore("2024-01-01")).forEach(f => console.log(f.toString()));

    console.log("\\n>> Rating >= 4");
    (await libary.getRatedAtLeast(4)).forEach(f => console.log(f.toString()));

    console.log("\\n>> Titolo contenente 'war'");
    (await libary.getFilmsByTitle('war')).forEach(f => console.log(f.toString()));

    db.close(); // chiusura esplicita della connessione
}

demo().catch(console.error);

Osservazioni critiche

Modifica dei dati nel database

Obiettivo

Estendere la FilmLibrary per inserire, cancellare e aggiornare record nella tabella films di films.db, restituendo sempre una Promise e visualizzando un messaggio di conferma o errore al termine di ciascuna operazione.

Procedura/Spiegazione passo-passo

  1. Richiamo a db.run per query di scrittura

    Le query che non restituiscono righe (INSERT, DELETE, UPDATE) si eseguono con db.run(sql, [params], function(err) {...}).

    Nota: la callback non può essere una arrow-function, perché occorrono le proprietà this.lastID e this.changes fornite da sqlite3.