import { HttpError } from "@batch-cooking/error-tools"; import { wrapAsyncHandler } from "@batch-cooking/express-tools"; import { ErrorCode, createRecipeSchema, listRecipesSchema, updateRecipeSchema, } from "@batch-cooking/shared"; import { Router } from "express"; import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js"; import { addFavorite, createRecipe, deleteRecipe, getRecipe, listRecipes, removeFavorite, updateRecipe, } from "./recipe.service.js"; /** Router mounted at `/recipes` in app.ts. Every route requires a session — the catalog is shared across households, not public (same reasoning as `planning`/`house`: it's app content, not signup-time reference data). */ export const recipeRouter = Router(); /** Parses and validates an `:id` route param, shared by every route below that targets one recipe. */ function parseRecipeId(rawId: string | undefined): number { const id = Number(rawId); if (!Number.isInteger(id)) { throw new HttpError(400, ErrorCode.VALIDATION_ERROR, "id must be an integer"); } return id; } recipeRouter.get( "/", requireAuth, wrapAsyncHandler(async (req, res) => { const input = listRecipesSchema.parse(req.query); const { id: viewerId, houseId } = res.locals.userProfile; res.status(200).json( await listRecipes(viewerId, houseId, input.tab, { search: input.search, suitableForHousehold: input.suitableForHousehold, ingredientIds: input.ingredientIds, dietIds: input.dietIds, }), ); }), ); recipeRouter.get( "/:id", requireAuth, wrapAsyncHandler(async (req, res) => { const id = parseRecipeId(req.params.id); const { id: viewerId, houseId } = res.locals.userProfile; res.status(200).json(await getRecipe(id, viewerId, houseId)); }), ); recipeRouter.post( "/", requireAuth, wrapAsyncHandler(async (req, res) => { const input = createRecipeSchema.parse(req.body); const { id: authorId, houseId } = res.locals.userProfile; res.status(201).json(await createRecipe(input, authorId, houseId)); }), ); recipeRouter.patch( "/:id", requireAuth, wrapAsyncHandler(async (req, res) => { const id = parseRecipeId(req.params.id); const input = updateRecipeSchema.parse(req.body); const { id: viewerId, houseId } = res.locals.userProfile; res.status(200).json(await updateRecipe(id, input, viewerId, houseId)); }), ); recipeRouter.delete( "/:id", requireAuth, wrapAsyncHandler(async (req, res) => { const id = parseRecipeId(req.params.id); const { id: viewerId, houseId } = res.locals.userProfile; await deleteRecipe(id, viewerId, houseId); res.status(204).end(); }), ); recipeRouter.post( "/:id/favorite", requireAuth, wrapAsyncHandler(async (req, res) => { const id = parseRecipeId(req.params.id); const { id: viewerId, houseId } = res.locals.userProfile; await addFavorite(id, viewerId, houseId); res.status(204).end(); }), ); recipeRouter.delete( "/:id/favorite", requireAuth, wrapAsyncHandler(async (req, res) => { const id = parseRecipeId(req.params.id); await removeFavorite(id, res.locals.userProfile.id); res.status(204).end(); }), );