Files
flyer-crawler.projectium.com/src/routes/recipe.routes.ts
Torben Sorensen b4306a6092
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 50s
more rate limiting
2026-01-05 14:53:49 -08:00

165 lines
5.2 KiB
TypeScript

// src/routes/recipe.routes.ts
import { Router } from 'express';
import { z } from 'zod';
import * as db from '../services/db/index.db';
import { aiService } from '../services/aiService.server';
import passport from './passport.routes';
import { validateRequest } from '../middleware/validation.middleware';
import { requiredString, numericIdParam, optionalNumeric } from '../utils/zodUtils';
import { publicReadLimiter, suggestionLimiter } from '../config/rateLimiters';
const router = Router();
const bySalePercentageSchema = z.object({
query: z.object({
minPercentage: optionalNumeric({ default: 50, min: 0, max: 100 }),
}),
});
const bySaleIngredientsSchema = z.object({
query: z.object({
minIngredients: optionalNumeric({ default: 3, integer: true, positive: true }),
}),
});
const byIngredientAndTagSchema = z.object({
query: z.object({
ingredient: requiredString('Query parameter "ingredient" is required.'),
tag: requiredString('Query parameter "tag" is required.'),
}),
});
const recipeIdParamsSchema = numericIdParam('recipeId');
const suggestRecipeSchema = z.object({
body: z.object({
ingredients: z.array(z.string().min(1)).nonempty('At least one ingredient is required.'),
}),
});
/**
* GET /api/recipes/by-sale-percentage - Get recipes based on the percentage of their ingredients on sale.
*/
router.get(
'/by-sale-percentage',
publicReadLimiter,
validateRequest(bySalePercentageSchema),
async (req, res, next) => {
try {
// Explicitly parse req.query to apply coercion (string -> number) and default values
const { query } = bySalePercentageSchema.parse({ query: req.query });
const recipes = await db.recipeRepo.getRecipesBySalePercentage(query.minPercentage!, req.log);
res.json(recipes);
} catch (error) {
req.log.error({ error }, 'Error fetching recipes in /api/recipes/by-sale-percentage:');
next(error);
}
},
);
/**
* GET /api/recipes/by-sale-ingredients - Get recipes by the minimum number of sale ingredients.
*/
router.get(
'/by-sale-ingredients',
publicReadLimiter,
validateRequest(bySaleIngredientsSchema),
async (req, res, next) => {
try {
// Explicitly parse req.query to apply coercion (string -> number) and default values
const { query } = bySaleIngredientsSchema.parse({ query: req.query });
const recipes = await db.recipeRepo.getRecipesByMinSaleIngredients(
query.minIngredients!,
req.log,
);
res.json(recipes);
} catch (error) {
req.log.error({ error }, 'Error fetching recipes in /api/recipes/by-sale-ingredients:');
next(error);
}
},
);
/**
* GET /api/recipes/by-ingredient-and-tag - Find recipes by a specific ingredient and tag.
*/
router.get(
'/by-ingredient-and-tag',
publicReadLimiter,
validateRequest(byIngredientAndTagSchema),
async (req, res, next) => {
try {
const { query } = byIngredientAndTagSchema.parse({ query: req.query });
const recipes = await db.recipeRepo.findRecipesByIngredientAndTag(
query.ingredient,
query.tag,
req.log,
);
res.json(recipes);
} catch (error) {
req.log.error({ error }, 'Error fetching recipes in /api/recipes/by-ingredient-and-tag:');
next(error);
}
},
);
/**
* GET /api/recipes/:recipeId/comments - Get all comments for a specific recipe.
*/
router.get('/:recipeId/comments', publicReadLimiter, validateRequest(recipeIdParamsSchema), async (req, res, next) => {
try {
// Explicitly parse req.params to coerce recipeId to a number
const { params } = recipeIdParamsSchema.parse({ params: req.params });
const comments = await db.recipeRepo.getRecipeComments(params.recipeId, req.log);
res.json(comments);
} catch (error) {
req.log.error({ error }, `Error fetching comments for recipe ID ${req.params.recipeId}:`);
next(error);
}
});
/**
* GET /api/recipes/:recipeId - Get a single recipe by its ID, including ingredients and tags.
*/
router.get('/:recipeId', publicReadLimiter, validateRequest(recipeIdParamsSchema), async (req, res, next) => {
try {
// Explicitly parse req.params to coerce recipeId to a number
const { params } = recipeIdParamsSchema.parse({ params: req.params });
const recipe = await db.recipeRepo.getRecipeById(params.recipeId, req.log);
res.json(recipe);
} catch (error) {
req.log.error({ error }, `Error fetching recipe ID ${req.params.recipeId}:`);
next(error);
}
});
/**
* POST /api/recipes/suggest - Generates a simple recipe suggestion from a list of ingredients.
* This is a protected endpoint.
*/
router.post(
'/suggest',
suggestionLimiter,
passport.authenticate('jwt', { session: false }),
validateRequest(suggestRecipeSchema),
async (req, res, next) => {
try {
const { body } = req as unknown as z.infer<typeof suggestRecipeSchema>;
const suggestion = await aiService.generateRecipeSuggestion(body.ingredients, req.log);
if (!suggestion) {
return res
.status(503)
.json({ message: 'AI service is currently unavailable or failed to generate a suggestion.' });
}
res.json({ suggestion });
} catch (error) {
req.log.error({ error }, 'Error generating recipe suggestion');
next(error);
}
},
);
export default router;