Séance 2 sur 37 livrablesQuiz corrigé
BrickFinder Le catalogue & le cerveau
Ton back renvoie un inventaire propre. Aujourd'hui on lui donne une mémoire (MongoDB), un catalogue de sets (API Rebrickable), et un cerveau : le moteur qui dit « avec tes pièces, tu peux construire ça ».
Par Josselyn C., prof de BTS SIO. Publié le , mis à jour le .
- On construit
- une base MongoDB + un catalogue rempli par l'API Rebrickable + le moteur de faisabilité
- On installe
- MongoDB (Atlas gratuit ou Docker), Mongoose — pas à pas
- Pré-requis
- le back du Milestone 1 (qui renvoie l'inventaire). On repart de là.
Milestone 2 sur 3. À la fin : tu envoies un inventaire, ton serveur répond « sets constructibles + pièces manquantes ».
Où on en est — et où on va
Au Milestone 1, ton back sait transformer une photo en inventaire JSON vérifié :
POST /api/inventaire (une photo)
-> [ { "type":"brique2x4", "couleur":"rouge", "quantite":5 }, ... ]
Aujourd'hui, on répond à la vraie question : « avec ces pièces, qu'est-ce que je peux construire ? ». Pour ça, il faut deux choses nouvelles :
| Brique | Rôle | Outil |
|---|---|---|
| Un catalogue | la liste des pièces de vrais sets Lego (la « recette » de chacun) | API Rebrickable + MongoDB |
| Un moteur | comparer ton inventaire à chaque set → faisable à X %, pièces manquantes | ton code (l'algo) |
1. C'est quoi une base de données ?
Jusqu'ici, si tu arrêtes ton serveur, tout est oublié. Une base de données (BDD) garde les informations, même éteint. On va y stocker notre catalogue de sets.
| Monde SQL | Monde MongoDB |
|---|---|
| une table | une collection (ex. modeles) |
| une ligne | un document (ex. un set) |
| une colonne | un champ (ex. nom, pieces) |
Démarrer une base (le plus simple)
Deux options — choisis-en une :
Mets ta chaîne dans le .env (à côté de ta clé Claude) :
MONGO_URI=mongodb+srv://user:motdepasse@cluster0.xxxx.mongodb.net/brickfinder
Flag A1
Checkpoint
Rends : quelle option tu as choisie (Atlas ou Docker), et la forme de ta chaîne de connexion sans le mot de passe (remplace-le par ***).
2. Mongoose : parler à la base
Mongoose est la librairie qui fait le pont entre ton code et MongoDB. Elle permet de définir la forme de tes documents (un schéma) et de les lire/écrire facilement.
Installer et se connecter
npm install mongoose
Crée un fichier db.js :
import mongoose from "mongoose"
export async function connecterBase() {
await mongoose.connect(process.env.MONGO_URI)
console.log("Base connectée")
}
Dans index.js, appelle-la au démarrage (tout en haut, après les imports) :
import { connecterBase } from "./db.js"
await connecterBase()
Base connectée
Serveur prêt sur http://localhost:3000
Le schéma d'un modèle (un set)
Crée models/Modele.js. Un set a un numéro, un nom, et une liste de pièces (sa « recette ») :
import mongoose from "mongoose"
const PieceSchema = new mongoose.Schema({
type: String,
couleur: String,
quantite: Number
}, { _id: false })
const ModeleSchema = new mongoose.Schema({
setNum: { type: String, unique: true },
nom: String,
image: String,
pieces: [PieceSchema]
})
export const Modele = mongoose.model("Modele", ModeleSchema)
Flag B1
Production
Dans index.js, après la connexion, crée un set « test » à la main et enregistre-le :
await Modele.create({ setNum: "TEST-1", nom: "Petit test", pieces: [{ type:"brique2x4", couleur:"rouge", quantite:4 }] })
Relance, puis vérifie dans Atlas (Browse Collections) ou avec une route. Rends : une capture du document créé dans la base. (Puis supprime cette ligne de création.)
3. Le catalogue : l'API Rebrickable
On ne va pas inventer les recettes des sets : elles existent déjà. Rebrickable est un site qui connaît la liste exacte des pièces de chaque set Lego, et il offre une API gratuite.
Obtenir une clé (gratuite)
Crée un compte sur rebrickable.com → Account → Settings → API → copie ta clé. Mets-la dans le .env :
REBRICKABLE_KEY=ta-cle-rebrickable
Le premier appel
L'adresse pour les pièces d'un set : https://rebrickable.com/api/v3/lego/sets/{numéro}/parts/. Essayons avec le set 31147-1 (un Creator) :
const KEY = process.env.REBRICKABLE_KEY
const setNum = "31147-1"
const url = `https://rebrickable.com/api/v3/lego/sets/${setNum}/parts/?key=${KEY}&page_size=1000`
const rep = await fetch(url)
const data = await rep.json()
console.log("Pièces reçues :", data.results.length)
console.log(data.results[0])
Pièces reçues : 182
{
part: { part_num: "3001", name: "Brick 2 x 4" },
color: { name: "Red" },
quantity: 6,
...
}
Flag D1
Investigation
Sur rebrickable.com, trouve le numéro de 2 petits sets que tu pourrais acheter (≈100-250 pièces). Rends : leurs numéros (format xxxxx-1) et leur nombre de pièces.
4. Normaliser & stocker le catalogue
La réponse de Rebrickable n'a pas la même forme que notre inventaire. On va la traduire vers notre format { type, couleur, quantite }, puis l'enregistrer en base.
Traduire une pièce Rebrickable
function normaliser(resultats) {
return resultats.map(r => ({
type: r.part.name,
couleur: r.color.name,
quantite: r.quantity
}))
}
Une fonction qui va chercher ET stocke un set
async function importerSet(setNum) {
const base = "https://rebrickable.com/api/v3/lego"
const key = process.env.REBRICKABLE_KEY
// 1. les infos du set : son NOM et son IMAGE (la photo de la boîte !)
const info = await (await fetch(`${base}/sets/${setNum}/?key=${key}`)).json()
// 2. ses pièces
const data = await (await fetch(`${base}/sets/${setNum}/parts/?key=${key}&page_size=1000`)).json()
const pieces = normaliser(data.results)
await Modele.findOneAndUpdate(
{ setNum },
{ setNum, nom: info.name, image: info.set_img_url, pieces },
{ upsert: true }
)
console.log(`Set ${setNum} (${info.name}) importé : ${pieces.length} types de pièces`)
}
Une route pour remplir le catalogue
app.post("/api/catalogue/:setNum", async (req, res) => {
await importerSet(req.params.setNum)
res.json({ importe: req.params.setNum })
})
Set 31147-1 importé : 182 types de pièces
{"importe":"31147-1"}
Flag B2
Production
Importe 3 sets (les tiens du Flag D1, ou 31147-1, 31135-1, 30669-1). Vérifie dans la base que tu as bien 3 documents dans modeles. Rends : les 3 numéros + le nombre de types de pièces de chacun.
5. Le moteur de faisabilité
Le cœur de BrickFinder. On a l'inventaire (ce que tu as) et le catalogue (ce qu'il faut pour chaque set). Question : pour chaque set, peux-tu le construire, et à combien de % ? On construit l'algo étape par étape.
Étape 1 — retrouver vite « combien j'ai de telle pièce »
// une clé "propre" : on compare malgré la casse et les espaces
function cle(type, couleur) {
return `${type}|${couleur}`.toLowerCase().replace(/\s+/g, "")
}
function indexer(inventaire) {
const index = {}
for (const p of inventaire) {
index[cle(p.type, p.couleur)] = p.quantite
}
return index
}
Étape 2 — évaluer UN set
function evaluer(modele, index) {
let ratioMin = Infinity
const manquantes = []
for (const piece of modele.pieces) {
const dispo = index[cle(piece.type, piece.couleur)] || 0
const ratio = dispo / piece.quantite
if (ratio < ratioMin) ratioMin = ratio
if (dispo < piece.quantite) {
manquantes.push({ ...piece, manque: piece.quantite - dispo })
}
}
return {
nom: modele.nom,
setNum: modele.setNum,
image: modele.image,
pourcentage: Math.round(Math.min(ratioMin, 1) * 100),
constructible: manquantes.length === 0,
manquantes
}
}
Étape 3 — évaluer TOUT le catalogue et classer
async function faisabilite(inventaire) {
const index = indexer(inventaire)
const modeles = await Modele.find()
return modeles
.map(m => evaluer(m, index))
.sort((a, b) => b.pourcentage - a.pourcentage)
}
Flag B3
Production (le gros morceau)
Teste ta fonction avec un inventaire écrit à la main qui contient une partie des pièces d'un set importé. Rends : le résultat (nom, pourcentage, pièces manquantes) et vérifie à la main le facteur limitant sur une pièce.
6. Brancher le tout : l'endpoint
On relie le moteur à une vraie route. Idéalement, on part de l'inventaire renvoyé par le Milestone 1. Ici, on fait une route qui reçoit un inventaire et renvoie la faisabilité.
app.post("/api/faisabilite", express.json(), async (req, res) => {
const inventaire = req.body.inventaire
if (!Array.isArray(inventaire)) {
return res.status(400).json({ erreur: "inventaire (tableau) requis" })
}
const resultats = await faisabilite(inventaire)
res.json({ resultats })
})
Teste (Thunder Client, POST, Body → JSON) avec par ex. :
Envoi : { "inventaire": [ {"type":"Brick 2 x 4","couleur":"Red","quantite":3}, ... ] }
Réponse :
{ "resultats": [
{ "nom":"31147-1", "pourcentage":60, "constructible":false,
"manquantes":[ {"type":"Plate 1 x 2","couleur":"Blue","quantite":4,"manque":2} ] },
...
] }
Flag C1
Debug / robustesse
Envoie une requête sans champ inventaire, puis avec un inventaire vide []. Rends : ce que répond le serveur dans chaque cas, et vérifie qu'il ne plante pas (il renvoie 400 ou une liste à 0 %).
7. La liste de courses
Dernière valeur ajoutée, très concrète : « qu'est-ce qu'il me manque en tout pour débloquer plus de sets ? ». On agrège les pièces manquantes.
function listeDeCourses(resultats) {
const total = {}
for (const r of resultats) {
for (const m of r.manquantes) {
const cle = `${m.type}|${m.couleur}`
total[cle] = (total[cle] || 0) + m.manque
}
}
return Object.entries(total).map(([cle, manque]) => {
const [type, couleur] = cle.split("|")
return { type, couleur, manque }
})
}
Flag E1
Extension
Ajoute un endpoint POST /api/liste-courses qui renvoie cette liste à partir d'un inventaire. Rends : la liste obtenue pour ton inventaire de test, triée par manque décroissant (indice : .sort((a,b)=>b.manque-a.manque)).
Bravo — ton back a un cerveau
| Brique | Ce que tu sais faire |
|---|---|
| MongoDB / Mongoose | connecter une base, définir un schéma, créer/lire des documents |
| API Rebrickable | interroger une API externe, normaliser sa réponse vers ton format |
| Catalogue | remplir et mettre à jour une collection sans doublons (upsert) |
| Moteur de faisabilité | comparer inventaire ↔ catalogue : % faisable, facteur limitant, manquantes |
| Liste de courses | agréger les manques sur plusieurs sets |
Mini-quiz (vérifie-toi)
Exercice Réussi À revoir
- En MongoDB, qu'est-ce qu'une collection et un document ?
- À quoi sert
upsert: truedansfindOneAndUpdate? - Dans le moteur, qu'est-ce que le facteur limitant ?
- Pourquoi transforme-t-on l'inventaire en dictionnaire (
indexer) ? - Un set a besoin de 10 briques rouges, tu en as 4. Quel ratio pour cette pièce ? Combien en manque-t-il ?
Voir le corrigéMasquer le corrigé de l'exercice
1. Une collection = un groupe de documents (≈ une table) ; un document = un enregistrement façon JSON (≈ une ligne).
2. À créer le document s'il n'existe pas, sinon le mettre à jour — donc pas de doublon en relançant l'import.
3. La pièce avec le plus petit rapport (dispo ÷ requis) : c'est elle qui limite le nombre de sets constructibles.
4. Pour retrouver une quantité instantanément par type|couleur, au lieu de reparcourir toute la liste à chaque pièce.
5. Ratio = 4 ÷ 10 = 0,4 (40 %) ; il en manque 6.
Tu avais trouvé ?
Glossaire
| Mot | Définition |
|---|---|
| Base de données | un stockage persistant (garde les données même serveur éteint) |
| MongoDB / NoSQL | BDD qui range des documents façon JSON (pas de tables) |
| Collection / document | groupe de documents / un enregistrement |
| Mongoose | librairie Node pour parler à MongoDB (schémas, requêtes) |
| Schéma | la forme attendue d'un document (champs + types) |
| upsert | update-or-insert : met à jour si existe, crée sinon |
| API externe | un service d'un autre serveur interrogé par fetch (ex. Rebrickable) |
| Normaliser | traduire des données vers son propre format |
| Facteur limitant | la pièce au plus petit rapport dispo÷requis |