diff --git a/email_templates/changes_requested_by_manager/html.pug b/email_templates/changes_requested_by_manager/html.pug index 2f5b2b8383..c12e4f7a82 100644 --- a/email_templates/changes_requested_by_manager/html.pug +++ b/email_templates/changes_requested_by_manager/html.pug @@ -2,7 +2,7 @@ style include ../email.css p Hello, p -p #{managerName} requested changed to report #{displayId}. +p #{managerName} requested changes to report #{displayId}. if comments p #{managerName} provided the following comments: blockquote !{comments} diff --git a/src/lib/apiErrorHandler.js b/src/lib/apiErrorHandler.js index 745048828c..0568aafb5a 100644 --- a/src/lib/apiErrorHandler.js +++ b/src/lib/apiErrorHandler.js @@ -13,7 +13,6 @@ import { sequelize } from '../models'; * @returns {Promise} - The ID of the stored request error, or null if storing failed. */ async function logRequestError(req, operation, error, logContext) { - // Check if error logging should be suppressed if ( operation !== 'SequelizeError' && process.env.SUPPRESS_ERROR_LOGGING @@ -21,26 +20,19 @@ async function logRequestError(req, operation, error, logContext) { ) { return 0; } + if (!error) { + return 0; + } try { - // Prepare the response body for storage const responseBody = typeof error === 'object' - && error !== null ? { ...error, errorStack: error?.stack } : error; + ? { ...error, errorStack: error?.stack } + : error; - // Prepare the request body for storage const requestBody = { - ...(req.body - && typeof req.body === 'object' - && Object.keys(req.body).length > 0 - && { body: req.body }), - ...(req.params - && typeof req.params === 'object' - && Object.keys(req.params).length > 0 - && { params: req.params }), - ...(req.query - && typeof req.query === 'object' - && Object.keys(req.query).length > 0 - && { query: req.query }), + ...(req.body && typeof req.body === 'object' && Object.keys(req.body).length > 0 && { body: req.body }), + ...(req.params && typeof req.params === 'object' && Object.keys(req.params).length > 0 && { params: req.params }), + ...(req.query && typeof req.query === 'object' && Object.keys(req.query).length > 0 && { query: req.query }), }; // Create a request error in the database and get its ID @@ -69,7 +61,6 @@ async function logRequestError(req, operation, error, logContext) { * @param {Object} logContext - The context for logging. */ export const handleError = async (req, res, error, logContext) => { - // Check if the environment is development if (process.env.NODE_ENV === 'development') { logger.error(error); } @@ -77,7 +68,6 @@ export const handleError = async (req, res, error, logContext) => { let operation; let label; - // Check if the error is an instance of Sequelize.Error if (error instanceof Sequelize.Error) { operation = 'SequelizeError'; label = 'Sequelize error'; @@ -86,27 +76,17 @@ export const handleError = async (req, res, error, logContext) => { label = 'UNEXPECTED ERROR'; } - // eslint-disable-next-line max-len - if (error instanceof Sequelize.ConnectionError || error instanceof Sequelize.ConnectionAcquireTimeoutError) { + if (error instanceof Sequelize.ConnectionError + || error instanceof Sequelize.ConnectionAcquireTimeoutError) { const pool = sequelize?.connectionManager?.pool; - const usedConnections = pool ? pool?.used?.length : null; - const waitingConnections = pool ? pool?.pending?.length : null; + const usedConnections = pool ? pool.used.length : null; + const waitingConnections = pool ? pool.pending.length : null; logger.error(`${logContext.namespace} Connection Pool: Used Connections - ${usedConnections}, Waiting Connections - ${waitingConnections}`); } - - // Log the request error and get the error ID const requestErrorId = await logRequestError(req, operation, error, logContext); - let errorMessage; + const errorMessage = error?.stack || error; - // Check if the error has a stack property - if (error?.stack) { - errorMessage = error.stack; - } else { - errorMessage = error; - } - - // Log the error message with the error ID if available if (requestErrorId) { logger.error(`${logContext.namespace} - id: ${requestErrorId} ${label} - ${errorMessage}`); } else { @@ -117,12 +97,11 @@ export const handleError = async (req, res, error, logContext) => { }; /** - * Handles any unexpected errors in an error handler catch block - * - * @param {*} req - request - * @param {*} res - response - * @param {*} error - error - * @param {*} logContext - useful data for logging + * Handles any unexpected errors in an error handler catch block. + * @param {Object} req - The request object. + * @param {Object} res - The response object. + * @param {Error} error - The error object. + * @param {Object} logContext - The context for logging. */ export function handleUnexpectedErrorInCatchBlock(req, res, error, logContext) { logger.error(`${logContext.namespace} - Unexpected error in catch block - ${error}`); @@ -131,11 +110,10 @@ export function handleUnexpectedErrorInCatchBlock(req, res, error, logContext) { /** * Handles API errors. Saves data in the RequestErrors table and sends 500 error. - * - * @param {*} req - request - * @param {*} res - response - * @param {*} error - error - * @param {*} logContext - useful data for logging + * @param {Object} req - The request object. + * @param {Object} res - The response object. + * @param {Error} error - The error object. + * @param {Object} logContext - The context for logging. */ export default async function handleErrors(req, res, error, logContext) { try { @@ -144,3 +122,116 @@ export default async function handleErrors(req, res, error, logContext) { handleUnexpectedErrorInCatchBlock(req, res, e, logContext); } } + +/** + * Logs a worker error and stores it in the database. + * @param {Object} job - The job object. + * @param {string} operation - The operation name. + * @param {Error} error - The error object. + * @param {Object} logContext - The logging context. + * @returns {Promise} - The ID of the stored request error, or null if storing failed. + */ +const logWorkerError = async (job, operation, error, logContext) => { + if ( + operation !== 'SequelizeError' + && process.env.SUPPRESS_ERROR_LOGGING + && process.env.SUPPRESS_ERROR_LOGGING.toLowerCase() === 'true' + ) { + return 0; + } + if (!error) { + return 0; + } + + try { + const responseBody = typeof error === 'object' + ? { ...error, errorStack: error?.stack } + : error; + + const requestBody = { + ...(job.data && typeof job.data === 'object' && Object.keys(job.data).length > 0 && { data: job.data }), + }; + + const requestErrorId = await createRequestError({ + operation, + uri: job.queue.name, + method: 'PROCESS_JOB', + requestBody, + responseBody, + responseCode: INTERNAL_SERVER_ERROR, + }); + + return requestErrorId; + } catch (e) { + logger.error(`${logContext.namespace} - Sequelize error - unable to store RequestError - ${e}`); + } + + return null; +}; + +/** + * Handles errors in a worker job. + * @param {Object} job - The job object. + * @param {Error} error - The error object. + * @param {Object} logContext - The context for logging. + */ +export const handleWorkerError = async (job, error, logContext) => { + if (process.env.NODE_ENV === 'development') { + logger.error(error); + } + + let operation; + let label; + + if (error instanceof Sequelize.Error) { + operation = 'SequelizeError'; + label = 'Sequelize error'; + } else { + operation = 'UNEXPECTED_ERROR'; + label = 'UNEXPECTED ERROR'; + } + + if (error instanceof Sequelize.ConnectionError + || error instanceof Sequelize.ConnectionAcquireTimeoutError) { + const pool = sequelize?.connectionManager?.pool; + const usedConnections = pool ? pool.used.length : null; + const waitingConnections = pool ? pool.pending.length : null; + logger.error(`${logContext.namespace} Connection Pool: Used Connections - ${usedConnections}, Waiting Connections - ${waitingConnections}`); + } + + const requestErrorId = await logWorkerError(job, operation, error, logContext); + + const errorMessage = error?.stack || error; + + if (requestErrorId) { + logger.error(`${logContext.namespace} - id: ${requestErrorId} ${label} - ${errorMessage}`); + } else { + logger.error(`${logContext.namespace} - ${label} - ${errorMessage}`); + } + + // Handle job failure as needed +}; + +/** + * Handles any unexpected errors in a worker error handler catch block. + * @param {Object} job - The job object. + * @param {Error} error - The error object. + * @param {Object} logContext - The context for logging. + */ +export const handleUnexpectedWorkerError = (job, error, logContext) => { + logger.error(`${logContext.namespace} - Unexpected error in catch block - ${error}`); +}; + +/** + * Handles worker job errors. Logs the error and stores it in the database. + * @param {Object} job - The job object. + * @param {Error} error - The error object. + * @param {Object} logContext - The context for logging. + */ +export const handleWorkerErrors = async (job, error, logContext) => { + try { + await handleWorkerError(job, error, logContext); + } catch (e) { + handleUnexpectedWorkerError(job, e, logContext); + } +}; diff --git a/src/lib/apiErrorHandler.test.js b/src/lib/apiErrorHandler.test.js index 59506f6e7e..fca179fedd 100644 --- a/src/lib/apiErrorHandler.test.js +++ b/src/lib/apiErrorHandler.test.js @@ -1,7 +1,8 @@ import Sequelize from 'sequelize'; import { INTERNAL_SERVER_ERROR } from 'http-codes'; import db, { RequestErrors } from '../models'; -import handleErrors, { handleUnexpectedErrorInCatchBlock } from './apiErrorHandler'; +import handleErrors, { handleUnexpectedErrorInCatchBlock, handleWorkerErrors, handleUnexpectedWorkerError } from './apiErrorHandler'; +import { auditLogger as logger } from '../logger'; const mockUser = { id: 47, @@ -31,16 +32,29 @@ const mockResponse = { })), }; +const mockJob = { + data: { jobDetail: 'example job detail' }, + queue: { name: 'exampleQueue' }, +}; + const mockSequelizeError = new Sequelize.Error('Not all ok here'); const mockLogContext = { namespace: 'TEST', }; +jest.mock('../logger', () => ({ + auditLogger: { + error: jest.fn(), + }, +})); + describe('apiErrorHandler', () => { beforeEach(async () => { await RequestErrors.destroy({ where: {} }); + jest.clearAllMocks(); }); + afterAll(async () => { await RequestErrors.destroy({ where: {} }); await db.sequelize.close(); @@ -54,6 +68,7 @@ describe('apiErrorHandler', () => { const requestErrors = await RequestErrors.findAll(); expect(requestErrors.length).not.toBe(0); + expect(requestErrors[0].operation).toBe('SequelizeError'); }); it('handles a generic error', async () => { @@ -65,9 +80,10 @@ describe('apiErrorHandler', () => { const requestErrors = await RequestErrors.findAll(); expect(requestErrors.length).not.toBe(0); + expect(requestErrors[0].operation).toBe('UNEXPECTED_ERROR'); }); - it('can handle unexpected error in catch block', async () => { + it('handles unexpected error in catch block', async () => { const mockUnexpectedErr = new Error('Unexpected error'); handleUnexpectedErrorInCatchBlock(mockRequest, mockResponse, mockUnexpectedErr, mockLogContext); @@ -77,4 +93,99 @@ describe('apiErrorHandler', () => { expect(requestErrors.length).toBe(0); }); + + it('handles error suppression when SUPPRESS_ERROR_LOGGING is true', async () => { + process.env.SUPPRESS_ERROR_LOGGING = 'true'; + const mockGenericError = new Error('Unknown error'); + await handleErrors(mockRequest, mockResponse, mockGenericError, mockLogContext); + + expect(mockResponse.status).toHaveBeenCalledWith(INTERNAL_SERVER_ERROR); + + const requestErrors = await RequestErrors.findAll(); + + expect(requestErrors.length).toBe(0); + + delete process.env.SUPPRESS_ERROR_LOGGING; + }); + + it('logs connection pool information on connection errors', async () => { + const mockConnectionError = new Sequelize.ConnectionError(new Error('Connection error')); + await handleErrors(mockRequest, mockResponse, mockConnectionError, mockLogContext); + + expect(mockResponse.status).toHaveBeenCalledWith(INTERNAL_SERVER_ERROR); + expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Connection Pool: Used Connections')); + + const requestErrors = await RequestErrors.findAll(); + + expect(requestErrors.length).not.toBe(0); + expect(requestErrors[0].operation).toBe('SequelizeError'); + }); + + it('handles worker errors', async () => { + const mockWorkerError = new Error('Worker error'); + await handleWorkerErrors(mockJob, mockWorkerError, mockLogContext); + + const requestErrors = await RequestErrors.findAll(); + + expect(requestErrors.length).not.toBe(0); + expect(requestErrors[0].operation).toBe('UNEXPECTED_ERROR'); + }); + + it('handles worker Sequelize errors', async () => { + const mockSequelizeWorkerError = new Sequelize.Error('Sequelize worker error'); + await handleWorkerErrors(mockJob, mockSequelizeWorkerError, mockLogContext); + + const requestErrors = await RequestErrors.findAll(); + + expect(requestErrors.length).not.toBe(0); + expect(requestErrors[0].operation).toBe('SequelizeError'); + }); + + it('handles unexpected worker error in catch block', async () => { + const mockUnexpectedWorkerError = new Error('Unexpected worker error'); + handleUnexpectedWorkerError(mockJob, mockUnexpectedWorkerError, mockLogContext); + + const requestErrors = await RequestErrors.findAll(); + + expect(requestErrors.length).toBe(0); + }); + + it('handles null error', async () => { + await handleErrors(mockRequest, mockResponse, null, mockLogContext); + + expect(mockResponse.status).toHaveBeenCalledWith(INTERNAL_SERVER_ERROR); + + const requestErrors = await RequestErrors.findAll(); + + expect(requestErrors.length).toBe(0); + }); + + it('handles undefined error', async () => { + await handleErrors(mockRequest, mockResponse, undefined, mockLogContext); + + expect(mockResponse.status).toHaveBeenCalledWith(INTERNAL_SERVER_ERROR); + + const requestErrors = await RequestErrors.findAll(); + + expect(requestErrors.length).toBe(0); + }); + + it('handles specific Sequelize connection acquire timeout error', async () => { + const mockConnectionAcquireTimeoutError = new Sequelize + .ConnectionAcquireTimeoutError(new Error('Connection acquire timeout error')); + await handleErrors( + mockRequest, + mockResponse, + mockConnectionAcquireTimeoutError, + mockLogContext, + ); + + expect(mockResponse.status).toHaveBeenCalledWith(INTERNAL_SERVER_ERROR); + expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Connection Pool: Used Connections')); + + const requestErrors = await RequestErrors.findAll(); + + expect(requestErrors.length).not.toBe(0); + expect(requestErrors[0].operation).toBe('SequelizeError'); + }); }); diff --git a/src/lib/mailer/index.js b/src/lib/mailer/index.js index de4b2091de..412e882969 100644 --- a/src/lib/mailer/index.js +++ b/src/lib/mailer/index.js @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/return-await */ +import httpContext from 'express-http-context'; import { createTransport } from 'nodemailer'; import { uniq } from 'lodash'; import { QueryTypes } from 'sequelize'; @@ -17,6 +18,8 @@ import { } from '../../services/activityReports'; import { userById } from '../../services/users'; import logEmailNotification from './logNotifications'; +import transactionQueueWrapper from '../../workers/transactionWrapper'; +import referenceData from '../../workers/referenceData'; export const notificationQueue = newQueue('notifications'); @@ -355,6 +358,7 @@ export const collaboratorAssignedNotification = (report, newCollaborators) => { const data = { report, newCollaborator: collaborator.user, + ...referenceData(), }; notificationQueue.add(EMAIL_ACTIONS.COLLABORATOR_ADDED, data); } catch (err) { @@ -370,6 +374,7 @@ export const approverAssignedNotification = (report, newApprovers) => { const data = { report, newApprover: approver, + ...referenceData(), }; notificationQueue.add(EMAIL_ACTIONS.SUBMITTED, data); } catch (err) { @@ -385,6 +390,7 @@ export const reportApprovedNotification = (report, authorWithSetting, collabsWit report, authorWithSetting, collabsWithSettings, + ...referenceData(), }; notificationQueue.add(EMAIL_ACTIONS.APPROVED, data); } catch (err) { @@ -408,6 +414,7 @@ export const programSpecialistRecipientReportApprovedNotification = ( report, programSpecialists, recipients, + ...referenceData(), }; notificationQueue.add(EMAIL_ACTIONS.RECIPIENT_REPORT_APPROVED, data); } catch (err) { @@ -482,6 +489,7 @@ export const trVisionAndGoalComplete = async (event) => { emailTo: [user.email], debugMessage: `MAILER: Notifying ${user.email} that a POC completed work on TR ${event.id} | ${eId}`, templatePath: 'tr_poc_vision_goal_complete', + ...referenceData(), }; return notificationQueue.add(EMAIL_ACTIONS.TRAINING_REPORT_POC_VISION_GOAL_COMPLETE, data); @@ -514,6 +522,7 @@ export const trPocSessionComplete = async (event) => { emailTo: [user.email], debugMessage: `MAILER: Notifying ${user.email} that a POC completed work on TR ${event.id}`, templatePath: 'tr_poc_session_complete', + ...referenceData(), }; return notificationQueue.add(EMAIL_ACTIONS.TRAINING_REPORT_POC_SESSION_COMPLETE, data); @@ -550,6 +559,7 @@ export const trSessionCreated = async (event) => { ...event, displayId: eventId, }, + ...referenceData(), }; return notificationQueue.add(EMAIL_ACTIONS.TRAINING_REPORT_SESSION_CREATED, data); @@ -582,6 +592,7 @@ export const trSessionCompleted = async (event) => { emailTo: [user.email], debugMessage: `MAILER: Notifying ${user.email} that a session was completed for TR ${event.id}`, templatePath: 'tr_session_completed', + ...referenceData(), }; return notificationQueue.add(EMAIL_ACTIONS.TRAINING_REPORT_SESSION_COMPLETED, data); })); @@ -619,6 +630,7 @@ export const trCollaboratorAdded = async ( emailTo: [collaborator.email], templatePath: 'tr_collaborator_added', debugMessage: `MAILER: Notifying ${collaborator.email} that they were added as a collaborator to TR ${report.id}`, + ...referenceData(), }; notificationQueue.add(EMAIL_ACTIONS.TRAINING_REPORT_COLLABORATOR_ADDED, data); @@ -651,6 +663,7 @@ export const trPocAdded = async ( emailTo: [poc.email], debugMessage: `MAILER: Notifying ${poc.email} that they were added as a collaborator to TR ${report.id}`, templatePath: 'tr_poc_added', + ...referenceData(), }; notificationQueue.add(EMAIL_ACTIONS.TRAINING_REPORT_POC_ADDED, data); @@ -686,6 +699,7 @@ export const trPocEventComplete = async ( reportPath, debugMessage: `MAILER: Notifying ${user.email} that TR ${event.id} is complete`, templatePath: 'tr_event_complete', + ...referenceData(), }; return notificationQueue.add(EMAIL_ACTIONS.TRAINING_REPORT_EVENT_COMPLETED, data); @@ -708,6 +722,7 @@ export const changesRequestedNotification = ( approver, authorWithSetting, collabsWithSettings, + ...referenceData(), }; notificationQueue.add(EMAIL_ACTIONS.NEEDS_ACTION, data); } catch (err) { @@ -742,6 +757,7 @@ export async function collaboratorDigest(freq, subjectFreq) { type: EMAIL_ACTIONS.COLLABORATOR_DIGEST, freq, subjectFreq, + ...referenceData(), }; notificationQueue.add(EMAIL_ACTIONS.COLLABORATOR_DIGEST, data); return data; @@ -779,6 +795,7 @@ export async function changesRequestedDigest(freq, subjectFreq) { type: EMAIL_ACTIONS.NEEDS_ACTION_DIGEST, freq, subjectFreq, + ...referenceData(), }; notificationQueue.add(EMAIL_ACTIONS.NEEDS_ACTION_DIGEST, data); @@ -817,6 +834,7 @@ export async function submittedDigest(freq, subjectFreq) { type: EMAIL_ACTIONS.SUBMITTED_DIGEST, freq, subjectFreq, + ...referenceData(), }; notificationQueue.add(EMAIL_ACTIONS.SUBMITTED_DIGEST, data); @@ -856,6 +874,7 @@ export async function approvedDigest(freq, subjectFreq) { type: EMAIL_ACTIONS.APPROVED_DIGEST, freq, subjectFreq, + ...referenceData(), }; notificationQueue.add(EMAIL_ACTIONS.APPROVED_DIGEST, data); @@ -918,6 +937,7 @@ export async function recipientApprovedDigest(freq, subjectFreq) { type: EMAIL_ACTIONS.RECIPIENT_APPROVED_DIGEST, freq, subjectFreq, + ...referenceData(), }; notificationQueue.add(EMAIL_ACTIONS.RECIPIENT_APPROVED_DIGEST, data); @@ -992,51 +1012,136 @@ export const processNotificationQueue = () => { notificationQueue.on('completed', onCompletedNotification); increaseListeners(notificationQueue, 10); - notificationQueue.process(EMAIL_ACTIONS.NEEDS_ACTION, notifyChangesRequested); - notificationQueue.process(EMAIL_ACTIONS.SUBMITTED, notifyApproverAssigned); - notificationQueue.process(EMAIL_ACTIONS.APPROVED, notifyReportApproved); - notificationQueue.process(EMAIL_ACTIONS.COLLABORATOR_ADDED, notifyCollaboratorAssigned); - notificationQueue.process(EMAIL_ACTIONS.RECIPIENT_REPORT_APPROVED, notifyRecipientReportApproved); + notificationQueue.process( + EMAIL_ACTIONS.NEEDS_ACTION, + transactionQueueWrapper( + notifyApproverAssigned, + EMAIL_ACTIONS.NEEDS_ACTION, + ), + ); + + notificationQueue.process( + EMAIL_ACTIONS.SUBMITTED, + transactionQueueWrapper( + notifyApproverAssigned, + EMAIL_ACTIONS.SUBMITTED, + ), + ); + + notificationQueue.process( + EMAIL_ACTIONS.APPROVED, + transactionQueueWrapper( + notifyApproverAssigned, + EMAIL_ACTIONS.APPROVED, + ), + ); + + notificationQueue.process( + EMAIL_ACTIONS.COLLABORATOR_ADDED, + transactionQueueWrapper( + notifyApproverAssigned, + EMAIL_ACTIONS.COLLABORATOR_ADDED, + ), + ); + + notificationQueue.process( + EMAIL_ACTIONS.RECIPIENT_REPORT_APPROVED, + transactionQueueWrapper( + notifyApproverAssigned, + EMAIL_ACTIONS.RECIPIENT_REPORT_APPROVED, + ), + ); - notificationQueue.process(EMAIL_ACTIONS.NEEDS_ACTION_DIGEST, notifyDigest); - notificationQueue.process(EMAIL_ACTIONS.SUBMITTED_DIGEST, notifyDigest); - notificationQueue.process(EMAIL_ACTIONS.APPROVED_DIGEST, notifyDigest); - notificationQueue.process(EMAIL_ACTIONS.COLLABORATOR_DIGEST, notifyDigest); - notificationQueue.process(EMAIL_ACTIONS.RECIPIENT_REPORT_APPROVED_DIGEST, notifyDigest); + notificationQueue.process( + EMAIL_ACTIONS.NEEDS_ACTION_DIGEST, + transactionQueueWrapper( + notifyDigest, + EMAIL_ACTIONS.NEEDS_ACTION_DIGEST, + ), + ); + notificationQueue.process( + EMAIL_ACTIONS.SUBMITTED_DIGEST, + transactionQueueWrapper( + notifyDigest, + EMAIL_ACTIONS.SUBMITTED_DIGEST, + ), + ); + notificationQueue.process( + EMAIL_ACTIONS.APPROVED_DIGEST, + transactionQueueWrapper( + notifyDigest, + EMAIL_ACTIONS.APPROVED_DIGEST, + ), + ); + notificationQueue.process( + EMAIL_ACTIONS.COLLABORATOR_DIGEST, + transactionQueueWrapper( + notifyDigest, + EMAIL_ACTIONS.COLLABORATOR_DIGEST, + ), + ); + notificationQueue.process( + EMAIL_ACTIONS.RECIPIENT_REPORT_APPROVED_DIGEST, + transactionQueueWrapper( + notifyDigest, + EMAIL_ACTIONS.RECIPIENT_REPORT_APPROVED_DIGEST, + ), + ); notificationQueue.process( EMAIL_ACTIONS.TRAINING_REPORT_COLLABORATOR_ADDED, - sendTrainingReportNotification, + transactionQueueWrapper( + sendTrainingReportNotification, + EMAIL_ACTIONS.TRAINING_REPORT_COLLABORATOR_ADDED, + ), ); notificationQueue.process( EMAIL_ACTIONS.TRAINING_REPORT_SESSION_CREATED, - sendTrainingReportNotification, + transactionQueueWrapper( + sendTrainingReportNotification, + EMAIL_ACTIONS.TRAINING_REPORT_SESSION_CREATED, + ), ); notificationQueue.process( EMAIL_ACTIONS.TRAINING_REPORT_SESSION_COMPLETED, - sendTrainingReportNotification, + transactionQueueWrapper( + sendTrainingReportNotification, + EMAIL_ACTIONS.TRAINING_REPORT_SESSION_COMPLETED, + ), ); notificationQueue.process( EMAIL_ACTIONS.TRAINING_REPORT_EVENT_COMPLETED, - sendTrainingReportNotification, + transactionQueueWrapper( + sendTrainingReportNotification, + EMAIL_ACTIONS.TRAINING_REPORT_EVENT_COMPLETED, + ), ); notificationQueue.process( EMAIL_ACTIONS.TRAINING_REPORT_POC_ADDED, - sendTrainingReportNotification, + transactionQueueWrapper( + sendTrainingReportNotification, + EMAIL_ACTIONS.TRAINING_REPORT_POC_ADDED, + ), ); notificationQueue.process( EMAIL_ACTIONS.TRAINING_REPORT_POC_VISION_GOAL_COMPLETE, - sendTrainingReportNotification, + transactionQueueWrapper( + sendTrainingReportNotification, + EMAIL_ACTIONS.TRAINING_REPORT_POC_VISION_GOAL_COMPLETE, + ), ); notificationQueue.process( EMAIL_ACTIONS.TRAINING_REPORT_POC_SESSION_COMPLETE, - sendTrainingReportNotification, + transactionQueueWrapper( + sendTrainingReportNotification, + EMAIL_ACTIONS.TRAINING_REPORT_POC_SESSION_COMPLETE, + ), ); }; diff --git a/src/lib/mailer/index.test.js b/src/lib/mailer/index.test.js index 9562612b43..2d5b528fb5 100644 --- a/src/lib/mailer/index.test.js +++ b/src/lib/mailer/index.test.js @@ -132,7 +132,6 @@ const submittedReport = { ...reportObject, activityRecipients: [{ grantId: 1 }], submissionStatus: REPORT_STATUSES.SUBMITTED, - // calculatedStatus: REPORT_STATUSES.SUBMITTED, numberOfParticipants: 1, deliveryMethod: 'method', duration: 0, @@ -170,9 +169,10 @@ describe('mailer tests', () => { process.env = oldEnv; await db.sequelize.close(); }); + describe('Changes requested by manager', () => { it('Tests that an email is sent', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyChangesRequested({ data: { report: mockReport, @@ -189,12 +189,12 @@ describe('mailer tests', () => { ]); const message = JSON.parse(email.message); expect(message.subject).toBe(`Activity Report ${mockReport.displayId}: Changes requested`); - expect(message.text).toContain(`${mockManager.name} requested changed to report ${mockReport.displayId}.`); + expect(message.text).toContain(`${mockManager.name} requested changes to report ${mockReport.displayId}.`); expect(message.text).toContain(mockApprover.note); expect(message.text).toContain(reportPath); }); it('Tests that an email is not sent if no recipients', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyChangesRequested({ data: { report: mockReport, @@ -206,19 +206,20 @@ describe('mailer tests', () => { expect(email).toBe(null); }); it('Tests that emails are not sent without SEND_NOTIFICATIONS', async () => { - process.env.SEND_NOTIFICATIONS = false; - await expect(notifyChangesRequested({ + process.env.SEND_NOTIFICATIONS = 'false'; + const email = await notifyChangesRequested({ data: { report: mockReport }, - }, jsonTransport)).toBeNull(); + }, jsonTransport); + expect(email).toBeNull(); }); }); + describe('Report Approved', () => { it('Tests that an email is sent', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyReportApproved({ data: { report: mockReport, - approver: mockApprover, authorWithSetting: mockReport.author, collabsWithSettings: [mockCollaborator1, mockCollaborator2], }, @@ -235,11 +236,10 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('Tests that an email is not sent if no recipients', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyReportApproved({ data: { report: mockReport, - approver: mockApprover, authorWithSetting: null, collabsWithSettings: [], }, @@ -247,15 +247,17 @@ describe('mailer tests', () => { expect(email).toBe(null); }); it('Tests that emails are not sent without SEND_NOTIFICATIONS', async () => { - process.env.SEND_NOTIFICATIONS = false; - await expect(notifyReportApproved({ + process.env.SEND_NOTIFICATIONS = 'false'; + const email = await notifyReportApproved({ data: { report: mockReport }, - }, jsonTransport)).toBeNull(); + }, jsonTransport); + expect(email).toBeNull(); }); }); + describe('Program Specialists: Recipient Report Approved', () => { it('Tests that an email is sent', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyRecipientReportApproved({ data: { report: mockReport, @@ -272,7 +274,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('Tests that an email is not sent if no program specialists/recipients', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyRecipientReportApproved({ data: { report: mockReport, @@ -283,19 +285,21 @@ describe('mailer tests', () => { expect(email).toBe(null); }); it('Tests that emails are not sent without SEND_NOTIFICATIONS', async () => { - process.env.SEND_NOTIFICATIONS = false; - await expect(notifyRecipientReportApproved({ + process.env.SEND_NOTIFICATIONS = 'false'; + const email = await notifyRecipientReportApproved({ data: { report: mockReport, programSpecialists: [mockProgramSpecialist], recipients: [mockRecipient], }, - }, jsonTransport)).toBeNull(); + }, jsonTransport); + expect(email).toBeNull(); }); }); + describe('Manager Approval Requested', () => { it('Tests that an email is sent', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyApproverAssigned({ data: { report: mockReport, newApprover: mockApprover }, }, jsonTransport); @@ -309,15 +313,17 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('Tests that emails are not sent without SEND_NOTIFICATIONS', async () => { - process.env.SEND_NOTIFICATIONS = false; - expect(notifyApproverAssigned({ + process.env.SEND_NOTIFICATIONS = 'false'; + const email = await notifyApproverAssigned({ data: { report: mockReport }, - }, jsonTransport)).toBeNull(); + }, jsonTransport); + expect(email).toBeNull(); }); }); + describe('Add Collaborators', () => { it('Tests that an email is sent', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyCollaboratorAssigned({ data: { report: mockReport, newCollaborator: mockNewCollaborator }, }, jsonTransport); @@ -331,16 +337,17 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('Tests that emails are not sent without SEND_NOTIFICATIONS', async () => { - process.env.SEND_NOTIFICATIONS = false; - expect(notifyCollaboratorAssigned({ + process.env.SEND_NOTIFICATIONS = 'false'; + const email = await notifyCollaboratorAssigned({ data: { report: mockReport, newCollaborator: mockCollaborator1 }, - }, jsonTransport)).toBeNull(); + }, jsonTransport); + expect(email).toBeNull(); }); }); describe('sendTrainingReportNotification', () => { it('Tests that an email is sent', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; process.env.CI = ''; const data = { emailTo: [mockNewCollaborator.email], @@ -366,7 +373,7 @@ describe('mailer tests', () => { expect(message.text).toContain('/asdf/'); }); it('Honors no send', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; process.env.CI = ''; const data = { emailTo: [`no-send_${mockNewCollaborator.email}`], @@ -385,7 +392,7 @@ describe('mailer tests', () => { expect(email).toBeNull(); }); it('Tests that emails are not sent without SEND_NOTIFICATIONS', async () => { - process.env.SEND_NOTIFICATIONS = false; + process.env.SEND_NOTIFICATIONS = 'false'; const data = { emailTo: [mockNewCollaborator.email], templatePath: 'tr_session_completed', @@ -397,14 +404,15 @@ describe('mailer tests', () => { displayId: 'mockReport-1', }, }; - await expect(sendTrainingReportNotification({ + const email = await sendTrainingReportNotification({ data, - }, jsonTransport)).resolves.toBeNull(); + }, jsonTransport); + expect(email).toBeNull(); }); it('Tests that emails are not sent on CI', async () => { - process.env.SEND_NOTIFICATIONS = true; - process.env.CI = true; + process.env.SEND_NOTIFICATIONS = 'true'; + process.env.CI = 'true'; const data = { emailTo: [mockNewCollaborator.email], templatePath: 'tr_session_completed', @@ -416,15 +424,16 @@ describe('mailer tests', () => { displayId: 'mockReport-1', }, }; - await expect(sendTrainingReportNotification({ + const email = await sendTrainingReportNotification({ data, - }, jsonTransport)).resolves.toBeNull(); + }, jsonTransport); + expect(email).toBeNull(); }); }); describe('Collaborators digest', () => { it('tests that an email is sent for a daily setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -451,7 +460,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent for a weekly setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -476,7 +485,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent for a monthly setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -501,7 +510,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent if there are no new collaborator notifications', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -523,21 +532,22 @@ describe('mailer tests', () => { }); it('tests that emails are not sent without SEND_NOTIFICATIONS', async () => { - process.env.SEND_NOTIFICATIONS = false; - await expect(notifyDigest({ + process.env.SEND_NOTIFICATIONS = 'false'; + const email = await notifyDigest({ data: { user: mockNewCollaborator, reports: [], type: EMAIL_ACTIONS.COLLABORATOR_DIGEST, freq: EMAIL_DIGEST_FREQ.DAILY, }, - }, jsonTransport)).toBeNull(); + }, jsonTransport); + expect(email).toBeNull(); }); }); describe('Changes requested digest', () => { it('tests that an email is sent for a daily setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -564,7 +574,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent for a weekly setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -589,7 +599,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent for a monthly setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -614,7 +624,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent if there are no changes requested notifications', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -638,7 +648,7 @@ describe('mailer tests', () => { describe('Submitted digest', () => { it('tests that an email is sent for a daily setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -665,7 +675,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent for a weekly setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -690,7 +700,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent for a monthly setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -715,7 +725,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent if there are no submitted notifications', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -739,7 +749,7 @@ describe('mailer tests', () => { describe('Approved digest', () => { it('tests that an email is sent for a daily setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -766,7 +776,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent for a weekly setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -791,7 +801,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent for a monthly setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -816,7 +826,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent if there are no approved reports notifications', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { user: mockNewCollaborator, @@ -840,7 +850,7 @@ describe('mailer tests', () => { describe('Program Specialist: Report approved digest', () => { it('tests that an email is sent for a daily setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { reports: [mockReport], @@ -865,7 +875,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent for a weekly setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { reports: [mockReport], @@ -890,7 +900,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent for a monthly setting', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { reports: [mockReport], @@ -915,7 +925,7 @@ describe('mailer tests', () => { expect(message.text).toContain(reportPath); }); it('tests that an email is sent if there are no approved reports notifications', async () => { - process.env.SEND_NOTIFICATIONS = true; + process.env.SEND_NOTIFICATIONS = 'true'; const email = await notifyDigest({ data: { reports: [], diff --git a/src/lib/maintenance/common.js b/src/lib/maintenance/common.js index 3f4849eddd..5bcb5239c4 100644 --- a/src/lib/maintenance/common.js +++ b/src/lib/maintenance/common.js @@ -5,6 +5,8 @@ const { MaintenanceLog } = require('../../models'); const { MAINTENANCE_TYPE, MAINTENANCE_CATEGORY } = require('../../constants'); const { auditLogger, logger } = require('../../logger'); const { default: LockManager } = require('../lockManager'); +const { default: transactionQueueWrapper } = require('../../workers/transactionWrapper'); +const { default: referenceData } = require('../../workers/referenceData'); const maintenanceQueue = newQueue('maintenance'); const maintenanceQueueProcessors = {}; @@ -103,7 +105,13 @@ const processMaintenanceQueue = () => { // Process each category in the queue using its corresponding processor Object.entries(maintenanceQueueProcessors) - .map(([category, processor]) => maintenanceQueue.process(category, processor)); + .map(([category, processor]) => maintenanceQueue.process( + category, + transactionQueueWrapper( + processor, + category, + ), + )); }; /** @@ -124,7 +132,7 @@ const enqueueMaintenanceJob = async ( if (category in maintenanceQueueProcessors) { try { // Add the job to the maintenance queue - maintenanceQueue.add(category, data); + maintenanceQueue.add(category, { ...data, ...referenceData() }); } catch (err) { // Log any errors that occur when adding the job to the queue auditLogger.error(err); diff --git a/src/lib/maintenance/common.test.js b/src/lib/maintenance/common.test.js index d4623cc750..f3f0a87f5b 100644 --- a/src/lib/maintenance/common.test.js +++ b/src/lib/maintenance/common.test.js @@ -21,6 +21,7 @@ const { MAINTENANCE_TYPE, MAINTENANCE_CATEGORY } = require('../../constants'); const { MaintenanceLog } = require('../../models'); const { auditLogger, logger } = require('../../logger'); +const { default: transactionWrapper } = require('../../workers/transactionWrapper'); jest.mock('../../models', () => ({ MaintenanceLog: { @@ -113,13 +114,25 @@ describe('Maintenance Queue', () => { addQueueProcessor(category1, processor1); addQueueProcessor(category2, processor2); processMaintenanceQueue(); + expect(maintenanceQueue.process).toHaveBeenCalledTimes(3); - expect(maintenanceQueue.process).toHaveBeenCalledWith(category1, processor1); - expect(maintenanceQueue.process).toHaveBeenCalledWith(category2, processor2); expect(maintenanceQueue.process) - .toHaveBeenCalledWith( + .toHaveBeenNthCalledWith( + 1, MAINTENANCE_CATEGORY.MAINTENANCE, - maintenance, + expect.any(Function), + ); + expect(maintenanceQueue.process) + .toHaveBeenNthCalledWith( + 2, + category1, + expect.any(Function), + ); + expect(maintenanceQueue.process) + .toHaveBeenNthCalledWith( + 3, + category2, + expect.any(Function), ); }); }); @@ -129,7 +142,15 @@ describe('Maintenance Queue', () => { jest.clearAllMocks(); }); it('should add a job to the maintenance queue if a processor is defined for the given category', () => { - const data = { test: 'enqueueMaintenanceJob - should add a job to the maintenance queue if a processor is defined for the given category' }; + const data = { + test: 'enqueueMaintenanceJob - should add a job to the maintenance queue if a processor is defined for the given category', + referenceData: { + impersonationId: undefined, + sessionSig: undefined, + transactionId: undefined, + userId: undefined, + }, + }; const category = 'test-category'; const processor = jest.fn(); addQueueProcessor(category, processor); diff --git a/src/routes/courses/handlers.ts b/src/routes/courses/handlers.ts index 5c97b894fd..4ebaccc83f 100644 --- a/src/routes/courses/handlers.ts +++ b/src/routes/courses/handlers.ts @@ -28,7 +28,7 @@ export async function allCourses(req: Request, res: Response) { const courses = await getAllCourses(); res.json(courses); } catch (err) { - await handleErrors(err, req, res, logContext); + await handleErrors(req, res, err, logContext); } } @@ -38,7 +38,7 @@ export async function getCourseById(req: Request, res: Response) { const course = await getById(Number(id)); res.json(course); } catch (err) { - await handleErrors(err, req, res, logContext); + await handleErrors(req, res, err, logContext); } } @@ -70,7 +70,7 @@ export async function updateCourseById(req: Request, res: Response) { res.json(newCourse); } catch (err) { - await handleErrors(err, req, res, logContext); + await handleErrors(req, res, err, logContext); } } @@ -90,7 +90,7 @@ export async function createCourseByName(req: Request, res: Response) { res.json(course); } catch (err) { - await handleErrors(err, req, res, logContext); + await handleErrors(req, res, err, logContext); } } @@ -119,7 +119,7 @@ export async function deleteCourseById(req: Request, res: Response) { } res.status(204).send(); } catch (err) { - await handleErrors(err, req, res, logContext); + await handleErrors(req, res, err, logContext); } } diff --git a/src/routes/transactionWrapper.js b/src/routes/transactionWrapper.js index 8bd0b46b84..50afd9cfe6 100644 --- a/src/routes/transactionWrapper.js +++ b/src/routes/transactionWrapper.js @@ -1,3 +1,4 @@ +import httpContext from 'express-http-context'; import { sequelize } from '../models'; import { addAuditTransactionSettings, removeFromAuditedTransactions } from '../models/auditModelGenerator'; import handleErrors from '../lib/apiErrorHandler'; @@ -14,7 +15,8 @@ export default function transactionWrapper(originalFunction, context = '') { const startTime = Date.now(); try { // eslint-disable-next-line @typescript-eslint/return-await - return await sequelize.transaction(async () => { + return await sequelize.transaction(async (transaction) => { + httpContext.set('transactionId', transaction.id); try { await addAuditTransactionSettings(sequelize, null, null, 'transaction', originalFunction.name); const result = await originalFunction(req, res, next); diff --git a/src/services/requestErrors.js b/src/services/requestErrors.js index 8d823e3502..56dafd5696 100644 --- a/src/services/requestErrors.js +++ b/src/services/requestErrors.js @@ -1,5 +1,3 @@ -import models from '../models'; - export default async function createRequestError({ operation, uri, @@ -8,11 +6,13 @@ export default async function createRequestError({ responseBody = 'N/A', responseCode = 'N/A', }) { + // eslint-disable-next-line global-require + const { RequestErrors } = require('../models'); try { const requestErrorBody = { operation, uri, method, requestBody, responseBody, responseCode, }; - const requestError = await models.RequestErrors.create(requestErrorBody, { transaction: null }); + const requestError = await RequestErrors.create(requestErrorBody, { transaction: null }); return requestError.id; } catch (err) { throw new Error('Error creating RequestError entry'); @@ -20,12 +20,14 @@ export default async function createRequestError({ } export async function requestErrors({ filter = '{}', range = '[0,9]', sort = '["createdAt","DESC"]' } = {}) { + // eslint-disable-next-line global-require + const { RequestErrors } = require('../models'); const offset = JSON.parse(range)[0]; const limit = JSON.parse(range)[1]; const order = JSON.parse(sort); const where = JSON.parse(filter); - return models.RequestErrors.findAndCountAll({ + return RequestErrors.findAndCountAll({ where, order: [order], offset, @@ -34,20 +36,26 @@ export async function requestErrors({ filter = '{}', range = '[0,9]', sort = '[" } export async function requestErrorById(id) { - return models.RequestErrors.findOne({ + // eslint-disable-next-line global-require + const { RequestErrors } = require('../models'); + return RequestErrors.findOne({ where: { id }, }); } export async function requestErrorsByIds({ filter = '{}' } = {}) { - return models.RequestErrors.findAll({ + // eslint-disable-next-line global-require + const { RequestErrors } = require('../models'); + return RequestErrors.findAll({ where: JSON.parse(filter), attributes: ['id'], }); } export async function delRequestErrors({ filter = '{}' } = {}) { - return models.RequestErrors.destroy({ + // eslint-disable-next-line global-require + const { RequestErrors } = require('../models'); + return RequestErrors.destroy({ where: JSON.parse(filter), }); } diff --git a/src/services/resourceQueue.js b/src/services/resourceQueue.js index 1a00fdc107..5c957500cd 100644 --- a/src/services/resourceQueue.js +++ b/src/services/resourceQueue.js @@ -2,6 +2,8 @@ import newQueue, { increaseListeners } from '../lib/queue'; import { RESOURCE_ACTIONS } from '../constants'; import { logger, auditLogger } from '../logger'; import { getResourceMetaDataJob } from '../lib/resource'; +import transactionQueueWrapper from '../workers/transactionWrapper'; +import referenceData from '../workers/referenceData'; const resourceQueue = newQueue('resource'); @@ -17,6 +19,7 @@ const addGetResourceMetadataToQueue = async (id, url) => { resourceId: id, resourceUrl: url, key: RESOURCE_ACTIONS.GET_METADATA, + ...referenceData(), }; return resourceQueue.add( RESOURCE_ACTIONS.GET_METADATA, @@ -47,7 +50,10 @@ const processResourceQueue = () => { // Get resource metadata. resourceQueue.process( RESOURCE_ACTIONS.GET_METADATA, - getResourceMetaDataJob, + transactionQueueWrapper( + getResourceMetaDataJob, + RESOURCE_ACTIONS.GET_METADATA, + ), ); }; diff --git a/src/services/s3Queue.js b/src/services/s3Queue.js index 05c02a233d..e28a4730b6 100644 --- a/src/services/s3Queue.js +++ b/src/services/s3Queue.js @@ -2,6 +2,8 @@ import newQueue, { increaseListeners } from '../lib/queue'; import { S3_ACTIONS } from '../constants'; import { logger, auditLogger } from '../logger'; import { deleteFileFromS3Job } from '../lib/s3'; +import transactionQueueWrapper from '../workers/transactionWrapper'; +import referenceData from '../workers/referenceData'; const s3Queue = newQueue('s3'); @@ -11,6 +13,7 @@ const addDeleteFileToQueue = (id, key) => { fileId: id, fileKey: key, key: S3_ACTIONS.DELETE_FILE, + ...referenceData(), }; s3Queue.add(S3_ACTIONS.DELETE_FILE, data); return data; @@ -33,7 +36,10 @@ const processS3Queue = () => { // Delete S3 file. s3Queue.process( S3_ACTIONS.DELETE_FILE, - deleteFileFromS3Job, + transactionQueueWrapper( + deleteFileFromS3Job, + S3_ACTIONS.DELETE_FILE, + ), ); }; diff --git a/src/services/scanQueue.js b/src/services/scanQueue.js index ca4a4b7aac..3fa1575f5b 100644 --- a/src/services/scanQueue.js +++ b/src/services/scanQueue.js @@ -1,6 +1,8 @@ import newQueue, { increaseListeners } from '../lib/queue'; import { logger, auditLogger } from '../logger'; import processFile from '../workers/files'; +import transactionQueueWrapper from '../workers/transactionWrapper'; +import referenceData from '../workers/referenceData'; const scanQueue = newQueue('scan'); const addToScanQueue = (fileKey) => { @@ -12,7 +14,10 @@ const addToScanQueue = (fileKey) => { }; return scanQueue.add( - fileKey, + { + ...fileKey, + ...referenceData(), + }, { attempts: retries, backoff: backOffOpts, @@ -35,7 +40,8 @@ const processScanQueue = () => { scanQueue.on('failed', onFailedScanQueue); scanQueue.on('completed', onCompletedScanQueue); increaseListeners(scanQueue); - scanQueue.process((job) => processFile(job.data.key)); + const process = (job) => processFile(job.data.key); + scanQueue.process(transactionQueueWrapper(process)); }; export { diff --git a/src/worker.ts b/src/worker.ts index 3dd7d1f37a..2456be9d12 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -6,6 +6,7 @@ if (process.env.NODE_ENV === 'production') { import {} from 'dotenv/config'; import throng from 'throng'; +import httpContext from 'express-http-context'; import { processScanQueue, } from './services/scanQueue'; @@ -25,22 +26,26 @@ import { // Number of workers to spawn const workers = process.env.WORKER_CONCURRENCY || 2; -// Pull jobs off the redis queue and process them. +// Wrap your process functions to use httpContext async function start(context: { id: number }) { - // File Scanning Queue - processScanQueue(); + httpContext.ns.run(() => { + httpContext.set('workerId', context.id); - // S3 Queue. - processS3Queue(); + // File Scanning Queue + processScanQueue(); - // Resource Queue. - processResourceQueue(); + // S3 Queue. + processS3Queue(); - // Notifications Queue - processNotificationQueue(); + // Resource Queue. + processResourceQueue(); - // Maintenance Queue - processMaintenanceQueue(); + // Notifications Queue + processNotificationQueue(); + + // Maintenance Queue + processMaintenanceQueue(); + }); } // spawn workers and start them diff --git a/src/workers/referenceData.ts b/src/workers/referenceData.ts new file mode 100644 index 0000000000..ea21b58ee6 --- /dev/null +++ b/src/workers/referenceData.ts @@ -0,0 +1,28 @@ +import httpContext from 'express-http-context'; + +interface ReferenceData { + referenceData: { + userId: number | undefined; + impersonationId: number | undefined; + transactionId: string | undefined; + sessionSig: string | undefined; + } +} + +const referenceData = (): ReferenceData => { + const userId = httpContext.get('loggedUser') as number | undefined; + const impersonationId = httpContext.get('impersonationUserId') as number | undefined; + const transactionId = httpContext.get('transactionId') as string | undefined; + const sessionSig = httpContext.get('sessionSig') as string | undefined; + + return { + referenceData: { + userId, + impersonationId, + transactionId, + sessionSig, + }, + }; +}; + +export default referenceData; diff --git a/src/workers/transactionWrapper.ts b/src/workers/transactionWrapper.ts new file mode 100644 index 0000000000..ab834217dd --- /dev/null +++ b/src/workers/transactionWrapper.ts @@ -0,0 +1,51 @@ +import httpContext from 'express-http-context'; +import { addAuditTransactionSettings, removeFromAuditedTransactions } from '../models/auditModelGenerator'; +import { sequelize } from '../models'; +import { handleWorkerErrors } from '../lib/apiErrorHandler'; +import { auditLogger } from '../logger'; + +const namespace = 'WORKER:WRAPPER'; +const logContext = { + namespace, +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Job = any; // Define the correct type for your job here + +const transactionQueueWrapper = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + originalFunction: (job: Job) => Promise, + context = '', + // eslint-disable-next-line @typescript-eslint/no-explicit-any +) => async (job: Job): Promise => { + const startTime = Date.now(); + return httpContext.ns.runPromise(async () => { + httpContext.set('loggedUser', job.referenceData.userId); + httpContext.set('impersonationUserId', job.referenceData.impersonationUserId); + httpContext.set('sessionSig', job.id); + httpContext.set('auditDescriptor', originalFunction.name); + try { + // eslint-disable-next-line @typescript-eslint/return-await + return await sequelize.transaction(async (transaction) => { + httpContext.set('transactionId', transaction.id); + try { + // eslint-disable-next-line + await addAuditTransactionSettings(sequelize, null, null, 'transaction', originalFunction.name); + const result = await originalFunction(job); + const duration = Date.now() - startTime; + auditLogger.info(`${originalFunction.name} ${context} execution time: ${duration}ms`); + removeFromAuditedTransactions(); + return result; + } catch (err) { + auditLogger.error(`Error executing ${originalFunction.name} ${context}: ${(err as Error).message}`); + throw err; + } + }); + } catch (err) { + await handleWorkerErrors(job, err, logContext); + throw err; + } + }); +}; + +export default transactionQueueWrapper;