A la lliçó anterior vam decidir el model de dades d'Escena Viva i vam deixar preparada la frontera src/repositoris/. Ara baixem al codi: connectarem l'aplicació a MongoDB, escriurem el mòdul de connexió que s'integra amb l'aturada ordenada de src/servidor.js, i traduirem el diagrama de la lliçó 07-01 a esquemes i models de Mongoose a src/models/.

Aquesta lliçó encara no escriu consultes —això és la 07-03—. Escriu l'estructura: quina forma tenen els documents, quines regles fan complir, quins camps derivats exposen i quins índexs els sostenen.

Contingut

  1. Què és MongoDB: documents, col·leccions i ObjectId
  2. Què és Mongoose i si val la pena
  3. Connexió, arrencada i aturada ordenada
  4. L'esquema Esdeveniment amb sessions incrustades
  5. Tipus i opcions d'esquema
  6. Comanda, Entrada i un Usuari mínim
  7. Validadors d'esquema davant de validació zod
  8. _id davant d'identificadors de negoci
  9. Mètodes, estàtics i virtuals
  10. Middleware d'esquema i toJSON
  11. Índexs declarats

Què és MongoDB: documents, col·leccions i ObjectId

MongoDB desa documents en col·leccions. Un document és una estructura semblant a un objecte JavaScript, amb camps imbricats i arrays. Una col·lecció és l'equivalent lax d'una taula: un contenidor que no exigeix que tots els seus documents tinguin la mateixa forma.

El format intern és BSON (Binary JSON): una codificació binària que afegeix tipus que JSON no té i que a nosaltres ens importen.

Tipus BSON Equivalent JS Per què importa a Escena Viva
ObjectId Objecte de 12 bytes Identificador per defecte de tot document
Date Date dataHora es desa com a data real, no com a text
Int32 / Int64 number aforament, venudes, preuCentims són enters de veritat
Decimal128 / Binary — / Buffer Decimal exacte (no el fem servir: anem en cèntims) i buffers del M3

Tot document té _id únic dins la seva col·lecció. Si no el dónes, MongoDB genera un ObjectId: 12 bytes que combinen marca de temps, identificador de màquina/procés i un comptador. Dues conseqüències pràctiques: són únics sense coordinació central i són aproximadament ordenables per data de creació, perquè els primers 4 bytes són l'instant en segons.

En text es veu com 507f1f77bcf86cd799439011: 24 caràcters hexadecimals. Compte amb això, perquè passar una cadena que no té aquesta forma a una consulta per _id produeix un CastError, i aquest error cal traduir-lo a un 400, no deixar que es converteixi en un 500.

Què és Mongoose i si val la pena

Mongoose és un ODM: se situa sobre el controlador oficial mongodb i aporta esquema (la forma esperada dels teus documents), validació abans d'escriure, tipatge i conversió automàtica, i funcionalitat de model: hooks pre/post, mètodes, virtuals i populate.

Val la pena davant del controlador natiu? La resposta honesta: depèn de quanta disciplina puguis garantir sense ajuda. Amb el controlador natiu escrius exactament la consulta que s'executa i no hi ha sorpreses de rendiment, però cada camp obligatori, cada valor per defecte i cada conversió de tipus és codi teu repetit a cada punt d'escriptura. Mongoose centralitza això i fa que un document mal format sigui difícil de crear per accident. El cost és una capa d'indirecció: cal saber que find() retorna un Query mandrós, que lean() canvia el que reps i que un pre('save') no es dispara en un updateOne. Per a un domini amb invariants com el nostre la balança s'inclina cap a Mongoose; per a un script d'ingesta d'un milió de documents, baixaria al controlador natiu sense dubtar-ho.

S'instal·la amb npm install mongoose; inclou el controlador mongodb com a dependència, així que no cal instal·lar-lo a part.

Connexió, arrencada i aturada ordenada

L'URL entra pel únic lloc autoritzat a llegir process.env: src/config/index.js.

// src/config/index.js (fragment nou)
const mongodbUrl = process.env.MONGODB_URL ?? 'mongodb://localhost:27017/escena_viva';

if (!/^mongodb(\+srv)?:\/\//.test(mongodbUrl)) {
  throw new Error('MONGODB_URL ha de comencar per mongodb:// o mongodb+srv://');
}

const configuracio = Object.freeze({
  mongodbUrl, // ...al costat dels camps anteriors del modul 6
  mongodbTempsEspera: Number(process.env.MONGODB_TEMPS_ESPERA ?? 5000),
});

Validar en arrencar és deliberat: si l'URL és incorrecta ens en volem assabentar en llançar el procés, no a la primera petició d'un client.

// src/db/connexio.js
'use strict';

const mongoose = require('mongoose');
const { configuracio } = require('../config/index.js');

// Mongoose 7+ no permet consultar per camps no declarats a l'esquema.
mongoose.set('strictQuery', true);

let connexioActiva = null;

/** Obre la connexio. Es idempotent: si ja n'hi ha una, la reutilitza. */
async function connectar({ url = configuracio.mongodbUrl } = {}) {
  if (connexioActiva) return connexioActiva;
  registrarEsdeveniments();
  await mongoose.connect(url, {
    // Si no troba servidor en aquest temps, rebutja en lloc d'esperar sempre.
    serverSelectionTimeoutMS: configuracio.mongodbTempsEspera,
    // Pool de sockets reutilitzats: operacions simultanies contra el motor.
    maxPoolSize: 10,
    minPoolSize: 1,
  });
  connexioActiva = mongoose.connection;
  return connexioActiva;
}

async function desconnectar() {
  if (!connexioActiva) return;
  await mongoose.disconnect();
  connexioActiva = null;
}

function registrarEsdeveniments() {
  const { connection } = mongoose;
  // No registrem l'URL completa: pot portar credencials.
  connection.on('connected', () => console.log(`[db] connectat a '${connection.name}'`));
  connection.on('error', (error) => console.error('[db] error:', error.message));
  connection.on('disconnected', () => console.warn('[db] desconnectat; reintentara'));
}

module.exports = { connectar, desconnectar };

Tres detalls que no són adorn. serverSelectionTimeoutMS: sense ell, una URL equivocada deixa l'arrencada penjada sense explicació. El pool: Mongoose no obre una connexió per consulta, manté un conjunt de sockets; si maxPoolSize fos 1, serialitzaries tota l'aplicació. Els esdeveniments: després d'un disconnected el controlador reintenta automàticament i encua les operacions pendents; no cal que escriguis reconnexió, però sí que registris l'esdeveniment per no quedar-te cec durant una caiguda.

crearAplicacio() continua sent una factoria pura que no toca la xarxa. La connexió pertany al procés, i el procés el governa src/servidor.js:

// src/servidor.js (fragment adaptat)
async function arrencarServidor() {
  await connectar(); // 1. Primer la base: sense dades no te sentit donar transit.
  const servidor = crearAplicacio().listen(configuracio.port);

  async function aturar(senyal) {
    console.log(`[http] rebuda ${senyal}, aturant de forma ordenada`);
    // 2. Deixem d'acceptar peticions i, nomes quan les que estan en vol
    //    han acabat, 3. tanquem la connexio amb la base de dades.
    servidor.close(async () => {
      await desconnectar();
      process.exit(0);
    });
  }

  process.on('SIGTERM', () => aturar('SIGTERM'));
  process.on('SIGINT', () => aturar('SIGINT'));
  return servidor;
}

L'ordre importa i és simètric. En arrencar: base primer, HTTP després, perquè cap petició no arribi a una aplicació sense dades. En aturar: HTTP primer, base després, perquè tancar la connexió amb peticions en vol produiria errors en compres ja en curs. La connexió és un recurs del procés, amb el seu mateix cicle de vida.

L'esquema Esdeveniment amb sessions incrustades

// src/models/esdeveniment.js
'use strict';

const mongoose = require('mongoose');

const { Schema } = mongoose;
const ESTATS_ESDEVENIMENT = ['esborrany', 'publicat', 'finalitzat'];

const esquemaSessio = new Schema(
  {
    // Identificador de negoci, amb el format ses-NNN-M que ja valida zod.
    sessioId: { type: String, required: true, match: /^ses-\d{3}-\d+$/ },
    dataHora: { type: Date, required: true },
    aforament: { type: Number, required: true, min: 1 },
    venudes: { type: Number, required: true, default: 0, min: 0 },
    preuCentims: { type: Number, required: true, min: 0 },
  },
  // Els subdocuments no necessiten _id: els identifica sessioId.
  { _id: false },
);

// Validador personalitzat: la invariant central del domini.
esquemaSessio.path('venudes').validate(function comprovarAforament(valor) {
  return valor <= this.aforament;
}, "Les entrades venudes no poden superar l'aforament");

const esquemaEsdeveniment = new Schema(
  {
    esdevenimentId: { type: String, required: true, unique: true, match: /^evt-\d{3}$/ },
    titol: { type: String, required: true, trim: true, maxlength: 160 },
    sala: { type: String, required: true, trim: true, index: true },
    organitzadorId: { type: String, required: true, index: true },
    categoria: { type: String, required: true, trim: true },
    estat: { type: String, required: true, enum: ESTATS_ESDEVENIMENT, default: 'esborrany' },
    duracioMinuts: { type: Number, required: true, min: 1, max: 600 },
    sessions: { type: [esquemaSessio], default: [] },
  },
  { timestamps: true },
);

const Esdeveniment = mongoose.model('Esdeveniment', esquemaEsdeveniment);

module.exports = { Esdeveniment, ESTATS_ESDEVENIMENT };

L'esquema reprodueix camp a camp el que ja sabíem del domini: evt-001 "Concierto de Otono" al Teatro Almendra, amb org-almendra i les sessions ses-001-1 i ses-001-2. No s'inventa res de nou; es persisteix el que ja existia. I { timestamps: true } afegeix createdAt i updatedAt automàtics: el principi de l'historial que trobàvem a faltar al JSON.

Tipus i opcions d'esquema

Opció Què fa Exemple
type String, Number, Date, Boolean, ObjectId, arrays, subdocuments dataHora: Date
required Rebutja el desament si falta aforament
default Valor si no s'indica; admet funció venudes: 0
enum Llista tancada de valors estat
min / max Rang numèric o de dates preuCentims: { min: 0 }
minlength / maxlength Longitud de cadena titol fins a 160
match Expressió regular codi: EV-\d{4}-\d{6}
trim / lowercase Normalitza abans de desar correu
unique / index No validen: declaren índexs (únic o normal) esdevenimentId, sala
immutable Impedeix modificar-lo després de crear-lo codi
timestamps Opció d'esquema: createdAt/updatedAt Tots els models

La fila de unique mereix èmfasi: unique: true no valida res a Mongoose. Demana a MongoDB que creï un índex únic, i qui rebutja el duplicat és el motor, amb un error E11000 en temps d'escriptura, no un ValidationError. A la lliçó següent el traduirem a un 409.

Comanda, Entrada i un Usuari mínim

// src/models/comanda.js
const ESTATS_COMANDA = ['pendent', 'pagat', 'emes', 'anullat'];

const esquemaComanda = new Schema(
  {
    // Referencia a una altra colleccio: desa el _id, no l'usuari sencer.
    usuariId: { type: Schema.Types.ObjectId, ref: 'Usuari', required: true, index: true },
    esdevenimentId: { type: String, required: true },
    sessioId: { type: String, required: true, index: true },
    quantitat: { type: Number, required: true, min: 1, max: 6 },
    totalCentims: { type: Number, required: true, min: 0 },
    estat: { type: String, required: true, enum: ESTATS_COMANDA, default: 'pendent' },
    canal: { type: String, required: true, enum: ['web', 'taquilla', 'telefon'] },
  },
  { timestamps: true },
);

const Comanda = mongoose.model('Comanda', esquemaComanda);
module.exports = { Comanda, ESTATS_COMANDA };
// src/models/entrada.js
const ESTATS_ENTRADA = ['valida', 'usada', 'anullada'];

const esquemaEntrada = new Schema(
  {
    // El codi impres: EV-<any>-<6 digits>. Unic i immutable.
    codi: { type: String, required: true, unique: true, immutable: true, match: /^EV-\d{4}-\d{6}$/ },
    comandaId: { type: Schema.Types.ObjectId, ref: 'Comanda', required: true, index: true },
    sessioId: { type: String, required: true, index: true },
    estat: { type: String, required: true, enum: ESTATS_ENTRADA, default: 'valida' },
    // Moment de l'escaneig a la porta; nul mentre no s'ha usat.
    usadaEl: { type: Date, default: null },
  },
  { timestamps: true },
);

const Entrada = mongoose.model('Entrada', esquemaEntrada);
module.exports = { Entrada, ESTATS_ENTRADA };

// --- src/models/usuari.js ---
const ROLS = ['assistent', 'organitzador', 'administrador'];

const esquemaUsuari = new Schema(
  {
    correu: { type: String, required: true, unique: true, lowercase: true, trim: true },
    nom: { type: String, required: true, trim: true },
    rol: { type: String, required: true, enum: ROLS, default: 'assistent' },
    // Aqui NO hi ha contrasenya. L'autenticacio es el modul 8 sencer:
    // hash, sessions, JWT i control d'acces per rol.
  },
  { timestamps: true },
);

const Usuari = mongoose.model('Usuari', esquemaUsuari);
module.exports = { Usuari, ROLS };

Aquest comentari a Usuari no és un descuit: és una decisió. L'usuari existeix per poder referenciar-lo des de les comandes i perquè els rols ja estiguin modelats, però no implementem autenticació. Desar contrasenyes malament és pitjor que no desar-les.

Validadors d'esquema davant de validació zod

Al mòdul 6 vam posar zod a la vora, amb esquemaCrearComanda i el middleware validar que deixa el resultat a req.dadesValidades. Ara apareix la validació de l'esquema de Mongoose. No és duplicar feina? No: són capes diferents, amb clients diferents.

zod (vora HTTP) Mongoose (esquema)
Què valida La forma del cos de la petició La forma del document abans d'escriure'l
Qui la dispara Cada petició HTTP Cada save() o create(), vingui d'on vingui
Protegeix de Clients que envien brossa Bugs propis, scripts, migracions, llavors
Error que produeix ErrorDeValidacio → 400/422 ValidationError de Mongoose

La regla en una frase: zod valida el que entra per la porta; Mongoose valida el que surt cap al disc. Un script d'importació nocturn no passa per Express i per tant no passa per zod; si l'esquema no validés, podria deixar venudes: 9999 en una sessió d'aforament 300. I zod pot rebutjar coses que a Mongoose li són indiferents (un camp extra, un canal desconegut) abans de gastar un viatge a la base. Per damunt de totes dues continua el domini: Sessio.vendre() conserva la seva invariant en memòria. Tres capes, tres moments, cap redundant.

_id davant d'identificadors de negoci

Els nostres documents tenen dos identificadors. El _id és tècnic: únic, generat sense coordinació, eficient com a clau d'índex i destí de les referències que fa servir populate. L'identificador de negoci (evt-001, ses-001-1, EV-2026-000431) és semàntic: apareix a URL, correus, entrades impreses, els CSV del mòdul 3 i els esquemes zod del mòdul 6; i és estable davant de migracions, perquè si demà movem les dades a PostgreSQL els _id desapareixen però evt-001 continua sent evt-001. Fer servir _id a URL públiques et lliga al motor i filtra informació interna; fer servir només el de negoci com a _id és defensable, però perds l'ordenació temporal implícita. Conservar tots dos, amb índex únic sobre el de negoci, és l'habitual en sistemes que esperen viure anys.

Mètodes, estàtics i virtuals

Els virtuals reprodueixen els getters rics del domini del mòdul 2: camps calculats que no es desen.

// src/models/esdeveniment.js (ampliacio)
esquemaSessio.virtual('lliures').get(function () {
  return this.aforament - this.venudes;
});

esquemaSessio.virtual('ocupacio').get(function () {
  return this.aforament === 0 ? 0 : Number((this.venudes / this.aforament).toFixed(4));
});

esquemaSessio.virtual('exhaurida').get(function () {
  return this.venudes >= this.aforament;
});

esquemaEsdeveniment.virtual('aforamentTotal').get(function () {
  return this.sessions.reduce((total, sessio) => total + sessio.aforament, 0);
});

// Metode d'instancia: opera sobre UN document.
esquemaEsdeveniment.methods.cercarSessio = function (sessioId) {
  return this.sessions.find((sessio) => sessio.sessioId === sessioId) ?? null;
};

// Metode estatic: opera sobre el model (la colleccio sencera).
esquemaEsdeveniment.statics.cercarPerSala = function (sala) {
  return this.find({ sala, estat: 'publicat' }).sort({ titol: 1 });
};

Dos avisos. Els virtuals no existeixen a la base de dades: no pots filtrar ni ordenar per ocupacio, perquè MongoDB no sap que existeix; per a això cal una agregació (lliçó 07-04) o desar-ho desnormalitzat. I lean() els elimina: si demanes objectes plans per rendiment, perds virtuals i mètodes.

Sobre function davant d'arrow: a mètodes, virtuals i hooks cal fer servir function, perquè Mongoose vincula this al document. Una arrow captura el this lèxic del mòdul i et deixarà amb undefined. És l'error número u de qui comença.

Middleware d'esquema i toJSON

// src/models/comanda.js: s'executa abans de desar; llancar avorta l'escriptura.
esquemaComanda.pre('save', function (next) {
  if (this.isNew && this.totalCentims === 0 && this.quantitat > 0) {
    return next(new Error('Una comanda amb entrades no pot tenir total zero'));
  }
  next();
});

Ús responsable dels hooks significa tres coses:

  1. Res d'efectes externs. Enviar un correu o cridar una API dins d'un pre('save') acobla la persistència a un servei remot: una fallada de xarxa impediria desar. Això va fora, o al GestorDeVendes que ja tenim.
  2. Recorda quins hooks es disparen. pre('save') no s'executa en updateOne, findOneAndUpdate ni deleteMany, perquè aquestes operacions passen al servidor sense materialitzar el document. Si la lògica és innegociable, posa-la a l'esquema o al repositori. I que siguin ràpids: un hook lent es paga a cada escriptura.
// Transformacio de sortida, aplicable als quatre esquemes.
const configurarSortida = (esquema) => esquema.set('toJSON', {
  virtuals: true,     // inclou lliures, ocupacio, exhaurida...
  versionKey: false,  // elimina __v
  transform(document, pla) {
    pla.id = document._id.toString();
    delete pla._id;
    return pla;
  },
});

El __v és el comptador intern de versió de Mongoose: no aporta res a un client HTTP i només genera preguntes. I exposar _id en brut acobla el teu contracte públic al motor. Aquesta transformació és la frontera entre "com deso" i "què publico", la mateixa separació que defensaven els repositoris.

Índexs declarats

Un índex accelera lectures i encareix escriptures. Es declaren per consulta prevista, no per caprici:

// Cataleg d'una sala, ordenat: primer els filtres per igualtat, despres el
// camp d'ordenacio.
esquemaEsdeveniment.index({ estat: 1, sala: 1, titol: 1 });
// Rang de dates. En ser un camp d'un array de subdocuments, MongoDB crea
// un index multiclau automaticament.
esquemaEsdeveniment.index({ 'sessions.dataHora': 1 });
// Localitzar l'esdeveniment que conte una sessio. Unic: un sessioId no es repeteix.
esquemaEsdeveniment.index({ 'sessions.sessioId': 1 }, { unique: true });
Índex Consulta que accelera
esdevenimentId (per unique) GET /esdeveniments/evt-001
{ estat, sala, titol } Catàleg del Teatro Almendra ordenat per títol
sessions.dataHora "Què hi ha en cartell de l'1 al 15 de març"
sessions.sessioId (únic) Trobar ses-001-2 per vendre
Entrada.codi (únic) / Comanda.usuariId Escaneig a la porta / "les meves comandes"

En desenvolupament Mongoose crea aquests índexs en arrencar (autoIndex). En producció s'ha de desactivar (mongoose.set('autoIndex', false)) i crear-los a la migració corresponent: construir un índex sobre milions de documents durant l'arrencada pot blocar l'aplicació uns quants minuts. Ho reprenem a la lliçó 07-06.

Errors Comuns i Consells

  • Arrow functions a mètodes, virtuals o hooks. this deixa de ser el document i tot retorna undefined.
  • Creure que unique: true valida. És un índex; l'error arriba com a E11000, no com a ValidationError.
  • Cridar mongoose.connect a cada petició. La connexió és del procés i té pool; connectar per petició exhaureix els sockets del sistema.
  • Registrar l'URL de connexió. Si porta credencials, acabes de filtrar-les. Registra connection.name.
  • Definir el mateix model dues vegades. mongoose.model('Esdeveniment', ...) en dos fitxers llança OverwriteModelError.
  • Consell: comença amb required i enum generosos —és més barat relaxar un esquema que netejar dades incoherents— i els diners sempre enters i en cèntims.

Exercicis

Exercici 1: model de sala

Escriu src/models/sala.js amb salaId (únic, format sala-xxx), nom, ciutat, aforamentMaxim (enter ≥ 1) i activa (booleà, per defecte true). Afegeix un virtual etiqueta que retorni "<nom> (<ciutat>)", un estàtic cercarActives(), timestamps i la transformació toJSON.

Exercici 2: validador creuat

Afegeix a Entrada un validador que impedeixi marcar usadaEl amb una data quan l'estat sigui anullada. Explica per què no es dispararia amb findByIdAndUpdate sense opcions.

Exercici 3: triar índexs

Decideix l'índex i l'ordre dels seus camps per a: (a) comandes pagat d'un usuari ordenades per data descendent; (b) entrades valida d'una sessió; (c) esdeveniments d'un organitzador en estat publicat.

Solucions

Exercici 1.

// src/models/sala.js
const esquemaSala = new Schema(
  {
    salaId: { type: String, required: true, unique: true, match: /^sala-[a-z]+$/ },
    nom: { type: String, required: true, trim: true },
    ciutat: { type: String, required: true, trim: true, index: true },
    aforamentMaxim: { type: Number, required: true, min: 1 },
    activa: { type: Boolean, required: true, default: true },
  },
  { timestamps: true },
);

esquemaSala.virtual('etiqueta').get(function () {
  return `${this.nom} (${this.ciutat})`;
});
esquemaSala.statics.cercarActives = function () {
  return this.find({ activa: true }).sort({ nom: 1 });
};
esquemaSala.set('toJSON', { virtuals: true, versionKey: false });

module.exports = { Sala: mongoose.model('Sala', esquemaSala) };

Exercici 2.

esquemaEntrada.path('usadaEl').validate(function comprovarUs(valor) {
  // Una entrada anullada mai no pot tenir marca d'us.
  return !(this.estat === 'anullada' && valor !== null);
}, "Una entrada anullada no pot registrar data d'us");

No es dispara perquè les actualitzacions s'executen al servidor sense construir el document: Mongoose només valida si li passes { runValidators: true }, i tot i així this és la consulta, no el document, així que un validador que depèn d'altres camps pot no tenir-hi accés.

Exercici 3. (a) { usuariId: 1, estat: 1, createdAt: -1 }. (b) { sessioId: 1, estat: 1 }. (c) { organitzadorId: 1, estat: 1 }. La regla s'anomena ESR: primer els camps comparats per igualtat (Equality), després el d'ordenació (Sort) i finalment els de rang (Range).

Conclusió

Escena Viva ja té persistència real. Saps què desa MongoDB i com, què hi afegeix Mongoose per damunt i a quin preu, i com obrir la connexió des de la configuració validada i integrar-la amb l'arrencada i l'aturada ordenada. Has traduït el diagrama de la lliçó anterior a quatre models —Esdeveniment amb les seves sessions incrustades, Comanda, Entrada i un Usuari sense contrasenyes a l'espera del mòdul 8— amb els seus tipus, validadors, virtuals, middleware, toJSON net i índexs raonats. I has vist per què conviuen tres capes de validació sense destorbar-se: zod a la vora, l'esquema abans del disc i el domini en memòria.

A la lliçó següent comencem a moure dades: les quatre operacions CRUD amb els seus paranys —Query mandrosos, lean(), paginació, runValidators, $inc, esborrat lògic—, la traducció dels errors de Mongoose a la nostra jerarquia del mòdul 6, i el moment que esperàvem: jubilar src/cataleg-dades.js i substituir-lo per src/repositoris/esdeveniments.js sense que els controladors se n'assabentin.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats