batchCooking/apps/api/src/db/reference-seed-data.ts
kyuno053 c34eaa89d0
chore(web): session de polish global — version, checkbox, danger zone, icônes (#20)
* chore(web): session de polish global — version, checkbox, danger zone, icônes

- Affiche le numéro de version (package.json, injecté via Vite) en bas de
  la sidebar, masqué en mode collapse et en mobile.
- Factorise les checkbox/radio dupliqués (AllergySelect, DietTagSelect,
  IngredientPicker, UserPreferencesPage) en composants partagés
  CheckboxOption/RadioOption (components/ui/), et inverse le layout pour
  que la case soit à gauche du label.
- Teinte la "zone de danger" de suppression de compte en rouge (fond +
  bordure), pas seulement le bouton.
- Migre les icônes de navigation générale vers lucide-react (nav-icons.tsx
  devient un fichier de ré-export) ; les pictogrammes d'ingrédients métier
  restent en SVG custom (pas d'équivalents fins côté lucide).

Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle manuelle (sidebar desktop/collapsed/mobile, light/dark).

* feat(web): icônes d'ingrédients depuis foodiconpack.com + page de crédits

- Remplace 19 des 22 pictogrammes génériques d'ingrédients par des icônes
  curées du pack gratuit "Common ingredient icons"/"Common Utensils" de
  foodiconpack.com (CC BY 4.0) : carotte, pomme, basilic, bœuf, poulet,
  saumon, crevette, riz, pois chiches, amandes, lait, cheddar, œufs,
  cannelle, miel, huile d'olive, bière, marmite, sucre.
- BREAD/DOUGH/SPROUT restent en SVG custom : pas d'équivalent net dans le
  pack (packs "ingrédients"/"ustensiles"/"plats"/"boissons" vérifiés).
  Architecture inchangée : `icon` reste un enum de 22 valeurs partagées en
  base (pas de migration, pas de mapping par ingrédient — cf. le
  commentaire du fichier sur l'historique emoji→enum générique).
- Nouveau wrapper FilledIcon (fill="currentColor", viewBox 2048) à côté du
  wrapper Icon existant (stroke) — les deux stylent au même endroit via
  CSS, donc le mélange des 19+3 icônes reste visuellement homogène.
- Ajoute /parametres/credits (CreditsPage) créditant foodiconpack.com et
  liant la licence CC BY 4.0, requis par la licence des icônes utilisées ;
  nouvelle entrée de nav "Crédits" (icône lucide Info).

Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle (grille des 22 icônes dans le picker, page crédits).

* feat(web,api): zone dangereuse rouge, préférences élargies, onglet favoris par défaut, e2e recettes, catalogue en uid+i18n

- Zone dangereuse (compte) : le bouton "Supprimer mon compte" est rouge.
- Pages préférences/paramétrage : contenu centré et élargi (32rem -> 56rem)
  au lieu de coller à gauche sur un écran large.
- Page recettes : l'onglet "Favoris" est sélectionné par défaut.
- Ajout de apps/web/cypress/e2e/recipes.cy.ts (onglets, recherche, sélection
  master-detail, favori, suppression, lien nouvelle recette).
- Catalogue de référence (ingrédients/régimes/allergènes) : la colonne
  `name` (le libellé français, utilisé comme clé unique) devient `key`, un
  slug stable et opaque au sens produit (ex. "vegetarien", "boeuf_hache").
  Le libellé lui-même déménage entièrement côté client, dans
  apps/web/src/locales/fr/translation.json sous le namespace `catalog.*`,
  résolu via `t(\`catalog.ingredients.${key}\`)` etc. — même schéma que
  IngredientCategory/IngredientSubcategory. Migration Prisma
  (rename + backfill des ~456 lignes déjà seedées), seed/service/tests API
  et composants web mis à jour en conséquence.
  - apps/api/src/utils/slugify.ts + scripts/generate-catalog-i18n.ts
    (regénère le fichier de traduction depuis reference-seed-data.ts).
  - 102 tests Mocha + 32 scénarios Cucumber passent contre la base migrée.

Note : cypress run plante dans cet environnement (le processus GPU
Chromium/Electron crash même headless, indépendamment des flags) — les
recipes.cy.ts n'ont pas pu être exécutés ici ; vérifiés par lecture du code
source des composants visés et par un passage manuel dans le navigateur de
prévisualisation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(api): les uids du catalogue sont en anglais, pas des slugs français

reference-seed-data.ts reste rédigé en français (c'est juste le libellé
d'autoring, jamais stocké/exposé), mais la clé stable (`Diet.key`/
`Category.key`/`Ingredient.key`) qu'on en dérive doit elle-même être un
identifiant anglais, indépendant de la langue d'autoring — pas juste le
même texte français passé à slugify().

- apps/api/src/db/catalog-en-keys.ts : dictionnaire écrit à la main
  (label français -> clé anglaise) pour les 5 régimes, 14 allergènes et
  437 ingrédients ; getEnglishKey() lève une erreur explicite si un
  nouvel élément n'a pas encore d'entrée plutôt que de retomber sur un
  slug français silencieux.
- scripts/validate-catalog-en-keys.ts : vérifie que chaque diet/allergène/
  ingrédient de reference-seed-data.ts a une entrée, et que les clés
  anglaises résultantes sont uniques (437/437, 14/14, 5/5 — zéro manquant,
  zéro collision).
- reference-seed-data.ts et scripts/generate-catalog-i18n.ts utilisent
  désormais getEnglishKey() au lieu de slugify(nom français).
- Nouvelle migration (20260818193000_catalog_keys_to_english) qui
  remappe les lignes déjà seedées avec un slug français (par la migration
  précédente) vers leur clé anglaise définitive.
- apps/web/src/locales/fr/translation.json régénéré : catalog.* est
  maintenant indexé par clé anglaise ("vegetarian", "eggs",
  "ground_beef"...), toujours avec le libellé français en valeur.
- Tests/step-definitions mis à jour (getEnglishKey() au lieu de
  slugify()) ; 102 tests Mocha + 32 scénarios Cucumber passent contre la
  base migrée. Vérifié aussi en direct via GET /reference/diets et
  /reference/allergies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(web): crypto.randomUUID plante hors contexte sécurisé, empêchant d'associer un ingrédient

Écran noir + "TypeError: crypto.randomUUID is not a function" au clic sur
une carte d'ingrédient dans le formulaire de recette. crypto.randomUUID()
n'est défini que dans un "contexte sécurisé" (https, ou littéralement le
host "localhost") — il est absent sur une IP locale (test sur un vrai
appareil), dans une WebView Capacitor (l'enrobage mobile prévu pour cette
app), ou en http sur un vrai domaine. RecipeFormPage/StepListEditor s'en
servaient pour générer l'identité React (`key`) de chaque ligne
d'ingrédient/étape en brouillon.

- apps/web/src/lib/client-key.ts : remplace par un générateur qui ne
  touche jamais `crypto` — un compteur + Math.random suffit, cette valeur
  n'a besoin d'être unique que le temps de la session de rendu, jamais
  envoyée au serveur.
- apps/web/cypress/e2e/recipe-form.cy.ts : couvre l'association d'un
  ingrédient (recherche, sélection, exclusion du picker une fois
  sélectionné, retrait), la création et l'édition d'une recette, et un
  test de non-régression dédié qui supprime crypto.randomUUID avant le
  chargement de la page (comme le ferait un vrai contexte non sécurisé)
  pour vérifier que l'ajout de plusieurs ingrédients/étapes ne plante
  plus.

Vérifié en direct dans le navigateur de prévisualisation en supprimant
crypto.randomUUID à la main (reproduit le crash), puis en confirmant que
l'ajout d'ingrédient fonctionne à nouveau après le correctif. cypress run
ne peut toujours pas s'exécuter dans cet environnement (voir le commit
précédent) — non exécutés avec Cypress lui-même, mais vérifiés par
lecture des sélecteurs réels et rejoués à la main dans le navigateur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ci): corrige les specs Cypress cassées par le refactor uid+i18n, applique biome

- onboarding.cy.ts / preferences.cy.ts / recipes.cy.ts mockaient encore
  GET /reference/diets|allergies avec l'ancienne forme {id, name}. Depuis
  les deux derniers commits l'API renvoie {id, key} (uid anglais) et le
  composant résout le libellé via i18n (t(`catalog.diets.${key}`)) — avec
  key manquant, ça affichait littéralement "catalog.diets.undefined" au
  lieu de "Végétarien"/"Omnivore"/etc., faisant échouer cy.select()/
  cy.contains() dans ces 3 specs. Corrigé pour mocker {key: "vegetarian"},
  {key: "peanuts"}, etc.
- recipes.cy.ts : le test "shows a not-found message" utilisait le
  mauvais code d'erreur (4041 au lieu de ErrorCode.RECIPE_NOT_FOUND =
  4045), donc RecipeDetailPanel tombait dans son état d'erreur générique
  au lieu du message "Cette recette n'existe pas." — bug dans mon propre
  test, sans rapport avec le refactor.
- pnpm lint (biome) : les fichiers touchés par le refactor précédent
  avaient quelques soucis de formatage/tri d'imports (des sed multi-
  fichiers, pas d'édition via l'outil habituel) — corrigés par
  `biome check --write`.

Vérifié : ces 3 specs + recipe-form.cy.ts passent maintenant dans le job
CI GitHub Actions (Linux, Cypress s'y exécute réellement — contrairement
à cet environnement Windows sandboxé, voir les commits précédents) ; 102
tests Mocha + 32 scénarios Cucumber toujours au vert en local.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 20:52:12 +02:00

1014 lines
43 KiB
TypeScript

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<IngredientSeed, "icon" | "dietNames"> & {
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<void> {
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 });
}
}