Skip to content

Commit 05b3331

Browse files
Merge pull request #90 from ChangePlusPlusVandy/redis
redis fixes
2 parents 9778dc2 + 163a7de commit 05b3331

File tree

20 files changed

+562
-74
lines changed

20 files changed

+562
-74
lines changed

backend/app.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ import pageContentRoutes from './routes/pageContentRoutes.js';
1919
import mapRoutes from './routes/mapRoutes.js';
2020
import { prisma } from './config/prisma.js';
2121
import { clerkClient, clerkMiddleware } from '@clerk/express';
22+
import { connectRedis } from './config/redis.js';
2223

2324
const app = express();
2425

26+
connectRedis().catch(err => {
27+
console.error('Failed to connect to Redis on startup:', err);
28+
});
29+
2530
const allowedOrigins = [
2631
'http://localhost:5173',
2732
'https://tcba-frontend.vercel.app',

backend/config/redis.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { createClient } from 'redis';
2+
3+
const redisClient = createClient({
4+
url: process.env.REDIS_URL || 'redis://localhost:6379',
5+
socket: {
6+
reconnectStrategy: retries => {
7+
if (retries > 10) {
8+
console.error('Redis: Max reconnection attempts reached');
9+
return new Error('Max reconnection attempts reached');
10+
}
11+
return Math.min(retries * 100, 3000);
12+
},
13+
},
14+
});
15+
16+
redisClient.on('error', err => {
17+
console.error('Redis Client Error:', err);
18+
});
19+
20+
redisClient.on('connect', () => {
21+
console.log('Redis Client Connected');
22+
});
23+
24+
redisClient.on('ready', () => {
25+
console.log('Redis Client Ready');
26+
});
27+
28+
redisClient.on('reconnecting', () => {
29+
console.log('Redis Client Reconnecting');
30+
});
31+
32+
export const connectRedis = async () => {
33+
try {
34+
if (!redisClient.isOpen) {
35+
await redisClient.connect();
36+
}
37+
} catch (error) {
38+
console.error('Failed to connect to Redis:', error);
39+
if (process.env.NODE_ENV === 'production') {
40+
throw error;
41+
}
42+
}
43+
};
44+
45+
export const disconnectRedis = async () => {
46+
try {
47+
if (redisClient.isOpen) {
48+
await redisClient.disconnect();
49+
}
50+
} catch (error) {
51+
console.error('Failed to disconnect from Redis:', error);
52+
}
53+
};
54+
55+
export default redisClient;

backend/controllers/alertController.ts

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import { prisma } from '../config/prisma.js';
55
import { EmailService } from '../services/EmailService.js';
66
import { createNotification } from './inAppNotificationController.js';
77
import { sendAlertEmails as sendAlertEmailNotifications } from '../services/emailNotificationService.js';
8+
import { CacheService, CacheKeys, CacheTTL } from '../utils/cache.js';
89

910
const isAdmin = (role?: OrganizationRole) => role === 'ADMIN';
1011

1112
/**
12-
* @desc Get all alerts (public shows only published, admins see all)
13-
* @route GET /api/alerts?priority
13+
* @desc Get all alerts with pagination
14+
* @route GET /api/alerts?priority&page=1&limit=20
1415
* @access Public (optional authentication)
1516
*/
1617
export const getAlerts = async (req: AuthenticatedRequest, res: Response) => {
@@ -40,8 +41,31 @@ export const getAlerts = async (req: AuthenticatedRequest, res: Response) => {
4041
}
4142
}
4243

44+
const page = parseInt(req.query.page as string) || 1;
45+
const limit = parseInt(req.query.limit as string) || 20;
46+
const skip = (page - 1) * limit;
47+
4348
const { priority, isPublished } = req.query;
4449

50+
const cacheKey = CacheKeys.alerts(
51+
page,
52+
limit,
53+
isAuthenticatedAdmin
54+
? isPublished === 'true'
55+
? true
56+
: isPublished === 'false'
57+
? false
58+
: undefined
59+
: true,
60+
priority as string
61+
);
62+
const cachedData = await CacheService.get<any>(cacheKey);
63+
64+
if (cachedData) {
65+
console.log(`Returning cached alerts (admin: ${isAuthenticatedAdmin})`);
66+
return res.status(200).json(cachedData);
67+
}
68+
4569
const where: any = {
4670
...(priority && { priority: priority as AlertPriority }),
4771

@@ -51,13 +75,31 @@ export const getAlerts = async (req: AuthenticatedRequest, res: Response) => {
5175
isPublished !== undefined && { isPublished: isPublished === 'true' }),
5276
};
5377

54-
const alerts = await prisma.alert.findMany({
55-
where,
56-
orderBy: [{ priority: 'asc' }, { createdAt: 'desc' }],
57-
});
78+
const [alerts, total] = await Promise.all([
79+
prisma.alert.findMany({
80+
where,
81+
orderBy: [{ priority: 'asc' }, { createdAt: 'desc' }],
82+
take: limit,
83+
skip,
84+
}),
85+
prisma.alert.count({ where }),
86+
]);
87+
88+
const response = {
89+
data: alerts,
90+
pagination: {
91+
total,
92+
page,
93+
limit,
94+
totalPages: Math.ceil(total / limit),
95+
hasMore: skip + alerts.length < total,
96+
},
97+
};
98+
99+
await CacheService.set(cacheKey, response, CacheTTL.ALERTS_LIST);
58100

59101
console.log(`Returning ${alerts.length} alerts (admin: ${isAuthenticatedAdmin})`);
60-
res.status(200).json(alerts);
102+
res.status(200).json(response);
61103
} catch (error) {
62104
console.error('Error fetching alerts:', error);
63105
res.status(500).json({ error: 'Failed to fetch alerts' });
@@ -200,6 +242,8 @@ export const createAlert = async (req: AuthenticatedRequest, res: Response) => {
200242
},
201243
});
202244

245+
await CacheService.deletePattern(CacheKeys.alertsAll());
246+
203247
if (newAlert.isPublished) {
204248
try {
205249
await createNotification('ALERT', newAlert.title, newAlert.id);
@@ -248,7 +292,9 @@ export const updateAlert = async (req: AuthenticatedRequest, res: Response) => {
248292
},
249293
});
250294

251-
// If alert is being published for the first time, send email notifications
295+
await CacheService.deletePattern(CacheKeys.alertsAll());
296+
await CacheService.delete(CacheKeys.alertById(id));
297+
252298
if (updatedAlert.isPublished && !existingAlert.isPublished) {
253299
await sendAlertEmails(updatedAlert);
254300
}
@@ -267,7 +313,6 @@ export const updateAlert = async (req: AuthenticatedRequest, res: Response) => {
267313
*/
268314
export const deleteAlert = async (req: AuthenticatedRequest, res: Response) => {
269315
try {
270-
// Ensure user is authenticated
271316
if (!req.user?.id) {
272317
return res.status(401).json({ error: 'Authentication required' });
273318
}
@@ -284,6 +329,10 @@ export const deleteAlert = async (req: AuthenticatedRequest, res: Response) => {
284329
}
285330

286331
await prisma.alert.delete({ where: { id } });
332+
333+
await CacheService.deletePattern(CacheKeys.alertsAll());
334+
await CacheService.delete(CacheKeys.alertById(id));
335+
287336
res.status(204).end();
288337
} catch (error) {
289338
console.error('Error deleting alert:', error);
@@ -325,6 +374,9 @@ export const publishAlert = async (req: AuthenticatedRequest, res: Response) =>
325374
},
326375
});
327376

377+
await CacheService.deletePattern(CacheKeys.alertsAll());
378+
await CacheService.delete(CacheKeys.alertById(id));
379+
328380
try {
329381
await sendAlertEmailNotifications(publishedAlert.id);
330382
console.log('Alert notifications sent successfully');
@@ -346,7 +398,6 @@ export const publishAlert = async (req: AuthenticatedRequest, res: Response) =>
346398
*/
347399
async function sendAlertEmails(alert: any): Promise<void> {
348400
try {
349-
// Get all active organizations with membershipActive = true
350401
const activeOrganizations = await prisma.organization.findMany({
351402
where: {
352403
membershipActive: true,
@@ -362,17 +413,13 @@ async function sendAlertEmails(alert: any): Promise<void> {
362413
},
363414
});
364415

365-
// Filter organizations by matching tags
366416
let targetOrganizations = activeOrganizations;
367417

368-
// If alert has tags, only send to organizations with matching tags
369418
if (alert.tags && alert.tags.length > 0) {
370419
targetOrganizations = activeOrganizations.filter(org => {
371-
// Check if organization has any matching tags with the alert
372420
return alert.tags.some((alertTag: string) => org.tags.includes(alertTag));
373421
});
374422
}
375-
// If alert has no tags, it's a broadcast alert sent to all active organizations
376423

377424
console.log(
378425
`Sending alert "${alert.title}" to ${targetOrganizations.length} organizations` +

backend/controllers/announcementController.ts

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { OrganizationRole } from '@prisma/client';
44
import { AuthenticatedRequest } from '../types/index.js';
55
import { createNotification } from './inAppNotificationController.js';
66
import { sendAnnouncementEmails } from '../services/emailNotificationService.js';
7+
import { CacheService, CacheKeys, CacheTTL } from '../utils/cache.js';
78

89
const isAdmin = (role?: OrganizationRole) => role === 'ADMIN';
910

@@ -16,8 +17,8 @@ const generateSlug = async (title: string, id: string): Promise<string> => {
1617
};
1718

1819
/**
19-
* @desc Get all announcements
20-
* @route GET /api/announcements
20+
* @desc Get all announcements with pagination
21+
* @route GET /api/announcements?page=1&limit=10
2122
* @access Public (returns only published) / Admin (returns all)
2223
*/
2324
export const getAnnouncements = async (req: AuthenticatedRequest, res: Response) => {
@@ -47,14 +48,46 @@ export const getAnnouncements = async (req: AuthenticatedRequest, res: Response)
4748
}
4849
}
4950

50-
const announcements = await prisma.announcements.findMany({
51-
where: isAuthenticatedAdmin ? {} : { isPublished: true },
52-
orderBy: { createdAt: 'desc' },
53-
include: { tags: true },
54-
});
51+
const page = parseInt(req.query.page as string) || 1;
52+
const limit = parseInt(req.query.limit as string) || 20;
53+
const skip = (page - 1) * limit;
54+
55+
const cacheKey = CacheKeys.announcements(page, limit, isAuthenticatedAdmin ? undefined : true);
56+
const cachedData = await CacheService.get<any>(cacheKey);
57+
58+
if (cachedData) {
59+
console.log(`Returning cached announcements (admin: ${isAuthenticatedAdmin})`);
60+
return res.status(200).json(cachedData);
61+
}
62+
63+
const where = isAuthenticatedAdmin ? {} : { isPublished: true };
64+
65+
const [announcements, total] = await Promise.all([
66+
prisma.announcements.findMany({
67+
where,
68+
orderBy: { createdAt: 'desc' },
69+
include: { tags: true },
70+
take: limit,
71+
skip,
72+
}),
73+
prisma.announcements.count({ where }),
74+
]);
75+
76+
const response = {
77+
data: announcements,
78+
pagination: {
79+
total,
80+
page,
81+
limit,
82+
totalPages: Math.ceil(total / limit),
83+
hasMore: skip + announcements.length < total,
84+
},
85+
};
86+
87+
await CacheService.set(cacheKey, response, CacheTTL.ANNOUNCEMENTS_LIST);
5588

5689
console.log(`Returning ${announcements.length} announcements (admin: ${isAuthenticatedAdmin})`);
57-
res.status(200).json(announcements);
90+
res.status(200).json(response);
5891
} catch (error) {
5992
console.error('Error fetching announcements:', error);
6093
res.status(500).json({ error: 'Failed to fetch announcements' });
@@ -91,13 +124,25 @@ export const getAnnouncementById = async (req: Request, res: Response) => {
91124
export const getAnnouncementBySlug = async (req: Request, res: Response) => {
92125
try {
93126
const { slug } = req.params;
127+
128+
const cacheKey = CacheKeys.announcementBySlug(slug);
129+
const cachedAnnouncement = await CacheService.get<any>(cacheKey);
130+
131+
if (cachedAnnouncement) {
132+
return res.status(200).json(cachedAnnouncement);
133+
}
134+
94135
const announcement = await prisma.announcements.findUnique({
95136
where: { slug },
96137
include: { tags: true },
97138
});
139+
98140
if (!announcement) {
99141
return res.status(404).json({ error: 'Announcement not found' });
100142
}
143+
144+
await CacheService.set(cacheKey, announcement, CacheTTL.ANNOUNCEMENT_DETAIL);
145+
101146
res.status(200).json(announcement);
102147
} catch (error) {
103148
console.error('Error fetching announcement by slug:', error);
@@ -191,6 +236,8 @@ export const createAnnouncement = async (req: AuthenticatedRequest, res: Respons
191236
});
192237
console.log('Announcement updated successfully:', newAnnouncement.id);
193238

239+
await CacheService.deletePattern(CacheKeys.announcementsAll());
240+
194241
if (newAnnouncement.isPublished) {
195242
try {
196243
await createNotification('ANNOUNCEMENT', newAnnouncement.title, newAnnouncement.slug);
@@ -242,6 +289,10 @@ export const updateAnnouncement = async (req: AuthenticatedRequest, res: Respons
242289
data: updateData,
243290
include: { tags: true },
244291
});
292+
293+
await CacheService.deletePattern(CacheKeys.announcementsAll());
294+
await CacheService.delete(CacheKeys.announcementBySlug(updatedAnnouncement.slug));
295+
245296
res.status(200).json(updatedAnnouncement);
246297
} catch (error) {
247298
console.error(error);
@@ -269,6 +320,8 @@ export const deleteAnnouncement = async (req: AuthenticatedRequest, res: Respons
269320
},
270321
});
271322

323+
await CacheService.deletePattern(CacheKeys.announcementsAll());
324+
272325
res.status(204).end();
273326
} catch (error) {
274327
console.error(error);
@@ -299,6 +352,9 @@ export const publishAnnouncement = async (req: AuthenticatedRequest, res: Respon
299352
include: { tags: true },
300353
});
301354

355+
await CacheService.deletePattern(CacheKeys.announcementsAll());
356+
await CacheService.delete(CacheKeys.announcementBySlug(publishedAnnouncement.slug));
357+
302358
try {
303359
await createNotification(
304360
'ANNOUNCEMENT',
@@ -341,6 +397,9 @@ export const unpublishAnnouncement = async (req: AuthenticatedRequest, res: Resp
341397
include: { tags: true },
342398
});
343399

400+
await CacheService.deletePattern(CacheKeys.announcementsAll());
401+
await CacheService.delete(CacheKeys.announcementBySlug(unpublishedAnnouncement.slug));
402+
344403
res.json(unpublishedAnnouncement);
345404
} catch (error) {
346405
console.error('Error unpublishing announcement:', error);

0 commit comments

Comments
 (0)