import type { AllergenKind, IngredientCategory, IngredientIcon, IngredientSubcategory, PrismaClient, } from "@prisma/client"; import { getEnglishKey } from "./catalog-en-keys.js"; // Short, optional-to-pick regime list — `UserProfile.dietId` stays // nullable, this is not meant to be exhaustive. Exported for // `apps/api/scripts/generate-catalog-i18n.ts` (one-off, regenerates // `apps/web`'s `locales/fr/translation.json` `catalog.diets` section from // this same list). export const DIETS = ["Omnivore", "Végétarien", "Végan", "Pescétarien", "Sans gluten"]; // The 14 allergens EU Regulation 1169/2011 (Annex II) requires food // businesses to declare — a standard, defensible reference list rather than // an invented one. Split into ALLERGY (classic IgE-mediated immune // reaction) vs INTOLERANCE (non-immune — gluten sensitivity, sulfite // sensitivity) per the product decision discussed in chat: only Gluten and // Sulfites are commonly-recognized intolerances among the 14; the rest are // true allergens. export const ALLERGENS: Array<{ name: string; kind: AllergenKind }> = [ { name: "Gluten", kind: "INTOLERANCE" }, { name: "Crustacés", kind: "ALLERGY" }, { name: "Œufs", kind: "ALLERGY" }, { name: "Poissons", kind: "ALLERGY" }, { name: "Arachides", kind: "ALLERGY" }, { name: "Soja", kind: "ALLERGY" }, { name: "Lait", kind: "ALLERGY" }, { name: "Fruits à coque", kind: "ALLERGY" }, { name: "Céleri", kind: "ALLERGY" }, { name: "Moutarde", kind: "ALLERGY" }, { name: "Graines de sésame", kind: "ALLERGY" }, { name: "Sulfites", kind: "INTOLERANCE" }, { name: "Lupin", kind: "ALLERGY" }, { name: "Mollusques", kind: "ALLERGY" }, ]; interface IngredientSeed { name: string; /** * Generic pictogram type, overriding its group's `defaultIcon` below — * only needed for the exceptions within a subcategory (a wedge of cheese * inside an otherwise-milk "produits laitiers" group, a stockpot inside * an otherwise-flour "bases" group…). See `IngredientIcon` in * schema.prisma and `apps/web`'s `features/recipes/ingredient-icons.tsx` * for the actual pictograms — this was a free-text emoji field until the * product decision recorded in chat replaced it with this small, shared * vocabulary. */ icon?: IngredientIcon; allergenNames: string[]; /** * Diet regimes this ingredient is compatible with, overriding its group's * `defaultDiets` below — only needed for the exceptions within a * subcategory (a fish-based stock inside an otherwise-vegan "bases" * group, a butter-based dough inside an otherwise-vegan "pâtes à * cuire"…). References `DIETS` by name, same as `allergenNames` * references `ALLERGENS`. Deliberately never includes `"Omnivore"` * (trivial, every ingredient qualifies) or `"Sans gluten"` (derived from * `allergenNames` instead — see `IngredientDiet` in schema.prisma for * why). */ dietNames?: string[]; } // A broad pantry list — the goal is to cover the large majority of what a // home cook reaches for (viandes, poissons, légumes, fruits, féculents, // condiments, épices...), not just enough to exercise the recipe catalog in // tests. Ingredients are reference data (see `Ingredient` in schema.prisma: // `name` is `@unique`, there's no create/update/delete endpoint), so this is // meant to already be comprehensive at first deploy rather than grown // piecemeal as recipes need more of it. `allergenNames` reference // `ALLERGENS` above by name — every one of the 14 EU-regulated allergens is // covered by at least one ingredient here. // // Grouped by (`category`, `subcategory`) — mirrors `IngredientCategory`/ // `IngredientSubcategory` in schema.prisma, a supermarket-aisle taxonomy // ("rayons") reworked from an earlier, less intuitive scheme that mixed // cuisine-of-origin buckets ("cuisine italienne") in with aisle-style ones // ("légumes") — an ingredient's category used to depend on which angle you // thought of first. The picker UI (`apps/web`'s `IngredientPicker`) uses // this to offer category browsing, not just free-text search — with 400+ // ingredients, search alone doesn't scale to actually *finding* something, // and a single flat list of 7 aisles alone wouldn't either (some aisles // would be 100+ items deep). Each group's key is the single source of // truth for that mapping; `INGREDIENTS` below just flattens it back to one // array for the seeding loop. // // `defaultDiets`/`defaultIcon` are what every item in the group shares // unless it sets its own `dietNames`/`icon` — most groups are homogeneous // on both counts (a vegetable is always vegan and always looks like a // vegetable; a cut of meat never is and never does), so this avoids // repeating the same values on hundreds of items; only a group's // exceptions (a fish-based stock inside "bases", a cheese inside "produits // laitiers"…) need a per-item override. export const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; subcategory: IngredientSubcategory; defaultDiets: string[]; defaultIcon: IngredientIcon; items: IngredientSeed[]; }> = [ // ========================================================================= // Produits frais // ========================================================================= { category: "PRODUITS_FRAIS", subcategory: "LEGUMES", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "VEGETABLE", items: [ { name: "Tomate", allergenNames: [] }, { name: "Oignon", allergenNames: [] }, { name: "Échalote", allergenNames: [] }, { name: "Ail", allergenNames: [] }, { name: "Carotte", allergenNames: [] }, { name: "Courgette", allergenNames: [] }, { name: "Concombre", allergenNames: [] }, { name: "Cornichons", allergenNames: [] }, { name: "Poivron", allergenNames: [] }, { name: "Champignon", allergenNames: [] }, { name: "Cèpes", allergenNames: [] }, { name: "Aubergine", allergenNames: [] }, { name: "Brocoli", allergenNames: [] }, { name: "Chou-fleur", allergenNames: [] }, { name: "Chou blanc", allergenNames: [] }, { name: "Chou rouge", allergenNames: [] }, { name: "Chou de Bruxelles", allergenNames: [] }, { name: "Épinard", allergenNames: [] }, { name: "Blette", allergenNames: [] }, { name: "Salade", allergenNames: [] }, { name: "Roquette", allergenNames: [] }, { name: "Cresson", allergenNames: [] }, { name: "Poireau", allergenNames: [] }, { name: "Céleri", allergenNames: ["Céleri"] }, { name: "Radis", allergenNames: [] }, { name: "Betterave", allergenNames: [] }, { name: "Navet", allergenNames: [] }, { name: "Panais", allergenNames: [] }, { name: "Haricot vert", allergenNames: [] }, { name: "Petit pois", allergenNames: [] }, { name: "Maïs", allergenNames: [] }, { name: "Artichaut", allergenNames: [] }, { name: "Fenouil", allergenNames: [] }, { name: "Endive", allergenNames: [] }, { name: "Potiron", allergenNames: [] }, { name: "Butternut", allergenNames: [] }, { name: "Asperge", allergenNames: [] }, { name: "Avocat", allergenNames: [] }, { name: "Pomme de terre", allergenNames: [] }, { name: "Patate douce", allergenNames: [] }, { name: "Tomates cerises", allergenNames: [] }, { name: "Pak-choï", allergenNames: [] }, { name: "Germes de soja", allergenNames: ["Soja"] }, { name: "Shiitake", allergenNames: [] }, { name: "Daikon", allergenNames: [] }, { name: "Piment vert frais", allergenNames: [] }, ], }, { category: "PRODUITS_FRAIS", subcategory: "FRUITS", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "FRUIT", items: [ { name: "Citron", allergenNames: [] }, { name: "Citron vert", allergenNames: [] }, { name: "Pomme", allergenNames: [] }, { name: "Poire", allergenNames: [] }, { name: "Banane", allergenNames: [] }, { name: "Orange", allergenNames: [] }, { name: "Clémentine", allergenNames: [] }, { name: "Pamplemousse", allergenNames: [] }, { name: "Fraise", allergenNames: [] }, { name: "Framboise", allergenNames: [] }, { name: "Myrtille", allergenNames: [] }, { name: "Mûre", allergenNames: [] }, { name: "Cerise", allergenNames: [] }, { name: "Abricot", allergenNames: [] }, { name: "Pêche", allergenNames: [] }, { name: "Prune", allergenNames: [] }, { name: "Raisin", allergenNames: [] }, { name: "Melon", allergenNames: [] }, { name: "Pastèque", allergenNames: [] }, { name: "Ananas", allergenNames: [] }, { name: "Mangue", allergenNames: [] }, { name: "Kiwi", allergenNames: [] }, { name: "Figue", allergenNames: [] }, { name: "Datte", allergenNames: [] }, { name: "Litchi", allergenNames: [] }, { name: "Grenade", allergenNames: [] }, { name: "Rhubarbe", allergenNames: [] }, { name: "Coing", allergenNames: [] }, ], }, { category: "PRODUITS_FRAIS", subcategory: "HERBES_FRAICHES", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "HERB", items: [ { name: "Basilic", allergenNames: [] }, { name: "Persil", allergenNames: [] }, { name: "Thym", allergenNames: [] }, { name: "Romarin", allergenNames: [] }, { name: "Laurier", allergenNames: [] }, { name: "Ciboulette", allergenNames: [] }, { name: "Coriandre fraîche", allergenNames: [] }, { name: "Menthe", allergenNames: [] }, { name: "Origan", allergenNames: [] }, { name: "Aneth", allergenNames: [] }, { name: "Estragon", allergenNames: [] }, { name: "Sarriette", allergenNames: [] }, { name: "Marjolaine", allergenNames: [] }, { name: "Sauge", allergenNames: [] }, { name: "Cerfeuil", allergenNames: [] }, { name: "Gingembre", allergenNames: [] }, { name: "Citronnelle", allergenNames: [] }, { name: "Combava", allergenNames: [] }, ], }, // ========================================================================= // Boucherie & poissonnerie // ========================================================================= { category: "BOUCHERIE_POISSONNERIE", subcategory: "VIANDES", defaultDiets: [], defaultIcon: "MEAT", items: [ { name: "Lapin", allergenNames: [] }, { name: "Bœuf haché", allergenNames: [] }, { name: "Steak de bœuf", allergenNames: [] }, { name: "Rôti de bœuf", allergenNames: [] }, { name: "Escalope de veau", allergenNames: [] }, { name: "Filet mignon de porc", allergenNames: [] }, { name: "Côte de porc", allergenNames: [] }, { name: "Agneau", allergenNames: [] }, { name: "Gigot d'agneau", allergenNames: [] }, { name: "Lardons", allergenNames: [] }, { name: "Bacon", allergenNames: [] }, { name: "Jambon blanc", allergenNames: [] }, { name: "Jambon cru", allergenNames: [] }, { name: "Saucisse", allergenNames: [] }, { name: "Chorizo", allergenNames: [] }, { name: "Merguez", allergenNames: [] }, { name: "Prosciutto", allergenNames: [] }, { name: "Pancetta", allergenNames: [] }, { name: "Mortadelle", allergenNames: [] }, { name: "Salami", allergenNames: [] }, ], }, { category: "BOUCHERIE_POISSONNERIE", subcategory: "VOLAILLES", defaultDiets: [], defaultIcon: "POULTRY", items: [ { name: "Poulet", allergenNames: [] }, { name: "Dinde", allergenNames: [] }, { name: "Canard", allergenNames: [] }, { name: "Magret de canard", allergenNames: [] }, ], }, { category: "BOUCHERIE_POISSONNERIE", subcategory: "POISSONS", defaultDiets: ["Pescétarien"], defaultIcon: "FISH", items: [ { name: "Saumon", allergenNames: ["Poissons"] }, { name: "Thon", allergenNames: ["Poissons"] }, { name: "Cabillaud", allergenNames: ["Poissons"] }, { name: "Truite", allergenNames: ["Poissons"] }, { name: "Sardine", allergenNames: ["Poissons"] }, { name: "Anchois", allergenNames: ["Poissons"] }, { name: "Merlan", allergenNames: ["Poissons"] }, { name: "Surimi", allergenNames: ["Poissons"] }, { name: "Bar (loup de mer)", allergenNames: ["Poissons"] }, { name: "Dorade", allergenNames: ["Poissons"] }, { name: "Sole", allergenNames: ["Poissons"] }, { name: "Turbot", allergenNames: ["Poissons"] }, { name: "Merlu", allergenNames: ["Poissons"] }, { name: "Colin", allergenNames: ["Poissons"] }, { name: "Lieu noir", allergenNames: ["Poissons"] }, { name: "Églefin", allergenNames: ["Poissons"] }, { name: "Maquereau", allergenNames: ["Poissons"] }, { name: "Hareng", allergenNames: ["Poissons"] }, { name: "Rouget", allergenNames: ["Poissons"] }, { name: "Raie", allergenNames: ["Poissons"] }, { name: "Lotte", allergenNames: ["Poissons"] }, { name: "Flétan", allergenNames: ["Poissons"] }, { name: "Espadon", allergenNames: ["Poissons"] }, { name: "Carpe", allergenNames: ["Poissons"] }, { name: "Brochet", allergenNames: ["Poissons"] }, { name: "Perche", allergenNames: ["Poissons"] }, { name: "Tilapia", allergenNames: ["Poissons"] }, { name: "Panga", allergenNames: ["Poissons"] }, { name: "Saumon fumé", allergenNames: ["Poissons"] }, { name: "Poisson séché", allergenNames: ["Poissons"] }, ], }, { category: "BOUCHERIE_POISSONNERIE", subcategory: "CRUSTACES_FRUITS_DE_MER", defaultDiets: ["Pescétarien"], defaultIcon: "SHELLFISH", items: [ { name: "Crevettes", allergenNames: ["Crustacés"] }, { name: "Langoustines", allergenNames: ["Crustacés"] }, { name: "Homard", allergenNames: ["Crustacés"] }, { name: "Crabe", allergenNames: ["Crustacés"] }, { name: "Langouste", allergenNames: ["Crustacés"] }, { name: "Moules", allergenNames: ["Mollusques"] }, { name: "Huîtres", allergenNames: ["Mollusques"] }, { name: "Saint-Jacques", allergenNames: ["Mollusques"] }, { name: "Calamar", allergenNames: ["Mollusques"] }, { name: "Poulpe", allergenNames: ["Mollusques"] }, { name: "Palourdes", allergenNames: ["Mollusques"] }, { name: "Bulots", allergenNames: ["Mollusques"] }, ], }, // ========================================================================= // Épicerie sèche // ========================================================================= { category: "EPICERIE_SECHE", subcategory: "FECULENTS", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "GRAIN", items: [ { name: "Semoule", allergenNames: ["Gluten"] }, { name: "Couscous", allergenNames: ["Gluten"] }, { name: "Boulgour", allergenNames: ["Gluten"] }, { name: "Polenta", allergenNames: [] }, { name: "Quinoa", allergenNames: [] }, { name: "Pâtes", allergenNames: ["Gluten"] }, { name: "Pâtes complètes", allergenNames: ["Gluten"] }, { name: "Riz", allergenNames: [] }, { name: "Riz basmati", allergenNames: [] }, { name: "Riz complet", allergenNames: [] }, { name: "Flocons d'avoine", allergenNames: ["Gluten"] }, { name: "Spaghetti", allergenNames: ["Gluten"] }, { name: "Penne", allergenNames: ["Gluten"] }, { name: "Tagliatelles", allergenNames: ["Gluten"] }, { name: "Lasagnes (feuilles)", allergenNames: ["Gluten"] }, { name: "Gnocchi", allergenNames: ["Gluten"] }, { name: "Riz arborio", allergenNames: [] }, { name: "Nouilles de riz", allergenNames: [] }, { name: "Nouilles udon", allergenNames: ["Gluten"] }, { name: "Nouilles soba", allergenNames: ["Gluten"] }, { name: "Nouilles chinoises", allergenNames: ["Gluten"] }, { name: "Vermicelles de riz", allergenNames: [] }, { name: "Vermicelles de soja", allergenNames: [] }, { name: "Riz gluant", allergenNames: [] }, { name: "Riz à sushi", allergenNames: [] }, { name: "Riz jasmin", allergenNames: [] }, ], }, { category: "EPICERIE_SECHE", subcategory: "LEGUMINEUSES", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "LEGUME", items: [ { name: "Lentilles vertes", allergenNames: [] }, { name: "Lentilles corail", allergenNames: [] }, { name: "Pois chiches", allergenNames: [] }, { name: "Haricots blancs", allergenNames: [] }, { name: "Haricots rouges", allergenNames: [] }, { name: "Haricots noirs", allergenNames: [] }, { name: "Pois cassés", allergenNames: [] }, { name: "Fèves", allergenNames: [] }, { name: "Edamame", allergenNames: ["Soja"] }, { name: "Haricots pinto", allergenNames: [] }, ], }, { category: "EPICERIE_SECHE", subcategory: "GRAINES_FRUITS_SECS", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "NUT_SEED", items: [ { name: "Cacahuètes", allergenNames: ["Arachides"] }, { name: "Amandes", allergenNames: ["Fruits à coque"] }, { name: "Noix", allergenNames: ["Fruits à coque"] }, { name: "Noisettes", allergenNames: ["Fruits à coque"] }, { name: "Noix de cajou", allergenNames: ["Fruits à coque"] }, { name: "Pistaches", allergenNames: ["Fruits à coque"] }, { name: "Noix de pécan", allergenNames: ["Fruits à coque"] }, { name: "Poudre d'amande", allergenNames: ["Fruits à coque"] }, { name: "Pignons de pin", allergenNames: [] }, { name: "Graines de tournesol", allergenNames: [] }, { name: "Graines de courge", allergenNames: [] }, { name: "Noix de coco râpée", allergenNames: [] }, { name: "Raisins secs", allergenNames: ["Sulfites"] }, { name: "Pruneaux", allergenNames: ["Sulfites"] }, { name: "Abricots secs", allergenNames: ["Sulfites"] }, { name: "Graines de sésame", allergenNames: ["Graines de sésame"] }, ], }, { category: "EPICERIE_SECHE", subcategory: "AUTRES", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "JAR", items: [ { name: "Champignons noirs", allergenNames: [] }, { name: "Algue nori", allergenNames: [] }, { name: "Algue wakamé", allergenNames: [] }, { name: "Algue kombu", allergenNames: [] }, { name: "Pousses de bambou", allergenNames: [] }, { name: "Châtaignes d'eau", allergenNames: [] }, ], }, // ========================================================================= // Boulangerie // ========================================================================= { category: "BOULANGERIE", subcategory: "PAINS", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "BREAD", items: [ { name: "Pain", allergenNames: ["Gluten"] }, { name: "Pain de mie", allergenNames: ["Gluten"] }, { name: "Pain complet", allergenNames: ["Gluten"] }, { name: "Baguette", allergenNames: ["Gluten"] }, { name: "Pain de seigle", allergenNames: ["Gluten"] }, { name: "Chapelure", allergenNames: ["Gluten"] }, { name: "Pain à burger", allergenNames: ["Gluten", "Lait", "Œufs"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Pain brioché", allergenNames: ["Gluten", "Lait", "Œufs"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Pain à hot-dog", allergenNames: ["Gluten"] }, { name: "Pain pita", allergenNames: ["Gluten"] }, { name: "Pain bagel", allergenNames: ["Gluten"] }, { name: "Naan", allergenNames: ["Gluten"] }, { name: "Pain wrap", allergenNames: ["Gluten"] }, { name: "Pain viennois", allergenNames: ["Gluten", "Lait"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Pain de campagne", allergenNames: ["Gluten"] }, { name: "Pain aux céréales", allergenNames: ["Gluten"] }, { name: "Petit pain", allergenNames: ["Gluten"] }, { name: "Pain suédois", allergenNames: ["Gluten"] }, { name: "Pain sans gluten", allergenNames: [] }, { name: "Biscotte", allergenNames: ["Gluten"] }, { name: "Croûtons", allergenNames: ["Gluten"] }, { name: "Focaccia", allergenNames: ["Gluten"] }, { name: "Ciabatta", allergenNames: ["Gluten"] }, { name: "Tortilla de maïs", allergenNames: [] }, { name: "Tortilla de blé", allergenNames: ["Gluten"] }, ], }, { category: "BOULANGERIE", subcategory: "PATES_A_CUIRE", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "DOUGH", items: [ { name: "Pâte feuilletée", allergenNames: ["Gluten", "Lait"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Pâte brisée", allergenNames: ["Gluten", "Lait"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Pâte à pizza", allergenNames: ["Gluten"] }, { name: "Pâte à tarte sablée", allergenNames: ["Gluten", "Lait"], dietNames: ["Végétarien", "Pescétarien"], }, ], }, // ========================================================================= // Crémerie & fromage // ========================================================================= { category: "CREMERIE_FROMAGE", subcategory: "PRODUITS_LAITIERS", defaultDiets: ["Végétarien", "Pescétarien"], defaultIcon: "MILK", items: [ { name: "Lait", allergenNames: ["Lait"] }, { name: "Beurre", allergenNames: ["Lait"] }, { name: "Crème fraîche", allergenNames: ["Lait"] }, { name: "Crème liquide", allergenNames: ["Lait"] }, { name: "Fromage", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Emmental", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Gruyère", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Parmesan", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Mozzarella", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Chèvre (fromage)", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Feta", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Comté", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Fromage blanc", allergenNames: ["Lait"] }, { name: "Mascarpone", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Yaourt", allergenNames: ["Lait"] }, { name: "Burrata", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Ricotta", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Pecorino", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Gorgonzola", icon: "CHEESE", allergenNames: ["Lait"] }, { name: "Cheddar", icon: "CHEESE", allergenNames: ["Lait"] }, ], }, { category: "CREMERIE_FROMAGE", subcategory: "OEUFS", defaultDiets: ["Végétarien", "Pescétarien"], defaultIcon: "EGG", items: [{ name: "Œuf", allergenNames: ["Œufs"] }], }, { category: "CREMERIE_FROMAGE", subcategory: "ALTERNATIVES", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "SPROUT", items: [ { name: "Lait de coco", icon: "MILK", allergenNames: [] }, { name: "Crème de coco", icon: "MILK", allergenNames: [] }, { name: "Lait d'amande", icon: "MILK", allergenNames: ["Fruits à coque"] }, { name: "Lait d'avoine", icon: "MILK", allergenNames: ["Gluten"] }, { name: "Tofu", allergenNames: ["Soja"] }, { name: "Tofu soyeux", allergenNames: ["Soja"] }, ], }, // ========================================================================= // Condiments & épices // ========================================================================= { category: "CONDIMENTS_EPICES", subcategory: "EPICES", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "SPICE", items: [ { name: "Herbes de Provence", allergenNames: [] }, { name: "Poivre noir", allergenNames: [] }, { name: "Paprika", allergenNames: [] }, { name: "Piment d'Espelette", allergenNames: [] }, { name: "Piment de Cayenne", allergenNames: [] }, { name: "Cumin", allergenNames: [] }, { name: "Curry (poudre)", allergenNames: [] }, { name: "Curcuma", allergenNames: [] }, { name: "Cannelle", allergenNames: [] }, { name: "Muscade", allergenNames: [] }, { name: "Safran", allergenNames: [] }, { name: "Clou de girofle", allergenNames: [] }, { name: "Vanille (gousse)", allergenNames: [] }, { name: "Poivre blanc", allergenNames: [] }, { name: "Poivre rose", allergenNames: [] }, { name: "Poivre du Sichuan", allergenNames: [] }, { name: "Paprika fumé", allergenNames: [] }, { name: "Piment oiseau", allergenNames: [] }, { name: "Baies de genièvre", allergenNames: [] }, { name: "Anis étoilé (badiane)", allergenNames: [] }, { name: "Anis vert", allergenNames: [] }, { name: "Graines de fenouil", allergenNames: [] }, { name: "Sumac", allergenNames: [] }, { name: "Nigelle", allergenNames: [] }, { name: "Quatre épices", allergenNames: [] }, { name: "Colombo (poudre)", allergenNames: [] }, { name: "Baharat", allergenNames: [] }, { name: "Raifort", allergenNames: [] }, { name: "Sel aux herbes", allergenNames: [] }, { name: "Sel de céleri", allergenNames: ["Céleri"] }, { name: "Fleur de sel", allergenNames: [] }, { name: "Sel", allergenNames: [] }, { name: "Cinq épices", allergenNames: [] }, { name: "Garam masala", allergenNames: [] }, { name: "Graines de coriandre", allergenNames: [] }, { name: "Cardamome", allergenNames: [] }, { name: "Fenugrec", allergenNames: [] }, { name: "Piment jalapeño", allergenNames: [] }, { name: "Piment chipotle", allergenNames: [] }, { name: "Piment poblano", allergenNames: [] }, { name: "Piment habanero", allergenNames: [] }, { name: "Ras el hanout", allergenNames: [] }, { name: "Za'atar", allergenNames: ["Graines de sésame"] }, ], }, { category: "CONDIMENTS_EPICES", subcategory: "SAUCES", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "JAR", items: [ { name: "Sauce soja", allergenNames: ["Soja"] }, { name: "Moutarde", allergenNames: ["Moutarde"] }, { name: "Mayonnaise", allergenNames: ["Œufs"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Ketchup", allergenNames: [] }, { name: "Tabasco", allergenNames: [] }, { name: "Sauce Worcestershire", allergenNames: ["Poissons"], dietNames: ["Pescétarien"], }, { name: "Sauce nuoc-mâm", allergenNames: ["Poissons"], dietNames: ["Pescétarien"], }, { name: "Wasabi", allergenNames: [] }, { name: "Harissa", allergenNames: [] }, { name: "Pâte de curry", allergenNames: [] }, { name: "Beurre de cacahuète", allergenNames: ["Arachides"] }, { name: "Moutarde de Dijon", allergenNames: ["Moutarde"] }, { name: "Moutarde à l'ancienne", allergenNames: ["Moutarde"] }, { name: "Sauce barbecue", allergenNames: [] }, { name: "Sauce tartare", allergenNames: ["Œufs"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Sauce cocktail", allergenNames: ["Œufs"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Sauce béarnaise", allergenNames: ["Œufs", "Lait"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Sauce hollandaise", allergenNames: ["Œufs", "Lait"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Sauce béchamel", allergenNames: ["Lait", "Gluten"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Sauce teriyaki", allergenNames: ["Soja"] }, { name: "Sauce ponzu", allergenNames: ["Soja", "Poissons"], dietNames: ["Pescétarien"], }, { name: "Chimichurri", allergenNames: [] }, { name: "Pesto rouge (tomates séchées)", allergenNames: ["Lait", "Fruits à coque"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Pesto", allergenNames: ["Lait", "Fruits à coque"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Sauce huître", allergenNames: ["Mollusques"], dietNames: ["Pescétarien"], }, { name: "Sauce hoisin", allergenNames: ["Soja"] }, { name: "Sauce sriracha", allergenNames: [] }, { name: "Sauce sweet chili", allergenNames: [] }, { name: "Miso", allergenNames: ["Soja"] }, { name: "Pâte de crevettes", allergenNames: ["Crustacés"], dietNames: ["Pescétarien"], }, { name: "Pâte de curry rouge (thaï)", allergenNames: [] }, { name: "Pâte de curry vert (thaï)", allergenNames: [] }, { name: "Tahini", allergenNames: ["Graines de sésame"] }, ], }, { category: "CONDIMENTS_EPICES", subcategory: "ASSAISONNEMENTS", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "BOTTLE", items: [ { name: "Huile d'olive", allergenNames: [] }, { name: "Huile de tournesol", allergenNames: [] }, { name: "Huile de colza", allergenNames: [] }, { name: "Huile de coco", allergenNames: [] }, { name: "Huile de sésame", allergenNames: ["Graines de sésame"] }, { name: "Vinaigre de cidre", allergenNames: [] }, { name: "Vinaigre blanc", allergenNames: [] }, { name: "Vinaigre balsamique", allergenNames: ["Sulfites"] }, { name: "Câpres", icon: "JAR", allergenNames: [] }, { name: "Olives", icon: "JAR", allergenNames: [] }, { name: "Vin blanc (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] }, { name: "Vin rouge (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] }, { name: "Vinaigre de vin rouge", allergenNames: ["Sulfites"] }, { name: "Vinaigre de vin blanc", allergenNames: ["Sulfites"] }, { name: "Vinaigre de xérès", allergenNames: ["Sulfites"] }, { name: "Huile de noix", allergenNames: ["Fruits à coque"] }, { name: "Huile de noisette", allergenNames: ["Fruits à coque"] }, { name: "Huile d'arachide", allergenNames: ["Arachides"] }, { name: "Huile pimentée", allergenNames: [] }, { name: "Vinaigre de riz", allergenNames: [] }, { name: "Mirin", icon: "DRINK", allergenNames: [] }, { name: "Saké (cuisine)", icon: "DRINK", allergenNames: [] }, { name: "Jus de citron", icon: "DRINK", allergenNames: [] }, { name: "Jus de citron vert", icon: "DRINK", allergenNames: [] }, { name: "Jus d'orange", icon: "DRINK", allergenNames: [] }, { name: "Jus de pomme", icon: "DRINK", allergenNames: [] }, { name: "Jus de raisin", icon: "DRINK", allergenNames: [] }, { name: "Jus de tomate", icon: "DRINK", allergenNames: [] }, { name: "Jus de cranberry", icon: "DRINK", allergenNames: [] }, { name: "Café", icon: "DRINK", allergenNames: [] }, { name: "Thé", icon: "DRINK", allergenNames: [] }, { name: "Bière (cuisine)", icon: "DRINK", allergenNames: ["Gluten"] }, { name: "Cidre (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] }, { name: "Champagne / vin pétillant (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"], }, { name: "Porto (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] }, { name: "Vin jaune (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] }, { name: "Cognac", icon: "DRINK", allergenNames: [] }, { name: "Rhum", icon: "DRINK", allergenNames: [] }, { name: "Whisky", icon: "DRINK", allergenNames: [] }, { name: "Vodka", icon: "DRINK", allergenNames: [] }, ], }, // ========================================================================= // Aides culinaires // ========================================================================= { category: "AIDES_CULINAIRES", subcategory: "BASES", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "GRAIN", items: [ { name: "Farine de blé", allergenNames: ["Gluten"] }, { name: "Farine complète", allergenNames: ["Gluten"] }, { name: "Farine de maïs", allergenNames: [] }, { name: "Farine de sarrasin", allergenNames: [] }, { name: "Farine de riz", allergenNames: [] }, { name: "Bouillon cube légumes", icon: "STOCK_POT", allergenNames: ["Céleri"] }, { name: "Bouillon cube volaille", icon: "STOCK_POT", allergenNames: ["Céleri"], dietNames: [], }, { name: "Concentré de tomate", icon: "JAR", allergenNames: [] }, { name: "Coulis de tomate", icon: "JAR", allergenNames: [] }, { name: "Tomates pelées (conserve)", icon: "JAR", allergenNames: [] }, { name: "Tomates séchées", icon: "JAR", allergenNames: [] }, { name: "Fond de veau", icon: "STOCK_POT", allergenNames: [], dietNames: [] }, { name: "Fond de volaille", icon: "STOCK_POT", allergenNames: [], dietNames: [] }, { name: "Bouillon cube bœuf", icon: "STOCK_POT", allergenNames: ["Céleri"], dietNames: [], }, { name: "Bouillon cube poisson", icon: "STOCK_POT", allergenNames: ["Poissons", "Céleri"], dietNames: ["Pescétarien"], }, { name: "Bouillon de légumes", icon: "STOCK_POT", allergenNames: ["Céleri"] }, { name: "Bouillon de volaille", icon: "STOCK_POT", allergenNames: ["Céleri"], dietNames: [], }, { name: "Bouillon de bœuf", icon: "STOCK_POT", allergenNames: ["Céleri"], dietNames: [] }, { name: "Court-bouillon", icon: "STOCK_POT", allergenNames: [] }, { name: "Dashi (bouillon japonais)", icon: "STOCK_POT", allergenNames: ["Poissons"], dietNames: ["Pescétarien"], }, { name: "Bisque de crustacés", icon: "STOCK_POT", allergenNames: ["Crustacés"], dietNames: ["Pescétarien"], }, { name: "Farine de tapioca", allergenNames: [] }, { name: "Masa harina", allergenNames: [] }, { name: "Eau", icon: "DRINK", allergenNames: [] }, { name: "Eau gazeuse", icon: "DRINK", allergenNames: [] }, { name: "Eau de fleur d'oranger", icon: "DRINK", allergenNames: [] }, { name: "Eau de rose", icon: "DRINK", allergenNames: [] }, { name: "Fumet de poisson", icon: "STOCK_POT", allergenNames: ["Poissons"], dietNames: ["Pescétarien"], }, ], }, { category: "AIDES_CULINAIRES", subcategory: "EPAISSISSANTS", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "JAR", items: [ { name: "Levure boulangère", allergenNames: [] }, { name: "Levure chimique", allergenNames: [] }, { name: "Maïzena", allergenNames: [] }, { name: "Farine de lupin", allergenNames: ["Lupin"] }, // Animal collagen (bones/skin, usually pork or beef) — not // vegetarian/vegan, and not reliably fish-derived either, so no // pescetarian flag. { name: "Gélatine", allergenNames: [], dietNames: [] }, { name: "Bicarbonate de soude", allergenNames: [] }, { name: "Fécule de pomme de terre", allergenNames: [] }, ], }, { category: "AIDES_CULINAIRES", subcategory: "SUCRES", defaultDiets: ["Végétarien", "Végan", "Pescétarien"], defaultIcon: "SUGAR", items: [ { name: "Sucre", allergenNames: [] }, { name: "Miel", allergenNames: [], dietNames: ["Végétarien", "Pescétarien"] }, { name: "Sirop d'érable", allergenNames: [] }, { name: "Sucre roux", allergenNames: [] }, { name: "Sucre glace", allergenNames: [] }, { name: "Cassonade", allergenNames: [] }, { name: "Chocolat noir", allergenNames: [] }, { name: "Chocolat au lait", allergenNames: ["Lait"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Chocolat blanc", allergenNames: ["Lait"], dietNames: ["Végétarien", "Pescétarien"], }, { name: "Pépites de chocolat", allergenNames: [] }, { name: "Cacao en poudre", allergenNames: [] }, { name: "Extrait de vanille", allergenNames: [] }, { name: "Sucre de palme", allergenNames: [] }, { name: "Sirop de sucre de canne", allergenNames: [] }, ], }, ]; const INGREDIENTS: Array< Omit & { category: IngredientCategory; subcategory: IngredientSubcategory; icon: IngredientIcon; dietNames: string[]; } > = INGREDIENT_GROUPS.flatMap(({ category, subcategory, defaultDiets, defaultIcon, items }) => items.map((item) => ({ ...item, category, subcategory, icon: item.icon ?? defaultIcon, dietNames: item.dietNames ?? defaultDiets, })), ); /** * Populates the `Diet`/`Category`/`Allergy` reference tables. Idempotent * (safe to call against a database that already has this data — upserts by * `key`, all `@unique`) — used both by `prisma/seed.ts` (the CLI entry * point, `prisma db seed`) and by `test-support/reset-db.ts` (so every * test starts from the same realistic reference data the real app seeds, * not an empty table). * * Every `name` below (`DIETS`, `ALLERGENS`, `INGREDIENT_GROUPS`) is an * *authoring* label, never written to the database or seen by a client — * {@link getEnglishKey} resolves each row's real, stable, English `key` * from it (see `catalog-en-keys.ts` for why the key must be English even * though this file's labels are French). The database only ever stores * that key; the French label itself lives in `apps/web`'s * `locales/fr/translation.json` (`catalog.*` * namespace, kept in sync by `scripts/generate-catalog-i18n.ts`). */ export async function seedReferenceData(prisma: PrismaClient): Promise { for (const name of DIETS) { const key = getEnglishKey(name); await prisma.diet.upsert({ where: { key }, update: {}, create: { key } }); } // `Allergy` itself carries no `key` — it's the selectable instance of a // keyed `Category` (see schema.prisma) — so seeding an allergen means one // Category (upserted by key) plus exactly one Allergy row under it, // created only the first time. `update: { kind }` (not `{}`) — a reseed // must correct `kind` on an already-existing category if the // classification above ever changes, not just skip it. for (const { name, kind } of ALLERGENS) { const key = getEnglishKey(name); const category = await prisma.category.upsert({ where: { key }, update: { kind }, create: { key, kind }, }); const existing = await prisma.allergy.findFirst({ where: { categoryId: category.id } }); if (!existing) { await prisma.allergy.create({ data: { categoryId: category.id } }); } } // Ingredients: bulk, not one upsert per row (`INGREDIENTS` is a few // hundred entries long, and `seedReferenceData` re-runs on every single // test's `resetDatabase()` — a per-row round trip made the whole suite // measurably slower). Bulk-create whatever's missing in one query, then // reconcile `icon`/`category`/`subcategory` only for the rows where any // of them actually changed — on a freshly-truncated table (the common // test-suite case) that's zero updates, on a real re-deploy it's however // many rows were edited in code since the last deploy, never the full // list. const ingredientKeys = INGREDIENTS.map((i) => getEnglishKey(i.name)); const existingIngredients = await prisma.ingredient.findMany({ where: { key: { in: ingredientKeys } }, select: { id: true, key: true, icon: true, category: true, subcategory: true }, }); const existingByKey = new Map(existingIngredients.map((i) => [i.key, i])); const missingIngredients = INGREDIENTS.filter((i) => !existingByKey.has(getEnglishKey(i.name))); if (missingIngredients.length > 0) { await prisma.ingredient.createMany({ data: missingIngredients.map(({ name, icon, category, subcategory }) => ({ key: getEnglishKey(name), icon, category, subcategory, })), }); } const changed = INGREDIENTS.filter((i) => { const existing = existingByKey.get(getEnglishKey(i.name)); return ( existing && (existing.icon !== i.icon || existing.category !== i.category || existing.subcategory !== i.subcategory) ); }); for (const { name, icon, category, subcategory } of changed) { await prisma.ingredient.update({ where: { key: getEnglishKey(name) }, data: { icon, category, subcategory }, }); } // Re-resolve every ingredient's id (existing + just-created) and every // allergy's id (by its category key) once, then link them in a single // bulk insert — same "re-derived every time, not upserted per link" // reasoning as before for `IngredientAllergy` (it has no natural per-row // identity to upsert against), just batched instead of looped. const allIngredients = await prisma.ingredient.findMany({ where: { key: { in: ingredientKeys } }, select: { id: true, key: true }, }); const ingredientIdByKey = new Map(allIngredients.map((i) => [i.key, i.id])); const allergies = await prisma.allergy.findMany({ include: { category: true } }); const allergyIdByCategoryKey = new Map(allergies.map((a) => [a.category.key, a.id])); const links: Array<{ ingredientId: number; allergyId: number }> = []; for (const { name, allergenNames } of INGREDIENTS) { const ingredientId = ingredientIdByKey.get(getEnglishKey(name)); if (ingredientId === undefined) continue; for (const allergenName of allergenNames) { const allergyId = allergyIdByCategoryKey.get(getEnglishKey(allergenName)); if (allergyId !== undefined) links.push({ ingredientId, allergyId }); } } if (links.length > 0) { await prisma.ingredientAllergy.createMany({ data: links, skipDuplicates: true }); } // Same bulk-insert approach as the allergy links above, resolved against // `dietNames` (item override, falling back to its group's `defaultDiets` // in the `INGREDIENTS` flatten step) instead of `allergenNames`. const diets = await prisma.diet.findMany(); const dietIdByKey = new Map(diets.map((d) => [d.key, d.id])); const dietLinks: Array<{ ingredientId: number; dietId: number }> = []; for (const { name, dietNames } of INGREDIENTS) { const ingredientId = ingredientIdByKey.get(getEnglishKey(name)); if (ingredientId === undefined) continue; for (const dietName of dietNames) { const dietId = dietIdByKey.get(getEnglishKey(dietName)); if (dietId !== undefined) dietLinks.push({ ingredientId, dietId }); } } if (dietLinks.length > 0) { await prisma.ingredientDiet.createMany({ data: dietLinks, skipDuplicates: true }); } }