Ja tenim connexió i models. Toca moure dades. CRUD són les quatre operacions que sostenen qualsevol aplicació amb estat: crear, llegir, actualitzar i esborrar. Amb Mongoose cadascuna té diverses maneres d'escriure's, i no totes són equivalents: unes validen i altres no, unes retornen el document anterior i altres el nou, unes executen la consulta i altres només la construeixen.
Aquesta lliçó recorre les quatre amb els seus paranys reals, tradueix els errors de Mongoose a la jerarquia del mòdul 6, i culmina amb el moment anunciat des de la primera lliçó del mòdul: src/cataleg-dades.js es retira i src/repositoris/esdeveniments.js ocupa el seu lloc amb la mateixa signatura pública, sense que els controladors ni el domini se n'assabentin.
Contingut
- Crear documents
- Errors de clau duplicada i la jerarquia del mòdul 6
- Llegir:
Querymandrosos, projeccions ilean() - Ordenar, limitar i paginar
- Operadors de consulta essencials
- Actualitzar:
runValidators,$set,$inc - Esborrar i esborrat lògic
- El repositori: jubilant
cataleg-dades.js POST /comandescontra la base de dades- Errors de Mongoose i el seu estat HTTP
Crear documents
Hi ha dos camins, i la diferència no és estètica.
// Cami 1: instanciar i desar. Permet manipular el document abans d'escriure.
const esdeveniment = new Esdeveniment({
esdevenimentId: 'evt-001', titol: 'Concierto de Otono', sala: 'Teatro Almendra',
organitzadorId: 'org-almendra', categoria: 'musica', estat: 'publicat', duracioMinuts: 95,
sessions: [
{ sessioId: 'ses-001-1', dataHora: new Date('2026-10-03T20:00:00Z'), aforament: 400, venudes: 312, preuCentims: 2500 },
{ sessioId: 'ses-001-2', dataHora: new Date('2026-10-04T19:00:00Z'), aforament: 400, venudes: 289, preuCentims: 2500 },
],
});
// Encara no hi ha res a la base: el document viu en memoria.
const desat = await esdeveniment.save(); // valida, dispara pre('save') i escriu
// Cami 2: crear directament. Equival a new + save en una linia.
const altre = await Esdeveniment.create({ esdevenimentId: 'evt-002', /* ... */ });
// Insercio massiva: una sola anada i tornada per a molts documents.
const diversos = await Esdeveniment.insertMany([documentA, documentB], { ordered: false });save() i create() retornen el document ja persistit, amb el seu _id, els seus timestamps i els seus virtuals. insertMany és molt més ràpid que un bucle de create, però per defecte és ordenat: si el tercer document falla, els dos primers ja estan escrits i els següents no s'intenten; amb { ordered: false } els intenta tots i t'informa dels fallits, que és el que voldràs en una llavor. Quin fer servir: new + save() et deixa aplicar lògica entre construir i escriure (calcular el total, generar codis); create() és més directe quan les dades ja vénen completes.
Errors de clau duplicada i la jerarquia del mòdul 6
esdevenimentId i codi tenen índex únic. Si insereixes un duplicat, MongoDB respon amb E11000 duplicate key error. Si no el tradueixes, el gestor del mòdul 6 el tractarà com un bug i retornarà un 500. Però no és un bug: és un conflicte d'estat operatiu, i mereix un 409.
// src/repositoris/errors-mongo.js
'use strict';
const { ConflicteDEstat, ErrorDeValidacio } = require('../errors.js');
/**
* Tradueix un error de Mongoose/MongoDB a la jerarquia de l'aplicacio. Si no el
* reconeix el retorna tal qual: sera un 500, i esta be, perque significa que
* es una fallada que no haviem previst.
*/
function traduirErrorDeMongo(error) {
// Clau duplicada: violacio d'un index unic -> 409.
if (error.code === 11000) {
const camp = Object.keys(error.keyPattern ?? {})[0] ?? 'desconegut';
return new ConflicteDEstat(`Ja existeix un registre amb el mateix ${camp}`, {
codi: 'ESTAT_INVALID', detalls: { camp },
});
}
// Validacio de l'esquema: un o diversos camps incompleixen les regles -> 400.
if (error.name === 'ValidationError') {
return new ErrorDeValidacio("El document no compleix l'esquema", {
codi: 'DADES_INVALIDES',
detalls: Object.entries(error.errors).map(([camp, f]) => ({ camp, missatge: f.message })),
});
}
// Identificador amb forma invalida ('hola' no es un ObjectId) -> 400, no 500.
if (error.name === 'CastError') {
return new ErrorDeValidacio(`El valor de '${error.path}' no es valid`, {
codi: 'DADES_INVALIDES', detalls: { camp: error.path, valor: String(error.value) },
});
}
return error;
}
module.exports = { traduirErrorDeMongo };Fixa't on viu aquesta traducció: a la capa de repositori, no al controlador. És coherent amb l'arquitectura de la lliçó 07-01: el controlador no ha de saber que existeix MongoDB, i per tant tampoc què és un E11000. El repositori tradueix del llenguatge del motor al de l'aplicació.
Llegir: Query mandrosos, projeccions i lean()
Aquí hi ha la sorpresa que descol·loca gairebé tothom la primera vegada:
// Aixo NO executa cap consulta: construeix un objecte Query.
const consulta = Esdeveniment.find({ sala: 'Teatro Almendra' });
console.log(consulta.constructor.name); // 'Query'
// Query te .then(), aixi que await l'executa: es un "thenable", no una
// promesa. Per aixo es pot refinar abans d'executar.
const publicats = await consulta.where('estat').equals('publicat').sort({ titol: 1 }).limit(20);Que sigui mandrós és un avantatge: pots construir la consulta per parts segons els filtres opcionals que arriben per query string i executar-la al final. El parany és que una consulta sense await no fa res i no dóna error. Regla: sempre await, o .exec(), que retorna una promesa de veritat i millora les traces de pila.
const cataleg = await Esdeveniment.find({ estat: 'publicat' }); // array, buit si no n'hi ha
const un = await Esdeveniment.findOne({ esdevenimentId: 'evt-001' }); // document o null
const perId = await Esdeveniment.findById('507f1f77bcf86cd799439011'); // document o nullCap no llança si no troba res: retornen null o [], i convertir aquest null en un RecursNoTrobat és feina del repositori o del controlador. Amb .orFail() llança en lloc de retornar null, la qual cosa de vegades és més còmoda.
Projecció: demanar només els camps necessaris. Menys bytes, menys memòria, menys feina del motor.
// Inclusiva: nomes aquests camps (mes _id, llevat que l'excloguis).
await Esdeveniment.find({ estat: 'publicat' }, { esdevenimentId: 1, titol: 1, _id: 0 });
// Exclusiva: tot menys aixo. No es poden barrejar inclusio i exclusio
// (llevat de _id, que es la unica excepcio).
await Esdeveniment.find({}).select('-sessions');Aquest darrer exemple importa: la portada del catàleg no necessita les 7 sessions amb el seu aforament i el seu preu, només el títol i la sala.
lean() retorna objectes JavaScript plans en lloc de documents de Mongoose. Per defecte, Mongoose embolcalla cada resultat en una instància amb seguiment de canvis, getters, virtuals i mètodes: construir-la és car. Un objecte lean no té save(), ni virtuals, ni conversió de getters —et arriben els tipus BSON crus—, però es construeix gairebé de franc.
La regla: fes servir lean() en tota lectura que acabi en res.json(); no el facis servir quan hagis de modificar el document. En llistats grans la diferència és de diverses vegades en temps i memòria. A Escena Viva hi ha un matís: el nostre repositori retorna objectes del domini, així que farà lean() i després Esdeveniment.desDeJSON(...), més barat que hidratar dues vegades.
Ordenar, limitar i paginar
// Paginacio classica per desplacament.
const esdeveniments = await Esdeveniment.find({ estat: 'publicat' })
.sort({ titol: 1 })
.skip((pagina - 1) * perPagina)
.limit(perPagina)
.lean();Per a les primeres pàgines és perfecte. Però skip es degrada linealment: per servir la pàgina 5.000 el motor localitza i descarta 100.000 documents abans de començar a retornar; no els salta màgicament, els recorre. A més, si algú insereix o esborra entre dues peticions, veuràs elements repetits o te'n saltaràs algun, perquè el desplaçament es calcula sobre un conjunt que ha canviat. L'alternativa és la paginació per cursor (o keyset): en lloc de "salta't 100.000", dius "dóna'm els següents a partir d'aquest valor".
/** Paginacio per cursor sobre un camp ordenat i unic (aqui, el _id). */
async function paginarEsdeveniments({ despresDe = null, limit = 20 } = {}) {
const filtre = { estat: 'publicat' };
// Nomes documents posteriors a l'ultim vist: fa servir l'index, no descarta res.
if (despresDe) filtre._id = { $gt: despresDe };
const documents = await Esdeveniment.find(filtre).sort({ _id: 1 }).limit(limit).lean();
return { documents, seguent: documents.length === limit ? documents.at(-1)._id : null };
}El cost és constant independentment de la profunditat i és estable davant d'insercions. El preu: no pots saltar a la pàgina 37, només avançar. Per a un catàleg amb desplaçament infinit o una API de consum automàtic, és el correcte.
Operadors de consulta essencials
| Operador | Significat | Exemple a Escena Viva |
|---|---|---|
$eq / $ne |
Igual (implícit) / diferent | { estat: { $ne: 'esborrany' } } |
$gt / $gte / $lt / $lte |
Comparacions | { preuCentims: { $lte: 3000 } } |
$in / $nin |
És / no és a la llista | { sala: { $in: ['Teatro Almendra', 'Auditorio Ribera'] } } |
$and / $or |
Lògics | Publicats o del propi organitzador |
$regex / $exists |
Expressió regular / el camp existeix | Cerca per títol |
$expr |
Comparar dos camps entre si | venudes < aforament |
$elemMatch |
Condicions sobre el mateix element d'un array | Sessions en un rang |
// 1. Esdeveniments amb alguna sessio en un rang de dates.
// COMPTE: sense $elemMatch, les dues condicions les poden complir sessions DIFERENTS.
const rang = { $gte: new Date('2026-03-01'), $lt: new Date('2026-03-16') };
await Esdeveniment.find({ sessions: { $elemMatch: { dataHora: rang } } }).lean();
// 2. Esdeveniments amb alguna sessio amb entrades lliures: comparar dos camps del
// mateix document exigeix $expr, perque el filtre normal compara amb literals.
await Esdeveniment.find({
estat: 'publicat',
sessions: { $elemMatch: { $expr: { $lt: ['$venudes', '$aforament'] } } },
}).lean();
// 3. Cerca per titol insensible a majuscules. Escapem l'entrada de
// l'usuari: un regex sense escapar es un vector de denegacio de servei.
await Esdeveniment.find({ titol: { $regex: escapar(terme), $options: 'i' } }).lean();El punt 1 és un error clàssic: { 'sessions.dataHora': { $gte: a, $lt: b } } no significa "una sessió dins del rang", sinó "alguna sessió posterior a a i alguna anterior a b", cosa que pot complir un esdeveniment sense cap sessió al març.
Actualitzar: runValidators, $set, $inc
// Sense portar el document: rapid, pero no dispara pre('save').
// Retorna { acknowledged, matchedCount, modifiedCount, upsertedId }.
await Esdeveniment.updateOne({ esdevenimentId: 'evt-003' }, { $set: { estat: 'publicat' } });
// Actualitzar i rebre el document resultant.
const actualitzat = await Esdeveniment.findByIdAndUpdate(
id,
{ $set: { duracioMinuts: 120 } },
{ new: true, runValidators: true },
);Les dues opcions són gairebé obligatòries:
new: trueretorna el document després del canvi. Per defecte —i això sorprèn tothom— retorna l'anterior. Sense ella respondràs al client amb dades velles.runValidators: trueexecuta els validadors de l'esquema. Sense ella et saltes la validació per complet: podries posarestat: 'inventat'encara que hi hagi unenum, oaforament: -5encara que hi hagimin: 1. És el forat més gran i silenciós de Mongoose. Activa'l globalment ambmongoose.set('runValidators', true). I compte amb una limitació real: en validar una actualització,thisés la consulta, no el document, així que un validador que comparavenudesambaforamentno té accés aaforamentsi no l'estàs actualitzant també. La invariant crítica no pot descansar només allà.
| Operador | Què fa | Ús a Escena Viva |
|---|---|---|
$set / $unset |
Assigna / elimina un camp | Canviar estat |
$inc |
Suma (o resta) atòmicament | Incrementar venudes |
$push / $pull |
Afegeix / treu d'un array | Afegir o retirar una sessió |
$addToSet / $min / $max |
Afegeix si no hi és / assigna si és menor o major | Etiquetes, marques històriques |
I aquí apareix la primera peça del problema de l'aforament:
// Increment ATOMIC del comptador d'una sessio concreta. L'operador posicional $
// apunta a l'element de l'array que ha casat amb el filtre.
await Esdeveniment.updateOne(
{ esdevenimentId: 'evt-001', 'sessions.sessioId': 'ses-001-1' },
{ $inc: { 'sessions.$.venudes': 2 } },
);Per què és un avenç? Perquè $inc no fa llegir-modificar-escriure al teu procés: mana al motor la instrucció "suma 2 a aquest camp", i el motor l'aplica sota el seu propi blocatge de document. Dues peticions simultànies amb $inc: 1 deixen el comptador a +2, mai a +1. S'ha acabat la pèrdua d'actualitzacions.
Però no n'hi ha prou, i convé veure-ho ara per no endur-se una falsa sensació de seguretat: $inc suma incondicionalment. Si quedava una entrada i arriben dues compres, totes dues sumen i venudes acaba valent aforament + 1. Hem resolt la pèrdua d'escriptures, no la comprovació de la condició. La peça que falta —un filtre que formi part de la mateixa operació atòmica— arriba a la lliçó 07-06.
Esborrar i esborrat lògic
await Esdeveniment.deleteOne({ esdevenimentId: 'evt-009' });
await Esdeveniment.findByIdAndDelete(id); // retorna l'esborrat, o null
const { deletedCount } = await Entrada.deleteMany({ estat: 'anullada' });
// Pero a Escena Viva gairebe mai no esborrem comandes ni entrades: les marquem.
await Comanda.findByIdAndUpdate(comandaId, { $set: { estat: 'anullat' } }, { new: true, runValidators: true });
await Entrada.updateMany({ comandaId }, { $set: { estat: 'anullada' } });Les raons són de negoci: comptabilitat (una comanda cobrada i després anul·lada ha de continuar apareixent al tancament del mes), traçabilitat (amb l'idPeticio del mòdul 6 podem reconstruir què va passar, però no si el document no existeix), integritat referencial (sense claus foranes, esborrar una comanda deixa entrades òrfenes) i errors humans (un esborrat lògic es reverteix canviant un camp; un de físic, restaurant una còpia de seguretat, amb sort). El preu —totes les consultes han de filtrar per estat— és baix i previsible.
El repositori: jubilant cataleg-dades.js
Ha arribat el moment. src/cataleg-dades.js exposava obtenirCataleg i obtenirEsdevenimentPerId llegint dades/esdeveniments.json. El nou mòdul exposa exactament el mateix, llegint MongoDB.
// src/repositoris/esdeveniments.js
'use strict';
const { Esdeveniment: ModelEsdeveniment } = require('../models/esdeveniment.js');
const { Esdeveniment } = require('../domini/esdeveniment.js');
const { traduirErrorDeMongo } = require('./errors-mongo.js');
/**
* Converteix un document pla de Mongo en una instancia del domini.
* Es la frontera: d'aqui cap amunt, ningu no sap que existeix Mongo.
*/
function aDomini({ esdevenimentId, sessions, ...resta }) {
return Esdeveniment.desDeJSON({
...resta,
id: esdevenimentId,
sessions: sessions.map(({ sessioId, dataHora, ...dades }) => ({
...dades, // aforament, venudes, preuCentims
id: sessioId,
dataHora: dataHora.toISOString(),
})),
});
}
/** Cataleg complet, com a instancies del domini. */
async function obtenirCataleg() {
try {
// lean(): nomes llegim i transformem, no necessitem documents de Mongoose.
const docs = await ModelEsdeveniment.find({ estat: { $ne: 'esborrany' } }).sort({ titol: 1 }).lean();
return docs.map(aDomini);
} catch (error) {
throw traduirErrorDeMongo(error);
}
}
/** Un esdeveniment pel seu identificador de negoci, o null si no existeix. */
async function obtenirEsdevenimentPerId(esdevenimentId) {
const document = await ModelEsdeveniment.findOne({ esdevenimentId }).lean();
return document ? aDomini(document) : null;
}
module.exports = { obtenirCataleg, obtenirEsdevenimentPerId };El canvi als controladors és literalment una línia: on src/controladors/esdeveniments.js feia require('../cataleg-dades.js'), ara fa require('../repositoris/esdeveniments.js'). Això és tot. llistarEsdeveniments, carregarEsdeveniment, obtenirEsdeveniment i llistarSessionsDeLEsdeveniment continuen funcionant sense tocar una coma, perquè continuen rebent instàncies d'Esdeveniment del domini amb els seus getters ocupacio, exhaurida i recaptacioCentims. Això és el que compra el patró repositori, i és la mateixa porta per la qual entrarà Sequelize a la lliçó 07-05.
POST /comandes contra la base de dades
El controlador ja rep dades validades per zod a req.dadesValidades. El que canvia és d'on surten i on van.
// src/repositoris/comandes.js (requires omesos per brevetat)
'use strict';
// Genera un codi d'entrada EV-<any>-<6 digits>.
const generarCodi = (any, n) => `EV-${any}-${String(n).padStart(6, '0')}`;
async function crearComanda({ usuariId, sessioId, quantitat, canal }) {
const esdeveniment = await Esdeveniment.findOne({ 'sessions.sessioId': sessioId });
if (!esdeveniment) {
throw new RecursNoTrobat(`No existeix la sessio ${sessioId}`, { codi: 'SESSIO_NO_TROBADA' });
}
const sessio = esdeveniment.sessions.find((element) => element.sessioId === sessioId);
const lliures = sessio.aforament - sessio.venudes;
if (lliures < quantitat) {
throw new ConflicteDEstat('No queden entrades suficients', {
codi: 'AFORAMENT_INSUFICIENT',
detalls: { lliures, sollicitades: quantitat },
});
}
try {
// ATENCIO: entre la comprovacio d'aforament i aquest $inc hi ha una finestra en
// la qual una altra peticio es pot colar. Continua existint sobrevenda. Ho resolem
// a la llico 07-06 amb una actualitzacio atomica condicional.
await Esdeveniment.updateOne(
{ esdevenimentId: esdeveniment.esdevenimentId, 'sessions.sessioId': sessioId },
{ $inc: { 'sessions.$.venudes': quantitat } },
);
const comanda = await Comanda.create({
usuariId, esdevenimentId: esdeveniment.esdevenimentId, sessioId, quantitat, canal,
totalCentims: sessio.preuCentims * quantitat,
estat: 'pagat',
});
const any = new Date().getUTCFullYear();
const base = await Entrada.countDocuments();
const entrades = await Entrada.insertMany(
Array.from({ length: quantitat }, (unused, i) => ({
codi: generarCodi(any, base + i + 1), comandaId: comanda._id, sessioId, estat: 'valida',
})),
);
return { comanda, entrades };
} catch (error) {
throw traduirErrorDeMongo(error);
}
}
module.exports = { crearComanda };Deixa aquest comentari en majúscules on és. És el problema del curs, ara amb nom i línia concreta: hem guanyat atomicitat per operació, però no per conjunt, i a més la comanda i les entrades es poden crear sense que l'aforament s'hagi reservat, o a l'inrevés. A la 07-06 aquesta funció es reescriurà sencera i el comentari desapareixerà.
Errors de Mongoose i el seu estat HTTP
| Error | Quan apareix | Traducció | Estat |
|---|---|---|---|
ValidationError |
Un camp incompleix l'esquema | DADES_INVALIDES |
400 |
CastError |
findById('hola'), data no analitzable |
DADES_INVALIDES |
400 |
E11000 |
Índex únic violat | ESTAT_INVALID |
409 |
DocumentNotFoundError |
orFail() sense resultat |
..._NO_TROBAT |
404 |
MongoServerSelectionError / MongoNetworkError |
La base no respon | SERVEI_EXTERN_CAIGUT |
503 |
El CastError mereix atenció especial perquè és la fallada més freqüent en producció: un client demana GET /comandes/12345 amb un identificador que no és un ObjectId, Mongoose no el pot convertir i llança. Sense traducció, la teva aplicació retorna un 500 i el teu panell d'alertes s'omple d'errors que no són bugs: són peticions mal formades i mereixen un 400. La taula ESTAT_PER_CODI del mòdul 6 fa la resta; només cal donar-li el codi correcte.
Errors Comuns i Consells
- Oblidar
runValidators: true. El teuenumi els teusmindeixen d'existir a les actualitzacions. - Oblidar
new: true. Retornes l'estat anterior al canvi i passes mitja tarda buscant el bug al frontend. - No fer servir
await. UnQuerysense executar no falla: simplement no passa res, o passa a destemps. - Hidratar documents complets només per serialitzar-los.
lean()i una projecció són l'optimització més barata que existeix. - Paginar amb
skipen un catàleg gran. Funciona fins que no funciona, i falla just quan tens trànsit. - Interpolar entrada d'usuari en un
$regex. Un patró com(a+)+$pot penjar el motor. Escapa sempre. - Consell: fes servir
.orFail()quan l'absència sigui un error real, i activamongoose.set('debug', true)en desenvolupament: veuràs cada consulta que s'envia i descobriràs consultes que no sabies que feies.
Exercicis
Exercici 1: comandes per usuari
Afegeix a src/repositoris/comandes.js una funció llistarComandesDUsuari(usuariId, { pagina, perPagina }) que retorni les comandes no anul·lades d'un usuari, de la més recent a la més antiga, amb projecció de sessioId, quantitat, totalCentims, estat i createdAt, fent servir lean(). Indica quin índex necessita.
Exercici 2: cercador del catàleg
Escriu cercarEsdeveniments({ sala, categoria, desDe, finsA, nomesAmbEntrades }) que construeixi el filtre de forma incremental (afegint només les condicions rebudes) i retorni els esdeveniments publicats que hi casin. Fes servir $elemMatch per al rang de dates i $expr per a les entrades lliures.
Exercici 3: anul·lar una comanda
Implementa anullarComanda(comandaId) que comprovi que la comanda existeix i està en estat pagat o emes (si no, ConflicteDEstat amb ESTAT_INVALID), la marqui com a anullat, marqui les seves entrades com a anullades i retorni l'aforament. Anota què passa si el procés mor entre dues d'aquestes operacions.
Solucions
Exercici 1.
async function llistarComandesDUsuari(usuariId, { pagina = 1, perPagina = 20 } = {}) {
return Comanda.find({ usuariId, estat: { $ne: 'anullat' } })
.select('sessioId quantitat totalCentims estat createdAt')
.sort({ createdAt: -1 })
.skip((pagina - 1) * perPagina)
.limit(perPagina)
.lean();
}Índex: { usuariId: 1, estat: 1, createdAt: -1 }, seguint la regla ESR — igualtat primer, ordenació després i en el mateix sentit.
Exercici 2.
async function cercarEsdeveniments({ sala, categoria, desDe, finsA, nomesAmbEntrades = false } = {}) {
const filtre = { estat: 'publicat' };
if (sala) filtre.sala = sala;
if (categoria) filtre.categoria = categoria;
const condicions = {};
if (desDe) condicions.dataHora = { $gte: new Date(desDe) };
if (finsA) condicions.dataHora = { ...condicions.dataHora, $lte: new Date(finsA) };
if (nomesAmbEntrades) condicions.$expr = { $lt: ['$venudes', '$aforament'] };
if (Object.keys(condicions).length > 0) filtre.sessions = { $elemMatch: condicions };
return Esdeveniment.find(filtre).sort({ titol: 1 }).lean();
}Exercici 3.
async function anullarComanda(comandaId) {
const comanda = await Comanda.findById(comandaId).orFail();
if (!['pagat', 'emes'].includes(comanda.estat)) {
throw new ConflicteDEstat(`Una comanda ${comanda.estat} no es pot anullar`, { codi: 'ESTAT_INVALID' });
}
comanda.estat = 'anullat';
await comanda.save();
await Entrada.updateMany({ comandaId }, { $set: { estat: 'anullada' } });
await Esdeveniment.updateOne(
{ esdevenimentId: comanda.esdevenimentId, 'sessions.sessioId': comanda.sessioId },
{ $inc: { 'sessions.$.venudes': -comanda.quantitat } },
);
// Si el proces mor entre aquestes tres escriptures, l'estat queda incoherent:
// comanda anullada amb entrades valides, o aforament mai retornat. Cada operacio es
// atomica per separat, pero el CONJUNT no ho es. Aixo son les transaccions (07-06).
return comanda;
}Conclusió
Ja saps moure dades amb Mongoose de veritat: crear amb save() o create() sabent què retorna cadascun, llegir entenent que un Query és mandrós i que lean() i les projeccions són l'optimització més rendible, paginar sense caure al parany de skip, filtrar amb els operadors adequats —inclosos $elemMatch i $expr, que eviten errors subtils sobre arrays—, actualitzar sense saltar-te la validació i esborrar lògicament per no perdre la història. I saps traduir els errors del motor a la jerarquia del mòdul 6 des de la capa correcta. Sobretot, src/cataleg-dades.js està jubilat: src/repositoris/esdeveniments.js ocupa el seu lloc amb la mateixa signatura, els controladors no se n'han assabentat i el domini continua rebent les seves instàncies d'Esdeveniment. L'arquitectura de la lliçó 07-01 acaba de demostrar que servia per a alguna cosa.
Queda un deute anotat en majúscules dins de crearComanda: $inc ens ha donat atomicitat per operació, però no per conjunt, i la sobrevenda continua viva. A la lliçó següent aparquem aquest problema una mica més per atacar el modelatge avançat: incrustar davant de referenciar, populate i el problema N+1, desnormalització deliberada, el framework d'agregació aplicat a informes reals d'Escena Viva —que substituiran els que al mòdul 3 fèiem llegint CSV— i índexs de debò, amb explain() per veure si les teves consultes els fan servir o estan recorrent la col·lecció sencera.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
