261 lines
7.2 KiB
TypeScript
261 lines
7.2 KiB
TypeScript
import { prisma } from '@shieldai/db';
|
|
import { Queue, Worker, Job } from 'bullmq';
|
|
import { Redis } from 'ioredis';
|
|
import { reportService } from '@shieldai/report';
|
|
|
|
const redisHost = process.env.REDIS_HOST || 'localhost';
|
|
const redisPort = parseInt(process.env.REDIS_PORT || '6379', 10);
|
|
|
|
const connection = new Redis({
|
|
host: redisHost,
|
|
port: redisPort,
|
|
retryStrategy: (times: number) => Math.min(times * 50, 2000),
|
|
});
|
|
|
|
const QUEUE_CONFIG = {
|
|
reportGeneration: {
|
|
name: 'report-generation',
|
|
concurrency: parseInt(process.env.REPORT_CONCURRENCY || '3', 10),
|
|
defaultJobTimeout: parseInt(process.env.REPORT_JOB_TIMEOUT || '30000', 10),
|
|
maxAttempts: parseInt(process.env.REPORT_MAX_ATTEMPTS || '2', 10),
|
|
},
|
|
reportScheduler: {
|
|
name: 'report-scheduler',
|
|
concurrency: 1,
|
|
},
|
|
};
|
|
|
|
export const reportGenerationQueue = new Queue(
|
|
QUEUE_CONFIG.reportGeneration.name,
|
|
{ connection }
|
|
);
|
|
|
|
export const reportSchedulerQueue = new Queue(
|
|
QUEUE_CONFIG.reportScheduler.name,
|
|
{ connection }
|
|
);
|
|
|
|
async function processReportGeneration(
|
|
job: Job<{
|
|
reportId: string;
|
|
userId: string;
|
|
subscriptionId: string;
|
|
reportType: string;
|
|
periodStart?: string;
|
|
periodEnd?: string;
|
|
notifyEmail?: string;
|
|
}>
|
|
) {
|
|
const { reportId, userId, subscriptionId, reportType, periodStart, periodEnd, notifyEmail } = job.data;
|
|
|
|
job.updateProgress(10);
|
|
console.log(`[Report:Generate] Starting report ${reportId} for user ${userId}`);
|
|
|
|
try {
|
|
const report = await reportService.generateReport({
|
|
userId,
|
|
subscriptionId,
|
|
reportType,
|
|
periodStart: periodStart ? new Date(periodStart) : undefined,
|
|
periodEnd: periodEnd ? new Date(periodEnd) : undefined,
|
|
});
|
|
|
|
job.updateProgress(80);
|
|
|
|
if (notifyEmail && report.status === 'COMPLETED') {
|
|
const { EmailService } = await import('@shieldai/shared-notifications');
|
|
const emailService = EmailService.getInstance();
|
|
|
|
const dashboardUrl = process.env.DASHBOARD_URL || 'https://app.shieldai.com';
|
|
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: { name: true, email: true },
|
|
});
|
|
|
|
const userName = user?.name || notifyEmail.split('@')[0];
|
|
|
|
await emailService.sendWithTemplate(notifyEmail, {
|
|
templateId: 'report_ready',
|
|
variables: {
|
|
name: userName,
|
|
report_title: report.title,
|
|
report_summary: report.summary || 'Your protection report contains detailed statistics and recommendations.',
|
|
report_url: `${dashboardUrl}/reports/${report.id}`,
|
|
pdf_url: report.pdfUrl || `${dashboardUrl}/api/v1/reports/${report.id}/pdf`,
|
|
},
|
|
});
|
|
|
|
await prisma.securityReport.update({
|
|
where: { id: report.id },
|
|
data: {
|
|
status: 'DELIVERED',
|
|
deliveredAt: new Date(),
|
|
},
|
|
});
|
|
|
|
job.updateProgress(95);
|
|
}
|
|
|
|
job.updateProgress(100);
|
|
|
|
return {
|
|
status: report.status,
|
|
reportId: report.id,
|
|
title: report.title,
|
|
};
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Report generation failed';
|
|
console.error(`[Report:Generate] Job ${job.id} failed:`, message);
|
|
|
|
await prisma.securityReport.update({
|
|
where: { id: reportId },
|
|
data: {
|
|
status: 'FAILED',
|
|
error: message,
|
|
},
|
|
});
|
|
|
|
job.updateProgress(100);
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
|
|
async function processReportScheduler(job: Job) {
|
|
console.log('[Report:Scheduler] Running scheduled report check');
|
|
|
|
try {
|
|
const pendingReports = await prisma.securityReport.findMany({
|
|
where: {
|
|
status: 'PENDING',
|
|
scheduledFor: {
|
|
lte: new Date(),
|
|
},
|
|
},
|
|
include: {
|
|
user: { select: { email: true } },
|
|
},
|
|
});
|
|
|
|
const results: Array<{ reportId: string; queued: boolean }> = [];
|
|
|
|
for (const report of pendingReports) {
|
|
try {
|
|
await reportGenerationQueue.add('generate-report', {
|
|
reportId: report.id,
|
|
userId: report.userId,
|
|
subscriptionId: report.subscriptionId,
|
|
reportType: report.reportType,
|
|
periodStart: report.periodStart.toISOString(),
|
|
periodEnd: report.periodEnd.toISOString(),
|
|
notifyEmail: report.user?.email,
|
|
}, {
|
|
attempts: QUEUE_CONFIG.reportGeneration.maxAttempts,
|
|
backoff: { type: 'exponential', delay: 5000 },
|
|
jobId: `report-gen-${report.id}`,
|
|
});
|
|
|
|
results.push({ reportId: report.id, queued: true });
|
|
} catch (err) {
|
|
console.error(`[Report:Scheduler] Failed to queue report ${report.id}:`, err);
|
|
results.push({ reportId: report.id, queued: false });
|
|
}
|
|
}
|
|
|
|
return { processed: results.length, completedAt: new Date().toISOString() };
|
|
} catch (error) {
|
|
console.error('[Report:Scheduler] Error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export const reportGenerationWorker = new Worker(
|
|
QUEUE_CONFIG.reportGeneration.name,
|
|
processReportGeneration,
|
|
{
|
|
connection,
|
|
concurrency: QUEUE_CONFIG.reportGeneration.concurrency,
|
|
removeOnComplete: {
|
|
age: 7 * 24 * 60 * 60,
|
|
count: 500,
|
|
},
|
|
removeOnFail: {
|
|
age: 30 * 24 * 60 * 60,
|
|
count: 100,
|
|
},
|
|
}
|
|
);
|
|
|
|
export const reportSchedulerWorker = new Worker(
|
|
QUEUE_CONFIG.reportScheduler.name,
|
|
processReportScheduler,
|
|
{
|
|
connection,
|
|
concurrency: QUEUE_CONFIG.reportScheduler.concurrency,
|
|
}
|
|
);
|
|
|
|
reportGenerationWorker.on('completed', (job, result) => {
|
|
console.log(`[Report:Generate] Job ${job.id} completed:`, result);
|
|
});
|
|
|
|
reportGenerationWorker.on('failed', (job, err) => {
|
|
console.error(`[Report:Generate] Job ${job?.id} failed:`, err.message);
|
|
});
|
|
|
|
reportGenerationWorker.on('error', (err) => {
|
|
console.error('[Report:Generate] Worker error:', err.message);
|
|
});
|
|
|
|
reportSchedulerWorker.on('completed', (job, result) => {
|
|
console.log(`[Report:Scheduler] Job ${job.id} completed:`, result);
|
|
});
|
|
|
|
reportSchedulerWorker.on('failed', (job, err) => {
|
|
console.error(`[Report:Scheduler] Job ${job?.id} failed:`, err.message);
|
|
});
|
|
|
|
export async function queueReportGeneration(data: {
|
|
reportId: string;
|
|
userId: string;
|
|
subscriptionId: string;
|
|
reportType: string;
|
|
periodStart?: string;
|
|
periodEnd?: string;
|
|
notifyEmail?: string;
|
|
}) {
|
|
return reportGenerationQueue.add('generate-report', data, {
|
|
attempts: QUEUE_CONFIG.reportGeneration.maxAttempts,
|
|
backoff: { type: 'exponential', delay: 5000 },
|
|
jobId: `report-gen-${data.reportId}-${Date.now()}`,
|
|
});
|
|
}
|
|
|
|
export async function scheduleReportProcessor() {
|
|
return reportSchedulerQueue.add('check-pending-reports', {}, {
|
|
repeat: { pattern: '0 */6 * * *' },
|
|
jobId: 'report-scheduler-recurring',
|
|
});
|
|
}
|
|
|
|
export async function scheduleMonthlyReportTrigger() {
|
|
return reportSchedulerQueue.add('trigger-monthly-reports', {}, {
|
|
repeat: { pattern: '0 0 1 * *' },
|
|
jobId: 'monthly-report-trigger',
|
|
});
|
|
}
|
|
|
|
export async function scheduleAnnualReportTrigger() {
|
|
return reportSchedulerQueue.add('trigger-annual-reports', {}, {
|
|
repeat: { pattern: '0 0 1 1 *' },
|
|
jobId: 'annual-report-trigger',
|
|
});
|
|
}
|
|
|
|
export default {
|
|
reportGenerationQueue,
|
|
reportGenerationWorker,
|
|
reportSchedulerQueue,
|
|
reportSchedulerWorker,
|
|
};
|