Marmiton s'est avéré inaccessible pour du scraping (bloqué même via WebFetch, signe de protection anti-bot) — TheMealDB (themealdb.com) est une vraie API JSON publique et gratuite, sans scraping, testée en conditions réelles (list → fetchDetail → parse fonctionnent bout en bout contre l'API live). - Source.iconUrl (nullable) + RecipeSourceAdapter.iconUrl (requis, même convention que `official`) synchronisé par syncRecipeSources. - apps/api/src/sources/the-meal-db.ts : premier RecipeSourceAdapter réel — official: true (API officielle, pas de scraping), utilise fetch natif (aucune dépendance ajoutée). list() fait une recherche (pas de vrai "browse" côté TheMealDB, mais une requête vide renvoie un échantillon de secours) ; parse() éclate les instructions en étapes par ligne et ignore les emplacements d'ingrédients vides. - apps/api/src/sources/index.ts : registerAllRecipeSources(), appelé par server.ts (process réel) et prisma/seed.ts — délibérément PAS importé par app.ts, pour ne jamais dépendre de l'ordre des tests. - SourceSelect (web) affiche désormais le logo de la source à côté de son nom. Vérifié en conditions réelles : seed → table sources peuplée avec le vrai logo TheMealDB ; endpoint /reference/sources sur serveur réel ; parcours navigateur complet (onboarding → étape sources visible avec icône chargée → activation → paramètres foyer reflète le choix). 186 tests passent (16 nouveaux, dont le moteur TheMealDB testé avec un stub de fetch — aucun appel réseau réel dans la suite automatisée). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
import type {
|
|
AllergyView,
|
|
DietView,
|
|
IngredientView,
|
|
SourceView,
|
|
TechStepView,
|
|
UnitView,
|
|
} from "@batch-cooking/shared";
|
|
import { prisma } from "../../db/prisma.js";
|
|
|
|
/**
|
|
* All reference dietary regimes, ordered by `key` — small, static list (see
|
|
* prisma/seed.ts). `key` is a stable slug, not the display label (see
|
|
* {@link DietView}), so this is an alphabetical-by-slug order rather than a
|
|
* true French alphabetical one — close enough for a 5-item list, and the
|
|
* server has no other order to offer now that the label itself only exists
|
|
* client-side (`apps/web`'s `locales/fr/translation.json`).
|
|
*/
|
|
export async function getDiets(): Promise<DietView[]> {
|
|
return prisma.diet.findMany({ orderBy: { key: "asc" } });
|
|
}
|
|
|
|
/**
|
|
* All reference allergens, ordered by key (see {@link getDiets} for why key,
|
|
* not label). `Allergy` carries no `key` of its own — it's the selectable
|
|
* instance of a keyed `Category` (see schema.prisma) — so this resolves
|
|
* each allergen's key from its category and flattens the split away for
|
|
* callers.
|
|
*/
|
|
export async function getAllergies(): Promise<AllergyView[]> {
|
|
const allergies = await prisma.allergy.findMany({
|
|
include: { category: { select: { key: true, kind: true } } },
|
|
orderBy: { category: { key: "asc" } },
|
|
});
|
|
return allergies.map((allergy) => ({
|
|
id: allergy.id,
|
|
key: allergy.category.key,
|
|
kind: allergy.category.kind,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* All reference recipe-ingredient units, ordered by key (see {@link getDiets}
|
|
* for why) — small, static list (see `reference-seed-data.ts`'s `UNITS`).
|
|
* `toBaseFactor` comes back as a Prisma `Decimal`, converted to a plain
|
|
* `number` here the same way `recipe.service.ts` does for
|
|
* `RecipeIngredient.quantity`.
|
|
*/
|
|
export async function getUnits(): Promise<UnitView[]> {
|
|
const units = await prisma.unit.findMany({ orderBy: { key: "asc" } });
|
|
return units.map((unit) => ({
|
|
id: unit.id,
|
|
key: unit.key,
|
|
type: unit.type,
|
|
toBaseFactor: Number(unit.toBaseFactor),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* All reference cooking techniques, ordered by key (see {@link getDiets}
|
|
* for why) — small, static list (see `reference-seed-data.ts`'s
|
|
* `TECH_STEPS`). Not consumed by the recipe UI yet — see {@link TechStepView}.
|
|
*/
|
|
export async function getTechSteps(): Promise<TechStepView[]> {
|
|
return prisma.techStep.findMany({ orderBy: { key: "asc" } });
|
|
}
|
|
|
|
/**
|
|
* Every implemented recipe source, ordered by name (not `key` — unlike
|
|
* every other reference catalog, `name` here *is* the display string a
|
|
* household picks from, see {@link SourceView}, so alphabetical-by-name is
|
|
* what a real picker should show). Empty until a concrete adapter is
|
|
* registered (see `recipe-source-registry.ts`) and synced (see
|
|
* `recipe-source-sync.ts`'s `syncRecipeSources`).
|
|
*/
|
|
export async function getSources(): Promise<SourceView[]> {
|
|
// Explicit `select` — `url` exists on the `Source` row but isn't part of
|
|
// `SourceView` yet, so it must not leak into the response the way a bare
|
|
// `findMany()` would let it.
|
|
return prisma.source.findMany({
|
|
select: { id: true, key: true, name: true, official: true, iconUrl: true },
|
|
orderBy: { name: "asc" },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* All reference ingredients, ordered by key (see {@link getDiets} for why),
|
|
* each resolved to its allergens (see `IngredientAllergy` in schema.prisma)
|
|
* and compatible diet regimes (see `IngredientDiet`) — same flattening
|
|
* approach as {@link getAllergies}. Ingredients with no linked
|
|
* allergen/diet come back with `allergens: []`/`diets: []`.
|
|
*/
|
|
export async function getIngredients(): Promise<IngredientView[]> {
|
|
const ingredients = await prisma.ingredient.findMany({
|
|
include: {
|
|
allergies: { include: { allergy: { include: { category: true } } } },
|
|
diets: { include: { diet: true } },
|
|
},
|
|
orderBy: { key: "asc" },
|
|
});
|
|
return ingredients.map((ingredient) => ({
|
|
id: ingredient.id,
|
|
key: ingredient.key,
|
|
icon: ingredient.icon,
|
|
category: ingredient.category,
|
|
subcategory: ingredient.subcategory,
|
|
reproducible: ingredient.reproducible,
|
|
allergens: ingredient.allergies.map(({ allergy }) => ({
|
|
id: allergy.id,
|
|
key: allergy.category.key,
|
|
kind: allergy.category.kind,
|
|
})),
|
|
diets: ingredient.diets.map(({ diet }) => ({ id: diet.id, key: diet.key })),
|
|
}));
|
|
}
|