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
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m26s
This commit is contained in:
@@ -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}`);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
1
src/types/express.d.ts
vendored
1
src/types/express.d.ts
vendored
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user