Aller au contenu

Mongoose : validation et hooks

const schemaClient = new mongoose.Schema({
nom: {
type: String,
required: [true, 'le nom est obligatoire'],
minlength: 2,
maxlength: 100,
},
email: {
type: String,
required: true,
match: [/^[^@\s]+@[^@\s]+\.[^@\s]+$/, 'adresse e-mail invalide'],
},
age: { type: Number, min: [0, 'âge négatif'], max: [130, 'âge irréaliste'] },
statut: {
type: String,
enum: { values: ['actif', 'suspendu'], message: '{VALUE} non autorisé' },
},
});
Validateur Types
required Tous
min, max Number, Date
minlength, maxlength, match, enum String
const schemaCommande = new mongoose.Schema({
lignes: {
type: [schemaLigne],
validate: {
validator: (lignes) => lignes.length > 0,
message: 'une commande doit contenir au moins une ligne',
},
},
telephone: {
type: String,
validate: {
validator: (v) => /^\+?\d{9,15}$/.test(v),
message: (props) => `${props.value} n'est pas un numéro valide`,
},
},
});

Un validateur peut être asynchrone (async validator), par exemple pour vérifier l’existence d’une référence ; limiter ces vérifications, elles ajoutent des requêtes.

try {
await Client.create({ nom: 'A', email: 'pas-un-email' });
} catch (err) {
if (err.name === 'ValidationError') {
const details = Object.values(err.errors).map((e) => ({
champ: e.path,
message: e.message,
}));
console.log(details);
} else if (err.code === 11000) {
console.log('doublon :', err.keyValue);
} else {
throw err;
}
}
Erreur Cause
ValidationError Une règle du schéma n’est pas respectée
CastError Valeur non convertible dans le type attendu
MongoServerError avec code: 11000 Violation d’un index unique

Validation sans enregistrement :

const client = new Client({ nom: '', email: 'x' });
const erreur = client.validateSync();
console.log(erreur?.errors);

unique: true crée un index unique dans MongoDB, il ne valide pas avant l’écriture : un doublon provoque l’erreur E11000 à l’enregistrement. L’index doit exister (voir Performances et production) ; tant qu’il n’est pas créé, les doublons passent. Traiter err.code === 11000 pour renvoyer une réponse claire (409).

await Client.updateOne({ _id }, { $set: { age: -5 } }, { runValidators: true });

Sans runValidators: true, updateOne et findOneAndUpdate ne valident pas. Avec cette option, certaines validations (comme required sur un champ absent de la mise à jour) ne s’appliquent qu’aux champs modifiés.

Des fonctions exécutées avant (pre) ou après (post) une opération :

import bcrypt from 'bcrypt';
schemaClient.pre('save', async function () {
if (this.isModified('motDePasse')) {
this.motDePasse = await bcrypt.hash(this.motDePasse, 12);
}
});
schemaClient.post('save', function (doc) {
console.log('client enregistré :', doc._id);
});
schemaCommande.pre('validate', function () {
this.totalCentimes = this.lignes.reduce(
(s, l) => s + l.quantite * l.prixCentimes,
0,
);
});
Hook Déclenché par
validate validate() et avant save
save save(), create()
updateOne, findOneAndUpdate, deleteOne… (hooks de requête) Les opérations correspondantes
find, findOne Lectures
aggregate Agrégations

Exemple de hook de requête : exclure par défaut les documents « supprimés logiquement » :

schemaClient.pre(/^find/, function () {
this.where({ supprimeLe: null });
});

Règles :

  • Utiliser function (pas de fonction fléchée) : this désigne le document ou la requête.
  • Les fonctions async sont prises en charge ; ne pas combiner avec next sauf besoin.
  • Model.updateOne, insertMany, bulkWrite ne déclenchent pas les hooks de document save.
Fenêtre de terminal
npm install bcrypt
schemaClient.add({
motDePasse: { type: String, required: true, select: false },
});
schemaClient.methods.verifierMotDePasse = function (candidat) {
return bcrypt.compare(candidat, this.motDePasse);
};
const client = await Client.findOne({ email }).select('+motDePasse');
const valide = client && (await client.verifierMotDePasse(motDePasseSaisi));

Ne jamais stocker un mot de passe en clair. select: false l’exclut des lectures ; select('+motDePasse') l’inclut explicitement.

app.use((err, req, res, next) => {
if (err.name === 'ValidationError') {
return res.status(400).json({
erreur: 'données invalides',
details: Object.values(err.errors).map((e) => ({
champ: e.path,
message: e.message,
})),
});
}
if (err.code === 11000) {
return res.status(409).json({
erreur: 'valeur déjà utilisée',
champs: Object.keys(err.keyValue),
});
}
if (err.name === 'CastError') {
return res
.status(400)
.json({ erreur: `identifiant invalide : ${err.value}` });
}
console.error(err);
res.status(500).json({ erreur: 'erreur interne' });
});

Avec Express 5, une erreur levée dans un gestionnaire async atteint ce gestionnaire automatiquement (voir Express : middlewares).

Symptôme Piste
La validation ne s’applique pas à une mise à jour runValidators: true
Doublons acceptés malgré unique Index non créé : Client.syncIndexes() ou createIndexes() ; données existantes en doublon
Le hook pre('save') ne s’exécute pas Mise à jour par updateOne ou insertMany : utiliser save()
Cannot read properties of undefined dans un hook Fonction fléchée : utiliser function
ValidatorError sans message lisible Ajouter un message dans la définition du champ
Mot de passe renvoyé dans le JSON select: false et suppression dans toJSON

Ubuntu 24.04 et 26.04 : différences à connaître

Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »

Aucune.