batchCooking/apps/api/src/lib/recipe-source-registry.ts
Nicolas aedeb257ce feat(recipes): relie les recettes à leur source (sourceId + externalId)
Répond au besoin identifié précédemment : la table `sources` devient
un vrai catalogue des sources implémentées, et une recette importée
pourra être reliée à l'item source dont elle provient.

- Source.key (unique) — même convention que Diet.key/Unit.key/
  TechStep.key. Le catalogue est désormais synchronisé depuis le
  registre d'adaptateurs (recipe-source-registry.ts) via
  syncRecipeSources() (nouveau apps/api/src/db/recipe-source-sync.ts),
  plutôt que maintenu à la main comme DIETS/UNITS — reste vide tant
  qu'aucun adaptateur concret n'est enregistré.
- Recipe.externalId (nullable) — l'identifiant de la recette côté
  source. Contrainte @@unique([sourceId, externalId]) : empêche
  d'importer deux fois la même recette (les recettes manuelles, aux
  deux colonnes nulles, ne sont jamais en conflit entre elles).
- findImportedExternalIds(prisma, sourceKey, externalIds) — le
  pendant DB de markAlreadyImported (recipe-source-adapter.ts),
  ferme la boucle commencée dans la PR précédente pour distinguer les
  recettes déjà intégrées lors du browse.
- syncRecipeSources() appelé après seedReferenceData() dans
  prisma/seed.ts et test-support/reset-db.ts.

Toujours pas de route HTTP ni de champ sourceId/externalId exposé
dans createRecipeSchema — la sauvegarde effective d'une recette
importée reste pour une PR ultérieure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 07:43:51 +02:00

56 lines
2.6 KiB
TypeScript

import type { RecipeSourceAdapter } from "./recipe-source-adapter.js";
/**
* In-memory registry of every {@link RecipeSourceAdapter} (recipe-source-adapter.ts)
* this process knows about, keyed by `adapter.key`. Deliberately not
* DB-backed — an adapter *is* code (a website's fetch/parse logic can't
* live in a database row) — but the `Source` table (schema.prisma) is kept
* in sync with it (see `syncRecipeSources`, recipe-source-sync.ts) so a
* saved `Recipe.sourceId` has a row to point at. Actually saving an
* imported recipe (setting `Recipe.sourceId`/`externalId`) is still future
* work for whichever module ends up driving the import pipeline — this
* registry only answers "which sources can we import from right now".
*
* No adapter is registered here yet — this file only provides the
* mechanism; `registerRecipeSource` is meant to be called once per adapter
* module, at whatever point a concrete source is added.
*/
const adapters = new Map<string, RecipeSourceAdapter>();
/**
* Registers `adapter` under its own `key`. Throws if that key is already
* taken — two adapters silently overwriting each other would be a bug (a
* caller reaching for "marmiton" should never get a different adapter than
* the one it registered), not a case to swallow.
*/
export function registerRecipeSource<TRawDetail>(adapter: RecipeSourceAdapter<TRawDetail>): void {
if (adapters.has(adapter.key)) {
throw new Error(`Recipe source "${adapter.key}" is already registered`);
}
// `TRawDetail` only matters within one adapter's own list/fetchDetail/parse
// trio — once stored, callers look adapters up by key and drive the same
// three methods generically, so the registry itself doesn't need to know
// each adapter's raw type. This cast is the standard way to store a
// heterogeneous collection of otherwise-identically-shaped generics.
adapters.set(adapter.key, adapter as RecipeSourceAdapter);
}
/** The adapter registered under `key`, or `undefined` if none is. */
export function getRecipeSource(key: string): RecipeSourceAdapter | undefined {
return adapters.get(key);
}
/** Every registered adapter — e.g. to offer a source picker. */
export function listRecipeSources(): RecipeSourceAdapter[] {
return [...adapters.values()];
}
/**
* Empties the registry. Not meant for application code — `apps/api/src`
* never calls this — only for test isolation, the same role
* `test-support/reset-db.ts` plays for the database: without it, adapters
* registered by one test file would leak into the next.
*/
export function clearRecipeSources(): void {
adapters.clear();
}