diff --git a/.vscode/launch.json b/.vscode/launch.json index 8975652643..6a14f43c43 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -77,7 +77,7 @@ "ACCOUNT_PORT": "3000", "FRONT_URL": "http://localhost:8080", "outputCapture": "std", - "SES_URL": "http://localhost:8091", + "SES_URL": "", "MINIO_ACCESS_KEY": "minioadmin", "MINIO_SECRET_KEY": "minioadmin", "MINIO_ENDPOINT": "localhost" diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index c6a5517e73..d9bfcfe71a 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1377,8 +1377,8 @@ dependencies: specifier: ^1.20.2 version: 1.20.2 browserslist: - specifier: 4.21.5 - version: 4.21.5 + specifier: ^4.23.3 + version: 4.23.3 bson: specifier: ^6.8.0 version: 6.8.0 @@ -1815,8 +1815,8 @@ dependencies: specifier: ^5.3.3 version: 5.3.3 update-browserslist-db: - specifier: ~1.0.11 - version: 1.0.13(browserslist@4.21.5) + specifier: ^1.1.0 + version: 1.1.0(browserslist@4.23.3) url-loader: specifier: ~4.1.1 version: 4.1.1(file-loader@6.2.0)(webpack@5.90.3) @@ -2657,7 +2657,7 @@ packages: dependencies: '@babel/compat-data': 7.23.5 '@babel/helper-validator-option': 7.23.5 - browserslist: 4.23.0 + browserslist: 4.23.3 lru-cache: 5.1.1 semver: 6.3.1 dev: false @@ -10805,7 +10805,7 @@ packages: peerDependencies: postcss: ^8.1.0 dependencies: - browserslist: 4.23.0 + browserslist: 4.23.3 caniuse-lite: 1.0.30001589 fraction.js: 4.3.7 normalize-range: 0.1.2 @@ -11196,26 +11196,15 @@ packages: pako: 0.2.9 dev: false - /browserslist@4.21.5: - resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} + /browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001589 - electron-to-chromium: 1.4.679 - node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.21.5) - dev: false - - /browserslist@4.23.0: - resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001589 - electron-to-chromium: 1.4.679 - node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.23.0) + caniuse-lite: 1.0.30001651 + electron-to-chromium: 1.5.11 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) dev: false /bs-logger@0.2.6: @@ -11468,6 +11457,10 @@ packages: resolution: {integrity: sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==} dev: false + /caniuse-lite@1.0.30001651: + resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} + dev: false + /case-anything@2.1.13: resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} engines: {node: '>=12.13'} @@ -12019,7 +12012,7 @@ packages: /core-js-compat@3.36.0: resolution: {integrity: sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==} dependencies: - browserslist: 4.23.0 + browserslist: 4.23.3 dev: false /core-js@3.36.0: @@ -13121,8 +13114,8 @@ packages: type-fest: 2.19.0 dev: false - /electron-to-chromium@1.4.679: - resolution: {integrity: sha512-NhQMsz5k0d6m9z3qAxnsOR/ebal4NAGsrNVRwcDo4Kc/zQ7KdsTKZUxZoygHcVRb0QDW3waEDIcE3isZ79RP6g==} + /electron-to-chromium@1.5.11: + resolution: {integrity: sha512-R1CccCDYqndR25CaXFd6hp/u9RaaMcftMkphmvuepXr5b1vfLkRml6aWVeBhXJ7rbevHkKEMJtz8XqPf7ffmew==} dev: false /electron-updater@6.2.1: @@ -18443,8 +18436,8 @@ packages: write-file-atomic: 1.3.4 dev: false - /node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + /node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} dev: false /nopt@1.0.10: @@ -19229,6 +19222,10 @@ packages: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: false + /picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + dev: false + /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -22785,26 +22782,15 @@ packages: path-exists: 5.0.0 dev: false - /update-browserslist-db@1.0.13(browserslist@4.21.5): - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + /update-browserslist-db@1.1.0(browserslist@4.23.3): + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.21.5 + browserslist: 4.23.3 escalade: 3.1.2 - picocolors: 1.0.0 - dev: false - - /update-browserslist-db@1.0.13(browserslist@4.23.0): - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - dependencies: - browserslist: 4.23.0 - escalade: 3.1.2 - picocolors: 1.0.0 + picocolors: 1.0.1 dev: false /uri-js@4.4.1: @@ -23282,7 +23268,7 @@ packages: '@webassemblyjs/wasm-parser': 1.11.6 acorn: 8.11.3 acorn-import-assertions: 1.9.0(acorn@8.11.3) - browserslist: 4.23.0 + browserslist: 4.23.3 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 es-module-lexer: 1.4.1 @@ -23323,7 +23309,7 @@ packages: '@webassemblyjs/wasm-parser': 1.11.6 acorn: 8.11.3 acorn-import-assertions: 1.9.0(acorn@8.11.3) - browserslist: 4.23.0 + browserslist: 4.23.3 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 es-module-lexer: 1.4.1 @@ -24298,7 +24284,7 @@ packages: dev: false file:projects/attachment-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): - resolution: {integrity: sha512-ZEkxtUt4tXWqiEQOGDSFHr65Zw9PtVeCQJ5zS8zdDdpF+vh1BAaLW7OydhYafKYw5RheG50HOK/R6wPDcNd3Lw==, tarball: file:projects/attachment-resources.tgz} + resolution: {integrity: sha512-yEPWonMOvXhIk5+IaNQVsyw6wnfqdcB/4UsEh8BLCQwhpepBeVrNSXgS+mLspQaL9g15E81PHwyrWyF1OcqQOQ==, tarball: file:projects/attachment-resources.tgz} id: file:projects/attachment-resources.tgz name: '@rush-temp/attachment-resources' version: 0.0.0 @@ -25127,7 +25113,7 @@ packages: dev: false file:projects/contact-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): - resolution: {integrity: sha512-iVkm89qU3Sx3zlhJRYkBsMy03G536YvuMT1tc6WTLMzklcpqYyCOtklXRWUGGiF/+a56Dog4hu7ai10QUIajrA==, tarball: file:projects/contact-resources.tgz} + resolution: {integrity: sha512-Vlw7yfM/X0ViZ1mzmcJFVPAZk/2hp4l768lxErLF/sA2HKsRUCAvkjbzfHmImsK5e2WycFWOWwnUJu9p1irQWQ==, tarball: file:projects/contact-resources.tgz} id: file:projects/contact-resources.tgz name: '@rush-temp/contact-resources' version: 0.0.0 @@ -25320,7 +25306,7 @@ packages: dev: false file:projects/core.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): - resolution: {integrity: sha512-BgJt/g8bufJyz3tFDrdM6Y3GHHTzKs7DUVMylyP2kyPCyrq3TrBDOGajz6QJ36kObLH/KWWau/LRmB3ZWIFU7Q==, tarball: file:projects/core.tgz} + resolution: {integrity: sha512-qorGEpUYJYJMlHr0WkKpS8sUfQt3GQtF7xCdUSU5K9B/aRgAL6z0LGRJoUmn/kYHHV+r6DITWhOiP6zIHcKmdg==, tarball: file:projects/core.tgz} id: file:projects/core.tgz name: '@rush-temp/core' version: 0.0.0 @@ -25515,7 +25501,7 @@ packages: dev: false file:projects/desktop.tgz(bufferutil@4.0.8)(sass@1.71.1)(utf-8-validate@6.0.4): - resolution: {integrity: sha512-j8onQxy51K29b/9q8g+lOG8e8dOFO2WpQQTdtnhP2vy2EcvIuSgckenQpg++0r1AtJ1G7FC7hr5QYNsrTPWkBA==, tarball: file:projects/desktop.tgz} + resolution: {integrity: sha512-+dbbvdgHJD76pLjOUSctj1tb9kC3hWa4CisFJxvSmlObAUII4NI+4EmE4u1x8OWaGEDqRCPCmrGFk1aSiPF5fQ==, tarball: file:projects/desktop.tgz} id: file:projects/desktop.tgz name: '@rush-temp/desktop' version: 0.0.0 @@ -25525,7 +25511,7 @@ packages: '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) '@vercel/webpack-asset-relocator-loader': 1.7.4 autoprefixer: 10.4.17(postcss@8.4.35) - browserslist: 4.21.5 + browserslist: 4.23.3 commander: 8.3.0 compression-webpack-plugin: 10.0.0(webpack@5.90.3) copy-webpack-plugin: 11.0.0(webpack@5.90.3) @@ -25567,7 +25553,7 @@ packages: ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3) typescript: 5.3.3 - update-browserslist-db: 1.0.13(browserslist@4.21.5) + update-browserslist-db: 1.1.0(browserslist@4.23.3) webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4) webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.90.3) @@ -25812,7 +25798,7 @@ packages: dev: false file:projects/document-resources.tgz(@tiptap/pm@2.2.4)(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): - resolution: {integrity: sha512-paT/sxyRFUxsWTwzgk98+gGZZkT1a5EyFzSKE4UPyW9sqjM4IiyyIIrr57YZEpILDApfg51PF3qhjEtWV9kf1Q==, tarball: file:projects/document-resources.tgz} + resolution: {integrity: sha512-YK9gh+kYmMkJHyLvlo94mEr3QznTo4V7TMypOcnHxg4ZKTfRl1mrDNI/dSAW566wmbmm7Eln0WRboX30yRxPTA==, tarball: file:projects/document-resources.tgz} id: file:projects/document-resources.tgz name: '@rush-temp/document-resources' version: 0.0.0 @@ -26438,7 +26424,7 @@ packages: dev: false file:projects/hr-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): - resolution: {integrity: sha512-85fy55EMpfKKMiNAp0tW70KLnbppfEp9cDx93jYlteFoPpsi05GRZ2ZNYM9rLWaZ1wMYF1vsX42M0opDIX6KOQ==, tarball: file:projects/hr-resources.tgz} + resolution: {integrity: sha512-O0jhwaTcNjWwviO2wUzfwpH7AroMYBCAbgsRsLwBsDdW08fnp1LqKswgViDL/ZOsQ5ZvboCwRAOaR8VYVfojZg==, tarball: file:projects/hr-resources.tgz} id: file:projects/hr-resources.tgz name: '@rush-temp/hr-resources' version: 0.0.0 @@ -26776,7 +26762,7 @@ packages: dev: false file:projects/lead-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): - resolution: {integrity: sha512-DIL0rCz2r3nKYgE2fOrEdVSwLGq1Zz2MjsU1HvDxnIs4IE1Q4s8oNMBtvtunERIQ2fNnJHcE0XXijawtYilrjQ==, tarball: file:projects/lead-resources.tgz} + resolution: {integrity: sha512-TJVh5S1o+GvRWeeNWwXveGOpMtsQTR0n5RMGjK3kXsuAUYCQRPzgMh3noJxe2n4vvijK8IUNjAR9+AjeSPo5kw==, tarball: file:projects/lead-resources.tgz} id: file:projects/lead-resources.tgz name: '@rush-temp/lead-resources' version: 0.0.0 @@ -27182,7 +27168,7 @@ packages: dev: false file:projects/model-analytics-collector.tgz: - resolution: {integrity: sha512-OcczFEesVXSXUuJ/HpZ880slAZeV8Y/MhZArlxBlO90Tm3jO42cyQLAi0kO36ZAkO/gGVpAHUpuZT0QB70ovvw==, tarball: file:projects/model-analytics-collector.tgz} + resolution: {integrity: sha512-ht/R4aa2RvdT8odpbznVfFTed3dJGToS1q5tKHqJIF8BQhjNcVh9ZUmAJNnNZNTeD8J6yuKIPq7n/Io2CuTYdg==, tarball: file:projects/model-analytics-collector.tgz} name: '@rush-temp/model-analytics-collector' version: 0.0.0 dependencies: @@ -27391,7 +27377,7 @@ packages: dev: false file:projects/model-drive.tgz: - resolution: {integrity: sha512-V1Zaxs9HItgN9Zp8M2rdPjTUS1ZSkNaDUCeRcW5xuOdD0YY8xijRwZ+d2HF5G6xYgRwTpfqIw0O1cUBmqJAY8A==, tarball: file:projects/model-drive.tgz} + resolution: {integrity: sha512-JG62skvnh8n7bBwsriHH6TE71W/gVm287OVnmy4VRJVf/tDC0/sCKlg+75qvcSu3CmAjaYqnnJz5q1Lz9EH3xw==, tarball: file:projects/model-drive.tgz} name: '@rush-temp/model-drive' version: 0.0.0 dependencies: @@ -28389,7 +28375,7 @@ packages: dev: false file:projects/model-telegram.tgz: - resolution: {integrity: sha512-WjCLxLQvcXBwLNQQnCbClBIum033r+fwS3nwB7RmCTgERzxT7lqedPlCwsd1TUs7PF6C0Y6cp5O/Lrnn+7Z46g==, tarball: file:projects/model-telegram.tgz} + resolution: {integrity: sha512-uXXH0LQVtX+rJ0+XLPft4NLphBmtXs9ZY+g16hC9x7j5JToKwsYsBHR/rktANxYU3CyAHeoqA7S7CiYbGLrDVw==, tarball: file:projects/model-telegram.tgz} name: '@rush-temp/model-telegram' version: 0.0.0 dependencies: @@ -28674,7 +28660,7 @@ packages: dev: false file:projects/notification-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): - resolution: {integrity: sha512-D5FLFfyRUTqKy52JGk4IMGhROrTRq+YZ+34+y2awY2U7JBUbpjcxhMkFBMgr/9TrZhSTvZrw7VvL5FCruoEN4Q==, tarball: file:projects/notification-resources.tgz} + resolution: {integrity: sha512-xpvJbpFBrKB3RPG8SgT9+cJj2IkysBNcl4jQAV69ACUvnTQMh4RS9P31zpaO7CObYXrm9/ImRF0fXbH+AkiJsg==, tarball: file:projects/notification-resources.tgz} id: file:projects/notification-resources.tgz name: '@rush-temp/notification-resources' version: 0.0.0 @@ -29099,7 +29085,7 @@ packages: dev: false file:projects/pod-analytics-collector.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4): - resolution: {integrity: sha512-Sx9mTOCsgDTcoC8h9VXeemVHerKLMDzdW4aJ08fdsF9UA35tLWmbDXw+fn3gPUNj2l+QT5ExEF9rV1Iez1FIbw==, tarball: file:projects/pod-analytics-collector.tgz} + resolution: {integrity: sha512-15hQZSiqnsgeZDg3n3AE2D2mx6xBToMB+2z+nDKLAKbaa+XAjMyj7J/Z9nLwfjs2EX6Z8x10JoPvs4au/SXxlA==, tarball: file:projects/pod-analytics-collector.tgz} id: file:projects/pod-analytics-collector.tgz name: '@rush-temp/pod-analytics-collector' version: 0.0.0 @@ -30043,7 +30029,7 @@ packages: dev: false file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.71.1)(ts-node@10.9.2)(utf-8-validate@6.0.4): - resolution: {integrity: sha512-U0i5q0YgmqM+29OoNKE5F7SYHLenVyjGDEus4MgN7CqMIC1n6n2gtztzRm2O6/j8hba5E6asgHgGF+ljESlbGw==, tarball: file:projects/prod.tgz} + resolution: {integrity: sha512-lYEzH4pmYid/iB0WNGOZXt3y9nyDbs70r3jYZJ6qIQEyvQVqWXnAd8Rw+nqcEbI/QtTou81FrvAK2Fnp5Imeww==, tarball: file:projects/prod.tgz} id: file:projects/prod.tgz name: '@rush-temp/prod' version: 0.0.0 @@ -30051,7 +30037,7 @@ packages: '@sentry/svelte': 7.101.1(svelte@4.2.12) '@types/node': 20.11.19 autoprefixer: 10.4.17(postcss@8.4.35) - browserslist: 4.21.5 + browserslist: 4.23.3 compression-webpack-plugin: 10.0.0(webpack@5.90.3) cross-env: 7.0.3 css-loader: 5.2.7(webpack@5.90.3) @@ -30074,7 +30060,7 @@ packages: svgo-loader: 3.0.3 ts-loader: 9.5.1(typescript@5.3.3)(webpack@5.90.3) typescript: 5.3.3 - update-browserslist-db: 1.0.13(browserslist@4.21.5) + update-browserslist-db: 1.1.0(browserslist@4.23.3) webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4) webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.90.3) @@ -31700,7 +31686,7 @@ packages: dev: false file:projects/server-gmail-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): - resolution: {integrity: sha512-jc5tLhz7YJs4Nw0dwPgOpZnZA+wMstl6Vt3BuL5BOE8iMTeUTZP1fKJXf6pGVeZAGEaYzayHM9Jmw+S45/l9MQ==, tarball: file:projects/server-gmail-resources.tgz} + resolution: {integrity: sha512-V9M3+rquV4MPbPtmwWPdekguzA5WKJRDvU7xtpYKQSJ7J7hkLPKNah4GtSjin0opg6lTgtoAJUQaYLKheeCCbA==, tarball: file:projects/server-gmail-resources.tgz} id: file:projects/server-gmail-resources.tgz name: '@rush-temp/server-gmail-resources' version: 0.0.0 @@ -33412,7 +33398,7 @@ packages: dev: false file:projects/tags-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): - resolution: {integrity: sha512-LuFrjyWlBTgRhkuiQvvLLtUh4kw9sRrUH7kZaJYjXeHh6J4PDfbrLl1cNLCTKXZiCP4X3GIeMz0eK0eUqzdeVg==, tarball: file:projects/tags-resources.tgz} + resolution: {integrity: sha512-5ldBcfo7wMt0BzAf8fA0FMc2ICyqweNug+4KPSisBcFJRvqUysw+x97qpqBosCl2BI2ro/MTnUEwYpN/3m5oVQ==, tarball: file:projects/tags-resources.tgz} id: file:projects/tags-resources.tgz name: '@rush-temp/tags-resources' version: 0.0.0 @@ -33520,7 +33506,7 @@ packages: dev: false file:projects/task-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): - resolution: {integrity: sha512-BXlz1rd2Pp3cTgW0USdUS2Ha+hDUYnwAvT9uzlZY1Nn0zOxt6rSC/Qb4e6N1n0gNxpI63ZZl+zyhTkR100yL+g==, tarball: file:projects/task-resources.tgz} + resolution: {integrity: sha512-GxlxQIm65Qvb6rIh4tqIgtNxEhom+Q8bxQeclK4J+elxv3/YoQdQrS6pKiX+9ueY/TSz+ulPbgpNfk6y1cgSUA==, tarball: file:projects/task-resources.tgz} id: file:projects/task-resources.tgz name: '@rush-temp/task-resources' version: 0.0.0 @@ -34127,7 +34113,7 @@ packages: dev: false file:projects/time-resources.tgz(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4)(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): - resolution: {integrity: sha512-YHDZu9vfplV3yHTWsp3czuSbb59RRHGIYpYvMZKVG7mnk52y121rzcxiu1W6j8vh69xFXgxMowO6TQEwTEFfKQ==, tarball: file:projects/time-resources.tgz} + resolution: {integrity: sha512-S0T2DEhL7CMoryh+gjH7KorQwrUvwu96LH2VUaSZTADN69oSN9XQZb05ab1zhn5MbwNoRr2aLnMDqRmhuxT1Ag==, tarball: file:projects/time-resources.tgz} id: file:projects/time-resources.tgz name: '@rush-temp/time-resources' version: 0.0.0 diff --git a/desktop/package.json b/desktop/package.json index b6aa591571..a3f4ecfa41 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -42,8 +42,8 @@ "compression-webpack-plugin": "^10.0.0", "html-webpack-plugin": "^5.5.0", "fork-ts-checker-webpack-plugin": "~7.3.0", - "update-browserslist-db": "~1.0.11", - "browserslist": "4.21.5", + "update-browserslist-db": "^1.1.0", + "browserslist": "^4.23.3", "typescript": "^5.3.3", "ts-node": "^10.8.0", "ts-node-dev": "^2.0.0", diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index c345920520..4e1b6a1f9c 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -62,8 +62,8 @@ services: - LAST_NAME_FIRST=true - ACCOUNTS_URL=http://localhost:3000 - BRANDING_PATH=/var/cfg/branding.json - - INIT_SCRIPT_URL=https://raw.githubusercontent.com/hcengineering/init/main/script.yaml - - INIT_WORKSPACE=onboarding + # - INIT_SCRIPT_URL=https://raw.githubusercontent.com/hcengineering/init/main/script.yaml + # - INIT_WORKSPACE=onboarding restart: unless-stopped collaborator: image: hardcoreeng/collaborator diff --git a/dev/prod/package.json b/dev/prod/package.json index 0273ec1c23..ee3cddfae8 100644 --- a/dev/prod/package.json +++ b/dev/prod/package.json @@ -43,8 +43,8 @@ "compression-webpack-plugin": "^10.0.0", "html-webpack-plugin": "^5.5.0", "fork-ts-checker-webpack-plugin": "~7.3.0", - "update-browserslist-db": "~1.0.11", - "browserslist": "4.21.5", + "update-browserslist-db": "^1.1.0", + "browserslist": "^4.23.3", "esbuild": "^0.20.0", "esbuild-loader": "^4.0.3", "typescript": "^5.3.3", diff --git a/models/contact/src/migration.ts b/models/contact/src/migration.ts index dc7989ea55..668b6f5d8b 100644 --- a/models/contact/src/migration.ts +++ b/models/contact/src/migration.ts @@ -37,8 +37,8 @@ async function createEmployeeEmail (client: TxOperations): Promise { ).filter((it) => it.provider === contact.channelProvider.Email) const channelsMap = new Map(channels.map((p) => [p.attachedTo, p])) for (const employee of employees) { - const acc = await client.findOne(contact.class.PersonAccount, { person: employee._id }) - if (acc === undefined) continue + const acc = client.getModel().getAccountByPersonId(employee._id) + if (acc.length === 0) continue const current = channelsMap.get(employee._id) if (current === undefined) { await client.addCollection( @@ -49,13 +49,13 @@ async function createEmployeeEmail (client: TxOperations): Promise { 'channels', { provider: contact.channelProvider.Email, - value: acc.email.trim() + value: acc[0].email.trim() }, undefined, employee.modifiedOn ) - } else if (current.value !== acc.email.trim()) { - await client.update(current, { value: acc.email.trim() }, false, current.modifiedOn) + } else if (current.value !== acc[0].email.trim()) { + await client.update(current, { value: acc[0].email.trim() }, false, current.modifiedOn) } } } diff --git a/models/server-activity/src/index.ts b/models/server-activity/src/index.ts index 9a88c922e1..3cc96079bc 100644 --- a/models/server-activity/src/index.ts +++ b/models/server-activity/src/index.ts @@ -19,6 +19,7 @@ import core from '@hcengineering/core/src/component' import serverActivity from '@hcengineering/server-activity' import serverNotification from '@hcengineering/server-notification' import activity from '@hcengineering/activity' +import notification from '@hcengineering/notification' export { activityServerOperation } from './migration' export { serverActivityId } from '@hcengineering/server-activity' @@ -43,6 +44,9 @@ export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverActivity.trigger.ActivityMessagesHandler, + txMatch: { + 'tx.objectClass': { $nin: [activity.class.ActivityMessage, notification.class.DocNotifyContext] } + }, isAsync: true }) @@ -52,6 +56,10 @@ export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverActivity.trigger.ReferenceTrigger, + txMatch: { + 'tx.objectClass': { $ne: activity.class.ActivityMessage }, + objectClass: { $nin: [notification.class.InboxNotification, notification.class.DocNotifyContext] } + }, isAsync: true }) } diff --git a/models/server-gmail/src/index.ts b/models/server-gmail/src/index.ts index 8218a09a36..3715b8f060 100644 --- a/models/server-gmail/src/index.ts +++ b/models/server-gmail/src/index.ts @@ -49,6 +49,6 @@ export function createModel (builder: Builder): void { }) builder.mixin(gmail.ids.EmailNotification, notification.class.NotificationType, serverNotification.mixin.TypeMatch, { - func: serverGmail.function.IsIncomingMessage + func: serverGmail.function.IsIncomingMessageTypeMatch }) } diff --git a/models/server-lead/src/index.ts b/models/server-lead/src/index.ts index fbd1b04588..443dc9e69b 100644 --- a/models/server-lead/src/index.ts +++ b/models/server-lead/src/index.ts @@ -39,7 +39,7 @@ export function createModel (builder: Builder): void { notification.class.NotificationType, serverNotification.mixin.TypeMatch, { - func: serverNotification.function.IsUserEmployeeInFieldValue + func: serverNotification.function.IsUserEmployeeInFieldValueTypeMatch } ) diff --git a/models/server-notification/src/index.ts b/models/server-notification/src/index.ts index d6e371b5a4..9fc5874823 100644 --- a/models/server-notification/src/index.ts +++ b/models/server-notification/src/index.ts @@ -16,21 +16,22 @@ import { type Builder, Mixin, Model } from '@hcengineering/model' -import core, { type Account, type Doc, type Ref, type Tx } from '@hcengineering/core' +import core, { type Ref } from '@hcengineering/core' import { TClass, TDoc } from '@hcengineering/model-core' import { TNotificationType } from '@hcengineering/model-notification' -import notification, { type NotificationProvider, type NotificationType } from '@hcengineering/notification' +import notification, { type NotificationProvider } from '@hcengineering/notification' import { type Resource } from '@hcengineering/platform' -import serverCore, { type TriggerControl } from '@hcengineering/server-core' +import serverCore from '@hcengineering/server-core' import serverNotification, { type HTMLPresenter, + type NotificationContentProvider, type NotificationPresenter, + type NotificationProviderFunc, + type NotificationProviderResources, type Presenter, type TextPresenter, type TypeMatch, - type NotificationContentProvider, - type NotificationProviderResources, - type NotificationProviderFunc + type TypeMatchFunc } from '@hcengineering/server-notification' export { serverNotificationId } from '@hcengineering/server-notification' @@ -52,9 +53,7 @@ export class TNotificationPresenter extends TClass implements NotificationPresen @Mixin(serverNotification.mixin.TypeMatch, notification.class.NotificationType) export class TTypeMatch extends TNotificationType implements TypeMatch { - func!: Resource< - (tx: Tx, doc: Doc, user: Ref, type: NotificationType, control: TriggerControl) => Promise - > + func!: TypeMatchFunc } @Model(serverNotification.class.NotificationProviderResources, core.class.Doc) diff --git a/models/server-recruit/src/index.ts b/models/server-recruit/src/index.ts index 1ffa4c9bd7..25abf15e3d 100644 --- a/models/server-recruit/src/index.ts +++ b/models/server-recruit/src/index.ts @@ -93,7 +93,7 @@ export function createModel (builder: Builder): void { notification.class.NotificationType, serverNotification.mixin.TypeMatch, { - func: serverNotification.function.IsUserEmployeeInFieldValue + func: serverNotification.function.IsUserEmployeeInFieldValueTypeMatch } ) } diff --git a/models/server-telegram/src/index.ts b/models/server-telegram/src/index.ts index 08f2899a90..07b58aa547 100644 --- a/models/server-telegram/src/index.ts +++ b/models/server-telegram/src/index.ts @@ -56,7 +56,7 @@ export function createModel (builder: Builder): void { notification.class.NotificationType, serverNotification.mixin.TypeMatch, { - func: serverTelegram.function.IsIncomingMessage + func: serverTelegram.function.IsIncomingMessageTypeMatch } ) diff --git a/models/server-tracker/src/index.ts b/models/server-tracker/src/index.ts index 8796d1f85e..784d6fda16 100644 --- a/models/server-tracker/src/index.ts +++ b/models/server-tracker/src/index.ts @@ -77,7 +77,7 @@ export function createModel (builder: Builder): void { notification.class.NotificationType, serverNotification.mixin.TypeMatch, { - func: serverNotification.function.IsUserEmployeeInFieldValue + func: serverNotification.function.IsUserEmployeeInFieldValueTypeMatch } ) } diff --git a/packages/core/src/__tests__/minmodel.ts b/packages/core/src/__tests__/minmodel.ts index 319cec20d5..9b256fe029 100644 --- a/packages/core/src/__tests__/minmodel.ts +++ b/packages/core/src/__tests__/minmodel.ts @@ -187,7 +187,7 @@ export function genMinModel (): TxCUD[] { txes.push( createClass(core.class.Blob, { label: 'Blob' as IntlString, - extends: core.class.Blob, + extends: core.class.Doc, kind: ClassifierKind.CLASS }) ) diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index 27e8c0c912..b8e42732c0 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -440,6 +440,8 @@ export interface Permission extends Doc { export interface Account extends Doc { email: string role: AccountRole + + person?: Ref } /** diff --git a/packages/core/src/hierarchy.ts b/packages/core/src/hierarchy.ts index 61e4dfed60..cf83ff9edd 100644 --- a/packages/core/src/hierarchy.ts +++ b/packages/core/src/hierarchy.ts @@ -30,7 +30,7 @@ export class Hierarchy { private readonly attributes = new Map, Map>() private readonly attributesById = new Map, AnyAttribute>() private readonly descendants = new Map, Ref[]>() - private readonly ancestors = new Map, Ref[]>() + private readonly ancestors = new Map, { ordered: Ref[], set: Set> }>() private readonly proxies = new Map>, ProxyHandler>() private readonly classifierProperties = new Map, Record>() @@ -166,7 +166,7 @@ export class Hierarchy { if (result === undefined) { throw new Error('ancestors not found: ' + _class) } - return result + return result.ordered } getClass(_class: Ref>): Class { @@ -301,17 +301,7 @@ export class Hierarchy { * It will iterate over parents. */ isDerived(_class: Ref>, from: Ref>): boolean { - let cl: Ref> | undefined = _class - while (cl !== undefined) { - if (cl === from) return true - - const cll = this.classifiers.get(cl) - if (cll === undefined || this.isInterface(cll)) { - return false - } - cl = (cll as Class).extends - } - return false + return this.ancestors.get(_class)?.set?.has(from) ?? false } /** @@ -398,15 +388,19 @@ export class Hierarchy { const list = this.ancestors.get(_class) if (list === undefined) { if (add) { - this.ancestors.set(_class, [classifier]) + this.ancestors.set(_class, { ordered: [classifier], set: new Set([classifier]) }) } } else { if (add) { - addIf(list, classifier) + if (!list.set.has(classifier)) { + list.ordered.push(classifier) + list.set.add(classifier) + } } else { - const pos = list.indexOf(classifier) + const pos = list.ordered.indexOf(classifier) if (pos !== -1) { - list.splice(pos, 1) + list.ordered.splice(pos, 1) + list.set.delete(classifier) } } } diff --git a/packages/core/src/memdb.ts b/packages/core/src/memdb.ts index 42cbfb9c87..bead5d88b0 100644 --- a/packages/core/src/memdb.ts +++ b/packages/core/src/memdb.ts @@ -15,7 +15,7 @@ import { PlatformError, Severity, Status } from '@hcengineering/platform' import { Lookup, MeasureContext, ReverseLookups, getObjectValue } from '.' -import type { AttachedDoc, Class, Doc, Ref } from './classes' +import type { Account, AttachedDoc, Class, Doc, Ref } from './classes' import core from './component' import { Hierarchy } from './hierarchy' import { checkMixinKey, matchQuery, resultSort } from './query' @@ -31,6 +31,8 @@ export abstract class MemDb extends TxProcessor implements Storage { private readonly objectsByClass = new Map>, Map, Doc>>() private readonly objectById = new Map, Doc>() + private readonly accountByPersonId = new Map, Account[]>() + constructor (protected readonly hierarchy: Hierarchy) { super() } @@ -75,6 +77,10 @@ export abstract class MemDb extends TxProcessor implements Storage { return doc as T } + getAccountByPersonId (ref: Ref): Account[] { + return this.accountByPersonId.get(ref) ?? [] + } + findObject(_id: Ref): T | undefined { const doc = this.objectById.get(_id) return doc as T @@ -214,6 +220,12 @@ export abstract class MemDb extends TxProcessor implements Storage { const arr = this.getObjectsByClass(_class) arr.set(doc._id, doc) }) + if (this.hierarchy.isDerived(doc._class, core.class.Account)) { + const account = doc as Account + if (account.person !== undefined) { + this.accountByPersonId.set(account.person, [...(this.accountByPersonId.get(account.person) ?? []), account]) + } + } this.objectById.set(doc._id, doc) } @@ -226,6 +238,37 @@ export abstract class MemDb extends TxProcessor implements Storage { this.hierarchy.getAncestors(doc._class).forEach((_class) => { this.cleanObjectByClass(_class, _id) }) + if (this.hierarchy.isDerived(doc._class, core.class.Account)) { + const account = doc as Account + if (account.person !== undefined) { + const acc = this.accountByPersonId.get(account.person) ?? [] + this.accountByPersonId.set( + account.person, + acc.filter((it) => it._id !== _id) + ) + } + } + } + + updateDoc (_id: Ref, doc: Doc, update: TxUpdateDoc | TxMixin): void { + if ( + this.hierarchy.isDerived(doc._class, core.class.Account) && + update._class === core.class.TxUpdateDoc && + (update as TxUpdateDoc).operations.person !== undefined + ) { + const account = doc as Account + if (account.person !== undefined) { + const acc = this.accountByPersonId.get(account.person) ?? [] + this.accountByPersonId.set( + account.person, + acc.filter((it) => it._id !== _id) + ) + } + const newPerson = (update as TxUpdateDoc).operations.person + if (newPerson !== undefined) { + this.accountByPersonId.set(newPerson, [...(this.accountByPersonId.get(newPerson) ?? []), account]) + } + } } } @@ -297,6 +340,7 @@ export class ModelDb extends MemDb { const cud = tx as TxUpdateDoc const doc = this.findObject(cud.objectId) if (doc !== undefined) { + this.updateDoc(cud.objectId, doc, cud) TxProcessor.updateDoc2Doc(doc, cud) } else { ctx.error('no document found, failed to apply model transaction, skipping', { @@ -320,9 +364,10 @@ export class ModelDb extends MemDb { break case core.class.TxMixin: { const mix = tx as TxMixin - const obj = this.findObject(mix.objectId) - if (obj !== undefined) { - TxProcessor.updateMixin4Doc(obj, mix) + const doc = this.findObject(mix.objectId) + if (doc !== undefined) { + this.updateDoc(mix.objectId, doc, mix) + TxProcessor.updateMixin4Doc(doc, mix) } else { ctx.error('no document found, failed to apply model transaction, skipping', { _id: tx._id, @@ -339,6 +384,7 @@ export class ModelDb extends MemDb { protected async txUpdateDoc (tx: TxUpdateDoc): Promise { try { const doc = this.getObject(tx.objectId) as any + this.updateDoc(tx.objectId, doc, tx) TxProcessor.updateDoc2Doc(doc, tx) return tx.retrieve === true ? { object: doc } : {} } catch (err: any) {} @@ -354,8 +400,9 @@ export class ModelDb extends MemDb { // TODO: process ancessor mixins protected async txMixin (tx: TxMixin): Promise { - const obj = this.getObject(tx.objectId) as any - TxProcessor.updateMixin4Doc(obj, tx) + const doc = this.getObject(tx.objectId) as any + this.updateDoc(tx.objectId, doc, tx) + TxProcessor.updateMixin4Doc(doc, tx) return {} } } diff --git a/packages/text/src/markup/utils.ts b/packages/text/src/markup/utils.ts index fb90d069cb..8ba4813746 100644 --- a/packages/text/src/markup/utils.ts +++ b/packages/text/src/markup/utils.ts @@ -18,10 +18,10 @@ import { Editor, Extensions, getSchema } from '@tiptap/core' import { generateHTML, generateJSON } from '@tiptap/html' import { Node as ProseMirrorNode, Schema } from '@tiptap/pm/model' -import { defaultExtensions } from '../extensions' -import { MarkupMark, MarkupNode, MarkupNodeType, emptyMarkupNode } from './model' -import { nodeDoc, nodeParagraph, nodeText } from './dsl' import { deepEqual } from 'fast-equals' +import { defaultExtensions } from '../extensions' +import { nodeDoc, nodeParagraph, nodeText } from './dsl' +import { MarkupMark, MarkupNode, MarkupNodeType, emptyMarkupNode } from './model' /** @public */ export const EmptyMarkup: Markup = jsonToMarkup(emptyMarkupNode()) @@ -226,9 +226,11 @@ export function pmNodeToHTML (node: ProseMirrorNode, extensions?: Extensions): s const ELLIPSIS_CHAR = '…' const WHITESPACE = ' ' +const defaultSchema = getSchema(defaultExtensions) + /** @public */ export function stripTags (markup: Markup, textLimit = 0, extensions: Extensions | undefined = undefined): string { - const schema = getSchema(extensions ?? defaultExtensions) + const schema = extensions === undefined ? defaultSchema : getSchema(extensions) const parsed = markupToPmNode(markup, schema) const textParts: string[] = [] diff --git a/packages/text/src/ydoc.ts b/packages/text/src/ydoc.ts index ec50666c92..12e80f2773 100644 --- a/packages/text/src/ydoc.ts +++ b/packages/text/src/ydoc.ts @@ -56,13 +56,15 @@ export function yDocContentToNode ( return yDocToNode(ydoc, field, schema, extensions) } +const defaultSchema = getSchema(defaultExtensions) + /** * Get ProseMirror node from Y.Doc * * @public */ export function yDocToNode (ydoc: YDoc, field?: string, schema?: Schema, extensions?: Extensions): Node { - schema ??= getSchema(extensions ?? defaultExtensions) + schema ??= extensions === undefined ? defaultSchema : getSchema(extensions ?? defaultExtensions) try { const body = yDocToProsemirrorJSON(ydoc, field) @@ -79,7 +81,7 @@ export function yDocToNode (ydoc: YDoc, field?: string, schema?: Schema, extensi * @public */ export function yDocContentToNodes (content: ArrayBuffer, schema?: Schema, extensions?: Extensions): Node[] { - schema ??= getSchema(extensions ?? defaultExtensions) + schema ??= extensions === undefined ? defaultSchema : getSchema(extensions ?? defaultExtensions) const nodes: Node[] = [] @@ -112,7 +114,7 @@ export function updateYDocContent ( schema?: Schema, extensions?: Extensions ): YDoc | undefined { - schema ??= getSchema(extensions ?? defaultExtensions) + schema ??= extensions === undefined ? defaultSchema : getSchema(extensions ?? defaultExtensions) try { const ydoc = new YDoc() @@ -140,7 +142,7 @@ export function updateYDocContent ( * @public */ export function YDocFromContent (content: MarkupNode, field: string, schema?: Schema, extensions?: Extensions): YDoc { - schema ??= getSchema(extensions ?? defaultExtensions) + schema ??= extensions === undefined ? defaultSchema : getSchema(extensions ?? defaultExtensions) const res = new YDoc({ gc: false }) diff --git a/plugins/contact-resources/src/index.ts b/plugins/contact-resources/src/index.ts index cbbe385e37..5589c90807 100644 --- a/plugins/contact-resources/src/index.ts +++ b/plugins/contact-resources/src/index.ts @@ -268,8 +268,8 @@ async function kickEmployee (doc: Person): Promise { const client = getClient() const employee = client.getHierarchy().as(doc, contact.mixin.Employee) - const email = await client.findOne(contact.class.PersonAccount, { person: doc._id }) - if (email === undefined) { + const accounts = client.getModel().getAccountByPersonId(doc._id) + if (accounts.length === 0) { await client.update(employee, { active: false }) } else { showPopup( @@ -282,9 +282,12 @@ async function kickEmployee (doc: Person): Promise { (res?: boolean) => { if (res === true) { // eslint-disable-next-line @typescript-eslint/no-floating-promises - getResource(login.function.LeaveWorkspace).then(async (f) => { - await f(email.email) - }) + const p = getResource(login.function.LeaveWorkspace) + for (const i of accounts) { + void p.then(async (f) => { + await f(i.email) + }) + } } } ) diff --git a/plugins/contact-resources/src/utils.ts b/plugins/contact-resources/src/utils.ts index ec788c0f2b..096de69a57 100644 --- a/plugins/contact-resources/src/utils.ts +++ b/plugins/contact-resources/src/utils.ts @@ -320,7 +320,7 @@ function fillStores (): void { const accountPersonQuery = createQuery(true) const query = createQuery(true) - query.query(contact.mixin.Employee, { [contact.mixin.Employee + '.active']: { $in: [true, false] } }, (res) => { + query.query(contact.mixin.Employee, { active: { $in: [true, false] } }, (res) => { employeesStore.set(res) employeeByIdStore.set(toIdMap(res)) }) diff --git a/server-plugins/activity-resources/src/references.ts b/server-plugins/activity-resources/src/references.ts index 407141e9c8..781b515d8f 100644 --- a/server-plugins/activity-resources/src/references.ts +++ b/server-plugins/activity-resources/src/references.ts @@ -13,7 +13,9 @@ // limitations under the License. // +import activity, { ActivityMessage, ActivityReference, UserMentionInfo } from '@hcengineering/activity' import { loadCollaborativeDoc, yDocToBuffer } from '@hcengineering/collaboration' +import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact' import core, { Account, AttachedDoc, @@ -34,41 +36,30 @@ import core, { TxProcessor, TxRemoveDoc, TxUpdateDoc, - Type + Type, + type MeasureContext } from '@hcengineering/core' import notification, { CommonInboxNotification, MentionInboxNotification } from '@hcengineering/notification' +import { StorageAdapter, TriggerControl } from '@hcengineering/server-core' import { + applyNotificationProviders, + getCommonNotificationTxes, + getNotificationContent, + getNotificationProviderControl, + getPushCollaboratorTx, + isShouldNotifyTx, + NotifyResult, + shouldNotifyCommon, + toReceiverInfo, + type NotificationProviderControl +} from '@hcengineering/server-notification-resources' +import { + areEqualJson, extractReferences, markupToPmNode, pmNodeToMarkup, - yDocContentToNodes, - areEqualJson + yDocContentToNodes } from '@hcengineering/text' -import { StorageAdapter, TriggerControl } from '@hcengineering/server-core' -import activity, { ActivityMessage, ActivityReference, UserMentionInfo } from '@hcengineering/activity' -import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact' -import { - getCommonNotificationTxes, - getPushCollaboratorTx, - shouldNotifyCommon, - isShouldNotifyTx, - NotifyResult, - applyNotificationProviders, - getNotificationContent, - toReceiverInfo -} from '@hcengineering/server-notification-resources' - -async function getPersonAccount (person: Ref, control: TriggerControl): Promise { - return ( - await control.modelDb.findAll( - contact.class.PersonAccount, - { - person - }, - { limit: 1 } - ) - )[0] -} export function isDocMentioned (doc: Ref, content: string | Buffer): boolean { const references = [] @@ -93,20 +84,22 @@ export function isDocMentioned (doc: Ref, content: string | Buffer): boolea } export async function getPersonNotificationTxes ( + ctx: MeasureContext, reference: Data, control: TriggerControl, senderId: Ref, space: Ref, - originTx: TxCUD + originTx: TxCUD, + notificationControl: NotificationProviderControl ): Promise { const receiverPersonId = reference.attachedTo as Ref - const receiver = await getPersonAccount(receiverPersonId, control) + const receiver = control.modelDb.getAccountByPersonId(receiverPersonId) as PersonAccount[] - if (receiver === undefined) { + if (receiver.length === 0) { return [] } - if (receiver._id === senderId) { + if (receiver.some((it) => it._id === senderId)) { return [] } @@ -120,14 +113,21 @@ export async function getPersonNotificationTxes ( const doc = (await control.findAll(reference.srcDocClass, { _id: reference.srcDocId }))[0] const receiverPerson = ( - await control.findAll(contact.mixin.Employee, { _id: receiver.person as Ref, active: true }, { limit: 1 }) + await control.findAll( + contact.mixin.Employee, + { _id: receiverPersonId as Ref, active: true }, + { limit: 1 } + ) )[0] if (receiverPerson === undefined) return res - const receiverSpace = (await control.findAll(contact.class.PersonSpace, { person: receiver.person }, { limit: 1 }))[0] + const receiverSpace = ( + await control.findAll(contact.class.PersonSpace, { person: receiverPersonId }, { limit: 1 }) + )[0] if (receiverSpace === undefined) return res - const collaboratorsTx = await getCollaboratorsTxes(reference, control, receiver, doc) + // TODO: Do we need for all or just one? + const collaboratorsTx = await getCollaboratorsTxes(reference, control, receiver[0], doc) res.push(...collaboratorsTx) @@ -159,13 +159,13 @@ export async function getPersonNotificationTxes ( }) ) } - + // TODO: Select a proper reciever const data: Omit, 'docNotifyContext'> = { header: activity.string.MentionedYouIn, messageHtml: reference.message, mentionedIn: reference.attachedDocId ?? reference.srcDocId, mentionedInClass: reference.attachedDocClass ?? reference.srcDocClass, - user: receiver._id, + user: receiver[0]._id, isViewed: false, archived: false } @@ -180,8 +180,8 @@ export async function getPersonNotificationTxes ( : undefined const receiverInfo = toReceiverInfo(control.hierarchy, { - _id: receiver._id, - account: receiver, + _id: receiver[0]._id, + account: receiver[0], person: receiverPerson, space: receiverSpace._id }) @@ -193,8 +193,20 @@ export async function getPersonNotificationTxes ( person: senderPerson } - const notifyResult = await shouldNotifyCommon(control, receiver._id, notification.ids.MentionCommonNotificationType) - const messageNotifyResult = await getMessageNotifyResult(reference, receiver, control, originTx, doc) + const notifyResult = await shouldNotifyCommon( + control, + receiver.map((it) => it._id), + notification.ids.MentionCommonNotificationType, + notificationControl + ) + const messageNotifyResult = await getMessageNotifyResult( + reference, + receiver, + control, + originTx, + doc, + notificationControl + ) for (const [provider] of messageNotifyResult.entries()) { if (notifyResult.has(provider)) { @@ -204,6 +216,7 @@ export async function getPersonNotificationTxes ( if (notifyResult.has(notification.providers.InboxNotificationProvider)) { const txes = await getCommonNotificationTxes( + ctx, control, doc, data, @@ -221,7 +234,7 @@ export async function getPersonNotificationTxes ( const context = ( await control.findAll( notification.class.DocNotifyContext, - { objectId: reference.srcDocId, user: receiver._id }, + { objectId: reference.srcDocId, user: { $in: receiver.map((it) => it._id) } }, { projection: { _id: 1 } } ) )[0] @@ -255,21 +268,24 @@ export async function getPersonNotificationTxes ( } async function checkSpace ( - user: PersonAccount, + users: PersonAccount[], spaceId: Ref, control: TriggerControl, res: Tx[] ): Promise { const space = (await control.findAll(core.class.Space, { _id: spaceId }, { limit: 1 }))[0] - const isMember = space.members.includes(user._id) + const toAdd = users.filter((user) => !space.members.includes(user._id)) + const isMember = toAdd.length === 0 if (space.private) { return isMember } if (!isMember) { - res.push( - control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { $push: { members: user._id } }) - ) + for (const user of toAdd) { + res.push( + control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { $push: { members: user._id } }) + ) + } } return true @@ -327,10 +343,11 @@ async function getCollaboratorsTxes ( async function getMessageNotifyResult ( reference: Data, - account: PersonAccount, + account: PersonAccount[], control: TriggerControl, originTx: TxCUD, - doc: Doc + doc: Doc, + notificationControl: NotificationProviderControl ): Promise { const { hierarchy } = control const tx = TxProcessor.extractTx(originTx) as TxCUD @@ -345,7 +362,7 @@ async function getMessageNotifyResult ( const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators) - if (mixin === undefined || !mixin.collaborators.includes(account._id)) { + if (mixin === undefined || !account.some((account) => mixin.collaborators.includes(account._id))) { return new Map() } @@ -353,7 +370,7 @@ async function getMessageNotifyResult ( return new Map() } - return await isShouldNotifyTx(control, tx, originTx, doc, account, false, false, undefined) + return await isShouldNotifyTx(control, tx, originTx, doc, account, false, false, notificationControl, undefined) } function isMarkupType (type: Ref>>): boolean { @@ -365,6 +382,7 @@ function isCollaborativeType (type: Ref>>): boolean { } async function getCreateReferencesTxes ( + ctx: MeasureContext, control: TriggerControl, storage: StorageAdapter, txFactory: TxFactory, @@ -410,10 +428,11 @@ async function getCreateReferencesTxes ( ? (srcDocId as Ref) : srcDocSpace - return await getReferencesTxes(control, txFactory, refs, refSpace, [], [], originTx) + return await getReferencesTxes(ctx, control, txFactory, refs, refSpace, [], [], originTx) } async function getUpdateReferencesTxes ( + ctx: MeasureContext, control: TriggerControl, storage: StorageAdapter, txFactory: TxFactory, @@ -474,7 +493,7 @@ async function getUpdateReferencesTxes ( ? (srcDocId as Ref) : srcDocSpace - return await getReferencesTxes(control, txFactory, references, refSpace, current, userMentions, originTx) + return await getReferencesTxes(ctx, control, txFactory, references, refSpace, current, userMentions, originTx) } return [] @@ -519,14 +538,16 @@ export function getReferencesData ( } async function createReferenceTxes ( + ctx: MeasureContext, control: TriggerControl, txFactory: TxFactory, ref: Data, space: Ref, - originTx: TxCUD + originTx: TxCUD, + notificationControl: NotificationProviderControl ): Promise { if (control.hierarchy.isDerived(ref.attachedToClass, contact.class.Person)) { - return await getPersonNotificationTxes(ref, control, txFactory.account, space, originTx) + return await getPersonNotificationTxes(ctx, ref, control, txFactory.account, space, originTx, notificationControl) } const refTx = control.txFactory.createTxCreateDoc(activity.class.ActivityReference, space, ref) @@ -536,6 +557,7 @@ async function createReferenceTxes ( } async function getReferencesTxes ( + ctx: MeasureContext, control: TriggerControl, txFactory: TxFactory, references: Data[], @@ -568,6 +590,8 @@ async function getReferencesTxes ( } } + const notificationControl = await getNotificationProviderControl(ctx, control) + for (const mention of mentions) { const refIndex = references.findIndex( (r) => mention.user === r.attachedTo && mention.attachedTo === r.attachedDocId @@ -588,7 +612,7 @@ async function getReferencesTxes ( // Add missing references for (const ref of references) { - txes.push(...(await createReferenceTxes(control, txFactory, ref, space, originTx))) + txes.push(...(await createReferenceTxes(ctx, control, txFactory, ref, space, originTx, notificationControl))) } return txes @@ -659,6 +683,7 @@ async function ActivityReferenceCreate (tx: TxCUD, etx: TxCUD, control const targetTx = guessReferenceTx(control.hierarchy, tx) const txes: Tx[] = await getCreateReferencesTxes( + control.ctx, control, control.storageAdapter, txFactory, @@ -706,6 +731,7 @@ async function ActivityReferenceUpdate (tx: TxCUD, etx: TxCUD, control const targetTx = guessReferenceTx(control.hierarchy, tx) const txes: Tx[] = await getUpdateReferencesTxes( + control.ctx, control, control.storageAdapter, txFactory, diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index 1c1a6f8777..a1d15cea72 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -22,7 +22,7 @@ import chunter, { ChunterSpace, ThreadMessage } from '@hcengineering/chunter' -import contact, { Person, PersonAccount } from '@hcengineering/contact' +import { Person, PersonAccount } from '@hcengineering/contact' import core, { Account, AttachedDoc, @@ -56,7 +56,7 @@ import { import { markupToHTML, markupToText, stripTags } from '@hcengineering/text' import { workbenchId } from '@hcengineering/workbench' -import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification' +import { getPersonAccountById, NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification' import { encodeObjectURI } from '@hcengineering/view' const updateChatInfoDelay = 12 * 60 * 60 * 1000 // 12 hours @@ -121,13 +121,8 @@ export async function CommentRemove ( } async function OnThreadMessageCreated (originTx: TxCUD, control: TriggerControl): Promise { - const hierarchy = control.hierarchy const tx = TxProcessor.extractTx(originTx) as TxCreateDoc - if (tx._class !== core.class.TxCreateDoc || !hierarchy.isDerived(tx.objectClass, chunter.class.ThreadMessage)) { - return [] - } - const threadMessage = TxProcessor.createDoc2Doc(tx) const message = (await control.findAll(activity.class.ActivityMessage, { _id: threadMessage.attachedTo }))[0] @@ -166,13 +161,6 @@ async function OnChatMessageCreated (tx: TxCUD, control: TriggerControl): P const hierarchy = control.hierarchy const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc - if ( - actualTx._class !== core.class.TxCreateDoc || - !hierarchy.isDerived(actualTx.objectClass, chunter.class.ChatMessage) - ) { - return [] - } - const message = TxProcessor.createDoc2Doc(actualTx) const mixin = hierarchy.classHierarchyMixin(message.attachedToClass, notification.mixin.ClassCollaborators) @@ -244,13 +232,8 @@ function joinChannel (control: TriggerControl, channel: Channel, user: Ref { - const hierarchy = control.hierarchy const removeTx = TxProcessor.extractTx(tx) as TxRemoveDoc - if (!hierarchy.isDerived(removeTx.objectClass, chunter.class.ThreadMessage)) { - return [] - } - const message = control.removedMap.get(removeTx.objectId) as ThreadMessage if (message === undefined) { @@ -286,15 +269,40 @@ async function OnThreadMessageDeleted (tx: Tx, control: TriggerControl): Promise */ export async function ChunterTrigger (tx: TxCUD, control: TriggerControl): Promise { const res: Tx[] = [] - res.push( - ...(await control.ctx.with('OnThreadMessageCreated', {}, async (ctx) => await OnThreadMessageCreated(tx, control))) - ) - res.push( - ...(await control.ctx.with('OnThreadMessageDeleted', {}, async (ctx) => await OnThreadMessageDeleted(tx, control))) - ) - res.push( - ...(await control.ctx.with('OnChatMessageCreated', {}, async (ctx) => await OnChatMessageCreated(tx, control))) - ) + const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc + + if ( + actualTx._class === core.class.TxCreateDoc && + control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage) + ) { + res.push( + ...(await control.ctx.with( + 'OnThreadMessageCreated', + {}, + async (ctx) => await OnThreadMessageCreated(tx, control) + )) + ) + } + if ( + actualTx._class === core.class.TxRemoveDoc && + control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage) + ) { + res.push( + ...(await control.ctx.with( + 'OnThreadMessageDeleted', + {}, + async (ctx) => await OnThreadMessageDeleted(tx, control) + )) + ) + } + if ( + actualTx._class === core.class.TxCreateDoc && + control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ChatMessage) + ) { + res.push( + ...(await control.ctx.with('OnChatMessageCreated', {}, async (ctx) => await OnChatMessageCreated(tx, control))) + ) + } return res } @@ -455,7 +463,7 @@ async function hideOldChannels ( } export async function updateChatInfo (control: TriggerControl, status: UserStatus, date: Timestamp): Promise { - const account = await control.modelDb.findOne(contact.class.PersonAccount, { _id: status.user as Ref }) + const account = getPersonAccountById(status.user as Ref, control) if (account === undefined) return const update = (await control.findAll(chunter.class.ChatInfo, { user: account.person })).shift() @@ -559,14 +567,14 @@ async function OnContextUpdate (tx: TxUpdateDoc, control: Trig return [] } -async function JoinChannelTypeMatch (originTx: Tx, _: Doc, user: Ref): Promise { - if (originTx.modifiedBy === user) return false +function JoinChannelTypeMatch (originTx: Tx, _: Doc, user: Ref[]): boolean { + if (user.some((it) => originTx.modifiedBy === it)) return false if (originTx._class !== core.class.TxUpdateDoc) return false const tx = originTx as TxUpdateDoc const added = combineAttributes([tx.operations], 'members', '$push', '$each') - return added.includes(user) + return user.some((it) => added.includes(it)) } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type diff --git a/server-plugins/contact-resources/src/index.ts b/server-plugins/contact-resources/src/index.ts index 836648cc4f..864f8188aa 100644 --- a/server-plugins/contact-resources/src/index.ts +++ b/server-plugins/contact-resources/src/index.ts @@ -21,13 +21,13 @@ import contact, { Organization, Person, PersonAccount, + PersonSpace, contactId, formatContactName, formatName, getFirstName, getLastName, - getName, - PersonSpace + getName } from '@hcengineering/contact' import core, { Account, @@ -37,13 +37,13 @@ import core, { Ref, SpaceType, Tx, + TxCUD, TxCreateDoc, TxMixin, TxProcessor, TxRemoveDoc, TxUpdateDoc, - concatLink, - TxCUD + concatLink } from '@hcengineering/core' import notification, { Collaborators } from '@hcengineering/notification' import { getMetadata } from '@hcengineering/platform' @@ -85,41 +85,49 @@ export async function OnSpaceTypeMembers (tx: Tx, control: TriggerControl): Prom export async function OnEmployeeCreate (tx: Tx, control: TriggerControl): Promise { const mixinTx = tx as TxMixin if (mixinTx.attributes.active !== true) return [] - const acc = await control.modelDb.findOne(contact.class.PersonAccount, { person: mixinTx.objectId }) - if (acc === undefined) return [] + const acc = control.modelDb.getAccountByPersonId(mixinTx.objectId) + if (acc.length === 0) return [] const spaces = await control.findAll(core.class.Space, { autoJoin: true }) const result: Tx[] = [] - const txes = await createPersonSpace(acc._id, mixinTx.objectId, control) + const txes = await createPersonSpace( + acc.map((it) => it._id), + mixinTx.objectId, + control + ) result.push(...txes) for (const space of spaces) { - if (space.members.includes(acc._id)) continue - const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { - $push: { - members: acc._id - } - }) - result.push(pushTx) + const toAdd = acc.filter((it) => !space.members.includes(it._id)) + if (toAdd.length === 0) continue + for (const a of toAdd) { + const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { + $push: { + members: a._id + } + }) + result.push(pushTx) + } } return result } async function createPersonSpace ( - account: Ref, + account: Ref[], person: Ref, control: TriggerControl ): Promise[]> { const personSpace = (await control.findAll(contact.class.PersonSpace, { person }, { limit: 1 })).shift() if (personSpace !== undefined) { - if (personSpace.members.includes(account)) return [] - return [ + const toAdd = account.filter((it) => !personSpace.members.includes(it)) + if (toAdd.length === 0) return [] + return toAdd.map((it) => control.txFactory.createTxUpdateDoc(personSpace._class, personSpace.space, personSpace._id, { $push: { - members: account + members: it } }) - ] + ) } return [ @@ -129,7 +137,7 @@ async function createPersonSpace ( private: true, archived: false, person, - members: [account] + members: account }) ] } @@ -143,7 +151,7 @@ export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): P const spaces = await control.findAll(core.class.Space, { autoJoin: true }) const result: Tx[] = [] - const txes = await createPersonSpace(acc._id, person._id, control) + const txes = await createPersonSpace([acc._id], person._id, control) result.push(...txes) diff --git a/server-plugins/gmail-resources/src/index.ts b/server-plugins/gmail-resources/src/index.ts index 0c6384377c..6547d8a53d 100644 --- a/server-plugins/gmail-resources/src/index.ts +++ b/server-plugins/gmail-resources/src/index.ts @@ -88,13 +88,13 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise /** * @public */ -export async function IsIncomingMessage ( +export function IsIncomingMessageTypeMatch ( tx: Tx, doc: Doc, user: Ref, type: NotificationType, control: TriggerControl -): Promise { +): boolean { const message = TxProcessor.createDoc2Doc(TxProcessor.extractTx(tx) as TxCreateDoc) return message.incoming && message.sendOn > (doc.createdOn ?? doc.modifiedOn) } @@ -183,7 +183,7 @@ export default async () => ({ OnMessageCreate }, function: { - IsIncomingMessage, + IsIncomingMessageTypeMatch, FindMessages, SendEmailNotifications } diff --git a/server-plugins/gmail/src/index.ts b/server-plugins/gmail/src/index.ts index cbacf260db..98eab2f0b9 100644 --- a/server-plugins/gmail/src/index.ts +++ b/server-plugins/gmail/src/index.ts @@ -32,7 +32,7 @@ export default plugin(serverGmailId, { OnMessageCreate: '' as Resource }, function: { - IsIncomingMessage: '' as TypeMatchFunc, + IsIncomingMessageTypeMatch: '' as TypeMatchFunc, FindMessages: '' as Resource, SendEmailNotifications: '' as Resource } diff --git a/server-plugins/hr-resources/src/index.ts b/server-plugins/hr-resources/src/index.ts index c75f30bccd..5e5494e5c3 100644 --- a/server-plugins/hr-resources/src/index.ts +++ b/server-plugins/hr-resources/src/index.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import contact, { Contact, Employee, Person, PersonAccount, formatName, getName } from '@hcengineering/contact' +import contact, { Contact, Employee, formatName, getName, Person, PersonAccount } from '@hcengineering/contact' import core, { Doc, Ref, @@ -27,6 +27,7 @@ import core, { TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core' +import gmail from '@hcengineering/gmail' import hr, { Department, DepartmentMember, @@ -39,10 +40,13 @@ import hr, { import notification, { NotificationType } from '@hcengineering/notification' import { translate } from '@hcengineering/platform' import { TriggerControl } from '@hcengineering/server-core' -import { getEmployee, getPersonAccountById } from '@hcengineering/server-notification' -import { getContentByTemplate, isAllowed } from '@hcengineering/server-notification-resources' -import gmail from '@hcengineering/gmail' import { sendEmailNotification } from '@hcengineering/server-gmail-resources' +import { getEmployee, getPersonAccountById } from '@hcengineering/server-notification' +import { + getContentByTemplate, + getNotificationProviderControl, + isAllowed +} from '@hcengineering/server-notification-resources' async function getOldDepartment ( currentTx: TxMixin | TxUpdateDoc, @@ -97,21 +101,29 @@ function exlude (first: Ref[], second: Ref[]): Ref, + account: Ref[], added: Ref[], removed?: Ref[] ): Tx[] { - const pushTxes = added.map((dep) => - factory.createTxUpdateDoc(hr.class.Department, core.space.Workspace, dep, { - $push: { members: account } - }) - ) + const pushTxes = added + .map((dep) => + account.map((it) => + factory.createTxUpdateDoc(hr.class.Department, core.space.Workspace, dep, { + $push: { members: it } + }) + ) + ) + .flat() if (removed === undefined) return pushTxes - const pullTxes = removed.map((dep) => - factory.createTxUpdateDoc(hr.class.Department, core.space.Workspace, dep, { - $pull: { members: account } - }) - ) + const pullTxes = removed + .map((dep) => + account.map((it) => + factory.createTxUpdateDoc(hr.class.Department, core.space.Workspace, dep, { + $pull: { members: it } + }) + ) + ) + .flat() return [...pullTxes, ...pushTxes] } @@ -121,12 +133,8 @@ function getTxes ( export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promise { const ctx = TxProcessor.extractTx(tx) as TxMixin - const targetAccount = ( - await control.modelDb.findAll(contact.class.PersonAccount, { - person: ctx.objectId - }) - )[0] - if (targetAccount === undefined) return [] + const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[] + if (targetAccount.length === 0) return [] if (ctx.attributes.department !== undefined) { const lastDepartment = await getOldDepartment(ctx, control) @@ -137,7 +145,7 @@ export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promi const removed = await buildHierarchy(lastDepartment, control) return getTxes( control.txFactory, - targetAccount._id, + targetAccount.map((it) => it._id), [], removed.map((p) => p._id) ) @@ -146,13 +154,22 @@ export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promi const push = (await buildHierarchy(departmentId, control)).map((p) => p._id) if (lastDepartment === undefined) { - return getTxes(control.txFactory, targetAccount._id, push) + return getTxes( + control.txFactory, + targetAccount.map((it) => it._id), + push + ) } let removed = (await buildHierarchy(lastDepartment, control)).map((p) => p._id) const added = exlude(removed, push) removed = exlude(push, removed) - return getTxes(control.txFactory, targetAccount._id, added, removed) + return getTxes( + control.txFactory, + targetAccount.map((it) => it._id), + added, + removed + ) } return [] @@ -183,16 +200,14 @@ export async function OnDepartmentRemove (tx: Tx, control: TriggerControl): Prom employee.forEach((em) => { res.push(control.txFactory.createTxMixin(em._id, em._class, em.space, hr.mixin.Staff, { department: undefined })) }) - targetAccounts.forEach((acc) => { - res.push( - ...getTxes( - control.txFactory, - acc._id, - [], - removed.map((p) => p._id) - ) + res.push( + ...getTxes( + control.txFactory, + targetAccounts.map((it) => it._id), + [], + removed.map((p) => p._id) ) - }) + ) return res } @@ -232,19 +247,15 @@ export async function OnEmployeeDeactivate (tx: Tx, control: TriggerControl): Pr return [] } - const targetAccount = ( - await control.modelDb.findAll(contact.class.PersonAccount, { - person: ctx.objectId - }) - )[0] - if (targetAccount === undefined) return [] + const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[] + if (targetAccount.length === 0) return [] const lastDepartment = await getOldDepartment(ctx, control) if (lastDepartment === undefined) return [] const removed = await buildHierarchy(lastDepartment, control) return getTxes( control.txFactory, - targetAccount._id, + targetAccount.map((it) => it._id), [], removed.map((p) => p._id) ) @@ -268,19 +279,21 @@ async function sendEmailNotifications ( } // should respect employee settings - const accounts = await control.modelDb.findAll(contact.class.PersonAccount, { - person: { $in: Array.from(contacts.values()) as Ref[] } - }) const type = await control.modelDb.findOne(notification.class.NotificationType, { _id: typeId }) if (type === undefined) return const provider = await control.modelDb.findOne(notification.class.NotificationProvider, { _id: gmail.providers.EmailNotificationProvider }) if (provider === undefined) return - for (const account of accounts) { - const allowed = await isAllowed(control, account._id, type, provider) - if (!allowed) { - contacts.delete(account.person) + + const notificationControl = await getNotificationProviderControl(control.ctx, control) + for (const accountId of contacts.values()) { + const accounts = control.modelDb.getAccountByPersonId(accountId) as PersonAccount[] + for (const account of accounts) { + const allowed = isAllowed(control, account._id, type, provider, notificationControl) + if (!allowed) { + contacts.delete(account.person) + } } } @@ -306,7 +319,7 @@ async function sendEmailNotifications ( export async function OnRequestCreate (tx: Tx, control: TriggerControl): Promise { const ctx = TxProcessor.extractTx(tx) as TxCreateDoc - const sender = await getPersonAccountById(ctx.modifiedBy, control) + const sender = getPersonAccountById(ctx.modifiedBy, control) if (sender === undefined) return [] const request = TxProcessor.createDoc2Doc(ctx) @@ -321,7 +334,7 @@ export async function OnRequestCreate (tx: Tx, control: TriggerControl): Promise export async function OnRequestUpdate (tx: Tx, control: TriggerControl): Promise { const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc - const sender = await getPersonAccountById(ctx.modifiedBy, control) + const sender = getPersonAccountById(ctx.modifiedBy, control) if (sender === undefined) return [] const request = (await control.findAll(hr.class.Request, { _id: ctx.objectId }))[0] as Request @@ -337,7 +350,7 @@ export async function OnRequestUpdate (tx: Tx, control: TriggerControl): Promise export async function OnRequestRemove (tx: Tx, control: TriggerControl): Promise { const ctx = TxProcessor.extractTx(tx) as TxCreateDoc - const sender = await getPersonAccountById(ctx.modifiedBy, control) + const sender = getPersonAccountById(ctx.modifiedBy, control) if (sender === undefined) return [] const request = control.removedMap.get(ctx.objectId) as Request @@ -389,7 +402,7 @@ export async function RequestTextPresenter (doc: Doc, control: TriggerControl): export async function OnPublicHolidayCreate (tx: Tx, control: TriggerControl): Promise { const ctx = TxProcessor.extractTx(tx) as TxCreateDoc - const sender = await getPersonAccountById(ctx.modifiedBy, control) + const sender = getPersonAccountById(ctx.modifiedBy, control) if (sender === undefined) return [] const employee = await getEmployee(sender.person as Ref, control) if (employee === undefined) return [] @@ -410,7 +423,7 @@ export async function OnPublicHolidayCreate (tx: Tx, control: TriggerControl): P */ export async function PublicHolidayHTMLPresenter (doc: Doc, control: TriggerControl): Promise { const holiday = doc as PublicHoliday - const sender = await getPersonAccountById(holiday.modifiedBy, control) + const sender = getPersonAccountById(holiday.modifiedBy, control) if (sender === undefined) return '' const employee = await getEmployee(sender.person as Ref, control) if (employee === undefined) return '' @@ -426,7 +439,7 @@ export async function PublicHolidayHTMLPresenter (doc: Doc, control: TriggerCont */ export async function PublicHolidayTextPresenter (doc: Doc, control: TriggerControl): Promise { const holiday = doc as PublicHoliday - const sender = await getPersonAccountById(holiday.modifiedBy, control) + const sender = getPersonAccountById(holiday.modifiedBy, control) if (sender === undefined) return '' const employee = await getEmployee(sender.person as Ref, control) if (employee === undefined) return '' diff --git a/server-plugins/love-resources/src/index.ts b/server-plugins/love-resources/src/index.ts index 57e76813fb..5efa896134 100644 --- a/server-plugins/love-resources/src/index.ts +++ b/server-plugins/love-resources/src/index.ts @@ -37,7 +37,11 @@ import love, { import notification from '@hcengineering/notification' import { translate } from '@hcengineering/platform' import { TriggerControl } from '@hcengineering/server-core' -import { createPushNotification, isAllowed } from '@hcengineering/server-notification-resources' +import { + createPushNotification, + getNotificationProviderControl, + isAllowed +} from '@hcengineering/server-notification-resources' import { workbenchId } from '@hcengineering/workbench' export async function OnEmployee (tx: Tx, control: TriggerControl): Promise { @@ -267,16 +271,19 @@ export async function OnKnock (tx: Tx, control: TriggerControl): Promise { _id: notification.providers.PushNotificationProvider }) if (provider === undefined) return [] + + const notificationControl = await getNotificationProviderControl(control.ctx, control) for (const user of roomInfo.persons) { - const userAcc = await control.modelDb.findOne(contact.class.PersonAccount, { person: user }) - if (userAcc === undefined) continue - if (await isAllowed(control, userAcc._id, type, provider)) { + const userAcc = control.modelDb.getAccountByPersonId(user) as PersonAccount[] + if (userAcc.length === 0) continue + if (userAcc.some((it) => isAllowed(control, it._id, type, provider, notificationControl))) { const path = [workbenchId, control.workspace.workspaceUrl, loveId] const title = await translate(love.string.KnockingLabel, {}) const body = await translate(love.string.IsKnocking, { name: formatName(from.name, control.branding?.lastNameFirst) }) - await createPushNotification(control, userAcc._id, title, body, request._id, from, path) + // TODO: Select proper account target + await createPushNotification(control, userAcc[0]._id, title, body, request._id, from, path) } } return res @@ -293,8 +300,8 @@ export async function OnInvite (tx: Tx, control: TriggerControl): Promise if (invite.status === RequestStatus.Pending) { const target = (await control.findAll(contact.class.Person, { _id: invite.target }))[0] if (target === undefined) return [] - const userAcc = await control.modelDb.findOne(contact.class.PersonAccount, { person: target._id }) - if (userAcc === undefined) return [] + const userAcc = control.modelDb.getAccountByPersonId(target._id) as PersonAccount[] + if (userAcc.length === 0) return [] const from = (await control.findAll(contact.class.Person, { _id: invite.from }))[0] const type = await control.modelDb.findOne(notification.class.NotificationType, { _id: love.ids.InviteNotification @@ -304,7 +311,8 @@ export async function OnInvite (tx: Tx, control: TriggerControl): Promise _id: notification.providers.PushNotificationProvider }) if (provider === undefined) return [] - if (await isAllowed(control, userAcc._id, type, provider)) { + const notificationControl = await getNotificationProviderControl(control.ctx, control) + if (userAcc.some((it) => isAllowed(control, it._id, type, provider, notificationControl))) { const path = [workbenchId, control.workspace.workspaceUrl, loveId] const title = await translate(love.string.InivitingLabel, {}) const body = @@ -313,7 +321,8 @@ export async function OnInvite (tx: Tx, control: TriggerControl): Promise name: formatName(from.name, control.branding?.lastNameFirst) }) : await translate(love.string.InivitingLabel, {}) - await createPushNotification(control, userAcc._id, title, body, invite._id, from, path) + // TODO: Select a proper user + await createPushNotification(control, userAcc[0]._id, title, body, invite._id, from, path) } } } diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index f6fcace22f..565f255867 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -17,11 +17,11 @@ import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' import chunter, { ChatMessage } from '@hcengineering/chunter' import contact, { - type AvatarInfo, getAvatarProviderId, getGravatarUrl, Person, - PersonAccount + PersonAccount, + type AvatarInfo } from '@hcengineering/contact' import core, { Account, @@ -72,7 +72,6 @@ import { getMetadata, getResource, translate } from '@hcengineering/platform' import { type TriggerControl } from '@hcengineering/server-core' import serverCore from '@hcengineering/server-core' import serverNotification, { - getPersonAccount, getPersonAccountById, NOTIFICATION_BODY_SIZE, PUSH_NOTIFICATION_TITLE_SIZE, @@ -92,17 +91,19 @@ import { getHTMLPresenter, getNotificationContent, getNotificationLink, + getNotificationProviderControl, getTextPresenter, getUsersInfo, isAllowed, isMixinTx, isShouldNotifyTx, - isUserEmployeeInFieldValue, - isUserInFieldValue, + isUserEmployeeInFieldValueTypeMatch, + isUserInFieldValueTypeMatch, messageToMarkup, replaceAll, toReceiverInfo, - updateNotifyContextsSpace + updateNotifyContextsSpace, + type NotificationProviderControl } from './utils' export function getPushCollaboratorTx ( @@ -124,6 +125,7 @@ export function getPushCollaboratorTx ( } export async function getCommonNotificationTxes ( + ctx: MeasureContext, control: TriggerControl, doc: Doc, data: Partial>, @@ -144,6 +146,7 @@ export async function getCommonNotificationTxes ( const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { objectId: attachedTo }) const notificationTx = await pushInboxNotifications( + ctx, control, res, receiver, @@ -255,15 +258,14 @@ export async function getContentByTemplate ( } } -async function getValueCollaborators (value: any, attr: AnyAttribute, control: TriggerControl): Promise[]> { +function getValueCollaborators (value: any, attr: AnyAttribute, control: TriggerControl): Ref[] { const hierarchy = control.hierarchy if (attr.type._class === core.class.RefTo) { const to = (attr.type as RefTo).to if (hierarchy.isDerived(to, contact.class.Person)) { - const acc = await getPersonAccount(value, control) - return acc !== undefined ? [acc._id] : [] + return (control.modelDb.getAccountByPersonId(value) as PersonAccount[]).map((it) => it._id) } else if (hierarchy.isDerived(to, core.class.Account)) { - const acc = await getPersonAccountById(value, control) + const acc = getPersonAccountById(value, control) return acc !== undefined ? [acc._id] : [] } } else if (attr.type._class === core.class.ArrOf) { @@ -271,12 +273,12 @@ async function getValueCollaborators (value: any, attr: AnyAttribute, control: T if (arrOf._class === core.class.RefTo) { const to = (arrOf as RefTo).to if (hierarchy.isDerived(to, contact.class.Person)) { - const employeeAccounts = await control.modelDb.findAll(contact.class.PersonAccount, { + const employeeAccounts = control.modelDb.findAllSync(contact.class.PersonAccount, { person: { $in: Array.isArray(value) ? value : [value] } }) return employeeAccounts.map((p) => p._id) } else if (hierarchy.isDerived(to, core.class.Account)) { - const employeeAccounts = await control.modelDb.findAll(contact.class.PersonAccount, { + const employeeAccounts = control.modelDb.findAllSync(contact.class.PersonAccount, { _id: { $in: Array.isArray(value) ? value : [value] } }) return employeeAccounts.map((p) => p._id) @@ -295,7 +297,7 @@ async function getKeyCollaborators ( if (value !== undefined && value !== null) { const attr = control.hierarchy.findAttribute(docClass, field) if (attr !== undefined) { - return await getValueCollaborators(value, attr, control) + return getValueCollaborators(value, attr, control) } } } @@ -315,7 +317,7 @@ export async function getDocCollaborators ( const newCollaborators = await ctx.with( 'getKeyCollaborators', {}, - async () => await getKeyCollaborators(doc._class, value, field, control) + async (ctx) => await getKeyCollaborators(doc._class, value, field, control) ) if (newCollaborators !== undefined) { for (const newCollaborator of newCollaborators) { @@ -327,6 +329,7 @@ export async function getDocCollaborators ( } export async function pushInboxNotifications ( + ctx: MeasureContext, control: TriggerControl, res: Tx[], receiver: ReceiverInfo, @@ -344,6 +347,7 @@ export async function pushInboxNotifications ( if (context === undefined) { docNotifyContextId = await createNotifyContext( + ctx, control, objectId, objectClass, @@ -584,6 +588,7 @@ async function sendPushToSubscription ( * @public */ export async function pushActivityInboxNotifications ( + ctx: MeasureContext, originTx: TxCUD, control: TriggerControl, res: Tx[], @@ -594,7 +599,7 @@ export async function pushActivityInboxNotifications ( activityMessage: ActivityMessage, shouldUpdateTimestamp: boolean ): Promise | undefined> { - const content = await getNotificationContent(originTx, receiver.account, sender, object, control) + const content = await getNotificationContent(originTx, [receiver.account], sender, object, control) const data: Partial> = { ...content, attachedTo: activityMessage._id, @@ -602,6 +607,7 @@ export async function pushActivityInboxNotifications ( } return await pushInboxNotifications( + ctx, control, res, receiver, @@ -628,7 +634,7 @@ export async function applyNotificationProviders ( sender: SenderInfo, message?: ActivityMessage ): Promise { - const resources = await control.modelDb.findAll(serverNotification.class.NotificationProviderResources, {}) + const resources = control.modelDb.findAllSync(serverNotification.class.NotificationProviderResources, {}) for (const [provider, types] of notifyResult.entries()) { if (provider === notification.providers.PushNotificationProvider) { // const now = Date.now() @@ -663,6 +669,7 @@ export async function applyNotificationProviders ( } async function createNotifyContext ( + ctx: MeasureContext, control: TriggerControl, objectId: Ref, objectClass: Ref>, @@ -678,7 +685,7 @@ async function createNotifyContext ( isPinned: false, lastUpdateTimestamp: updateTimestamp }) - await control.apply([createTx]) + await ctx.with('apply', {}, () => control.apply([createTx])) if (receiver.account?.email !== undefined) { control.operationContext.derived.targets['docNotifyContext' + createTx._id] = (it) => { if (it._id === createTx._id) { @@ -690,6 +697,7 @@ async function createNotifyContext ( } export async function getNotificationTxes ( + ctx: MeasureContext, control: TriggerControl, object: Doc, tx: TxCUD, @@ -698,7 +706,8 @@ export async function getNotificationTxes ( sender: SenderInfo, params: NotifyParams, docNotifyContexts: DocNotifyContext[], - activityMessages: ActivityMessage[] + activityMessages: ActivityMessage[], + settings: NotificationProviderControl ): Promise { if (receiver.account === undefined) { return [] @@ -713,14 +722,16 @@ export async function getNotificationTxes ( tx, originTx, object, - receiver.account, + [receiver.account], params.isOwn, params.isSpace, + settings, docMessage ) if (notifyResult.has(notification.providers.InboxNotificationProvider)) { const notificationTx = await pushActivityInboxNotifications( + ctx, originTx, control, res, @@ -753,6 +764,7 @@ export async function getNotificationTxes ( if (context === undefined) { await createNotifyContext( + ctx, control, message.attachedTo, message.attachedToClass, @@ -767,20 +779,17 @@ export async function getNotificationTxes ( } async function updateContextsTimestamp ( + ctx: MeasureContext, contexts: DocNotifyContext[], timestamp: Timestamp, control: TriggerControl, modifiedBy: Ref ): Promise { if (contexts.length === 0) return - - const accounts = await control.modelDb.findAll(contact.class.PersonAccount, { - _id: { $in: contexts.map((it) => it.user as Ref) } - }) const res: Tx[] = [] for (const context of contexts) { - const account = accounts.find(({ _id }) => _id === context.user) + const account = getPersonAccountById(context.user, control) // accounts.find(({ _id }) => _id === context.user) const isViewed = context.lastViewedTimestamp !== undefined && (context.lastUpdateTimestamp ?? 0) <= context.lastViewedTimestamp const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { @@ -803,10 +812,13 @@ async function updateContextsTimestamp ( } } - await control.apply(res) + if (res.length > 0) { + await ctx.with('apply', {}, () => control.apply(res)) + } } async function removeContexts ( + ctx: MeasureContext, contexts: DocNotifyContext[], unsubscribe: Ref[], control: TriggerControl @@ -814,13 +826,13 @@ async function removeContexts ( if (contexts.length === 0) return if (unsubscribe.length === 0) return - const unsubscribeAccounts = await control.modelDb.findAll(contact.class.PersonAccount, { - _id: { $in: unsubscribe } - }) const res: Tx[] = [] for (const context of contexts) { - const account = unsubscribeAccounts.find(({ _id }) => _id === context.user) + if (!unsubscribe.includes(context.user as Ref)) { + continue + } + const account = control.modelDb.getObject(context.user) if (account === undefined) continue const removeTx = control.txFactory.createTxRemoveDoc(context._class, context.space, context._id) @@ -864,23 +876,31 @@ export async function createCollabDocInfo ( objectId: object._id, user: { $in: unsubscribe } }) - await removeContexts(notifyContexts, unsubscribe, control) + await removeContexts(ctx, notifyContexts, unsubscribe, control) } return res } - const notifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, { objectId: object._id }) - await removeContexts(notifyContexts, unsubscribe, control) - await updateContextsTimestamp(notifyContexts, originTx.modifiedOn, control, originTx.modifiedBy) + let notifyContexts: DocNotifyContext[] = [] + if (!(object._id === tx.objectId && tx._class === core.class.TxCreateDoc)) { + notifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, { objectId: object._id }) + } + + if (notifyContexts.length > 0) { + await updateContextsTimestamp(ctx, notifyContexts, originTx.modifiedOn, control, originTx.modifiedBy) + } + if (notifyContexts.length > 0 && unsubscribe.length > 0) { + await removeContexts(ctx, notifyContexts, unsubscribe, control) + } const targets = new Set(collaborators) // user is not collaborator of himself, but we should notify user of changes related to users account (mentions, comments etc) if (control.hierarchy.isDerived(object._class, contact.class.Person)) { - const acc = await getPersonAccount(object._id as Ref, control) - if (acc !== undefined) { - targets.add(acc._id) + const acc = control.modelDb.getAccountByPersonId(object._id as Ref) as PersonAccount[] + for (const a of acc.map((it) => it._id)) { + targets.add(a) } } @@ -893,19 +913,19 @@ export async function createCollabDocInfo ( {}, async (ctx) => await getUsersInfo(ctx, [...Array.from(targets), originTx.modifiedBy as Ref], control) ) - const sender: SenderInfo = usersInfo.find(({ _id }) => _id === originTx.modifiedBy) ?? { + const sender: SenderInfo = usersInfo.get(originTx.modifiedBy) ?? { _id: originTx.modifiedBy } + const settings = await getNotificationProviderControl(ctx, control) + for (const target of targets) { - const info: ReceiverInfo | undefined = toReceiverInfo( - control.hierarchy, - usersInfo.find(({ _id }) => _id === target) - ) + const info: ReceiverInfo | undefined = toReceiverInfo(control.hierarchy, usersInfo.get(target)) if (info === undefined) continue const targetRes = await getNotificationTxes( + ctx, control, object, tx, @@ -914,7 +934,8 @@ export async function createCollabDocInfo ( sender, params, notifyContexts, - docMessages + docMessages, + settings ) const ids = new Set(targetRes.map((it) => it._id)) if (info.account?.email !== undefined) { @@ -994,6 +1015,7 @@ async function getTxCollabs ( } async function getSpaceCollabTxes ( + ctx: MeasureContext, control: TriggerControl, doc: Doc, tx: TxCUD, @@ -1005,7 +1027,8 @@ async function getSpaceCollabTxes ( return [] } - const space = cache.get(doc.space) ?? (await control.findAll(core.class.Space, { _id: doc.space }, { limit: 1 }))[0] + const space = + cache.get(doc.space) ?? (await control.findAllCtx(ctx, core.class.Space, { _id: doc.space }, { limit: 1 }))[0] if (space === undefined) return [] cache.set(space._id, space) @@ -1018,7 +1041,7 @@ async function getSpaceCollabTxes ( const collabs = control.hierarchy.as(space, notification.mixin.Collaborators) if (collabs.collaborators !== undefined) { return await createCollabDocInfo( - control.ctx, + ctx, collabs.collaborators as Ref[], control, tx, @@ -1059,7 +1082,7 @@ async function createCollaboratorDoc ( const notificationTxes = await ctx.with( 'create-collabdocinfo', {}, - async () => + async (ctx) => await createCollabDocInfo( ctx, collaborators as Ref[], @@ -1078,7 +1101,7 @@ async function createCollaboratorDoc ( ...(await ctx.with( 'get-space-collabtxes', {}, - async () => await getSpaceCollabTxes(control, doc, tx, originTx, activityMessage, cache) + async (ctx) => await getSpaceCollabTxes(ctx, control, doc, tx, originTx, activityMessage, cache) )) ) @@ -1100,6 +1123,8 @@ async function updateCollaboratorsMixin ( const res: Tx[] = [] + const notificationControl = await getNotificationProviderControl(ctx, control) + if (tx.attributes.collaborators !== undefined) { const createTx = hierarchy.isDerived(tx.objectClass, core.class.AttachedDoc) ? ( @@ -1142,7 +1167,7 @@ async function updateCollaboratorsMixin ( for (const collab of tx.attributes.collaborators) { if (!prevCollabs.has(collab) && tx.modifiedBy !== collab) { for (const provider of providers) { - if (await isAllowed(control, collab as Ref, type, provider)) { + if (isAllowed(control, collab as Ref, type, provider, notificationControl)) { newCollabs.push(collab) break } @@ -1161,17 +1186,15 @@ async function updateCollaboratorsMixin ( {}, async (ctx) => await getUsersInfo(ctx, [...newCollabs, originTx.modifiedBy] as Ref[], control) ) - const sender: SenderInfo = infos.find(({ _id }) => _id === originTx.modifiedBy) ?? { _id: originTx.modifiedBy } + const sender: SenderInfo = infos.get(originTx.modifiedBy) ?? { _id: originTx.modifiedBy } for (const collab of newCollabs) { - const target = toReceiverInfo( - hierarchy, - infos.find(({ _id }) => _id === collab) - ) + const target = toReceiverInfo(hierarchy, infos.get(collab)) if (target === undefined) continue for (const message of activityMessages) { await pushActivityInboxNotifications( + ctx, originTx, control, res, @@ -1225,7 +1248,7 @@ async function collectionCollabDoc ( const collaborators = await ctx.with( 'get-collaborators', {}, - async () => await getCollaborators(doc, control, tx, res) + async (ctx) => await getCollaborators(ctx, doc, control, tx, res) ) res = res.concat( @@ -1377,7 +1400,7 @@ async function updateCollaboratorDoc ( const doc = await ctx.with( 'find-doc', { _class: tx.objectClass }, - async () => + async (ctx) => cache.get(tx.objectId) ?? (await control.findAllCtx(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] ) if (doc === undefined) return [] @@ -1388,7 +1411,7 @@ async function updateCollaboratorDoc ( const collabsInfo = await ctx.with( 'get-tx-collaborators', {}, - async () => await getTxCollabs(ctx, tx, control, doc) + async (ctx) => await getTxCollabs(ctx, tx, control, doc) ) if (collabsInfo.added.length > 0) { @@ -1403,7 +1426,7 @@ async function updateCollaboratorDoc ( await ctx.with( 'create-collab-docinfo', {}, - async () => + async (ctx) => await createCollabDocInfo( ctx, collabsInfo.result as Ref[], @@ -1421,7 +1444,7 @@ async function updateCollaboratorDoc ( const collaborators = await ctx.with( 'get-doc-collaborators', {}, - async () => await getDocCollaborators(ctx, doc, mixin, control) + async (ctx) => await getDocCollaborators(ctx, doc, mixin, control) ) res.push(getMixinTx(tx, control, collaborators)) res = res.concat( @@ -1442,11 +1465,11 @@ async function updateCollaboratorDoc ( await ctx.with( 'get-space-collabtxes', {}, - async () => await getSpaceCollabTxes(control, doc, tx, originTx, activityMessages, cache) + async (ctx) => await getSpaceCollabTxes(ctx, control, doc, tx, originTx, activityMessages, cache) ) ) res = res.concat( - await ctx.with('update-notify-context-space', {}, async () => await updateNotifyContextsSpace(control, tx)) + await ctx.with('update-notify-context-space', {}, async (ctx) => await updateNotifyContextsSpace(ctx, control, tx)) ) return res @@ -1546,7 +1569,7 @@ async function applyUserTxes ( for (const [user, txs] of map.entries()) { const account = (cache.get(user) as PersonAccount) ?? - (await ctx.with('get-person-account', {}, async () => await getPersonAccountById(user, control))) + ctx.withSync('get-person-account', {}, () => getPersonAccountById(user, control)) if (account !== undefined) { cache.set(account._id, account) @@ -1604,15 +1627,15 @@ async function updateCollaborators (ctx: MeasureContext, control: TriggerControl const contexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: objectId }) const addedInfo = await getUsersInfo(ctx, toAdd as Ref[], control) - for (const addedUser of addedInfo) { + for (const addedUser of addedInfo.values()) { const info = toReceiverInfo(hierarchy, addedUser) if (info === undefined) continue const context = getDocNotifyContext(control, contexts, objectId, info._id) if (context !== undefined) continue - await createNotifyContext(control, objectId, objectClass, objectSpace, info) + await createNotifyContext(ctx, control, objectId, objectClass, objectSpace, info) } - await removeContexts(contexts, removedCollaborators as Ref[], control) + await removeContexts(ctx, contexts, removedCollaborators as Ref[], control) return res } @@ -1630,7 +1653,7 @@ export async function createCollaboratorNotifications ( return await ctx.with( 'updateDerivedCollaborators', {}, - async () => await updateCollaborators(ctx, control, TxProcessor.extractTx(tx) as TxCUD) + async (ctx) => await updateCollaborators(ctx, control, TxProcessor.extractTx(tx) as TxCUD) ) } @@ -1643,7 +1666,7 @@ export async function createCollaboratorNotifications ( const res = await ctx.with( 'createCollaboratorDoc', {}, - async () => + async (ctx) => await createCollaboratorDoc(ctx, tx as TxCreateDoc, control, activityMessages, originTx ?? tx, cache) ) @@ -1654,14 +1677,14 @@ export async function createCollaboratorNotifications ( let res = await ctx.with( 'updateCollaboratorDoc', {}, - async () => + async (ctx) => await updateCollaboratorDoc(ctx, tx as TxUpdateDoc, control, originTx ?? tx, activityMessages, cache) ) res = res.concat( await ctx.with( 'updateCollaboratorMixin', {}, - async () => + async (ctx) => await updateCollaboratorsMixin( ctx, tx as TxMixin, @@ -1703,6 +1726,7 @@ export async function removeDocInboxNotifications (_id: Ref, co } export async function getCollaborators ( + ctx: MeasureContext, doc: Doc, control: TriggerControl, tx: TxCUD, @@ -1717,7 +1741,7 @@ export async function getCollaborators ( if (control.hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { return control.hierarchy.as(doc, notification.mixin.Collaborators).collaborators } else { - const collaborators = await getDocCollaborators(control.ctx, doc, mixin, control) + const collaborators = await getDocCollaborators(ctx, doc, mixin, control) res.push(getMixinTx(tx, control, collaborators)) return collaborators @@ -1821,7 +1845,7 @@ export default async () => ({ OnDocRemove }, function: { - IsUserInFieldValue: isUserInFieldValue, - IsUserEmployeeInFieldValue: isUserEmployeeInFieldValue + IsUserInFieldValueTypeMatch: isUserInFieldValueTypeMatch, + IsUserEmployeeInFieldValueTypeMatch: isUserEmployeeInFieldValueTypeMatch } }) diff --git a/server-plugins/notification-resources/src/utils.ts b/server-plugins/notification-resources/src/utils.ts index 9cc8a6df2e..db16ff0b48 100644 --- a/server-plugins/notification-resources/src/utils.ts +++ b/server-plugins/notification-resources/src/utils.ts @@ -12,24 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import notification, { - BaseNotificationType, - Collaborators, - CommonNotificationType, - NotificationContent, - notificationId, - NotificationProvider, - NotificationType -} from '@hcengineering/notification' -import serverCore, { TriggerControl } from '@hcengineering/server-core' +import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' import { Analytics } from '@hcengineering/analytics' -import contact, { formatName, PersonAccount } from '@hcengineering/contact' +import chunter, { ChatMessage } from '@hcengineering/chunter' +import contact, { formatName, PersonAccount, type Person, type PersonSpace } from '@hcengineering/contact' import core, { Account, Class, + concatLink, Doc, DocumentUpdate, + groupByArray, Hierarchy, + Markup, matchQuery, MixinUpdate, Ref, @@ -42,11 +37,23 @@ import core, { TxProcessor, TxRemoveDoc, TxUpdateDoc, + type IdMap, type MeasureContext, - Markup, - concatLink + type WithLookup } from '@hcengineering/core' +import notification, { + BaseNotificationType, + Collaborators, + CommonNotificationType, + NotificationContent, + notificationId, + NotificationProvider, + NotificationType, + type NotificationProviderSetting, + type NotificationTypeSetting +} from '@hcengineering/notification' import { getMetadata, getResource, IntlString, translate } from '@hcengineering/platform' +import serverCore, { TriggerControl } from '@hcengineering/server-core' import serverNotification, { getPersonAccountById, HTMLPresenter, @@ -55,28 +62,26 @@ import serverNotification, { SenderInfo, TextPresenter } from '@hcengineering/server-notification' -import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' import serverView from '@hcengineering/server-view' -import { workbenchId } from '@hcengineering/workbench' import { encodeObjectURI } from '@hcengineering/view' -import chunter, { ChatMessage } from '@hcengineering/chunter' +import { workbenchId } from '@hcengineering/workbench' import { NotifyResult } from './types' /** * @public */ -export async function isUserEmployeeInFieldValue ( +export function isUserEmployeeInFieldValueTypeMatch ( _: Tx, doc: Doc, - user: Ref, + user: Ref[], type: NotificationType, control: TriggerControl -): Promise { +): boolean { if (type.field === undefined) return false const value = (doc as any)[type.field] if (value == null) return false - const employee = (await control.modelDb.findAll(contact.class.PersonAccount, { _id: user as Ref }))[0] + const employee = control.modelDb.findAllSync(contact.class.PersonAccount, { _id: user[0] as Ref })[0] if (employee === undefined) return false if (Array.isArray(value)) { return value.includes(employee.person) @@ -88,12 +93,7 @@ export async function isUserEmployeeInFieldValue ( /** * @public */ -export async function isUserInFieldValue ( - _: Tx, - doc: Doc, - user: Ref, - type: NotificationType -): Promise { +export function isUserInFieldValueTypeMatch (_: Tx, doc: Doc, user: Ref[], type: NotificationType): boolean { if (type.field === undefined) { return false } @@ -104,7 +104,7 @@ export async function isUserInFieldValue ( return false } - return Array.isArray(value) ? value.includes(user) : value === user + return Array.isArray(value) ? user.some((it) => value.includes(it)) : user.includes(value) } export function replaceAll (str: string, find: string, replace: string): string { @@ -117,8 +117,9 @@ function escapeRegExp (str: string): string { export async function shouldNotifyCommon ( control: TriggerControl, - user: Ref, - typeId: Ref + user: Ref[], + typeId: Ref, + notificationControl: NotificationProviderControl ): Promise { const type = (await control.modelDb.findAll(notification.class.CommonNotificationType, { _id: typeId }))[0] @@ -130,7 +131,9 @@ export async function shouldNotifyCommon ( const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {}) for (const provider of providers) { - const allowed = await isAllowed(control, user as Ref, type, provider) + const allowed = user.some((user) => + isAllowed(control, user as Ref, type, provider, notificationControl) + ) if (allowed) { const cur = result.get(provider._id) ?? [] result.set(provider._id, [...cur, type]) @@ -140,17 +143,15 @@ export async function shouldNotifyCommon ( return result } -export async function isAllowed ( +export function isAllowed ( control: TriggerControl, receiver: Ref, type: BaseNotificationType, - provider: NotificationProvider -): Promise { - const providersSettings = await control.queryFind(control.ctx, notification.class.NotificationProviderSetting, { - space: core.space.Workspace - }) - const providerSetting = providersSettings.find( - ({ attachedTo, modifiedBy }) => attachedTo === provider._id && modifiedBy === receiver + provider: NotificationProvider, + notificationControl: NotificationProviderControl +): boolean { + const providerSetting = (notificationControl.byProvider.get(provider._id) ?? []).find( + ({ attachedTo, modifiedBy }) => modifiedBy === receiver ) if (providerSetting !== undefined && !providerSetting.enabled) { @@ -161,17 +162,13 @@ export async function isAllowed ( return false } - const providerDefaults = await control.modelDb.findAll(notification.class.NotificationProviderDefaults, {}) + const providerDefaults = control.modelDb.findAllSync(notification.class.NotificationProviderDefaults, {}) if (providerDefaults.some((it) => it.provider === provider._id && it.ignoredTypes.includes(type._id))) { return false } - - const typesSettings = await control.queryFind(control.ctx, notification.class.NotificationTypeSetting, { - space: core.space.Workspace - }) - const setting = typesSettings.find( - (it) => it.attachedTo === provider._id && it.type === type._id && it.modifiedBy === receiver + const setting = (notificationControl.settingsByProvider.get(provider._id) ?? []).find( + (it) => it.type === type._id && it.modifiedBy === receiver ) if (setting !== undefined) { @@ -192,29 +189,23 @@ export async function isShouldNotifyTx ( tx: TxCUD, originTx: TxCUD, object: Doc, - user: PersonAccount, + user: PersonAccount[], isOwn: boolean, isSpace: boolean, + notificationControl: NotificationProviderControl, docUpdateMessage?: DocUpdateMessage ): Promise { - const types = await getMatchedTypes( - control, - tx, - originTx, - isOwn, - isSpace, - docUpdateMessage?.attributeUpdates?.attrKey - ) - const modifiedAccount = await getPersonAccountById(tx.modifiedBy, control) + const types = getMatchedTypes(control, tx, originTx, isOwn, isSpace, docUpdateMessage?.attributeUpdates?.attrKey) + const modifiedAccount = getPersonAccountById(tx.modifiedBy, control) const result = new Map, BaseNotificationType[]>() - const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {}) + const providers = control.modelDb.findAllSync(notification.class.NotificationProvider, {}) for (const type of types) { if ( type.allowedForAuthor !== true && - (tx.modifiedBy === user._id || + (user.some((it) => tx.modifiedBy === it._id) || // Also check if we have different account for same user. - (user?.person !== undefined && user?.person === modifiedAccount?.person)) + (user?.[0].person !== undefined && user?.[0]?.person === modifiedAccount?.person)) ) { continue } @@ -222,12 +213,18 @@ export async function isShouldNotifyTx ( const mixin = control.hierarchy.as(type, serverNotification.mixin.TypeMatch) if (mixin.func !== undefined) { const f = await getResource(mixin.func) - const res = await f(tx, object, user._id, type, control) + const res = f( + tx, + object, + user.map((it) => it._id), + type, + control + ) if (!res) continue } } for (const provider of providers) { - const allowed = await isAllowed(control, user._id, type, provider) + const allowed = user.some((it) => isAllowed(control, it._id, type, provider, notificationControl)) if (allowed) { const cur = result.get(provider._id) ?? [] @@ -239,17 +236,17 @@ export async function isShouldNotifyTx ( return result } -async function getMatchedTypes ( +function getMatchedTypes ( control: TriggerControl, tx: TxCUD, originTx: TxCUD, isOwn: boolean, isSpace: boolean, field?: string -): Promise { - const allTypes = ( - await control.modelDb.findAll(notification.class.NotificationType, { ...(field !== undefined ? { field } : {}) }) - ).filter((p) => (isSpace ? p.spaceSubscribe === true : p.spaceSubscribe !== true)) +): NotificationType[] { + const allTypes = control.modelDb + .findAllSync(notification.class.NotificationType, { ...(field !== undefined ? { field } : {}) }) + .filter((p) => (isSpace ? p.spaceSubscribe === true : p.spaceSubscribe !== true)) const filtered: NotificationType[] = [] for (const type of allTypes) { if (isTypeMatched(control, type, tx, originTx, isOwn)) { @@ -300,6 +297,7 @@ function fieldUpdated (field: string, ops: DocumentUpdate | MixinUpdate | TxMixin ): Promise { @@ -312,7 +310,7 @@ export async function updateNotifyContextsSpace ( return [] } - const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { objectId: tx.objectId }) + const notifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, { objectId: tx.objectId }) return notifyContexts.map((value) => control.txFactory.createTxUpdateDoc(value._class, value.space, value._id, { @@ -421,7 +419,7 @@ function getNotificationPresenter (_class: Ref>, hierarchy: Hierarchy export async function getNotificationContent ( originTx: TxCUD, - targetUser: PersonAccount, + targetUser: PersonAccount[], sender: SenderInfo, object: Doc, control: TriggerControl @@ -440,7 +438,7 @@ export async function getNotificationContent ( if (notificationPresenter !== undefined) { const getFuillfillmentParams = await getResource(notificationPresenter.presenter) - const updateParams = await getFuillfillmentParams(object, originTx, targetUser._id, control) + const updateParams = await getFuillfillmentParams(object, originTx, targetUser[0]._id, control) title = updateParams.title body = updateParams.body data = updateParams.data @@ -474,33 +472,57 @@ export async function getUsersInfo ( ctx: MeasureContext, ids: Ref[], control: TriggerControl -): Promise<(ReceiverInfo | SenderInfo)[]> { - if (ids.length === 0) return [] - const accounts = await control.modelDb.findAll(contact.class.PersonAccount, { _id: { $in: ids } }) +): Promise, ReceiverInfo | SenderInfo>> { + if (ids.length === 0) return new Map() + const accounts = control.modelDb.findAllSync(contact.class.PersonAccount, { _id: { $in: ids } }) + const personIds = accounts.map((it) => it.person) + const personIdsMap = new Set(personIds) const accountById = toIdMap(accounts) - const persons = toIdMap( - await ctx.with( - 'find-persons', - {}, - async () => await control.findAll(contact.class.Person, { _id: { $in: personIds } }) + const persons: IdMap> = new Map() + const spaces = new Map, PersonSpace>() + + const p = await ctx.with('find-persons', {}, async (ctx) => + (await control.queryFind(ctx, contact.mixin.Employee, { active: { $in: [true, false] } }, {})).filter((it) => + personIdsMap.has(it._id) ) ) - const spaces = await ctx.with('find-person-spaces', {}, async () => { - const res = await control.findAll(contact.class.PersonSpace, { person: { $in: personIds } }) + for (const pp of p) { + persons.set(pp._id, pp) + } - return new Map(res.map((s) => [s.person, s])) - }) + const nonEmployee = personIds.filter((it) => !persons.has(it)) - return ids.map((_id) => { - const account = accountById.get(_id) - return { - _id, - account, - person: account !== undefined ? persons.get(account.person) : undefined, - space: account !== undefined ? spaces.get(account.person)?._id : undefined - } - }) + const p2 = await ctx.with('find-persons', {}, async (ctx) => + (await control.queryFind(ctx, contact.class.Person, { _id: { $in: Array.from(nonEmployee) } }, {})).filter((it) => + personIdsMap.has(it._id) + ) + ) + for (const pp of p2) { + persons.set(pp._id, pp) + } + + const res = await ctx.with('find-person-spaces', {}, async (ctx) => + (await control.queryFind(ctx, contact.class.PersonSpace, {}, {})).filter((it) => personIdsMap.has(it.person)) + ) + for (const r of res) { + spaces.set(r.person, r) + } + + return new Map( + ids.map((_id) => { + const account = accountById.get(_id) + return [ + _id, + { + _id, + account, + person: account !== undefined ? persons.get(account.person) : undefined, + space: account !== undefined ? spaces.get(account.person)?._id : undefined + } + ] + }) + ) } export function toReceiverInfo (hierarchy: Hierarchy, info?: SenderInfo | ReceiverInfo): ReceiverInfo | undefined { @@ -611,3 +633,36 @@ export async function messageToMarkup (control: TriggerControl, message: Activit return undefined } +export class NotificationProviderControl { + public byProvider: Map, NotificationProviderSetting[]> + public settingsByProvider: Map, NotificationTypeSetting[]> + constructor ( + readonly providersSettings: NotificationProviderSetting[], + readonly typesSettings: NotificationTypeSetting[] + ) { + this.byProvider = groupByArray(providersSettings, (it) => it.attachedTo) + this.settingsByProvider = groupByArray(typesSettings, (it) => it.attachedTo) + } +} +const notificationProvidersKey = 'notification_provider_settings' +const typesSettingsKey = 'notification_type_settings' +export async function getNotificationProviderControl ( + ctx: MeasureContext, + control: TriggerControl +): Promise { + let providersSettings: NotificationProviderSetting[] = control.contextCache.get(notificationProvidersKey) + if (providersSettings === undefined) { + providersSettings = await control.queryFind(ctx, notification.class.NotificationProviderSetting, { + space: core.space.Workspace + }) + control.contextCache.set(notificationProvidersKey, providersSettings) + } + let typesSettings: NotificationTypeSetting[] = control.contextCache.get(typesSettingsKey) + if (typesSettings === undefined) { + typesSettings = await control.queryFind(ctx, notification.class.NotificationTypeSetting, { + space: core.space.Workspace + }) + control.contextCache.set(typesSettingsKey, typesSettings) + } + return new NotificationProviderControl(providersSettings, typesSettings) +} diff --git a/server-plugins/notification/src/index.ts b/server-plugins/notification/src/index.ts index 0d252646df..8b49bd5aaa 100644 --- a/server-plugins/notification/src/index.ts +++ b/server-plugins/notification/src/index.ts @@ -14,6 +14,7 @@ // limitations under the License. // +import { ActivityMessage } from '@hcengineering/activity' import contact, { Employee, Person, PersonAccount, PersonSpace } from '@hcengineering/contact' import { Account, Class, Doc, Mixin, Ref, Tx, TxCUD } from '@hcengineering/core' import { @@ -25,7 +26,6 @@ import { } from '@hcengineering/notification' import { Metadata, Plugin, Resource, plugin } from '@hcengineering/platform' import type { TriggerControl, TriggerFunc } from '@hcengineering/server-core' -import { ActivityMessage } from '@hcengineering/activity' /** * @public @@ -35,37 +35,13 @@ export const serverNotificationId = 'server-notification' as Plugin /** * @public */ -export async function getPersonAccount ( - person: Ref, - control: TriggerControl -): Promise { - const account = ( - await control.modelDb.findAll( - contact.class.PersonAccount, - { - person - }, - { limit: 1 } - ) - )[0] - return account -} - -/** - * @public - */ -export async function getPersonAccountById ( - _id: Ref, - control: TriggerControl -): Promise { - const account = ( - await control.modelDb.findAll( - contact.class.PersonAccount, - { - _id: _id as Ref - }, - { limit: 1 } - ) +export function getPersonAccountById (_id: Ref, control: TriggerControl): PersonAccount | undefined { + const account = control.modelDb.findAllSync( + contact.class.PersonAccount, + { + _id: _id as Ref + }, + { limit: 1 } )[0] return account } @@ -109,7 +85,7 @@ export interface TextPresenter extends Class { * @public */ export type TypeMatchFunc = Resource< -(tx: Tx, doc: Doc, user: Ref, type: NotificationType, control: TriggerControl) => Promise +(tx: Tx, doc: Doc, user: Ref[], type: NotificationType, control: TriggerControl) => boolean > /** @@ -192,7 +168,7 @@ export default plugin(serverNotificationId, { OnDocRemove: '' as Resource }, function: { - IsUserInFieldValue: '' as TypeMatchFunc, - IsUserEmployeeInFieldValue: '' as TypeMatchFunc + IsUserInFieldValueTypeMatch: '' as TypeMatchFunc, + IsUserEmployeeInFieldValueTypeMatch: '' as TypeMatchFunc } }) diff --git a/server-plugins/request-resources/src/index.ts b/server-plugins/request-resources/src/index.ts index 6aa6439a23..68eead4aaa 100644 --- a/server-plugins/request-resources/src/index.ts +++ b/server-plugins/request-resources/src/index.ts @@ -13,7 +13,17 @@ // limitations under the License. // -import core, { Doc, Tx, TxCUD, TxCollectionCUD, TxCreateDoc, TxUpdateDoc, TxProcessor, Ref } from '@hcengineering/core' +import core, { + Doc, + Tx, + TxCUD, + TxCollectionCUD, + TxCreateDoc, + TxUpdateDoc, + TxProcessor, + Ref, + type MeasureContext +} from '@hcengineering/core' import request, { Request, RequestStatus } from '@hcengineering/request' import { getResource, translate } from '@hcengineering/platform' import type { TriggerControl } from '@hcengineering/server-core' @@ -25,7 +35,8 @@ import { getCollaborators, getTextPresenter, getUsersInfo, - toReceiverInfo + toReceiverInfo, + getNotificationProviderControl } from '@hcengineering/server-notification-resources' import { PersonAccount } from '@hcengineering/contact' @@ -46,7 +57,7 @@ export async function OnRequest (tx: Tx, control: TriggerControl): Promise let res: Tx[] = [] - res = res.concat(await getRequestNotificationTx(ptx, control)) + res = res.concat(await getRequestNotificationTx(control.ctx, ptx, control)) if (ptx.tx._class === core.class.TxUpdateDoc) { res = res.concat(await OnRequestUpdate(ptx, control)) @@ -121,7 +132,11 @@ async function getRequest (tx: TxCUD, control: TriggerControl): Promise } // We need request-specific logic to attach a activity message on request create/update to parent, but use request collaborators for notifications -async function getRequestNotificationTx (tx: TxCollectionCUD, control: TriggerControl): Promise { +async function getRequestNotificationTx ( + ctx: MeasureContext, + tx: TxCollectionCUD, + control: TriggerControl +): Promise { const request = await getRequest(tx.tx, control) if (request === undefined) return [] @@ -140,7 +155,7 @@ async function getRequestNotificationTx (tx: TxCollectionCUD, cont const messages = messagesTxes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc) ) - const collaborators = await getCollaborators(request, control, tx.tx, res) + const collaborators = await getCollaborators(control.ctx, request, control, tx.tx, res) if (collaborators.length === 0) return res @@ -148,18 +163,18 @@ async function getRequestNotificationTx (tx: TxCollectionCUD, cont objectId: doc._id }) const usersInfo = await getUsersInfo(control.ctx, [...collaborators, tx.modifiedBy] as Ref[], control) - const senderInfo = usersInfo.find(({ _id }) => _id === tx.modifiedBy) ?? { + const senderInfo = usersInfo.get(tx.modifiedBy) ?? { _id: tx.modifiedBy } + const notificationControl = await getNotificationProviderControl(ctx, control) + for (const target of collaborators) { - const targetInfo = toReceiverInfo( - control.hierarchy, - usersInfo.find(({ _id }) => _id === target) - ) + const targetInfo = toReceiverInfo(control.hierarchy, usersInfo.get(target)) if (targetInfo === undefined) continue const txes = await getNotificationTxes( + ctx, control, request, tx.tx, @@ -168,7 +183,8 @@ async function getRequestNotificationTx (tx: TxCollectionCUD, cont senderInfo, { isOwn: true, isSpace: false, shouldUpdateTimestamp: true }, notifyContexts, - messages + messages, + notificationControl ) res.push(...txes) } diff --git a/server-plugins/telegram-resources/src/index.ts b/server-plugins/telegram-resources/src/index.ts index ffed70de82..e442fc31f1 100644 --- a/server-plugins/telegram-resources/src/index.ts +++ b/server-plugins/telegram-resources/src/index.ts @@ -90,13 +90,13 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise /** * @public */ -export async function IsIncomingMessage ( +export function IsIncomingMessageTypeMatch ( tx: Tx, doc: Doc, - user: Ref, + user: Ref[], type: NotificationType, control: TriggerControl -): Promise { +): boolean { const message = TxProcessor.createDoc2Doc(TxProcessor.extractTx(tx) as TxCreateDoc) return message.incoming && message.sendOn > (doc.createdOn ?? doc.modifiedOn) } @@ -280,7 +280,7 @@ export default async () => ({ OnMessageCreate }, function: { - IsIncomingMessage, + IsIncomingMessageTypeMatch, FindMessages, GetCurrentEmployeeTG, GetIntegrationOwnerTG, diff --git a/server-plugins/telegram/src/index.ts b/server-plugins/telegram/src/index.ts index 1b57fc9fbd..9a675102a1 100644 --- a/server-plugins/telegram/src/index.ts +++ b/server-plugins/telegram/src/index.ts @@ -35,7 +35,7 @@ export default plugin(serverTelegramId, { OnMessageCreate: '' as Resource }, function: { - IsIncomingMessage: '' as TypeMatchFunc, + IsIncomingMessageTypeMatch: '' as TypeMatchFunc, FindMessages: '' as Resource, GetCurrentEmployeeTG: '' as Resource, GetIntegrationOwnerTG: '' as Resource, diff --git a/server-plugins/time-resources/src/index.ts b/server-plugins/time-resources/src/index.ts index f34faa32a5..c24bb09b05 100644 --- a/server-plugins/time-resources/src/index.ts +++ b/server-plugins/time-resources/src/index.ts @@ -34,17 +34,18 @@ import core, { import notification, { CommonInboxNotification } from '@hcengineering/notification' import { getResource } from '@hcengineering/platform' import type { TriggerControl } from '@hcengineering/server-core' +import { ReceiverInfo, SenderInfo } from '@hcengineering/server-notification' import { getCommonNotificationTxes, getNotificationContent, + getNotificationProviderControl, isShouldNotifyTx } from '@hcengineering/server-notification-resources' +import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time' import task, { makeRank } from '@hcengineering/task' import { jsonToMarkup, nodeDoc, nodeParagraph, nodeText } from '@hcengineering/text' -import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker' -import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time' import time, { ProjectToDo, ToDo, ToDoPriority, TodoAutomationHelper, WorkSlot } from '@hcengineering/time' -import { ReceiverInfo, SenderInfo } from '@hcengineering/server-notification' +import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker' /** * @public @@ -179,9 +180,9 @@ export async function OnToDoCreate (tx: TxCUD, control: TriggerControl): Pr } const todo = TxProcessor.createDoc2Doc(createTx) - const account = await getPersonAccount(todo.user, control) + const account = control.modelDb.getAccountByPersonId(todo.user) as PersonAccount[] - if (account === undefined) { + if (account.length === 0) { return [] } @@ -189,23 +190,24 @@ export async function OnToDoCreate (tx: TxCUD, control: TriggerControl): Pr if (object === undefined) return [] const person = ( - await control.findAll(contact.mixin.Employee, { _id: account.person as Ref, active: true }, { limit: 1 }) + await control.findAll(contact.mixin.Employee, { _id: todo.user as Ref, active: true }, { limit: 1 }) )[0] if (person === undefined) return [] - const personSpace = (await control.findAll(contact.class.PersonSpace, { person: account.person }, { limit: 1 }))[0] + const personSpace = (await control.findAll(contact.class.PersonSpace, { person: todo.user }, { limit: 1 }))[0] if (personSpace === undefined) return [] + // TODO: Select a proper account const receiverInfo: ReceiverInfo = { - _id: account._id, - account, + _id: account[0]._id, + account: account[0], person, space: personSpace._id } - const senderAccount = await control.modelDb.findOne(contact.class.PersonAccount, { + const senderAccount = control.modelDb.findAllSync(contact.class.PersonAccount, { _id: tx.modifiedBy as Ref - }) + })[0] const senderPerson = senderAccount !== undefined ? (await control.findAll(contact.class.Person, { _id: senderAccount.person }))[0] @@ -216,8 +218,8 @@ export async function OnToDoCreate (tx: TxCUD, control: TriggerControl): Pr account: senderAccount, person: senderPerson } - - const notifyResult = await isShouldNotifyTx(control, createTx, tx, todo, account, true, false) + const notificationControl = await getNotificationProviderControl(control.ctx, control) + const notifyResult = await isShouldNotifyTx(control, createTx, tx, todo, account, true, false, notificationControl) const content = await getNotificationContent(tx, account, senderInfo, todo, control) const data: Partial> = { ...content, @@ -229,6 +231,7 @@ export async function OnToDoCreate (tx: TxCUD, control: TriggerControl): Pr } const txes = await getCommonNotificationTxes( + control.ctx, control, object, data, @@ -465,31 +468,18 @@ async function createIssueHandler (issue: Issue, control: TriggerControl): Promi return [] } -async function getPersonAccount (person: Ref, control: TriggerControl): Promise { - const account = ( - await control.modelDb.findAll( - contact.class.PersonAccount, - { - person - }, - { limit: 1 } - ) - )[0] - return account -} - async function getIssueToDoData ( issue: Issue, user: Ref, control: TriggerControl ): Promise | undefined> { - const acc = await getPersonAccount(user, control) - if (acc === undefined) return + const acc = control.modelDb.getAccountByPersonId(user) as PersonAccount[] + if (acc.length === 0) return const firstTodoItem = ( await control.findAll( time.class.ToDo, { - user: acc.person, + user: { $in: acc.map((it) => it.person) }, doneOn: null }, { @@ -506,7 +496,7 @@ async function getIssueToDoData ( priority: ToDoPriority.NoPriority, visibility: 'public', title: issue.title, - user: acc.person, + user, rank } return data diff --git a/server-plugins/training-resources/src/functions/TrainingRequestNotificationTypeMatch.ts b/server-plugins/training-resources/src/functions/TrainingRequestNotificationTypeMatch.ts index d8cbeb4c5f..fb91307749 100644 --- a/server-plugins/training-resources/src/functions/TrainingRequestNotificationTypeMatch.ts +++ b/server-plugins/training-resources/src/functions/TrainingRequestNotificationTypeMatch.ts @@ -13,25 +13,25 @@ // limitations under the License. // +import contact, { type Employee, type PersonAccount } from '@hcengineering/contact' import { type Account, type Ref, type TxCUD } from '@hcengineering/core' import type { NotificationType } from '@hcengineering/notification' import type { TriggerControl } from '@hcengineering/server-core' import type { TrainingRequest } from '@hcengineering/training' -import contact, { type Employee, type PersonAccount } from '@hcengineering/contact' import { isTxCreateDoc } from '../utils/isTxCreateDoc' import { isTxUpdateDoc } from '../utils/isTxUpdateDoc' -export async function TrainingRequestNotificationTypeMatch ( +export function TrainingRequestNotificationTypeMatch ( tx: TxCUD, doc: TrainingRequest, - user: Ref, + user: Ref[], type: NotificationType, control: TriggerControl -): Promise { +): boolean { if (isTxCreateDoc(tx)) { - const employeeRef: Ref = ( - await control.modelDb.findAll(contact.class.PersonAccount, { _id: user as Ref }) - )[0]?.person as Ref + const employeeRef: Ref = control.modelDb.findAllSync(contact.class.PersonAccount, { + _id: user[0] as Ref + })[0]?.person as Ref return doc.trainees.includes(employeeRef) } @@ -41,9 +41,9 @@ export async function TrainingRequestNotificationTypeMatch ( return false } - const employeeRef: Ref = ( - await control.modelDb.findAll(contact.class.PersonAccount, { _id: user as Ref }) - )[0]?.person as Ref + const employeeRef: Ref = control.modelDb.findAllSync(contact.class.PersonAccount, { + _id: user[0] as Ref + })[0]?.person as Ref const newTrainees = typeof pushed === 'object' ? pushed.$each : [pushed] return newTrainees.includes(employeeRef) } diff --git a/server/account/src/operations.ts b/server/account/src/operations.ts index d85508644e..a13ba1b150 100644 --- a/server/account/src/operations.ts +++ b/server/account/src/operations.ts @@ -1983,7 +1983,7 @@ async function createPersonAccount ( client ?? (await connect(getEndpoint(ctx, workspaceInfo, EndpointKind.Internal), getWorkspaceId(workspace, productId))) try { - const ops = new TxOperations(connection, core.account.System) + const ops = new TxOperations(connection, core.account.System).apply('create-person' + generateId()) const name = combineName(account.first, account.last) // Check if PersonAccount is not exists @@ -2033,6 +2033,7 @@ async function createPersonAccount ( } } } + await ops.commit() } finally { if (client === undefined) { await connection.close() diff --git a/server/core/src/server/storage.ts b/server/core/src/server/storage.ts index 8528895b60..5e08471945 100644 --- a/server/core/src/server/storage.ts +++ b/server/core/src/server/storage.ts @@ -879,12 +879,10 @@ export class TServerStorage implements ServerStorage { ctx: SessionOperationContext, findAll: ServerStorage['findAll'] ): Promise { - derived.sort((a, b) => a.modifiedOn - b.modifiedOn) - - await ctx.with('derived-route-tx', {}, (ctx) => this.routeTx(ctx, ...derived)) - const nestedTxes: Tx[] = [] if (derived.length > 0) { + derived.sort((a, b) => a.modifiedOn - b.modifiedOn) + await ctx.with('derived-route-tx', {}, (ctx) => this.routeTx(ctx, ...derived)) nestedTxes.push(...(await this.processDerived(ctx, derived, findAll))) } @@ -992,6 +990,7 @@ export class TServerStorage implements ServerStorage { const onEnds: (() => void)[] = [] const result: TxResult[] = [] const derived: Tx[] = [...txes].filter((it) => it._class !== core.class.TxApplyIf) + let txPromise: Promise | undefined try { this.fillTxes(txes, txToStore, modelTx, txToProcess, applyTxes) @@ -1023,7 +1022,7 @@ export class TServerStorage implements ServerStorage { await this.modelDb.tx(tx) } if (txToStore.length > 0) { - await ctx.with( + txPromise = ctx.with( 'domain-tx', {}, async (ctx) => await this.getAdapter(DOMAIN_TX, true).tx(ctx.ctx, ...txToStore), @@ -1034,9 +1033,8 @@ export class TServerStorage implements ServerStorage { ) } if (txToProcess.length > 0) { - result.push(...(await ctx.with('routeTx', {}, (ctx) => this.routeTx(ctx, ...txToProcess))), { - count: txToProcess.length - }) + const routerResult = await ctx.with('routeTx', {}, (ctx) => this.routeTx(ctx, ...txToProcess)) + result.push(...routerResult, { count: txToProcess.length }) // invoke triggers and store derived objects derived.push(...(await this.processDerived(ctx, txToProcess, _findAll))) } @@ -1053,6 +1051,10 @@ export class TServerStorage implements ServerStorage { { count: txToProcess.length + derived.length } ) } + if (txPromise !== undefined) { + // Wait for main Tx to be stored + await txPromise + } } catch (err: any) { ctx.ctx.error('error process tx', { error: err }) Analytics.handleError(err) diff --git a/server/mongo/src/storage.ts b/server/mongo/src/storage.ts index a5971e6887..051af5249c 100644 --- a/server/mongo/src/storage.ts +++ b/server/mongo/src/storage.ts @@ -16,7 +16,6 @@ import core, { DOMAIN_MODEL, DOMAIN_TX, - RateLimiter, SortingOrder, TxProcessor, addOperation, @@ -76,6 +75,7 @@ import { createHash } from 'crypto' import { type AbstractCursor, type AnyBulkWriteOperation, + type BulkWriteResult, type Collection, type Db, type Document, @@ -141,9 +141,6 @@ export interface DbAdapterOptions { abstract class MongoAdapterBase implements DbAdapter { _db: DBCollectionHelper - findRateLimit = new RateLimiter(parseInt(process.env.FIND_RLIMIT ?? '1000')) - rateLimit = new RateLimiter(parseInt(process.env.TX_RLIMIT ?? '5')) - handlers: DbAdapterHandler[] = [] on (handler: DbAdapterHandler): void { @@ -706,130 +703,119 @@ abstract class MongoAdapterBase implements DbAdapter { const stTime = Date.now() const mongoQuery = this.translateQuery(_class, query, options) const fQuery = { ...mongoQuery.base, ...mongoQuery.lookup } - return await addOperation( - ctx, - 'find-all', - {}, - async () => - await this.findRateLimit.exec(async () => { - const st = Date.now() - let result: FindResult - const domain = options?.domain ?? this.hierarchy.getDomain(_class) - if ( - options != null && - (options?.lookup != null || this.isEnumSort(_class, options) || this.isRulesSort(options)) - ) { - return await this.findWithPipeline(ctx, domain, _class, query, options, stTime) - } - const coll = this.collection(domain) - - if (options?.limit === 1 || typeof query._id === 'string') { - // Skip sort/projection/etc. - return await ctx.with( - 'find-one', - { domain }, - async (ctx) => { - const findOptions: MongoFindOptions = {} - - if (options?.sort !== undefined) { - findOptions.sort = this.collectSort(options, _class) - } - if (options?.projection !== undefined) { - findOptions.projection = this.calcProjection(options, _class) - } - - let doc: WithId | null - - if (typeof fQuery._id === 'string') { - doc = await coll.findOne({ _id: fQuery._id }, findOptions) - if (doc != null && matchQuery([doc as unknown as Doc], query, _class, this.hierarchy).length === 0) { - doc = null - } - } else { - doc = await coll.findOne(fQuery, findOptions) - } - - let total = -1 - if (options?.total === true) { - total = await coll.countDocuments({ ...mongoQuery.base, ...mongoQuery.lookup }) - } - if (doc != null) { - return toFindResult([this.stripHash(doc as unknown as T) as T], total) - } - return toFindResult([], total) - }, - { domain, mongoQuery, _idOnly: typeof fQuery._id === 'string' } - ) - } - - let cursor = coll.find(fQuery) - - if (options?.projection !== undefined) { - const projection = this.calcProjection(options, _class) - if (projection != null) { - cursor = cursor.project(projection) - } - } - let total: number = -1 - if (options != null) { - if (options.sort !== undefined) { - const sort = this.collectSort(options, _class) - if (sort !== undefined) { - cursor = cursor.sort(sort) - } - } - if (options.limit !== undefined || typeof query._id === 'string') { - if (options.total === true) { - total = await coll.countDocuments(fQuery) - } - cursor = cursor.limit(options.limit ?? 1) - } - } - // Error in case of timeout - try { - let res: T[] = [] - await ctx.with( - 'find-all', - {}, - async (ctx) => { - res = await toArray(cursor) - }, - () => ({ - size: res.length, - queueTime: stTime - st, - mongoQuery, - options, - domain - }) - ) - if (options?.total === true && options?.limit === undefined) { - total = res.length - } - result = toFindResult(this.stripHash(res) as T[], total) - } catch (e) { - console.error('error during executing cursor in findAll', _class, cutObjectArray(query), options, e) - throw e - } - - const edTime = Date.now() - if (edTime - st > 1000 || st - stTime > 1000) { - ctx.error('FindAll', { - time: edTime - st, - _class, - query: fQuery, - options, - queueTime: st - stTime - }) - } - this.handleEvent(domain, 'read', result.length) - return result - }), - { - _class, - query: fQuery, - options + return await addOperation(ctx, 'find-all', {}, async () => { + const st = Date.now() + let result: FindResult + const domain = options?.domain ?? this.hierarchy.getDomain(_class) + if ( + options != null && + (options?.lookup != null || this.isEnumSort(_class, options) || this.isRulesSort(options)) + ) { + return await this.findWithPipeline(ctx, domain, _class, query, options, stTime) } - ) + const coll = this.collection(domain) + + if (options?.limit === 1 || typeof query._id === 'string') { + // Skip sort/projection/etc. + return await ctx.with( + 'find-one', + { domain }, + async (ctx) => { + const findOptions: MongoFindOptions = {} + + if (options?.sort !== undefined) { + findOptions.sort = this.collectSort(options, _class) + } + if (options?.projection !== undefined) { + findOptions.projection = this.calcProjection(options, _class) + } + + let doc: WithId | null + + if (typeof fQuery._id === 'string') { + doc = await coll.findOne({ _id: fQuery._id }, findOptions) + if (doc != null && matchQuery([doc as unknown as Doc], query, _class, this.hierarchy).length === 0) { + doc = null + } + } else { + doc = await coll.findOne(fQuery, findOptions) + } + + let total = -1 + if (options?.total === true) { + total = await coll.countDocuments({ ...mongoQuery.base, ...mongoQuery.lookup }) + } + if (doc != null) { + return toFindResult([this.stripHash(doc as unknown as T) as T], total) + } + return toFindResult([], total) + }, + { domain, mongoQuery, _idOnly: typeof fQuery._id === 'string' } + ) + } + + let cursor = coll.find(fQuery) + + if (options?.projection !== undefined) { + const projection = this.calcProjection(options, _class) + if (projection != null) { + cursor = cursor.project(projection) + } + } + let total: number = -1 + if (options != null) { + if (options.sort !== undefined) { + const sort = this.collectSort(options, _class) + if (sort !== undefined) { + cursor = cursor.sort(sort) + } + } + if (options.limit !== undefined || typeof query._id === 'string') { + if (options.total === true) { + total = await coll.countDocuments(fQuery) + } + cursor = cursor.limit(options.limit ?? 1) + } + } + // Error in case of timeout + try { + let res: T[] = [] + await ctx.with( + 'find-all', + {}, + async (ctx) => { + res = await toArray(cursor) + }, + () => ({ + size: res.length, + queueTime: stTime - st, + mongoQuery, + options, + domain + }) + ) + if (options?.total === true && options?.limit === undefined) { + total = res.length + } + result = toFindResult(this.stripHash(res) as T[], total) + } catch (e) { + console.error('error during executing cursor in findAll', _class, cutObjectArray(query), options, e) + throw e + } + + const edTime = Date.now() + if (edTime - st > 1000 || st - stTime > 1000) { + ctx.error('FindAll', { + time: edTime - st, + _class, + query: fQuery, + options, + queueTime: st - stTime + }) + } + this.handleEvent(domain, 'read', result.length) + return result + }) } private collectSort( @@ -1129,13 +1115,14 @@ class MongoAdapter extends MongoAdapterBase { if (bulk.length === 0) { return } + const promises: Promise[] = [] for (const [domain, ops] of bulk) { if (ops === undefined || ops.length === 0) { continue } const coll = this.db.collection(domain) - await this.rateLimit.exec(() => + promises.push( addOperation( ctx, 'bulk-write', @@ -1146,8 +1133,7 @@ class MongoAdapter extends MongoAdapterBase { { domain }, () => coll.bulkWrite(ops, { - ordered: false, - writeConcern: { w: 0 } + ordered: false }), { domain, @@ -1157,6 +1143,7 @@ class MongoAdapter extends MongoAdapterBase { ) ) } + await Promise.all(promises) } async pushBulk (ctx: MeasureContext, domain: Domain, ops: AnyBulkWriteOperation[]): Promise { @@ -1166,6 +1153,8 @@ class MongoAdapter extends MongoAdapterBase { } else { this.bulkOps.set(domain, ops) } + // We need to wait next cycle to send request + await new Promise((resolve) => setImmediate(resolve)) await this._pushBulk(ctx) } @@ -1528,6 +1517,10 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter { const txes = this.txBulk this.txBulk = [] + if (txes.length === 0) { + return + } + const opName = txes.length === 1 ? 'tx-one' : 'tx' await addOperation( ctx, @@ -1541,8 +1534,7 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter { this.txCollection().insertMany( txes.map((it) => translateDoc(it)), { - ordered: false, - writeConcern: { w: 0 } + ordered: false } ), { @@ -1561,6 +1553,9 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter { return [] } this.txBulk.push(...tx) + + // We need to wait next cycle to send request + await new Promise((resolve) => setImmediate(resolve)) await this._bulkTx(ctx) return [] } diff --git a/templates/package/package.json b/templates/package/package.json index 5516160784..557d351390 100644 --- a/templates/package/package.json +++ b/templates/package/package.json @@ -27,8 +27,8 @@ "compression-webpack-plugin": "^10.0.0", "html-webpack-plugin": "^5.5.0", "fork-ts-checker-webpack-plugin": "~7.3.0", - "update-browserslist-db": "~1.0.11", - "browserslist": "4.21.5", + "update-browserslist-db": "^1.1.0", + "browserslist": "^4.23.3", "esbuild": "^0.20.0", "esbuild-loader": "^4.0.3", "typescript": "^5.3.3", diff --git a/tests/sanity/tests/model/channel-page.ts b/tests/sanity/tests/model/channel-page.ts index d4c898ea3d..62e1a7ebd7 100644 --- a/tests/sanity/tests/model/channel-page.ts +++ b/tests/sanity/tests/model/channel-page.ts @@ -27,6 +27,9 @@ export class ChannelPage extends CommonPage { readonly chooseChannel = (channel: string): Locator => this.page.getByRole('button', { name: channel }) readonly closePopupWindow = (): Locator => this.page.locator('.notifyPopup button[data-id="btnNotifyClose"]') readonly openAddMemberToChannel = (userName: string): Locator => this.page.getByRole('button', { name: userName }) + readonly addMemberToChannelTableButton = (userName: string): Locator => + this.page.locator('.antiTable-body__row').getByText(userName) + readonly addMemberToChannelButton = (userName: string): Locator => this.page.getByText(userName) readonly joinChannelButton = (): Locator => this.page.getByRole('button', { name: 'Join' }) readonly addEmojiButton = (): Locator => @@ -204,7 +207,7 @@ export class ChannelPage extends CommonPage { } async clickOnUser (user: string): Promise { - await this.addMemberToChannelButton(user).click() + await this.addMemberToChannelTableButton(user).click() } async addMemberToChannel (user: string): Promise {