feature/IO-3096-GlobalNotifications - Cleanup and Package bumps

This commit is contained in:
Dave Richer
2025-02-19 12:50:01 -05:00
parent 366f7b9c4a
commit 1384616d66
11 changed files with 415 additions and 198 deletions

176
client/package-lock.json generated
View File

@@ -9,18 +9,18 @@
"version": "0.2.1",
"hasInstallScript": true,
"dependencies": {
"@ant-design/pro-layout": "^7.22.2",
"@apollo/client": "^3.12.11",
"@ant-design/pro-layout": "^7.22.3",
"@apollo/client": "^3.13.1",
"@emotion/is-prop-valid": "^1.3.1",
"@fingerprintjs/fingerprintjs": "^4.6.0",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.5.0",
"@sentry/cli": "^2.40.0",
"@sentry/cli": "^2.42.1",
"@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.13.0",
"@tanem/react-nprogress": "^5.0.53",
"@vitejs/plugin-react": "^4.3.4",
"antd": "^5.24.0",
"antd": "^5.24.1",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1",
@@ -36,9 +36,9 @@
"firebase": "^10.13.2",
"graphql": "^16.10.0",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-browser-languagedetector": "^8.0.3",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.18",
"libphonenumber-js": "^1.11.20",
"logrocket": "^8.1.2",
"markerjs2": "^2.32.3",
"memoize-one": "^6.0.0",
@@ -56,7 +56,7 @@
"react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.1.3",
"react-icons": "^5.4.0",
"react-icons": "^5.5.0",
"react-image-lightbox": "^5.1.4",
"react-markdown": "^9.0.3",
"react-number-format": "^5.4.3",
@@ -74,12 +74,12 @@
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.84.0",
"sass": "^1.85.0",
"socket.io-client": "^4.8.1",
"styled-components": "^6.1.15",
"subscriptions-transport-ws": "^0.11.0",
"use-memo-one": "^1.1.3",
"userpilot": "^1.3.6",
"userpilot": "^1.3.7",
"vite-plugin-ejs": "^1.7.0",
"web-vitals": "^3.5.2"
},
@@ -105,10 +105,10 @@
"globals": "^15.15.0",
"memfs": "^4.17.0",
"os-browserify": "^0.3.0",
"react-error-overlay": "6.0.11",
"react-error-overlay": "^6.1.0",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^6.1.0",
"vite": "^6.1.1",
"vite-plugin-babel": "^1.3.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.23.0",
@@ -218,15 +218,15 @@
"license": "MIT"
},
"node_modules/@ant-design/pro-layout": {
"version": "7.22.2",
"resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.2.tgz",
"integrity": "sha512-RlXqN+EVnF1Sup84O0IjS/vMMgwFnbBZwvR+GVnmZg/+cIa4/BDTXyhbb1KRwUqzn1ctDzj7JfbWOWqmGMw6yA==",
"version": "7.22.3",
"resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.3.tgz",
"integrity": "sha512-di/EOMDuoMDRjBweqesYyCxEYr2LCmO82y6A4bSwmmJ6ehxN7HGC73Wx4RuBkzDR7kHLTOXt7WxI6875ENT8mg==",
"license": "MIT",
"dependencies": {
"@ant-design/cssinjs": "^1.21.1",
"@ant-design/icons": "^5.0.0",
"@ant-design/pro-provider": "2.15.3",
"@ant-design/pro-utils": "2.16.3",
"@ant-design/pro-utils": "2.16.4",
"@babel/runtime": "^7.18.0",
"@umijs/route-utils": "^4.0.0",
"@umijs/use-params": "^1.0.9",
@@ -265,9 +265,9 @@
}
},
"node_modules/@ant-design/pro-utils": {
"version": "2.16.3",
"resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.16.3.tgz",
"integrity": "sha512-uNjKh51v/SUlCJbWfhg2lRQB/TB0MyNMCQkFZ8mZBQ2rk3Ew47Sly6VssVVWMjIWBLE+g9fOgPg0C1IVeilIXA==",
"version": "2.16.4",
"resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.16.4.tgz",
"integrity": "sha512-PFxqF0fsUsLj8ORvJSuMgVv9NDHwAxZaglzPN/u3jZX7rWYcrHD04EMJEXooZaSyT6Q4+j7SqXDx6oBsdb9zNw==",
"license": "MIT",
"dependencies": {
"@ant-design/icons": "^5.0.0",
@@ -322,9 +322,9 @@
}
},
"node_modules/@apollo/client": {
"version": "3.12.11",
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.12.11.tgz",
"integrity": "sha512-1RppV9U3E6Uusl/33yGkZa+rXpkGU5iCstcYltwWjdTjoA/YBD2Yyu0aHy8J4uKfIExUgnMW1HJWn4A0E0rRsw==",
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.13.1.tgz",
"integrity": "sha512-HaAt62h3jNUXpJ1v5HNgUiCzPP1c5zc2Q/FeTb2cTk/v09YlhoqKKHQFJI7St50VCJ5q8JVIc03I5bRcBrQxsg==",
"license": "MIT",
"dependencies": {
"@graphql-typed-document-node/core": "^3.1.1",
@@ -5510,9 +5510,9 @@
}
},
"node_modules/@sentry/cli": {
"version": "2.41.1",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.41.1.tgz",
"integrity": "sha512-0GVmDiTV7R1492wkVY4bGcfC0fSmRmQjuxaaPI8CIV9B2VP9pBVCUizi1mevXaaE4I3fM60LI+XYrKFEneuVog==",
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.42.1.tgz",
"integrity": "sha512-3fonGZoGwlze/iGYDdCJXpG5skXc6j/yYom+k6TqVvJJqSct1RgLJHjCw1P0IxHsR8pNz9f1H85OdLXKxrc6sw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -5529,19 +5529,19 @@
"node": ">= 10"
},
"optionalDependencies": {
"@sentry/cli-darwin": "2.41.1",
"@sentry/cli-linux-arm": "2.41.1",
"@sentry/cli-linux-arm64": "2.41.1",
"@sentry/cli-linux-i686": "2.41.1",
"@sentry/cli-linux-x64": "2.41.1",
"@sentry/cli-win32-i686": "2.41.1",
"@sentry/cli-win32-x64": "2.41.1"
"@sentry/cli-darwin": "2.42.1",
"@sentry/cli-linux-arm": "2.42.1",
"@sentry/cli-linux-arm64": "2.42.1",
"@sentry/cli-linux-i686": "2.42.1",
"@sentry/cli-linux-x64": "2.42.1",
"@sentry/cli-win32-i686": "2.42.1",
"@sentry/cli-win32-x64": "2.42.1"
}
},
"node_modules/@sentry/cli-darwin": {
"version": "2.41.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.41.1.tgz",
"integrity": "sha512-7pS3pu/SuhE6jOn3wptstAg6B5nUP878O6s+2svT7b5fKNfYUi/6NPK6dAveh2Ca0rwVq40TO4YFJabWMgTpdQ==",
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.42.1.tgz",
"integrity": "sha512-WZFsrzSWtsRK24SiTa+Xod+4Hjlw7xaggmM4lbuo0lISO1EQj+K29jyGX+Ku0qflO1qp1z32bSP/RlWx/1rBjg==",
"license": "BSD-3-Clause",
"optional": true,
"os": [
@@ -5552,9 +5552,9 @@
}
},
"node_modules/@sentry/cli-linux-arm": {
"version": "2.41.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.41.1.tgz",
"integrity": "sha512-wNUvquD6qjOCczvuBGf9OiD29nuQ6yf8zzfyPJa5Bdx1QXuteKsKb6HBrMwuIR3liyuu0duzHd+H/+p1n541Hg==",
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.1.tgz",
"integrity": "sha512-3xR2B9v8e7NjB6U9+oMu2puR3xOv/Axd7qNuUrZxQnNZYtgtnAqIDgSmFTWHOOoged1+AZXe+xDWLN0Y11Q03Q==",
"cpu": [
"arm"
],
@@ -5569,9 +5569,9 @@
}
},
"node_modules/@sentry/cli-linux-arm64": {
"version": "2.41.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.41.1.tgz",
"integrity": "sha512-EzYCEnnENBnS5kpNW+2dBcrPZn1MVfywh2joGVQZTpmgDL5YFJ59VOd+K0XuEwqgFI8BSNI14KXZ75s4DD1/Vw==",
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.1.tgz",
"integrity": "sha512-8A43bLvDIzquCXblHNadaRm109ANw1Q9VRXg5qLYv7DrPkUm2oQP+oRnuNUgOJ3W/8QQSvANpG9pPko+mJs4xw==",
"cpu": [
"arm64"
],
@@ -5586,9 +5586,9 @@
}
},
"node_modules/@sentry/cli-linux-i686": {
"version": "2.41.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.41.1.tgz",
"integrity": "sha512-urpQCWrdYnSAsZY3udttuMV88wTJzKZL10xsrp7sjD/Hd+O6qSLVLkxebIlxts70jMLLFHYrQ2bkRg5kKuX6Fg==",
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.1.tgz",
"integrity": "sha512-YBz6prKqh1i0gzTg3Rus8ALQWmAk5Acap2U2dGuVYgTt7Bbu6SJbxNC9d8j3RUGu7ylupofUEMqKd391mTHf7g==",
"cpu": [
"x86",
"ia32"
@@ -5604,9 +5604,9 @@
}
},
"node_modules/@sentry/cli-linux-x64": {
"version": "2.41.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.41.1.tgz",
"integrity": "sha512-ZqpYwHXAaK4MMEFlyaLYr6mJTmpy9qP6n30jGhLTW7kHKS3s6GPLCSlNmIfeClrInEt0963fM633ZRnXa04VPw==",
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.1.tgz",
"integrity": "sha512-Rvc6Jy3kLZrcyO7Ysy1gj0iQi0nGVUN79VqC3OO9JDV44aDtKBDYuBkeFKE3gd1SL8EvPetKH85en2u2wdWxYg==",
"cpu": [
"x64"
],
@@ -5621,9 +5621,9 @@
}
},
"node_modules/@sentry/cli-win32-i686": {
"version": "2.41.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.41.1.tgz",
"integrity": "sha512-AuRimCeVsx99DIOr9cwdYBHk39tlmAuPDdy2r16iNzY0InXs4xOys4gGzM7N4vlFQvFkzuc778Su0HkfasgprA==",
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.1.tgz",
"integrity": "sha512-FC8FE6dk+G83PCO09Ux/9NJNouF5yXKhpzLV5BZkqQye39hV9GDrFTu+VWTnwI1P77fnaJkPEEKRkjwNiPGjLA==",
"cpu": [
"x86",
"ia32"
@@ -5638,9 +5638,9 @@
}
},
"node_modules/@sentry/cli-win32-x64": {
"version": "2.41.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.41.1.tgz",
"integrity": "sha512-6JcPvXGye61+wPp0xdzfc2YLE/Dcud8JdaK8VxLM3b/8+Em7E+UyliDu3uF8+YGUqizY5JYTd3fs17DC8DZhLw==",
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.1.tgz",
"integrity": "sha512-1595wD7JQSu5J9pA4m/B3WrjjIXltSV9VzuErehvanBvfusQ/YgBcvsNzgIf8aJsgSAYGbpR3Zqu81pjohdjgA==",
"cpu": [
"x64"
],
@@ -6759,9 +6759,9 @@
}
},
"node_modules/antd": {
"version": "5.24.0",
"resolved": "https://registry.npmjs.org/antd/-/antd-5.24.0.tgz",
"integrity": "sha512-05PZBIf6ijLHAQskBTW3nwS2t7tQmyLA6Xq8vK2Sk5tsgCsH/UE1cNCDYnKFGRJ7cKYuWJ565JDo9LejbiO42A==",
"version": "5.24.1",
"resolved": "https://registry.npmjs.org/antd/-/antd-5.24.1.tgz",
"integrity": "sha512-RGwpXpSr2RtoUnrpJl3V6ZaTExwSXkFVxV24VUowwC04n6oA1sGyJrofQOKNqD623sVxL5UJBmf0a+BFBImP3Q==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^7.2.0",
@@ -6794,7 +6794,7 @@
"rc-motion": "^2.9.5",
"rc-notification": "~5.6.3",
"rc-pagination": "~5.1.0",
"rc-picker": "~4.11.0",
"rc-picker": "~4.11.1",
"rc-progress": "~4.0.0",
"rc-rate": "~2.13.1",
"rc-resize-observer": "^1.4.3",
@@ -6803,7 +6803,7 @@
"rc-slider": "~11.1.8",
"rc-steps": "~6.0.1",
"rc-switch": "~4.1.0",
"rc-table": "~7.50.2",
"rc-table": "~7.50.3",
"rc-tabs": "~15.5.1",
"rc-textarea": "~1.9.0",
"rc-tooltip": "~6.4.0",
@@ -11525,9 +11525,9 @@
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz",
"integrity": "sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==",
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.3.tgz",
"integrity": "sha512-beOOLArattPBc2YZG5IXGJytdYFgUR7cS8Wd6HT4IczIoWKgmTspOQ2yasaGklelVo5seLPmnEKvLHR+E/MdWQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
@@ -12723,9 +12723,9 @@
}
},
"node_modules/libphonenumber-js": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.19.tgz",
"integrity": "sha512-bW/Yp/9dod6fmyR+XqSUL1N5JE7QRxQ3KrBIbYS1FTv32e5i3SEtQVX+71CYNv8maWNSOgnlCoNp9X78f/cKiA==",
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.20.tgz",
"integrity": "sha512-/ipwAMvtSZRdiQBHqW1qxqeYiBMzncOQLVA+62MWYr7N4m7Q2jqpJ0WgT7zlOEOpyLRSqrMXidbJpC0J77AaKA==",
"license": "MIT"
},
"node_modules/lie": {
@@ -15485,9 +15485,9 @@
}
},
"node_modules/rc-table": {
"version": "7.50.2",
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.2.tgz",
"integrity": "sha512-+nJbzxzstBriLb5sr9U7Vjs7+4dO8cWlouQbMwBVYghk2vr508bBdkHJeP/z9HVjAIKmAgMQKxmtbgDd3gc5wA==",
"version": "7.50.3",
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.3.tgz",
"integrity": "sha512-Z4/zNCzjv7f/XzPRecb+vJU0DJKdsYt4YRkDzNl4G05m7JmxrKGYC2KqN1Ew6jw2zJq7cxVv3z39qyZOHMuf7A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.10.1",
@@ -15761,9 +15761,9 @@
}
},
"node_modules/react-error-overlay": {
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz",
"integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==",
"dev": true,
"license": "MIT"
},
@@ -15816,9 +15816,9 @@
}
},
"node_modules/react-icons": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
"integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
"license": "MIT",
"peerDependencies": {
"react": "*"
@@ -16798,9 +16798,9 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.84.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.84.0.tgz",
"integrity": "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==",
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz",
"integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==",
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
@@ -17861,9 +17861,9 @@
}
},
"node_modules/swr": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.0.tgz",
"integrity": "sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.2.tgz",
"integrity": "sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.3",
@@ -18849,9 +18849,9 @@
}
},
"node_modules/userpilot": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/userpilot/-/userpilot-1.3.6.tgz",
"integrity": "sha512-NK/4sQTnWrpER164PkWzLdLjUc2766B4yeLdLiFDoRfyLNAc3SecLWszZH6oPlv67B+XcYzqtmzEalE86bkljw==",
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/userpilot/-/userpilot-1.3.7.tgz",
"integrity": "sha512-F6RiNcaYaHuWZyVLzyYBZQ4YAQzAgN4tWYyRkI0ZST/HSYwoVI6DGenNHEEqPoy28rZngVehXMyKKQ/1aRL7vQ==",
"license": "MIT",
"dependencies": {
"@ndhoule/includes": "^2.0.1",
@@ -18962,14 +18962,14 @@
}
},
"node_modules/vite": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz",
"integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==",
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.1.1.tgz",
"integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.24.2",
"postcss": "^8.5.1",
"postcss": "^8.5.2",
"rollup": "^4.30.1"
},
"bin": {
@@ -19194,9 +19194,9 @@
}
},
"node_modules/vite/node_modules/postcss": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{

View File

@@ -8,18 +8,18 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/pro-layout": "^7.22.2",
"@apollo/client": "^3.12.11",
"@ant-design/pro-layout": "^7.22.3",
"@apollo/client": "^3.13.1",
"@emotion/is-prop-valid": "^1.3.1",
"@fingerprintjs/fingerprintjs": "^4.6.0",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.5.0",
"@sentry/cli": "^2.40.0",
"@sentry/cli": "^2.42.1",
"@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.13.0",
"@tanem/react-nprogress": "^5.0.53",
"@vitejs/plugin-react": "^4.3.4",
"antd": "^5.24.0",
"antd": "^5.24.1",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1",
@@ -35,9 +35,9 @@
"firebase": "^10.13.2",
"graphql": "^16.10.0",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-browser-languagedetector": "^8.0.3",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.18",
"libphonenumber-js": "^1.11.20",
"logrocket": "^8.1.2",
"markerjs2": "^2.32.3",
"memoize-one": "^6.0.0",
@@ -55,7 +55,7 @@
"react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.1.3",
"react-icons": "^5.4.0",
"react-icons": "^5.5.0",
"react-image-lightbox": "^5.1.4",
"react-markdown": "^9.0.3",
"react-number-format": "^5.4.3",
@@ -73,12 +73,12 @@
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.84.0",
"sass": "^1.85.0",
"socket.io-client": "^4.8.1",
"styled-components": "^6.1.15",
"subscriptions-transport-ws": "^0.11.0",
"use-memo-one": "^1.1.3",
"userpilot": "^1.3.6",
"userpilot": "^1.3.7",
"vite-plugin-ejs": "^1.7.0",
"web-vitals": "^3.5.2"
},
@@ -141,10 +141,10 @@
"globals": "^15.15.0",
"memfs": "^4.17.0",
"os-browserify": "^0.3.0",
"react-error-overlay": "6.0.11",
"react-error-overlay": "^6.1.0",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^6.1.0",
"vite": "^6.1.1",
"vite-plugin-babel": "^1.3.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.23.0",

16
package-lock.json generated
View File

@@ -24,8 +24,8 @@
"better-queue": "^3.8.12",
"bluebird": "^3.7.2",
"body-parser": "^1.20.3",
"bullmq": "^5.40.4",
"chart.js": "^4.4.6",
"bullmq": "^5.41.3",
"chart.js": "^4.4.8",
"cloudinary": "^2.5.1",
"compression": "^1.8.0",
"cookie-parser": "^1.4.7",
@@ -4093,9 +4093,9 @@
}
},
"node_modules/bullmq": {
"version": "5.40.4",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.40.4.tgz",
"integrity": "sha512-MaIOhc31ZbVi9HbY0VAalsXoywelzEPNr6dojoKSMCXDnEVTQ27LkT5LA0Mlpr7ZunMLfpH94SLYrWNsPMsQrg==",
"version": "5.41.3",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.3.tgz",
"integrity": "sha512-tWTeuO/BHDg6gKVnQJMjO42zkhsGss6s4bMdgJU24JVBT53yUvDjaO9H0L/BHKAtsMi4xlxkrDuMNSYWeHlekA==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.9.0",
@@ -4235,9 +4235,9 @@
}
},
"node_modules/chart.js": {
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz",
"integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==",
"version": "4.4.8",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz",
"integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"

View File

@@ -34,8 +34,8 @@
"better-queue": "^3.8.12",
"bluebird": "^3.7.2",
"body-parser": "^1.20.3",
"bullmq": "^5.40.4",
"chart.js": "^4.4.6",
"bullmq": "^5.41.3",
"chart.js": "^4.4.8",
"cloudinary": "^2.5.1",
"compression": "^1.8.0",
"cookie-parser": "^1.4.7",

View File

@@ -1,65 +1,87 @@
/**
* Parses an event by comparing old and new data to determine which fields have changed.
*
* This function analyzes the differences between previous (`oldData`) and current (`newData`)
* data states to identify changed fields. It determines if the event is a new entry or an update
* and optionally extracts a `jobId` based on a specified field. The result includes details
* about changed fields, the event type, and associated metadata.
*
* @param {Object} options - Configuration options for parsing the event.
* @param {Object} [options.oldData] - The previous state of the data (undefined for new entries).
* @param {Object} options.newData - The current state of the data.
* @param {string} options.trigger - The type of event trigger (e.g., 'INSERT', 'UPDATE').
* @param {string} options.table - The name of the table associated with the event.
* @param {string} [options.jobIdField] - The field name used to extract the jobId (optional).
* @returns {Object} An object containing the parsed event details:
* - {Array<string>} changedFieldNames - List of field names that have changed.
* - {Object} changedFields - Map of changed fields with their old and new values.
* - {boolean} isNew - True if the event is a new entry (no oldData provided).
* - {Object} data - The current data state (`newData`).
* - {string} trigger - The event trigger type.
* - {string} table - The table name.
* - {string|null} jobId - The extracted jobId or null if not applicable.
*/
const eventParser = async ({ oldData, newData, trigger, table, jobIdField }) => {
const isNew = !oldData;
const isNew = !oldData; // True if no old data exists, indicating a new entry
let changedFields = {};
let changedFieldNames = [];
if (isNew) {
// If there's no old data, every field in newData is considered new
// For new entries, all fields in newData are considered "changed" (from undefined to their value)
changedFields = Object.fromEntries(
Object.entries(newData).map(([key, value]) => [key, { old: undefined, new: value }])
);
changedFieldNames = Object.keys(newData);
changedFieldNames = Object.keys(newData); // All keys are new
} else {
// Compare oldData with newData for changes
// Compare oldData and newData to detect updates
for (const key in newData) {
if (Object.prototype.hasOwnProperty.call(newData, key)) {
// Check if the key exists in oldData and if values differ
// Check if the field is new or its value has changed
if (
!Object.prototype.hasOwnProperty.call(oldData, key) ||
JSON.stringify(oldData[key]) !== JSON.stringify(newData[key])
!Object.prototype.hasOwnProperty.call(oldData, key) || // Field didnt exist before
JSON.stringify(oldData[key]) !== JSON.stringify(newData[key]) // Values differ (deep comparison)
) {
changedFields[key] = {
old: oldData[key], // Could be undefined if key didnt exist in oldData
old: oldData[key], // Undefined if field wasnt in oldData
new: newData[key]
};
changedFieldNames.push(key);
}
}
}
// Check for fields that were removed
// Identify fields removed in newData (present in oldData but absent in newData)
for (const key in oldData) {
if (Object.prototype.hasOwnProperty.call(oldData, key) && !Object.prototype.hasOwnProperty.call(newData, key)) {
changedFields[key] = {
old: oldData[key],
new: null // Indicate field was removed
new: null // Mark as removed
};
changedFieldNames.push(key);
}
}
}
// Extract jobId based on jobIdField
// Extract jobId if jobIdField is provided
let jobId = null;
if (jobIdField) {
let keyName = jobIdField;
const prefix = "req.body.event.new.";
// Strip prefix if present to isolate the actual field name
if (keyName.startsWith(prefix)) {
keyName = keyName.slice(prefix.length);
}
// Look for jobId in newData first, then fallback to oldData if necessary
jobId = newData[keyName] || (oldData && oldData[keyName]) || null;
}
return {
changedFieldNames,
changedFields,
isNew,
data: newData,
trigger,
table,
jobId
changedFieldNames, // Array of fields that changed
changedFields, // Object with old/new values for changed fields
isNew, // Boolean indicating if this is a new entry
data: newData, // Current data state
trigger, // Event trigger (e.g., 'INSERT', 'UPDATE')
table, // Associated table name
jobId // Extracted jobId or null
};
};

View File

@@ -1,23 +0,0 @@
const path = require("path");
require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
const Queue = require("better-queue");
const logger = require("../utils/logger");
const notificationsEmailQueue = () =>
new Queue(
(taskIds, cb) => {
logger.log("Processing Notification Emails: ", "silly", null, null);
cb(null);
},
{
batchSize: 50,
batchDelay: 5000,
// The lower this is, the more likely we are to hit the rate limit.
batchDelayTimeout: 1000
}
);
module.exports = { notificationsEmailQueue };

View File

@@ -3,22 +3,36 @@ const { Queue, Worker } = require("bullmq");
let addQueue;
let consolidateQueue;
/**
* Initializes the notification queues and workers for adding and consolidating notifications.
*
* @param {Object} options - Configuration options for queue initialization.
* @param {Object} options.pubClient - Redis client instance for queue communication.
* @param {Object} options.logger - Logger instance for logging events and debugging.
* @param {Object} options.redisHelpers - Utility functions for Redis operations.
* @param {Object} options.ioRedis - Socket.io Redis adapter for real-time event emission.
* @returns {Queue} The initialized `addQueue` instance for dispatching notifications.
*/
const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
// Only initialize if queues don't already exist
if (!addQueue || !consolidateQueue) {
logger.logger.info("Initializing Notifications Queues");
// Create queue for adding notifications
addQueue = new Queue("notificationsAdd", {
connection: pubClient,
prefix: "{BULLMQ}",
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
prefix: "{BULLMQ}", // Namespace prefix for BullMQ in Redis
defaultJobOptions: { removeOnComplete: true, removeOnFail: true } // Cleanup jobs after success/failure
});
// Create queue for consolidating notifications
consolidateQueue = new Queue("notificationsConsolidate", {
connection: pubClient,
prefix: "{BULLMQ}",
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
});
// Worker to process jobs from the addQueue
const addWorker = new Worker(
"notificationsAdd",
async (job) => {
@@ -28,27 +42,32 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
const redisKeyPrefix = `app:notifications:${jobId}`;
const notification = { key, variables, timestamp: Date.now() };
// Store notification for each recipient in Redis
for (const recipient of recipients) {
const { user } = recipient;
const userKey = `${redisKeyPrefix}:${user}`;
const existingNotifications = await pubClient.get(userKey);
const notifications = existingNotifications ? JSON.parse(existingNotifications) : [];
notifications.push(notification);
// Set with 40-second expiration to avoid stale data
await pubClient.set(userKey, JSON.stringify(notifications), "EX", 40);
logger.logger.debug(`Stored notification for ${user} under ${userKey}: ${JSON.stringify(notifications)}`);
}
const consolidateKey = `app:consolidate:${jobId}`;
// setnx ensures only one consolidation job is scheduled (atomic operation)
const flagSet = await pubClient.setnx(consolidateKey, "pending");
logger.logger.debug(`Consolidation flag set for jobId ${jobId}: ${flagSet}`);
if (flagSet) {
// Schedule consolidation job to run after a 5-second delay
await consolidateQueue.add(
"consolidate-notifications",
{ jobId, recipients },
{ jobId: `consolidate:${jobId}`, delay: 5000 }
);
logger.logger.info(`Scheduled consolidation for jobId ${jobId}`);
// Set expiration on flag to clean up after 5 minutes
await pubClient.expire(consolidateKey, 300);
} else {
logger.logger.debug(`Consolidation already scheduled for jobId ${jobId}`);
@@ -57,10 +76,11 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
{
connection: pubClient,
prefix: "{BULLMQ}",
concurrency: 5
concurrency: 5 // Process up to 5 jobs concurrently
}
);
// Worker to process jobs from the consolidateQueue
const consolidateWorker = new Worker(
"notificationsConsolidate",
async (job) => {
@@ -69,15 +89,18 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
const redisKeyPrefix = `app:notifications:${jobId}`;
const lockKey = `lock:consolidate:${jobId}`;
// Acquire a lock to prevent concurrent consolidation (NX = set if not exists)
const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", 10);
logger.logger.debug(`Lock acquisition for jobId ${jobId}: ${lockAcquired}`);
if (lockAcquired) {
try {
const allNotifications = {};
// Get unique user IDs to avoid duplicate processing
const uniqueUsers = [...new Set(recipients.map((r) => r.user))];
logger.logger.debug(`Unique users for jobId ${jobId}: ${uniqueUsers}`);
// Retrieve and structure notifications by user and bodyShopId
for (const user of uniqueUsers) {
const userKey = `${redisKeyPrefix}:${user}`;
const notifications = await pubClient.get(userKey);
@@ -90,7 +113,7 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
allNotifications[user] = allNotifications[user] || {};
allNotifications[user][bodyShopId] = parsedNotifications;
}
await pubClient.del(userKey);
await pubClient.del(userKey); // Clean up after retrieval
logger.logger.debug(`Deleted Redis key ${userKey}`);
} else {
logger.logger.warn(`No notifications found for ${user} under ${userKey}`);
@@ -99,6 +122,7 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
logger.logger.debug(`Consolidated notifications: ${JSON.stringify(allNotifications)}`);
// Emit notifications to users via Socket.io
for (const [user, bodyShopData] of Object.entries(allNotifications)) {
const userMapping = await redisHelpers.getUserSocketMapping(user);
logger.logger.debug(`User socket mapping for ${user}: ${JSON.stringify(userMapping)}`);
@@ -107,7 +131,11 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
if (userMapping && userMapping[bodyShopId]?.socketIds) {
userMapping[bodyShopId].socketIds.forEach((socketId) => {
logger.logger.debug(
`Emitting to socket ${socketId}: ${JSON.stringify({ jobId, bodyShopId, notifications })}`
`Emitting to socket ${socketId}: ${JSON.stringify({
jobId,
bodyShopId,
notifications
})}`
);
ioRedis.to(socketId).emit("notification", {
jobId,
@@ -124,12 +152,13 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
}
}
// Clean up consolidation flag after processing
await pubClient.del(`app:consolidate:${jobId}`);
} catch (err) {
logger.logger.error(`Consolidation error for jobId ${jobId}: ${err.message}`, { error: err });
throw err; // Re-throw to trigger failed event
throw err; // Re-throw to trigger BullMQ's failed event
} finally {
await pubClient.del(lockKey);
await pubClient.del(lockKey); // Release lock regardless of success/failure
}
} else {
logger.logger.info(`Skipped consolidation for jobId ${jobId} - lock held by another worker`);
@@ -138,42 +167,63 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
{
connection: pubClient,
prefix: "{BULLMQ}",
concurrency: 1,
limiter: { max: 1, duration: 5000 }
concurrency: 1, // Single concurrency to avoid race conditions
limiter: { max: 1, duration: 5000 } // Rate limit: 1 job every 5 seconds
}
);
// Log worker completion events
addWorker.on("completed", (job) => logger.logger.info(`Add job ${job.id} completed`));
consolidateWorker.on("completed", (job) => logger.logger.info(`Consolidate job ${job.id} completed`));
// Log worker failure events with error details
addWorker.on("failed", (job, err) =>
logger.logger.error(`Add job ${job.id} failed: ${err.message}`, { error: err })
);
consolidateWorker.on("failed", (job, err) =>
logger.logger.error(`Consolidate job ${job.id} failed: ${err.message}`, { error: err })
);
// Graceful shutdown handler for workers
const shutdown = async () => {
logger.logger.info("Closing app queue workers...");
await Promise.all([addWorker.close(), consolidateWorker.close()]);
logger.logger.info("App queue workers closed");
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown); // Handle termination signal
process.on("SIGINT", shutdown); // Handle interrupt signal (e.g., Ctrl+C)
}
return addQueue; // Return the add queue for dispatching
return addQueue; // Return queue for external use
};
/**
* Retrieves the initialized `addQueue` instance.
*
* @returns {Queue} The `addQueue` instance for adding notifications.
* @throws {Error} If `addQueue` is not initialized (i.e., `loadAppQueue` wasnt called).
*/
const getQueue = () => {
if (!addQueue) throw new Error("Add queue not initialized. Ensure loadAppQueue is called during bootstrap.");
return addQueue;
};
/**
* Dispatches notifications to the `addQueue` for processing.
*
* @param {Object} options - Options for dispatching notifications.
* @param {Array} options.appsToDispatch - Array of notification objects to dispatch.
* @param {Object} options.logger - Logger instance for logging dispatch events.
* @returns {Promise<void>} Resolves when all notifications are added to the queue.
*/
const dispatchAppsToQueue = async ({ appsToDispatch, logger }) => {
const appQueue = getQueue();
for (const app of appsToDispatch) {
const { jobId, bodyShopId, key, variables, recipients } = app;
// Unique jobId with timestamp to avoid duplicates
await appQueue.add(
"add-notification",
{ jobId, bodyShopId, key, variables, recipients },

View File

@@ -4,30 +4,44 @@ const { sendTaskEmail } = require("../../email/sendemail");
let emailQueue;
let worker;
const loadEmailQueue = async ({ pubClient, logger, redisHelpers }) => {
// Consolidate the same way the App Queue Does.
/**
* Initializes the email queue and worker for sending notifications via email.
*
* @param {Object} options - Configuration options for queue initialization.
* @param {Object} options.pubClient - Redis client instance for queue communication.
* @param {Object} options.logger - Logger instance for logging events and debugging.
* @returns {Queue} The initialized `emailQueue` instance for dispatching emails.
*/
const loadEmailQueue = async ({ pubClient, logger }) => {
// Only initialize if queue doesn't already exist
if (!emailQueue) {
logger.logger.info("Initializing Notifications Email Queue");
// Create queue for email notifications
emailQueue = new Queue("notificationsEmails", {
connection: pubClient,
prefix: "{BULLMQ}",
prefix: "{BULLMQ}", // Namespace prefix for BullMQ in Redis
defaultJobOptions: {
attempts: 3,
attempts: 3, // Retry failed jobs up to 3 times
backoff: {
type: "exponential",
delay: 1000
type: "exponential", // Exponential backoff strategy
delay: 1000 // Initial delay of 1 second
}
}
});
// Initialize the worker during queue setup
// Worker to process jobs from the emailQueue
worker = new Worker(
"notificationsEmails",
async (job) => {
const { subject, body, recipients } = job.data;
logger.logger.debug(`Processing email job ${job.id} for ${recipients.length} recipients`);
const { subject, body, recipient } = job.data;
logger.logger.debug(`Processing email job ${job.id} for recipient ${recipient}`);
// Send email to a single recipient
await sendTaskEmail({
to: recipients.map((r) => r.user),
to: recipient, // Single email address
subject,
type: "text",
text: body
@@ -38,9 +52,9 @@ const loadEmailQueue = async ({ pubClient, logger, redisHelpers }) => {
{
connection: pubClient,
prefix: "{BULLMQ}",
concurrency: 2, // Reduced for multi-node setup; adjust based on load
concurrency: 2, // Process up to 2 jobs concurrently
limiter: {
max: 10, // Max 10 jobs per minute per worker
max: 10, // Maximum of 10 jobs per minute
duration: 60 * 1000 // 1 minute
}
}
@@ -59,7 +73,7 @@ const loadEmailQueue = async ({ pubClient, logger, redisHelpers }) => {
logger.logger.error("Worker error:", { error: err });
});
// Graceful shutdown handling
// Graceful shutdown handler for the worker
const shutdown = async () => {
if (worker) {
logger.logger.info("Closing email queue worker...");
@@ -68,13 +82,19 @@ const loadEmailQueue = async ({ pubClient, logger, redisHelpers }) => {
}
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown); // Handle termination signal
process.on("SIGINT", shutdown); // Handle interrupt signal (e.g., Ctrl+C)
}
return emailQueue;
return emailQueue; // Return queue for external use
};
/**
* Retrieves the initialized `emailQueue` instance.
*
* @returns {Queue} The `emailQueue` instance for sending emails.
* @throws {Error} If `emailQueue` is not initialized (i.e., `loadEmailQueue` wasnt called).
*/
const getQueue = () => {
if (!emailQueue) {
throw new Error("Email queue not initialized. Ensure loadEmailQueue is called during bootstrap.");
@@ -82,17 +102,31 @@ const getQueue = () => {
return emailQueue;
};
/**
* Dispatches emails to the `emailQueue` for processing, creating one job per recipient.
*
* @param {Object} options - Options for dispatching emails.
* @param {Array} options.emailsToDispatch - Array of email objects to dispatch.
* @param {Object} options.logger - Logger instance for logging dispatch events.
* @returns {Promise<void>} Resolves when all email jobs are added to the queue.
*/
const dispatchEmailsToQueue = async ({ emailsToDispatch, logger }) => {
const emailQueue = getQueue();
for (const email of emailsToDispatch) {
const { subject, body, recipients } = email;
await emailQueue.add("send-email", {
subject,
body,
recipients
}); // Job options moved to defaultJobOptions in Queue
logger.logger.debug(`Added email to queue: ${subject} for ${recipients.length} recipients`);
// Create an array of jobs, one per recipient
const jobs = recipients.map((recipient) => ({
name: "send-email",
data: {
subject,
body,
recipient: recipient.user // Extract the email address from recipient object
}
}));
// Add all jobs for this email in one operation
await emailQueue.addBulk(jobs);
logger.logger.debug(`Added ${jobs.length} email jobs to queue for subject: ${subject}`);
}
};

View File

@@ -1,14 +1,29 @@
const { getJobAssignmentType } = require("./stringHelpers");
/**
* Populates the recipients for app, email, and FCM notifications based on scenario watchers.
*
* @param {Object} data - The data object containing scenarioWatchers and bodyShopId.
* @param {Object} result - The result object to populate with recipients for app, email, and FCM notifications.
*/
const populateWatchers = (data, result) => {
data.scenarioWatchers.forEach((recipients) => {
const { user, app, fcm, email } = recipients;
// Add user to app recipients with bodyShopId if app notification is enabled
if (app === true) result.app.recipients.push({ user, bodyShopId: data.bodyShopId });
// Add user to FCM recipients if FCM notification is enabled
if (fcm === true) result.fcm.recipients.push(user);
// Add user to email recipients if email notification is enabled
if (email === true) result.email.recipients.push({ user });
});
};
/**
* Builds notification data for changes to alternate transport.
*
* @param {Object} data - The data object containing job details and alternate transport changes.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const alternateTransportChangedBuilder = (data) => {
const result = {
app: {
@@ -33,6 +48,12 @@ const alternateTransportChangedBuilder = (data) => {
return result;
};
/**
* Builds notification data for bill posted events.
*
* @param {Object} data - The data object containing job and billing details.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const billPostedHandler = (data) => {
const result = {
app: {
@@ -56,6 +77,12 @@ const billPostedHandler = (data) => {
return result;
};
/**
* Builds notification data for changes to critical parts status.
*
* @param {Object} data - The data object containing job details and critical parts status changes.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const criticalPartsStatusChangedBuilder = (data) => {
const result = {
app: {
@@ -80,7 +107,14 @@ const criticalPartsStatusChangedBuilder = (data) => {
return result;
};
/**
* Builds notification data for completed intake or delivery checklists.
*
* @param {Object} data - The data object containing job details and checklist changes.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const intakeDeliveryChecklistCompletedBuilder = (data) => {
// Determine checklist type based on which field was changed
const checklistType = data.changedFields.intakechecklist ? "intake" : "delivery";
const result = {
app: {
@@ -105,6 +139,12 @@ const intakeDeliveryChecklistCompletedBuilder = (data) => {
return result;
};
/**
* Builds notification data for job assignment events.
*
* @param {Object} data - The data object containing job details and scenario fields.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const jobAssignedToMeBuilder = (data) => {
const result = {
app: {
@@ -128,6 +168,12 @@ const jobAssignedToMeBuilder = (data) => {
return result;
};
/**
* Builds notification data for jobs added to production.
*
* @param {Object} data - The data object containing job details.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const jobsAddedToProductionBuilder = (data) => {
const result = {
app: {
@@ -149,7 +195,12 @@ const jobsAddedToProductionBuilder = (data) => {
return result;
};
// Verified
/**
* Builds notification data for job status changes.
*
* @param {Object} data - The data object containing job details and status changes.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const jobStatusChangeBuilder = (data) => {
const result = {
app: {
@@ -164,7 +215,7 @@ const jobStatusChangeBuilder = (data) => {
},
email: {
subject: `The status of ${data?.jobRoNumber} (${data.bodyShopName}) has changed from ${data.changedFields.status.old} to ${data.data.status}`,
body: `...`,
body: `...`, // Placeholder indicating email body may need further customization
recipients: []
},
fcm: { recipients: [] }
@@ -174,6 +225,12 @@ const jobStatusChangeBuilder = (data) => {
return result;
};
/**
* Builds notification data for new media added or reassigned events.
*
* @param {Object} data - The data object containing job details.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const newMediaAddedReassignedBuilder = (data) => {
const result = {
app: {
@@ -195,7 +252,12 @@ const newMediaAddedReassignedBuilder = (data) => {
return result;
};
// Verified
/**
* Builds notification data for new notes added to a job.
*
* @param {Object} data - The data object containing job details and note text.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const newNoteAddedBuilder = (data) => {
const result = {
app: {
@@ -219,6 +281,12 @@ const newNoteAddedBuilder = (data) => {
return result;
};
/**
* Builds notification data for new time tickets posted.
*
* @param {Object} data - The data object containing job details.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const newTimeTicketPostedBuilder = (data) => {
const result = {
app: {
@@ -240,6 +308,12 @@ const newTimeTicketPostedBuilder = (data) => {
return result;
};
/**
* Builds notification data for parts marked as back-ordered.
*
* @param {Object} data - The data object containing job details and parts status changes.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const partMarkedBackOrderedBuilder = (data) => {
const result = {
app: {
@@ -264,6 +338,12 @@ const partMarkedBackOrderedBuilder = (data) => {
return result;
};
/**
* Builds notification data for payment collection events.
*
* @param {Object} data - The data object containing job and payment details.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const paymentCollectedCompletedBuilder = (data) => {
const result = {
app: {
@@ -287,6 +367,12 @@ const paymentCollectedCompletedBuilder = (data) => {
return result;
};
/**
* Builds notification data for changes to scheduled dates.
*
* @param {Object} data - The data object containing job details and scheduling changes.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const scheduledDatesChangedBuilder = (data) => {
const result = {
app: {
@@ -315,6 +401,12 @@ const scheduledDatesChangedBuilder = (data) => {
return result;
};
/**
* Builds notification data for supplement imported events.
*
* @param {Object} data - The data object containing job and supplement details.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const supplementImportedBuilder = (data) => {
const result = {
app: {
@@ -338,6 +430,12 @@ const supplementImportedBuilder = (data) => {
return result;
};
/**
* Builds notification data for tasks updated or created.
*
* @param {Object} data - The data object containing job details and task event type.
* @returns {Object} Notification data structured for app, email, and FCM channels.
*/
const tasksUpdatedCreatedBuilder = (data) => {
const result = {
app: {

View File

@@ -2,6 +2,9 @@
* @module scenarioParser
* @description
* This module exports a function that parses an event and triggers notification scenarios based on the event data.
* It integrates with event parsing utilities, GraphQL queries, and notification queues to manage the dispatching
* of notifications via email and app channels. The function processes event data, identifies relevant scenarios,
* queries user notification preferences, and dispatches notifications accordingly.
*/
const eventParser = require("./eventParser");
@@ -9,23 +12,28 @@ const { client: gqlClient } = require("../graphql-client/graphql-client");
const queries = require("../graphql-client/queries");
const { isEmpty, isFunction } = require("lodash");
const { getMatchingScenarios } = require("./scenarioMapperr");
const consoleDir = require("../utils/consoleDir");
const { dispatchEmailsToQueue } = require("./queues/emailQueue");
const { dispatchAppsToQueue } = require("./queues/appQueue");
/**
* Parses an event and determines matching scenarios for notifications.
* Queries job watchers and notification settings before triggering scenario builders.
*
* @param {Object} req - The request object containing event data, trigger, table, and logger.
* @param {string} jobIdField - The field name used to extract the job ID from the event data.
* @returns {Promise<void>} Resolves when the parsing and notification dispatching process is complete.
* @throws {Error} If required request fields (event data, trigger, or table) or body shop data are missing.
*/
const scenarioParser = async (req, jobIdField) => {
const { event, trigger, table } = req.body;
const { logger } = req;
// Validate that required fields are present in the request body
if (!event?.data || !trigger || !table) {
throw new Error("Missing required request fields: event data, trigger, or table.");
}
// Step 1: Parse event data to extract necessary details.
// Step 1: Parse the event data to extract details like job ID and changed fields
const eventData = await eventParser({
newData: event.data.new,
oldData: event.data.old,
@@ -34,11 +42,12 @@ const scenarioParser = async (req, jobIdField) => {
jobIdField
});
// Step 2: Query job watchers for the given job ID.
// Step 2: Query job watchers associated with the job ID using GraphQL
const watcherData = await gqlClient.request(queries.GET_JOB_WATCHERS, {
jobid: eventData.jobId
});
// Transform watcher data into a simplified format with email and employee details
const jobWatchers = watcherData?.job_watchers_aggregate?.nodes?.map((watcher) => ({
email: watcher.user_email,
firstName: watcher?.user?.employee?.first_name,
@@ -46,21 +55,23 @@ const scenarioParser = async (req, jobIdField) => {
employeeId: watcher?.user?.employee?.id
}));
// Exit early if no job watchers are found for this job
if (isEmpty(jobWatchers)) {
return;
}
// Step 3: Retrieve body shop information from the job.
// Step 3: Extract body shop information from the job data
const bodyShopId = watcherData?.job?.bodyshop?.id;
const bodyShopName = watcherData?.job?.bodyshop?.shopname;
const jobRoNumber = watcherData?.job?.ro_number;
const jobClaimNumber = watcherData?.job?.clm_no;
// Validate that body shop data exists, as its required for notifications
if (!bodyShopId || !bodyShopName) {
throw new Error("No bodyshop data found for this job.");
}
// Step 4: Determine matching scenarios based on event data.
// Step 4: Identify scenarios that match the event data and job context
const matchingScenarios = getMatchingScenarios({
...eventData,
jobWatchers,
@@ -68,10 +79,12 @@ const scenarioParser = async (req, jobIdField) => {
bodyShopName
});
// Exit early if no matching scenarios are identified
if (isEmpty(matchingScenarios)) {
return;
}
// Combine event data with additional context for scenario processing
const finalScenarioData = {
...eventData,
jobWatchers,
@@ -80,22 +93,24 @@ const scenarioParser = async (req, jobIdField) => {
matchingScenarios
};
// Step 5: Query notification settings for job watchers.
// Step 5: Query notification settings for the job watchers
const associationsData = await gqlClient.request(queries.GET_NOTIFICATION_ASSOCIATIONS, {
emails: jobWatchers.map((x) => x.email),
shopid: bodyShopId
});
// Exit early if no notification associations are found
if (isEmpty(associationsData?.associations)) {
return;
}
// Step 6: Filter scenario watchers based on enabled notification methods.
// Step 6: Filter scenario watchers based on their enabled notification methods
finalScenarioData.matchingScenarios = finalScenarioData.matchingScenarios.map((scenario) => ({
...scenario,
scenarioWatchers: associationsData.associations
.filter((assoc) => {
const settings = assoc.notification_settings && assoc.notification_settings[scenario.key];
// Include only watchers with at least one enabled notification method (app, email, or FCM)
return settings && (settings.app || settings.email || settings.fcm);
})
.map((assoc) => {
@@ -103,6 +118,7 @@ const scenarioParser = async (req, jobIdField) => {
const watcherEmail = assoc.user || assoc.useremail;
const matchingWatcher = jobWatchers.find((watcher) => watcher.email === watcherEmail);
// Build watcher object with notification preferences and personal details
return {
user: watcherEmail,
email: settings.email,
@@ -115,21 +131,23 @@ const scenarioParser = async (req, jobIdField) => {
})
}));
// Exit early if no scenarios have eligible watchers after filtering
if (isEmpty(finalScenarioData?.matchingScenarios)) {
return;
}
// Step 7: Trigger scenario builders for matching scenarios with eligible watchers.
// Step 7: Build and collect scenarios to dispatch notifications for
const scenariosToDispatch = [];
for (const scenario of finalScenarioData.matchingScenarios) {
// Skip if no watchers or no builder function is defined for the scenario
if (isEmpty(scenario.scenarioWatchers) || !isFunction(scenario.builder)) {
continue;
}
let eligibleWatchers = scenario.scenarioWatchers;
// Ensure watchers are only notified if they are assigned to the changed field.
// Filter watchers to only those assigned to changed fields, if specified
if (scenario.matchToUserFields && scenario.matchToUserFields.length > 0) {
eligibleWatchers = scenario.scenarioWatchers.filter((watcher) =>
scenario.matchToUserFields.some(
@@ -138,14 +156,16 @@ const scenarioParser = async (req, jobIdField) => {
);
}
// Skip if no watchers remain after filtering
if (isEmpty(eligibleWatchers)) {
continue;
}
// Step 8: Filter scenario fields to only include changed fields.
// Step 8: Filter scenario fields to include only those that changed
const filteredScenarioFields =
scenario.fields?.filter((field) => eventData.changedFieldNames.includes(field)) || [];
// Use the scenarios builder to construct the notification data
scenariosToDispatch.push(
scenario.builder({
trigger: finalScenarioData.trigger.name,
@@ -167,30 +187,33 @@ const scenarioParser = async (req, jobIdField) => {
);
}
// Exit early if no scenarios are ready to dispatch
if (isEmpty(scenariosToDispatch)) {
return;
}
// Step 9: Dispatch Email Notifications to the Email Notification Queue
// Step 9: Dispatch email notifications to the email queue
const emailsToDispatch = scenariosToDispatch.map((scenario) => scenario?.email);
if (!isEmpty(emailsToDispatch)) {
dispatchEmailsToQueue({
emailsToDispatch,
logger
}).catch((e) =>
// Log any errors encountered during email dispatching
logger.log("Something went wrong dispatching emails to the Email Notification Queue", "error", "queue", null, {
message: e?.message
})
);
}
// Step 10: Dispatch App Notifications to the App Notification Queue
// Step 10: Dispatch app notifications to the app queue
const appsToDispatch = scenariosToDispatch.map((scenario) => scenario?.app);
if (!isEmpty(appsToDispatch)) {
dispatchAppsToQueue({
appsToDispatch,
logger
}).catch((e) =>
// Log any errors encountered during app notification dispatching
logger.log("Something went wrong dispatching apps to the App Notification Queue", "error", "queue", null, {
message: e?.message
})

View File

@@ -1,3 +1,16 @@
/**
* @module jobAssignmentHelper
* @description
* This module provides utility functions for handling job assignment types.
* Currently, it includes a function to map lowercase job assignment codes to their corresponding human-readable job types.
*/
/**
* Maps a lowercase job assignment code to its corresponding human-readable job type.
*
* @param {string} data - The lowercase job assignment code (e.g., "employee_pre").
* @returns {string} The human-readable job type (e.g., "Prep"). Returns an empty string if the code is unknown or if the input is null/undefined.
*/
const getJobAssignmentType = (data) => {
switch (data) {
case "employee_pre":