remove routes from 'public.routes'
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 6m11s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 6m11s
This commit is contained in:
78
src/routes/flyer.routes.ts
Normal file
78
src/routes/flyer.routes.ts
Normal 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;
|
||||
@@ -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;
|
||||
45
src/routes/personalization.routes.ts
Normal file
45
src/routes/personalization.routes.ts
Normal 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;
|
||||
@@ -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;
|
||||
72
src/routes/recipe.routes.ts
Normal file
72
src/routes/recipe.routes.ts
Normal 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;
|
||||
Reference in New Issue
Block a user