Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 50s
165 lines
5.2 KiB
TypeScript
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;
|