remove routes from 'public.routes'
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 6m11s

This commit is contained in:
2025-12-09 20:06:56 -08:00
parent a30ec029d4
commit e16a519aa2
6 changed files with 266 additions and 246 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;