feature/IO-3096-GlobalNotifications - Cleanup and Package bumps
This commit is contained in:
176
client/package-lock.json
generated
176
client/package-lock.json
generated
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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
16
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 didn’t exist before
|
||||
JSON.stringify(oldData[key]) !== JSON.stringify(newData[key]) // Values differ (deep comparison)
|
||||
) {
|
||||
changedFields[key] = {
|
||||
old: oldData[key], // Could be undefined if key didn’t exist in oldData
|
||||
old: oldData[key], // Undefined if field wasn’t 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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 };
|
||||
@@ -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` wasn’t 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 },
|
||||
|
||||
@@ -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` wasn’t 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}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 it’s 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 scenario’s 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
|
||||
})
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user