diff --git a/server/notifications/scenarioMapper.js b/server/notifications/scenarioMapper.js index 7b623a517..6a8b59ba3 100644 --- a/server/notifications/scenarioMapper.js +++ b/server/notifications/scenarioMapper.js @@ -71,6 +71,7 @@ const notificationScenarios = [ key: "job-added-to-production", table: "jobs", fields: ["inproduction"], + onlyTruthyValues: ["inproduction"], builder: jobsAddedToProductionBuilder }, { @@ -129,6 +130,19 @@ const notificationScenarios = [ } ]; +/** + * Returns an array of scenarios that match the given event data. + * + * @param {Object} eventData - The parsed event data. + * Expected properties: + * - table: an object with a `name` property (e.g. { name: "tasks", schema: "public" }) + * - changedFieldNames: an array of changed field names (e.g. [ "description", "updated_at" ]) + * - isNew: boolean indicating whether the record is new or updated + * - data: the new data object (used to check field values) + * - (other properties may be added such as jobWatchers, bodyShopId, etc.) + * + * @returns {Array} An array of matching scenario objects. + */ /** * Returns an array of scenarios that match the given event data. * @@ -181,6 +195,36 @@ const getMatchingScenarios = (eventData) => } } + // OnlyTruthyValues logic: + // If onlyTruthyValues is defined, check that the new values of specified fields (or all changed fields if true) + // are truthy. If an array, only check the listed fields, which must be in scenario.fields. + if (Object.prototype.hasOwnProperty.call(scenario, "onlyTruthyValues")) { + let fieldsToCheck; + + if (scenario.onlyTruthyValues === true) { + // If true, check all fields in the scenario that changed + fieldsToCheck = scenario.fields.filter((field) => eventData.changedFieldNames.includes(field)); + } else if (Array.isArray(scenario.onlyTruthyValues) && scenario.onlyTruthyValues.length > 0) { + // If an array, check only the specified fields, ensuring they are in scenario.fields + fieldsToCheck = scenario.onlyTruthyValues.filter( + (field) => scenario.fields.includes(field) && eventData.changedFieldNames.includes(field) + ); + // If no fields in onlyTruthyValues match the scenario’s fields or changed fields, skip this scenario + if (fieldsToCheck.length === 0) { + return false; + } + } else { + // Invalid onlyTruthyValues (not true or a non-empty array), skip this scenario + return false; + } + + // Ensure all fields to check have truthy new values + const allTruthy = fieldsToCheck.every((field) => Boolean(eventData.data[field])); + if (!allTruthy) { + return false; + } + } + return true; }); diff --git a/server/notifications/scenarioParser.js b/server/notifications/scenarioParser.js index 1a5e2bf97..180493880 100644 --- a/server/notifications/scenarioParser.js +++ b/server/notifications/scenarioParser.js @@ -31,7 +31,9 @@ const scenarioParser = async (req, jobIdField) => { const { event, trigger, table } = req.body; const { logger } = req; - // Validate we know what user committed the action that fired the parser + // Step 1: Validate we know what user committed the action that fired the parser + // console.log("Step 1"); + const hasuraUserId = event?.session_variables?.["x-hasura-user-id"]; // Bail if we don't know who started the scenario @@ -45,7 +47,9 @@ const scenarioParser = async (req, jobIdField) => { throw new Error("Missing required request fields: event data, trigger, or table."); } - // Step 1a: Extract just the jobId using the provided jobIdField + // Step 2: Extract just the jobId using the provided jobIdField + // console.log("Step 2"); + let jobId = null; if (jobIdField) { let keyName = jobIdField; @@ -61,7 +65,9 @@ const scenarioParser = async (req, jobIdField) => { return; } - // Step 2: Query job watchers associated with the job ID using GraphQL + // Step 3: Query job watchers associated with the job ID using GraphQL + // console.log("Step 3"); + const watcherData = await gqlClient.request(queries.GET_JOB_WATCHERS, { jobid: jobId }); @@ -85,7 +91,9 @@ const scenarioParser = async (req, jobIdField) => { return; } - // Step 1b: Perform the full event diff now that we know there are watchers + // Step 5: Perform the full event diff now that we know there are watchers + // console.log("Step 5"); + const eventData = await eventParser({ newData: event.data.new, oldData: event.data.old, @@ -94,7 +102,9 @@ const scenarioParser = async (req, jobIdField) => { jobId }); - // Step 3: Extract body shop information from the job data + // Step 6: Extract body shop information from the job data + // console.log("Step 6"); + const bodyShopId = watcherData?.job?.bodyshop?.id; const bodyShopName = watcherData?.job?.bodyshop?.shopname; const jobRoNumber = watcherData?.job?.ro_number; @@ -105,7 +115,9 @@ const scenarioParser = async (req, jobIdField) => { throw new Error("No bodyshop data found for this job."); } - // Step 4: Identify scenarios that match the event data and job context + // Step 7: Identify scenarios that match the event data and job context + // console.log("Step 7"); + const matchingScenarios = getMatchingScenarios({ ...eventData, jobWatchers, @@ -132,7 +144,9 @@ const scenarioParser = async (req, jobIdField) => { matchingScenarios }; - // Step 5: Query notification settings for the job watchers + // Step 8: Query notification settings for the job watchers + // console.log("Step 8"); + const associationsData = await gqlClient.request(queries.GET_NOTIFICATION_ASSOCIATIONS, { emails: jobWatchers.map((x) => x.email), shopid: bodyShopId @@ -148,7 +162,9 @@ const scenarioParser = async (req, jobIdField) => { return; } - // Step 6: Filter scenario watchers based on their enabled notification methods + // Step 9: Filter scenario watchers based on their enabled notification methods + // console.log("Step 9"); + finalScenarioData.matchingScenarios = finalScenarioData.matchingScenarios.map((scenario) => ({ ...scenario, scenarioWatchers: associationsData.associations @@ -186,7 +202,9 @@ const scenarioParser = async (req, jobIdField) => { return; } - // Step 7: Build and collect scenarios to dispatch notifications for + // Step 10: Build and collect scenarios to dispatch notifications for + // console.log("Step 10"); + const scenariosToDispatch = []; for (const scenario of finalScenarioData.matchingScenarios) { @@ -211,7 +229,9 @@ const scenarioParser = async (req, jobIdField) => { continue; } - // Step 8: Filter scenario fields to include only those that changed + // Step 11: Filter scenario fields to include only those that changed + // console.log("Step 11"); + const filteredScenarioFields = scenario.fields?.filter((field) => eventData.changedFieldNames.includes(field)) || []; @@ -242,7 +262,9 @@ const scenarioParser = async (req, jobIdField) => { return; } - // Step 8: Dispatch email notifications to the email queue + // Step 12: Dispatch email notifications to the email queue + // console.log("Step 12"); + const emailsToDispatch = scenariosToDispatch.map((scenario) => scenario?.email); if (!isEmpty(emailsToDispatch)) { dispatchEmailsToQueue({ emailsToDispatch, logger }).catch((e) => @@ -253,7 +275,9 @@ const scenarioParser = async (req, jobIdField) => { ); } - // Step 9: Dispatch app notifications to the app queue + // Step 13: Dispatch app notifications to the app queue + // console.log("Step 13"); + const appsToDispatch = scenariosToDispatch.map((scenario) => scenario?.app); if (!isEmpty(appsToDispatch)) { dispatchAppsToQueue({ appsToDispatch, logger }).catch((e) =>