Première étape du chantier "onglet Sources" (parcourir toutes les
recettes externes des sources activées par le foyer, importées ou non,
et déclencher leur import à l'ajout au planning) — celle-ci pose les
endpoints backend de lecture seule, rien n'est encore sauvegardé.
- RecipeSourceAdapter gagne `locale` (theMealDbAdapter: "en") — nécessaire
pour que translateRecipe/matchTechStepSpans sachent contre quel jeu de
TechStepMapping/labels d'ingrédients traduire une source donnée.
- findImportedExternalIds (recipe-source-sync.ts) devient
findImportedRecipeIds : renvoie une Map<externalId, recipeId> au lieu
d'un simple Set — son premier vrai appelant (le parcours) a besoin de
l'id réel pour naviguer directement vers la recette déjà importée, pas
seulement savoir qu'elle l'est.
- Nouveau module apps/api/src/modules/sources/ :
- GET /sources/:sourceKey/browse — appelle list() de l'adaptateur,
flague chaque item alreadyImported/recipeId. Restreint aux sources
activées par le foyer courant (HouseSource) ; 404 SOURCE_NOT_FOUND
sinon, même si la source existe (même posture que la visibilité des
recettes : "pas trouvée" plutôt que "pas autorisée").
- GET /sources/:sourceKey/preview/:externalId — fetchDetail + parse +
résolution complète (translateRecipeIngredients, matchTechStepSpans
avec spans réels) contre la locale de la source, sans rien
sauvegarder. Ingrédients non résolus → null plutôt qu'une erreur.
- Nouveaux types partagés (packages/shared/src/types/sources.ts) :
BrowsableSourceItemView, RecipeImportDraftView (+ Draft*View).
Vérifié en conditions réelles contre TheMealDB (recette "Chicken Handi") :
ingrédients résolus avec la bonne quantité/unité (1.2 kg de poulet, 8
gousses d'ail...), non-résolus corrects (huile végétale, piment vert),
et chaque étape avec ses techniques détectées et leurs spans exacts
(cook/fry/plate/setAside sur la même phrase, etc.).
Tests : 276 passing (+8 nouveaux, sources.test.ts). Étape suivante (2/4) :
l'UI de parcours (onglet Sources) — voir le plan de session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
73 lines
3.2 KiB
TypeScript
73 lines
3.2 KiB
TypeScript
import type { PrismaClient } from "@prisma/client";
|
|
import { listRecipeSources } from "../lib/recipe-source-registry.js";
|
|
|
|
/**
|
|
* Upserts one `Source` row (schema.prisma) per adapter currently in
|
|
* `recipe-source-registry.ts`, keyed by `adapter.key` — keeps the
|
|
* `sources` catalog an exact mirror of "which sources are actually
|
|
* implemented in code", rather than a hand-maintained list that can drift
|
|
* out of sync the way `DIETS`/`UNITS` (`reference-seed-data.ts`) would if
|
|
* copy-pasted here. Call once at startup (`prisma/seed.ts`) and in test
|
|
* setup (`test-support/reset-db.ts`), the same place `seedReferenceData`
|
|
* runs — kept as its own function rather than folded into that one, since
|
|
* it reads from the adapter registry instead of a static array.
|
|
*
|
|
* Never deletes a `Source` row whose key fell out of the registry (e.g. an
|
|
* adapter temporarily removed from code) — a recipe already imported from
|
|
* it should keep citing it rather than having `sourceId` silently nulled
|
|
* out from under it (see `onDelete: SetNull` on `Recipe.source` in
|
|
* schema.prisma, which is what *would* happen on an actual delete).
|
|
*
|
|
* Safe to call with an empty registry (leaves the `sources` table
|
|
* untouched) — the case whenever nothing has called `registerRecipeSource`
|
|
* yet, e.g. most test files (see `apps/api/src/sources/index.ts` for where
|
|
* the app's own concrete adapters — currently just TheMealDB — register
|
|
* themselves at startup).
|
|
*/
|
|
export async function syncRecipeSources(prisma: PrismaClient): Promise<void> {
|
|
for (const adapter of listRecipeSources()) {
|
|
await prisma.source.upsert({
|
|
where: { key: adapter.key },
|
|
update: { name: adapter.name, official: adapter.official, iconUrl: adapter.iconUrl },
|
|
create: {
|
|
key: adapter.key,
|
|
name: adapter.name,
|
|
official: adapter.official,
|
|
iconUrl: adapter.iconUrl,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Which of `externalIds` already have a `Recipe` imported from the source
|
|
* registered under `sourceKey`, mapped to that `Recipe`'s id — the
|
|
* DB-touching counterpart to `markAlreadyImported` (recipe-source-adapter.ts),
|
|
* which stays pure and takes a plain `ReadonlySet<string>` (this map's
|
|
* `.keys()`) rather than querying itself. The id (not just membership) is
|
|
* what `sources.service.ts`'s browse endpoint needs to link an
|
|
* already-imported item straight to its real `Recipe`, instead of a
|
|
* caller having to look it up again. Returns an empty map (not an error)
|
|
* for a `sourceKey` with no matching `Source` row — nothing can have been
|
|
* imported from a source we don't even have a catalog entry for.
|
|
*/
|
|
export async function findImportedRecipeIds(
|
|
prisma: PrismaClient,
|
|
sourceKey: string,
|
|
externalIds: string[],
|
|
): Promise<Map<string, number>> {
|
|
if (externalIds.length === 0) return new Map();
|
|
|
|
const source = await prisma.source.findUnique({ where: { key: sourceKey } });
|
|
if (!source) return new Map();
|
|
|
|
const imported = await prisma.recipe.findMany({
|
|
where: { sourceId: source.id, externalId: { in: externalIds } },
|
|
select: { id: true, externalId: true },
|
|
});
|
|
return new Map(
|
|
imported.flatMap((recipe) =>
|
|
recipe.externalId !== null ? [[recipe.externalId, recipe.id]] : [],
|
|
),
|
|
);
|
|
}
|