modulo 17 di 24 / Ore 41-60

Moduli 17-24 / Checkpoint 60 ore

Adapter verso database leggero o file JSON

Come scrivere un adapter Node.js che legge e scrive un file JSON con fs, rispettando lo stesso contratto degli altri adapter nel Registro del laboratorio scolastico.

30 minArchitettura del software

Adapter verso database leggero o file JSON

Questa lezione appartiene al Modulo 17 — Variante database o API.

Il lavoro continua il progetto guida, il Registro del laboratorio scolastico, aggiungendo una variante infrastrutturale basata su file JSON letto con Node.js — senza modificare dominio o casi d’uso.

Obiettivo della lezione

Scrivi un adapter che legge e scrive un file db.json usando fs.readFileSync e fs.writeFileSync di Node.js. Il contratto esposto deve essere identico a quello degli altri adapter del Registro.

Alla fine devi avere: un file infrastructure/archivioStrumentiFile.js funzionante, con lo stesso contratto di archivioStrumentiLocalStorage.js.

Il contratto da rispettare

Tutti gli adapter del Registro rispettano lo stesso contratto:

caricaTutti()    // → Strumento[]  (o Promise<Strumento[]>)
salva(strumenti) // → void         (o Promise<void>)
trovaPerId(id)   // → Strumento | null

L’adapter per file JSON usa operazioni sincrone (readFileSync, writeFileSync), quindi le funzioni possono restituire valori diretti senza async/await. Questo semplifica l’implementazione rispetto all’adapter fetch, ma il contratto rimane lo stesso.

Come funziona fs con un file JSON

Node.js include il modulo fs nella libreria standard. Le due operazioni fondamentali per leggere e scrivere JSON sono:

import { readFileSync, writeFileSync } from "fs";

// leggere
const testo = readFileSync("db.json", "utf-8");
const dati = JSON.parse(testo);

// scrivere
writeFileSync("db.json", JSON.stringify(dati, null, 2), "utf-8");

null, 2 nella JSON.stringify serve a indentare il file con due spazi — utile per ispezionarlo a mano durante il debug.

Struttura del file db.json

Il file JSON deve avere la stessa struttura logica dei dati in localStorage, ma come file su disco:

{
  "strumenti": [
    { "id": "s1", "name": "Multimetro", "category": "misura", "status": "disponibile" },
    { "id": "s2", "name": "Oscilloscopio", "category": "misura", "status": "in-prestito" }
  ],
  "prestiti": [
    { "id": "p1", "strumentoId": "s2", "dataApertura": "2025-10-01", "dataCiusura": null }
  ]
}

I nomi dei campi sono in inglese per separare il formato di persistenza dal modello di dominio (dove si usa nome, categoria, stato).

Nel Registro del laboratorio

Il caso d’uso CercaStrumenti chiama archivio.caricaTutti() e riceve un array di oggetti Strumento. Non sa se quei dati vengono da localStorage, da un file JSON o da un’API. Cambiare l’adapter in main.js è sufficiente:

// main.js — per usare l'adapter file JSON
import * as archivio from "./infrastructure/archivioStrumentiFile.js";

Procedura guidata

  1. Crea il file db.json con la struttura iniziale (strumenti vuoto, prestiti vuoto).
  2. Crea src/infrastructure/archivioStrumentiFile.js.
  3. Importa readFileSync e writeFileSync da "fs".
  4. Scrivi una funzione privata leggiDB() che legge e fa il parse del file.
  5. Scrivi una funzione privata scriviDB(dati) che serializza e scrive il file.
  6. Implementa le tre funzioni del contratto usando leggiDB e scriviDB.
  7. Sostituisci l’import in main.js e verifica che i casi d’uso funzionino invariati.

Attività in classe

Compila questa scheda dopo aver scritto l’adapter:

## Adapter file JSON

Funzione leggiDB — cosa fa:

Funzione scriviDB — cosa fa:

Funzione caricaTutti — implementazione:

Funzione trovaPerId — implementazione:

Funzione salva — implementazione:

Contratto rispettato rispetto a archivioStrumentiLocalStorage: sì / no
Casi d'uso modificati: sì / no — se sì, perché:
Errori gestiti: quali scenari?

Spiegazione guidata per studiare

Un adapter verso file JSON è la variante più trasparente da studiare perché il file su disco è visibile e ispezionabile. Puoi aprire db.json con un editor dopo ogni operazione e verificare esattamente cosa è stato scritto.

Il punto fondamentale è che il contratto non cambia. Il caso d’uso RegistraStrumento chiama archivio.salva(strumenti) e non sa se quella funzione usa localStorage.setItem, writeFileSync o fetch. È il principio di sostituzione applicato all’infrastruttura.

L’unica differenza tecnica rilevante è che fs è sincrono e disponibile solo in Node.js. Se il progetto deve girare nel browser, questo adapter non funziona — ma puoi tenerlo come adapter di test o di script CLI separato. Per il browser, localStorage rimane la scelta principale.

Passo per passo nel progetto

  1. Crea db.json nella root del progetto:
{
  "strumenti": [],
  "prestiti": []
}
  1. Crea src/infrastructure/archivioStrumentiFile.js:
// infrastructure/archivioStrumentiFile.js
import { readFileSync, writeFileSync } from "fs";

const PERCORSO = "db.json";

function leggiDB() {
  try {
    const testo = readFileSync(PERCORSO, "utf-8");
    return JSON.parse(testo);
  } catch {
    return { strumenti: [], prestiti: [] };
  }
}

function scriviDB(dati) {
  writeFileSync(PERCORSO, JSON.stringify(dati, null, 2), "utf-8");
}

function dtoToStrumento(dto) {
  return {
    id: dto.id,
    nome: dto.name,
    categoria: dto.category,
    stato: dto.status
  };
}

function strumentoToDTO(strumento) {
  return {
    id: strumento.id,
    name: strumento.nome,
    category: strumento.categoria,
    status: strumento.stato
  };
}

export function caricaTutti() {
  const db = leggiDB();
  return db.strumenti.map(dtoToStrumento);
}

export function trovaPerId(id) {
  const db = leggiDB();
  const dto = db.strumenti.find(s => s.id === id);
  return dto ? dtoToStrumento(dto) : null;
}

export function salva(strumenti) {
  const db = leggiDB();
  db.strumenti = strumenti.map(strumentoToDTO);
  scriviDB(db);
}
  1. Verifica manualmente: esegui uno script Node.js che chiama salva con uno strumento e poi caricaTutti, e controlla il file db.json dopo l’esecuzione.
// script-test.mjs
import * as archivio from "./src/infrastructure/archivioStrumentiFile.js";

archivio.salva([
  { id: "s1", nome: "Multimetro", categoria: "misura", stato: "disponibile" }
]);

const tutti = archivio.caricaTutti();
console.log(tutti); // deve stampare l'array con il Multimetro
  1. Esegui con node script-test.mjs e apri db.json per ispezionare il risultato.

Esempio JS — gestione del file corrotto

Un caso reale: cosa succede se db.json è vuoto o contiene JSON non valido?

function leggiDB() {
  try {
    const testo = readFileSync(PERCORSO, "utf-8");
    return JSON.parse(testo);
  } catch (errore) {
    // file non esiste o JSON non valido → partiamo vuoti
    console.warn("db.json non leggibile, si parte con archivio vuoto:", errore.message);
    return { strumenti: [], prestiti: [] };
  }
}

Il catch non nasconde l’errore (viene loggato con console.warn) ma permette al sistema di continuare. Questo è il comportamento corretto per un registro scolastico: meglio partire vuoti che bloccare tutto.

Prova di comprensione

Rispondi per iscritto:

  • Perché leggiDB e scriviDB sono funzioni private e non esportate?
  • Cosa succede se il file db.json viene cancellato mentre l’applicazione è in uso?
  • Perché i nomi dei campi nel DTO (name, status) sono diversi da quelli del dominio (nome, stato)?
  • Come testeresti trovaPerId senza usare il file su disco?

Errori da evitare

  • usare direttamente readFileSync nei casi d’uso invece di delegare all’adapter;
  • dimenticare il try/catch attorno al readFileSync — il file potrebbe non esistere alla prima esecuzione;
  • salvare oggetti di dominio direttamente nel JSON senza mappatura — accoppia il formato di persistenza al modello;
  • chiamare scriviDB per ogni operazione di lettura — si scrive solo quando i dati cambiano.

Prodotto da consegnare

Il file src/infrastructure/archivioStrumentiFile.js funzionante, più un piccolo script di verifica che mostra caricaTutti e salva in azione con Node.js. Come controllo: apri db.json dopo l’esecuzione e verifica che i dati siano scritti correttamente.

Checklist di chiusura

  • le tre funzioni del contratto sono implementate e esportate;
  • le funzioni di mapping separano il formato di persistenza dal modello di dominio;
  • leggiDB gestisce il caso di file mancante o corrotto;
  • i casi d’uso non sono stati modificati;
  • ho verificato il contenuto di db.json dopo almeno un salva.

Risultato atteso

Ho creato archivioStrumentiFile.js con fs.readFileSync e writeFileSync.
Il contratto è identico agli altri adapter: caricaTutti, salva, trovaPerId.
Ho verificato che i casi d'uso non cambiano — solo l'import in main.js.
Ho aperto db.json dopo il salvataggio e ho visto i dati scritti correttamente.
Il prossimo passo: aggiungere la gestione dei prestiti nell'adapter file.