import { HttpError } from "@batch-cooking/error-tools"; import { wrapAsyncHandler } from "@batch-cooking/express-tools"; import { createRecipeSchema, ErrorCode, listRecipesSchema, submitTechStepCorrectionSchema, 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"; import { listTechStepCorrections, submitTechStepCorrection, } from "./recipe-tech-step-correction.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; } /** Same shape as {@link parseRecipeId}, for the `:stepId` route param of the tech-step-correction routes below — a distinct function only so the error message names the right param. */ function parseStepId(rawId: string | undefined): number { const id = Number(rawId); if (!Number.isInteger(id)) { throw new HttpError(400, ErrorCode.VALIDATION_ERROR, "stepId 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(); }), ); // Open to any authenticated viewer who can see the recipe, not just its // author — see recipe-tech-step-correction.service.ts's own doc comment // for why. recipeRouter.post( "/:id/steps/:stepId/corrections", requireAuth, wrapAsyncHandler(async (req, res) => { const id = parseRecipeId(req.params.id); const stepId = parseStepId(req.params.stepId); const input = submitTechStepCorrectionSchema.parse(req.body); const { id: correctorId, houseId } = res.locals.userProfile; res.status(201).json(await submitTechStepCorrection(id, stepId, input, correctorId, houseId)); }), ); recipeRouter.get( "/:id/steps/:stepId/corrections", requireAuth, wrapAsyncHandler(async (req, res) => { const id = parseRecipeId(req.params.id); const stepId = parseStepId(req.params.stepId); const { id: viewerId, houseId } = res.locals.userProfile; res.status(200).json(await listTechStepCorrections(id, stepId, viewerId, houseId)); }), );