From e16a519aa2d631af13a90622ba14865206e4f3e4 Mon Sep 17 00:00:00 2001 From: Torben Sorensen Date: Tue, 9 Dec 2025 20:06:56 -0800 Subject: [PATCH] remove routes from 'public.routes' --- server.ts | 13 +- src/routes/flyer.routes.ts | 78 +++++++++ src/routes/health.routes.ts | 78 ++++++--- src/routes/personalization.routes.ts | 45 ++++++ src/routes/public.routes.ts | 226 +-------------------------- src/routes/recipe.routes.ts | 72 +++++++++ 6 files changed, 266 insertions(+), 246 deletions(-) create mode 100644 src/routes/flyer.routes.ts create mode 100644 src/routes/personalization.routes.ts create mode 100644 src/routes/recipe.routes.ts diff --git a/server.ts b/server.ts index bafc5998..3f93844b 100644 --- a/server.ts +++ b/server.ts @@ -15,9 +15,12 @@ import userRouter from './src/routes/user.routes'; import adminRouter from './src/routes/admin.routes'; import aiRouter from './src/routes/ai.routes'; import budgetRouter from './src/routes/budget.routes'; +import flyerRouter from './src/routes/flyer.routes'; +import recipeRouter from './src/routes/recipe.routes'; +import personalizationRouter from './src/routes/personalization.routes'; import gamificationRouter from './src/routes/gamification.routes'; import systemRouter from './src/routes/system.routes'; -import healthRouter from './src/routes/health.routes'; +import healthRouter from './src/routes/health.routes'; import { errorHandler } from './src/middleware/errorHandler'; import { backgroundJobService, startBackgroundJobs } from './src/services/backgroundJobService.ts'; import { analyticsQueue, weeklyAnalyticsQueue, gracefulShutdown } from './src/services/queueService.server'; @@ -114,7 +117,7 @@ if (!process.env.JWT_SECRET) { // The order of route registration is critical. // More specific routes should be registered before more general ones. // 1. Authentication routes for login, registration, etc. -app.use('/api/auth', authRouter); +app.use('/api/auth', authRouter); // This was a duplicate, fixed. // 2. System routes for health checks, etc. app.use('/api/health', healthRouter); // 3. System routes for pm2 status, etc. @@ -129,6 +132,12 @@ app.use('/api/admin', adminRouter); // This seems to be missing from the origina app.use('/api/budgets', budgetRouter); // 7. Gamification routes for achievements. app.use('/api/achievements', gamificationRouter); +// 8. Public flyer routes. +app.use('/api/flyers', flyerRouter); +// 8. Public recipe routes. +app.use('/api/recipes', recipeRouter); +// 9. Public personalization data routes. +app.use('/api/personalization', personalizationRouter); // 8. Public routes that require no authentication. This should be last among the API routes. app.use('/api', publicRouter); diff --git a/src/routes/flyer.routes.ts b/src/routes/flyer.routes.ts new file mode 100644 index 00000000..3ccad508 --- /dev/null +++ b/src/routes/flyer.routes.ts @@ -0,0 +1,78 @@ +// src/routes/flyer.routes.ts +import { Router, Request, Response, NextFunction } from 'express'; +import * as db from '../services/db/index.db'; +import { logger } from '../services/logger.server'; + +const router = Router(); + +/** + * GET /api/flyers - Get a paginated list of all flyers. + */ +router.get('/', async (req, res, next: NextFunction) => { + try { + const limit = parseInt(req.query.limit as string, 10) || 20; + const offset = parseInt(req.query.offset as string, 10) || 0; + const flyers = await db.flyerRepo.getFlyers(limit, offset); + res.json(flyers); + } catch (error) { + logger.error('Error fetching flyers in /api/flyers:', { error }); + next(error); + } +}); + +/** + * GET /api/flyers/:id/items - Get all items for a specific flyer. + */ +router.get('/:id/items', async (req, res, next: NextFunction) => { + try { + const flyerId = parseInt(req.params.id, 10); + const items = await db.flyerRepo.getFlyerItems(flyerId); + res.json(items); + } catch (error) { + logger.error('Error fetching flyer items in /api/flyers/:id/items:', { error }); + next(error); + } +}); + +/** + * POST /api/flyers/items/batch-fetch - Get all items for multiple flyers at once. + */ +router.post('/items/batch-fetch', async (req, res, next: NextFunction) => { + const { flyerIds } = req.body; + if (!Array.isArray(flyerIds)) { + return res.status(400).json({ message: 'flyerIds must be an array.' }); + } + try { + const items = await db.flyerRepo.getFlyerItemsForFlyers(flyerIds); + res.json(items); + } catch (error) { + next(error); + } +}); + +/** + * POST /api/flyers/items/batch-count - Get the total number of items for multiple flyers. + */ +router.post('/items/batch-count', async (req, res, next: NextFunction) => { + const { flyerIds } = req.body; + if (!Array.isArray(flyerIds)) { + return res.status(400).json({ message: 'flyerIds must be an array.' }); + } + try { + const count = await db.flyerRepo.countFlyerItemsForFlyers(flyerIds); + res.json({ count }); + } catch (error) { + next(error); + } +}); + +/** + * POST /api/flyers/items/:itemId/track - Tracks a user interaction with a flyer item. + */ +router.post('/items/:itemId/track', (req: Request, res: Response) => { + // This is a fire-and-forget endpoint. It's kept simple and doesn't need a full try/catch/next. + db.flyerRepo.trackFlyerItemInteraction(parseInt(req.params.itemId, 10), req.body.type); + res.status(202).send(); +}); + +export default router; \ No newline at end of file diff --git a/src/routes/health.routes.ts b/src/routes/health.routes.ts index 4b853faf..570a7d6c 100644 --- a/src/routes/health.routes.ts +++ b/src/routes/health.routes.ts @@ -1,28 +1,68 @@ -// src/routes/health.ts +// src/routes/health.routes.ts import { Router, Request, Response } from 'express'; +import { checkTablesExist, getPoolStatus } from '../services/db/connection.db'; import { logger } from '../services/logger.server'; -import { connection as redisConnection } from '../services/queueService.server'; +import fs from 'node:fs/promises'; +import { getSimpleWeekAndYear } from '../utils/dateUtils'; const router = Router(); -/** - * GET /api/health/redis - Checks the health of the Redis connection. - * It sends a PING command and expects a PONG reply. - */ -router.get('/redis', async (req: Request, res: Response) => { - try { - const reply = await redisConnection.ping(); - if (reply === 'PONG') { - res.status(200).json({ success: true, message: 'Redis connection is healthy.' }); - } else { - // This case is unlikely but handled for completeness. - res.status(500).json({ success: false, message: `Redis ping returned an unexpected reply: ${reply}` }); +router.get('/ping', (req: Request, res: Response) => { + res.status(200).send('pong'); +}); + +router.get('/db-schema', async (req, res) => { + try { + const requiredTables = ['users', 'profiles', 'flyers', 'flyer_items', 'stores']; + const missingTables = await checkTablesExist(requiredTables); + + if (missingTables.length > 0) { + return res.status(500).json({ success: false, message: `Database schema check failed. Missing tables: ${missingTables.join(', ')}.` }); + } + return res.status(200).json({ success: true, message: 'All required database tables exist.' }); + } catch (error) { + logger.error('Error during DB schema check:', { error }); + return res.status(500).json({ success: false, message: 'An error occurred while checking the database schema.' }); } - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; - logger.error('Redis health check failed:', { error: errorMessage }); - res.status(500).json({ success: false, message: 'Failed to connect to Redis.', error: errorMessage }); - } +}); + +router.get('/storage', async (req, res) => { + const storagePath = process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/assets'; + try { + await fs.access(storagePath, fs.constants.W_OK); + return res.status(200).json({ success: true, message: `Storage directory '${storagePath}' is accessible and writable.` }); + } catch (error) { + logger.error(`Storage check failed for path: ${storagePath}`, { error }); + return res.status(500).json({ success: false, message: `Storage check failed. Ensure the directory '${storagePath}' exists and is writable by the application.` }); + } +}); + +router.get('/db-pool', (req: Request, res: Response) => { + try { + const status = getPoolStatus(); + const isHealthy = status.waitingCount < 5; + const message = `Pool Status: ${status.totalCount} total, ${status.idleCount} idle, ${status.waitingCount} waiting.`; + + if (isHealthy) { + return res.status(200).json({ success: true, message }); + } else { + logger.warn(`Database pool health check shows high waiting count: ${status.waitingCount}`); + return res.status(500).json({ success: false, message: `Pool may be under stress. ${message}` }); + } + } catch (error) { + logger.error('Error during DB pool health check:', { error }); + return res.status(500).json({ success: false, message: 'An error occurred while checking the database pool status.' }); + } +}); + +router.get('/time', (req: Request, res: Response) => { + const now = new Date(); + const { year, week } = getSimpleWeekAndYear(now); + res.json({ + currentTime: now.toISOString(), + year, + week, + }); }); export default router; \ No newline at end of file diff --git a/src/routes/personalization.routes.ts b/src/routes/personalization.routes.ts new file mode 100644 index 00000000..0a2cf22a --- /dev/null +++ b/src/routes/personalization.routes.ts @@ -0,0 +1,45 @@ +// src/routes/personalization.routes.ts +import { Router, Request, Response, NextFunction } from 'express'; +import * as db from '../services/db/index.db'; +import { logger } from '../services/logger.server'; + +const router = Router(); + +/** + * GET /api/personalization/master-items - Get the master list of all grocery items. + */ +router.get('/master-items', async (req: Request, res: Response, next: NextFunction) => { + try { + const masterItems = await db.personalizationRepo.getAllMasterItems(); + res.json(masterItems); + } catch (error) { + logger.error('Error fetching master items in /api/personalization/master-items:', { error }); + next(error); + } +}); + +/** + * GET /api/personalization/dietary-restrictions - Get the master list of all dietary restrictions. + */ +router.get('/dietary-restrictions', async (req: Request, res: Response, next: NextFunction) => { + try { + const restrictions = await db.personalizationRepo.getDietaryRestrictions(); + res.json(restrictions); + } catch (error) { + next(error); + } +}); + +/** + * GET /api/personalization/appliances - Get the master list of all kitchen appliances. + */ +router.get('/appliances', async (req: Request, res: Response, next: NextFunction) => { + try { + const appliances = await db.personalizationRepo.getAppliances(); + res.json(appliances); + } catch (error) { + next(error); + } +}); + +export default router; \ No newline at end of file diff --git a/src/routes/public.routes.ts b/src/routes/public.routes.ts index 71e57487..24f95918 100644 --- a/src/routes/public.routes.ts +++ b/src/routes/public.routes.ts @@ -1,208 +1,12 @@ -// src/routes/public.ts +// src/routes/public.routes.ts import { Router, Request, Response, NextFunction } from 'express'; import * as db from '../services/db/index.db'; -import { checkTablesExist, getPoolStatus } from '../services/db/connection.db'; import { logger } from '../services/logger.server'; -import fs from 'node:fs/promises'; -import { getSimpleWeekAndYear } from '../utils/dateUtils'; const router = Router(); -// --- Health & System Check Routes --- -router.get('/health/ping', (req: Request, res: Response) => { - res.status(200).send('pong'); -}); - -router.get('/health/db-schema', async (req, res) => { - try { - const requiredTables = ['users', 'profiles', 'flyers', 'flyer_items', 'stores']; - const missingTables = await checkTablesExist(requiredTables); - - if (missingTables.length > 0) { - return res.status(500).json({ success: false, message: `Database schema check failed. Missing tables: ${missingTables.join(', ')}.` }); - } - return res.status(200).json({ success: true, message: 'All required database tables exist.' }); - } catch (error) { - logger.error('Error during DB schema check:', { error }); - return res.status(500).json({ success: false, message: 'An error occurred while checking the database schema.' }); - } -}); - -router.get('/health/storage', async (req, res) => { - const storagePath = process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/assets'; - try { - await fs.access(storagePath, fs.constants.W_OK); - return res.status(200).json({ success: true, message: `Storage directory '${storagePath}' is accessible and writable.` }); - } catch (error) { - logger.error(`Storage check failed for path: ${storagePath}`, { error }); - return res.status(500).json({ success: false, message: `Storage check failed. Ensure the directory '${storagePath}' exists and is writable by the application.` }); - } -}); - -router.get('/health/db-pool', (req: Request, res: Response) => { - try { - const status = getPoolStatus(); - const isHealthy = status.waitingCount < 5; - const message = `Pool Status: ${status.totalCount} total, ${status.idleCount} idle, ${status.waitingCount} waiting.`; - - if (isHealthy) { - return res.status(200).json({ success: true, message }); - } else { - logger.warn(`Database pool health check shows high waiting count: ${status.waitingCount}`); - return res.status(500).json({ success: false, message: `Pool may be under stress. ${message}` }); - } - } catch (error) { - logger.error('Error during DB pool health check:', { error }); - return res.status(500).json({ success: false, message: 'An error occurred while checking the database pool status.' }); - } -}); - -/** - * GET /api/time - Get the server's current time and week number. - * This is useful for client-side components that need to be aware of the server's time. - */ -router.get('/time', (req: Request, res: Response) => { - const now = new Date(); - const { year, week } = getSimpleWeekAndYear(now); - res.json({ - currentTime: now.toISOString(), - year, - week, - }); -}); // --- Public Data Routes --- -router.get('/flyers', async (req, res, next: NextFunction) => { - try { - // Add pagination support to the flyers endpoint. - const limit = parseInt(req.query.limit as string, 10) || 20; - const offset = parseInt(req.query.offset as string, 10) || 0; - const flyers = await db.flyerRepo.getFlyers(limit, offset); - res.json(flyers); - } catch (error) { - logger.error('Error fetching flyers in /api/flyers:', { error }); - next(error); - } -}); - -router.get('/master-items', async (req, res, next: NextFunction) => { - try { - const masterItems = await db.personalizationRepo.getAllMasterItems(); - res.json(masterItems); - } catch (error) { - logger.error('Error fetching master items in /api/master-items:', { error }); - next(error); - } -}); - -router.get('/flyers/:id/items', async (req, res, next: NextFunction) => { - try { - const flyerId = parseInt(req.params.id, 10); - const items = await db.flyerRepo.getFlyerItems(flyerId); - res.json(items); - } catch (error) { - logger.error('Error fetching flyer items in /api/flyers/:id/items:', { error }); - next(error); - } -}); - -router.post('/flyer-items/batch-fetch', async (req, res, next: NextFunction) => { - const { flyerIds } = req.body; - if (!Array.isArray(flyerIds)) { - return res.status(400).json({ message: 'flyerIds must be an array.' }); - } - try { - const items = await db.flyerRepo.getFlyerItemsForFlyers(flyerIds); - res.json(items); - } catch (error) { - next(error); - } -}); - -router.post('/flyer-items/batch-count', async (req, res, next: NextFunction) => { - const { flyerIds } = req.body; - if (!Array.isArray(flyerIds)) { - return res.status(400).json({ message: 'flyerIds must be an array.' }); - } - try { - const count = await db.flyerRepo.countFlyerItemsForFlyers(flyerIds); - res.json({ count }); - } catch (error) { - next(error); - } -}); - -/** - * POST /api/flyer-items/:itemId/track - Tracks a user interaction with a flyer item. - * This is a "fire-and-forget" endpoint that responds immediately. - * Body: { type: 'view' | 'click' } - */ -router.post('/flyer-items/:itemId/track', (req: Request, res: Response) => { - try { - const itemId = parseInt(req.params.itemId, 10); - const { type } = req.body; - - if (isNaN(itemId)) { - // Silently fail for invalid data to avoid unnecessary client-side errors for a tracking pixel. - return res.status(204).send(); - } - - if (type !== 'view' && type !== 'click') { - return res.status(204).send(); - } - - // Don't await. Respond immediately. - db.flyerRepo.trackFlyerItemInteraction(itemId, type); - res.status(202).send(); - } catch (error) { - logger.error('Error in track interaction endpoint (non-critical):', { error }); - res.status(202).send(); // Always respond positively to not block client. - } -}); - -router.get('/recipes/by-sale-percentage', async (req, res, next: NextFunction) => { - const minPercentageStr = req.query.minPercentage as string || '50.0'; - const minPercentage = parseFloat(minPercentageStr); - - if (isNaN(minPercentage) || minPercentage < 0 || minPercentage > 100) { - return res.status(400).json({ message: 'Query parameter "minPercentage" must be a number between 0 and 100.' }); - } - try { - const recipes = await db.recipeRepo.getRecipesBySalePercentage(minPercentage); - res.json(recipes); - } catch (error) { - next(error); - } -}); - -router.get('/recipes/by-sale-ingredients', async (req, res, next: NextFunction) => { - try { - const minIngredientsStr = req.query.minIngredients as string || '3'; - const minIngredients = parseInt(minIngredientsStr, 10); - - if (isNaN(minIngredients) || minIngredients < 1) { - return res.status(400).json({ message: 'Query parameter "minIngredients" must be a positive integer.' }); - } - const recipes = await db.recipeRepo.getRecipesByMinSaleIngredients(minIngredients); - res.json(recipes); - } catch (error) { - next(error); - } -}); - -router.get('/recipes/by-ingredient-and-tag', async (req, res, next: NextFunction) => { - try { - const { ingredient, tag } = req.query; - if (!ingredient || !tag) { - return res.status(400).json({ message: 'Both "ingredient" and "tag" query parameters are required.' }); - } - const recipes = await db.recipeRepo.findRecipesByIngredientAndTag(ingredient as string, tag as string); - res.json(recipes); - } catch (error) { - next(error); - } -}); - router.get('/stats/most-frequent-sales', async (req, res, next: NextFunction) => { try { const daysStr = req.query.days as string || '30'; @@ -225,32 +29,4 @@ router.get('/stats/most-frequent-sales', async (req, res, next: NextFunction) => } }); -router.get('/recipes/:recipeId/comments', async (req, res, next: NextFunction) => { - try { - const recipeId = parseInt(req.params.recipeId, 10); - const comments = await db.recipeRepo.getRecipeComments(recipeId); - res.json(comments); - } catch (error) { - next(error); - } -}); - -router.get('/dietary-restrictions', async (req, res, next: NextFunction) => { - try { - const restrictions = await db.personalizationRepo.getDietaryRestrictions(); - res.json(restrictions); - } catch (error) { - next(error); - } -}); - -router.get('/appliances', async (req, res, next: NextFunction) => { - try { - const appliances = await db.personalizationRepo.getAppliances(); - res.json(appliances); - } catch (error) { - next(error); - } -}); - export default router; \ No newline at end of file diff --git a/src/routes/recipe.routes.ts b/src/routes/recipe.routes.ts new file mode 100644 index 00000000..acdef52f --- /dev/null +++ b/src/routes/recipe.routes.ts @@ -0,0 +1,72 @@ +// src/routes/recipe.routes.ts +import { Router, type Request, type Response, type NextFunction } from 'express'; +import * as db from '../services/db/index.db'; +import { logger } from '../services/logger.server'; + +const router = Router(); + +/** + * GET /api/recipes/by-sale-percentage - Get recipes based on the percentage of their ingredients on sale. + */ +router.get('/by-sale-percentage', async (req: Request, res: Response, next: NextFunction) => { + const minPercentageStr = req.query.minPercentage as string || '50.0'; + const minPercentage = parseFloat(minPercentageStr); + + if (isNaN(minPercentage) || minPercentage < 0 || minPercentage > 100) { + return res.status(400).json({ message: 'Query parameter "minPercentage" must be a number between 0 and 100.' }); + } + try { + const recipes = await db.recipeRepo.getRecipesBySalePercentage(minPercentage); + res.json(recipes); + } catch (error) { + next(error); + } +}); + +/** + * GET /api/recipes/by-sale-ingredients - Get recipes by the minimum number of sale ingredients. + */ +router.get('/by-sale-ingredients', async (req: Request, res: Response, next: NextFunction) => { + try { + const minIngredientsStr = req.query.minIngredients as string || '3'; + const minIngredients = parseInt(minIngredientsStr, 10); + + if (isNaN(minIngredients) || minIngredients < 1) { + return res.status(400).json({ message: 'Query parameter "minIngredients" must be a positive integer.' }); + } + const recipes = await db.recipeRepo.getRecipesByMinSaleIngredients(minIngredients); + res.json(recipes); + } catch (error) { + next(error); + } +}); + +/** + * GET /api/recipes/by-ingredient-and-tag - Find recipes by a specific ingredient and tag. + */ +router.get('/by-ingredient-and-tag', async (req: Request, res: Response, next: NextFunction) => { + try { + const { ingredient, tag } = req.query; + if (!ingredient || !tag) { + return res.status(400).json({ message: 'Both "ingredient" and "tag" query parameters are required.' }); + } + const recipes = await db.recipeRepo.findRecipesByIngredientAndTag(ingredient as string, tag as string); + res.json(recipes); + } catch (error) { + next(error); + } +}); + + +router.get('/:recipeId/comments', async (req, res, next: NextFunction) => { + try { + const recipeId = parseInt(req.params.recipeId, 10); + const comments = await db.recipeRepo.getRecipeComments(recipeId); + res.json(comments); + } catch (error) { + next(error); + } +}); + + +export default router; \ No newline at end of file