refactor: enhance type safety by adding NextFunction type to async route handlers
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m26s

This commit is contained in:
2025-12-04 13:13:34 -08:00
parent 5c214fb6f4
commit 8604be4720
7 changed files with 37 additions and 36 deletions

View File

@@ -1,5 +1,5 @@
// src/routes/admin.ts
import { Router } from 'express';
import { Router, NextFunction } from 'express';
import passport from './passport';
import { isAdmin } from './passport'; // Correctly imported
import multer from 'multer';
@@ -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 ---
@@ -92,7 +93,7 @@ router.post('/corrections/:id/reject', async (req, res) => {
res.status(200).json({ message: 'Correction rejected successfully.' });
});
router.put('/corrections/:id', async (req, res, next) => {
router.put('/corrections/:id', async (req, res, next: NextFunction) => {
const correctionId = parseInt(req.params.id, 10);
const { suggested_value } = req.body;
if (!suggested_value) {
@@ -170,7 +171,7 @@ router.get('/users/:id', async (req, res) => {
res.json(user);
});
router.put('/users/:id', async (req, res, next) => {
router.put('/users/:id', async (req, res, next: NextFunction) => {
const { role } = req.body;
if (!role || !['user', 'admin'].includes(role)) {
return res.status(400).json({ message: 'A valid role ("user" or "admin") is required.' });
@@ -202,7 +203,7 @@ router.delete('/users/:id', async (req, res) => {
* 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, res, next) => {
router.post('/trigger/daily-deal-check', async (req, res, next: NextFunction) => {
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, res, next) => {
* 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, res, next) => {
router.post('/trigger/analytics-report', async (req, res, next: NextFunction) => {
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, res, next) => {
* 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, res, next) => {
router.post('/flyers/:flyerId/cleanup', async (req, res, next: NextFunction) => {
const adminUser = req.user as UserProfile;
const flyerId = parseInt(req.params.flyerId, 10);
@@ -262,7 +263,7 @@ router.post('/flyers/:flyerId/cleanup', async (req, res, 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, res, next) => {
router.post('/trigger/failing-job', async (req, res, next: NextFunction) => {
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, res, next) => {
* 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, res, next) => {
router.post('/system/clear-geocode-cache', async (req, res, next: NextFunction) => {
const adminUser = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for geocode cache clear received from user: ${adminUser.user_id}`);

View File

@@ -10,7 +10,6 @@ import * as db from '../services/db';
// Mock the AI service to avoid making real AI calls
vi.mock('../services/aiService.server');
const mockedAiService = aiService as Mocked<typeof aiService>;
// Mock the entire db service, as the /flyers/process route uses it.
vi.mock('../services/db'); // Keep this mock, as db is used by the route

View File

@@ -72,7 +72,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, res, next) => {
router.post('/upload-and-process', optionalAuth, uploadToDisk.single('flyerFile'), async (req, res, next: NextFunction) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'A flyer file (PDF or image) is required.' });
@@ -157,7 +157,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, res, next) => {
router.post('/flyers/process', optionalAuth, uploadToDisk.single('flyerImage'), async (req, res, next: NextFunction) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'Flyer image file is required.' });
@@ -187,7 +187,7 @@ router.post('/flyers/process', optionalAuth, uploadToDisk.single('flyerImage'),
// No explicit `data` field found. Attempt to interpret req.body as an object (Express may have parsed multipart fields differently).
try {
parsed = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
} catch (err) { // eslint-disable-line
} catch (err) {
logger.warn('[API /ai/flyers/process] Failed to JSON.parse req.body; using empty object', { error: errMsg(err) });
parsed = req.body || {};
}
@@ -293,7 +293,7 @@ router.post('/flyers/process', optionalAuth, uploadToDisk.single('flyerImage'),
* This endpoint checks if an image is a flyer. It uses `optionalAuth` to allow
* both authenticated and anonymous users to perform this check.
*/
router.post('/check-flyer', optionalAuth, uploadToDisk.single('image'), async (req, res, next) => {
router.post('/check-flyer', optionalAuth, uploadToDisk.single('image'), async (req, res, next: NextFunction) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'Image file is required.' });
@@ -305,7 +305,7 @@ router.post('/check-flyer', optionalAuth, uploadToDisk.single('image'), async (r
}
});
router.post('/extract-address', optionalAuth, uploadToDisk.single('image'), async (req, res, next) => {
router.post('/extract-address', optionalAuth, uploadToDisk.single('image'), async (req, res, next: NextFunction) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'Image file is required.' });
@@ -317,7 +317,7 @@ router.post('/extract-address', optionalAuth, uploadToDisk.single('image'), asyn
}
});
router.post('/extract-logo', optionalAuth, uploadToDisk.array('images'), async (req, res, next) => {
router.post('/extract-logo', optionalAuth, uploadToDisk.array('images'), async (req, res, next: NextFunction) => {
try {
if (!req.files || !Array.isArray(req.files) || req.files.length === 0) {
return res.status(400).json({ message: 'Image files are required.' });
@@ -329,7 +329,7 @@ router.post('/extract-logo', optionalAuth, uploadToDisk.array('images'), async (
}
});
router.post('/quick-insights', passport.authenticate('jwt', { session: false }), async (req, res, next) => {
router.post('/quick-insights', passport.authenticate('jwt', { session: false }), async (req, res, next: NextFunction) => {
try {
logger.info(`Server-side quick insights requested.`);
res.status(200).json({ text: "This is a server-generated quick insight: buy the cheap stuff!" }); // Stubbed response
@@ -338,7 +338,7 @@ router.post('/quick-insights', passport.authenticate('jwt', { session: false }),
}
});
router.post('/deep-dive', passport.authenticate('jwt', { session: false }), async (req, res, next) => {
router.post('/deep-dive', passport.authenticate('jwt', { session: false }), async (req, res, next: NextFunction) => {
try {
logger.info(`Server-side deep dive requested.`);
res.status(200).json({ text: "This is a server-generated deep dive analysis. It is very detailed." }); // Stubbed response
@@ -347,7 +347,7 @@ router.post('/deep-dive', passport.authenticate('jwt', { session: false }), asyn
}
});
router.post('/search-web', passport.authenticate('jwt', { session: false }), async (req, res, next) => {
router.post('/search-web', passport.authenticate('jwt', { session: false }), async (req, res, next: NextFunction) => {
try {
logger.info(`Server-side web search requested.`);
res.status(200).json({ text: "The web says this is good.", sources: [] }); // Stubbed response
@@ -356,7 +356,7 @@ router.post('/search-web', passport.authenticate('jwt', { session: false }), asy
}
});
router.post('/plan-trip', passport.authenticate('jwt', { session: false }), async (req, res, next) => {
router.post('/plan-trip', passport.authenticate('jwt', { session: false }), async (req, res, next: NextFunction) => {
// try {
// const { items, store, userLocation } = req.body;
// logger.info(`Server-side trip planning requested for user.`);

View File

@@ -1,5 +1,5 @@
// src/routes/budget.ts
import express from 'express';
import express, { NextFunction } from 'express';
import passport from './passport';
import {
getBudgetsForUser,
@@ -19,7 +19,7 @@ router.use(passport.authenticate('jwt', { session: false }));
/**
* GET /api/budgets - Get all budgets for the authenticated user.
*/
router.get('/', async (req, res, next) => {
router.get('/', async (req, res, next: NextFunction) => {
const user = req.user as UserProfile;
try {
const budgets = await getBudgetsForUser(user.user_id);
@@ -33,7 +33,7 @@ router.get('/', async (req, res, next) => {
/**
* POST /api/budgets - Create a new budget for the authenticated user.
*/
router.post('/', async (req, res, next) => {
router.post('/', async (req, res, next: NextFunction) => {
const user = req.user as UserProfile;
try {
const newBudget = await createBudget(user.user_id, req.body);
@@ -47,7 +47,7 @@ router.post('/', async (req, res, next) => {
/**
* PUT /api/budgets/:id - Update an existing budget.
*/
router.put('/:id', async (req, res, next) => {
router.put('/:id', async (req, res, next: NextFunction) => {
const user = req.user as UserProfile;
const budgetId = parseInt(req.params.id, 10);
try {
@@ -62,7 +62,7 @@ router.put('/:id', async (req, res, next) => {
/**
* DELETE /api/budgets/:id - Delete a budget.
*/
router.delete('/:id', async (req, res, next) => {
router.delete('/:id', async (req, res, next: NextFunction) => {
const user = req.user as UserProfile;
const budgetId = parseInt(req.params.id, 10);
try {
@@ -78,7 +78,7 @@ router.delete('/:id', async (req, res, next) => {
* 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, res, next) => {
router.get('/spending-analysis', async (req, res, next: NextFunction) => {
const user = req.user as UserProfile;
const { startDate, endDate } = req.query;

View File

@@ -1,5 +1,5 @@
// src/routes/gamification.ts
import express from 'express';
import express, { NextFunction } from 'express';
import passport, { isAdmin } from './passport';
import { getAllAchievements, getUserAchievements, awardAchievement, getLeaderboard } from '../services/db';
import { logger } from '../services/logger.server';
@@ -11,7 +11,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, res, next) => {
router.get('/', async (req, res, next: NextFunction) => {
try {
const achievements = await getAllAchievements();
res.json(achievements);
@@ -25,7 +25,7 @@ router.get('/', async (req, res, next) => {
* GET /api/achievements/leaderboard - Get the top users by points.
* This is a public endpoint.
*/
router.get('/leaderboard', async (req, res, next) => {
router.get('/leaderboard', async (req, res, next: NextFunction) => {
// 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 +45,7 @@ router.get('/leaderboard', async (req, res, next) => {
router.get(
'/me',
passport.authenticate('jwt', { session: false }),
async (req, res, next) => {
async (req, res, next: NextFunction) => {
const user = req.user as UserProfile;
try {
const userAchievements = await getUserAchievements(user.user_id);
@@ -65,7 +65,7 @@ router.post(
'/award',
passport.authenticate('jwt', { session: false }),
isAdmin,
async (req, res, next) => {
async (req, res, next: NextFunction) => {
const { userId, achievementName } = req.body;
if (!userId || !achievementName) {

View File

@@ -1,5 +1,5 @@
// src/routes/public.ts
import { Router, Request, Response } from 'express';
import { Router, Request, Response, NextFunction } from 'express';
import * as db from '../services/db';
import { logger } from '../services/logger.server';
import fs from 'fs/promises';
@@ -67,7 +67,7 @@ router.get('/flyers', async (req, res, next) => {
}
});
router.get('/master-items', async (req, res, next) => {
router.get('/master-items', async (req, res, next: NextFunction) => {
try {
const masterItems = await db.getAllMasterItems();
res.json(masterItems);
@@ -77,7 +77,7 @@ router.get('/master-items', async (req, res, next) => {
}
});
router.get('/flyers/:id/items', async (req, res, next) => {
router.get('/flyers/:id/items', async (req, res, next: NextFunction) => {
try {
const flyerId = parseInt(req.params.id, 10);
const items = await db.getFlyerItems(flyerId);
@@ -88,7 +88,7 @@ router.get('/flyers/:id/items', async (req, res, next) => {
}
});
router.post('/flyer-items/batch-fetch', async (req, res, next) => {
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.' });
@@ -101,7 +101,7 @@ router.post('/flyer-items/batch-fetch', async (req, res, next) => {
}
});
router.post('/flyer-items/batch-count', async (req, res, next) => {
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.' });
@@ -114,7 +114,7 @@ router.post('/flyer-items/batch-count', async (req, res, next) => {
}
});
router.get('/recipes/by-sale-percentage', async (req, res, next) => {
router.get('/recipes/by-sale-percentage', async (req, res, next: NextFunction) => {
const minPercentageStr = req.query.minPercentage as string || '50.0';
const minPercentage = parseFloat(minPercentageStr);

View File

@@ -1,5 +1,6 @@
// src/types/express.d.ts
import { Request, Response, NextFunction, RequestHandler } from 'express';
import * as qs from 'qs';
/**
* Defines a more accurate type for asynchronous Express route handlers.