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>
56 lines
2.6 KiB
TypeScript
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();
|
|
}
|