5 exercices · Solutions masquées
Créez un schéma Mongoose pour une plateforme e-learning : Course (titre, description, price, level: enum, modules: embedded[], instructor: ref User, tags, published). Ajoutez la validation, les timestamps, et un index text sur titre+description.
const mongoose = require('mongoose');
const moduleSchema = new mongoose.Schema({
title: { type: String, required: true, trim: true },
content: { type: String },
duration: { type: Number, min: 0 },
order: { type: Number, required: true }
}, { _id: true });
const courseSchema = new mongoose.Schema({
title: {
type: String, required: [true, 'Titre requis'],
trim: true, minlength: 5, maxlength: 200
},
description: { type: String, trim: true, maxlength: 2000 },
price: {
type: Number, required: true, min: [0, 'Prix non négatif'],
default: 0
},
level: {
type: String,
enum: { values: ['beginner','intermediate','advanced'], message: 'Niveau invalide' },
default: 'beginner'
},
modules: [moduleSchema],
instructor: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
tags: [{ type: String, lowercase: true, trim: true }],
published: { type: Boolean, default: false },
rating: { type: Number, min: 0, max: 5, default: 0 },
enrolledCount: { type: Number, default: 0 }
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
courseSchema.index({ title: 'text', description: 'text' });
courseSchema.index({ instructor: 1, published: 1 });
courseSchema.index({ tags: 1 });
courseSchema.virtual('moduleCount').get(function() {
return this.modules.length;
});
courseSchema.virtual('totalDuration').get(function() {
return this.modules.reduce((sum, m) => sum + (m.duration || 0), 0);
});
const Course = mongoose.model('Course', courseSchema);
module.exports = Course;
Ajoutez sur un schéma User : (a) pre-save pour hasher le mot de passe uniquement si modifié, (b) méthode d'instance comparePassword, (c) pre-find pour exclure les comptes supprimés, (d) post-remove pour supprimer les cours associés.
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true, minlength: 8, select: false },
name: String,
role: { type: String, enum: ['user','instructor','admin'], default: 'user' },
active: { type: Boolean, default: true }
}, { timestamps: true });
// (a) Hash password
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
// (b) comparePassword
userSchema.methods.comparePassword = async function(candidate) {
return bcrypt.compare(candidate, this.password);
};
// (c) Exclure comptes supprimés
userSchema.pre(/^find/, function(next) {
this.find({ active: { $ne: false } });
next();
});
// (d) Supprimer les cours associés à la suppression
userSchema.post('deleteOne', { document: true }, async function() {
await mongoose.model('Course').deleteMany({ instructor: this._id });
console.log(`Cours de l'instructeur ${this._id} supprimés`);
});
const User = mongoose.model('User', userSchema);
module.exports = User;
Implémentez une fonction getCourseDetails(id) qui retourne un cours avec : l'instructeur (name, email), les 5 derniers avis (reviewer.name + content + rating), et le nombre total d'inscrits. Utilisez un virtual populate pour les reviews.
// Ajout virtual populate sur courseSchema
courseSchema.virtual('reviews', {
ref: 'Review',
localField: '_id',
foreignField: 'courseId'
});
const reviewSchema = new mongoose.Schema({
courseId: { type: mongoose.Schema.Types.ObjectId, ref: 'Course', required: true },
reviewerId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
rating: { type: Number, min: 1, max: 5, required: true },
content: { type: String, maxlength: 1000 }
}, { timestamps: true });
const Review = mongoose.model('Review', reviewSchema);
async function getCourseDetails(id) {
const course = await Course.findById(id)
.populate('instructor', 'name email avatar')
.populate({
path: 'reviews',
options: { sort: { createdAt: -1 }, limit: 5 },
populate: { path: 'reviewerId', select: 'name avatar' }
})
.lean();
if (!course) return null;
const stats = await Review.aggregate([
{ $match: { courseId: new mongoose.Types.ObjectId(id) } },
{ $group: {
_id: null,
avgRating: { $avg: '$rating' },
count: { $sum: 1 }
}}
]);
return {
...course,
stats: stats[0] || { avgRating: 0, count: 0 }
};
}
Créez un système de notifications avec un schema de base Notification et 3 sous-types via discriminators : EmailNotification (to, subject, body), PushNotification (deviceToken, title, icon), SMSNotification (phone, message). Requête toutes les notifs d'un user non lues.
const baseSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
read: { type: Boolean, default: false },
readAt: Date,
createdAt: { type: Date, default: Date.now }
}, { discriminatorKey: 'type', collection: 'notifications' });
baseSchema.methods.markRead = function() {
this.read = true;
this.readAt = new Date();
return this.save();
};
const Notification = mongoose.model('Notification', baseSchema);
const EmailNotification = Notification.discriminator('email',
new mongoose.Schema({
to: { type: String, required: true },
subject: { type: String, required: true },
body: { type: String, required: true }
})
);
const PushNotification = Notification.discriminator('push',
new mongoose.Schema({
deviceToken: { type: String, required: true },
title: { type: String, required: true },
icon: { type: String, default: '/icon.png' }
})
);
const SMSNotification = Notification.discriminator('sms',
new mongoose.Schema({
phone: { type: String, required: true },
message: { type: String, required: true, maxlength: 160 }
})
);
// Créer des notifications
await EmailNotification.create({
userId: userId,
to: 'alice@example.com', subject: 'Bienvenue', body: 'Bonjour!'
});
// Requête toutes les notifs non lues d'un user
const unread = await Notification.find({ userId, read: false })
.sort({ createdAt: -1 }).lean();
module.exports = { Notification, EmailNotification, PushNotification, SMSNotification };
Comparez les performances entre une requête standard et .lean(). Créez une fonction de migration qui utilise un curseur Mongoose pour traiter 100 000 documents par lots de 100 sans saturer la RAM.
// Benchmark lean vs normal
async function benchmark(userId) {
console.time('normal');
const normal = await Course.find({ instructor: userId })
.populate('instructor', 'name');
console.timeEnd('normal');
console.time('lean');
const lean = await Course.find({ instructor: userId })
.populate('instructor', 'name').lean();
console.timeEnd('lean');
// lean est ~2x plus rapide, mais sans méthodes de document ni virtuals
}
// Migration par curseur (évite l'OOM sur 100k docs)
async function migrateAddField() {
let processed = 0, updated = 0;
const cursor = Course.find({ searchKeywords: { $exists: false } }).cursor();
for await (const course of cursor) {
const keywords = [
...course.title.toLowerCase().split(/\s+/),
...(course.tags || [])
].filter((v, i, a) => a.indexOf(v) === i); // unique
await Course.updateOne(
{ _id: course._id },
{ $set: { searchKeywords: keywords } }
);
updated++;
if (++processed % 1000 === 0) {
console.log(`Migration: ${processed} traités, ${updated} mis à jour`);
}
}
console.log(`Migration terminée: ${updated} / ${processed}`);
}
// Version bulk pour de meilleures perfs
async function migrateBulk() {
const cursor = Course.find({ searchKeywords: { $exists: false } }).cursor();
let ops = [];
for await (const course of cursor) {
const keywords = course.title.toLowerCase().split(/\s+/);
ops.push({
updateOne: {
filter: { _id: course._id },
update: { $set: { searchKeywords: keywords } }
}
});
if (ops.length >= 100) {
await Course.bulkWrite(ops);
ops = [];
}
}
if (ops.length) await Course.bulkWrite(ops);
}