diff --git a/.version b/.version index 1e17a1ebab..e8be64cc0f 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.1.175 +0.1.176 diff --git a/bun.lock b/bun.lock index b988aadafe..0d70c2ac6b 100644 --- a/bun.lock +++ b/bun.lock @@ -11,13 +11,13 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.4.0", "prettier": "^3.5.3", - "turbo": "^2.5.2", + "turbo": "^2.5.3", "typescript-eslint": "^8.32.0", }, }, "packages/client-query": { "name": "@hcengineering/communication-client-query", - "version": "0.1.174-beta.2", + "version": "0.1.0", "dependencies": { "@hcengineering/communication-query": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", @@ -33,7 +33,7 @@ }, "packages/cockroach": { "name": "@hcengineering/communication-cockroach", - "version": "0.1.174-beta.2", + "version": "0.1.0", "dependencies": { "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-shared": "workspace:*", @@ -50,7 +50,7 @@ }, "packages/query": { "name": "@hcengineering/communication-query", - "version": "0.1.174-beta.2", + "version": "0.1.0", "dependencies": { "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-shared": "workspace:*", @@ -69,12 +69,12 @@ }, "packages/rest-client": { "name": "@hcengineering/communication-rest-client", - "version": "0.1.174-beta.2", + "version": "0.1.0", "dependencies": { "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-shared": "workspace:*", "@hcengineering/communication-types": "workspace:*", - "@hcengineering/core": "^0.7.28", + "@hcengineering/core": "^0.7.88", "snappyjs": "^0.7.0", }, "devDependencies": { @@ -87,10 +87,10 @@ }, "packages/sdk-types": { "name": "@hcengineering/communication-sdk-types", - "version": "0.1.174-beta.2", + "version": "0.1.0", "dependencies": { "@hcengineering/communication-types": "workspace:*", - "@hcengineering/core": "^0.7.28", + "@hcengineering/core": "^0.7.88", }, "devDependencies": { "@types/bun": "^1.1.14", @@ -101,15 +101,16 @@ }, "packages/server": { "name": "@hcengineering/communication-server", - "version": "0.1.174-beta.2", + "version": "0.1.0", "dependencies": { - "@hcengineering/account-client": "^0.7.28", + "@hcengineering/account-client": "^0.7.88", "@hcengineering/communication-cockroach": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-types": "workspace:*", - "@hcengineering/core": "^0.7.28", - "@hcengineering/platform": "^0.7.28", - "@hcengineering/server-token": "^0.7.28", + "@hcengineering/communication-yaml": "workspace:*", + "@hcengineering/core": "^0.7.88", + "@hcengineering/platform": "^0.7.88", + "@hcengineering/server-token": "^0.7.88", "zod": "^3.24.2", }, "devDependencies": { @@ -124,7 +125,7 @@ }, "packages/shared": { "name": "@hcengineering/communication-shared", - "version": "0.1.174-beta.2", + "version": "0.1.0", "dependencies": { "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-types": "workspace:*", @@ -138,11 +139,11 @@ }, "packages/types": { "name": "@hcengineering/communication-types", - "version": "0.1.174-beta.2", + "version": "0.1.0", "dependencies": { - "@hcengineering/card": "^0.7.28", - "@hcengineering/core": "^0.7.28", - "@hcengineering/tags": "^0.7.28", + "@hcengineering/card": "^0.7.88", + "@hcengineering/core": "^0.7.88", + "@hcengineering/tags": "^0.7.88", }, "devDependencies": { "@types/bun": "^1.1.14", @@ -153,7 +154,7 @@ }, "packages/yaml": { "name": "@hcengineering/communication-yaml", - "version": "0.1.174-beta.2", + "version": "0.1.0", "dependencies": { "@hcengineering/communication-shared": "workspace:*", "@hcengineering/communication-types": "workspace:*", @@ -201,11 +202,11 @@ "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.0", "", { "dependencies": { "tslib": "2" } }, "sha512-4rB4g+3hESy1bHSBG3tDFaMY2CH67iT7yne1e+0CLTsGLDcmoEWWpJjjpWVaYgYfYuohIRuo0E+N536gd2ZHZA=="], - "@hcengineering/account-client": ["@hcengineering/account-client@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/account-client/0.7.28/32c9bc0dd297f00f4d5461467f25e072d01b9c76", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28" } }, "sha512-k1dkioX+jYrLyAXLLLsBYVCdZKf3WjhnDHw/yeAQgLQQ/9W2ffLtDMZ8lYWV3XMQ3eWrUCodtTDOiAQgXd/LEQ=="], + "@hcengineering/account-client": ["@hcengineering/account-client@0.7.88", "https://npm.pkg.github.com/download/@hcengineering/account-client/0.7.88/57caa95e62fecc4ca22a32c76bcdc900b0384f04", { "dependencies": { "@hcengineering/core": "^0.7.88", "@hcengineering/platform": "^0.7.88" } }, "sha512-CZWOJT/JZAUy+FuYuX0QcJQYDnVDNAB0QHU4RxKc7gSlYAYleibL7qJC0vRGL4rNkvDVZfJMKDPUQW4AauGqtA=="], - "@hcengineering/analytics": ["@hcengineering/analytics@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/analytics/0.7.28/a8faf3d575bb28b9682b659495e738f8e0ca596e", { "dependencies": { "@hcengineering/platform": "^0.7.28" } }, "sha512-+CI/wJ8zgop+3mEcvhlB1UJBU6tIKq2Y7qTichZjGli8BckEIEwbD4j2beu2qXRSoTT2yUGWM9zpS9+fmH/DAw=="], + "@hcengineering/analytics": ["@hcengineering/analytics@0.7.88", "https://npm.pkg.github.com/download/@hcengineering/analytics/0.7.88/385c2ae512bbcae39189d63e53ea527aa21a662f", { "dependencies": { "@hcengineering/platform": "^0.7.88" } }, "sha512-3jT3h2Y85awrgG0JWZSrpWOXaX1J3dpq8zouo3QMw8OVO1RgHCwPEqrBPOALnB+gc9cZL02p6Aq4J+fnTZ2fww=="], - "@hcengineering/card": ["@hcengineering/card@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/card/0.7.28/5348996f5b7b4a34517780a5bf324122cbddfbad", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "@hcengineering/ui": "^0.7.28" } }, "sha512-1d3allyx9jTpE/gr9kk/g5qo5848i4FKsRiHgCpJqzNEdRW7TAednoHAr2hKRU5DZC/i3Br56axSkUQzV2Idhg=="], + "@hcengineering/card": ["@hcengineering/card@0.7.88", "https://npm.pkg.github.com/download/@hcengineering/card/0.7.88/e39b9e182325945494f162f2dd6f16ef9bea0f3e", { "dependencies": { "@hcengineering/core": "^0.7.88", "@hcengineering/platform": "^0.7.88", "@hcengineering/ui": "^0.7.88" } }, "sha512-3JDgocHFSGmQuKsrtQ5CTTm4rpd/PHahIx6oNX8sshDGpS2hnvNtrHNkJf39seIKiq0NNKmJ//Etl4I21h4c6w=="], "@hcengineering/communication-client-query": ["@hcengineering/communication-client-query@workspace:packages/client-query"], @@ -225,21 +226,21 @@ "@hcengineering/communication-yaml": ["@hcengineering/communication-yaml@workspace:packages/yaml"], - "@hcengineering/core": ["@hcengineering/core@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/core/0.7.28/43b84c63262e2b8fe5554c1967812e2ce6bb56e1", { "dependencies": { "@hcengineering/analytics": "^0.7.28", "@hcengineering/platform": "^0.7.28", "fast-equals": "^5.2.2" } }, "sha512-qv4QE7mkect6JYlGMljW5CQ2uU80hFGq0E17lZ5rO79h1boWEKLJpINv+w3J2BdR/nNG7taIsdZg3yfl9fY4bg=="], + "@hcengineering/core": ["@hcengineering/core@0.7.88", "https://npm.pkg.github.com/download/@hcengineering/core/0.7.88/ca01c3ccd43c85ea44b189d995b0693c5465956c", { "dependencies": { "@hcengineering/analytics": "^0.7.88", "@hcengineering/platform": "^0.7.88", "fast-equals": "^5.2.2" } }, "sha512-c3GRTGAZYARu1kMt4TqkugmnuprYesZ4XZUeakReRtZdYei9tAF+i9xPpDKb4J3hvMC3u82LlEiV8iK26iXpTA=="], - "@hcengineering/platform": ["@hcengineering/platform@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/platform/0.7.28/2126ff97284b679ceab3c86f24febd73a3a28712", { "dependencies": { "intl-messageformat": "^10.7.14" } }, "sha512-0zEifwsLdOs/M6TZ9xnPrQTTlhwW28sooiyL4ZfQsiC2AJUy05vLP5/W8V9eDLta+EiR5bjtlulGsg1GfPjUoQ=="], + "@hcengineering/platform": ["@hcengineering/platform@0.7.88", "https://npm.pkg.github.com/download/@hcengineering/platform/0.7.88/b3fbe09d6b849f7eb3641792682bf133103037f0", { "dependencies": { "intl-messageformat": "^10.7.14" } }, "sha512-iFAwYQZjGox1NqEgUh8hiT1mRHdSJqYmYux0lkeIFyt8Nn/BAZOVXe0br0Dx7wXiueFPWhSZSeWVhxvBb78zoA=="], - "@hcengineering/preference": ["@hcengineering/preference@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/preference/0.7.28/f1739991cc012d194fa18f99565ae5bd64f0ad07", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "@hcengineering/ui": "^0.7.28" } }, "sha512-WwwOfjZ1TLdTfPhQGvSjo8dS63HyuE7BDVhfstxP8D+H4BI5UKYoev/6vlwQf/3d+jI1S4f2ivO//IDJAD7sGg=="], + "@hcengineering/preference": ["@hcengineering/preference@0.7.88", "https://npm.pkg.github.com/download/@hcengineering/preference/0.7.88/e1181dae3cb2eaf53fed66ddd3d5c4854717ccf0", { "dependencies": { "@hcengineering/core": "^0.7.88", "@hcengineering/platform": "^0.7.88", "@hcengineering/ui": "^0.7.88" } }, "sha512-idzAlphM/HyNJfFMFwbpyKg6KvFTJ6rTaqgduNNJqkO4DsUCEF5/1WLNQMmiAeoFDY9y5+/mHJrx+1Ztq1XIuw=="], - "@hcengineering/server-token": ["@hcengineering/server-token@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/server-token/0.7.28/b1cffcb628aa0c6ac73171ef340c8ccf0f85ab07", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "jwt-simple": "^0.5.6" } }, "sha512-TaHwzxuoFKN6jnWHIZk0UcRmB+UQ1QE1HZzUh5doPSFUYOAtTrSb32iJDA8e8oEADcJpOp9RGIUP/EP/VED7BQ=="], + "@hcengineering/server-token": ["@hcengineering/server-token@0.7.88", "https://npm.pkg.github.com/download/@hcengineering/server-token/0.7.88/11b3a8247869a50037fcbb13572e16545116ff93", { "dependencies": { "@hcengineering/core": "^0.7.88", "@hcengineering/platform": "^0.7.88", "jwt-simple": "^0.5.6" } }, "sha512-UQfW0E+v+PTwR7ICR+kSTQqAokpiV008ukqsxRSQJkCM4vmNYciUgd/7Y/ifDMFhKDvNyODb+wgAQs3QxkSbkA=="], - "@hcengineering/tags": ["@hcengineering/tags@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/tags/0.7.28/42545cc43e3c61e32ce96be87c245fa929f5cbb6", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "@hcengineering/ui": "^0.7.28", "@hcengineering/view": "^0.7.28" } }, "sha512-2KCVUu5N/GAkDsDRbdUi7gnBq4ze1Z3eCLoc8nz2G/zeAjAga8gmWkZEbXqAC8rNVqBQv72EKeJFGJyqnev6Xw=="], + "@hcengineering/tags": ["@hcengineering/tags@0.7.88", "https://npm.pkg.github.com/download/@hcengineering/tags/0.7.88/f3f0de35b1be71d1f3f6711b8e4f161e0cf857eb", { "dependencies": { "@hcengineering/core": "^0.7.88", "@hcengineering/platform": "^0.7.88", "@hcengineering/ui": "^0.7.88", "@hcengineering/view": "^0.7.88" } }, "sha512-wpnVX/1Y8E4YPYnUcw+ZyVOpZvXYpboZC9D1btMqFs34CoMGc4tW1XT3YfsS8KxlkH6tsQjsAUvtWOPy2J46dg=="], - "@hcengineering/theme": ["@hcengineering/theme@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/theme/0.7.28/557640a7ba2ffaa4d6e5411a54577f60dc528300", { "dependencies": { "@hcengineering/analytics": "^0.7.28", "@hcengineering/platform": "^0.7.28", "svelte": "^4.2.19" } }, "sha512-Hv3wQKHr327TC7wMHCBiG4bMtg8vSlb7+UFwdKI8ztEs3359meqkXu+exqi7Nl5334voXIz5ZFxjwZwOMzNSFA=="], + "@hcengineering/theme": ["@hcengineering/theme@0.7.88", "https://npm.pkg.github.com/download/@hcengineering/theme/0.7.88/d939d1e2a9047def2795dc5124653e49ca522cfc", { "dependencies": { "@hcengineering/analytics": "^0.7.88", "@hcengineering/platform": "^0.7.88", "svelte": "^4.2.19" } }, "sha512-PCt9bFuGPYOf1kQgjzxPSZgGXU4G14Z1XZJfgGu71u/zHxG6fu6nls35aSrEjy2Ks6oR+OCqqvpDh6RTf6utrg=="], - "@hcengineering/ui": ["@hcengineering/ui@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/ui/0.7.28/82025fc775702ea8c8569611356662bfbf28eb74", { "dependencies": { "@hcengineering/analytics": "^0.7.28", "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "@hcengineering/theme": "^0.7.28", "autolinker": "4.0.0", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "dompurify": "^3.1.6", "emojibase": "^16.0.0", "fast-equals": "^5.2.2", "hls.js": "^1.5.20", "plyr": "^3.7.8", "svelte": "^4.2.19" } }, "sha512-QsUpO0+Idu1n2dC6KtorPJ6SSp30It8FHSuG3Y+Awv4td/ftVocOSFqhskbk+us9Yw9KqOYgkqhGYEHbfRfw4Q=="], + "@hcengineering/ui": ["@hcengineering/ui@0.7.88", "https://npm.pkg.github.com/download/@hcengineering/ui/0.7.88/fce3392a59f706c248ba4b17714163e5725d5f01", { "dependencies": { "@hcengineering/analytics": "^0.7.88", "@hcengineering/core": "^0.7.88", "@hcengineering/platform": "^0.7.88", "@hcengineering/theme": "^0.7.88", "autolinker": "4.0.0", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "dompurify": "^3.1.6", "emojibase": "^16.0.0", "fast-equals": "^5.2.2", "hls.js": "^1.5.20", "plyr": "^3.7.8", "svelte": "^4.2.19" } }, "sha512-mzHXcX3aBC/OXUaV9F2hnB6+irwMfEYM5J1CnfqiHqgv3Hza20BZR6BrYe/nQCLb44DuHkXojbwL94AKK3QW6w=="], - "@hcengineering/view": ["@hcengineering/view@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/view/0.7.28/8aad5b07d3750463a78aae1b9482361f2ff9f45e", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "@hcengineering/preference": "^0.7.28", "@hcengineering/ui": "^0.7.28" } }, "sha512-cG0isakqIxuiAG7T2KyRnztfOp3falr+oG77Qod915EeBKNEDqXfNrD29Dp04Wwf5TowifDT9smLzAyH8l6kgg=="], + "@hcengineering/view": ["@hcengineering/view@0.7.88", "https://npm.pkg.github.com/download/@hcengineering/view/0.7.88/8a13ddff0178ac3b705ebed683d0c1cc328c8e1c", { "dependencies": { "@hcengineering/core": "^0.7.88", "@hcengineering/platform": "^0.7.88", "@hcengineering/preference": "^0.7.88", "@hcengineering/ui": "^0.7.88" } }, "sha512-W3/OgCu9W+OGZ8v8hoT0bjoCNflvIcWFwFafk40FtWFxvqzObm41e5ZWNj3yGWFdlVsyN/Nqdjkyz0txWUXQAA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -691,19 +692,19 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "turbo": ["turbo@2.5.2", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.2", "turbo-darwin-arm64": "2.5.2", "turbo-linux-64": "2.5.2", "turbo-linux-arm64": "2.5.2", "turbo-windows-64": "2.5.2", "turbo-windows-arm64": "2.5.2" }, "bin": { "turbo": "bin/turbo" } }, "sha512-Qo5lfuStr6LQh3sPQl7kIi243bGU4aHGDQJUf6ylAdGwks30jJFloc9NYHP7Y373+gGU9OS0faA4Mb5Sy8X9Xw=="], + "turbo": ["turbo@2.5.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.3", "turbo-darwin-arm64": "2.5.3", "turbo-linux-64": "2.5.3", "turbo-linux-arm64": "2.5.3", "turbo-windows-64": "2.5.3", "turbo-windows-arm64": "2.5.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-iHuaNcq5GZZnr3XDZNuu2LSyCzAOPwDuo5Qt+q64DfsTP1i3T2bKfxJhni2ZQxsvAoxRbuUK5QetJki4qc5aYA=="], - "turbo-darwin-64": ["turbo-darwin-64@2.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-2aIl0Sx230nLk+Cg2qSVxvPOBWCZpwKNuAMKoROTvWKif6VMpkWWiR9XEPoz7sHeLmCOed4GYGMjL1bqAiIS/g=="], + "turbo-darwin-64": ["turbo-darwin-64@2.5.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-YSItEVBUIvAGPUDpAB9etEmSqZI3T6BHrkBkeSErvICXn3dfqXUfeLx35LfptLDEbrzFUdwYFNmt8QXOwe9yaw=="], - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MrFYhK/jYu8N6QlqZtqSHi3e4QVxlzqU3ANHTKn3/tThuwTLbNHEvzBPWSj5W7nZcM58dCqi6gYrfRz6bJZyAA=="], + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5PefrwHd42UiZX7YA9m1LPW6x9YJBDErXmsegCkVp+GjmWrADfEOxpFrGQNonH3ZMj77WZB2PVE5Aw3gA+IOhg=="], - "turbo-linux-64": ["turbo-linux-64@2.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-LxNqUE2HmAJQ/8deoLgMUDzKxd5bKxqH0UBogWa+DF+JcXhtze3UTMr6lEr0dEofdsEUYK1zg8FRjglmwlN5YA=="], + "turbo-linux-64": ["turbo-linux-64@2.5.3", "", { "os": "linux", "cpu": "x64" }, "sha512-M9xigFgawn5ofTmRzvjjLj3Lqc05O8VHKuOlWNUlnHPUltFquyEeSkpQNkE/vpPdOR14AzxqHbhhxtfS4qvb1w=="], - "turbo-linux-arm64": ["turbo-linux-arm64@2.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-0MI1Ao1q8zhd+UUbIEsrM+yLq1BsrcJQRGZkxIsHFlGp7WQQH1oR3laBgfnUCNdCotCMD6w4moc9pUbXdOR3bg=="], + "turbo-linux-arm64": ["turbo-linux-arm64@2.5.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-auJRbYZ8SGJVqvzTikpg1bsRAsiI9Tk0/SDkA5Xgg0GdiHDH/BOzv1ZjDE2mjmlrO/obr19Dw+39OlMhwLffrw=="], - "turbo-windows-64": ["turbo-windows-64@2.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-hOLcbgZzE5ttACHHyc1ajmWYq4zKT42IC3G6XqgiXxMbS+4eyVYTL+7UvCZBd3Kca1u4TLQdLQjeO76zyDJc2A=="], + "turbo-windows-64": ["turbo-windows-64@2.5.3", "", { "os": "win32", "cpu": "x64" }, "sha512-arLQYohuHtIEKkmQSCU9vtrKUg+/1TTstWB9VYRSsz+khvg81eX6LYHtXJfH/dK7Ho6ck+JaEh5G+QrE1jEmCQ=="], - "turbo-windows-arm64": ["turbo-windows-arm64@2.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-fMU41ABhSLa18H8V3Z7BMCGynQ8x+wj9WyBMvWm1jeyRKgkvUYJsO2vkIsy8m0vrwnIeVXKOIn6eSe1ddlBVqw=="], + "turbo-windows-arm64": ["turbo-windows-arm64@2.5.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3JPn66HAynJ0gtr6H+hjY4VHpu1RPKcEwGATvGUTmLmYSYBQieVlnGDRMMoYN066YfyPqnNGCfhYbXfH92Cm0g=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], diff --git a/package.json b/package.json index bd859afaee..f950e32b63 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "eslint-plugin-prettier": "^5.4.0", "prettier": "^3.5.3", "typescript-eslint": "^8.32.0", - "turbo": "^2.5.2" + "turbo": "^2.5.3" }, "packageManager": "bun@1.2.9" } diff --git a/packages/cockroach/src/adapter.ts b/packages/cockroach/src/adapter.ts index f9493b4735..b42e1ceeb1 100644 --- a/packages/cockroach/src/adapter.ts +++ b/packages/cockroach/src/adapter.ts @@ -41,10 +41,10 @@ import { type FindLabelsParams, type LabelID, type CardType, - type MessageData + type MessageData, + type PatchData } from '@hcengineering/communication-types' import type { DbAdapter } from '@hcengineering/communication-sdk-types' -import { retry } from '@hcengineering/communication-shared' import { MessagesDb } from './db/message' import { NotificationsDb } from './db/notification' @@ -77,9 +77,10 @@ export class CockroachAdapter implements DbAdapter { creator: SocialID, created: Date, data?: MessageData, - externalId?: string + externalId?: string, + id?: MessageID ): Promise { - return await this.message.createMessage(card, type, content, creator, created, data, externalId) + return await this.message.createMessage(card, type, content, creator, created, data, externalId, id) } async removeMessages(card: CardID, messages: MessageID[], socialIds?: SocialID[]): Promise { @@ -91,11 +92,11 @@ export class CockroachAdapter implements DbAdapter { message: MessageID, messageCreated: Date, type: PatchType, - content: RichText, + data: PatchData, creator: SocialID, created: Date ): Promise { - await this.message.createPatch(card, message, messageCreated, type, content, creator, created) + await this.message.createPatch(card, message, messageCreated, type, data, creator, created) } async createMessagesGroup(card: CardID, blobId: BlobID, fromDate: Date, toDate: Date, count: number): Promise { @@ -150,9 +151,10 @@ export class CockroachAdapter implements DbAdapter { message: MessageID, messageCreated: Date, thread: CardID, + threadType: CardType, created: Date ): Promise { - await this.message.createThread(card, message, messageCreated, thread, created) + await this.message.createThread(card, message, messageCreated, thread, threadType, created) } async updateThread(thread: CardID, op: 'increment' | 'decrement', lastReply?: Date): Promise { @@ -243,77 +245,13 @@ export async function createDbAdapter( logger?: Logger, options?: Options ): Promise { - const greenUrl = process.env.GREEN_URL ?? '' const connection = connect(connectionString) const sql = await connection.getClient() await initSchema(sql) - if (greenUrl !== '') { - const client = new GreenClient(greenUrl, sql) - return new CockroachAdapter(client, workspace, logger, options) - } else { - const client = new CockroachClient(connection, sql) + const client = new CockroachClient(connection, sql) - return new CockroachAdapter(client, workspace, logger, options) - } -} - -class GreenClient implements SqlClient { - private readonly url: string - private readonly token: string - constructor( - private readonly endpoint: string, - private readonly sql: postgres.Sql - ) { - const url = new URL(this.endpoint) - this.token = url.searchParams.get('token') ?? 'secret' - - const compression = url.searchParams.get('compression') ?? '' - - const newHost = url.host - const newPathname = url.pathname - const newSearchParams = new URLSearchParams() - - if (compression !== '') { - newSearchParams.set('compression', compression) - } - - this.url = `${url.protocol}//${newHost}${newPathname}${newSearchParams.size > 0 ? '?' + newSearchParams.toString() : ''}` - } - - async execute(query: string, params?: SqlParams): Promise { - return await retry(() => this.fetch(query, params), { retries: 5 }) - } - - cursor(query: string, params?: SqlParams, size?: number): AsyncIterable[]> { - const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query - - return this.sql.unsafe(sql).cursor(size) - } - - close(): void { - // do nothing - } - - private async fetch(query: string, params?: SqlParams): Promise { - const url = this.url.endsWith('/') ? this.url + 'api/v1/sql' : this.url + '/api/v1/sql' - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + this.token, - Connection: 'keep-alive' - }, - body: JSON.stringify({ query, params }, (_, value) => (typeof value === 'bigint' ? value.toString() : value)) - }) - - if (!response.ok) { - throw new Error(`Failed to execute sql: ${response.status} ${response.statusText}`) - } - - return await response.json() - } + return new CockroachAdapter(client, workspace, logger, options) } class CockroachClient implements SqlClient { diff --git a/packages/cockroach/src/db/mapping.ts b/packages/cockroach/src/db/mapping.ts index 3eb6012bf7..2e766fb208 100644 --- a/packages/cockroach/src/db/mapping.ts +++ b/packages/cockroach/src/db/mapping.ts @@ -33,8 +33,10 @@ import { type SocialID, type Thread, type MessageData, - type Label + type Label, + type CardType } from '@hcengineering/communication-types' +import { applyPatches } from '@hcengineering/communication-shared' import { type FileDb, @@ -51,6 +53,7 @@ import { interface RawMessage extends MessageDb { thread_id?: CardID + thread_type?: CardType replies_count?: number last_reply?: Date patches?: PatchDb[] @@ -72,7 +75,7 @@ interface RawNotification extends NotificationDb { message_group_count?: number message_patches?: { patch_type: PatchType - patch_content: RichText + patch_data: Record patch_creator: SocialID patch_created: Date }[] @@ -83,32 +86,38 @@ type RawContext = ContextDb & { id: ContextID } & { } export function toMessage(raw: RawMessage): Message { - const lastPatch = raw.patches?.[0] + const patches = (raw.patches ?? []).map((it) => toPatch(it)) - return { + const rawMessage: Message = { id: String(raw.id) as MessageID, type: raw.type, card: raw.card_id, - content: lastPatch?.content ?? raw.content, + content: raw.content, creator: raw.creator, - created: raw.created, + created: new Date(raw.created), data: raw.data, externalId: raw.external_id, - edited: lastPatch?.created ?? undefined, thread: - raw.thread_id != null + raw.thread_id != null && raw.thread_type != null ? { card: raw.card_id, message: String(raw.id) as MessageID, messageCreated: new Date(raw.created), thread: raw.thread_id, - repliesCount: raw.replies_count ?? 0, + threadType: raw.thread_type, + repliesCount: raw.replies_count ? parseInt(raw.replies_count as any) : 0, lastReply: raw.last_reply ?? new Date() } : undefined, reactions: (raw.reactions ?? []).map(toReaction), files: (raw.files ?? []).map(toFile) } + + if (patches.length === 0) { + return rawMessage + } + + return applyPatches(rawMessage, patches, [PatchType.update]) } export function toReaction(raw: ReactionDb): Reaction { @@ -116,7 +125,7 @@ export function toReaction(raw: ReactionDb): Reaction { message: String(raw.message_id) as MessageID, reaction: raw.reaction, creator: raw.creator, - created: raw.created + created: new Date(raw.created) } } @@ -130,7 +139,7 @@ export function toFile(raw: FileDb): File { filename: raw.filename, size: parseInt(raw.size as any), creator: raw.creator, - created: raw.created + created: new Date(raw.created) } } @@ -145,12 +154,12 @@ export function toMessagesGroup(raw: MessagesGroupDb): MessagesGroup { } } -export function toPatch(raw: PatchDb): Patch { +export function toPatch(raw: Omit): Patch { return { type: raw.type, messageCreated: new Date(raw.message_created), message: String(raw.message_id) as MessageID, - content: raw.content, + data: raw.data as any, creator: raw.creator, created: new Date(raw.created) } @@ -162,8 +171,9 @@ export function toThread(raw: ThreadDb): Thread { message: String(raw.message_id) as MessageID, messageCreated: new Date(raw.message_created), thread: raw.thread_id, - repliesCount: raw.replies_count, - lastReply: raw.last_reply + threadType: raw.thread_type, + repliesCount: parseInt(raw.replies_count as any), + lastReply: new Date(raw.last_reply) } } @@ -198,20 +208,35 @@ function toNotificationRaw( raw.message_created != null && raw.message_type != null ) { - const lastPatch = (raw.message_patches ?? []).find((it) => it.patch_type === PatchType.update) + const patches = (raw.message_patches ?? []).map((it) => + toPatch({ + card_id: card, + message_id: raw.message_id, + type: it.patch_type, + data: it.patch_data, + creator: it.patch_creator, + created: new Date(it.patch_created), + message_created: raw.message_created ? new Date(raw.message_created) : created + }) + ) + message = { id: String(raw.message_id) as MessageID, type: raw.message_type, card, - content: lastPatch?.patch_content ?? raw.message_content, + content: raw.message_content, data: raw.message_data, externalId: raw.message_external_id, creator: raw.message_creator, created: new Date(raw.message_created), - edited: lastPatch?.patch_created != null ? new Date(lastPatch.patch_created) : undefined, + edited: undefined, reactions: [], files: [] } + + if (patches.length > 0) { + message = applyPatches(message, patches, [PatchType.update]) + } } if (message != null) { diff --git a/packages/cockroach/src/db/message.ts b/packages/cockroach/src/db/message.ts index 2046e2fb21..69d98b9159 100644 --- a/packages/cockroach/src/db/message.ts +++ b/packages/cockroach/src/db/message.ts @@ -16,6 +16,7 @@ import { type BlobID, type CardID, + type CardType, type FindMessagesGroupsParams, type FindMessagesParams, type Message, @@ -23,6 +24,7 @@ import { type MessageID, type MessagesGroup, type MessageType, + type PatchData, PatchType, type RichText, type SocialID, @@ -34,6 +36,7 @@ import { BaseDb } from './base' import { type FileDb, type MessageDb, + messageSchema, type MessagesGroupDb, type PatchDb, type ReactionDb, @@ -52,9 +55,10 @@ export class MessagesDb extends BaseDb { creator: SocialID, created: Date, data?: MessageData, - externalId?: string + externalId?: string, + id?: MessageID ): Promise { - const db: Omit = { + const db: Omit & { id?: MessageID } = { type, workspace_id: this.workspace, card_id: card, @@ -62,18 +66,26 @@ export class MessagesDb extends BaseDb { creator, created, data, - external_id: externalId + external_id: externalId, + id } - const sql = `INSERT INTO ${TableName.Message} (workspace_id, card_id, content, creator, created, type, data, external_id) - VALUES ($1::uuid, $2::varchar, $3::text, $4::varchar, $5::timestamptz, $6::varchar, $7::jsonb, $8::varchar) - RETURNING id::text` + const values: any[] = [] + const keys: string[] = [] - const result = await this.execute( - sql, - [db.workspace_id, db.card_id, db.content, db.creator, db.created, db.type, db.data ?? {}, externalId ?? null], - 'insert message' - ) + for (const key in db) { + const value: any = (db as any)[key] + if (value == null) continue + keys.push(key) + values.push(value) + } + + const placeholders = keys.map((key, i) => `$${i + 1}::${(messageSchema as any)[key]}`) + const sql = `INSERT INTO ${TableName.Message} (${keys.join(', ')}) + VALUES (${placeholders.join(', ')}) + RETURNING id::text` + + const result = await this.execute(sql, values, 'insert message') return result.map((it: any) => it.id)[0] } @@ -104,7 +116,10 @@ export class MessagesDb extends BaseDb { values.push(messages) } - const sql = `DELETE FROM ${TableName.Message} WHERE ${where.join(' AND ')} RETURNING id::text` + const sql = `DELETE + FROM ${TableName.Message} + WHERE ${where.join(' AND ')} + RETURNING id::text` const result = await this.execute(sql, values, 'remove messages') @@ -116,7 +131,7 @@ export class MessagesDb extends BaseDb { message: MessageID, messageCreated: Date, type: PatchType, - content: string, + data: PatchData, creator: SocialID, created: Date ): Promise { @@ -125,18 +140,20 @@ export class MessagesDb extends BaseDb { card_id: card, message_id: message, type, - content, + data, creator, created, message_created: messageCreated } - const sql = `INSERT INTO ${TableName.Patch} (workspace_id, card_id, message_id, type, content, creator, created, message_created) - VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::text, $6::varchar, $7::timestamptz, $8::timestamptz)` + const sql = `INSERT INTO ${TableName.Patch} (workspace_id, card_id, message_id, type, data, creator, created, + message_created) + VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::jsonb, $6::varchar, $7::timestamptz, + $8::timestamptz)` await this.execute( sql, - [db.workspace_id, db.card_id, db.message_id, db.type, db.content, db.creator, db.created, db.message_created], + [db.workspace_id, db.card_id, db.message_id, db.type, db.data, db.creator, db.created, db.message_created], 'insert patch' ) } @@ -165,8 +182,10 @@ export class MessagesDb extends BaseDb { created, message_created: messageCreated } - const sql = `INSERT INTO ${TableName.File} (workspace_id, card_id, message_id, blob_id, type, filename, creator, created, message_created, size) - VALUES ($1::uuid, $2::varchar, $3::int8, $4::uuid, $5::varchar, $6::varchar, $7::varchar, $8::timestamptz, $9::timestamptz, $10::int8)` + const sql = `INSERT INTO ${TableName.File} (workspace_id, card_id, message_id, blob_id, type, filename, creator, + created, message_created, size) + VALUES ($1::uuid, $2::varchar, $3::int8, $4::uuid, $5::varchar, $6::varchar, $7::varchar, + $8::timestamptz, $9::timestamptz, $10::int8)` await this.execute( sql, @@ -206,10 +225,12 @@ export class MessagesDb extends BaseDb { created: Date ): Promise { const select = `SELECT m.id - FROM ${TableName.Message} m - WHERE m.id = $1::bigint` + FROM ${TableName.Message} m + WHERE m.workspace_id = $1::uuid + AND m.card_id = $2::varchar + AND m.id = $3::bigint` - const messageDb = await this.execute(select, [message], 'select message') + const messageDb = await this.execute(select, [this.workspace, card, message], 'select message') if (messageDb.length > 0) { const db: ReactionDb = { @@ -221,7 +242,7 @@ export class MessagesDb extends BaseDb { created } const sql = `INSERT INTO ${TableName.Reaction} (workspace_id, card_id, message_id, reaction, creator, created) - VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::varchar, $6::timestamptz)` + VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::varchar, $6::timestamptz)` await this.execute( sql, @@ -229,7 +250,7 @@ export class MessagesDb extends BaseDb { 'insert reaction' ) } else { - await this.createPatch(card, message, messageCreated, PatchType.addReaction, reaction, creator, created) + await this.createPatch(card, message, messageCreated, PatchType.addReaction, { reaction }, creator, created) } } @@ -242,22 +263,24 @@ export class MessagesDb extends BaseDb { created: Date ): Promise { const select = `SELECT m.id - FROM ${TableName.Message} m - WHERE m.id = $1::bigint` + FROM ${TableName.Message} m + WHERE m.workspace_id = $1::uuid + AND m.card_id = $2::varchar + AND m.id = $3::bigint` - const messageDb = await this.execute(select, [message], 'select message') + const messageDb = await this.execute(select, [this.workspace, card, message], 'select message') if (messageDb.length > 0) { const sql = `DELETE - FROM ${TableName.Reaction} - WHERE workspace_id = $1::uuid - AND card_id = $2::varchar - AND message_id = $3::bigint - AND reaction = $4::varchar - AND creator = $5::varchar` + FROM ${TableName.Reaction} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND message_id = $3::bigint + AND reaction = $4::varchar + AND creator = $5::varchar` await this.execute(sql, [this.workspace, card, message, reaction, creator], 'remove reaction') } else { - await this.createPatch(card, message, messageCreated, PatchType.removeReaction, reaction, creator, created) + await this.createPatch(card, message, messageCreated, PatchType.removeReaction, { reaction }, creator, created) } } @@ -267,6 +290,7 @@ export class MessagesDb extends BaseDb { message: MessageID, messageCreated: Date, thread: CardID, + threadType: CardType, created: Date ): Promise { const db: ThreadDb = { @@ -275,15 +299,25 @@ export class MessagesDb extends BaseDb { message_id: message, message_created: messageCreated, thread_id: thread, + thread_type: threadType, replies_count: 0, last_reply: created } - const sql = `INSERT INTO ${TableName.Thread} (workspace_id, card_id, message_id, thread_id, replies_count, - last_reply, message_created) - VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::int, $6::timestamptz, $7::timestamptz)` + const sql = `INSERT INTO ${TableName.Thread} (workspace_id, card_id, message_id, thread_id, thread_type, replies_count, + last_reply, message_created) + VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::varchar, $6::int, $7::timestamptz, $8::timestamptz)` await this.execute( sql, - [db.workspace_id, db.card_id, db.message_id, db.thread_id, db.replies_count, db.last_reply, db.message_created], + [ + db.workspace_id, + db.card_id, + db.message_id, + db.thread_id, + db.thread_type, + db.replies_count, + db.last_reply, + db.message_created + ], 'insert thread' ) } @@ -322,7 +356,7 @@ export class MessagesDb extends BaseDb { } const sql = `INSERT INTO ${TableName.MessagesGroup} (workspace_id, card_id, blob_id, from_date, to_date, count) - VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz, $6::int)` + VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz, $6::int)` await this.execute( sql, [db.workspace_id, db.card_id, db.blob_id, db.from_date, db.to_date, db.count], @@ -332,10 +366,10 @@ export class MessagesDb extends BaseDb { async removeMessagesGroup(card: CardID, blobId: BlobID): Promise { const sql = `DELETE - FROM ${TableName.MessagesGroup} - WHERE workspace_id = $1::uuid - AND card_id = $2::varchar - AND blob_id = $3::uuid` + FROM ${TableName.MessagesGroup} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND blob_id = $3::uuid` await this.execute(sql, [this.workspace, card, blobId], 'remove messages group') } @@ -391,6 +425,7 @@ export class MessagesDb extends BaseDb { 'message_created', f.message_created, 'blob_id', f.blob_id, 'type', f.type, + 'size', f.size, 'filename', f.filename, 'creator', f.creator, 'created', f.created @@ -438,10 +473,11 @@ export class MessagesDb extends BaseDb { p.message_id, jsonb_agg( jsonb_build_object( - 'content', p.content, + 'type', p.type, + 'data', p.data, 'creator', p.creator, 'created', p.created - ) ORDER BY p.created DESC + ) ORDER BY p.created ASC ) AS patches FROM ${TableName.Patch} p INNER JOIN limited_messages m @@ -457,7 +493,7 @@ export class MessagesDb extends BaseDb { private buildMainSelect(params: FindMessagesParams): string { const orderBy = this.buildOrderBy(params) const selectReplies = params.replies - ? `t.thread_id as thread_id, t.replies_count as replies_count, t.last_reply as last_reply,` + ? `t.thread_id as thread_id, t.thread_type as thread_type, t.replies_count::int as replies_count, t.last_reply as last_reply,` : '' const selectFiles = params.files ? `COALESCE(f.files, '[]'::jsonb) AS files,` : `'[]'::jsonb AS files,` @@ -483,32 +519,31 @@ export class MessagesDb extends BaseDb { : '' return ` - SELECT - m.id::text, - m.card_id, - m.type, - m.content, - m.creator, - m.created, - m.data, - m.external_id, - ${selectReplies} - ${selectFiles} - ${selectReactions} - COALESCE(p.patches, '[]'::jsonb) AS patches - FROM limited_messages m - LEFT JOIN ${TableName.Thread} t - ON t.workspace_id = m.workspace_id - AND t.card_id = m.card_id - AND t.message_id = m.id - ${joinFiles} - ${joinReactions} - LEFT JOIN agg_patches p - ON p.workspace_id = m.workspace_id - AND p.card_id = m.card_id - AND p.message_id = m.id - ${orderBy} - ` + SELECT m.id::text, + m.card_id, + m.type, + m.content, + m.creator, + m.created, + m.data, + m.external_id, + ${selectReplies} + ${selectFiles} + ${selectReactions} + COALESCE(p.patches, '[]'::jsonb) AS patches + FROM limited_messages m + LEFT JOIN ${TableName.Thread} t + ON t.workspace_id = m.workspace_id + AND t.card_id = m.card_id + AND t.message_id = m.id + ${joinFiles} + ${joinReactions} + LEFT JOIN agg_patches p + ON p.workspace_id = m.workspace_id + AND p.card_id = m.card_id + AND p.message_id = m.id + ${orderBy} + ` } buildMessageWhere(params: FindMessagesParams): { where: string; values: any[] } { @@ -546,15 +581,16 @@ export class MessagesDb extends BaseDb { // Find thread async findThread(thread: CardID): Promise { const sql = `SELECT t.card_id, - t.message_id::text, - t.message_created, - t.thread_id, - t.replies_count, - t.last_reply - FROM ${TableName.Thread} t - WHERE t.workspace_id = $1::uuid - AND t.thread_id = $2::varchar - LIMIT 1;` + t.message_id::text, + t.message_created, + t.thread_id, + t.thread_type, + t.replies_count::int, + t.last_reply + FROM ${TableName.Thread} t + WHERE t.workspace_id = $1::uuid + AND t.thread_id = $2::varchar + LIMIT 1;` const result = await this.execute(sql, [this.workspace, thread], 'find thread') return result.map((it: any) => toThread(it))[0] @@ -563,27 +599,27 @@ export class MessagesDb extends BaseDb { // Find messages groups async findMessagesGroups(params: FindMessagesGroupsParams): Promise { const select = ` - SELECT mg.card_id, - mg.blob_id, - mg.from_date, - mg.to_date, - mg.count, - patches - FROM ${TableName.MessagesGroup} mg - CROSS JOIN LATERAL ( - SELECT jsonb_agg(jsonb_build_object( - 'message_id', p.message_id::text, - 'message_created', p.message_created, - 'type', p.type, - 'content', p.content, - 'creator', p.creator, - 'created', p.created - ) ORDER BY p.created) AS patches - FROM ${TableName.Patch} p - WHERE p.workspace_id = mg.workspace_id - AND p.card_id = mg.card_id - AND p.message_created BETWEEN mg.from_date AND mg.to_date - ) sub` + SELECT mg.card_id, + mg.blob_id, + mg.from_date, + mg.to_date, + mg.count, + patches + FROM ${TableName.MessagesGroup} mg + CROSS JOIN LATERAL ( + SELECT jsonb_agg(jsonb_build_object( + 'message_id', p.message_id::text, + 'message_created', p.message_created, + 'type', p.type, + 'data', p.data, + 'creator', p.creator, + 'created', p.created + ) ORDER BY p.created) AS patches + FROM ${TableName.Patch} p + WHERE p.workspace_id = mg.workspace_id + AND p.card_id = mg.card_id + AND p.message_created BETWEEN mg.from_date AND mg.to_date + ) sub` const { where, values } = this.buildMessagesGroupWhere(params) const orderBy = diff --git a/packages/cockroach/src/db/notification.ts b/packages/cockroach/src/db/notification.ts index d8e22344b6..e2d6137df6 100644 --- a/packages/cockroach/src/db/notification.ts +++ b/packages/cockroach/src/db/notification.ts @@ -219,7 +219,7 @@ export class NotificationsDb extends BaseDb { JSON_AGG( JSONB_BUILD_OBJECT( 'patch_type', p.type, - 'patch_content', p.content, + 'patch_data', p.data, 'patch_creator', p.creator, 'patch_created', p.created ) ORDER BY p.created DESC diff --git a/packages/cockroach/src/db/schema.ts b/packages/cockroach/src/db/schema.ts index 69f2c6d07e..3e9663024b 100644 --- a/packages/cockroach/src/db/schema.ts +++ b/packages/cockroach/src/db/schema.ts @@ -54,6 +54,18 @@ export interface MessageDb { external_id?: string } +export const messageSchema: Record = { + id: 'int8', + workspace_id: 'uuid', + card_id: 'varchar', + content: 'text', + creator: 'varchar', + created: 'timestamptz', + type: 'varchar', + data: 'jsonb', + external_id: 'varchar' +} + export interface MessagesGroupDb { workspace_id: WorkspaceID card_id: CardID @@ -69,7 +81,7 @@ export interface PatchDb { card_id: CardID message_id: MessageID type: PatchType - content: RichText + data: Record creator: SocialID created: Date message_created: Date @@ -103,6 +115,7 @@ export interface ThreadDb { message_id: MessageID message_created: Date thread_id: CardID + thread_type: CardType replies_count: number last_reply: Date } diff --git a/packages/cockroach/src/init.ts b/packages/cockroach/src/init.ts index d23925e379..2c461d1522 100644 --- a/packages/cockroach/src/init.ts +++ b/packages/cockroach/src/init.ts @@ -17,17 +17,60 @@ import type postgres from 'postgres' const migrationsTableName = 'communication._migrations' -let isInitialized = false +let isSchemaInitialized = false +let initPromise: Promise | null = null -export async function initSchema(sql: postgres.Sql) { - if (isInitialized) return +export function isInitialized(): boolean { + return isSchemaInitialized +} + +export async function initSchema(sql: postgres.Sql): Promise { + if (isInitialized()) return + + if (!initPromise) { + initPromise = (async () => { + const maxAttempts = 3 + const retryDelay = 3000 + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + await init(sql) + isSchemaInitialized = true + return + } catch (err) { + if (attempt === maxAttempts) { + throw err + } + console.warn(`InitSchema attempt ${attempt} failed, retrying in ${retryDelay}ms…`, err) + await delay(retryDelay) + } + } + })() + .catch((err) => { + throw err + }) + .finally(() => { + initPromise = null + }) + } + + await initPromise +} + +function delay(ms: number): Promise { + return new Promise((res) => setTimeout(res, ms)) +} + +async function init(sql: postgres.Sql) { + if (isSchemaInitialized) return const start = performance.now() console.log('🗃️ Initializing schema...') await sql.unsafe('CREATE SCHEMA IF NOT EXISTS communication;') await sql.unsafe(`CREATE TABLE IF NOT EXISTS ${migrationsTableName} ( name VARCHAR(255) NOT NULL, - created_on TIMESTAMPTZ NOT NULL DEFAULT now() + created_on TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (name) )`) const appliedMigrations = await sql.unsafe(`SELECT name @@ -50,103 +93,100 @@ export async function initSchema(sql: postgres.Sql) { throw err } } - isInitialized = true + isSchemaInitialized = true const end = performance.now() const resTime = (end - start) / 1000 console.log(`🎉 All migrations complete in ${resTime.toFixed(2)} sec`) } function getMigrations(): [string, string][] { - return [ - migrationV1_1(), - migrationV2_1(), - migrationV3_1(), - migrationV4_1(), - migrationV4_2(), - migrationV5_1(), - migrationV5_2(), - migrationV5_3(), - migrationV5_4(), - migrationV5_5(), - migrationV5_6(), - migrationV6_1(), - migrationV6_2() - ] + return [migrationV1_1(), migrationV1_2()] } function migrationV1_1(): [string, string] { + const sql = ` + DROP SCHEMA IF EXISTS communication CASCADE; + CREATE SCHEMA IF NOT EXISTS communication; + CREATE TABLE IF NOT EXISTS ${migrationsTableName} + ( + name VARCHAR(255) NOT NULL, + created_on TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (name) + ) + ` + + return ['recreate_schema-v1_1', sql] +} + +function migrationV1_2(): [string, string] { const sql = ` CREATE TABLE IF NOT EXISTS communication.messages ( workspace_id UUID NOT NULL, card_id VARCHAR(255) NOT NULL, - id INT8 NOT NULL, - + id INT8 NOT NULL DEFAULT unique_rowid(), content TEXT NOT NULL, creator VARCHAR(255) NOT NULL, created TIMESTAMPTZ NOT NULL, - type VARCHAR(255) NOT NULL, data JSONB NOT NULL DEFAULT '{}', - - + external_id VARCHAR(255), PRIMARY KEY (workspace_id, card_id, id) ); CREATE INDEX IF NOT EXISTS idx_messages_workspace_card ON communication.messages (workspace_id, card_id); CREATE INDEX IF NOT EXISTS idx_messages_workspace_card_id ON communication.messages (workspace_id, card_id, id); + CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_unique_workspace_card_external_id + ON communication.messages (workspace_id, card_id, external_id) + WHERE external_id IS NOT NULL; CREATE TABLE IF NOT EXISTS communication.messages_groups ( - workspace_id UUID NOT NULL, - card_id VARCHAR(255) NOT NULL, - blob_id UUID NOT NULL, - - from_sec TIMESTAMPTZ(0) NOT NULL, - to_sec TIMESTAMPTZ(0) NOT NULL, - count INT NOT NULL, - + workspace_id UUID NOT NULL, + card_id VARCHAR(255) NOT NULL, + blob_id UUID NOT NULL, + from_date TIMESTAMPTZ NOT NULL, + to_date TIMESTAMPTZ NOT NULL, + count INT NOT NULL, PRIMARY KEY (workspace_id, card_id, blob_id) ); - CREATE INDEX IF NOT EXISTS idx_messages_groups_workspace_card ON communication.messages_groups (workspace_id, card_id); + CREATE TABLE IF NOT EXISTS communication.patch ( - id INT8 NOT NULL DEFAULT unique_rowid(), - workspace_id UUID NOT NULL, - card_id VARCHAR(255) NOT NULL, - message_id INT8 NOT NULL, - type VARCHAR(255) NOT NULL, - content TEXT NOT NULL, - creator VARCHAR(255) NOT NULL, - created TIMESTAMPTZ NOT NULL, - message_created_sec TIMESTAMPTZ(0) NOT NULL, - + id INT8 NOT NULL DEFAULT unique_rowid(), + workspace_id UUID NOT NULL, + card_id VARCHAR(255) NOT NULL, + message_id INT8 NOT NULL, + type VARCHAR(255) NOT NULL, + creator VARCHAR(255) NOT NULL, + created TIMESTAMPTZ NOT NULL, + message_created TIMESTAMPTZ NOT NULL, + data JSONB NOT NULL DEFAULT '{}', PRIMARY KEY (id) ); CREATE INDEX IF NOT EXISTS idx_patch_workspace_card_message ON communication.patch (workspace_id, card_id, message_id); + CREATE TABLE IF NOT EXISTS communication.files ( - workspace_id UUID NOT NULL, - card_id VARCHAR(255) NOT NULL, - message_id INT8 NOT NULL, - - blob_id UUID NOT NULL, - filename VARCHAR(255) NOT NULL, - type VARCHAR(255) NOT NULL, - size INT8 NOT NULL, - - creator VARCHAR(255) NOT NULL, - created TIMESTAMPTZ NOT NULL DEFAULT now(), - - message_created_sec TIMESTAMPTZ(0) NOT NULL, - + workspace_id UUID NOT NULL, + card_id VARCHAR(255) NOT NULL, + message_id INT8 NOT NULL, + blob_id UUID NOT NULL, + filename VARCHAR(255) NOT NULL, + type VARCHAR(255) NOT NULL, + size INT8 NOT NULL, + meta JSONB NOT NULL DEFAULT '{}', + creator VARCHAR(255) NOT NULL, + created TIMESTAMPTZ NOT NULL DEFAULT now(), + message_created TIMESTAMPTZ NOT NULL, PRIMARY KEY (workspace_id, card_id, message_id, blob_id) ); CREATE INDEX IF NOT EXISTS files_workspace_card_message_idx ON communication.files (workspace_id, card_id, message_id); + CREATE TABLE IF NOT EXISTS communication.reactions ( workspace_id UUID NOT NULL, @@ -155,38 +195,37 @@ function migrationV1_1(): [string, string] { reaction VARCHAR(100) NOT NULL, creator VARCHAR(255) NOT NULL, created TIMESTAMPTZ NOT NULL DEFAULT now(), - - FOREIGN KEY (workspace_id, card_id, message_id) REFERENCES communication.messages (workspace_id, card_id, id) ON DELETE CASCADE, - PRIMARY KEY (workspace_id, card_id, message_id, creator, reaction) + PRIMARY KEY (workspace_id, card_id, message_id, creator, reaction), + FOREIGN KEY (workspace_id, card_id, message_id) REFERENCES communication.messages (workspace_id, card_id, id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_reactions_workspace_card_message ON communication.reactions (workspace_id, card_id, message_id); + CREATE TABLE IF NOT EXISTS communication.thread ( - workspace_id UUID NOT NULL, - card_id VARCHAR(255) NOT NULL, - message_id INT8 NOT NULL, - thread_id VARCHAR(255) NOT NULL, - replies_count INT NOT NULL, - last_reply TIMESTAMPTZ NOT NULL, - + workspace_id UUID NOT NULL, + card_id VARCHAR(255) NOT NULL, + message_id INT8 NOT NULL, + thread_id VARCHAR(255) NOT NULL, + thread_type VARCHAR(255) NOT NULL, + replies_count INT NOT NULL, + last_reply TIMESTAMPTZ NOT NULL, + message_created TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (workspace_id, thread_id), - UNIQUE (workspace_id, card_id, message_id) + CONSTRAINT thread_unique_constraint UNIQUE (workspace_id, card_id, message_id) ); - CREATE INDEX IF NOT EXISTS idx_thread_workspace_card_message ON communication.thread (workspace_id, thread_id); + CREATE INDEX IF NOT EXISTS idx_thread_workspace_thread_message ON communication.thread (workspace_id, thread_id); CREATE INDEX IF NOT EXISTS idx_thread_workspace_card_message ON communication.thread (workspace_id, card_id, message_id); + CREATE TABLE IF NOT EXISTS communication.notification_context ( id INT8 NOT NULL DEFAULT unique_rowid(), - workspace_id UUID NOT NULL, card_id VARCHAR(255) NOT NULL, account UUID NOT NULL, - last_view TIMESTAMPTZ NOT NULL DEFAULT now(), last_update TIMESTAMPTZ NOT NULL DEFAULT now(), - PRIMARY KEY (id), UNIQUE (workspace_id, card_id, account) ); @@ -195,10 +234,9 @@ function migrationV1_1(): [string, string] { ( id INT8 NOT NULL DEFAULT unique_rowid(), context_id INT8 NOT NULL, - message_id INT8, + message_id INT8 NOT NULL, created TIMESTAMPTZ NOT NULL, content JSONB NOT NULL DEFAULT '{}', - PRIMARY KEY (id), FOREIGN KEY (context_id) REFERENCES communication.notification_context (id) ON DELETE CASCADE ); @@ -211,15 +249,10 @@ function migrationV1_1(): [string, string] { card_id VARCHAR(255) NOT NULL, account UUID NOT NULL, date TIMESTAMPTZ NOT NULL DEFAULT now(), - + card_type VARCHAR(255) NOT NULL, PRIMARY KEY (workspace_id, card_id, account) ); - ` - return ['init_tables_01', sql] -} -function migrationV2_1(): [string, string] { - const sql = ` CREATE TABLE IF NOT EXISTS communication.label ( workspace_id UUID NOT NULL, @@ -231,129 +264,5 @@ function migrationV2_1(): [string, string] { PRIMARY KEY (workspace_id, card_id, label_id, account) ); ` - return ['init_labels_02', sql] -} - -function migrationV3_1(): [string, string] { - const sql = ` - ALTER TABLE communication.collaborators - ADD COLUMN IF NOT EXISTS card_type VARCHAR(255) NOT NULL DEFAULT 'card:class:Card'; - ` - return ['add_card_type_to_collaborators_03', sql] -} - -function migrationV4_1(): [string, string] { - const sql = ` - ALTER TABLE communication.messages - ADD COLUMN IF NOT EXISTS external_id VARCHAR(255); - ` - return ['message_add_external_id_column', sql] -} - -function migrationV4_2(): [string, string] { - const sql = ` - CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_unique_external_id - ON communication.messages (external_id) - WHERE external_id IS NOT NULL; - ` - return ['message_add_external_id_column_unique_index', sql] -} - -function migrationV5_1(): [string, string] { - const sql = ` - ALTER TABLE communication.reactions - DROP CONSTRAINT IF EXISTS reactions_workspace_id_card_id_message_id_fkey; - ` - return ['remove-reactions-fk_v5_1', sql] -} - -function migrationV5_2(): [string, string] { - const sql = ` - ALTER TABLE communication.messages - ALTER COLUMN id SET DEFAULT unique_rowid(); - - ALTER TABLE communication.messages - DROP CONSTRAINT IF EXISTS messages_pkey, - ADD CONSTRAINT messages_pkey PRIMARY KEY (id);` - return ['migrate-message-id_v5_2', sql] -} - -function migrationV5_3(): [string, string] { - const sql = ` - ALTER TABLE communication.messages_groups ADD COLUMN from_date TIMESTAMPTZ; - ALTER TABLE communication.messages_groups ADD COLUMN to_date TIMESTAMPTZ; - ALTER TABLE communication.patch ADD COLUMN message_created TIMESTAMPTZ; - ALTER TABLE communication.files ADD COLUMN message_created TIMESTAMPTZ; - ALTER TABLE communication.thread ADD COLUMN IF NOT EXISTS message_created TIMESTAMPTZ NOT NULL DEFAULT now(); - DROP INDEX IF EXISTS communication.thread_workspace_id_card_id_message_id_key CASCADE; - ALTER TABLE communication.thread ADD CONSTRAINT thread_unique_constraint UNIQUE (message_id); - ` - return ['add-date-columns_v5_3', sql] -} - -function migrationV5_4(): [string, string] { - const sql = ` - UPDATE communication.messages_groups - SET from_date = from_sec::TIMESTAMPTZ, - to_date = to_sec::TIMESTAMPTZ; - - ALTER TABLE communication.messages_groups ALTER COLUMN from_date SET NOT NULL; - ALTER TABLE communication.messages_groups ALTER COLUMN to_date SET NOT NULL; - - ALTER TABLE communication.messages_groups DROP COLUMN from_sec; - ALTER TABLE communication.messages_groups DROP COLUMN to_sec; - - UPDATE communication.patch - SET message_created = message_created_sec::TIMESTAMPTZ; - - ALTER TABLE communication.patch ALTER COLUMN message_created SET NOT NULL; - ALTER TABLE communication.patch DROP COLUMN message_created_sec; - - UPDATE communication.files - SET message_created = message_created_sec::TIMESTAMPTZ; - - ALTER TABLE communication.files ALTER COLUMN message_created SET NOT NULL; - ALTER TABLE communication.files DROP COLUMN message_created_sec; - ` - return ['migrate-date-values_v5_4', sql] -} - -function migrationV5_5(): [string, string] { - const sql = ` - ALTER TABLE communication.reactions - DROP CONSTRAINT IF EXISTS reactions_pkey; - ALTER TABLE communication.reactions - ADD CONSTRAINT reactions_pkey PRIMARY KEY (message_id, creator, reaction); - ` - return ['migrate-reactions-pk_v5_5', sql] -} - -function migrationV5_6(): [string, string] { - const sql = ` - ALTER TABLE communication.files - DROP CONSTRAINT IF EXISTS files_pkey; - ALTER TABLE communication.files - ADD CONSTRAINT files_pkey PRIMARY KEY (message_id, blob_id); - - ALTER TABLE communication.reactions - ADD CONSTRAINT reactions_message_fkey FOREIGN KEY (message_id) - REFERENCES communication.messages (id) ON DELETE CASCADE; - ` - return ['migrate-constraints_v5_6', sql] -} - -function migrationV6_1(): [string, string] { - const sql = ` - DROP INDEX IF EXISTS communication.idx_messages_unique_external_id CASCADE; - ` - return ['message_drop_external_id_unique_index', sql] -} - -function migrationV6_2(): [string, string] { - const sql = ` - CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_unique_workspace_card_external_id - ON communication.messages (workspace_id, card_id, external_id) - WHERE external_id IS NOT NULL; - ` - return ['idx_messages_unique_workspace_card_external_id', sql] + return ['reinit_tables-v1_2', sql] } diff --git a/packages/query/src/messages/query.ts b/packages/query/src/messages/query.ts index 5f9dd20744..90ad211948 100644 --- a/packages/query/src/messages/query.ts +++ b/packages/query/src/messages/query.ts @@ -951,7 +951,7 @@ export class MessagesQuery implements PagedQuery { result.push(PatchType.addFile, PatchType.removeFile) } if (this.params.replies === true) { - result.push(PatchType.addReply, PatchType.removeReply) + result.push(PatchType.updateThread) } return result } diff --git a/packages/query/src/notification-contexts/query.ts b/packages/query/src/notification-contexts/query.ts index e22f2b1cab..824543769c 100644 --- a/packages/query/src/notification-contexts/query.ts +++ b/packages/query/src/notification-contexts/query.ts @@ -17,7 +17,6 @@ import { type FindNotificationContextParams, type Notification, type NotificationContext, - PatchType, SortingOrder, type WorkspaceID } from '@hcengineering/communication-types' @@ -43,8 +42,6 @@ import { QueryResult } from '../result' import { WindowImpl } from '../window' import { loadMessageFromGroup } from '../utils' -const allowedPatchTypes = [PatchType.update, PatchType.addReaction, PatchType.removeReaction] - export class NotificationContextsQuery implements PagedQuery { private result: QueryResult | Promise> private forward: Promise | NotificationContext[] = [] @@ -311,9 +308,7 @@ export class NotificationContextsQuery implements PagedQuery ({ ...it, message: - it.messageId === event.patch.message && it.message != null - ? applyPatch(it.message, event.patch, allowedPatchTypes) - : it.message + it.messageId === event.patch.message && it.message != null ? applyPatch(it.message, event.patch) : it.message })) }) diff --git a/packages/rest-client/package.json b/packages/rest-client/package.json index cdba5818af..5351b109fe 100644 --- a/packages/rest-client/package.json +++ b/packages/rest-client/package.json @@ -29,7 +29,7 @@ "@hcengineering/communication-types": "workspace:*", "@hcengineering/communication-shared": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", - "@hcengineering/core": "^0.7.28", + "@hcengineering/core": "^0.7.88", "snappyjs": "^0.7.0" }, "repository": { diff --git a/packages/rest-client/src/rest.ts b/packages/rest-client/src/rest.ts index 7d5de851fc..51002d836d 100644 --- a/packages/rest-client/src/rest.ts +++ b/packages/rest-client/src/rest.ts @@ -102,7 +102,9 @@ class RestClientImpl implements RestClient { creator: SocialID, type: MessageType, data?: MessageData, - created?: Date + created?: Date, + externalId?: string, + id?: MessageID ): Promise { const result = await this.event({ type: MessageRequestEventType.CreateMessage, @@ -112,7 +114,9 @@ class RestClientImpl implements RestClient { content, creator, data, - created + created, + externalId, + id }) return result as CreateMessageResult } @@ -130,8 +134,8 @@ class RestClientImpl implements RestClient { messageCreated, card, message, - content, - creator + creator, + data: { content } }) } diff --git a/packages/sdk-types/package.json b/packages/sdk-types/package.json index 732e4b3cfc..9038e0f791 100644 --- a/packages/sdk-types/package.json +++ b/packages/sdk-types/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@hcengineering/communication-types": "workspace:*", - "@hcengineering/core": "^0.7.28" + "@hcengineering/core": "^0.7.88" }, "peerDependencies": { "typescript": "^5.6.3" diff --git a/packages/sdk-types/src/db.ts b/packages/sdk-types/src/db.ts index e5eabaa3c3..0efa11e26c 100644 --- a/packages/sdk-types/src/db.ts +++ b/packages/sdk-types/src/db.ts @@ -39,7 +39,8 @@ import type { Label, FindLabelsParams, LabelID, - CardType + CardType, + PatchData } from '@hcengineering/communication-types' export interface DbAdapter { @@ -50,7 +51,8 @@ export interface DbAdapter { creator: SocialID, created: Date, data?: MessageData, - externalId?: string + externalId?: string, + id?: MessageID ): Promise removeMessages(card: CardID, ids: MessageID[], socialIds?: SocialID[]): Promise @@ -59,7 +61,7 @@ export interface DbAdapter { message: MessageID, messageCreated: Date, type: PatchType, - content: RichText, + data: PatchData, creator: SocialID, created: Date ): Promise @@ -96,7 +98,14 @@ export interface DbAdapter { ): Promise removeFile(card: CardID, message: MessageID, blobId: BlobID): Promise - createThread(card: CardID, message: MessageID, messageCreated: Date, thread: CardID, created: Date): Promise + createThread( + card: CardID, + message: MessageID, + messageCreated: Date, + thread: CardID, + threadType: CardType, + created: Date + ): Promise updateThread(thread: CardID, op: 'increment' | 'decrement', lastReply?: Date): Promise findMessages(params: FindMessagesParams): Promise diff --git a/packages/sdk-types/src/requestEvents/message.ts b/packages/sdk-types/src/requestEvents/message.ts index 066c99d3a8..d357958154 100644 --- a/packages/sdk-types/src/requestEvents/message.ts +++ b/packages/sdk-types/src/requestEvents/message.ts @@ -23,7 +23,8 @@ import type { PatchType, MessageType, MessageData, - CardType + CardType, + PatchData } from '@hcengineering/communication-types' import type { BaseRequestEvent } from './common' @@ -70,6 +71,7 @@ export interface CreateMessageEvent extends BaseRequestEvent { data?: MessageData externalId?: string created?: Date + id?: MessageID } export interface RemoveMessagesEvent extends BaseRequestEvent { @@ -84,7 +86,7 @@ export interface CreatePatchEvent extends BaseRequestEvent { card: CardID message: MessageID messageCreated: Date - content: RichText + data: PatchData creator: SocialID } @@ -133,6 +135,7 @@ export interface CreateThreadEvent extends BaseRequestEvent { message: MessageID messageCreated: Date thread: CardID + threadType: CardType } export interface UpdateThreadEvent extends BaseRequestEvent { diff --git a/packages/sdk-types/src/serverApi.ts b/packages/sdk-types/src/serverApi.ts index 48145c529b..d502a63b1d 100644 --- a/packages/sdk-types/src/serverApi.ts +++ b/packages/sdk-types/src/serverApi.ts @@ -23,7 +23,9 @@ import type { NotificationContext, Notification, FindLabelsParams, - Label + Label, + FindCollaboratorsParams, + Collaborator } from '@hcengineering/communication-types' import type { Account } from '@hcengineering/core' @@ -49,6 +51,7 @@ export interface ServerApi { ): Promise findLabels(session: SessionData, params: FindLabelsParams): Promise + findCollaborators(session: SessionData, params: FindCollaboratorsParams): Promise event(session: SessionData, event: RequestEvent): Promise diff --git a/packages/server/package.json b/packages/server/package.json index 0b30822ee4..b1ea23a8cd 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -24,13 +24,14 @@ "@types/ws": "^8.5.13" }, "dependencies": { - "@hcengineering/account-client": "^0.7.28", + "@hcengineering/account-client": "^0.7.88", "@hcengineering/communication-cockroach": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-types": "workspace:*", - "@hcengineering/core": "^0.7.28", - "@hcengineering/server-token": "^0.7.28", - "@hcengineering/platform": "^0.7.28", + "@hcengineering/communication-yaml": "workspace:*", + "@hcengineering/core": "^0.7.88", + "@hcengineering/server-token": "^0.7.88", + "@hcengineering/platform": "^0.7.88", "zod": "^3.24.2" }, "peerDependencies": { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 9a5dbe2a02..336ab639cc 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -25,7 +25,9 @@ import type { WorkspaceID, Notification, FindLabelsParams, - Label + Label, + FindCollaboratorsParams, + Collaborator } from '@hcengineering/communication-types' import { createDbAdapter } from '@hcengineering/communication-cockroach' import type { EventResult, RequestEvent, ServerApi, SessionData } from '@hcengineering/communication-sdk-types' @@ -89,6 +91,10 @@ export class Api implements ServerApi { return await this.middlewares.findLabels(session, params) } + async findCollaborators(session: SessionData, params: FindCollaboratorsParams): Promise { + return await this.middlewares.findCollaborators(session, params) + } + async unsubscribeQuery(session: SessionData, id: number): Promise { await this.middlewares.unsubscribeQuery(session, id) } diff --git a/packages/server/src/metadata.ts b/packages/server/src/metadata.ts index 04951b1060..3348fe61e0 100644 --- a/packages/server/src/metadata.ts +++ b/packages/server/src/metadata.ts @@ -19,6 +19,7 @@ export function getMetadata(): Metadata { return { msg2fileUrl: process.env.MSG2FILE_URL ?? '', accountsUrl: process.env.ACCOUNTS_URL ?? '', - secret: process.env.SERVER_SECRET + secret: process.env.SERVER_SECRET, + filesUrl: process.env.FILES_URL ?? '' } } diff --git a/packages/server/src/middleware/base.ts b/packages/server/src/middleware/base.ts index 0dfcd6f674..1491b2f61a 100644 --- a/packages/server/src/middleware/base.ts +++ b/packages/server/src/middleware/base.ts @@ -29,7 +29,9 @@ import type { NotificationContext, Notification, FindLabelsParams, - Label + Label, + FindCollaboratorsParams, + Collaborator } from '@hcengineering/communication-types' import type { Middleware, MiddlewareContext, QueryId } from '../types' @@ -72,12 +74,16 @@ export class BaseMiddleware implements Middleware { return await this.provideFindLabels(session, params, queryId) } + async findCollaborators(session: SessionData, params: FindCollaboratorsParams): Promise { + return await this.provideFindCollaborators(session, params) + } + async event(session: SessionData, event: RequestEvent, derived: boolean): Promise { return await this.provideEvent(session, event, derived) } - async response(session: SessionData, event: ResponseEvent): Promise { - return await this.provideResponse(session, event) + async response(session: SessionData, event: ResponseEvent, derived: boolean): Promise { + return await this.provideResponse(session, event, derived) } unsubscribeQuery(session: SessionData, queryId: number): void { @@ -151,9 +157,19 @@ export class BaseMiddleware implements Middleware { return [] } - protected async provideResponse(session: SessionData, event: ResponseEvent): Promise { + protected async provideFindCollaborators( + session: SessionData, + params: FindCollaboratorsParams + ): Promise { if (this.next !== undefined) { - return this.next.response(session, event) + return this.next.findCollaborators(session, params) + } + return [] + } + + protected async provideResponse(session: SessionData, event: ResponseEvent, derived: boolean): Promise { + if (this.next !== undefined) { + return this.next.response(session, event, derived) } } } diff --git a/packages/server/src/middleware/broadcast.ts b/packages/server/src/middleware/broadcast.ts index 33bc8d08ed..7e11f6597e 100644 --- a/packages/server/src/middleware/broadcast.ts +++ b/packages/server/src/middleware/broadcast.ts @@ -118,7 +118,7 @@ export class BroadcastMiddleware extends BaseMiddleware implements Middleware { data.contextQueries.delete(queryId) } - async response(session: SessionData, event: ResponseEvent): Promise { + async response(session: SessionData, event: ResponseEvent, derived: boolean): Promise { const sessionIds: string[] = [] for (const [sessionId, session] of this.dataBySessionId.entries()) { if (this.match(event, session)) { @@ -133,7 +133,7 @@ export class BroadcastMiddleware extends BaseMiddleware implements Middleware { this.context.ctx.error('Failed to broadcast event', { error: e }) } } - await this.provideResponse(session, event) + await this.provideResponse(session, event, derived) } closeSession(sessionId: string): void { diff --git a/packages/server/src/middleware/db.ts b/packages/server/src/middleware/db.ts index a06a206149..ba53812328 100644 --- a/packages/server/src/middleware/db.ts +++ b/packages/server/src/middleware/db.ts @@ -13,19 +13,21 @@ // limitations under the License. // -import type { - FindLabelsParams, - FindMessagesGroupsParams, - FindMessagesParams, - FindNotificationContextParams, - FindNotificationsParams, - Label, - Message, - MessagesGroup, - Notification, - NotificationContext, - Patch, - Reaction +import { + type Collaborator, + type FindCollaboratorsParams, + type FindLabelsParams, + type FindMessagesGroupsParams, + type FindMessagesParams, + type FindNotificationContextParams, + type FindNotificationsParams, + type Label, + type Message, + type MessagesGroup, + type Notification, + type NotificationContext, + type Patch, + type Reaction } from '@hcengineering/communication-types' import { type AddCollaboratorsEvent, @@ -117,10 +119,14 @@ export class DatabaseMiddleware extends BaseMiddleware implements Middleware { return await this.db.findLabels(params) } - async event(session: SessionData, event: RequestEvent): Promise { + async findCollaborators(_: SessionData, params: FindCollaboratorsParams): Promise { + return await this.db.findCollaborators(params) + } + + async event(session: SessionData, event: RequestEvent, derived: boolean): Promise { const result = await this.processEvent(session, event) if (result.responseEvent) { - void this.context.head?.response(session, result.responseEvent) + void this.context.head?.response(session, result.responseEvent, derived) } return result.result ?? {} @@ -220,7 +226,8 @@ export class DatabaseMiddleware extends BaseMiddleware implements Middleware { event.creator, created, event.data, - event.externalId + event.externalId, + event.id ) const message: Message = { id, @@ -257,19 +264,19 @@ export class DatabaseMiddleware extends BaseMiddleware implements Middleware { event.message, event.messageCreated, event.patchType, - event.content, + event.data, event.creator, created ) - const patch: Patch = { + const patch = { type: event.patchType, messageCreated: event.messageCreated, message: event.message, - content: event.content, + data: event.data, creator: event.creator, created - } + } as Patch const responseEvent: PatchCreatedEvent = { _id: event._id, type: MessageResponseEventType.PatchCreated, @@ -505,13 +512,14 @@ export class DatabaseMiddleware extends BaseMiddleware implements Middleware { private async createThread(event: CreateThreadEvent): Promise { const date = new Date() - await this.db.createThread(event.card, event.message, event.messageCreated, event.thread, date) + await this.db.createThread(event.card, event.message, event.messageCreated, event.thread, event.threadType, date) const responseEvent: ThreadCreatedEvent = { _id: event._id, type: MessageResponseEventType.ThreadCreated, thread: { card: event.card, thread: event.thread, + threadType: event.threadType, message: event.message, messageCreated: event.messageCreated, repliesCount: 0, diff --git a/packages/server/src/middleware/triggers.ts b/packages/server/src/middleware/triggers.ts index 5d1f696c28..b7d1461b55 100644 --- a/packages/server/src/middleware/triggers.ts +++ b/packages/server/src/middleware/triggers.ts @@ -33,13 +33,14 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware { this.ctx = context.ctx.newChild('triggers', {}) } - async response(session: SessionData, event: ResponseEvent): Promise { + async response(session: SessionData, event: ResponseEvent, derived: boolean): Promise { const ctx: Omit = { metadata: this.context.metadata, db: this.db, workspace: this.context.workspace, account: session.account, registeredCards: this.context.registeredCards, + derived, execute: async (event: RequestEvent) => { return (await this.context.head?.event(session, event, true)) ?? {} } diff --git a/packages/server/src/middleware/validate.ts b/packages/server/src/middleware/validate.ts index 1dbadefc1e..4542e1a2cf 100644 --- a/packages/server/src/middleware/validate.ts +++ b/packages/server/src/middleware/validate.ts @@ -22,6 +22,8 @@ import { NotificationRequestEventType } from '@hcengineering/communication-sdk-types' import type { + Collaborator, + FindCollaboratorsParams, FindLabelsParams, FindMessagesGroupsParams, FindMessagesParams, @@ -90,6 +92,11 @@ export class ValidateMiddleware extends BaseMiddleware implements Middleware { return await this.provideFindLabels(session, params, queryId) } + async findCollaborators(session: SessionData, params: FindCollaboratorsParams): Promise { + this.validate(params, FindCollaboratorsParamsSchema) + return await this.provideFindCollaborators(session, params) + } + async event(session: SessionData, event: RequestEvent, derived: boolean): Promise { if (derived) return await this.provideEvent(session, event, derived) switch (event.type) { @@ -165,6 +172,7 @@ const CardType = z.string() const ContextID = z.string() const LabelID = z.string() const MessageData = z.any() +const PatchData = z.any() const MessageID = z.string() const MessageType = z.string() const MessagesGroup = z.any() @@ -233,6 +241,10 @@ const FindLabelsParamsSchema = FindParamsSchema.extend({ account: AccountID.optional() }).strict() +const FindCollaboratorsParamsSchema = FindParamsSchema.extend({ + card: CardID.optional(), + account: z.union([AccountID, z.array(AccountID)]).optional() +}).strict() //Events const BaseRequestEventSchema = z @@ -267,7 +279,8 @@ const CreateMessageEventSchema = BaseRequestEventSchema.extend({ creator: SocialID, data: MessageData.optional(), externalId: z.string().optional(), - created: Date.optional() + created: Date.optional(), + id: MessageID.optional() }).strict() const RemoveMessagesEventSchema = BaseRequestEventSchema.extend({ @@ -282,7 +295,7 @@ const CreatePatchEventSchema = BaseRequestEventSchema.extend({ card: CardID, message: MessageID, messageCreated: Date, - content: RichText, + data: PatchData, creator: SocialID }).strict() @@ -329,7 +342,8 @@ const CreateThreadEventSchema = BaseRequestEventSchema.extend({ card: CardID, message: MessageID, messageCreated: Date, - thread: CardID + thread: CardID, + threadType: CardType }).strict() const UpdateThreadEventSchema = BaseRequestEventSchema.extend({ diff --git a/packages/server/src/middlewares.ts b/packages/server/src/middlewares.ts index c8c3d8030b..76dce9c121 100644 --- a/packages/server/src/middlewares.ts +++ b/packages/server/src/middlewares.ts @@ -16,6 +16,8 @@ import type { MeasureContext } from '@hcengineering/core' import type { DbAdapter, EventResult, RequestEvent, SessionData } from '@hcengineering/communication-sdk-types' import type { + Collaborator, + FindCollaboratorsParams, FindLabelsParams, FindMessagesGroupsParams, FindMessagesParams, @@ -149,6 +151,11 @@ export class Middlewares { return await this.head.findLabels(session, params) } + async findCollaborators(session: SessionData, params: FindCollaboratorsParams): Promise { + if (this.head === undefined) return [] + return await this.head.findCollaborators(session, params) + } + async unsubscribeQuery(session: SessionData, id: number): Promise { if (this.head === undefined) return this.head?.unsubscribeQuery(session, id) diff --git a/packages/server/src/triggers/message.ts b/packages/server/src/triggers/message.ts index 11444a8dbf..3f6bcb4b49 100644 --- a/packages/server/src/triggers/message.ts +++ b/packages/server/src/triggers/message.ts @@ -25,14 +25,23 @@ import { NotificationRequestEventType, type PatchCreatedEvent, type RequestEvent, + type ThreadCreatedEvent, type UpdateThreadEvent } from '@hcengineering/communication-sdk-types' -import { type CardID, PatchType, type File } from '@hcengineering/communication-types' +import { + type AddFilePatchData, + type CardID, + type Message, + MessageType, + PatchType +} from '@hcengineering/communication-types' import { generateToken } from '@hcengineering/server-token' import { concatLink, systemAccountUuid } from '@hcengineering/core' +import { generateMessageId } from '@hcengineering/communication-shared' import type { TriggerCtx, TriggerFn, Triggers } from '../types' import { findAccount } from '../utils' +import { findMessageInFiles } from './utils' async function onMessagesGroupCreated(ctx: TriggerCtx, event: MessagesGroupCreatedEvent): Promise { ctx.registeredCards.delete(event.group.card) @@ -49,11 +58,11 @@ async function onMessagesRemoved(ctx: TriggerCtx, event: MessagesRemovedEvent): return event.messages.flatMap(() => { const patchEvent: CreatePatchEvent = { type: MessageRequestEventType.CreatePatch, - patchType: PatchType.removeReply, + patchType: PatchType.updateThread, card: thread.card, message: thread.message, messageCreated: thread.messageCreated, - content: thread.thread, + data: { thread: thread.thread, threadType: thread.threadType, replies: 'decrement' }, creator: socialId } const threadEvent: UpdateThreadEvent = { @@ -62,7 +71,7 @@ async function onMessagesRemoved(ctx: TriggerCtx, event: MessagesRemovedEvent): replies: 'decrement' } - return [patchEvent, threadEvent] + return [threadEvent] }) } @@ -71,7 +80,7 @@ async function onFileCreated(ctx: TriggerCtx, event: FileCreatedEvent): Promise< if (message !== undefined) return [] const { file } = event - const patchContent: Omit = { + const patchData: AddFilePatchData = { blobId: file.blobId, type: file.type, filename: file.filename, @@ -85,7 +94,7 @@ async function onFileCreated(ctx: TriggerCtx, event: FileCreatedEvent): Promise< card: event.card, message: file.message, messageCreated: file.messageCreated, - content: JSON.stringify(patchContent), + data: patchData, creator: file.creator } ] @@ -103,7 +112,7 @@ async function onFileRemoved(ctx: TriggerCtx, event: FileRemovedEvent): Promise< card: event.card, message: event.message, messageCreated: event.messageCreated, - content: JSON.stringify({ blobId }), + data: { blobId }, creator: event.creator } ] @@ -157,6 +166,7 @@ async function addCollaborators(ctx: TriggerCtx, event: MessageCreatedEvent): Pr } async function addThreadReply(ctx: TriggerCtx, event: MessageCreatedEvent): Promise { + if (event.message.type !== MessageType.Message || ctx.derived) return [] const { message } = event const thread = await ctx.db.findThread(message.card) if (thread === undefined) return [] @@ -164,11 +174,11 @@ async function addThreadReply(ctx: TriggerCtx, event: MessageCreatedEvent): Prom return [ { type: MessageRequestEventType.CreatePatch, - patchType: PatchType.addReply, + patchType: PatchType.updateThread, card: thread.card, message: thread.message, messageCreated: thread.messageCreated, - content: thread.thread, + data: { thread: thread.thread, threadType: thread.threadType, replies: 'increment' }, creator: message.creator }, { @@ -180,6 +190,81 @@ async function addThreadReply(ctx: TriggerCtx, event: MessageCreatedEvent): Prom ] } +async function onThreadCreated(ctx: TriggerCtx, event: ThreadCreatedEvent): Promise { + let message: Message | undefined = ( + await ctx.db.findMessages({ + card: event.thread.card, + id: event.thread.message, + limit: 1, + files: true, + reactions: true + }) + )[0] + + const result: RequestEvent[] = [] + + if (message === undefined) { + message = await findMessageInFiles(ctx, event.thread.card, event.thread.message, event.thread.messageCreated) + + if (message !== undefined) { + result.push({ + type: MessageRequestEventType.CreatePatch, + patchType: PatchType.updateThread, + card: event.thread.card, + message: event.thread.message, + messageCreated: event.thread.messageCreated, + data: { thread: event.thread.thread, threadType: event.thread.threadType }, + creator: message.creator + }) + } + } + + if (message === undefined) { + return [] + } + + const messageId = generateMessageId() + result.push({ + type: MessageRequestEventType.CreateMessage, + messageType: message.type, + card: event.thread.thread, + cardType: event.thread.threadType, + content: message.content, + creator: message.creator, + data: message.data, + externalId: message.externalId, + created: message.created, + id: messageId + }) + + for (const file of message.files) { + result.push({ + type: MessageRequestEventType.CreateFile, + card: event.thread.thread, + message: messageId, + messageCreated: message.created, + blobId: file.blobId, + fileType: file.type, + filename: file.filename, + size: file.size, + creator: file.creator + }) + } + + for (const reaction of message.reactions) { + result.push({ + type: MessageRequestEventType.CreateReaction, + card: event.thread.thread, + message: messageId, + messageCreated: message.created, + reaction: reaction.reaction, + creator: reaction.creator + }) + } + + return result +} + const triggers: Triggers = [ ['add_collaborators_on_message_created', MessageResponseEventType.MessageCreated, addCollaborators as TriggerFn], ['add_thread_reply_on_message_created', MessageResponseEventType.MessageCreated, addThreadReply as TriggerFn], @@ -188,7 +273,8 @@ const triggers: Triggers = [ ['on_messages_group_created', MessageResponseEventType.MessagesGroupCreated, onMessagesGroupCreated as TriggerFn], ['remove_reply_on_messages_removed', MessageResponseEventType.MessagesRemoved, onMessagesRemoved as TriggerFn], ['on_file_created', MessageResponseEventType.FileCreated, onFileCreated as TriggerFn], - ['on_file_removed', MessageResponseEventType.FileRemoved, onFileRemoved as TriggerFn] + ['on_file_removed', MessageResponseEventType.FileRemoved, onFileRemoved as TriggerFn], + ['on_thread_created', MessageResponseEventType.ThreadCreated, onThreadCreated as TriggerFn] ] export default triggers diff --git a/packages/server/src/triggers/utils.ts b/packages/server/src/triggers/utils.ts new file mode 100644 index 0000000000..93b3dbc755 --- /dev/null +++ b/packages/server/src/triggers/utils.ts @@ -0,0 +1,76 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type CardID, type Message, type MessageID, SortingOrder } from '@hcengineering/communication-types' +import { loadGroupFile } from '@hcengineering/communication-yaml' +import { applyPatches } from '@hcengineering/communication-shared' + +import type { TriggerCtx } from '../types' + +export async function findMessage( + ctx: TriggerCtx, + card: CardID, + id: MessageID, + created: Date +): Promise { + const message = (await ctx.db.findMessages({ card, id, limit: 1, files: true }))[0] + if (message !== undefined) { + return message + } + return await findMessageInFiles(ctx, card, id, created) +} + +export async function findMessageInFiles( + ctx: TriggerCtx, + card: CardID, + id: MessageID, + created: Date +): Promise { + const filesUrl = ctx.metadata.filesUrl + if (filesUrl === '') { + ctx.ctx.error('FILES_URL is missing', { filesUrl }) + return undefined + } + + const group = ( + await ctx.db.findMessagesGroups({ + card, + fromDate: { lessOrEqual: created }, + toDate: { greaterOrEqual: created }, + limit: 1, + order: SortingOrder.Ascending, + orderBy: 'fromDate' + }) + )[0] + + if (group === undefined) { + return undefined + } + + try { + const parsedFile = await loadGroupFile(ctx.workspace, filesUrl, group, { retries: 3 }) + const messageFromFile = parsedFile.messages.find((it) => it.id === id) + if (messageFromFile === undefined) { + return undefined + } + + const patches = (group.patches ?? []).filter((it) => it.message === id) + + return patches.length > 0 ? applyPatches(messageFromFile, patches) : messageFromFile + } catch (e) { + ctx.ctx.error('Failed to find message in files', { card, id, created }) + ctx.ctx.error('Error:', { error: e }) + } +} diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 6592d8344e..bfa5a892eb 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -24,6 +24,8 @@ import type { } from '@hcengineering/communication-sdk-types' import type { CardID, + Collaborator, + FindCollaboratorsParams, FindLabelsParams, FindMessagesGroupsParams, FindMessagesParams, @@ -40,6 +42,7 @@ import type { export interface Metadata { msg2fileUrl: string accountsUrl: string + filesUrl: string secret?: string } @@ -65,12 +68,13 @@ export interface Middleware { ) => Promise findLabels: (session: SessionData, params: FindLabelsParams, queryId?: QueryId) => Promise + findCollaborators: (session: SessionData, params: FindCollaboratorsParams) => Promise event: (session: SessionData, event: RequestEvent, derived: boolean) => Promise unsubscribeQuery: (session: SessionData, queryId: number) => void - response: (session: SessionData, event: ResponseEvent) => Promise + response: (session: SessionData, event: ResponseEvent, derived: boolean) => Promise closeSession: (sessionId: string) => void close: () => void @@ -109,6 +113,7 @@ export interface TriggerCtx { workspace: WorkspaceID account: Account registeredCards: Set + derived: boolean execute: (event: RequestEvent) => Promise } diff --git a/packages/server/src/utils.ts b/packages/server/src/utils.ts index 3255368a55..6bc186b799 100644 --- a/packages/server/src/utils.ts +++ b/packages/server/src/utils.ts @@ -16,6 +16,7 @@ import type { AccountID, SocialID } from '@hcengineering/communication-types' import { generateToken } from '@hcengineering/server-token' import { systemAccountUuid } from '@hcengineering/core' +import { getClient as getAccountClient } from '@hcengineering/account-client' import type { TriggerCtx } from './types' @@ -28,47 +29,11 @@ export async function findAccount(ctx: TriggerCtx, socialString: SocialID): Prom if (url === '') return undefined const token = generateToken(systemAccountUuid) - // const account = getAccountClient(ctx.metadata.accountsUrl, token) + const account = getAccountClient(ctx.metadata.accountsUrl, token) try { - //TODO: FIXME - return await fetchAccount(socialString, url, token) + return (await account.findPersonBySocialId(socialString, true)) as AccountID | undefined } catch (err: any) { ctx.ctx.warn('Cannot find account', { socialString, err }) } } - -//TODO: replace with AccountClient -async function fetchAccount(socialId: SocialID, url: string, token: string): Promise { - const body = { - method: 'findPersonBySocialId' as const, - params: { socialId, requireAccount: true } - } - const request: RequestInit = { - keepalive: true, - headers: { - ...(token === undefined - ? {} - : { - Authorization: 'Bearer ' + token - }) - } - } - - const response = await fetch(url, { - ...request, - headers: { - ...request.headers, - 'Content-Type': 'application/json' - }, - method: 'POST', - body: JSON.stringify(body) - }) - - const result = await response.json() - if (result.error != null) { - throw Error(result.error) - } - - return result.result as AccountID | undefined -} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index f1ad93e6ab..d871556b29 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -15,3 +15,4 @@ export * from './retry' export * from './patch' +export * from './utils' diff --git a/packages/shared/src/patch.ts b/packages/shared/src/patch.ts index 04ec5cc5f2..a34645c3c4 100644 --- a/packages/shared/src/patch.ts +++ b/packages/shared/src/patch.ts @@ -16,16 +16,14 @@ import { PatchType, type BlobID, - type CardID, type Message, type Patch, type Reaction, type SocialID, - type File + type AddFilePatchData, + type UpdateThreadPatchData } from '@hcengineering/communication-types' -type PatchFile = Pick - export function applyPatches(message: Message, patches: Patch[], allowedPatchTypes: PatchType[] = []): Message { if (patches.length === 0) return message @@ -42,25 +40,24 @@ export function applyPatch(message: Message, patch: Patch, allowedPatchTypes: Pa return { ...message, edited: patch.created, - content: patch.content + content: patch.data.content ?? message.content, + data: patch.data.data ?? message.data } case PatchType.addReaction: return addReaction(message, { message: message.id, - reaction: patch.content, + reaction: patch.data.reaction, creator: patch.creator, created: patch.created }) case PatchType.removeReaction: - return removeReaction(message, patch.content, patch.creator) - case PatchType.addReply: - return addReply(message, patch.content as CardID, patch.created) - case PatchType.removeReply: - return removeReply(message, patch.content as CardID) + return removeReaction(message, patch.data.reaction, patch.creator) case PatchType.addFile: - return addFile(message, JSON.parse(patch.content) as PatchFile, patch.created, patch.creator) + return addFile(message, patch.data, patch.created, patch.creator) case PatchType.removeFile: - return removeFile(message, patch.content as BlobID) + return removeFile(message, patch.data.blobId) + case PatchType.updateThread: + return updateThread(message, patch.data, patch.created) } return message @@ -81,36 +78,38 @@ function removeReaction(message: Message, emoji: string, creator: SocialID): Mes } } -function addReply(message: Message, thread: CardID, created: Date): Message { - if (message.thread === undefined) { - return { - ...message, - thread: { - card: message.card, - message: message.id, - messageCreated: message.created, - thread, - repliesCount: 1, - lastReply: created - } - } +function updateThread(message: Message, data: UpdateThreadPatchData, created: Date): Message { + const thread = message.thread ?? { + card: message.card, + message: message.id, + messageCreated: message.created, + thread: data.thread, + threadType: data.threadType, + repliesCount: 0, + lastReply: created } - if (message.thread.thread !== thread) return message + thread.thread = data.thread + thread.threadType = data.threadType + + if (data.replies === 'increment') { + thread.repliesCount = thread.repliesCount + 1 + thread.lastReply = created + } + + if (data.replies === 'decrement') { + thread.repliesCount = Math.max(thread.repliesCount - 1, 0) + } return { ...message, - thread: { - ...message.thread, - repliesCount: message.thread.repliesCount + 1, - lastReply: created - } + thread } } -function addFile(message: Message, file: PatchFile, created: Date, creator: SocialID): Message { +function addFile(message: Message, data: AddFilePatchData, created: Date, creator: SocialID): Message { message.files.push({ - ...file, + ...data, card: message.card, message: message.id, created, @@ -129,15 +128,3 @@ function removeFile(message: Message, blobId: BlobID): Message { files } } - -function removeReply(message: Message, thread: CardID): Message { - if (message.thread === undefined || message.thread.thread !== thread) return message - - return { - ...message, - thread: { - ...message.thread, - repliesCount: message.thread.repliesCount - 1 - } - } -} diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts new file mode 100644 index 0000000000..d9a490c4f2 --- /dev/null +++ b/packages/shared/src/utils.ts @@ -0,0 +1,37 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { MessageID } from '@hcengineering/communication-types' + +const COUNTER_BITS = 12n +const RANDOM_BITS = 10n +const MAX_SEQUENCE = (1n << COUNTER_BITS) - 1n +const MAX_RANDOM = (1n << RANDOM_BITS) - 1n + +let counter = 0n + +/** + * Generate 64-bit MessageID and return it as string. + */ +export function generateMessageId(): MessageID { + const ts = BigInt(Date.now()) + counter = counter < MAX_SEQUENCE ? counter + 1n : 0n + + const random = BigInt(Math.floor(Math.random() * Number(MAX_RANDOM + 1n))) + + const id = (ts << (COUNTER_BITS + RANDOM_BITS)) | (counter << RANDOM_BITS) | random + + return id.toString() as MessageID +} diff --git a/packages/types/package.json b/packages/types/package.json index 202cc6728b..3110327364 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -25,9 +25,9 @@ "typescript": "^5.6.3" }, "dependencies": { - "@hcengineering/core": "^0.7.28", - "@hcengineering/card": "^0.7.28", - "@hcengineering/tags": "^0.7.28" + "@hcengineering/core": "^0.7.88", + "@hcengineering/card": "^0.7.88", + "@hcengineering/tags": "^0.7.88" }, "repository": { "type": "git", diff --git a/packages/types/src/file.ts b/packages/types/src/file.ts index c807088c0b..1740fecc0a 100644 --- a/packages/types/src/file.ts +++ b/packages/types/src/file.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import type { BlobID, CardID, RichText, SocialID } from './core' +import type { BlobID, CardID, CardType, RichText, SocialID } from './core' import type { Message, MessageID, MessageType, MessageData } from './message' export interface FileMetadata { @@ -54,6 +54,7 @@ export interface FileReaction { export interface FileThread { thread: CardID + threadType: CardType repliesCount: number lastReply: Date } diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index af912a3fbe..4832d9df81 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -14,7 +14,7 @@ // import type { Attribute, Class, Mixin, Ref } from '@hcengineering/core' -import type { BlobID, CardID, ID, RichText, SocialID } from './core' +import type { BlobID, CardID, CardType, ID, RichText, SocialID } from './core' import type { Card, Tag } from '@hcengineering/card' export type MessageID = ID & { message: true } @@ -85,23 +85,98 @@ export interface MessagesGroup { patches?: Patch[] } -export interface Patch { +interface BasePatch { message: MessageID messageCreated: Date type: PatchType - content: string creator: SocialID created: Date + + data: Record +} + +export interface UpdatePatch extends BasePatch { + type: PatchType.update + data: UpdatePatchData +} + +export interface AddReactionPatch extends BasePatch { + type: PatchType.addReaction + data: AddReactionPatchData +} + +export interface RemoveReactionPatch extends BasePatch { + type: PatchType.removeReaction + data: RemoveReactionPatchData +} +export interface UpdateThreadPatch extends BasePatch { + type: PatchType.updateThread + data: UpdateThreadPatchData +} + +export interface AddFilePatch extends BasePatch { + type: PatchType.addFile + data: AddFilePatchData +} + +export interface RemoveFilePatch extends BasePatch { + type: PatchType.removeFile + data: RemoveFilePatchData +} + +export type Patch = + | UpdatePatch + | AddReactionPatch + | RemoveReactionPatch + | AddFilePatch + | RemoveFilePatch + | UpdateThreadPatch + +export type PatchData = + | UpdatePatchData + | AddReactionPatchData + | RemoveReactionPatchData + | AddFilePatchData + | RemoveFilePatchData + | UpdateThreadPatchData + +export interface UpdateThreadPatchData { + thread: CardID + threadType: CardType + replies?: 'increment' | 'decrement' +} + +export interface UpdatePatchData { + content?: RichText + data?: MessageData +} + +export interface AddReactionPatchData { + reaction: string +} + +export interface RemoveReactionPatchData { + reaction: string +} + +export interface AddFilePatchData { + blobId: BlobID + type: string + filename: string + size: number +} + +export interface RemoveFilePatchData { + blobId: BlobID } export enum PatchType { update = 'update', addReaction = 'addReaction', removeReaction = 'removeReaction', - addReply = 'addReply', - removeReply = 'removeReply', addFile = 'addFile', - removeFile = 'removeFile' + removeFile = 'removeFile', + updateThread = 'updateThread' } export interface Reaction { @@ -128,6 +203,7 @@ export interface Thread { message: MessageID messageCreated: Date thread: CardID + threadType: CardType repliesCount: number lastReply: Date } diff --git a/packages/yaml/src/deserialize.ts b/packages/yaml/src/deserialize.ts index 6455b535ea..8e8af4dd5b 100644 --- a/packages/yaml/src/deserialize.ts +++ b/packages/yaml/src/deserialize.ts @@ -29,6 +29,7 @@ export function deserializeMessage(message: Message): FileMessage { message.thread != null ? { thread: message.thread.thread, + threadType: message.thread.threadType, repliesCount: message.thread.repliesCount, lastReply: message.thread.lastReply } diff --git a/packages/yaml/src/parse.ts b/packages/yaml/src/parse.ts index 3137c5e1b3..de06ede61b 100644 --- a/packages/yaml/src/parse.ts +++ b/packages/yaml/src/parse.ts @@ -77,6 +77,7 @@ export function parseYaml(data: string): ParsedFile { message: message.id, messageCreated: message.created, thread: message.thread.thread, + threadType: message.thread.threadType, repliesCount: message.thread.repliesCount, lastReply: message.thread.lastReply }