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
|
| 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
|
| 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 |
-
//
|
| 69 |
logger.info(`[SCHEDULER] No Day ${nextDay} for Track ${enrollment.trackId} β marking COMPLETED`);
|
| 70 |
-
|
| 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 |
}
|