Merged in feature/IO-3166-Global-Notifications-Part-2 (pull request #2144)
Feature/IO-3166 Global Notifications Part 2
This commit is contained in:
@@ -85,7 +85,11 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err) => console.error("MARK_NOTIFICATION_READ error:", err)
|
onError: (err) =>
|
||||||
|
console.error("MARK_NOTIFICATION_READ error:", {
|
||||||
|
message: err?.message,
|
||||||
|
stack: err?.stack
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const [markAllNotificationsRead] = useMutation(MARK_ALL_NOTIFICATIONS_READ, {
|
const [markAllNotificationsRead] = useMutation(MARK_ALL_NOTIFICATIONS_READ, {
|
||||||
|
|||||||
@@ -1127,6 +1127,46 @@
|
|||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
check: null
|
check: null
|
||||||
|
event_triggers:
|
||||||
|
- name: cache_bodyshop
|
||||||
|
definition:
|
||||||
|
enable_manual: false
|
||||||
|
update:
|
||||||
|
columns:
|
||||||
|
- shopname
|
||||||
|
- md_order_statuses
|
||||||
|
retry_conf:
|
||||||
|
interval_sec: 10
|
||||||
|
num_retries: 0
|
||||||
|
timeout_sec: 60
|
||||||
|
webhook_from_env: HASURA_API_URL
|
||||||
|
headers:
|
||||||
|
- name: event-secret
|
||||||
|
value_from_env: EVENT_SECRET
|
||||||
|
request_transform:
|
||||||
|
body:
|
||||||
|
action: transform
|
||||||
|
template: |-
|
||||||
|
{
|
||||||
|
"created_at": {{$body.created_at}},
|
||||||
|
"delivery_info": {{$body.delivery_info}},
|
||||||
|
"event": {
|
||||||
|
"data": {
|
||||||
|
"new": {
|
||||||
|
"id": {{$body.event.data.new.id}},
|
||||||
|
"shopname": {{$body.event.data.new.shopname}},
|
||||||
|
"md_order_statuses": {{$body.event.data.new.md_order_statuses}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"op": {{$body.event.op}},
|
||||||
|
"session_variables": {{$body.event.session_variables}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
method: POST
|
||||||
|
query_params: {}
|
||||||
|
template_engine: Kriti
|
||||||
|
url: '{{$base_url}}/bodyshop-cache'
|
||||||
|
version: 2
|
||||||
- table:
|
- table:
|
||||||
name: cccontracts
|
name: cccontracts
|
||||||
schema: public
|
schema: public
|
||||||
@@ -3240,11 +3280,10 @@
|
|||||||
- name: notifications_joblines
|
- name: notifications_joblines
|
||||||
definition:
|
definition:
|
||||||
enable_manual: false
|
enable_manual: false
|
||||||
insert:
|
|
||||||
columns: '*'
|
|
||||||
update:
|
update:
|
||||||
columns:
|
columns:
|
||||||
- critical
|
- critical
|
||||||
|
- status
|
||||||
retry_conf:
|
retry_conf:
|
||||||
interval_sec: 10
|
interval_sec: 10
|
||||||
num_retries: 0
|
num_retries: 0
|
||||||
@@ -3254,11 +3293,14 @@
|
|||||||
- name: event-secret
|
- name: event-secret
|
||||||
value_from_env: EVENT_SECRET
|
value_from_env: EVENT_SECRET
|
||||||
request_transform:
|
request_transform:
|
||||||
|
body:
|
||||||
|
action: transform
|
||||||
|
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body.event.session_variables.x-hasura-user-id}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"jobid\": {{$body.event.data.old.jobid}},\r\n \"critical\": {{$body.event.data.old.critical}},\r\n \"status\": {{$body.event.data.old.status}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"jobid\": {{$body.event.data.new.jobid}},\r\n \"critical\": {{$body.event.data.old.critical}},\r\n \"status\": {{$body.event.data.new.status}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_joblines\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"joblines\"\r\n }\r\n}\r\n"
|
||||||
method: POST
|
method: POST
|
||||||
query_params: {}
|
query_params: {}
|
||||||
template_engine: Kriti
|
template_engine: Kriti
|
||||||
url: '{{$base_url}}/notifications/events/handleJobLinesChange'
|
url: '{{$base_url}}/notifications/events/handleJobLinesChange'
|
||||||
version: 1
|
version: 2
|
||||||
- table:
|
- table:
|
||||||
name: joblines_status
|
name: joblines_status
|
||||||
schema: public
|
schema: public
|
||||||
|
|||||||
@@ -2757,3 +2757,13 @@ exports.INSERT_NOTIFICATIONS_MUTATION = ` mutation INSERT_NOTIFICATIONS($object
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
exports.GET_BODYSHOP_BY_ID = `
|
||||||
|
query GET_BODYSHOP_BY_ID($id: uuid!) {
|
||||||
|
bodyshops_by_pk(id: $id) {
|
||||||
|
id
|
||||||
|
md_order_statuses
|
||||||
|
shopname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -326,7 +326,6 @@ const newNoteAddedBuilder = (data) => {
|
|||||||
* Builds notification data for new time tickets posted.
|
* Builds notification data for new time tickets posted.
|
||||||
*/
|
*/
|
||||||
const newTimeTicketPostedBuilder = (data) => {
|
const newTimeTicketPostedBuilder = (data) => {
|
||||||
consoleDir(data);
|
|
||||||
const type = data?.data?.cost_center;
|
const type = data?.data?.cost_center;
|
||||||
const body = `An ${type} time ticket has been posted${data?.data?.flat_rate ? " (Flat Rate)" : ""}.`.trim();
|
const body = `An ${type} time ticket has been posted${data?.data?.flat_rate ? " (Flat Rate)" : ""}.`.trim();
|
||||||
|
|
||||||
@@ -368,10 +367,7 @@ const partMarkedBackOrderedBuilder = (data) => {
|
|||||||
bodyShopId: data.bodyShopId,
|
bodyShopId: data.bodyShopId,
|
||||||
key: "notifications.job.partBackOrdered",
|
key: "notifications.job.partBackOrdered",
|
||||||
body,
|
body,
|
||||||
variables: {
|
variables: {},
|
||||||
queuedForParts: data.changedFields.queued_for_parts?.new,
|
|
||||||
oldQueuedForParts: data.changedFields.queued_for_parts?.old
|
|
||||||
},
|
|
||||||
recipients: []
|
recipients: []
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ const {
|
|||||||
supplementImportedBuilder,
|
supplementImportedBuilder,
|
||||||
partMarkedBackOrderedBuilder
|
partMarkedBackOrderedBuilder
|
||||||
} = require("./scenarioBuilders");
|
} = require("./scenarioBuilders");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const { isFunction } = require("lodash");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of notification scenario definitions.
|
* An array of notification scenario definitions.
|
||||||
@@ -25,8 +27,9 @@ const {
|
|||||||
* - fields {Array<string>}: Fields to check for changes.
|
* - fields {Array<string>}: Fields to check for changes.
|
||||||
* - matchToUserFields {Array<string>}: Fields used to match scenarios to user data.
|
* - matchToUserFields {Array<string>}: Fields used to match scenarios to user data.
|
||||||
* - onNew {boolean|Array<boolean>}: Indicates whether the scenario should be triggered on new data.
|
* - onNew {boolean|Array<boolean>}: Indicates whether the scenario should be triggered on new data.
|
||||||
* - onlyTrue {Array<string>}: Specifies fields that must be true for the scenario to match.
|
|
||||||
* - builder {Function}: A function to handle the scenario.
|
* - builder {Function}: A function to handle the scenario.
|
||||||
|
* - onlyTruthyValues {boolean|Array<string>}: 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 = [
|
const notificationScenarios = [
|
||||||
{
|
{
|
||||||
@@ -86,7 +89,6 @@ const notificationScenarios = [
|
|||||||
builder: newTimeTicketPostedBuilder
|
builder: newTimeTicketPostedBuilder
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Good test for batching as this will hit multiple scenarios
|
|
||||||
key: "intake-delivery-checklist-completed",
|
key: "intake-delivery-checklist-completed",
|
||||||
table: "jobs",
|
table: "jobs",
|
||||||
fields: ["intakechecklist", "deliverchecklist"],
|
fields: ["intakechecklist", "deliverchecklist"],
|
||||||
@@ -109,29 +111,39 @@ const notificationScenarios = [
|
|||||||
key: "critical-parts-status-changed",
|
key: "critical-parts-status-changed",
|
||||||
table: "joblines",
|
table: "joblines",
|
||||||
fields: ["critical"],
|
fields: ["critical"],
|
||||||
onlyTrue: ["critical"],
|
onlyTruthyValues: ["critical"],
|
||||||
builder: criticalPartsStatusChangedBuilder
|
builder: criticalPartsStatusChangedBuilder
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "part-marked-back-ordered",
|
||||||
|
table: "joblines",
|
||||||
|
fields: ["status"],
|
||||||
|
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 ---------------
|
// -------------- Difficult ---------------
|
||||||
// Holding off on this one for now
|
// Holding off on this one for now
|
||||||
{
|
{
|
||||||
key: "supplement-imported",
|
key: "supplement-imported",
|
||||||
builder: supplementImportedBuilder
|
builder: supplementImportedBuilder
|
||||||
// spans multiple tables,
|
// spans multiple tables,
|
||||||
},
|
|
||||||
// This one may be tricky as the jobid is not directly in the event data (this is probably wrong)
|
|
||||||
// (should otherwise)
|
|
||||||
// Status needs to mark meta data 'md_backorderd' for example
|
|
||||||
// Double check Jobid
|
|
||||||
{
|
|
||||||
key: "part-marked-back-ordered",
|
|
||||||
table: "joblines",
|
|
||||||
builder: partMarkedBackOrderedBuilder
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array of scenarios that match the given event data.
|
* Returns an array of scenarios that match the given event data.
|
||||||
|
* Supports asynchronous callbacks for additional filtering.
|
||||||
*
|
*
|
||||||
* @param {Object} eventData - The parsed event data.
|
* @param {Object} eventData - The parsed event data.
|
||||||
* Expected properties:
|
* Expected properties:
|
||||||
@@ -140,28 +152,16 @@ const notificationScenarios = [
|
|||||||
* - isNew: boolean indicating whether the record is new or updated
|
* - isNew: boolean indicating whether the record is new or updated
|
||||||
* - data: the new data object (used to check field values)
|
* - data: the new data object (used to check field values)
|
||||||
* - (other properties may be added such as jobWatchers, bodyShopId, etc.)
|
* - (other properties may be added such as jobWatchers, bodyShopId, etc.)
|
||||||
*
|
* @param {Function} getBodyshopFromRedis - Function to retrieve bodyshop data from Redis.
|
||||||
* @returns {Array<Object>} An array of matching scenario objects.
|
* @returns {Promise<Array<Object>>} A promise resolving to an array of matching scenario objects.
|
||||||
*/
|
*/
|
||||||
/**
|
const getMatchingScenarios = async (eventData, getBodyshopFromRedis) => {
|
||||||
* Returns an array of scenarios that match the given event data.
|
const matches = [];
|
||||||
*
|
for (const scenario of notificationScenarios) {
|
||||||
* @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<Object>} An array of matching scenario objects.
|
|
||||||
*/
|
|
||||||
const getMatchingScenarios = (eventData) =>
|
|
||||||
notificationScenarios.filter((scenario) => {
|
|
||||||
// If eventData has a table, then only scenarios with a table property that matches should be considered.
|
// If eventData has a table, then only scenarios with a table property that matches should be considered.
|
||||||
if (eventData.table) {
|
if (eventData.table) {
|
||||||
if (!scenario.table || eventData.table.name !== scenario.table) {
|
if (!scenario.table || eventData.table.name !== scenario.table) {
|
||||||
return false;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,9 +169,9 @@ const getMatchingScenarios = (eventData) =>
|
|||||||
// Allow onNew to be either a boolean or an array of booleans.
|
// Allow onNew to be either a boolean or an array of booleans.
|
||||||
if (Object.prototype.hasOwnProperty.call(scenario, "onNew")) {
|
if (Object.prototype.hasOwnProperty.call(scenario, "onNew")) {
|
||||||
if (Array.isArray(scenario.onNew)) {
|
if (Array.isArray(scenario.onNew)) {
|
||||||
if (!scenario.onNew.includes(eventData.isNew)) return false;
|
if (!scenario.onNew.includes(eventData.isNew)) continue;
|
||||||
} else {
|
} else {
|
||||||
if (eventData.isNew !== scenario.onNew) return false;
|
if (eventData.isNew !== scenario.onNew) continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,19 +179,7 @@ const getMatchingScenarios = (eventData) =>
|
|||||||
if (scenario.fields && scenario.fields.length > 0) {
|
if (scenario.fields && scenario.fields.length > 0) {
|
||||||
const hasMatchingField = scenario.fields.some((field) => eventData.changedFieldNames.includes(field));
|
const hasMatchingField = scenario.fields.some((field) => eventData.changedFieldNames.includes(field));
|
||||||
if (!hasMatchingField) {
|
if (!hasMatchingField) {
|
||||||
return false;
|
continue;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyTrue logic:
|
|
||||||
// If a scenario defines an onlyTrue array, then at least one of those fields must have changed
|
|
||||||
// and its new value (from eventData.data) must be non-falsey.
|
|
||||||
if (scenario.onlyTrue && Array.isArray(scenario.onlyTrue) && scenario.onlyTrue.length > 0) {
|
|
||||||
const hasTruthyChange = scenario.onlyTrue.some(
|
|
||||||
(field) => eventData.changedFieldNames.includes(field) && Boolean(eventData.data[field])
|
|
||||||
);
|
|
||||||
if (!hasTruthyChange) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,22 +199,37 @@ const getMatchingScenarios = (eventData) =>
|
|||||||
);
|
);
|
||||||
// If no fields in onlyTruthyValues match the scenario’s fields or changed fields, skip this scenario
|
// If no fields in onlyTruthyValues match the scenario’s fields or changed fields, skip this scenario
|
||||||
if (fieldsToCheck.length === 0) {
|
if (fieldsToCheck.length === 0) {
|
||||||
return false;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Invalid onlyTruthyValues (not true or a non-empty array), skip this scenario
|
// Invalid onlyTruthyValues (not true or a non-empty array), skip this scenario
|
||||||
return false;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure all fields to check have truthy new values
|
// Ensure all fields to check have truthy new values
|
||||||
const allTruthy = fieldsToCheck.every((field) => Boolean(eventData.data[field]));
|
const allTruthy = fieldsToCheck.every((field) => Boolean(eventData.data[field]));
|
||||||
if (!allTruthy) {
|
if (!allTruthy) {
|
||||||
return false;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matches.push(scenario);
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
notificationScenarios,
|
notificationScenarios,
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ const FILTER_SELF_FROM_WATCHERS = process.env?.FILTER_SELF_FROM_WATCHERS !== "fa
|
|||||||
*/
|
*/
|
||||||
const scenarioParser = async (req, jobIdField) => {
|
const scenarioParser = async (req, jobIdField) => {
|
||||||
const { event, trigger, table } = req.body;
|
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
|
// Step 1: Validate we know what user committed the action that fired the parser
|
||||||
// console.log("Step 1");
|
// 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
|
// Step 7: Identify scenarios that match the event data and job context
|
||||||
// console.log("Step 7");
|
// console.log("Step 7");
|
||||||
|
|
||||||
const matchingScenarios = getMatchingScenarios({
|
const matchingScenarios = await getMatchingScenarios(
|
||||||
...eventData,
|
{
|
||||||
jobWatchers,
|
...eventData,
|
||||||
bodyShopId,
|
jobWatchers,
|
||||||
bodyShopName
|
bodyShopId,
|
||||||
});
|
bodyShopName
|
||||||
|
},
|
||||||
|
getBodyshopFromRedis
|
||||||
|
);
|
||||||
|
|
||||||
// Exit early if no matching scenarios are identified
|
// Exit early if no matching scenarios are identified
|
||||||
if (isEmpty(matchingScenarios)) {
|
if (isEmpty(matchingScenarios)) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLCl
|
|||||||
const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails");
|
const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails");
|
||||||
const { canvastest } = require("../render/canvas-handler");
|
const { canvastest } = require("../render/canvas-handler");
|
||||||
const { alertCheck } = require("../alerts/alertcheck");
|
const { alertCheck } = require("../alerts/alertcheck");
|
||||||
|
const updateBodyshopCache = require("../web-sockets/updateBodyshopCache");
|
||||||
const uuid = require("uuid").v4;
|
const uuid = require("uuid").v4;
|
||||||
|
|
||||||
//Test route to ensure Express is responding.
|
//Test route to ensure Express is responding.
|
||||||
@@ -58,6 +59,7 @@ router.get("/test-logs", eventAuthorizationMiddleware, (req, res) => {
|
|||||||
|
|
||||||
return res.status(500).send("Logs tested.");
|
return res.status(500).send("Logs tested.");
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/wstest", eventAuthorizationMiddleware, (req, res) => {
|
router.get("/wstest", eventAuthorizationMiddleware, (req, res) => {
|
||||||
const { ioRedis } = req;
|
const { ioRedis } = req;
|
||||||
ioRedis.to(`bodyshop-broadcast-room:bfec8c8c-b7f1-49e0-be4c-524455f4e582`).emit("new-message-summary", {
|
ioRedis.to(`bodyshop-broadcast-room:bfec8c8c-b7f1-49e0-be4c-524455f4e582`).emit("new-message-summary", {
|
||||||
@@ -137,4 +139,7 @@ router.post("/canvastest", validateFirebaseIdTokenMiddleware, canvastest);
|
|||||||
// Alert Check
|
// Alert Check
|
||||||
router.post("/alertcheck", eventAuthorizationMiddleware, alertCheck);
|
router.post("/alertcheck", eventAuthorizationMiddleware, alertCheck);
|
||||||
|
|
||||||
|
// Redis Cache Routes
|
||||||
|
router.post("/bodyshop-cache", eventAuthorizationMiddleware, updateBodyshopCache);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
const applyIOHelpers = ({ app, api, io, logger }) => {
|
const applyIOHelpers = ({ app, api, io, logger }) => {
|
||||||
const getBodyshopRoom = (bodyshopID) => `bodyshop-broadcast-room:${bodyshopID}`;
|
// Global Bodyshop Room
|
||||||
|
const getBodyshopRoom = (bodyshopId) => `bodyshop-broadcast-room:${bodyshopId}`;
|
||||||
|
|
||||||
// Messaging - conversation specific room to handle detailed messages when the user has a conversation open.
|
// Messaging - conversation specific room to handle detailed messages when the user has a conversation open.
|
||||||
const getBodyshopConversationRoom = ({bodyshopId, conversationId}) =>
|
const getBodyshopConversationRoom = ({ bodyshopId, conversationId }) =>
|
||||||
`bodyshop-conversation-room:${bodyshopId}:${conversationId}`;
|
`bodyshop-conversation-room:${bodyshopId}:${conversationId}`;
|
||||||
|
|
||||||
const ioHelpersAPI = {
|
const ioHelpersAPI = {
|
||||||
|
|||||||
@@ -1,3 +1,39 @@
|
|||||||
|
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
||||||
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
|
const BODYSHOP_CACHE_TTL = 3600; // 1 hour
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a cache key for a bodyshop
|
||||||
|
* @param bodyshopId
|
||||||
|
* @returns {`bodyshop-cache:${string}`}
|
||||||
|
*/
|
||||||
|
const getBodyshopCacheKey = (bodyshopId) => `bodyshop-cache:${bodyshopId}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch bodyshop data from the database
|
||||||
|
* @param bodyshopId
|
||||||
|
* @param logger
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
const fetchBodyshopFromDB = async (bodyshopId, logger) => {
|
||||||
|
try {
|
||||||
|
const response = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
||||||
|
const bodyshop = response.bodyshops_by_pk;
|
||||||
|
if (!bodyshop) {
|
||||||
|
throw new Error(`Bodyshop with ID ${bodyshopId} not found`);
|
||||||
|
}
|
||||||
|
return bodyshop; // Return the full object as-is
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("fetch-bodyshop-from-db", "ERROR", "redis", null, {
|
||||||
|
bodyshopId,
|
||||||
|
error: error?.message,
|
||||||
|
stack: error?.stack
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply Redis helper functions
|
* Apply Redis helper functions
|
||||||
* @param pubClient
|
* @param pubClient
|
||||||
@@ -234,6 +270,71 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get bodyshop data from Redis or fetch from DB if missing
|
||||||
|
const getBodyshopFromRedis = async (bodyshopId) => {
|
||||||
|
const key = getBodyshopCacheKey(bodyshopId);
|
||||||
|
try {
|
||||||
|
// Check if data exists in Redis
|
||||||
|
const cachedData = await pubClient.get(key);
|
||||||
|
if (cachedData) {
|
||||||
|
return JSON.parse(cachedData); // Parse and return the full object
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss: fetch from DB
|
||||||
|
const bodyshopData = await fetchBodyshopFromDB(bodyshopId, logger);
|
||||||
|
|
||||||
|
// Store in Redis as a single JSON string
|
||||||
|
const jsonData = JSON.stringify(bodyshopData);
|
||||||
|
await pubClient.set(key, jsonData);
|
||||||
|
await pubClient.expire(key, BODYSHOP_CACHE_TTL);
|
||||||
|
|
||||||
|
logger.log("bodyshop-cache-miss", "DEBUG", "redis", null, {
|
||||||
|
bodyshopId,
|
||||||
|
action: "Fetched from DB and cached"
|
||||||
|
});
|
||||||
|
|
||||||
|
return bodyshopData; // Return the full object
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("get-bodyshop-from-redis", "ERROR", "redis", null, {
|
||||||
|
bodyshopId,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update or invalidate bodyshop data in Redis
|
||||||
|
const updateOrInvalidateBodyshopFromRedis = async (bodyshopId, values = null) => {
|
||||||
|
const key = getBodyshopCacheKey(bodyshopId);
|
||||||
|
try {
|
||||||
|
if (!values) {
|
||||||
|
// Invalidate cache by deleting the key
|
||||||
|
await pubClient.del(key);
|
||||||
|
logger.log("bodyshop-cache-invalidate", "DEBUG", "api", "redis", {
|
||||||
|
bodyshopId,
|
||||||
|
action: "Cache invalidated"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Update cache with the full provided values
|
||||||
|
const jsonData = JSON.stringify(values);
|
||||||
|
await pubClient.set(key, jsonData);
|
||||||
|
await pubClient.expire(key, BODYSHOP_CACHE_TTL);
|
||||||
|
logger.log("bodyshop-cache-update", "DEBUG", "api", "redis", {
|
||||||
|
bodyshopId,
|
||||||
|
action: "Cache updated",
|
||||||
|
values
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("update-or-invalidate-bodyshop-from-redis", "ERROR", "api", "redis", {
|
||||||
|
bodyshopId,
|
||||||
|
values,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
setSessionData,
|
setSessionData,
|
||||||
getSessionData,
|
getSessionData,
|
||||||
@@ -251,7 +352,9 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
removeUserSocketMapping,
|
removeUserSocketMapping,
|
||||||
getUserSocketMappingByBodyshop,
|
getUserSocketMappingByBodyshop,
|
||||||
getUserSocketMapping,
|
getUserSocketMapping,
|
||||||
refreshUserSocketTTL
|
refreshUserSocketTTL,
|
||||||
|
getBodyshopFromRedis,
|
||||||
|
updateOrInvalidateBodyshopFromRedis
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(module.exports, api);
|
Object.assign(module.exports, api);
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ const redisSocketEvents = ({
|
|||||||
// Socket Auth Middleware
|
// Socket Auth Middleware
|
||||||
const authMiddleware = async (socket, next) => {
|
const authMiddleware = async (socket, next) => {
|
||||||
const { token, bodyshopId } = socket.handshake.auth;
|
const { token, bodyshopId } = socket.handshake.auth;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return next(new Error("Authentication error - no authorization token."));
|
return next(new Error("Authentication error - no authorization token."));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bodyshopId) {
|
if (!bodyshopId) {
|
||||||
return next(new Error("Authentication error - no bodyshopId provided."));
|
return next(new Error("Authentication error - no bodyshopId provided."));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await admin.auth().verifyIdToken(token);
|
const user = await admin.auth().verifyIdToken(token);
|
||||||
socket.user = user;
|
socket.user = user;
|
||||||
|
|||||||
36
server/web-sockets/updateBodyshopCache.js
Normal file
36
server/web-sockets/updateBodyshopCache.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Update or invalidate bodyshop cache
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const updateBodyshopCache = async (req, res) => {
|
||||||
|
const {
|
||||||
|
sessionUtils: { updateOrInvalidateBodyshopFromRedis },
|
||||||
|
logger
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const { event } = req.body;
|
||||||
|
const { new: newData } = event.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (newData && newData.id) {
|
||||||
|
// Update cache with the full new data object
|
||||||
|
await updateOrInvalidateBodyshopFromRedis(newData.id, newData);
|
||||||
|
logger.logger.debug("Bodyshop cache updated successfully.");
|
||||||
|
} else {
|
||||||
|
// Invalidate cache if no valid data provided
|
||||||
|
await updateOrInvalidateBodyshopFromRedis(newData.id);
|
||||||
|
logger.logger.debug("Bodyshop cache invalidated successfully.");
|
||||||
|
}
|
||||||
|
res.status(200).json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("bodyshop-cache-update-error", "ERROR", "api", "redis", {
|
||||||
|
message: error?.message,
|
||||||
|
stack: error?.stack
|
||||||
|
});
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = updateBodyshopCache;
|
||||||
Reference in New Issue
Block a user