From 76ec755d079fa12575e31bde6818dcef520cd2df Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 4 Mar 2025 17:50:58 -0500 Subject: [PATCH] IO-3166-Global-Notifications-Part-2 - Checkpoint --- hasura/metadata/tables.yaml | 2 - server/notifications/scenarioMapper.js | 76 +++++++++++++++----------- server/notifications/scenarioParser.js | 20 ++++--- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 04ee6d648..258a6860b 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -3280,8 +3280,6 @@ - name: notifications_joblines definition: enable_manual: false - insert: - columns: '*' update: columns: - critical diff --git a/server/notifications/scenarioMapper.js b/server/notifications/scenarioMapper.js index 4e41c0280..8a3923997 100644 --- a/server/notifications/scenarioMapper.js +++ b/server/notifications/scenarioMapper.js @@ -15,6 +15,7 @@ const { supplementImportedBuilder, partMarkedBackOrderedBuilder } = require("./scenarioBuilders"); +const logger = require("../utils/logger"); const { isFunction } = require("lodash"); /** @@ -28,7 +29,8 @@ const { isFunction } = require("lodash"); * - onNew {boolean|Array}: Indicates whether the scenario should be triggered on new data. * - builder {Function}: A function to handle the scenario. * - onlyTruthyValues {boolean|Array}: Specifies fields that must have truthy values for the scenario to match. - * */ + * - filterCallback {Function}: Optional callback (sync or async) to further filter the scenario based on event data (returns boolean). + */ const notificationScenarios = [ { key: "job-assigned-to-me", @@ -116,7 +118,19 @@ const notificationScenarios = [ key: "part-marked-back-ordered", table: "joblines", fields: ["status"], - builder: partMarkedBackOrderedBuilder + builder: partMarkedBackOrderedBuilder, + filterCallback: async ({ eventData, getBodyshopFromRedis }) => { + try { + const bodyshop = await getBodyshopFromRedis(eventData.bodyShopId); + return eventData?.data?.status !== bodyshop?.md_order_statuses?.default_bo; + } catch (err) { + logger.log("notifications-error-parts-marked-back-ordered", "error", "notifications", "mapper", { + message: err?.message, + stack: err?.stack + }); + return false; + } + } }, // -------------- Difficult --------------- // Holding off on this one for now @@ -129,6 +143,7 @@ const notificationScenarios = [ /** * Returns an array of scenarios that match the given event data. + * Supports asynchronous callbacks for additional filtering. * * @param {Object} eventData - The parsed event data. * Expected properties: @@ -137,28 +152,16 @@ const notificationScenarios = [ * - 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. + * @param {Function} getBodyshopFromRedis - Function to retrieve bodyshop data from Redis. + * @returns {Promise>} A promise resolving to an array of matching scenario objects. */ -/** - * 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. - */ -const getMatchingScenarios = (eventData) => - notificationScenarios.filter((scenario) => { +const getMatchingScenarios = async (eventData, getBodyshopFromRedis) => { + const matches = []; + for (const scenario of notificationScenarios) { // If eventData has a table, then only scenarios with a table property that matches should be considered. if (eventData.table) { if (!scenario.table || eventData.table.name !== scenario.table) { - return false; + continue; } } @@ -166,9 +169,9 @@ const getMatchingScenarios = (eventData) => // Allow onNew to be either a boolean or an array of booleans. if (Object.prototype.hasOwnProperty.call(scenario, "onNew")) { if (Array.isArray(scenario.onNew)) { - if (!scenario.onNew.includes(eventData.isNew)) return false; + if (!scenario.onNew.includes(eventData.isNew)) continue; } else { - if (eventData.isNew !== scenario.onNew) return false; + if (eventData.isNew !== scenario.onNew) continue; } } @@ -176,7 +179,7 @@ const getMatchingScenarios = (eventData) => if (scenario.fields && scenario.fields.length > 0) { const hasMatchingField = scenario.fields.some((field) => eventData.changedFieldNames.includes(field)); if (!hasMatchingField) { - return false; + continue; } } @@ -196,30 +199,37 @@ const getMatchingScenarios = (eventData) => ); // If no fields in onlyTruthyValues match the scenario’s fields or changed fields, skip this scenario if (fieldsToCheck.length === 0) { - return false; + continue; } } else { // Invalid onlyTruthyValues (not true or a non-empty array), skip this scenario - return false; + continue; } // Ensure all fields to check have truthy new values const allTruthy = fieldsToCheck.every((field) => Boolean(eventData.data[field])); if (!allTruthy) { - return false; + continue; } } - // Execute the callback if defined, passing eventData, and filter based on its return value - if (isFunction(scenario?.callback)) { - const shouldInclude = scenario.callback(eventData); - if (!shouldInclude) { - return false; + // Execute the callback if defined, supporting both sync and async, and filter based on its return value + if (isFunction(scenario?.filterCallback)) { + const shouldFilter = await Promise.resolve( + scenario.filterCallback({ + eventData, + getBodyshopFromRedis + }) + ); + if (shouldFilter) { + continue; } } - return true; - }); + matches.push(scenario); + } + return matches; +}; module.exports = { notificationScenarios, diff --git a/server/notifications/scenarioParser.js b/server/notifications/scenarioParser.js index 11c9f979e..1f6afea03 100644 --- a/server/notifications/scenarioParser.js +++ b/server/notifications/scenarioParser.js @@ -29,7 +29,10 @@ const FILTER_SELF_FROM_WATCHERS = process.env?.FILTER_SELF_FROM_WATCHERS !== "fa */ const scenarioParser = async (req, jobIdField) => { const { event, trigger, table } = req.body; - const { logger } = req; + const { + logger, + sessionUtils: { getBodyshopFromRedis } + } = req; // Step 1: Validate we know what user committed the action that fired the parser // console.log("Step 1"); @@ -118,12 +121,15 @@ const scenarioParser = async (req, jobIdField) => { // Step 7: Identify scenarios that match the event data and job context // console.log("Step 7"); - const matchingScenarios = getMatchingScenarios({ - ...eventData, - jobWatchers, - bodyShopId, - bodyShopName - }); + const matchingScenarios = await getMatchingScenarios( + { + ...eventData, + jobWatchers, + bodyShopId, + bodyShopName + }, + getBodyshopFromRedis + ); // Exit early if no matching scenarios are identified if (isEmpty(matchingScenarios)) {