5 exercices · Solutions masquées
À partir d'une collection orders (userId, items[], total, status, createdAt), écrivez un pipeline qui retourne : CA mensuel, nombre de commandes, panier moyen, et variation mensuelle en %.
const result = await db.collection('orders').aggregate([
{ $match: { status: 'completed' } },
{
$group: {
_id: { $dateToString: { format: '%Y-%m', date: '$createdAt' } },
revenue: { $sum: '$total' },
count: { $sum: 1 },
avgCart: { $avg: '$total' }
}
},
{ $sort: { _id: 1 } },
{
$setWindowFields: {
sortBy: { _id: 1 },
output: {
prevRevenue: {
$shift: { output: '$revenue', by: -1 }
}
}
}
},
{
$addFields: {
growth: {
$cond: {
if: { $gt: ['$prevRevenue', 0] },
then: {
$multiply: [
{ $divide: [{ $subtract: ['$revenue', '$prevRevenue'] }, '$prevRevenue'] },
100
]
},
else: null
}
}
}
},
{ $project: { month: '$_id', revenue: 1, count: 1, avgCart: { $round: ['$avgCart', 2] }, growth: { $round: ['$growth', 1] }, _id: 0 } }
]).toArray();
Créez un rapport "top 5 produits les plus commandés" en joignant order_items avec products et categories. Le résultat doit inclure : nom du produit, catégorie, total commandé, CA généré.
const topProducts = await db.collection('order_items').aggregate([
{
$group: {
_id: '$productId',
totalQty: { $sum: '$qty' },
revenue: { $sum: { $multiply: ['$price', '$qty'] } },
nbOrders: { $sum: 1 }
}
},
{ $sort: { totalQty: -1 } },
{ $limit: 5 },
{
$lookup: {
from: 'products',
localField: '_id',
foreignField: '_id',
as: 'product'
}
},
{ $unwind: '$product' },
{
$lookup: {
from: 'categories',
localField: 'product.categoryId',
foreignField: '_id',
as: 'category'
}
},
{ $unwind: { path: '$category', preserveNullAndEmpty: true } },
{
$project: {
productName: '$product.name',
categoryName: '$category.name',
totalQty: 1,
revenue: { $round: ['$revenue', 2] },
nbOrders: 1,
_id: 0
}
}
]).toArray();
Sur une collection articles avec un champ tags[], écrivez un pipeline qui retourne : pour chaque tag, le nombre d'articles, le nombre moyen de vues, et les 3 articles les plus lus.
const tagStats = await db.collection('articles').aggregate([
{ $match: { published: true } },
{ $unwind: '$tags' },
{
$group: {
_id: '$tags',
articleCount: { $sum: 1 },
avgViews: { $avg: '$views' },
articles: {
$push: { title: '$title', views: '$views', _id: '$_id' }
}
}
},
{ $sort: { articleCount: -1 } },
{
$project: {
tag: '$_id',
articleCount: 1,
avgViews: { $round: ['$avgViews', 0] },
top3: {
$slice: [
{ $sortArray: { input: '$articles', sortBy: { views: -1 } } },
3
]
},
_id: 0
}
}
]).toArray();
Implémentez un virement entre deux comptes bancaires MongoDB (collection accounts). La transaction doit : vérifier le solde, débiter l'expéditeur, créditer le destinataire, créer une entrée transactions. Rollback si solde insuffisant.
const { MongoClient, ObjectId } = require('mongodb');
async function transfer(fromAccountId, toAccountId, amount, description = '') {
const client = new MongoClient(process.env.MONGODB_URI);
await client.connect();
const db = client.db('banking');
const session = client.startSession();
try {
session.startTransaction({
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' }
});
const accounts = db.collection('accounts');
const sender = await accounts.findOne(
{ _id: new ObjectId(fromAccountId) }, { session }
);
if (!sender || sender.balance < amount) {
throw new Error(`Solde insuffisant: ${sender?.balance ?? 0} < ${amount}`);
}
await accounts.updateOne(
{ _id: new ObjectId(fromAccountId) },
{ $inc: { balance: -amount }, $currentDate: { updatedAt: true } },
{ session }
);
await accounts.updateOne(
{ _id: new ObjectId(toAccountId) },
{ $inc: { balance: amount }, $currentDate: { updatedAt: true } },
{ session }
);
const txn = await db.collection('transactions').insertOne({
fromAccountId: new ObjectId(fromAccountId),
toAccountId: new ObjectId(toAccountId),
amount, description, status: 'completed',
createdAt: new Date()
}, { session });
await session.commitTransaction();
return { txnId: txn.insertedId, success: true };
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
await session.endSession();
await client.close();
}
}
Implémentez un système d'alertes qui écoute la collection inventory et déclenche une alerte quand le stock d'un produit tombe en dessous de 5. Gérez la reconnexion en cas d'erreur en sauvegardant le resume token.
let resumeToken = null;
async function watchInventory(db) {
const collection = db.collection('inventory');
const pipeline = [
{
$match: {
operationType: { $in: ['update', 'replace'] },
'updateDescription.updatedFields.stock': { $exists: true }
}
}
];
const options = resumeToken
? { resumeAfter: resumeToken, fullDocument: 'updateLookup' }
: { fullDocument: 'updateLookup' };
const changeStream = collection.watch(pipeline, options);
changeStream.on('change', async (change) => {
resumeToken = change._id; // sauvegarder pour reconnexion
const doc = change.fullDocument;
if (doc && doc.stock < 5) {
await sendLowStockAlert({
productId: doc._id,
productName: doc.name,
currentStock: doc.stock,
threshold: 5
});
}
});
changeStream.on('error', (err) => {
console.error('Change stream error:', err.message);
changeStream.close();
// Reconnexion avec exponential backoff
setTimeout(() => watchInventory(db), 5000);
});
process.on('SIGINT', () => changeStream.close());
return changeStream;
}
async function sendLowStockAlert(info) {
console.warn(`[ALERT] Stock bas: ${info.productName} → ${info.currentStock} unités`);
// Intégration : Redis Pub/Sub, email, Slack...
}