DB refactor for easier testsing
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 5m16s

App.ts refactor into hooks
unit tests
This commit is contained in:
2025-12-08 20:46:12 -08:00
parent 0eda796fad
commit 158778c2ec
67 changed files with 4666 additions and 3599 deletions

View File

@@ -8,14 +8,16 @@ import * as db from '../services/db/index.db';
import { logger } from '../services/logger.server';
import { UserProfile } from '../types';
import { clearGeocodeCache } from '../services/geocodingService.server';
import { ForeignKeyConstraintError } from '../services/db/errors.db';
// --- Bull Board (Job Queue UI) Imports ---
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ExpressAdapter } from '@bull-board/express';
import type { Queue } from 'bullmq';
import { backgroundJobService } from '../services/backgroundJobService';
import { backgroundJobService } from '../services/backgroundJobService';
import { flyerQueue, emailQueue, analyticsQueue, cleanupQueue, flyerWorker, emailWorker, analyticsWorker, cleanupWorker } from '../services/queueService.server'; // Import your queues
import { getSimpleWeekAndYear } from '../utils/dateUtils';
const router = Router();
@@ -41,7 +43,8 @@ createBullBoard({
new BullMQAdapter(flyerQueue),
new BullMQAdapter(emailQueue),
new BullMQAdapter(analyticsQueue),
new BullMQAdapter(cleanupQueue) // Add the new cleanup queue here
new BullMQAdapter(cleanupQueue), // Add the new cleanup queue here
new BullMQAdapter((await import('../services/queueService.server')).weeklyAnalyticsQueue),
],
serverAdapter: serverAdapter,
});
@@ -58,7 +61,7 @@ router.use(passport.authenticate('jwt', { session: false }), isAdmin);
router.get('/corrections', async (req, res, next: NextFunction) => {
try {
const corrections = await db.getSuggestedCorrections();
const corrections = await db.adminRepo.getSuggestedCorrections();
res.json(corrections);
} catch (error) {
next(error);
@@ -67,7 +70,7 @@ router.get('/corrections', async (req, res, next: NextFunction) => {
router.get('/brands', async (req, res, next: NextFunction) => {
try {
const brands = await db.getAllBrands();
const brands = await db.flyerRepo.getAllBrands();
res.json(brands);
} catch (error) {
next(error);
@@ -76,7 +79,7 @@ router.get('/brands', async (req, res, next: NextFunction) => {
router.get('/stats', async (req, res, next: NextFunction) => {
try {
const stats = await db.getApplicationStats();
const stats = await db.adminRepo.getApplicationStats();
res.json(stats);
} catch (error) {
next(error);
@@ -85,7 +88,7 @@ router.get('/stats', async (req, res, next: NextFunction) => {
router.get('/stats/daily', async (req, res, next: NextFunction) => {
try {
const dailyStats = await db.getDailyStatsForLast30Days();
const dailyStats = await db.adminRepo.getDailyStatsForLast30Days();
res.json(dailyStats);
} catch (error) {
next(error);
@@ -99,7 +102,7 @@ router.post('/corrections/:id/approve', async (req, res, next: NextFunction) =>
return res.status(400).json({ message: 'Invalid correction ID provided.' });
}
try {
await db.approveCorrection(correctionId);
await db.adminRepo.approveCorrection(correctionId);
res.status(200).json({ message: 'Correction approved successfully.' });
} catch (error) {
next(error);
@@ -109,7 +112,7 @@ router.post('/corrections/:id/approve', async (req, res, next: NextFunction) =>
router.post('/corrections/:id/reject', async (req, res, next: NextFunction) => {
try {
const correctionId = parseInt(req.params.id, 10);
await db.rejectCorrection(correctionId);
await db.adminRepo.rejectCorrection(correctionId);
res.status(200).json({ message: 'Correction rejected successfully.' });
} catch (error) {
next(error);
@@ -123,10 +126,9 @@ router.put('/corrections/:id', async (req, res, next: NextFunction) => {
return res.status(400).json({ message: 'A new suggested_value is required.' });
}
try {
const updatedCorrection = await db.updateSuggestedCorrection(correctionId, suggested_value);
const updatedCorrection = await db.adminRepo.updateSuggestedCorrection(correctionId, suggested_value);
res.status(200).json(updatedCorrection);
} catch (error) {
// Check if the error message indicates "not found" to return a 404
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ message: error.message });
}
@@ -142,9 +144,12 @@ router.put('/recipes/:id/status', async (req, res, next: NextFunction) => {
return res.status(400).json({ message: 'A valid status (private, pending_review, public, rejected) is required.' });
}
try {
const updatedRecipe = await db.updateRecipeStatus(recipeId, status);
const updatedRecipe = await db.adminRepo.updateRecipeStatus(recipeId, status); // This is still a standalone function in admin.db.ts
res.status(200).json(updatedRecipe);
} catch (error) {
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ message: error.message });
}
next(error);
}
});
@@ -157,7 +162,7 @@ router.post('/brands/:id/logo', upload.single('logoImage'), async (req, res, nex
try {
const logoUrl = `/assets/${req.file.filename}`;
await db.updateBrandLogo(brandId, logoUrl);
await db.adminRepo.updateBrandLogo(brandId, logoUrl);
logger.info(`Brand logo updated for brand ID: ${brandId}`, { brandId, logoUrl });
res.status(200).json({ message: 'Brand logo updated successfully.', logoUrl });
@@ -168,7 +173,7 @@ router.post('/brands/:id/logo', upload.single('logoImage'), async (req, res, nex
router.get('/unmatched-items', async (req, res, next: NextFunction) => {
try {
const items = await db.getUnmatchedFlyerItems();
const items = await db.adminRepo.getUnmatchedFlyerItems();
res.json(items);
} catch (error) {
next(error);
@@ -183,16 +188,19 @@ router.put('/comments/:id/status', async (req, res, next: NextFunction) => {
return res.status(400).json({ message: 'A valid status (visible, hidden, reported) is required.' });
}
try {
const updatedComment = await db.updateRecipeCommentStatus(commentId, status);
const updatedComment = await db.adminRepo.updateRecipeCommentStatus(commentId, status); // This is still a standalone function in admin.db.ts
res.status(200).json(updatedComment);
} catch (error) {
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ message: error.message });
}
next(error);
}
});
router.get('/users', async (req, res, next: NextFunction) => {
try {
const users = await db.getAllUsers();
const users = await db.adminRepo.getAllUsers();
res.json(users);
} catch (error) {
next(error);
@@ -203,7 +211,7 @@ router.get('/activity-log', async (req, res, next: NextFunction) => {
const limit = parseInt(req.query.limit as string, 10) || 50;
const offset = parseInt(req.query.offset as string, 10) || 0;
try {
const logs = await db.getActivityLog(limit, offset);
const logs = await db.adminRepo.getActivityLog(limit, offset);
res.json(logs);
} catch (error) {
next(error);
@@ -212,7 +220,7 @@ router.get('/activity-log', async (req, res, next: NextFunction) => {
router.get('/users/:id', async (req, res, next: NextFunction) => {
try {
const user = await db.findUserProfileById(req.params.id);
const user = await db.userRepo.findUserProfileById(req.params.id);
if (!user) {
return res.status(404).json({ message: 'User not found.' });
}
@@ -229,11 +237,12 @@ router.put('/users/:id', async (req, res, next: NextFunction) => {
}
try {
const updatedUser = await db.updateUserRole(req.params.id, role);
const updatedUser = await db.adminRepo.updateUserRole(req.params.id, role);
res.json(updatedUser);
} catch (error) {
// Check if the error message indicates "not found" to return a 404
if (error instanceof Error && error.message.includes('not found')) {
if (error instanceof ForeignKeyConstraintError) { // This error is thrown by the DB layer
return res.status(404).json({ message: `User with ID ${req.params.id} not found.` });
} else if (error instanceof Error && error.message.includes('not found')) { // This handles the generic "not found" from the repo
return res.status(404).json({ message: error.message });
}
logger.error(`Error updating user ${req.params.id}:`, { error });
@@ -247,7 +256,7 @@ router.delete('/users/:id', async (req, res, next: NextFunction) => {
return res.status(400).json({ message: 'Admins cannot delete their own account.' });
}
try {
await db.deleteUserById(req.params.id);
await db.userRepo.deleteUserById(req.params.id);
res.status(204).send();
} catch (error) {
next(error);
@@ -432,4 +441,24 @@ router.post('/jobs/:queueName/:jobId/retry', async (req, res, next: NextFunction
}
});
/**
* POST /api/admin/trigger/weekly-analytics - Manually trigger the weekly analytics report job.
*/
router.post('/trigger/weekly-analytics', async (req, res, next: NextFunction) => {
const adminUser = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for weekly analytics report received from user: ${adminUser.user_id}`);
try {
const { year: reportYear, week: reportWeek } = getSimpleWeekAndYear();
const { weeklyAnalyticsQueue } = await import('../services/queueService.server');
const job = await weeklyAnalyticsQueue.add('generate-weekly-report', { reportYear, reportWeek }, {
jobId: `manual-weekly-report-${reportYear}-${reportWeek}-${Date.now()}` // Add timestamp to avoid ID conflict
});
res.status(202).json({ message: 'Successfully enqueued weekly analytics job.', jobId: job.id });
} catch (error) {
next(error);
}
});
export default router;