refactor: simplify async route handlers by making 'next' optional

This commit is contained in:
2025-12-04 10:00:54 -08:00
parent aac04353a3
commit 8cd61a8646
12 changed files with 120 additions and 73 deletions

View File

@@ -1,4 +1,5 @@
// src/pages/admin/components/SystemCheck.tsx
/// <reference types="vite/client" />
import React, { useState, useEffect, useCallback } from 'react';
import toast from 'react-hot-toast';
import { loginUser, pingBackend, checkDbSchema, checkStorage, checkDbPoolHealth, checkPm2Status, checkRedisHealth, triggerFailingJob, clearGeocodeCache } from '../../../services/apiClient';

View File

@@ -7,6 +7,7 @@ import multer from 'multer';
import * as db from '../services/db';
import { logger } from '../services/logger.server';
import { UserProfile } from '../types';
import { AsyncRequestHandler } from '../types/express';
import { clearGeocodeCache } from '../services/geocodingService.server';
// --- Bull Board (Job Queue UI) Imports ---
@@ -55,27 +56,27 @@ router.use(passport.authenticate('jwt', { session: false }), isAdmin);
// --- Admin Routes ---
router.get('/corrections', async (req: Request, res: Response, next: NextFunction) => {
router.get('/corrections', async (req, res, next) => {
const corrections = await db.getSuggestedCorrections();
res.json(corrections);
});
router.get('/brands', async (req: Request, res: Response, next: NextFunction) => {
router.get('/brands', async (req, res, next) => {
const brands = await db.getAllBrands();
res.json(brands);
});
router.get('/stats', async (req: Request, res: Response, next: NextFunction) => {
router.get('/stats', async (req, res, next) => {
const stats = await db.getApplicationStats();
res.json(stats);
});
router.get('/stats/daily', async (req: Request, res: Response, next: NextFunction) => {
router.get('/stats/daily', async (req, res, next) => {
const dailyStats = await db.getDailyStatsForLast30Days();
res.json(dailyStats);
});
router.post('/corrections/:id/approve', async (req: Request, res: Response, next: NextFunction) => {
router.post('/corrections/:id/approve', async (req, res, next) => {
const correctionId = parseInt(req.params.id, 10);
// Add validation to ensure the ID is a valid number.
if (isNaN(correctionId)) {
@@ -86,13 +87,13 @@ router.post('/corrections/:id/approve', async (req: Request, res: Response, next
res.status(200).json({ message: 'Correction approved successfully.' });
});
router.post('/corrections/:id/reject', async (req: Request, res: Response, next: NextFunction) => {
router.post('/corrections/:id/reject', async (req, res, next) => {
const correctionId = parseInt(req.params.id, 10);
await db.rejectCorrection(correctionId);
res.status(200).json({ message: 'Correction rejected successfully.' });
});
router.put('/corrections/:id', async (req: Request, res: Response, next: NextFunction) => {
router.put('/corrections/:id', async (req, res, next) => {
const correctionId = parseInt(req.params.id, 10);
const { suggested_value } = req.body;
if (!suggested_value) {
@@ -110,7 +111,7 @@ router.put('/corrections/:id', async (req: Request, res: Response, next: NextFun
}
});
router.put('/recipes/:id/status', async (req: Request, res: Response, next: NextFunction) => {
router.put('/recipes/:id/status', async (req, res, next) => {
const recipeId = parseInt(req.params.id, 10);
const { status } = req.body;
@@ -121,7 +122,7 @@ router.put('/recipes/:id/status', async (req: Request, res: Response, next: Next
res.status(200).json(updatedRecipe);
});
router.post('/brands/:id/logo', upload.single('logoImage'), async (req: Request, res: Response, next: NextFunction) => {
router.post('/brands/:id/logo', upload.single('logoImage'), async (req, res, next) => {
const brandId = parseInt(req.params.id, 10);
if (!req.file) {
return res.status(400).json({ message: 'Logo image file is required.' });
@@ -134,12 +135,12 @@ router.post('/brands/:id/logo', upload.single('logoImage'), async (req: Request,
res.status(200).json({ message: 'Brand logo updated successfully.', logoUrl });
});
router.get('/unmatched-items', async (req: Request, res: Response, next: NextFunction) => {
router.get('/unmatched-items', async (req, res, next) => {
const items = await db.getUnmatchedFlyerItems();
res.json(items);
});
router.put('/comments/:id/status', async (req: Request, res: Response, next: NextFunction) => {
router.put('/comments/:id/status', async (req, res, next) => {
const commentId = parseInt(req.params.id, 10);
const { status } = req.body;
@@ -150,19 +151,19 @@ router.put('/comments/:id/status', async (req: Request, res: Response, next: Nex
res.status(200).json(updatedComment);
});
router.get('/users', async (req: Request, res: Response, next: NextFunction) => {
router.get('/users', async (req, res, next) => {
const users = await db.getAllUsers();
res.json(users);
});
router.get('/activity-log', async (req: Request, res: Response, next: NextFunction) => {
router.get('/activity-log', async (req, res, next) => {
const limit = parseInt(req.query.limit as string, 10) || 50;
const offset = parseInt(req.query.offset as string, 10) || 0;
const logs = await db.getActivityLog(limit, offset);
res.json(logs);
});
router.get('/users/:id', async (req: Request, res: Response, next: NextFunction) => {
router.get('/users/:id', async (req, res, next) => {
const user = await db.findUserProfileById(req.params.id);
if (!user) {
return res.status(404).json({ message: 'User not found.' });
@@ -170,7 +171,7 @@ router.get('/users/:id', async (req: Request, res: Response, next: NextFunction)
res.json(user);
});
router.put('/users/:id', async (req: Request, res: Response, next: NextFunction) => {
router.put('/users/:id', async (req, res, next) => {
const { role } = req.body;
if (!role || !['user', 'admin'].includes(role)) {
return res.status(400).json({ message: 'A valid role ("user" or "admin") is required.' });
@@ -189,7 +190,7 @@ router.put('/users/:id', async (req: Request, res: Response, next: NextFunction)
}
});
router.delete('/users/:id', async (req: Request, res: Response, next: NextFunction) => {
router.delete('/users/:id', async (req, res, next) => {
const adminUser = req.user as UserProfile;
if (adminUser.user.user_id === req.params.id) {
return res.status(400).json({ message: 'Admins cannot delete their own account.' });
@@ -202,7 +203,7 @@ router.delete('/users/:id', async (req: Request, res: Response, next: NextFuncti
* POST /api/admin/trigger/daily-deal-check - Manually trigger the daily deal check job.
* This is useful for testing or forcing an update without waiting for the cron schedule.
*/
router.post('/trigger/daily-deal-check', async (req: Request, res: Response, next: NextFunction) => {
router.post('/trigger/daily-deal-check', async (req, res, next) => {
const adminUser = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for daily deal check received from user: ${adminUser.user_id}`);
@@ -221,7 +222,7 @@ router.post('/trigger/daily-deal-check', async (req: Request, res: Response, nex
* POST /api/admin/trigger/analytics-report - Manually enqueue a job to generate the daily analytics report.
* This is useful for testing or re-generating a report without waiting for the cron schedule.
*/
router.post('/trigger/analytics-report', async (req: Request, res: Response, next: NextFunction) => {
router.post('/trigger/analytics-report', async (req, res, next) => {
const adminUser = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for analytics report generation received from user: ${adminUser.user_id}`);
@@ -243,7 +244,7 @@ router.post('/trigger/analytics-report', async (req: Request, res: Response, nex
* POST /api/admin/flyers/:flyerId/cleanup - Enqueue a job to clean up a flyer's files.
* This is triggered by an admin after they have verified the flyer processing was successful.
*/
router.post('/flyers/:flyerId/cleanup', async (req: Request, res: Response, next: NextFunction) => {
router.post('/flyers/:flyerId/cleanup', async (req, res, next) => {
const adminUser = req.user as UserProfile;
const flyerId = parseInt(req.params.flyerId, 10);
@@ -262,7 +263,7 @@ router.post('/flyers/:flyerId/cleanup', async (req: Request, res: Response, next
* POST /api/admin/trigger/failing-job - Enqueue a test job designed to fail.
* This is for testing the retry mechanism and Bull Board UI.
*/
router.post('/trigger/failing-job', async (req: Request, res: Response, next: NextFunction) => {
router.post('/trigger/failing-job', async (req, res, next) => {
const adminUser = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for a failing job received from user: ${adminUser.user_id}`);
@@ -279,7 +280,7 @@ router.post('/trigger/failing-job', async (req: Request, res: Response, next: Ne
* POST /api/admin/system/clear-geocode-cache - Clears the Redis cache for geocoded addresses.
* Requires admin privileges.
*/
router.post('/system/clear-geocode-cache', async (req: Request, res: Response, next: NextFunction) => {
router.post('/system/clear-geocode-cache', async (req, res, next) => {
const adminUser = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for geocode cache clear received from user: ${adminUser.user_id}`);

View File

@@ -10,6 +10,7 @@ import * as aiService from '../services/aiService.server'; // Correctly import s
import { generateFlyerIcon } from '../utils/imageProcessor';
import { logger } from '../services/logger.server';
import { UserProfile } from '../types';
import { AsyncRequestHandler } from '../types/express';
import { flyerQueue } from '../services/queueService.server';
const router = Router();
@@ -68,7 +69,7 @@ router.use((req: Request, res: Response, next: NextFunction) => {
* NEW ENDPOINT: Accepts a single flyer file (PDF or image), enqueues it for
* background processing, and immediately returns a job ID.
*/
router.post('/upload-and-process', optionalAuth, uploadToDisk.single('flyerFile'), async (req: Request, res: Response, next: NextFunction) => {
router.post('/upload-and-process', optionalAuth, uploadToDisk.single('flyerFile'), async (req, res, next) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'A flyer file (PDF or image) is required.' });
@@ -153,7 +154,7 @@ router.get('/jobs/:jobId/status', async (req, res) => {
* in the flyer upload workflow after the AI has extracted the data.
* It uses `optionalAuth` to handle submissions from both anonymous and authenticated users.
*/
router.post('/flyers/process', optionalAuth, uploadToDisk.single('flyerImage'), async (req: Request, res: Response, next: NextFunction) => {
router.post('/flyers/process', optionalAuth, uploadToDisk.single('flyerImage'), async (req, res, next) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'Flyer image file is required.' });
@@ -389,7 +390,7 @@ router.post(
'/rescan-area',
passport.authenticate('jwt', { session: false }),
uploadToDisk.single('image'),
async (req: Request, res: Response, next: NextFunction) => {
async (req, res, next) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'Image file is required.' });

View File

@@ -10,6 +10,7 @@ import passport from './passport';
import * as db from '../services/db';
import { getPool } from '../services/db/connection';
import { logger } from '../services/logger.server';
import { AsyncRequestHandler } from '../types/express';
import { sendPasswordResetEmail } from '../services/emailService.server';
const router = Router();
@@ -41,7 +42,7 @@ const resetPasswordLimiter = rateLimit({
// --- Authentication Routes ---
// Registration Route
router.post('/register', async (req: Request, res: Response, next: NextFunction) => {
router.post('/register', async (req, res, next) => {
const { email, password, full_name, avatar_url } = req.body;
if (!email || !password) {
@@ -146,7 +147,7 @@ router.post('/login', (req: Request, res: Response, next: NextFunction) => {
});
// Route to request a password reset
router.post('/forgot-password', forgotPasswordLimiter, async (req: Request, res: Response, next: NextFunction) => {
router.post('/forgot-password', forgotPasswordLimiter, async (req, res, next) => {
const { email } = req.body;
if (!email) {
return res.status(400).json({ message: 'Email is required.' });
@@ -189,7 +190,7 @@ router.post('/forgot-password', forgotPasswordLimiter, async (req: Request, res:
});
// Route to reset the password using a token
router.post('/reset-password', resetPasswordLimiter, async (req: Request, res: Response, next: NextFunction) => {
router.post('/reset-password', resetPasswordLimiter, async (req, res, next) => {
const { token, newPassword } = req.body;
if (!token || !newPassword) {
return res.status(400).json({ message: 'Token and new password are required.' });

View File

@@ -10,6 +10,7 @@ import {
} from '../services/db';
import { logger } from '../services/logger.server';
import { UserProfile } from '../types';
import { AsyncRequestHandler } from '../types/express';
const router = express.Router();
@@ -19,7 +20,7 @@ router.use(passport.authenticate('jwt', { session: false }));
/**
* GET /api/budgets - Get all budgets for the authenticated user.
*/
router.get('/', async (req: Request, res: Response, next: NextFunction) => {
router.get('/', async (req, res, next) => {
const user = req.user as UserProfile;
try {
const budgets = await getBudgetsForUser(user.user_id);
@@ -33,7 +34,7 @@ router.get('/', async (req: Request, res: Response, next: NextFunction) => {
/**
* POST /api/budgets - Create a new budget for the authenticated user.
*/
router.post('/', async (req: Request, res: Response, next: NextFunction) => {
router.post('/', async (req, res, next) => {
const user = req.user as UserProfile;
try {
const newBudget = await createBudget(user.user_id, req.body);
@@ -47,7 +48,7 @@ router.post('/', async (req: Request, res: Response, next: NextFunction) => {
/**
* PUT /api/budgets/:id - Update an existing budget.
*/
router.put('/:id', async (req: Request, res: Response, next: NextFunction) => {
router.put('/:id', async (req, res, next) => {
const user = req.user as UserProfile;
const budgetId = parseInt(req.params.id, 10);
try {
@@ -62,7 +63,7 @@ router.put('/:id', async (req: Request, res: Response, next: NextFunction) => {
/**
* DELETE /api/budgets/:id - Delete a budget.
*/
router.delete('/:id', async (req: Request, res: Response, next: NextFunction) => {
router.delete('/:id', async (req, res, next) => {
const user = req.user as UserProfile;
const budgetId = parseInt(req.params.id, 10);
try {
@@ -78,7 +79,7 @@ router.delete('/:id', async (req: Request, res: Response, next: NextFunction) =>
* GET /api/spending-analysis - Get spending breakdown by category for a date range.
* Query params: startDate (YYYY-MM-DD), endDate (YYYY-MM-DD)
*/
router.get('/spending-analysis', async (req: Request, res: Response, next: NextFunction) => {
router.get('/spending-analysis', async (req, res, next) => {
const user = req.user as UserProfile;
const { startDate, endDate } = req.query;

View File

@@ -4,6 +4,7 @@ import passport, { isAdmin } from './passport';
import { getAllAchievements, getUserAchievements, awardAchievement, getLeaderboard } from '../services/db';
import { logger } from '../services/logger.server';
import { UserProfile } from '../types';
import { AsyncRequestHandler } from '../types/express';
const router = express.Router();
@@ -11,7 +12,7 @@ const router = express.Router();
* GET /api/achievements - Get the master list of all available achievements.
* This is a public endpoint.
*/
router.get('/', async (req: Request, res: Response, next: NextFunction) => {
router.get('/', async (req, res, next) => {
try {
const achievements = await getAllAchievements();
res.json(achievements);
@@ -25,7 +26,7 @@ router.get('/', async (req: Request, res: Response, next: NextFunction) => {
* GET /api/achievements/leaderboard - Get the top users by points.
* This is a public endpoint.
*/
router.get('/leaderboard', async (req: Request, res: Response, next: NextFunction) => {
router.get('/leaderboard', async (req, res, next) => {
// Allow client to specify a limit, but default to 10 and cap it at 50.
const limit = Math.min(parseInt(req.query.limit as string, 10) || 10, 50);
@@ -45,7 +46,7 @@ router.get('/leaderboard', async (req: Request, res: Response, next: NextFunctio
router.get(
'/me',
passport.authenticate('jwt', { session: false }),
async (req: Request, res: Response, next: NextFunction) => {
async (req, res, next) => {
const user = req.user as UserProfile;
try {
const userAchievements = await getUserAchievements(user.user_id);
@@ -65,7 +66,7 @@ router.post(
'/award',
passport.authenticate('jwt', { session: false }),
isAdmin,
async (req: Request, res: Response, next: NextFunction) => {
async (req, res, next) => {
const { userId, achievementName } = req.body;
if (!userId || !achievementName) {

View File

@@ -4,6 +4,7 @@ import * as db from '../services/db';
import { logger } from '../services/logger.server';
import fs from 'fs/promises';
import passport from 'passport';
import { AsyncRequestHandler } from '../types/express';
const router = Router();
@@ -12,7 +13,7 @@ router.get('/health/ping', (req: Request, res: Response) => {
res.status(200).send('pong');
});
router.get('/health/db-schema', async (req: Request, res: Response) => {
router.get('/health/db-schema', async (req, res) => {
try {
const requiredTables = ['users', 'profiles', 'flyers', 'flyer_items', 'stores'];
const missingTables = await db.checkTablesExist(requiredTables);
@@ -27,7 +28,7 @@ router.get('/health/db-schema', async (req: Request, res: Response) => {
}
});
router.get('/health/storage', async (req: Request, res: Response) => {
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);
@@ -58,7 +59,7 @@ router.get('/health/db-pool', (req: Request, res: Response) => {
// --- Public Data Routes ---
router.get('/flyers', async (req: Request, res: Response, next: NextFunction) => {
router.get('/flyers', async (req, res, next) => {
try {
const flyers = await db.getFlyers();
res.json(flyers);
@@ -68,7 +69,7 @@ router.get('/flyers', async (req: Request, res: Response, next: NextFunction) =>
}
});
router.get('/master-items', async (req: Request, res: Response, next: NextFunction) => {
router.get('/master-items', async (req, res, next) => {
try {
const masterItems = await db.getAllMasterItems();
res.json(masterItems);
@@ -78,7 +79,7 @@ router.get('/master-items', async (req: Request, res: Response, next: NextFuncti
}
});
router.get('/flyers/:id/items', async (req: Request, res: Response, next: NextFunction) => {
router.get('/flyers/:id/items', async (req, res, next) => {
try {
const flyerId = parseInt(req.params.id, 10);
const items = await db.getFlyerItems(flyerId);
@@ -89,7 +90,7 @@ router.get('/flyers/:id/items', async (req: Request, res: Response, next: NextFu
}
});
router.post('/flyer-items/batch-fetch', async (req: Request, res: Response, next: NextFunction) => {
router.post('/flyer-items/batch-fetch', async (req, res, next) => {
const { flyerIds } = req.body;
if (!Array.isArray(flyerIds)) {
return res.status(400).json({ message: 'flyerIds must be an array.' });
@@ -102,7 +103,7 @@ router.post('/flyer-items/batch-fetch', async (req: Request, res: Response, next
}
});
router.post('/flyer-items/batch-count', async (req: Request, res: Response, next: NextFunction) => {
router.post('/flyer-items/batch-count', async (req, res, next) => {
const { flyerIds } = req.body;
if (!Array.isArray(flyerIds)) {
return res.status(400).json({ message: 'flyerIds must be an array.' });
@@ -115,7 +116,7 @@ router.post('/flyer-items/batch-count', async (req: Request, res: Response, next
}
});
router.get('/recipes/by-sale-percentage', async (req: Request, res: Response, next: NextFunction) => {
router.get('/recipes/by-sale-percentage', async (req, res, next) => {
const minPercentageStr = req.query.minPercentage as string || '50.0';
const minPercentage = parseFloat(minPercentageStr);
@@ -130,7 +131,7 @@ router.get('/recipes/by-sale-percentage', async (req: Request, res: Response, ne
}
});
router.get('/recipes/by-sale-ingredients', async (req: Request, res: Response, next: NextFunction) => {
router.get('/recipes/by-sale-ingredients', async (req, res, next) => {
const minIngredientsStr = req.query.minIngredients as string || '3';
const minIngredients = parseInt(minIngredientsStr, 10);
@@ -141,7 +142,7 @@ router.get('/recipes/by-sale-ingredients', async (req: Request, res: Response, n
res.json(recipes);
});
router.get('/recipes/by-ingredient-and-tag', async (req: Request, res: Response, next: NextFunction) => {
router.get('/recipes/by-ingredient-and-tag', async (req, res, next) => {
const { ingredient, tag } = req.query;
if (!ingredient || !tag) {
return res.status(400).json({ message: 'Both "ingredient" and "tag" query parameters are required.' });
@@ -150,7 +151,7 @@ router.get('/recipes/by-ingredient-and-tag', async (req: Request, res: Response,
res.json(recipes);
});
router.get('/stats/most-frequent-sales', async (req: Request, res: Response, next: NextFunction) => {
router.get('/stats/most-frequent-sales', async (req, res, next) => {
const daysStr = req.query.days as string || '30';
const limitStr = req.query.limit as string || '10';
@@ -168,18 +169,18 @@ router.get('/stats/most-frequent-sales', async (req: Request, res: Response, nex
res.json(items);
});
router.get('/recipes/:recipeId/comments', async (req: Request, res: Response, next: NextFunction) => {
router.get('/recipes/:recipeId/comments', async (req, res, next) => {
const recipeId = parseInt(req.params.recipeId, 10);
const comments = await db.getRecipeComments(recipeId);
res.json(comments);
});
router.get('/dietary-restrictions', async (req: Request, res: Response, next: NextFunction) => {
router.get('/dietary-restrictions', async (req, res, next) => {
const restrictions = await db.getDietaryRestrictions();
res.json(restrictions);
});
router.get('/appliances', async (req: Request, res: Response, next: NextFunction) => {
router.get('/appliances', async (req, res, next) => {
const appliances = await db.getAppliances();
res.json(appliances);
});

View File

@@ -9,6 +9,7 @@ import zxcvbn from 'zxcvbn';
import * as db from '../services/db';
import { logger } from '../services/logger.server';
import { User, UserProfile, Address } from '../types';
import { AsyncRequestHandler } from '../types/express';
const router = express.Router();
@@ -27,7 +28,7 @@ fs.mkdir(avatarUploadDir, { recursive: true }).catch(err => {
*/
router.post(
'/profile/avatar',
async (req: Request, res: Response, next: NextFunction) => {
async (req, res, next) => {
// Initialize multer inside the route handler where req.user is available.
// This prevents the "Cannot read properties of undefined" error during test setup.
const storage = multer.diskStorage({
@@ -120,7 +121,7 @@ router.post(
/**
* GET /api/users/profile - Get the full profile for the authenticated user.
*/
router.get('/profile', async (req: Request, res: Response, next: NextFunction) => {
router.get('/profile', async (req, res, next) => {
logger.debug(`[ROUTE] GET /api/users/profile - ENTER`);
const user = req.user as UserProfile;
try {
@@ -140,7 +141,7 @@ router.get('/profile', async (req: Request, res: Response, next: NextFunction) =
/**
* PUT /api/users/profile - Update the user's profile information.
*/
router.put('/profile', async (req: Request, res: Response, next: NextFunction) => {
router.put('/profile', async (req, res, next) => {
logger.debug(`[ROUTE] PUT /api/users/profile - ENTER`);
const user = req.user as UserProfile;
const { full_name, avatar_url } = req.body;
@@ -161,7 +162,7 @@ router.put('/profile', async (req: Request, res: Response, next: NextFunction) =
/**
* PUT /api/users/profile/password - Update the user's password.
*/
router.put('/profile/password', async (req: Request, res: Response, next: NextFunction) => {
router.put('/profile/password', async (req, res, next) => {
logger.debug(`[ROUTE] PUT /api/users/profile/password - ENTER`);
const user = req.user as UserProfile;
const { newPassword } = req.body;
@@ -191,7 +192,7 @@ router.put('/profile/password', async (req: Request, res: Response, next: NextFu
/**
* DELETE /api/users/account - Delete the user's own account.
*/
router.delete('/account', async (req: Request, res: Response, next: NextFunction) => {
router.delete('/account', async (req, res, next) => {
logger.debug(`[ROUTE] DELETE /api/users/account - ENTER`);
const user = req.user as UserProfile;
const { password } = req.body;
@@ -222,7 +223,7 @@ router.delete('/account', async (req: Request, res: Response, next: NextFunction
/**
* GET /api/users/watched-items - Get all watched items for the authenticated user.
*/
router.get('/watched-items', async (req: Request, res: Response, next: NextFunction) => {
router.get('/watched-items', async (req, res, next) => {
logger.debug(`[ROUTE] GET /api/users/watched-items - ENTER`);
const user = req.user as UserProfile;
try {
@@ -237,7 +238,7 @@ router.get('/watched-items', async (req: Request, res: Response, next: NextFunct
/**
* POST /api/users/watched-items - Add a new item to the user's watchlist.
*/
router.post('/watched-items', async (req: Request, res: Response, next: NextFunction) => {
router.post('/watched-items', async (req, res, next) => {
logger.debug(`[ROUTE] POST /api/users/watched-items - ENTER`);
const user = req.user as UserProfile;
const { itemName, category } = req.body;
@@ -253,7 +254,7 @@ router.post('/watched-items', async (req: Request, res: Response, next: NextFunc
/**
* DELETE /api/users/watched-items/:masterItemId - Remove an item from the watchlist.
*/
router.delete('/watched-items/:masterItemId', async (req: Request, res: Response, next: NextFunction) => {
router.delete('/watched-items/:masterItemId', async (req, res, next) => {
logger.debug(`[ROUTE] DELETE /api/users/watched-items/:masterItemId - ENTER`);
const user = req.user as UserProfile;
const masterItemId = parseInt(req.params.masterItemId, 10);
@@ -269,7 +270,7 @@ router.delete('/watched-items/:masterItemId', async (req: Request, res: Response
/**
* GET /api/users/shopping-lists - Get all shopping lists for the user.
*/
router.get('/shopping-lists', async (req: Request, res: Response, next: NextFunction) => {
router.get('/shopping-lists', async (req, res, next) => {
logger.debug(`[ROUTE] GET /api/users/shopping-lists - ENTER`);
const user = req.user as UserProfile;
try {
@@ -284,7 +285,7 @@ router.get('/shopping-lists', async (req: Request, res: Response, next: NextFunc
/**
* POST /api/users/shopping-lists - Create a new shopping list.
*/
router.post('/shopping-lists', async (req: Request, res: Response, next: NextFunction) => {
router.post('/shopping-lists', async (req, res, next) => {
logger.debug(`[ROUTE] POST /api/users/shopping-lists - ENTER`);
const user = req.user as UserProfile;
const { name } = req.body;
@@ -300,7 +301,7 @@ router.post('/shopping-lists', async (req: Request, res: Response, next: NextFun
/**
* DELETE /api/users/shopping-lists/:listId - Delete a shopping list.
*/
router.delete('/shopping-lists/:listId', async (req: Request, res: Response, next: NextFunction) => {
router.delete('/shopping-lists/:listId', async (req, res, next) => {
logger.debug(`[ROUTE] DELETE /api/users/shopping-lists/:listId - ENTER`);
const user = req.user as UserProfile;
const listId = parseInt(req.params.listId, 10);
@@ -316,7 +317,7 @@ router.delete('/shopping-lists/:listId', async (req: Request, res: Response, nex
/**
* POST /api/users/shopping-lists/:listId/items - Add an item to a shopping list.
*/
router.post('/shopping-lists/:listId/items', async (req: Request, res: Response, next: NextFunction) => {
router.post('/shopping-lists/:listId/items', async (req, res, next) => {
logger.debug(`[ROUTE] POST /api/users/shopping-lists/:listId/items - ENTER`);
const listId = parseInt(req.params.listId, 10);
try {
@@ -331,7 +332,7 @@ router.post('/shopping-lists/:listId/items', async (req: Request, res: Response,
/**
* PUT /api/users/shopping-lists/items/:itemId - Update a shopping list item.
*/
router.put('/shopping-lists/items/:itemId', async (req: Request, res: Response, next: NextFunction) => {
router.put('/shopping-lists/items/:itemId', async (req, res, next) => {
logger.debug(`[ROUTE] PUT /api/users/shopping-lists/items/:itemId - ENTER`);
const itemId = parseInt(req.params.itemId, 10);
try {
@@ -346,7 +347,7 @@ router.put('/shopping-lists/items/:itemId', async (req: Request, res: Response,
/**
* DELETE /api/users/shopping-lists/items/:itemId - Remove an item from a shopping list.
*/
router.delete('/shopping-lists/items/:itemId', async (req: Request, res: Response, next: NextFunction) => {
router.delete('/shopping-lists/items/:itemId', async (req, res, next) => {
logger.debug(`[ROUTE] DELETE /api/users/shopping-lists/items/:itemId - ENTER`);
const itemId = parseInt(req.params.itemId, 10);
try {
@@ -361,7 +362,7 @@ router.delete('/shopping-lists/items/:itemId', async (req: Request, res: Respons
/**
* PUT /api/users/profile/preferences - Update user preferences.
*/
router.put('/profile/preferences', async (req: Request, res: Response, next: NextFunction) => {
router.put('/profile/preferences', async (req, res, next) => {
logger.debug(`[ROUTE] PUT /api/users/profile/preferences - ENTER`);
const user = req.user as UserProfile;
if (typeof req.body !== 'object' || req.body === null || Array.isArray(req.body)) {
@@ -376,7 +377,7 @@ router.put('/profile/preferences', async (req: Request, res: Response, next: Nex
}
});
router.get('/me/dietary-restrictions', async (req: Request, res: Response, next: NextFunction) => {
router.get('/me/dietary-restrictions', async (req, res, next) => {
logger.debug(`[ROUTE] GET /api/users/me/dietary-restrictions - ENTER`);
const user = req.user as UserProfile;
try {
@@ -388,7 +389,7 @@ router.get('/me/dietary-restrictions', async (req: Request, res: Response, next:
}
});
router.put('/me/dietary-restrictions', async (req: Request, res: Response, next: NextFunction) => {
router.put('/me/dietary-restrictions', async (req, res, next) => {
logger.debug(`[ROUTE] PUT /api/users/me/dietary-restrictions - ENTER`);
const user = req.user as UserProfile;
const { restrictionIds } = req.body;
@@ -401,7 +402,7 @@ router.put('/me/dietary-restrictions', async (req: Request, res: Response, next:
}
});
router.get('/me/appliances', async (req: Request, res: Response, next: NextFunction) => {
router.get('/me/appliances', async (req, res, next) => {
logger.debug(`[ROUTE] GET /api/users/me/appliances - ENTER`);
const user = req.user as UserProfile;
try {
@@ -413,7 +414,7 @@ router.get('/me/appliances', async (req: Request, res: Response, next: NextFunct
}
});
router.put('/me/appliances', async (req: Request, res: Response, next: NextFunction) => {
router.put('/me/appliances', async (req, res, next) => {
logger.debug(`[ROUTE] PUT /api/users/me/appliances - ENTER`);
const user = req.user as UserProfile;
const { applianceIds } = req.body;
@@ -430,7 +431,7 @@ router.put('/me/appliances', async (req: Request, res: Response, next: NextFunct
* GET /api/users/addresses/:addressId - Get a specific address by its ID.
* This is protected to ensure a user can only fetch their own address details.
*/
router.get('/addresses/:addressId', async (req: Request, res: Response, next: NextFunction) => {
router.get('/addresses/:addressId', async (req, res, next) => {
const user = req.user as UserProfile;
const addressId = parseInt(req.params.addressId, 10);
@@ -453,7 +454,7 @@ router.get('/addresses/:addressId', async (req: Request, res: Response, next: Ne
/**
* PUT /api/users/profile/address - Create or update the user's primary address.
*/
router.put('/profile/address', async (req: Request, res: Response, next: NextFunction) => {
router.put('/profile/address', async (req, res, next) => {
const user = req.user as UserProfile;
const addressData = req.body as Partial<Address>;

View File

@@ -25,8 +25,6 @@ describe('DB Connection Service', () => {
if (typeof pg.Pool === 'function') {
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const instance = new pg.Pool();
console.log('[DEBUG] connection.test.ts: new pg.Pool() SUCCESS. Instance:', Object.keys(instance));
} catch (e) {

20
src/services/express.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
// src/types/express.d.ts
import { Request, Response, NextFunction, RequestHandler } from 'express';
/**
* Defines a more accurate type for asynchronous Express route handlers.
* Standard `RequestHandler` requires `next` to be present, which is often unused
* in modern async/await handlers, leading to linting errors. This type makes `next` optional.
*
* It also allows for more specific typing of the request body, query, and params.
*/
export type AsyncRequestHandler<
P = Record<string, string>,
ResBody = any,
ReqBody = any,
ReqQuery = qs.ParsedQs
> = (
req: Request<P, ResBody, ReqBody, ReqQuery>,
res: Response<ResBody>,
next?: NextFunction
) => Promise<void | unknown>;

20
src/types/express.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
// src/types/express.d.ts
import { Request, Response, NextFunction, RequestHandler } from 'express';
/**
* Defines a more accurate type for asynchronous Express route handlers.
* Standard `RequestHandler` requires `next` to be present, which is often unused
* in modern async/await handlers, leading to linting errors. This type makes `next` optional.
*
* It also allows for more specific typing of the request body, query, and params.
*/
export type AsyncRequestHandler<
P = Record<string, string>,
ResBody = any,
ReqBody = any,
ReqQuery = qs.ParsedQs
> = (
req: Request<P, ResBody, ReqBody, ReqQuery>,
res: Response<ResBody>,
next?: NextFunction
) => Promise<void | unknown>;