import type { PrismaClient } from "@prisma/client"; import { listRecipeSources } from "../lib/recipe-sources/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 { try { 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, }, }); } } catch (err) { // Rethrown as-is — callers (app startup, `sources.service.ts` via test // setup) already handle/log failures centrally; this function just // isn't allowed a bare `await` per the repo's async/try-catch convention. throw err; } } /** * 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` (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> { try { 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]] : [], ), ); } catch (err) { throw err; // see syncRecipeSources()'s catch comment above } }