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",
|
"version": "0.2.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/pro-layout": "^7.22.2",
|
"@ant-design/pro-layout": "^7.22.3",
|
||||||
"@apollo/client": "^3.12.11",
|
"@apollo/client": "^3.13.1",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.0",
|
"@fingerprintjs/fingerprintjs": "^4.6.0",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.5.0",
|
"@reduxjs/toolkit": "^2.5.0",
|
||||||
"@sentry/cli": "^2.40.0",
|
"@sentry/cli": "^2.42.1",
|
||||||
"@sentry/react": "^7.114.0",
|
"@sentry/react": "^7.114.0",
|
||||||
"@splitsoftware/splitio-react": "^1.13.0",
|
"@splitsoftware/splitio-react": "^1.13.0",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"antd": "^5.24.0",
|
"antd": "^5.24.1",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^3.3.0",
|
"apollo-link-sentry": "^3.3.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
@@ -36,9 +36,9 @@
|
|||||||
"firebase": "^10.13.2",
|
"firebase": "^10.13.2",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.10.0",
|
||||||
"i18next": "^23.15.1",
|
"i18next": "^23.15.1",
|
||||||
"i18next-browser-languagedetector": "^8.0.2",
|
"i18next-browser-languagedetector": "^8.0.3",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.11.18",
|
"libphonenumber-js": "^1.11.20",
|
||||||
"logrocket": "^8.1.2",
|
"logrocket": "^8.1.2",
|
||||||
"markerjs2": "^2.32.3",
|
"markerjs2": "^2.32.3",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^14.1.3",
|
"react-i18next": "^14.1.3",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^9.0.3",
|
"react-markdown": "^9.0.3",
|
||||||
"react-number-format": "^5.4.3",
|
"react-number-format": "^5.4.3",
|
||||||
@@ -74,12 +74,12 @@
|
|||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.84.0",
|
"sass": "^1.85.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.15",
|
"styled-components": "^6.1.15",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"use-memo-one": "^1.1.3",
|
"use-memo-one": "^1.1.3",
|
||||||
"userpilot": "^1.3.6",
|
"userpilot": "^1.3.7",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
"web-vitals": "^3.5.2"
|
"web-vitals": "^3.5.2"
|
||||||
},
|
},
|
||||||
@@ -105,10 +105,10 @@
|
|||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"memfs": "^4.17.0",
|
"memfs": "^4.17.0",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"react-error-overlay": "6.0.11",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^6.1.0",
|
"vite": "^6.1.1",
|
||||||
"vite-plugin-babel": "^1.3.0",
|
"vite-plugin-babel": "^1.3.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
@@ -218,15 +218,15 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@ant-design/pro-layout": {
|
"node_modules/@ant-design/pro-layout": {
|
||||||
"version": "7.22.2",
|
"version": "7.22.3",
|
||||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.3.tgz",
|
||||||
"integrity": "sha512-RlXqN+EVnF1Sup84O0IjS/vMMgwFnbBZwvR+GVnmZg/+cIa4/BDTXyhbb1KRwUqzn1ctDzj7JfbWOWqmGMw6yA==",
|
"integrity": "sha512-di/EOMDuoMDRjBweqesYyCxEYr2LCmO82y6A4bSwmmJ6ehxN7HGC73Wx4RuBkzDR7kHLTOXt7WxI6875ENT8mg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/cssinjs": "^1.21.1",
|
"@ant-design/cssinjs": "^1.21.1",
|
||||||
"@ant-design/icons": "^5.0.0",
|
"@ant-design/icons": "^5.0.0",
|
||||||
"@ant-design/pro-provider": "2.15.3",
|
"@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",
|
"@babel/runtime": "^7.18.0",
|
||||||
"@umijs/route-utils": "^4.0.0",
|
"@umijs/route-utils": "^4.0.0",
|
||||||
"@umijs/use-params": "^1.0.9",
|
"@umijs/use-params": "^1.0.9",
|
||||||
@@ -265,9 +265,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ant-design/pro-utils": {
|
"node_modules/@ant-design/pro-utils": {
|
||||||
"version": "2.16.3",
|
"version": "2.16.4",
|
||||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.16.4.tgz",
|
||||||
"integrity": "sha512-uNjKh51v/SUlCJbWfhg2lRQB/TB0MyNMCQkFZ8mZBQ2rk3Ew47Sly6VssVVWMjIWBLE+g9fOgPg0C1IVeilIXA==",
|
"integrity": "sha512-PFxqF0fsUsLj8ORvJSuMgVv9NDHwAxZaglzPN/u3jZX7rWYcrHD04EMJEXooZaSyT6Q4+j7SqXDx6oBsdb9zNw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.0.0",
|
"@ant-design/icons": "^5.0.0",
|
||||||
@@ -322,9 +322,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@apollo/client": {
|
"node_modules/@apollo/client": {
|
||||||
"version": "3.12.11",
|
"version": "3.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.12.11.tgz",
|
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.13.1.tgz",
|
||||||
"integrity": "sha512-1RppV9U3E6Uusl/33yGkZa+rXpkGU5iCstcYltwWjdTjoA/YBD2Yyu0aHy8J4uKfIExUgnMW1HJWn4A0E0rRsw==",
|
"integrity": "sha512-HaAt62h3jNUXpJ1v5HNgUiCzPP1c5zc2Q/FeTb2cTk/v09YlhoqKKHQFJI7St50VCJ5q8JVIc03I5bRcBrQxsg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@graphql-typed-document-node/core": "^3.1.1",
|
"@graphql-typed-document-node/core": "^3.1.1",
|
||||||
@@ -5510,9 +5510,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli": {
|
"node_modules/@sentry/cli": {
|
||||||
"version": "2.41.1",
|
"version": "2.42.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.41.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.42.1.tgz",
|
||||||
"integrity": "sha512-0GVmDiTV7R1492wkVY4bGcfC0fSmRmQjuxaaPI8CIV9B2VP9pBVCUizi1mevXaaE4I3fM60LI+XYrKFEneuVog==",
|
"integrity": "sha512-3fonGZoGwlze/iGYDdCJXpG5skXc6j/yYom+k6TqVvJJqSct1RgLJHjCw1P0IxHsR8pNz9f1H85OdLXKxrc6sw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5529,19 +5529,19 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@sentry/cli-darwin": "2.41.1",
|
"@sentry/cli-darwin": "2.42.1",
|
||||||
"@sentry/cli-linux-arm": "2.41.1",
|
"@sentry/cli-linux-arm": "2.42.1",
|
||||||
"@sentry/cli-linux-arm64": "2.41.1",
|
"@sentry/cli-linux-arm64": "2.42.1",
|
||||||
"@sentry/cli-linux-i686": "2.41.1",
|
"@sentry/cli-linux-i686": "2.42.1",
|
||||||
"@sentry/cli-linux-x64": "2.41.1",
|
"@sentry/cli-linux-x64": "2.42.1",
|
||||||
"@sentry/cli-win32-i686": "2.41.1",
|
"@sentry/cli-win32-i686": "2.42.1",
|
||||||
"@sentry/cli-win32-x64": "2.41.1"
|
"@sentry/cli-win32-x64": "2.42.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-darwin": {
|
"node_modules/@sentry/cli-darwin": {
|
||||||
"version": "2.41.1",
|
"version": "2.42.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.41.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.42.1.tgz",
|
||||||
"integrity": "sha512-7pS3pu/SuhE6jOn3wptstAg6B5nUP878O6s+2svT7b5fKNfYUi/6NPK6dAveh2Ca0rwVq40TO4YFJabWMgTpdQ==",
|
"integrity": "sha512-WZFsrzSWtsRK24SiTa+Xod+4Hjlw7xaggmM4lbuo0lISO1EQj+K29jyGX+Ku0qflO1qp1z32bSP/RlWx/1rBjg==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5552,9 +5552,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-linux-arm": {
|
"node_modules/@sentry/cli-linux-arm": {
|
||||||
"version": "2.41.1",
|
"version": "2.42.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.41.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.1.tgz",
|
||||||
"integrity": "sha512-wNUvquD6qjOCczvuBGf9OiD29nuQ6yf8zzfyPJa5Bdx1QXuteKsKb6HBrMwuIR3liyuu0duzHd+H/+p1n541Hg==",
|
"integrity": "sha512-3xR2B9v8e7NjB6U9+oMu2puR3xOv/Axd7qNuUrZxQnNZYtgtnAqIDgSmFTWHOOoged1+AZXe+xDWLN0Y11Q03Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -5569,9 +5569,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-linux-arm64": {
|
"node_modules/@sentry/cli-linux-arm64": {
|
||||||
"version": "2.41.1",
|
"version": "2.42.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.41.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.1.tgz",
|
||||||
"integrity": "sha512-EzYCEnnENBnS5kpNW+2dBcrPZn1MVfywh2joGVQZTpmgDL5YFJ59VOd+K0XuEwqgFI8BSNI14KXZ75s4DD1/Vw==",
|
"integrity": "sha512-8A43bLvDIzquCXblHNadaRm109ANw1Q9VRXg5qLYv7DrPkUm2oQP+oRnuNUgOJ3W/8QQSvANpG9pPko+mJs4xw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -5586,9 +5586,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-linux-i686": {
|
"node_modules/@sentry/cli-linux-i686": {
|
||||||
"version": "2.41.1",
|
"version": "2.42.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.41.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.1.tgz",
|
||||||
"integrity": "sha512-urpQCWrdYnSAsZY3udttuMV88wTJzKZL10xsrp7sjD/Hd+O6qSLVLkxebIlxts70jMLLFHYrQ2bkRg5kKuX6Fg==",
|
"integrity": "sha512-YBz6prKqh1i0gzTg3Rus8ALQWmAk5Acap2U2dGuVYgTt7Bbu6SJbxNC9d8j3RUGu7ylupofUEMqKd391mTHf7g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x86",
|
"x86",
|
||||||
"ia32"
|
"ia32"
|
||||||
@@ -5604,9 +5604,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-linux-x64": {
|
"node_modules/@sentry/cli-linux-x64": {
|
||||||
"version": "2.41.1",
|
"version": "2.42.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.41.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.1.tgz",
|
||||||
"integrity": "sha512-ZqpYwHXAaK4MMEFlyaLYr6mJTmpy9qP6n30jGhLTW7kHKS3s6GPLCSlNmIfeClrInEt0963fM633ZRnXa04VPw==",
|
"integrity": "sha512-Rvc6Jy3kLZrcyO7Ysy1gj0iQi0nGVUN79VqC3OO9JDV44aDtKBDYuBkeFKE3gd1SL8EvPetKH85en2u2wdWxYg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -5621,9 +5621,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-win32-i686": {
|
"node_modules/@sentry/cli-win32-i686": {
|
||||||
"version": "2.41.1",
|
"version": "2.42.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.41.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.1.tgz",
|
||||||
"integrity": "sha512-AuRimCeVsx99DIOr9cwdYBHk39tlmAuPDdy2r16iNzY0InXs4xOys4gGzM7N4vlFQvFkzuc778Su0HkfasgprA==",
|
"integrity": "sha512-FC8FE6dk+G83PCO09Ux/9NJNouF5yXKhpzLV5BZkqQye39hV9GDrFTu+VWTnwI1P77fnaJkPEEKRkjwNiPGjLA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x86",
|
"x86",
|
||||||
"ia32"
|
"ia32"
|
||||||
@@ -5638,9 +5638,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-win32-x64": {
|
"node_modules/@sentry/cli-win32-x64": {
|
||||||
"version": "2.41.1",
|
"version": "2.42.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.41.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.1.tgz",
|
||||||
"integrity": "sha512-6JcPvXGye61+wPp0xdzfc2YLE/Dcud8JdaK8VxLM3b/8+Em7E+UyliDu3uF8+YGUqizY5JYTd3fs17DC8DZhLw==",
|
"integrity": "sha512-1595wD7JQSu5J9pA4m/B3WrjjIXltSV9VzuErehvanBvfusQ/YgBcvsNzgIf8aJsgSAYGbpR3Zqu81pjohdjgA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -6759,9 +6759,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/antd": {
|
"node_modules/antd": {
|
||||||
"version": "5.24.0",
|
"version": "5.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/antd/-/antd-5.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/antd/-/antd-5.24.1.tgz",
|
||||||
"integrity": "sha512-05PZBIf6ijLHAQskBTW3nwS2t7tQmyLA6Xq8vK2Sk5tsgCsH/UE1cNCDYnKFGRJ7cKYuWJ565JDo9LejbiO42A==",
|
"integrity": "sha512-RGwpXpSr2RtoUnrpJl3V6ZaTExwSXkFVxV24VUowwC04n6oA1sGyJrofQOKNqD623sVxL5UJBmf0a+BFBImP3Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/colors": "^7.2.0",
|
"@ant-design/colors": "^7.2.0",
|
||||||
@@ -6794,7 +6794,7 @@
|
|||||||
"rc-motion": "^2.9.5",
|
"rc-motion": "^2.9.5",
|
||||||
"rc-notification": "~5.6.3",
|
"rc-notification": "~5.6.3",
|
||||||
"rc-pagination": "~5.1.0",
|
"rc-pagination": "~5.1.0",
|
||||||
"rc-picker": "~4.11.0",
|
"rc-picker": "~4.11.1",
|
||||||
"rc-progress": "~4.0.0",
|
"rc-progress": "~4.0.0",
|
||||||
"rc-rate": "~2.13.1",
|
"rc-rate": "~2.13.1",
|
||||||
"rc-resize-observer": "^1.4.3",
|
"rc-resize-observer": "^1.4.3",
|
||||||
@@ -6803,7 +6803,7 @@
|
|||||||
"rc-slider": "~11.1.8",
|
"rc-slider": "~11.1.8",
|
||||||
"rc-steps": "~6.0.1",
|
"rc-steps": "~6.0.1",
|
||||||
"rc-switch": "~4.1.0",
|
"rc-switch": "~4.1.0",
|
||||||
"rc-table": "~7.50.2",
|
"rc-table": "~7.50.3",
|
||||||
"rc-tabs": "~15.5.1",
|
"rc-tabs": "~15.5.1",
|
||||||
"rc-textarea": "~1.9.0",
|
"rc-textarea": "~1.9.0",
|
||||||
"rc-tooltip": "~6.4.0",
|
"rc-tooltip": "~6.4.0",
|
||||||
@@ -11525,9 +11525,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/i18next-browser-languagedetector": {
|
"node_modules/i18next-browser-languagedetector": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.3.tgz",
|
||||||
"integrity": "sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==",
|
"integrity": "sha512-beOOLArattPBc2YZG5IXGJytdYFgUR7cS8Wd6HT4IczIoWKgmTspOQ2yasaGklelVo5seLPmnEKvLHR+E/MdWQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.23.2"
|
"@babel/runtime": "^7.23.2"
|
||||||
@@ -12723,9 +12723,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/libphonenumber-js": {
|
"node_modules/libphonenumber-js": {
|
||||||
"version": "1.11.19",
|
"version": "1.11.20",
|
||||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.19.tgz",
|
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.20.tgz",
|
||||||
"integrity": "sha512-bW/Yp/9dod6fmyR+XqSUL1N5JE7QRxQ3KrBIbYS1FTv32e5i3SEtQVX+71CYNv8maWNSOgnlCoNp9X78f/cKiA==",
|
"integrity": "sha512-/ipwAMvtSZRdiQBHqW1qxqeYiBMzncOQLVA+62MWYr7N4m7Q2jqpJ0WgT7zlOEOpyLRSqrMXidbJpC0J77AaKA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lie": {
|
"node_modules/lie": {
|
||||||
@@ -15485,9 +15485,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rc-table": {
|
"node_modules/rc-table": {
|
||||||
"version": "7.50.2",
|
"version": "7.50.3",
|
||||||
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.2.tgz",
|
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.3.tgz",
|
||||||
"integrity": "sha512-+nJbzxzstBriLb5sr9U7Vjs7+4dO8cWlouQbMwBVYghk2vr508bBdkHJeP/z9HVjAIKmAgMQKxmtbgDd3gc5wA==",
|
"integrity": "sha512-Z4/zNCzjv7f/XzPRecb+vJU0DJKdsYt4YRkDzNl4G05m7JmxrKGYC2KqN1Ew6jw2zJq7cxVv3z39qyZOHMuf7A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.10.1",
|
"@babel/runtime": "^7.10.1",
|
||||||
@@ -15761,9 +15761,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-error-overlay": {
|
"node_modules/react-error-overlay": {
|
||||||
"version": "6.0.11",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz",
|
||||||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
|
"integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -15816,9 +15816,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-icons": {
|
"node_modules/react-icons": {
|
||||||
"version": "5.4.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||||
"integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==",
|
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*"
|
"react": "*"
|
||||||
@@ -16798,9 +16798,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.84.0",
|
"version": "1.85.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.84.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz",
|
||||||
"integrity": "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==",
|
"integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
@@ -17861,9 +17861,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/swr": {
|
"node_modules/swr": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.2.tgz",
|
||||||
"integrity": "sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==",
|
"integrity": "sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dequal": "^2.0.3",
|
"dequal": "^2.0.3",
|
||||||
@@ -18849,9 +18849,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/userpilot": {
|
"node_modules/userpilot": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/userpilot/-/userpilot-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/userpilot/-/userpilot-1.3.7.tgz",
|
||||||
"integrity": "sha512-NK/4sQTnWrpER164PkWzLdLjUc2766B4yeLdLiFDoRfyLNAc3SecLWszZH6oPlv67B+XcYzqtmzEalE86bkljw==",
|
"integrity": "sha512-F6RiNcaYaHuWZyVLzyYBZQ4YAQzAgN4tWYyRkI0ZST/HSYwoVI6DGenNHEEqPoy28rZngVehXMyKKQ/1aRL7vQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ndhoule/includes": "^2.0.1",
|
"@ndhoule/includes": "^2.0.1",
|
||||||
@@ -18962,14 +18962,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.1.1.tgz",
|
||||||
"integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==",
|
"integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.24.2",
|
"esbuild": "^0.24.2",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.2",
|
||||||
"rollup": "^4.30.1"
|
"rollup": "^4.30.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -19194,9 +19194,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite/node_modules/postcss": {
|
"node_modules/vite/node_modules/postcss": {
|
||||||
"version": "8.5.1",
|
"version": "8.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||||
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
|
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,18 +8,18 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/pro-layout": "^7.22.2",
|
"@ant-design/pro-layout": "^7.22.3",
|
||||||
"@apollo/client": "^3.12.11",
|
"@apollo/client": "^3.13.1",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.0",
|
"@fingerprintjs/fingerprintjs": "^4.6.0",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.5.0",
|
"@reduxjs/toolkit": "^2.5.0",
|
||||||
"@sentry/cli": "^2.40.0",
|
"@sentry/cli": "^2.42.1",
|
||||||
"@sentry/react": "^7.114.0",
|
"@sentry/react": "^7.114.0",
|
||||||
"@splitsoftware/splitio-react": "^1.13.0",
|
"@splitsoftware/splitio-react": "^1.13.0",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"antd": "^5.24.0",
|
"antd": "^5.24.1",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^3.3.0",
|
"apollo-link-sentry": "^3.3.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
@@ -35,9 +35,9 @@
|
|||||||
"firebase": "^10.13.2",
|
"firebase": "^10.13.2",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.10.0",
|
||||||
"i18next": "^23.15.1",
|
"i18next": "^23.15.1",
|
||||||
"i18next-browser-languagedetector": "^8.0.2",
|
"i18next-browser-languagedetector": "^8.0.3",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.11.18",
|
"libphonenumber-js": "^1.11.20",
|
||||||
"logrocket": "^8.1.2",
|
"logrocket": "^8.1.2",
|
||||||
"markerjs2": "^2.32.3",
|
"markerjs2": "^2.32.3",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^14.1.3",
|
"react-i18next": "^14.1.3",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^9.0.3",
|
"react-markdown": "^9.0.3",
|
||||||
"react-number-format": "^5.4.3",
|
"react-number-format": "^5.4.3",
|
||||||
@@ -73,12 +73,12 @@
|
|||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.84.0",
|
"sass": "^1.85.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.15",
|
"styled-components": "^6.1.15",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"use-memo-one": "^1.1.3",
|
"use-memo-one": "^1.1.3",
|
||||||
"userpilot": "^1.3.6",
|
"userpilot": "^1.3.7",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
"web-vitals": "^3.5.2"
|
"web-vitals": "^3.5.2"
|
||||||
},
|
},
|
||||||
@@ -141,10 +141,10 @@
|
|||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"memfs": "^4.17.0",
|
"memfs": "^4.17.0",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"react-error-overlay": "6.0.11",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^6.1.0",
|
"vite": "^6.1.1",
|
||||||
"vite-plugin-babel": "^1.3.0",
|
"vite-plugin-babel": "^1.3.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"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",
|
"better-queue": "^3.8.12",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"bullmq": "^5.40.4",
|
"bullmq": "^5.41.3",
|
||||||
"chart.js": "^4.4.6",
|
"chart.js": "^4.4.8",
|
||||||
"cloudinary": "^2.5.1",
|
"cloudinary": "^2.5.1",
|
||||||
"compression": "^1.8.0",
|
"compression": "^1.8.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
@@ -4093,9 +4093,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bullmq": {
|
"node_modules/bullmq": {
|
||||||
"version": "5.40.4",
|
"version": "5.41.3",
|
||||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.40.4.tgz",
|
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.3.tgz",
|
||||||
"integrity": "sha512-MaIOhc31ZbVi9HbY0VAalsXoywelzEPNr6dojoKSMCXDnEVTQ27LkT5LA0Mlpr7ZunMLfpH94SLYrWNsPMsQrg==",
|
"integrity": "sha512-tWTeuO/BHDg6gKVnQJMjO42zkhsGss6s4bMdgJU24JVBT53yUvDjaO9H0L/BHKAtsMi4xlxkrDuMNSYWeHlekA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cron-parser": "^4.9.0",
|
"cron-parser": "^4.9.0",
|
||||||
@@ -4235,9 +4235,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chart.js": {
|
"node_modules/chart.js": {
|
||||||
"version": "4.4.7",
|
"version": "4.4.8",
|
||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz",
|
||||||
"integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==",
|
"integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kurkle/color": "^0.3.0"
|
"@kurkle/color": "^0.3.0"
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"bullmq": "^5.40.4",
|
"bullmq": "^5.41.3",
|
||||||
"chart.js": "^4.4.6",
|
"chart.js": "^4.4.8",
|
||||||
"cloudinary": "^2.5.1",
|
"cloudinary": "^2.5.1",
|
||||||
"compression": "^1.8.0",
|
"compression": "^1.8.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
|
|||||||
@@ -1,65 +1,87 @@
|
|||||||
/**
|
/**
|
||||||
* Parses an event by comparing old and new data to determine which fields have changed.
|
* 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 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 changedFields = {};
|
||||||
let changedFieldNames = [];
|
let changedFieldNames = [];
|
||||||
|
|
||||||
if (isNew) {
|
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(
|
changedFields = Object.fromEntries(
|
||||||
Object.entries(newData).map(([key, value]) => [key, { old: undefined, new: value }])
|
Object.entries(newData).map(([key, value]) => [key, { old: undefined, new: value }])
|
||||||
);
|
);
|
||||||
changedFieldNames = Object.keys(newData);
|
changedFieldNames = Object.keys(newData); // All keys are new
|
||||||
} else {
|
} else {
|
||||||
// Compare oldData with newData for changes
|
// Compare oldData and newData to detect updates
|
||||||
for (const key in newData) {
|
for (const key in newData) {
|
||||||
if (Object.prototype.hasOwnProperty.call(newData, key)) {
|
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 (
|
if (
|
||||||
!Object.prototype.hasOwnProperty.call(oldData, key) ||
|
!Object.prototype.hasOwnProperty.call(oldData, key) || // Field didn’t exist before
|
||||||
JSON.stringify(oldData[key]) !== JSON.stringify(newData[key])
|
JSON.stringify(oldData[key]) !== JSON.stringify(newData[key]) // Values differ (deep comparison)
|
||||||
) {
|
) {
|
||||||
changedFields[key] = {
|
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]
|
new: newData[key]
|
||||||
};
|
};
|
||||||
changedFieldNames.push(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) {
|
for (const key in oldData) {
|
||||||
if (Object.prototype.hasOwnProperty.call(oldData, key) && !Object.prototype.hasOwnProperty.call(newData, key)) {
|
if (Object.prototype.hasOwnProperty.call(oldData, key) && !Object.prototype.hasOwnProperty.call(newData, key)) {
|
||||||
changedFields[key] = {
|
changedFields[key] = {
|
||||||
old: oldData[key],
|
old: oldData[key],
|
||||||
new: null // Indicate field was removed
|
new: null // Mark as removed
|
||||||
};
|
};
|
||||||
changedFieldNames.push(key);
|
changedFieldNames.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract jobId based on jobIdField
|
// Extract jobId if jobIdField is provided
|
||||||
let jobId = null;
|
let jobId = null;
|
||||||
if (jobIdField) {
|
if (jobIdField) {
|
||||||
let keyName = jobIdField;
|
let keyName = jobIdField;
|
||||||
const prefix = "req.body.event.new.";
|
const prefix = "req.body.event.new.";
|
||||||
|
// Strip prefix if present to isolate the actual field name
|
||||||
if (keyName.startsWith(prefix)) {
|
if (keyName.startsWith(prefix)) {
|
||||||
keyName = keyName.slice(prefix.length);
|
keyName = keyName.slice(prefix.length);
|
||||||
}
|
}
|
||||||
|
// Look for jobId in newData first, then fallback to oldData if necessary
|
||||||
jobId = newData[keyName] || (oldData && oldData[keyName]) || null;
|
jobId = newData[keyName] || (oldData && oldData[keyName]) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
changedFieldNames,
|
changedFieldNames, // Array of fields that changed
|
||||||
changedFields,
|
changedFields, // Object with old/new values for changed fields
|
||||||
isNew,
|
isNew, // Boolean indicating if this is a new entry
|
||||||
data: newData,
|
data: newData, // Current data state
|
||||||
trigger,
|
trigger, // Event trigger (e.g., 'INSERT', 'UPDATE')
|
||||||
table,
|
table, // Associated table name
|
||||||
jobId
|
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 addQueue;
|
||||||
let consolidateQueue;
|
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 }) => {
|
const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||||
|
// Only initialize if queues don't already exist
|
||||||
if (!addQueue || !consolidateQueue) {
|
if (!addQueue || !consolidateQueue) {
|
||||||
logger.logger.info("Initializing Notifications Queues");
|
logger.logger.info("Initializing Notifications Queues");
|
||||||
|
|
||||||
|
// Create queue for adding notifications
|
||||||
addQueue = new Queue("notificationsAdd", {
|
addQueue = new Queue("notificationsAdd", {
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
prefix: "{BULLMQ}", // Namespace prefix for BullMQ in Redis
|
||||||
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
defaultJobOptions: { removeOnComplete: true, removeOnFail: true } // Cleanup jobs after success/failure
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create queue for consolidating notifications
|
||||||
consolidateQueue = new Queue("notificationsConsolidate", {
|
consolidateQueue = new Queue("notificationsConsolidate", {
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
prefix: "{BULLMQ}",
|
||||||
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Worker to process jobs from the addQueue
|
||||||
const addWorker = new Worker(
|
const addWorker = new Worker(
|
||||||
"notificationsAdd",
|
"notificationsAdd",
|
||||||
async (job) => {
|
async (job) => {
|
||||||
@@ -28,27 +42,32 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
const redisKeyPrefix = `app:notifications:${jobId}`;
|
const redisKeyPrefix = `app:notifications:${jobId}`;
|
||||||
const notification = { key, variables, timestamp: Date.now() };
|
const notification = { key, variables, timestamp: Date.now() };
|
||||||
|
|
||||||
|
// Store notification for each recipient in Redis
|
||||||
for (const recipient of recipients) {
|
for (const recipient of recipients) {
|
||||||
const { user } = recipient;
|
const { user } = recipient;
|
||||||
const userKey = `${redisKeyPrefix}:${user}`;
|
const userKey = `${redisKeyPrefix}:${user}`;
|
||||||
const existingNotifications = await pubClient.get(userKey);
|
const existingNotifications = await pubClient.get(userKey);
|
||||||
const notifications = existingNotifications ? JSON.parse(existingNotifications) : [];
|
const notifications = existingNotifications ? JSON.parse(existingNotifications) : [];
|
||||||
notifications.push(notification);
|
notifications.push(notification);
|
||||||
|
// Set with 40-second expiration to avoid stale data
|
||||||
await pubClient.set(userKey, JSON.stringify(notifications), "EX", 40);
|
await pubClient.set(userKey, JSON.stringify(notifications), "EX", 40);
|
||||||
logger.logger.debug(`Stored notification for ${user} under ${userKey}: ${JSON.stringify(notifications)}`);
|
logger.logger.debug(`Stored notification for ${user} under ${userKey}: ${JSON.stringify(notifications)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const consolidateKey = `app:consolidate:${jobId}`;
|
const consolidateKey = `app:consolidate:${jobId}`;
|
||||||
|
// setnx ensures only one consolidation job is scheduled (atomic operation)
|
||||||
const flagSet = await pubClient.setnx(consolidateKey, "pending");
|
const flagSet = await pubClient.setnx(consolidateKey, "pending");
|
||||||
logger.logger.debug(`Consolidation flag set for jobId ${jobId}: ${flagSet}`);
|
logger.logger.debug(`Consolidation flag set for jobId ${jobId}: ${flagSet}`);
|
||||||
|
|
||||||
if (flagSet) {
|
if (flagSet) {
|
||||||
|
// Schedule consolidation job to run after a 5-second delay
|
||||||
await consolidateQueue.add(
|
await consolidateQueue.add(
|
||||||
"consolidate-notifications",
|
"consolidate-notifications",
|
||||||
{ jobId, recipients },
|
{ jobId, recipients },
|
||||||
{ jobId: `consolidate:${jobId}`, delay: 5000 }
|
{ jobId: `consolidate:${jobId}`, delay: 5000 }
|
||||||
);
|
);
|
||||||
logger.logger.info(`Scheduled consolidation for jobId ${jobId}`);
|
logger.logger.info(`Scheduled consolidation for jobId ${jobId}`);
|
||||||
|
// Set expiration on flag to clean up after 5 minutes
|
||||||
await pubClient.expire(consolidateKey, 300);
|
await pubClient.expire(consolidateKey, 300);
|
||||||
} else {
|
} else {
|
||||||
logger.logger.debug(`Consolidation already scheduled for jobId ${jobId}`);
|
logger.logger.debug(`Consolidation already scheduled for jobId ${jobId}`);
|
||||||
@@ -57,10 +76,11 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
{
|
{
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
prefix: "{BULLMQ}",
|
||||||
concurrency: 5
|
concurrency: 5 // Process up to 5 jobs concurrently
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Worker to process jobs from the consolidateQueue
|
||||||
const consolidateWorker = new Worker(
|
const consolidateWorker = new Worker(
|
||||||
"notificationsConsolidate",
|
"notificationsConsolidate",
|
||||||
async (job) => {
|
async (job) => {
|
||||||
@@ -69,15 +89,18 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
|
|
||||||
const redisKeyPrefix = `app:notifications:${jobId}`;
|
const redisKeyPrefix = `app:notifications:${jobId}`;
|
||||||
const lockKey = `lock:consolidate:${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);
|
const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", 10);
|
||||||
logger.logger.debug(`Lock acquisition for jobId ${jobId}: ${lockAcquired}`);
|
logger.logger.debug(`Lock acquisition for jobId ${jobId}: ${lockAcquired}`);
|
||||||
|
|
||||||
if (lockAcquired) {
|
if (lockAcquired) {
|
||||||
try {
|
try {
|
||||||
const allNotifications = {};
|
const allNotifications = {};
|
||||||
|
// Get unique user IDs to avoid duplicate processing
|
||||||
const uniqueUsers = [...new Set(recipients.map((r) => r.user))];
|
const uniqueUsers = [...new Set(recipients.map((r) => r.user))];
|
||||||
logger.logger.debug(`Unique users for jobId ${jobId}: ${uniqueUsers}`);
|
logger.logger.debug(`Unique users for jobId ${jobId}: ${uniqueUsers}`);
|
||||||
|
|
||||||
|
// Retrieve and structure notifications by user and bodyShopId
|
||||||
for (const user of uniqueUsers) {
|
for (const user of uniqueUsers) {
|
||||||
const userKey = `${redisKeyPrefix}:${user}`;
|
const userKey = `${redisKeyPrefix}:${user}`;
|
||||||
const notifications = await pubClient.get(userKey);
|
const notifications = await pubClient.get(userKey);
|
||||||
@@ -90,7 +113,7 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
allNotifications[user] = allNotifications[user] || {};
|
allNotifications[user] = allNotifications[user] || {};
|
||||||
allNotifications[user][bodyShopId] = parsedNotifications;
|
allNotifications[user][bodyShopId] = parsedNotifications;
|
||||||
}
|
}
|
||||||
await pubClient.del(userKey);
|
await pubClient.del(userKey); // Clean up after retrieval
|
||||||
logger.logger.debug(`Deleted Redis key ${userKey}`);
|
logger.logger.debug(`Deleted Redis key ${userKey}`);
|
||||||
} else {
|
} else {
|
||||||
logger.logger.warn(`No notifications found for ${user} under ${userKey}`);
|
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)}`);
|
logger.logger.debug(`Consolidated notifications: ${JSON.stringify(allNotifications)}`);
|
||||||
|
|
||||||
|
// Emit notifications to users via Socket.io
|
||||||
for (const [user, bodyShopData] of Object.entries(allNotifications)) {
|
for (const [user, bodyShopData] of Object.entries(allNotifications)) {
|
||||||
const userMapping = await redisHelpers.getUserSocketMapping(user);
|
const userMapping = await redisHelpers.getUserSocketMapping(user);
|
||||||
logger.logger.debug(`User socket mapping for ${user}: ${JSON.stringify(userMapping)}`);
|
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) {
|
if (userMapping && userMapping[bodyShopId]?.socketIds) {
|
||||||
userMapping[bodyShopId].socketIds.forEach((socketId) => {
|
userMapping[bodyShopId].socketIds.forEach((socketId) => {
|
||||||
logger.logger.debug(
|
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", {
|
ioRedis.to(socketId).emit("notification", {
|
||||||
jobId,
|
jobId,
|
||||||
@@ -124,12 +152,13 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up consolidation flag after processing
|
||||||
await pubClient.del(`app:consolidate:${jobId}`);
|
await pubClient.del(`app:consolidate:${jobId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.logger.error(`Consolidation error for jobId ${jobId}: ${err.message}`, { error: 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 {
|
} finally {
|
||||||
await pubClient.del(lockKey);
|
await pubClient.del(lockKey); // Release lock regardless of success/failure
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.logger.info(`Skipped consolidation for jobId ${jobId} - lock held by another worker`);
|
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,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
prefix: "{BULLMQ}",
|
||||||
concurrency: 1,
|
concurrency: 1, // Single concurrency to avoid race conditions
|
||||||
limiter: { max: 1, duration: 5000 }
|
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`));
|
addWorker.on("completed", (job) => logger.logger.info(`Add job ${job.id} completed`));
|
||||||
consolidateWorker.on("completed", (job) => logger.logger.info(`Consolidate 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) =>
|
addWorker.on("failed", (job, err) =>
|
||||||
logger.logger.error(`Add job ${job.id} failed: ${err.message}`, { error: err })
|
logger.logger.error(`Add job ${job.id} failed: ${err.message}`, { error: err })
|
||||||
);
|
);
|
||||||
|
|
||||||
consolidateWorker.on("failed", (job, err) =>
|
consolidateWorker.on("failed", (job, err) =>
|
||||||
logger.logger.error(`Consolidate job ${job.id} failed: ${err.message}`, { error: err })
|
logger.logger.error(`Consolidate job ${job.id} failed: ${err.message}`, { error: err })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Graceful shutdown handler for workers
|
||||||
const shutdown = async () => {
|
const shutdown = async () => {
|
||||||
logger.logger.info("Closing app queue workers...");
|
logger.logger.info("Closing app queue workers...");
|
||||||
await Promise.all([addWorker.close(), consolidateWorker.close()]);
|
await Promise.all([addWorker.close(), consolidateWorker.close()]);
|
||||||
logger.logger.info("App queue workers closed");
|
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 = () => {
|
const getQueue = () => {
|
||||||
if (!addQueue) throw new Error("Add queue not initialized. Ensure loadAppQueue is called during bootstrap.");
|
if (!addQueue) throw new Error("Add queue not initialized. Ensure loadAppQueue is called during bootstrap.");
|
||||||
return addQueue;
|
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 dispatchAppsToQueue = async ({ appsToDispatch, logger }) => {
|
||||||
const appQueue = getQueue();
|
const appQueue = getQueue();
|
||||||
|
|
||||||
for (const app of appsToDispatch) {
|
for (const app of appsToDispatch) {
|
||||||
const { jobId, bodyShopId, key, variables, recipients } = app;
|
const { jobId, bodyShopId, key, variables, recipients } = app;
|
||||||
|
// Unique jobId with timestamp to avoid duplicates
|
||||||
await appQueue.add(
|
await appQueue.add(
|
||||||
"add-notification",
|
"add-notification",
|
||||||
{ jobId, bodyShopId, key, variables, recipients },
|
{ jobId, bodyShopId, key, variables, recipients },
|
||||||
|
|||||||
@@ -4,30 +4,44 @@ const { sendTaskEmail } = require("../../email/sendemail");
|
|||||||
let emailQueue;
|
let emailQueue;
|
||||||
let worker;
|
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) {
|
if (!emailQueue) {
|
||||||
logger.logger.info("Initializing Notifications Email Queue");
|
logger.logger.info("Initializing Notifications Email Queue");
|
||||||
|
|
||||||
|
// Create queue for email notifications
|
||||||
emailQueue = new Queue("notificationsEmails", {
|
emailQueue = new Queue("notificationsEmails", {
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
prefix: "{BULLMQ}", // Namespace prefix for BullMQ in Redis
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
attempts: 3,
|
attempts: 3, // Retry failed jobs up to 3 times
|
||||||
backoff: {
|
backoff: {
|
||||||
type: "exponential",
|
type: "exponential", // Exponential backoff strategy
|
||||||
delay: 1000
|
delay: 1000 // Initial delay of 1 second
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize the worker during queue setup
|
// Worker to process jobs from the emailQueue
|
||||||
worker = new Worker(
|
worker = new Worker(
|
||||||
"notificationsEmails",
|
"notificationsEmails",
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { subject, body, recipients } = job.data;
|
const { subject, body, recipient } = job.data;
|
||||||
logger.logger.debug(`Processing email job ${job.id} for ${recipients.length} recipients`);
|
logger.logger.debug(`Processing email job ${job.id} for recipient ${recipient}`);
|
||||||
|
|
||||||
|
// Send email to a single recipient
|
||||||
await sendTaskEmail({
|
await sendTaskEmail({
|
||||||
to: recipients.map((r) => r.user),
|
to: recipient, // Single email address
|
||||||
subject,
|
subject,
|
||||||
type: "text",
|
type: "text",
|
||||||
text: body
|
text: body
|
||||||
@@ -38,9 +52,9 @@ const loadEmailQueue = async ({ pubClient, logger, redisHelpers }) => {
|
|||||||
{
|
{
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
prefix: "{BULLMQ}",
|
||||||
concurrency: 2, // Reduced for multi-node setup; adjust based on load
|
concurrency: 2, // Process up to 2 jobs concurrently
|
||||||
limiter: {
|
limiter: {
|
||||||
max: 10, // Max 10 jobs per minute per worker
|
max: 10, // Maximum of 10 jobs per minute
|
||||||
duration: 60 * 1000 // 1 minute
|
duration: 60 * 1000 // 1 minute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +73,7 @@ const loadEmailQueue = async ({ pubClient, logger, redisHelpers }) => {
|
|||||||
logger.logger.error("Worker error:", { error: err });
|
logger.logger.error("Worker error:", { error: err });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Graceful shutdown handling
|
// Graceful shutdown handler for the worker
|
||||||
const shutdown = async () => {
|
const shutdown = async () => {
|
||||||
if (worker) {
|
if (worker) {
|
||||||
logger.logger.info("Closing email queue worker...");
|
logger.logger.info("Closing email queue worker...");
|
||||||
@@ -68,13 +82,19 @@ const loadEmailQueue = async ({ pubClient, logger, redisHelpers }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on("SIGTERM", shutdown);
|
process.on("SIGTERM", shutdown); // Handle termination signal
|
||||||
process.on("SIGINT", shutdown);
|
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 = () => {
|
const getQueue = () => {
|
||||||
if (!emailQueue) {
|
if (!emailQueue) {
|
||||||
throw new Error("Email queue not initialized. Ensure loadEmailQueue is called during bootstrap.");
|
throw new Error("Email queue not initialized. Ensure loadEmailQueue is called during bootstrap.");
|
||||||
@@ -82,17 +102,31 @@ const getQueue = () => {
|
|||||||
return emailQueue;
|
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 dispatchEmailsToQueue = async ({ emailsToDispatch, logger }) => {
|
||||||
const emailQueue = getQueue();
|
const emailQueue = getQueue();
|
||||||
|
|
||||||
for (const email of emailsToDispatch) {
|
for (const email of emailsToDispatch) {
|
||||||
const { subject, body, recipients } = email;
|
const { subject, body, recipients } = email;
|
||||||
await emailQueue.add("send-email", {
|
// Create an array of jobs, one per recipient
|
||||||
subject,
|
const jobs = recipients.map((recipient) => ({
|
||||||
body,
|
name: "send-email",
|
||||||
recipients
|
data: {
|
||||||
}); // Job options moved to defaultJobOptions in Queue
|
subject,
|
||||||
logger.logger.debug(`Added email to queue: ${subject} for ${recipients.length} recipients`);
|
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");
|
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) => {
|
const populateWatchers = (data, result) => {
|
||||||
data.scenarioWatchers.forEach((recipients) => {
|
data.scenarioWatchers.forEach((recipients) => {
|
||||||
const { user, app, fcm, email } = 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 });
|
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);
|
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 });
|
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 alternateTransportChangedBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -33,6 +48,12 @@ const alternateTransportChangedBuilder = (data) => {
|
|||||||
return result;
|
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 billPostedHandler = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -56,6 +77,12 @@ const billPostedHandler = (data) => {
|
|||||||
return result;
|
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 criticalPartsStatusChangedBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -80,7 +107,14 @@ const criticalPartsStatusChangedBuilder = (data) => {
|
|||||||
return result;
|
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) => {
|
const intakeDeliveryChecklistCompletedBuilder = (data) => {
|
||||||
|
// Determine checklist type based on which field was changed
|
||||||
const checklistType = data.changedFields.intakechecklist ? "intake" : "delivery";
|
const checklistType = data.changedFields.intakechecklist ? "intake" : "delivery";
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -105,6 +139,12 @@ const intakeDeliveryChecklistCompletedBuilder = (data) => {
|
|||||||
return result;
|
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 jobAssignedToMeBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -128,6 +168,12 @@ const jobAssignedToMeBuilder = (data) => {
|
|||||||
return result;
|
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 jobsAddedToProductionBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -149,7 +195,12 @@ const jobsAddedToProductionBuilder = (data) => {
|
|||||||
return result;
|
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 jobStatusChangeBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -164,7 +215,7 @@ const jobStatusChangeBuilder = (data) => {
|
|||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
subject: `The status of ${data?.jobRoNumber} (${data.bodyShopName}) has changed from ${data.changedFields.status.old} to ${data.data.status}`,
|
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: []
|
recipients: []
|
||||||
},
|
},
|
||||||
fcm: { recipients: [] }
|
fcm: { recipients: [] }
|
||||||
@@ -174,6 +225,12 @@ const jobStatusChangeBuilder = (data) => {
|
|||||||
return result;
|
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 newMediaAddedReassignedBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -195,7 +252,12 @@ const newMediaAddedReassignedBuilder = (data) => {
|
|||||||
return result;
|
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 newNoteAddedBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -219,6 +281,12 @@ const newNoteAddedBuilder = (data) => {
|
|||||||
return result;
|
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 newTimeTicketPostedBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -240,6 +308,12 @@ const newTimeTicketPostedBuilder = (data) => {
|
|||||||
return result;
|
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 partMarkedBackOrderedBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -264,6 +338,12 @@ const partMarkedBackOrderedBuilder = (data) => {
|
|||||||
return result;
|
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 paymentCollectedCompletedBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -287,6 +367,12 @@ const paymentCollectedCompletedBuilder = (data) => {
|
|||||||
return result;
|
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 scheduledDatesChangedBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -315,6 +401,12 @@ const scheduledDatesChangedBuilder = (data) => {
|
|||||||
return result;
|
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 supplementImportedBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
@@ -338,6 +430,12 @@ const supplementImportedBuilder = (data) => {
|
|||||||
return result;
|
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 tasksUpdatedCreatedBuilder = (data) => {
|
||||||
const result = {
|
const result = {
|
||||||
app: {
|
app: {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
* @module scenarioParser
|
* @module scenarioParser
|
||||||
* @description
|
* @description
|
||||||
* This module exports a function that parses an event and triggers notification scenarios based on the event data.
|
* 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");
|
const eventParser = require("./eventParser");
|
||||||
@@ -9,23 +12,28 @@ const { client: gqlClient } = require("../graphql-client/graphql-client");
|
|||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const { isEmpty, isFunction } = require("lodash");
|
const { isEmpty, isFunction } = require("lodash");
|
||||||
const { getMatchingScenarios } = require("./scenarioMapperr");
|
const { getMatchingScenarios } = require("./scenarioMapperr");
|
||||||
const consoleDir = require("../utils/consoleDir");
|
|
||||||
const { dispatchEmailsToQueue } = require("./queues/emailQueue");
|
const { dispatchEmailsToQueue } = require("./queues/emailQueue");
|
||||||
const { dispatchAppsToQueue } = require("./queues/appQueue");
|
const { dispatchAppsToQueue } = require("./queues/appQueue");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses an event and determines matching scenarios for notifications.
|
* Parses an event and determines matching scenarios for notifications.
|
||||||
* Queries job watchers and notification settings before triggering scenario builders.
|
* 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 scenarioParser = async (req, jobIdField) => {
|
||||||
const { event, trigger, table } = req.body;
|
const { event, trigger, table } = req.body;
|
||||||
const { logger } = req;
|
const { logger } = req;
|
||||||
|
|
||||||
|
// Validate that required fields are present in the request body
|
||||||
if (!event?.data || !trigger || !table) {
|
if (!event?.data || !trigger || !table) {
|
||||||
throw new Error("Missing required request fields: event data, trigger, or 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({
|
const eventData = await eventParser({
|
||||||
newData: event.data.new,
|
newData: event.data.new,
|
||||||
oldData: event.data.old,
|
oldData: event.data.old,
|
||||||
@@ -34,11 +42,12 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
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, {
|
const watcherData = await gqlClient.request(queries.GET_JOB_WATCHERS, {
|
||||||
jobid: eventData.jobId
|
jobid: eventData.jobId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Transform watcher data into a simplified format with email and employee details
|
||||||
const jobWatchers = watcherData?.job_watchers_aggregate?.nodes?.map((watcher) => ({
|
const jobWatchers = watcherData?.job_watchers_aggregate?.nodes?.map((watcher) => ({
|
||||||
email: watcher.user_email,
|
email: watcher.user_email,
|
||||||
firstName: watcher?.user?.employee?.first_name,
|
firstName: watcher?.user?.employee?.first_name,
|
||||||
@@ -46,21 +55,23 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
employeeId: watcher?.user?.employee?.id
|
employeeId: watcher?.user?.employee?.id
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Exit early if no job watchers are found for this job
|
||||||
if (isEmpty(jobWatchers)) {
|
if (isEmpty(jobWatchers)) {
|
||||||
return;
|
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 bodyShopId = watcherData?.job?.bodyshop?.id;
|
||||||
const bodyShopName = watcherData?.job?.bodyshop?.shopname;
|
const bodyShopName = watcherData?.job?.bodyshop?.shopname;
|
||||||
const jobRoNumber = watcherData?.job?.ro_number;
|
const jobRoNumber = watcherData?.job?.ro_number;
|
||||||
const jobClaimNumber = watcherData?.job?.clm_no;
|
const jobClaimNumber = watcherData?.job?.clm_no;
|
||||||
|
|
||||||
|
// Validate that body shop data exists, as it’s required for notifications
|
||||||
if (!bodyShopId || !bodyShopName) {
|
if (!bodyShopId || !bodyShopName) {
|
||||||
throw new Error("No bodyshop data found for this job.");
|
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({
|
const matchingScenarios = getMatchingScenarios({
|
||||||
...eventData,
|
...eventData,
|
||||||
jobWatchers,
|
jobWatchers,
|
||||||
@@ -68,10 +79,12 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
bodyShopName
|
bodyShopName
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Exit early if no matching scenarios are identified
|
||||||
if (isEmpty(matchingScenarios)) {
|
if (isEmpty(matchingScenarios)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Combine event data with additional context for scenario processing
|
||||||
const finalScenarioData = {
|
const finalScenarioData = {
|
||||||
...eventData,
|
...eventData,
|
||||||
jobWatchers,
|
jobWatchers,
|
||||||
@@ -80,22 +93,24 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
matchingScenarios
|
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, {
|
const associationsData = await gqlClient.request(queries.GET_NOTIFICATION_ASSOCIATIONS, {
|
||||||
emails: jobWatchers.map((x) => x.email),
|
emails: jobWatchers.map((x) => x.email),
|
||||||
shopid: bodyShopId
|
shopid: bodyShopId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Exit early if no notification associations are found
|
||||||
if (isEmpty(associationsData?.associations)) {
|
if (isEmpty(associationsData?.associations)) {
|
||||||
return;
|
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) => ({
|
finalScenarioData.matchingScenarios = finalScenarioData.matchingScenarios.map((scenario) => ({
|
||||||
...scenario,
|
...scenario,
|
||||||
scenarioWatchers: associationsData.associations
|
scenarioWatchers: associationsData.associations
|
||||||
.filter((assoc) => {
|
.filter((assoc) => {
|
||||||
const settings = assoc.notification_settings && assoc.notification_settings[scenario.key];
|
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);
|
return settings && (settings.app || settings.email || settings.fcm);
|
||||||
})
|
})
|
||||||
.map((assoc) => {
|
.map((assoc) => {
|
||||||
@@ -103,6 +118,7 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
const watcherEmail = assoc.user || assoc.useremail;
|
const watcherEmail = assoc.user || assoc.useremail;
|
||||||
const matchingWatcher = jobWatchers.find((watcher) => watcher.email === watcherEmail);
|
const matchingWatcher = jobWatchers.find((watcher) => watcher.email === watcherEmail);
|
||||||
|
|
||||||
|
// Build watcher object with notification preferences and personal details
|
||||||
return {
|
return {
|
||||||
user: watcherEmail,
|
user: watcherEmail,
|
||||||
email: settings.email,
|
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)) {
|
if (isEmpty(finalScenarioData?.matchingScenarios)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 7: Trigger scenario builders for matching scenarios with eligible watchers.
|
// Step 7: Build and collect scenarios to dispatch notifications for
|
||||||
const scenariosToDispatch = [];
|
const scenariosToDispatch = [];
|
||||||
|
|
||||||
for (const scenario of finalScenarioData.matchingScenarios) {
|
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)) {
|
if (isEmpty(scenario.scenarioWatchers) || !isFunction(scenario.builder)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let eligibleWatchers = scenario.scenarioWatchers;
|
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) {
|
if (scenario.matchToUserFields && scenario.matchToUserFields.length > 0) {
|
||||||
eligibleWatchers = scenario.scenarioWatchers.filter((watcher) =>
|
eligibleWatchers = scenario.scenarioWatchers.filter((watcher) =>
|
||||||
scenario.matchToUserFields.some(
|
scenario.matchToUserFields.some(
|
||||||
@@ -138,14 +156,16 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip if no watchers remain after filtering
|
||||||
if (isEmpty(eligibleWatchers)) {
|
if (isEmpty(eligibleWatchers)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 8: Filter scenario fields to only include changed fields.
|
// Step 8: Filter scenario fields to include only those that changed
|
||||||
const filteredScenarioFields =
|
const filteredScenarioFields =
|
||||||
scenario.fields?.filter((field) => eventData.changedFieldNames.includes(field)) || [];
|
scenario.fields?.filter((field) => eventData.changedFieldNames.includes(field)) || [];
|
||||||
|
|
||||||
|
// Use the scenario’s builder to construct the notification data
|
||||||
scenariosToDispatch.push(
|
scenariosToDispatch.push(
|
||||||
scenario.builder({
|
scenario.builder({
|
||||||
trigger: finalScenarioData.trigger.name,
|
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)) {
|
if (isEmpty(scenariosToDispatch)) {
|
||||||
return;
|
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);
|
const emailsToDispatch = scenariosToDispatch.map((scenario) => scenario?.email);
|
||||||
if (!isEmpty(emailsToDispatch)) {
|
if (!isEmpty(emailsToDispatch)) {
|
||||||
dispatchEmailsToQueue({
|
dispatchEmailsToQueue({
|
||||||
emailsToDispatch,
|
emailsToDispatch,
|
||||||
logger
|
logger
|
||||||
}).catch((e) =>
|
}).catch((e) =>
|
||||||
|
// Log any errors encountered during email dispatching
|
||||||
logger.log("Something went wrong dispatching emails to the Email Notification Queue", "error", "queue", null, {
|
logger.log("Something went wrong dispatching emails to the Email Notification Queue", "error", "queue", null, {
|
||||||
message: e?.message
|
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);
|
const appsToDispatch = scenariosToDispatch.map((scenario) => scenario?.app);
|
||||||
if (!isEmpty(appsToDispatch)) {
|
if (!isEmpty(appsToDispatch)) {
|
||||||
dispatchAppsToQueue({
|
dispatchAppsToQueue({
|
||||||
appsToDispatch,
|
appsToDispatch,
|
||||||
logger
|
logger
|
||||||
}).catch((e) =>
|
}).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, {
|
logger.log("Something went wrong dispatching apps to the App Notification Queue", "error", "queue", null, {
|
||||||
message: e?.message
|
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) => {
|
const getJobAssignmentType = (data) => {
|
||||||
switch (data) {
|
switch (data) {
|
||||||
case "employee_pre":
|
case "employee_pre":
|
||||||
|
|||||||
Reference in New Issue
Block a user