Introduction
RIDGE & REED vend des vêtements de plein air. Un client cherche un article vert, de taille moyenne, en stock et ne coûtant pas plus de 5000 centimes. Un produit peut avoir une variante verte et une variante de taille moyenne en stock sans avoir de variante verte de taille moyenne disponible. Même après avoir choisi le bon produit, afficher tout son tableau de variantes ou son option indisponible la moins chère induirait encore le client en erreur.
Vous enregistrerez un sélecteur de produits réutilisable, puis construirez un pipeline d’agrégation MongoDB qui renvoie tous les SKU admissibles et calcule le prix le plus bas à partir de ces mêmes variantes. Vous devez savoir naviguer dans un terminal, vous connecter à MongoDB, utiliser les objets/tableaux et posséder des connaissances élémentaires de find/update. Les modèles de fonctions JavaScript, les expressions et les étapes d’agrégation sont expliqués avant leur utilisation. Utilisez Terminal tout au long du TP ; MongoDB, le stock fictif et les clients en lecture seule sont prêts.
Modifiez uniquement selector.js et finder.js. Conservez tous les produits, identifiants de variante, attributs, stocks et prix d’origine, ainsi que les outils fournis. La disponibilité est une quantité en stock préparée, pas une garantie de réservation ni de passage en caisse. Chaque variante réelle possède un SKU, une couleur et une taille textuels, ainsi qu’un stock et un prix en centimes entiers non négatifs. Les variantes d’un produit peuvent être un tableau, null ou absentes ; les autres types d’entrée malformés sont hors du périmètre de cet exercice.
Appliquer toutes les conditions à une même variante
Dans cette étape, vous comparerez la demande d’un client à un véritable produit et enregistrerez un sélecteur réutilisable qui rejette une correspondance trompeuse combinant plusieurs variantes. Commencez dans Terminal. cd change de répertoire ; réutilisez ce chemin absolu si vous entrez dans un mauvais répertoire :
cd /home/labex/project/variant-desk
./catalog est un lecteur fourni dans ce répertoire ; ./ désigne le répertoire courant. Donnez-lui un identifiant de produit pour lire le véritable document enregistré :
./catalog TRAIL-1
Trail Jacket possède ces trois variantes distinctes :
| SKU | Couleur | Taille | Stock | Prix en centimes |
|---|---|---|---|---|
| T-GM | green | M | 0 | 1800 |
| T-BM | blue | M | 5 | 2200 |
| T-GL | green | L | 4 | 2100 |
La variante verte de taille moyenne est en rupture de stock. Les variantes bleue de taille moyenne et verte de grande taille sont disponibles, mais aucune ne satisfait la demande complète. Un SKU identifie une variante ; un identifiant de produit identifie son produit parent. Conservez ces relations.
./db connecte le shell MongoDB au catalogue en lecture seule de ce cours. --eval exécute le JavaScript entre guillemets puis quitte. Les guillemets simples du shell protègent les espaces et les opérateurs MongoDB préfixés par un dollar contre l’expansion du shell ; les guillemets doubles à l’intérieur appartiennent aux chaînes JavaScript. Les chemins à points tels que "variants.color" accèdent aux champs imbriqués. $gt: 0 signifie supérieur à zéro ; $lte: 5000 signifie inférieur ou égal à 5000, en incluant la limite du budget. Les valeurs numériques sont des nombres, pas des chaînes monétaires entre guillemets.
Cette requête ordinaire par chemins à points est volontairement trop large. Le deuxième argument de find projette uniquement l’identifiant de produit : 1 inclut un champ et _id: 0 omet l’identifiant interne de MongoDB. sort avec 1 trie par ordre croissant ; toArray() rassemble les documents renvoyés dans un tableau lisible :
./db --eval 'db.products.find({"variants.color":"green","variants.size":"M","variants.stock":{$gt:0},"variants.price_cents":{$lte:5000}},{_id:0,product_id:1}).sort({product_id:1}).toArray()'
Vous devez obtenir CAMP-3, RIDGE-2 et TRAIL-1. TRAIL-1 est la fausse correspondance : différents éléments du tableau fournissent différentes conditions. La requête s’est exécutée avec succès, mais sa réponse métier est incorrecte.
$elemMatch place tous les critères sur un seul élément du tableau. Vous connaissez peut-être déjà cet opérateur grâce aux bases des tableaux MongoDB ; il constitue ici la première brique d’un résultat complet destiné au client :
./db --eval 'db.products.find({variants:{$elemMatch:{color:"green",size:"M",stock:{$gt:0},price_cents:{$lte:5000}}}},{_id:0,product_id:1}).sort({product_id:1}).toArray()'
Il ne reste maintenant que CAMP-3 et RIDGE-2. Un produit correspondant possède au moins une variante admissible. Cela ne signifie pas encore que chaque variante de ce produit est admissible.
Enregistrez un sélecteur qui prend la demande en argument au lieu de fixer green/M/5000 dans la requête. Une fonction JavaScript est du code réutilisable : function buildSelector(request) lui donne un nom et reçoit un objet appelé request ; request.color lit la propriété couleur de cet objet. return fournit l’objet de requête au client préparé. Les accolades contiennent des objets ou le corps de la fonction, les deux-points relient les clés d’objet aux valeurs et les virgules séparent les propriétés. L’interface requise est buildSelector(request) renvoyant un objet de requête. Ce n’est ni une commande shell ni un export de module.
Dans le shell, cat > remplace le fichier de destination. <<'JS' commence un here-document littéral qui préserve les signes dollar et les guillemets. Collez le JS final seul sur une ligne ; saisissez le bloc entier à l’invite du shell :
cat > selector.js <<'JS'
function buildSelector(request) {
return {
variants: { $elemMatch: {
color: request.color,
size: request.size,
stock: { $gt: 0 },
price_cents: { $lte: request.max_cents }
} }
};
}
JS
./select lit cette fonction enregistrée et exécute son sélecteur renvoyé sans le modifier avec find de MongoDB. Ses trois arguments sont la couleur, la taille et le prix maximal en centimes. Le client convertit le budget en nombre ; les chaînes telles que green et M restent des libellés exacts sensibles à la casse :
./select green M 5000
Les véritables documents correspondants sont CAMP-3 et RIDGE-2. Leurs tableaux d’origine apparaissent encore en entier, y compris les variantes qui ne correspondent pas. C’est normal pour find : la sélection choisit les documents, pas les éléments à afficher. Conservez le sélecteur comme résultat utile, puis traitez le problème d’affichage à l’étape suivante.
Si un fichier manque, utilisez ls pour lister ce répertoire et vérifiez le nom du fichier ainsi que le répertoire de travail absolu. En cas d’erreur de syntaxe JavaScript, remplacez le bloc complet enregistré par sa version corrigée et relancez le client. Si le shell attend toujours, saisissez la ligne finale JS manquante. Ne modifiez pas le stock source, ne supprimez pas de variantes et ne réinitialisez pas la base pour donner l’impression qu’une requête est correcte.
Renvoyer chaque SKU admissible
Dans cette étape, vous transformerez la sélection de produits en un résultat complet de variantes, sans renvoyer uniquement la première correspondance ni exposer tout le tableau. Commencez dans le même Terminal, dans /home/labex/project/variant-desk.
Lisez les deux véritables correspondances :
./catalog RIDGE-2
./catalog CAMP-3
Pour green/M avec un maximum de 5000 centimes, Ridge Jacket possède deux SKU admissibles : R-GM1 à 4500 et R-GM2 à 5000. R-GM3 est trop cher ; les autres tailles/couleurs et la variante à stock nul ne sont pas admissibles. Camp Vest possède C-GM1 et C-GM2, tous deux à 4000. Des prix égaux ne transforment pas des SKU différents en doublons.
Une projection positionnelle, "variants.$": 1, renvoie uniquement le premier élément du tableau qui correspond. Observez pourquoi elle ne peut pas fournir l’ensemble complet demandé :
./db --eval 'db.products.find({variants:{$elemMatch:{color:"green",size:"M",stock:{$gt:0},price_cents:{$lte:5000}}}},{_id:0,product_id:1,"variants.$":1}).sort({product_id:1}).toArray()'
Chaque produit ne possède qu’une variante dans cette sortie. Le second SKU légitime manque alors qu’il est disponible. Renvoyer tout le tableau d’origine échouerait dans l’autre sens en incluant des variantes indésirables.
Un pipeline d’agrégation est un tableau d’étapes traitées dans l’ordre. Chaque étape reçoit les documents de l’étape précédente. $project choisit les champs de sortie et peut calculer un nouveau champ. $filter calcule un tableau contenant chaque élément dont la condition est vraie ; aucune limite n’est appliquée ici.
Dans $filter, input est le tableau source et as: "v" nomme la variante courante. "$variants" désigne un champ du document ; "$$v.color" désigne la couleur de l’élément courant du tableau. Ces signes dollar sont des expressions MongoDB dans des chaînes JavaScript. request.color est une valeur de demande JavaScript fournie par le client.
Les comparaisons d’agrégation utilisent un tableau d’opérandes : { $eq: [left, right] } vérifie l’égalité, $gt vérifie la relation strictement supérieure et $lte la relation inférieure ou égale. $and impose que chaque comparaison soit vraie pour ce même v. Cette écriture diffère de celle de l’objet de requête dans $elemMatch, même si les conditions métier sont identiques.
$ifNull: ["$variants", []] remplace un champ source null ou absent par un tableau vide. [] est un tableau JavaScript vide. Une valeur qui n’est pas un tableau, comme une chaîne, est hors du contrat d’entrée fourni ; ce pipeline ne prétend pas nettoyer des catalogues malformés quelconques.
Après le filtrage, $match retire les produits sans variante admissible. "variants.0" désigne l’indice zéro, le premier élément du tableau ; $exists: true exige son existence. Il s’agit d’un prédicat de requête dans une étape de correspondance, pas d’une expression dans la condition du filtre. Le $sort final rend la véritable sortie pratique à lire.
Enregistrez le modèle complet dans finder.js. Son interface requise est buildPipeline(request) renvoyant un tableau d’étapes. Le client exécutera ce pipeline renvoyé sans le modifier ; il ne filtre pas sa réponse ensuite :
cat > finder.js <<'JS'
function buildPipeline(request) {
return [
{ $project: {
_id: 0, product_id: 1, title: 1,
variants: { $filter: {
input: { $ifNull: ["$variants", []] },
as: "v",
cond: { $and: [
{ $eq: ["$$v.color", request.color] },
{ $eq: ["$$v.size", request.size] },
{ $gt: ["$$v.stock", 0] },
{ $lte: ["$$v.price_cents", request.max_cents] }
] }
} }
} },
{ $match: { "variants.0": { $exists: true } } },
{ $sort: { product_id: 1 } }
];
}
JS
./finder green M 5000
Vous devez obtenir exactement deux produits. CAMP-3 contient C-GM1 et C-GM2 ; RIDGE-2 contient R-GM1 et R-GM2. Chaque variante renvoyée conserve exactement son SKU, sa couleur, sa taille, son stock et son prix en centimes. Le produit conserve ses product_id et title exacts. Il n’y a pas encore de champ de prix minimal ; ce sera l’étape suivante.
Le pipeline renvoie maintenant les quatre SKU admissibles, y compris les deux variantes de Camp Vest à prix égal et le SKU de Ridge exactement à la limite du budget. TRAIL-1, les tableaux vides et les tableaux null ou absents ne produisent aucun produit dans le résultat. Aucun document source n’a été supprimé ou modifié pour créer ce résultat.
Si le client signale que buildPipeline doit renvoyer un tableau de pipeline, vérifiez que le fichier enregistré comprend la fonction entière et return [ ... ];. S’il signale une erreur de syntaxe, remplacez le bloc complet et réessayez. Conservez le JavaScript de requête dans le fichier enregistré ; n’ajoutez pas de commandes shell, de commande interactive use ni d’exports de module.
Calculer le prix de l’ensemble correspondant et réutiliser la recherche
Dans cette étape, vous ajouterez le prix admissible le plus bas et utiliserez la même requête enregistrée pour différentes demandes clients, y compris un résultat vide fidèle aux données. Commencez dans Terminal, dans /home/labex/project/variant-desk.
La variante d’origine la moins chère de Ridge Jacket coûte 900 centimes, mais son stock est nul. Une autre variante bon marché n’a pas la bonne couleur. Présenter l’une ou l’autre comme prix de départ pour green/M induirait le client en erreur, même si le produit lui-même était correctement sélectionné.
$set ajoute ou remplace un champ de sortie dans une étape d’agrégation ; il ne met pas à jour le produit enregistré. Ici, il ajoute min_price_cents. L’expression $min avec un tableau comme unique opérande renvoie sa plus petite valeur numérique. "$variants.price_cents" accède aux prix dans le tableau de variantes du document courant. Placez cette étape après le filtrage : elle doit lire le tableau correspondant produit par $project, pas le tableau d’origine. Une étape ultérieure rend aussi ce champ modifié disponible sans ambiguïté.
Remplacez finder.js par le pipeline complet ci-dessous. La seule nouvelle étape calcule le minimum après l’exclusion des tableaux vides :
cat > finder.js <<'JS'
function buildPipeline(request) {
return [
{ $project: {
_id: 0, product_id: 1, title: 1,
variants: { $filter: {
input: { $ifNull: ["$variants", []] },
as: "v",
cond: { $and: [
{ $eq: ["$$v.color", request.color] },
{ $eq: ["$$v.size", request.size] },
{ $gt: ["$$v.stock", 0] },
{ $lte: ["$$v.price_cents", request.max_cents] }
] }
} }
} },
{ $match: { "variants.0": { $exists: true } } },
{ $set: { min_price_cents: { $min: "$variants.price_cents" } } },
{ $sort: { product_id: 1 } }
];
}
JS
./finder green M 5000
Le résultat doit conserver les quatre SKU admissibles et ajouter le minimum de CAMP-3, 4000, et celui de RIDGE-2, 4500. min_price_cents est dérivé de l’ensemble de variantes renvoyé pour chaque produit. Ne dédupliquez pas les prix égaux, n’utilisez pas un minimum sur tout le produit et n’écrivez pas ces deux nombres à la main.
Réduisez maintenant le budget avec la même fonction. Les valeurs de la demande changent ; la requête enregistrée ne change pas :
./finder green M 4499
Seul CAMP-3 reste, toujours avec ses deux SKU à 4000 centimes et un minimum de 4000. Ridge Jacket n’a aucune variante green/M admissible sous ce nouveau plafond.
Changez la couleur et le plafond :
./finder blue M 2500
RIDGE-2 ne contient maintenant que R-BM à 1000 ; TRAIL-1 ne contient que T-BM à 2200. Un produit correctement exclu pour une demande peut être inclus pour une autre. La disponibilité relève d’une relation précise entre variante et demande, pas d’un indicateur permanent du produit.
Enfin, essayez un budget sans correspondance :
./finder green M 500
Le véritable résultat est []. Le client ne remplace pas la réponse par une démonstration et ne montre pas d’articles indisponibles pour remplir l’écran. Une requête manquante ou une erreur de base de données est signalée comme une erreur, pas déguisée en ce tableau vide réussi.
Le résultat de votre pipeline final contient exactement product_id, title, variants et min_price_cents. Renvoyez chaque produit correspondant une fois et chaque SKU admissible une fois, avec les cinq champs de variante d’origine. L’ordre des produits/variantes est libre ; les identités et les valeurs constituent le contrat métier. La requête doit aussi traiter un autre catalogue de même schéma avec des identifiants, des prix et des stocks différents. Les clients préparés lisent les véritables résultats MongoDB et ne corrigent ni la sélection, ni les tableaux, ni les prix.
Conservez les deux fichiers JavaScript enregistrés et tous les produits/outils d’origine. Vous avez construit une requête de catalogue réutilisable, pas un système de réservation : des modifications concurrentes du stock ou un passage en caisse ultérieur pourraient encore affecter la disponibilité. Le challenge suivant possède son propre inventaire de location indépendant et utilise les mêmes mécanismes de requête avec des noms d’attributs et des valeurs de demande différents.
Résumé
Vous avez rejeté les correspondances combinant plusieurs variantes, conservé chaque SKU admissible au lieu du seul premier et calculé les prix affichés à partir des tableaux filtrés. Vos fonctions enregistrées traitent les demandes modifiées, les prix égaux et les résultats vides tout en préservant le catalogue d’origine.



