CognxSafeTrack Claude Sonnet 4.6 commited on
Commit
2859b85
Β·
1 Parent(s): ec9b5da

fix: batch scheduler updates, typed JWT user, log catch blocks, move script

Browse files

- scheduler.ts: collect completed enrollment IDs in loop, single updateMany
after β€” eliminates N queries per day for completions
- ContentHandler.ts: log Redis cache failures instead of swallowing silently
- index.ts: remove (request as any).user cast β€” now safe via FastifyJWT
augmentation in types/fastify.d.ts
- types/fastify.d.ts: augment @fastify/jwt FastifyJWT interface so request.user
resolves to {id, role, organizationId} after jwtVerify(), not string|object|Buffer
- delete-user.ts: moved from apps/api root to apps/api/scripts/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

apps/api/{delete-user.ts β†’ scripts/delete-user.ts} RENAMED
File without changes
apps/api/src/index.ts CHANGED
@@ -80,7 +80,7 @@ const registerRoutes = async () => {
80
  }
81
 
82
  // Multi-Tenant Enforcement
83
- const user = request.user as any;
84
  const requestedOrgId = request.headers['x-organization-id'] as string;
85
 
86
  if (user && user.role !== 'SUPER_ADMIN') {
 
80
  }
81
 
82
  // Multi-Tenant Enforcement
83
+ const user = request.user;
84
  const requestedOrgId = request.headers['x-organization-id'] as string;
85
 
86
  if (user && user.role !== 'SUPER_ADMIN') {
apps/api/src/types/fastify.d.ts CHANGED
@@ -1,15 +1,20 @@
1
  import { PrismaClient } from '@repo/database';
 
 
 
 
 
 
 
 
 
 
2
 
3
  declare module 'fastify' {
4
  interface FastifyInstance {
5
  prisma: PrismaClient;
6
  }
7
  interface FastifyRequest {
8
- user: {
9
- id: string;
10
- role: string;
11
- organizationId: string;
12
- };
13
  organizationId?: string;
14
  rawBody?: Buffer;
15
  }
 
1
  import { PrismaClient } from '@repo/database';
2
+ import '@fastify/multipart';
3
+
4
+ // Tell @fastify/jwt what shape our JWT payload has.
5
+ // This makes request.user properly typed after jwtVerify().
6
+ declare module '@fastify/jwt' {
7
+ interface FastifyJWT {
8
+ payload: { id: string; role: string; organizationId: string };
9
+ user: { id: string; role: string; organizationId: string };
10
+ }
11
+ }
12
 
13
  declare module 'fastify' {
14
  interface FastifyInstance {
15
  prisma: PrismaClient;
16
  }
17
  interface FastifyRequest {
 
 
 
 
 
18
  organizationId?: string;
19
  rawBody?: Buffer;
20
  }
apps/whatsapp-worker/src/handlers/ContentHandler.ts CHANGED
@@ -17,7 +17,9 @@ export class ContentHandler implements JobHandler {
17
  try {
18
  const cached = await connection.get(cacheKey);
19
  if (cached) return JSON.parse(cached);
20
- } catch (err) {}
 
 
21
 
22
  const org = await prisma.organization.findUnique({
23
  where: { id: organizationId },
 
17
  try {
18
  const cached = await connection.get(cacheKey);
19
  if (cached) return JSON.parse(cached);
20
+ } catch (err) {
21
+ logger.warn({ err, organizationId }, '[ContentHandler] Redis cache lookup failed');
22
+ }
23
 
24
  const org = await prisma.organization.findUnique({
25
  where: { id: organizationId },
apps/whatsapp-worker/src/scheduler.ts CHANGED
@@ -41,13 +41,14 @@ export function startDailyScheduler() {
41
  }
42
  });
43
 
 
 
44
  for (const enrollment of activeEnrollments) {
45
- // Find progress for this specific track
46
  const progress = enrollment.user.progress.find(p => p.trackId === enrollment.trackId);
47
 
48
  if (progress?.exerciseStatus === 'PENDING') {
49
- const lastInteraction = progress.lastInteraction;
50
- const hoursSinceLast = (Date.now() - new Date(lastInteraction).getTime()) / (1000 * 60 * 60);
51
 
52
  if (hoursSinceLast >= 72) {
53
  logger.info(`[SCHEDULER] Queuing RESURRECTION nudge for User ${enrollment.userId}`);
@@ -60,21 +61,15 @@ export function startDailyScheduler() {
60
  }
61
 
62
  const nextDay = enrollment.currentDay + 1;
63
-
64
- // ── Check the next day content exists in memory ──────────────────────────
65
  const nextDayContent = enrollment.track.days.find(d => d.dayNumber === nextDay);
66
 
67
  if (!nextDayContent) {
68
- // No more content β†’ mark enrollment COMPLETED
69
  logger.info(`[SCHEDULER] No Day ${nextDay} for Track ${enrollment.trackId} β€” marking COMPLETED`);
70
- await prisma.enrollment.update({
71
- where: { id: enrollment.id },
72
- data: { status: 'COMPLETED', completedAt: new Date() }
73
- });
74
  continue;
75
  }
76
 
77
- // ── Queue the next lesson ─────────────────────────────────────
78
  await whatsappQueue.add('send-content', {
79
  userId: enrollment.userId,
80
  trackId: enrollment.trackId,
@@ -83,6 +78,15 @@ export function startDailyScheduler() {
83
 
84
  logger.info(`[SCHEDULER] Queued Day ${nextDay} for User ${enrollment.userId}`);
85
  }
 
 
 
 
 
 
 
 
 
86
  } catch (error) {
87
  logger.error('[SCHEDULER] Error:', error);
88
  }
 
41
  }
42
  });
43
 
44
+ const completedIds: string[] = [];
45
+
46
  for (const enrollment of activeEnrollments) {
47
+ // Find progress for this specific track (in-memory, no extra query)
48
  const progress = enrollment.user.progress.find(p => p.trackId === enrollment.trackId);
49
 
50
  if (progress?.exerciseStatus === 'PENDING') {
51
+ const hoursSinceLast = (Date.now() - new Date(progress.lastInteraction).getTime()) / (1000 * 60 * 60);
 
52
 
53
  if (hoursSinceLast >= 72) {
54
  logger.info(`[SCHEDULER] Queuing RESURRECTION nudge for User ${enrollment.userId}`);
 
61
  }
62
 
63
  const nextDay = enrollment.currentDay + 1;
 
 
64
  const nextDayContent = enrollment.track.days.find(d => d.dayNumber === nextDay);
65
 
66
  if (!nextDayContent) {
67
+ // Collect for batch update instead of N individual queries
68
  logger.info(`[SCHEDULER] No Day ${nextDay} for Track ${enrollment.trackId} β€” marking COMPLETED`);
69
+ completedIds.push(enrollment.id);
 
 
 
70
  continue;
71
  }
72
 
 
73
  await whatsappQueue.add('send-content', {
74
  userId: enrollment.userId,
75
  trackId: enrollment.trackId,
 
78
 
79
  logger.info(`[SCHEDULER] Queued Day ${nextDay} for User ${enrollment.userId}`);
80
  }
81
+
82
+ // Single batch update instead of one query per completed enrollment
83
+ if (completedIds.length > 0) {
84
+ await prisma.enrollment.updateMany({
85
+ where: { id: { in: completedIds } },
86
+ data: { status: 'COMPLETED', completedAt: new Date() }
87
+ });
88
+ logger.info(`[SCHEDULER] Marked ${completedIds.length} enrollment(s) as COMPLETED`);
89
+ }
90
  } catch (error) {
91
  logger.error('[SCHEDULER] Error:', error);
92
  }