Adjust groups (#31)

* Messages groups & threads
This commit is contained in:
Kristina
2025-02-27 11:14:55 +04:00
committed by GitHub
parent 197c3b4c27
commit c7184a2bb0
68 changed files with 2624 additions and 1527 deletions
+57 -84
View File
@@ -4,19 +4,19 @@
"": {
"name": "@hcengineering/communication",
"devDependencies": {
"@eslint/js": "^9.15.0",
"@types/bun": "^1.1.14",
"bun-types": "^1.1.38",
"eslint": "^9.15.0",
"@eslint/js": "^9.21.0",
"@types/bun": "^1.2.4",
"bun-types": "^1.2.4",
"eslint": "^9.21.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.3.3",
"typescript-eslint": "^8.15.0",
"eslint-plugin-prettier": "^5.2.3",
"prettier": "^3.5.2",
"typescript-eslint": "^8.25.0",
},
},
"packages/client-query": {
"name": "@hcengineering/communication-client-query",
"version": "0.1.9",
"version": "0.1.47",
"dependencies": {
"@hcengineering/communication-query": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
@@ -32,7 +32,7 @@
},
"packages/client-sqlite": {
"name": "@hcengineering/communication-client-sqlite",
"version": "0.1.9",
"version": "0.1.47",
"dependencies": {
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-sqlite-wasm": "workspace:*",
@@ -48,8 +48,9 @@
},
"packages/client-ws": {
"name": "@hcengineering/communication-client-ws",
"version": "0.1.9",
"version": "0.1.47",
"dependencies": {
"@hcengineering/communication-client-query": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
"@msgpack/msgpack": "^3.0.0-beta2",
@@ -63,12 +64,11 @@
},
"packages/cockroach": {
"name": "@hcengineering/communication-cockroach",
"version": "0.1.9",
"version": "0.1.47",
"dependencies": {
"@hcengineering/communication-core": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-shared": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
"pg": "8.12.0",
"postgres": "^3.4.4",
"uuid": "^11.0.3",
},
@@ -79,25 +79,10 @@
"typescript": "^5.6.3",
},
},
"packages/core": {
"name": "@hcengineering/communication-core",
"version": "0.1.9",
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
},
"devDependencies": {
"@types/bun": "^1.1.14",
},
"peerDependencies": {
"typescript": "^5.6.3",
},
},
"packages/examples": {
"name": "@hcengineering/communication-examples",
"version": "0.1.9",
"version": "0.1.47",
"dependencies": {
"@hcengineering/communication-client-query": "workspace:*",
"@hcengineering/communication-client-sqlite": "workspace:*",
"@hcengineering/communication-client-ws": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
},
@@ -110,9 +95,10 @@
},
"packages/query": {
"name": "@hcengineering/communication-query",
"version": "0.1.9",
"version": "0.1.47",
"dependencies": {
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-shared": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
"fast-equals": "^5.0.1",
},
@@ -126,7 +112,7 @@
},
"packages/sdk-types": {
"name": "@hcengineering/communication-sdk-types",
"version": "0.1.9",
"version": "0.1.47",
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
},
@@ -139,7 +125,7 @@
},
"packages/server": {
"name": "@hcengineering/communication-server",
"version": "0.1.9",
"version": "0.1.47",
"dependencies": {
"@hcengineering/communication-cockroach": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
@@ -156,9 +142,24 @@
"typescript": "^5.6.3",
},
},
"packages/shared": {
"name": "@hcengineering/communication-shared",
"version": "0.1.47",
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
"js-yaml": "^4.1.0",
},
"devDependencies": {
"@types/bun": "^1.1.14",
"@types/js-yaml": "^4.0.9",
},
"peerDependencies": {
"typescript": "^5.6.3",
},
},
"packages/sqlite-wasm": {
"name": "@hcengineering/communication-sqlite-wasm",
"version": "0.1.9",
"version": "0.1.47",
"dependencies": {
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
@@ -175,7 +176,7 @@
},
"packages/types": {
"name": "@hcengineering/communication-types",
"version": "0.1.9",
"version": "0.1.47",
"dependencies": {
"@hcengineering/card": "0.7.2",
"@hcengineering/core": "0.7.2",
@@ -199,15 +200,15 @@
"@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="],
"@eslint/core": ["@eslint/core@0.11.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA=="],
"@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.2.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ=="],
"@eslint/js": ["@eslint/js@9.20.0", "", {}, "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ=="],
"@eslint/js": ["@eslint/js@9.21.0", "", {}, "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="],
"@formatjs/ecma402-abstract": ["@formatjs/ecma402-abstract@2.3.3", "", { "dependencies": { "@formatjs/fast-memoize": "2.2.6", "@formatjs/intl-localematcher": "0.6.0", "decimal.js": "10", "tslib": "2" } }, "sha512-pJT1OkhplSmvvr6i3CWTPvC/FGC06MbN5TNBfRO6Ox62AEz90eMq+dVvtX9Bl3jxCEkS0tATzDarRZuOLw7oFg=="],
@@ -231,8 +232,6 @@
"@hcengineering/communication-cockroach": ["@hcengineering/communication-cockroach@workspace:packages/cockroach"],
"@hcengineering/communication-core": ["@hcengineering/communication-core@workspace:packages/core"],
"@hcengineering/communication-examples": ["@hcengineering/communication-examples@workspace:packages/examples"],
"@hcengineering/communication-query": ["@hcengineering/communication-query@workspace:packages/query"],
@@ -241,6 +240,8 @@
"@hcengineering/communication-server": ["@hcengineering/communication-server@workspace:packages/server"],
"@hcengineering/communication-shared": ["@hcengineering/communication-shared@workspace:packages/shared"],
"@hcengineering/communication-sqlite-wasm": ["@hcengineering/communication-sqlite-wasm@workspace:packages/sqlite-wasm"],
"@hcengineering/communication-types": ["@hcengineering/communication-types@workspace:packages/types"],
@@ -259,7 +260,7 @@
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.1", "", {}, "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
@@ -285,7 +286,7 @@
"@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="],
"@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
"@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
@@ -301,6 +302,8 @@
"@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="],
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
@@ -319,21 +322,21 @@
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.24.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/type-utils": "8.24.1", "@typescript-eslint/utils": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.25.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/type-utils": "8.25.0", "@typescript-eslint/utils": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.24.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.25.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/types": "8.25.0", "@typescript-eslint/typescript-estree": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1" } }, "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0" } }, "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.24.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/utils": "8.24.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.25.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.25.0", "@typescript-eslint/utils": "8.25.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.24.1", "", {}, "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.25.0", "", {}, "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.25.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/types": "8.25.0", "@typescript-eslint/typescript-estree": "8.25.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ=="],
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
@@ -357,7 +360,7 @@
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
"bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
@@ -391,7 +394,7 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.20.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g=="],
"eslint": ["eslint@9.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.21.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg=="],
"eslint-config-prettier": ["eslint-config-prettier@9.1.0", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw=="],
@@ -513,37 +516,13 @@
"periscopic": ["periscopic@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", "is-reference": "^3.0.0" } }, "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw=="],
"pg": ["pg@8.12.0", "", { "dependencies": { "pg-connection-string": "^2.6.4", "pg-pool": "^3.6.2", "pg-protocol": "^1.6.1", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.1.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ=="],
"pg-cloudflare": ["pg-cloudflare@1.1.1", "", {}, "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q=="],
"pg-connection-string": ["pg-connection-string@2.7.0", "", {}, "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-pool": ["pg-pool@3.7.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw=="],
"pg-protocol": ["pg-protocol@1.7.1", "", {}, "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ=="],
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"postgres": ["postgres@3.4.5", "", {}, "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.5.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw=="],
"prettier": ["prettier@3.5.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg=="],
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
@@ -569,8 +548,6 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -589,7 +566,7 @@
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"typescript-eslint": ["typescript-eslint@8.24.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/parser": "8.24.1", "@typescript-eslint/utils": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA=="],
"typescript-eslint": ["typescript-eslint@8.25.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.25.0", "@typescript-eslint/parser": "8.25.0", "@typescript-eslint/utils": "8.25.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
@@ -603,14 +580,10 @@
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.10.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+8 -7
View File
@@ -3,18 +3,19 @@
"type": "module",
"workspaces": ["packages/*"],
"scripts": {
"publish": "sh scripts/publish.sh",
"build": "sh scripts/build.sh && tsc --noEmit",
"lint": "eslint packages/**/src/*.ts",
"format": "prettier --write packages/**/src/*.ts && bun run lint"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"@types/bun": "^1.1.14",
"bun-types": "^1.1.38",
"eslint": "^9.15.0",
"@eslint/js": "^9.21.0",
"@types/bun": "^1.2.4",
"bun-types": "^1.2.4",
"eslint": "^9.21.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.3.3",
"typescript-eslint": "^8.15.0"
"eslint-plugin-prettier": "^5.2.3",
"prettier": "^3.5.2",
"typescript-eslint": "^8.25.0"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-client-query",
"version": "0.1.9",
"version": "0.1.47",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
+3 -2
View File
@@ -1,5 +1,6 @@
import { LiveQueries } from '@hcengineering/communication-query'
import type { QueryClient } from '@hcengineering/communication-sdk-types'
import type { WorkspaceID } from '@hcengineering/communication-types'
import { MessagesQuery, NotificationsQuery } from './query'
@@ -13,12 +14,12 @@ export function createNotificationsQuery(): NotificationsQuery {
return new NotificationsQuery(lq)
}
export function initLiveQueries(client: QueryClient) {
export function initLiveQueries(client: QueryClient, workspace: WorkspaceID, filesUrl: string): void {
if (lq != null) {
lq.close()
}
lq = new LiveQueries(client)
lq = new LiveQueries(client, workspace, filesUrl)
client.onEvent = (event) => {
void lq.onEvent(event)
+1 -1
View File
@@ -44,7 +44,7 @@ class BaseQuery<P extends Record<string, any>, C extends QueryCallback<any>> {
}
}
private needUpdate(params: FindMessagesParams, callback: MessagesQueryCallback): boolean {
private needUpdate(params: P, callback: C): boolean {
if (!deepEqual(params, this.oldQuery)) return true
if (!deepEqual(callback.toString(), this.oldCallback?.toString())) return true
return false
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-client-sqlite",
"version": "0.1.9",
"version": "0.1.47",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
+24 -6
View File
@@ -13,7 +13,10 @@ import {
type Notification,
type Attachment,
type Reaction,
type WorkspaceID
type WorkspaceID,
type FindMessagesGroupsParams,
type MessagesGroup,
PatchType
} from '@hcengineering/communication-types'
import {
type Client,
@@ -24,7 +27,8 @@ import {
} from '@hcengineering/communication-sdk-types'
import { createDbAdapter as createSqliteDbAdapter } from '@hcengineering/communication-sqlite-wasm'
class DbClient implements Client {
//TODO: FIXME
class DbClient {
onEvent: (event: ResponseEvent) => void = () => {}
constructor(
@@ -61,10 +65,14 @@ class DbClient implements Client {
this.onEvent({ type: ResponseEventType.MessageRemoved, message, card })
}
async createPatch(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise<void> {
async updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise<void> {
const created = new Date()
await this.db.createPatch(card, message, content, creator, created)
this.onEvent({ type: ResponseEventType.PatchCreated, card, patch: { message, content, creator, created } })
await this.db.createPatch(card, message, PatchType.update, content, creator, created)
this.onEvent({
type: ResponseEventType.PatchCreated,
card,
patch: { message, type: PatchType.update, content, creator, created }
})
}
async createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void> {
@@ -98,6 +106,10 @@ class DbClient implements Client {
return rawMessages.map((it) => this.toMessage(it))
}
async findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
return await this.db.findMessagesGroups(params)
}
async findMessage(params: FindMessagesParams): Promise<Message | undefined> {
return (await this.findMessages({ ...params, limit: 1 }))[0]
}
@@ -158,11 +170,17 @@ class DbClient implements Client {
return await this.db.findContexts(params, [this.personalWorkspace])
}
//eslint-disable-next-line @typescript-eslint/no-unused-vars
async findNotifications(params: FindNotificationsParams): Promise<Notification[]> {
//TODO: should we filter by workspace?
return await this.db.findNotifications(params, this.personalWorkspace)
}
//eslint-disable-next-line @typescript-eslint/no-unused-vars
async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void> {
//TODO: implement
}
async unsubscribeQuery() {
//ignore
}
@@ -178,5 +196,5 @@ export async function getSqliteClient(
dbUrl = 'file:communication.sqlite3?vfs=opfs'
): Promise<Client> {
const db = await createSqliteDbAdapter(dbUrl)
return new DbClient(db, workspace, personalWorkspace)
return new DbClient(db, workspace, personalWorkspace) as unknown as Client
}
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-client-ws",
"version": "0.1.9",
"version": "0.1.47",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
@@ -14,6 +14,7 @@
},
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/communication-client-query": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
"@msgpack/msgpack": "^3.0.0-beta2"
},
+22 -37
View File
@@ -1,18 +1,19 @@
import {
type Attachment,
type CardID,
type ContextID,
type FindMessagesGroupsParams,
type FindMessagesParams,
type FindNotificationContextParams,
type FindNotificationsParams,
type Message,
type MessageID,
type MessagesGroup,
type Notification,
type NotificationContext,
type NotificationContextUpdate,
type Reaction,
type RichText,
type SocialID
type SocialID,
type WorkspaceID
} from '@hcengineering/communication-types'
import {
RequestEventType,
@@ -35,6 +36,7 @@ import {
type ResponseEvent,
type UpdateNotificationContextEvent
} from '@hcengineering/communication-sdk-types'
import { initLiveQueries } from '@hcengineering/communication-client-query'
import { WebSocketConnection } from './connection'
@@ -75,7 +77,7 @@ class WsClient implements Client {
await this.sendEvent(event)
}
async createPatch(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise<void> {
async updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise<void> {
const event: CreatePatchEvent = {
type: RequestEventType.CreatePatch,
card,
@@ -130,39 +132,11 @@ class WsClient implements Client {
}
async findMessages(params: FindMessagesParams, queryId?: number): Promise<Message[]> {
const rawMessages = await this.ws.send('findMessages', [params, queryId])
return rawMessages.map((it: any) => this.toMessage(it))
return await this.ws.send('findMessages', [params, queryId])
}
toMessage(raw: any): Message {
return {
id: raw.id,
card: raw.card,
content: raw.content,
creator: raw.creator,
created: new Date(raw.created),
edited: new Date(raw.edited),
reactions: raw.reactions.map((it: any) => this.toReaction(it)),
attachments: raw.attachments.map((it: any) => this.toAttachment(it))
}
}
toAttachment(raw: any): Attachment {
return {
message: raw.message,
card: raw.card,
creator: raw.creator,
created: new Date(raw.created)
}
}
toReaction(raw: any): Reaction {
return {
message: raw.message,
reaction: raw.reaction,
creator: raw.creator,
created: new Date(raw.created)
}
async findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
return await this.ws.send('findMessagesGroups', [params])
}
async createNotification(message: MessageID, context: ContextID): Promise<void> {
@@ -229,12 +203,23 @@ class WsClient implements Client {
private async sendEvent(event: RequestEvent): Promise<EventResult> {
return await this.ws.send('event', [event])
}
//eslint-disable-next-line @typescript-eslint/no-unused-vars
async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void> {
//TODO: implement
}
close() {
void this.ws.close()
}
}
export async function getWebsocketClient(url: string, token: string): Promise<Client> {
return new WsClient(url, token)
export async function getWebsocketClient(
url: string,
token: string,
workspace: WorkspaceID,
filesUrl: string
): Promise<Client> {
const client = new WsClient(url, token)
initLiveQueries(client, workspace, filesUrl)
return client
}
+3
View File
@@ -1 +1,4 @@
import { createMessagesQuery, createNotificationsQuery } from '@hcengineering/communication-client-query'
export * from './client'
export { createMessagesQuery, createNotificationsQuery }
@@ -1,2 +1 @@
DROP SCHEMA IF EXISTS communication CASCADE;
CREATE SCHEMA IF NOT EXISTS communication;
+8 -3
View File
@@ -8,9 +8,11 @@ CREATE TABLE IF NOT EXISTS communication.messages
creator VARCHAR(255) NOT NULL,
created TIMESTAMPTZ NOT NULL,
PRIMARY KEY (id, card_id, workspace_id)
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 TABLE IF NOT EXISTS communication.messages_groups
(
@@ -18,11 +20,14 @@ CREATE TABLE IF NOT EXISTS communication.messages_groups
card_id VARCHAR(255) NOT NULL,
blob_id UUID NOT NULL,
from_id INT8 NOT NULL,
to_id INT8 NOT NULL,
from_date TIMESTAMPTZ NOT NULL,
to_date TIMESTAMPTZ NOT NULL,
from_id INT8 NOT NULL,
to_id INT8 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);
+2 -1
View File
@@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS communication.patch
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,
@@ -11,4 +12,4 @@ CREATE TABLE IF NOT EXISTS communication.patch
PRIMARY KEY (id)
);
CREATE INDEX idx_patch_message_id ON communication.patch (message_id);
CREATE INDEX IF NOT EXISTS idx_patch_workspace_card_message ON communication.patch (workspace_id, card_id, message_id);
@@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS communication.attachments
creator VARCHAR(255) NOT NULL,
created TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (message_id, card_id)
PRIMARY KEY (card_id, message_id)
);
CREATE INDEX IF NOT EXISTS attachment_message_idx ON communication.attachments (message_id);
@@ -7,7 +7,8 @@ CREATE TABLE IF NOT EXISTS communication.reactions
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)
);
CREATE INDEX IF NOT EXISTS reaction_message_idx ON communication.reactions (message_id);
CREATE INDEX IF NOT EXISTS idx_reactions_workspace_card_message ON communication.reactions (workspace_id, card_id, message_id);
@@ -1,15 +0,0 @@
CREATE TABLE IF NOT EXISTS communication.notification_context
(
id UUID NOT NULL DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
personal_workspace UUID NOT NULL,
archived_from TIMESTAMPTZ,
last_view TIMESTAMPTZ,
last_update TIMESTAMPTZ,
PRIMARY KEY (id),
UNIQUE (workspace_id, card_id, personal_workspace)
);
@@ -0,0 +1,15 @@
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,
PRIMARY KEY (workspace_id, thread_id),
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_card_message ON communication.thread (workspace_id, card_id, message_id);
@@ -1,3 +1,20 @@
CREATE TABLE IF NOT EXISTS communication.notification_context
(
id UUID NOT NULL DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
personal_workspace UUID NOT NULL,
archived_from TIMESTAMPTZ,
last_view TIMESTAMPTZ,
last_update TIMESTAMPTZ,
PRIMARY KEY (id),
UNIQUE (workspace_id, card_id, personal_workspace)
);
CREATE TABLE IF NOT EXISTS communication.notifications
(
message_id UUID NOT NULL,
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-cockroach",
"version": "0.1.9",
"version": "0.1.47",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
@@ -13,10 +13,9 @@
"@types/bun": "^1.1.14"
},
"dependencies": {
"@hcengineering/communication-core": "workspace:*",
"@hcengineering/communication-shared": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
"pg": "8.12.0",
"postgres": "^3.4.4",
"uuid": "^11.0.3"
},
+154 -47
View File
@@ -1,73 +1,88 @@
import type { ParameterOrJSON, Row } from 'postgres'
import type postgres from 'postgres'
import {
type Message,
type FindMessagesParams,
type BlobID,
type CardID,
type ContextID,
type FindMessagesGroupsParams,
type FindMessagesParams,
type FindNotificationContextParams,
type FindNotificationsParams,
type Message,
type MessageID,
type MessagesGroup,
type Notification,
type NotificationContext,
type NotificationContextUpdate,
type PatchType,
type RichText,
type SocialID,
type MessageID,
type ContextID,
type NotificationContextUpdate,
type FindNotificationContextParams,
type NotificationContext,
type FindNotificationsParams,
type Notification,
type BlobID,
type MessagesGroup,
type FindMessagesGroupsParams,
type WorkspaceID
type WorkspaceID,
type Thread
} 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'
import { connect, type PostgresClientReference } from './connection'
import { type Options, type Logger, type SqlClient } from './types'
export class CockroachAdapter implements DbAdapter {
private readonly message: MessagesDb
private readonly notification: NotificationsDb
constructor(
private readonly db: PostgresClientReference,
private readonly sqlClient: postgres.Sql,
private readonly workspace: WorkspaceID
private readonly sql: SqlClient,
private readonly workspace: WorkspaceID,
private readonly logger?: Logger,
private readonly options?: Options
) {
this.message = new MessagesDb(this.sqlClient, this.workspace)
this.notification = new NotificationsDb(this.sqlClient, this.workspace)
this.message = new MessagesDb(this.sql, this.workspace, logger, options)
this.notification = new NotificationsDb(this.sql, this.workspace, logger, options)
}
async createMessage(card: CardID, content: RichText, creator: SocialID, created: Date): Promise<MessageID> {
return await this.message.createMessage(card, content, creator, created)
}
async removeMessage(card: CardID, message: MessageID, socialIds?: SocialID[]): Promise<void> {
await this.message.removeMessage(card, message, socialIds)
}
async removeMessages(card: CardID, fromId: MessageID, toId: MessageID): Promise<void> {
await this.message.removeMessages(card, fromId, toId)
}
async createPatch(
card: CardID,
message: MessageID,
type: PatchType,
content: RichText,
creator: SocialID,
created: Date
): Promise<void> {
return await this.message.createPatch(card, message, content, creator, created)
await this.message.createPatch(card, message, type, content, creator, created)
}
async removeMessage(card: CardID, message: MessageID): Promise<MessageID | undefined> {
return await this.message.removeMessage(card, message)
}
async removeMessages(card: CardID, ids: MessageID[]): Promise<MessageID[]> {
return await this.message.removeMessages(card, ids)
async removePatches(card: CardID, fromId: MessageID, toId: MessageID): Promise<void> {
await this.message.removePatches(card, fromId, toId)
}
async createMessagesGroup(
card: CardID,
blobId: BlobID,
from_id: MessageID,
to_id: MessageID,
from_date: Date,
to_date: Date,
fromDate: Date,
toDate: Date,
fromId: MessageID,
toId: MessageID,
count: number
): Promise<void> {
return await this.message.createMessagesGroup(card, blobId, from_id, to_id, from_date, to_date, count)
await this.message.createMessagesGroup(card, blobId, fromDate, toDate, fromId, toId, count)
}
async removeMessagesGroup(card: CardID, blobId: BlobID): Promise<void> {
await this.message.removeMessagesGroup(card, blobId)
}
async createReaction(
@@ -77,35 +92,35 @@ export class CockroachAdapter implements DbAdapter {
creator: SocialID,
created: Date
): Promise<void> {
return await this.message.createReaction(card, message, reaction, creator, created)
await this.message.createReaction(card, message, reaction, creator, created)
}
async removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void> {
return await this.message.removeReaction(card, message, reaction, creator)
await this.message.removeReaction(card, message, reaction, creator, new Date())
}
async createAttachment(message: MessageID, attachment: CardID, creator: SocialID, created: Date): Promise<void> {
return await this.message.createAttachment(message, attachment, creator, created)
await this.message.createAttachment(message, attachment, creator, created)
}
async removeAttachment(message: MessageID, attachment: CardID): Promise<void> {
return await this.message.removeAttachment(message, attachment)
await this.message.removeAttachment(message, attachment)
}
async findMessages(params: FindMessagesParams): Promise<Message[]> {
return await this.message.find(params)
async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void> {
await this.message.createThread(card, message, thread, created)
}
async findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
return await this.message.findGroups(params)
async updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise<void> {
await this.message.updateThread(thread, lastReply, op)
}
async createNotification(message: MessageID, context: ContextID): Promise<void> {
return await this.notification.createNotification(message, context)
await this.notification.createNotification(message, context)
}
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
return await this.notification.removeNotification(message, context)
await this.notification.removeNotification(message, context)
}
async createContext(
@@ -118,11 +133,24 @@ export class CockroachAdapter implements DbAdapter {
}
async updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
return await this.notification.updateContext(context, update)
await this.notification.updateContext(context, update)
}
async removeContext(context: ContextID): Promise<void> {
return await this.notification.removeContext(context)
await this.notification.removeContext(context)
}
// Finds
async findMessages(params: FindMessagesParams): Promise<Message[]> {
return await this.message.find(params)
}
async findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
return await this.message.findMessagesGroups(params)
}
async findThread(thread: CardID): Promise<Thread | undefined> {
return await this.message.findThread(thread)
}
async findContexts(
@@ -142,13 +170,92 @@ export class CockroachAdapter implements DbAdapter {
}
close(): void {
this.db.close()
this.sql.close()
}
}
export async function createDbAdapter(connectionString: string, workspace: WorkspaceID): Promise<DbAdapter> {
const db = connect(connectionString)
const sqlClient = await db.getClient()
export async function createDbAdapter(
connectionString: string,
workspace: WorkspaceID,
logger?: Logger,
options?: Options
): Promise<DbAdapter> {
const greenUrl = process.env.GREEN_URL ?? ''
if (greenUrl !== '') {
const client = new GreenClient(greenUrl)
return new CockroachAdapter(client, workspace, logger, options)
} else {
const connection = connect(connectionString)
const sql = await connection.getClient()
const client = new CockroachClient(connection, sql)
return new CockroachAdapter(db, sqlClient, workspace)
return new CockroachAdapter(client, workspace, logger, options)
}
}
class GreenClient implements SqlClient {
private readonly url: string
private readonly token: string
constructor(endpoint: string) {
const url = new URL(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<T extends any[] = (Row & Iterable<Row>)[]>(query: string, params?: ParameterOrJSON<any>[]): Promise<T> {
return await retry(() => this.fetch<T>(query, params), { retries: 5 })
}
private async fetch<T extends any[] = (Row & Iterable<Row>)[]>(
query: string,
params?: ParameterOrJSON<any>[]
): Promise<T> {
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()
}
close(): void {
// do nothing
}
}
class CockroachClient implements SqlClient {
constructor(
private readonly db: PostgresClientReference,
private readonly sql: postgres.Sql
) {}
async execute<T extends any[] = (Row & Iterable<Row>)[]>(query: string, params?: ParameterOrJSON<any>[]): Promise<T> {
return await this.sql.unsafe<T>(query, params)
}
close(): void {
this.db.close()
}
}
+4 -5
View File
@@ -1,4 +1,4 @@
//Full copy from @hcengineering/postgres
// Full copy from @hcengineering/postgres
import postgres from 'postgres'
import { v4 as uuid } from 'uuid'
@@ -86,10 +86,9 @@ export function connect(connectionString: string, database?: string): PostgresCl
application_name: 'communication'
},
database,
max: 10,
transform: {
undefined: null
},
max: 5,
fetch_types: false,
prepare: true,
...extraOptions
})
+35 -73
View File
@@ -1,79 +1,41 @@
import type postgres from 'postgres'
import type {WorkspaceID} from "@hcengineering/communication-types";
import { type ParameterOrJSON, type Row } from 'postgres'
import type { WorkspaceID } from '@hcengineering/communication-types'
import { type SqlClient, type Logger, type Options } from '../types'
export class BaseDb {
constructor(
readonly client: postgres.Sql,
readonly workspace: WorkspaceID
) {
constructor (
readonly client: SqlClient,
readonly workspace: WorkspaceID,
readonly logger?: Logger,
readonly options?: Options
) {}
async execute<T extends any[] = (Row & Iterable<Row>)[]>(
sql: string,
params?: ParameterOrJSON<any>[],
name?: string
): Promise<T> {
if (this.options?.withLogs === true && this.logger !== undefined) {
return await this.executeWithLogs(name ?? 'execute sql', this.logger, sql, params)
}
async insert(table: string, data: Record<string, any>): Promise<void> {
const keys = Object.keys(data)
const values = Object.values(data)
const sql = `
INSERT INTO ${table} (${keys.map((k) => `"${k}"`).join(', ')})
VALUES (${keys.map((_, idx) => `$${idx + 1}`).join(', ')});
`
await this.client.unsafe(sql, values)
}
async insertWithReturn(table: string, data: Record<string, any>, returnField: string): Promise<any> {
const keys = Object.keys(data)
const values = Object.values(data)
const sql = `
INSERT INTO ${table} (${keys.map((k) => `"${k}"`).join(', ')})
VALUES (${keys.map((_, idx) => `$${idx + 1}`).join(', ')})
RETURNING ${returnField};`
const result = await this.client.unsafe(sql, values)
return result[0][returnField]
}
async remove(table: string, where: Record<string, any>): Promise<void> {
const keys = Object.keys(where)
const values = Object.values(where)
if (keys.length === 0) {
throw new Error("WHERE condition cannot be empty");
}
const sql = `
DELETE
FROM ${table}
WHERE ${keys.map((k, idx) => `"${k}" = $${idx + 1}`).join(' AND ')};`
await this.client.unsafe(sql, values)
}
async removeWithReturn(table: string, where: Record<string, any>, returnField: string): Promise<any[]> {
const keys = Object.keys(where);
const values: any[] = [];
if (keys.length === 0) {
throw new Error("WHERE condition cannot be empty");
}
const whereClause = keys.map((key) => {
const value = where[key];
if (Array.isArray(value)) {
const placeholders = value.map((_, i) => `$${values.length + i + 1}`).join(", ");
values.push(...value);
return `"${key}" IN (${placeholders})`;
} else {
values.push(value);
return `"${key}" = $${values.length}`;
}
}).join(" AND ");
const sql = `
DELETE FROM ${table}
WHERE ${whereClause}
RETURNING ${returnField};
`;
const result = await this.client.unsafe(sql, values);
return result.map((it: any) => it[returnField]);
return await this.client.execute(sql, params)
}
private async executeWithLogs<T extends any[] = (Row & Iterable<Row>)[]>(
name: string,
logger: Logger,
sql: string,
params?: ParameterOrJSON<any>[]
): Promise<T> {
const start = performance.now()
try {
return await this.client.execute(sql, params)
} finally {
const time = performance.now() - start
logger.info(name, { time })
}
}
}
+453 -274
View File
@@ -1,302 +1,481 @@
import {
type Message,
type MessageID,
type CardID,
type FindMessagesParams,
SortOrder,
type SocialID,
type RichText,
Direction,
type BlobID,
type MessagesGroup,
type FindMessagesGroupsParams
type Message,
type MessageID,
type CardID,
type FindMessagesParams,
type SocialID,
type RichText,
SortingOrder,
PatchType,
type Thread,
type BlobID,
type FindMessagesGroupsParams,
type MessagesGroup
} from '@hcengineering/communication-types'
import {generateMessageId} from "@hcengineering/communication-core";
import { generateMessageId } from '@hcengineering/communication-shared'
import {BaseDb} from './base.ts'
import { BaseDb } from './base'
import {
TableName,
type MessageDb,
type AttachmentDb,
type ReactionDb,
type PatchDb,
type MessagesGroupDb,
toMessage,
toMessagesGroup
} from './schema.ts'
import {getCondition} from './utils.ts';
TableName,
type MessageDb,
type AttachmentDb,
type ReactionDb,
type PatchDb,
toMessage,
type ThreadDb,
toThread,
type MessagesGroupDb,
toMessagesGroup
} from './schema'
import { getCondition } from './utils'
export class MessagesDb extends BaseDb {
//Message
async createMessage(card: CardID, content: RichText, creator: SocialID, created: Date): Promise<MessageID> {
const dbData: MessageDb = {
id: generateMessageId(),
workspace_id: this.workspace,
card_id: card,
content: content,
creator: creator,
created: created,
}
await this.insert(TableName.Message, dbData)
return dbData.id as MessageID
// Message
async createMessage (card: CardID, content: RichText, creator: SocialID, created: Date): Promise<MessageID> {
const id = generateMessageId()
const db: MessageDb = {
id,
workspace_id: this.workspace,
card_id: card,
content,
creator,
created
}
async removeMessage(card: CardID, message: MessageID): Promise<MessageID | undefined> {
const result = await this.removeWithReturn(TableName.Message, {id: message, workspace_id: this.workspace, card_id: card}, "id")
return result[0] as MessageID | undefined
const sql = `INSERT INTO ${TableName.Message} (workspace_id, card_id, id, content, creator, created)
VALUES ($1::uuid, $2::varchar, $3::bigint, $4::text, $5::varchar, $6::timestamptz)`
await this.execute(sql, [db.workspace_id, db.card_id, db.id, db.content, db.creator, db.created], 'insert message')
return id
}
async removeMessage (card: CardID, message: MessageID, socialIds?: SocialID[]): Promise<void> {
if (socialIds === undefined || socialIds.length === 0) {
const sql = `DELETE
FROM ${TableName.Message}
WHERE workspace_id = $1::uuid
AND card_id = $2::varchar
AND id = $2::bigint;`
await this.execute(sql, [this.workspace, card, message], 'remove message')
} else if (socialIds.length === 1) {
const sql = `DELETE
FROM ${TableName.Message}
WHERE workspace_id = $1::uuid
AND card_id = $2::varchar
AND id = $2::bigint
AND creator = $3::varchar;`
await this.execute(sql, [this.workspace, card, message, socialIds[0]], 'remove message')
} else {
const sql = `DELETE
FROM ${TableName.Message}
WHERE workspace_id = $1::uuid
AND card_id = $2::varchar
AND id = $2::bigint
AND creator = ANY ($3::varchar[]);`
await this.execute(sql, [this.workspace, card, message, socialIds], 'remove message')
}
}
async removeMessages (card: CardID, fromId: MessageID, toId: MessageID): Promise<void> {
const sql = `DELETE
FROM ${TableName.Message}
WHERE workspace_id = $1::uuid
AND card_id = $2::varchar
AND id >= $3::bigint
AND id <= $4::bigint;`
await this.execute(sql, [this.workspace, card, BigInt(fromId), BigInt(toId)], 'remove messages')
}
async createPatch (
card: CardID,
message: MessageID,
type: PatchType,
content: string,
creator: SocialID,
created: Date
): Promise<void> {
const db: PatchDb = {
workspace_id: this.workspace,
card_id: card,
message_id: message,
type,
content,
creator,
created
}
async removeMessages(card: CardID, ids: MessageID[]): Promise<MessageID[]> {
const result = await this.removeWithReturn(TableName.Message, {
workspace_id: this.workspace,
card_id: card,
id: ids
}, "id")
return result.map((it: any) => it.id)
const sql = `INSERT INTO ${TableName.Patch} (workspace_id, card_id, message_id, type, content, creator, created)
VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::text, $6::varchar, $7::timestamptz)`
await this.execute(
sql,
[db.workspace_id, db.card_id, db.message_id, db.type, db.content, db.creator, db.created],
'insert patch'
)
}
async removePatches (card: CardID, fromId: MessageID, toId: MessageID): Promise<void> {
const sql = `DELETE
FROM ${TableName.Patch}
WHERE workspace_id = $1::uuid
AND card_id = $2::varchar
AND message_id >= $3::bigint
AND message_id <= $4::bigint;`
await this.execute(sql, [this.workspace, card, BigInt(fromId), BigInt(toId)], 'remove patches')
}
// Attachment
async createAttachment (message: MessageID, card: CardID, creator: SocialID, created: Date): Promise<void> {
const db: AttachmentDb = {
message_id: message,
card_id: card,
creator,
created
}
const sql = `INSERT INTO ${TableName.Attachment} (message_id, card_id, creator, created)
VALUES ($1::bigint, $2::varchar, $3::varchar, $4::timestamptz)`
await this.execute(sql, [db.message_id, db.card_id, db.creator, db.created], 'insert attachment')
}
async removeAttachment (message: MessageID, card: CardID): Promise<void> {
const sql = `DELETE
FROM ${TableName.Attachment}
WHERE message_id = $1::bigint
AND card_id = $2::varchar`
await this.execute(sql, [message, card], 'remove attachment')
}
// Reaction
async createReaction (
card: CardID,
message: MessageID,
reaction: string,
creator: SocialID,
created: Date
): Promise<void> {
const select = `SELECT m.id
FROM ${TableName.Message} m
WHERE m.id = $1::bigint`
const messageDb = await this.execute(select, [message], 'select message')
if (messageDb.length > 0) {
const db: ReactionDb = {
workspace_id: this.workspace,
card_id: card,
message_id: message,
reaction,
creator,
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)`
await this.execute(
sql,
[db.workspace_id, db.card_id, db.message_id, db.reaction, db.creator, db.created],
'insert reaction'
)
} else {
await this.createPatch(card, message, PatchType.addReaction, reaction, creator, created)
}
}
async removeReaction (
card: CardID,
message: MessageID,
reaction: string,
creator: SocialID,
created: Date
): Promise<void> {
const select = `SELECT m.id
FROM ${TableName.Message} m
WHERE m.id = $1::bigint`
const messageDb = await this.execute(select, [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`
await this.execute(sql, [this.workspace, card, message, reaction, creator], 'remove reaction')
} else {
await this.createPatch(card, message, PatchType.removeReaction, reaction, creator, created)
}
}
// Thread
async createThread (card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void> {
const db: ThreadDb = {
workspace_id: this.workspace,
card_id: card,
message_id: message,
thread_id: thread,
replies_count: 0,
last_reply: created
}
const sql = `INSERT INTO ${TableName.Thread} (workspace_id, card_id, message_id, thread_id, replies_count,
last_reply)
VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::int, $6::timestamptz)`
await this.execute(
sql,
[db.workspace_id, db.card_id, db.message_id, db.thread_id, db.replies_count, db.last_reply],
'insert thread'
)
}
async updateThread (thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise<void> {
if (op === 'increment') {
const sql = `UPDATE ${TableName.Thread}
SET replies_count = replies_count + 1,
last_reply = $3::timestamptz
WHERE workspace_id = $1::uuid
AND thread_id = $2::varchar`
await this.execute(sql, [this.workspace, thread, lastReply], 'update thread')
} else if (op === 'decrement') {
const sql = `UPDATE ${TableName.Thread}
SET replies_count = GREATEST(replies_count - 1, 0)
WHERE workspace_id = $1::uuid
AND thread_id = $2::varchar`
await this.execute(sql, [this.workspace, thread], 'update thread')
}
}
// MessagesGroup
async createMessagesGroup (
card: CardID,
blobId: BlobID,
fromDate: Date,
toDate: Date,
fromId: MessageID,
toId: MessageID,
count: number
): Promise<void> {
const db: MessagesGroupDb = {
workspace_id: this.workspace,
card_id: card,
blob_id: blobId,
from_date: fromDate,
to_date: toDate,
from_id: fromId,
to_id: toId,
count
}
async createPatch(card: CardID, message: MessageID, content: RichText, creator: SocialID, created: Date): Promise<void> {
const dbData: PatchDb = {
workspace_id: this.workspace,
card_id: card,
message_id: message,
content: content,
creator: creator,
created: created
}
const sql = `INSERT INTO ${TableName.MessagesGroup} (workspace_id, card_id, blob_id, from_date, to_date, from_id,
to_id, count)
VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz, $6::bigint, $7::bigint,
$8::int)`
await this.execute(
sql,
[db.workspace_id, db.card_id, db.blob_id, db.from_date, db.to_date, db.from_id, db.to_id, db.count],
'insert messages group'
)
}
await this.insert(TableName.Patch, dbData)
async removeMessagesGroup (card: CardID, blobId: BlobID): Promise<void> {
const sql = `DELETE
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')
}
// Find messages
async find (params: FindMessagesParams): Promise<Message[]> {
// TODO: experiment with select to improve performance
const select = `SELECT m.id,
m.card_id,
m.content,
m.creator,
m.created,
t.thread_id as thread_id,
t.replies_count as replies_count,
t.last_reply as last_reply,
${this.subSelectPatches()},
${this.subSelectReactions()}
FROM ${TableName.Message} 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`
const { where, values } = this.buildMessageWhere(params)
const orderBy =
params.order != null ? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''
const sql = [select, where, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find messages')
return result.map((it: any) => toMessage(it))
}
buildMessageWhere (params: FindMessagesParams): { where: string, values: any[] } {
const where: string[] = ['m.workspace_id = $1::uuid']
const values: any[] = [this.workspace]
let index = 2
if (params.id != null) {
where.push(`m.id = $${index++}::bigint`)
values.push(params.id)
}
//MessagesGroup
async createMessagesGroup(card: CardID, blobId: BlobID, from_id: MessageID, to_id: MessageID, from_date: Date, to_date: Date, count: number): Promise<void> {
const dbData: MessagesGroupDb = {
workspace_id: this.workspace,
card_id: card,
blob_id: blobId,
from_id,
to_id,
from_date,
to_date,
count
}
await this.insert(TableName.MessagesGroup, dbData)
if (params.card != null) {
where.push(`m.card_id = $${index++}::varchar`)
values.push(params.card)
}
//Attachment
async createAttachment(message: MessageID, card: CardID, creator: SocialID, created: Date): Promise<void> {
const dbData: AttachmentDb = {
message_id: message,
card_id: card,
creator: creator,
created: created
}
await this.insert(TableName.Attachment, dbData)
const createdCondition = getCondition('m', 'created', index, params.created, 'timestamptz')
if (createdCondition != null) {
where.push(createdCondition.where)
values.push(createdCondition.value)
index++
}
async removeAttachment(message: MessageID, card: CardID): Promise<void> {
await this.remove(TableName.Attachment, {
message_id: message,
card_id: card
})
return { where: `WHERE ${where.join(' AND ')}`, values }
}
subSelectPatches (): string {
return `COALESCE(
(SELECT jsonb_agg(jsonb_build_object(
'content', p.content,
'creator', p.creator,
'created', p.created
) ORDER BY p.created DESC)
FROM ${TableName.Patch} p
WHERE p.message_id = m.id
AND p.workspace_id = m.workspace_id
AND p.card_id = m.card_id
AND p.type = 'update'
), '[]'::jsonb) AS patches`
}
subSelectAttachments (): string {
return `COALESCE(
(SELECT jsonb_agg(jsonb_build_object(
'card_id', a.card_id,
'message_id', a.message_id,
'creator', a.creator,
'created', a.created
))
FROM ${TableName.Attachment} a
WHERE a.message_id = m.id
), '[]'::jsonb) AS attachments`
}
subSelectReactions (): string {
return `COALESCE(
(SELECT jsonb_agg(jsonb_build_object(
'message_id', r.message_id,
'reaction', r.reaction,
'creator', r.creator,
'created', r.created
))
FROM ${TableName.Reaction} r
WHERE r.workspace_id = m.workspace_id
AND r.card_id = m.card_id
AND r.message_id = m.id
), '[]'::jsonb) AS reactions`
}
// Find thread
async findThread (thread: CardID): Promise<Thread | undefined> {
const sql = `SELECT t.card_id,
t.message_id,
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;`
const result = await this.execute(sql, [this.workspace, thread], 'find thread')
return result.map((it: any) => toThread(it))[0]
}
// Find messages groups
async findMessagesGroups (params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
const select = `SELECT mg.card_id,
mg.blob_id,
mg.from_date,
mg.to_date,
mg.from_id,
mg.to_id,
mg.count,
jsonb_agg(jsonb_build_object(
'message_id', p.message_id,
'type', p.type,
'content', p.content,
'creator', p.creator,
'created', p.created
) ORDER BY p.created) AS patches
FROM ${TableName.MessagesGroup} mg
LEFT JOIN ${TableName.Patch} p
ON p.workspace_id = mg.workspace_id
AND p.card_id = mg.card_id
AND p.message_id BETWEEN mg.from_id AND mg.to_id`
const { where, values } = this.buildMessagesGroupWhere(params)
const orderBy =
params.orderBy === 'toDate'
? `ORDER BY mg.to_date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}`
: `ORDER BY mg.from_date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}`
const groupBy = 'GROUP BY mg.card_id, mg.blob_id, mg.from_date, mg.to_date, mg.from_id, mg.to_id, mg.count'
const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''
const sql = [select, where, groupBy, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find messages groups')
return result.map((it: any) => toMessagesGroup(it))
}
buildMessagesGroupWhere (params: FindMessagesGroupsParams): {
where: string
values: any[]
} {
const where: string[] = ['mg.workspace_id = $1::uuid']
const values: any[] = [this.workspace]
let index = 2
if (params.card != null) {
where.push(`mg.card_id = $${index++}::varchar`)
values.push(params.card)
}
//Reaction
async createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID, created: Date): Promise<void> {
const dbData: ReactionDb = {
workspace_id: this.workspace,
card_id: card,
message_id: message,
reaction: reaction,
creator: creator,
created: created
}
await this.insert(TableName.Reaction, dbData)
if (params.blobId != null) {
where.push(`mg.blob_id = $${index++}`)
values.push(params.blobId)
}
async removeReaction( card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void> {
await this.remove(TableName.Reaction, {
workspace_id: this.workspace,
card_id: card,
message_id: message,
reaction: reaction,
creator: creator
})
const fromDateCondition = getCondition('mg', 'from_date', index, params.fromDate, 'timestamptz')
if (fromDateCondition != null) {
where.push(fromDateCondition.where)
values.push(fromDateCondition.value)
index++
}
//Find messages
async find(params: FindMessagesParams): Promise<Message[]> {
//TODO: experiment with select to improve performance
const select = `SELECT m.id,
m.card_id,
m.content,
m.creator,
m.created,
${this.subSelectPatches()},
${this.subSelectAttachments()},
${this.subSelectReactions()}
FROM ${TableName.Message} m`
const {where, values} = this.buildMessageWhere( params)
const orderBy = params.sort ? `ORDER BY m.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` : ''
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
const sql = [select, where, orderBy, limit].join(' ')
const result = await this.client.unsafe(sql, values)
return result.map((it: any) => toMessage(it))
const toDateCondition = getCondition('mg', 'to_date', index, params.toDate, 'timestamptz')
if (toDateCondition != null) {
where.push(toDateCondition.where)
values.push(toDateCondition.value)
index++
}
buildMessageWhere(params: FindMessagesParams): { where: string, values: any[] } {
const where: string[] = ['m.workspace_id = $1']
const values: any[] = [this.workspace]
let index = 2
if (params.id != null) {
where.push(`m.id = $${index++}`)
values.push(params.id)
}
if (params.card != null) {
where.push(`m.card_id = $${index++}`)
values.push(params.card)
}
if (params.from != null) {
const exclude = params.excluded ?? false
const direction = params.direction ?? Direction.Forward
const getOperator = () => {
if (exclude) {
return direction === Direction.Forward ? '>' : '<'
} else {
return direction === Direction.Forward ? '>=' : '<='
}
}
where.push(`m.created ${getOperator()} $${index++}`)
values.push(params.from)
}
return {where: `WHERE ${where.join(' AND ')}`, values}
}
subSelectPatches(): string {
return `array(
SELECT jsonb_build_object(
'content', p.content,
'creator', p.creator,
'created', p.created
)
FROM ${TableName.Patch} p
WHERE p.message_id = m.id AND p.workspace_id = m.workspace_id AND p.card_id = m.card_id
) AS patches`
}
subSelectAttachments(): string {
return `array(
SELECT jsonb_build_object(
'card_id', a.card_id,
'message_id', a.message_id,
'creator', a.creator,
'created', a.created
)
FROM ${TableName.Attachment} a
WHERE a.message_id = m.id
) AS attachments`
}
subSelectReactions(): string {
return `array(
SELECT jsonb_build_object(
'message_id', r.message_id,
'reaction', r.reaction,
'creator', r.creator,
'created', r.created
)
FROM ${TableName.Reaction} r
WHERE r.message_id = m.id AND r.workspace_id = m.workspace_id AND r.card_id = m.card_id
) AS reactions`
}
//Find messages groups
async findGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
const select = `SELECT mg.card_id,
mg.blob_id,
mg.from_id,
mg.to_id,
mg.count
FROM ${TableName.MessagesGroup} mg`
const {where, values, index} = this.buildMessagesGroupWhere(this.workspace, params)
const orderBy = params.sortBy ? `ORDER BY ${index} ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` : ''
if(params.sortBy) {
values.push(params.sortBy)
}
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
const sql = [select, where, orderBy, limit].join(' ')
const result = await this.client.unsafe(sql, values)
return result.map((it: any) => toMessagesGroup(it))
}
buildMessagesGroupWhere(workspace: string, params: FindMessagesGroupsParams): {
where: string,
values: any[],
index: number
} {
const where: string[] = ['mg.workspace_id = $1']
const values: any[] = [workspace]
let index = 2
if (params.card != null) {
where.push(`mg.card_id = $${index++}`)
values.push(params.card)
}
if (params.blobId != null) {
where.push(`mg.blob_id = $${index++}`)
values.push(params.blobId)
}
const fromIdCondition = getCondition("mg", "from_id", index, params.fromId);
if (fromIdCondition != null) {
where.push(fromIdCondition.where);
values.push(fromIdCondition.value);
index++;
}
const toIdCondition = getCondition("mg", "to_id", index, params.toId);
if (toIdCondition != null) {
where.push(toIdCondition.where);
values.push(toIdCondition.value);
index++;
}
const fromDateCondition = getCondition("mg", "from_date", index, params.fromDate);
if (fromDateCondition != null) {
where.push(fromDateCondition.where);
values.push(fromDateCondition.value);
index++;
}
const toDateCondition = getCondition("mg", "to_date", index, params.toDate);
if (toDateCondition != null) {
where.push(toDateCondition.where);
values.push(toDateCondition.value);
index++;
}
return {where: `WHERE ${where.join(' AND ')}`, values, index}
}
return { where: `WHERE ${where.join(' AND ')}`, values }
}
}
+205 -174
View File
@@ -1,95 +1,120 @@
import {
type MessageID,
type ContextID,
type CardID,
type NotificationContext,
type FindNotificationContextParams, SortOrder,
type FindNotificationsParams, type Notification,
type NotificationContextUpdate,
type WorkspaceID
type MessageID,
type ContextID,
type CardID,
type NotificationContext,
type FindNotificationContextParams,
SortingOrder,
type FindNotificationsParams,
type Notification,
type NotificationContextUpdate,
type WorkspaceID
} from '@hcengineering/communication-types'
import {BaseDb} from './base.ts'
import {TableName, type ContextDb, type NotificationDb} from './schema.ts'
import { BaseDb } from './base'
import { TableName, type ContextDb, type NotificationDb } from './schema'
export class NotificationsDb extends BaseDb {
async createNotification(message: MessageID, context: ContextID): Promise<void> {
const dbData: NotificationDb = {
message_id: message,
context
}
await this.insert(TableName.Notification, dbData)
async createNotification (message: MessageID, context: ContextID): Promise<void> {
const db: NotificationDb = {
message_id: message,
context
}
const sql = `INSERT INTO ${TableName.Notification} (message_id, context_id)
VALUES ($1::bigint, $2::uuid)`
await this.execute(sql, [db.message_id, db.context], 'insert notification')
}
async removeNotification (message: MessageID, context: ContextID): Promise<void> {
// TODO: do we need to remove notifications?
const sql = `DELETE
FROM ${TableName.NotificationContext}
WHERE id = $1::uuid`
await this.execute(sql, [context], 'remove notification')
}
async createContext (
personalWorkspace: WorkspaceID,
card: CardID,
lastView?: Date,
lastUpdate?: Date
): Promise<ContextID> {
const db: ContextDb = {
workspace_id: this.workspace,
card_id: card,
personal_workspace: personalWorkspace,
last_view: lastView,
last_update: lastUpdate
}
const sql = `INSERT INTO ${TableName.NotificationContext} (workspace_id, card_id, personal_workspace, last_view, last_update)
VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz)
RETURNING id`
const result = await this.execute(
sql,
[db.workspace_id, db.card_id, db.personal_workspace, db.last_view, db.last_update],
'insert notification context'
)
return result[0].id as ContextID
}
async removeContext (context: ContextID): Promise<void> {
const sql = `DELETE
FROM ${TableName.Notification}
WHERE context = $1::uuid`
await this.execute(sql, [context], 'remove notification context')
}
async updateContext (context: ContextID, update: NotificationContextUpdate): Promise<void> {
const dbData: Partial<ContextDb> = {}
if (update.archivedFrom != null) {
dbData.archived_from = update.archivedFrom
}
if (update.lastView != null) {
dbData.last_view = update.lastView
}
if (update.lastUpdate != null) {
dbData.last_update = update.lastUpdate
}
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
await this.remove(TableName.Notification, {
message_id: message,
context
})
if (Object.keys(dbData).length === 0) {
return
}
async createContext(personalWorkspace: WorkspaceID,card: CardID, lastView?: Date, lastUpdate?: Date): Promise<ContextID> {
const dbData: ContextDb = {
workspace_id: this.workspace,
card_id: card,
personal_workspace: personalWorkspace,
last_view: lastView,
last_update: lastUpdate
}
return await this.insertWithReturn(TableName.NotificationContext, dbData, 'id') as ContextID
}
const keys = Object.keys(dbData)
const values = Object.values(dbData)
async removeContext(context: ContextID): Promise<void> {
await this.remove(TableName.NotificationContext, {
id: context
})
}
const sql = `UPDATE ${TableName.NotificationContext}
SET ${keys.map((k, idx) => `"${k}" = $${idx + 1}::timestamptz`).join(', ')}
WHERE id = $${keys.length + 1}::uuid;`
await this.execute(sql, [values, context], 'update notification context')
}
async updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
const dbData: Partial<ContextDb> = {}
if (update.archivedFrom != null) {
dbData.archived_from = update.archivedFrom
}
if (update.lastView != null) {
dbData.last_view = update.lastView
}
if (update.lastUpdate != null) {
dbData.last_update = update.lastUpdate
}
if (Object.keys(dbData).length === 0) {
return
}
const keys = Object.keys(dbData)
const values = Object.values(dbData)
const sql = `UPDATE ${TableName.NotificationContext}
SET ${keys.map((k, idx) => `"${k}" = $${idx + 1}`).join(', ')}
WHERE id = $${keys.length + 1}`
await this.client.unsafe(sql, [values, context])
}
async findContexts(params: FindNotificationContextParams, personalWorkspaces: WorkspaceID[], workspace?: WorkspaceID,): Promise<NotificationContext[]> {
const select = `
async findContexts (
params: FindNotificationContextParams,
personalWorkspaces: WorkspaceID[],
workspace?: WorkspaceID
): Promise<NotificationContext[]> {
const select = `
SELECT nc.id, nc.card_id, nc.archived_from, nc.last_view, nc.last_update
FROM ${TableName.NotificationContext} nc`;
const {where, values} = this.buildContextWhere(params, personalWorkspaces, workspace)
// const orderSql = `ORDER BY nc.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}`
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
const sql = [select, where, limit].join(' ')
FROM ${TableName.NotificationContext} nc`
const { where, values } = this.buildContextWhere(params, personalWorkspaces, workspace)
// const orderSql = `ORDER BY nc.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}`
const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''
const sql = [select, where, limit].join(' ')
const result = await this.client.unsafe(sql, values);
const result = await this.execute(sql, values, 'find notification contexts')
return result.map(this.toNotificationContext);
}
return result.map((it) => this.toNotificationContext(it))
}
async findNotifications(params: FindNotificationsParams, personalWorkspace: WorkspaceID, workspace?: WorkspaceID): Promise<Notification[]> {
//TODO: experiment with select to improve performance, should join with attachments and reactions?
const select = `
async findNotifications (
params: FindNotificationsParams,
personalWorkspace: WorkspaceID,
workspace?: WorkspaceID
): Promise<Notification[]> {
// TODO: experiment with select to improve performance, should join with attachments and reactions?
const select = `
SELECT n.message_id,
n.context,
m.card_id AS message_card,
@@ -113,115 +138,121 @@ export class NotificationsDb extends BaseDb {
FROM ${TableName.Notification} n
JOIN ${TableName.NotificationContext} nc ON n.context = nc.id
JOIN ${TableName.Message} m ON n.message_id = m.id
`;
const {where, values} = this.buildNotificationWhere(params, personalWorkspace, workspace)
const orderBy = params.sort ? `ORDER BY m.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` : ''
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
const sql = [select, where, orderBy, limit].join(' ')
`
const { where, values } = this.buildNotificationWhere(params, personalWorkspace, workspace)
const orderBy =
params.order != null ? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''
const sql = [select, where, orderBy, limit].join(' ')
const result = await this.client.unsafe(sql, values);
const result = await this.execute(sql, values, 'find notifications')
return result.map(this.toNotification);
}
return result.map((it) => this.toNotification(it))
}
buildContextWhere(params: FindNotificationContextParams, personalWorkspaces: WorkspaceID[], workspace?: WorkspaceID): {
where: string,
values: any[]
buildContextWhere (
params: FindNotificationContextParams,
personalWorkspaces: WorkspaceID[],
workspace?: WorkspaceID
): {
where: string
values: any[]
} {
const where: string[] = []
const values: any[] = []
let index = 1
const where: string[] = []
const values: any[] = []
let index = 1
if (workspace != null) {
where.push(`nc.workspace_id = $${index++}`)
values.push(workspace)
}
if (personalWorkspaces.length > 0) {
where.push(`nc.personal_workspace IN (${personalWorkspaces.map((it) => `$${index++}`).join(', ')})`)
values.push(...personalWorkspaces)
}
if (params.card != null) {
where.push(`nc.card_id = $${index++}`)
values.push(params.card)
}
return {where: `WHERE ${where.join(' AND ')}`, values}
if (workspace != null) {
where.push(`nc.workspace_id = $${index++}::uuid`)
values.push(workspace)
}
buildNotificationWhere(params: FindNotificationsParams, personalWorkspace: WorkspaceID, workspace?: WorkspaceID): {
where: string,
values: any[]
if (personalWorkspaces.length > 0) {
where.push(`nc.personal_workspace IN (${personalWorkspaces.map((it) => `$${index++}::uuid`).join(', ')})`)
values.push(...personalWorkspaces)
}
if (params.card != null) {
where.push(`nc.card_id = $${index++}::varchar`)
values.push(params.card)
}
return { where: `WHERE ${where.join(' AND ')}`, values }
}
buildNotificationWhere (
params: FindNotificationsParams,
personalWorkspace: WorkspaceID,
workspace?: WorkspaceID
): {
where: string
values: any[]
} {
const where: string[] = ['nc.personal_workspace = $1']
const values: any[] = [personalWorkspace]
let index = 2
const where: string[] = ['nc.personal_workspace = $1::uuid']
const values: any[] = [personalWorkspace]
let index = 2
if (workspace != null) {
where.push(`nc.workspace_id = $${index++}`)
values.push(workspace)
}
if (params.context != null) {
where.push(`n.context = $${index++}`)
values.push(params.context)
}
if (params.read === true) {
where.push(`nc.last_view IS NOT NULL AND nc.last_view >= m.created`)
}
if (params.read === false) {
where.push(`(nc.last_view IS NULL OR nc.last_view > m.created)`)
}
if (params.archived === true) {
where.push(`nc.archived_from IS NOT NULL AND nc.archived_from >= m.created`)
}
if (params.archived === false) {
where.push(`(nc.archived_from IS NULL OR nc.archived_from > m.created)`)
}
return {where: `WHERE ${where.join(' AND ')}`, values}
if (workspace != null) {
where.push(`nc.workspace_id = $${index++}::uuid`)
values.push(workspace)
}
toNotificationContext(row: any): NotificationContext {
return {
id: row.id,
card: row.card_id,
workspace: row.workspace_id,
personalWorkspace: row.personal_workspace,
archivedFrom: row.archived_from ? new Date(row.archived_from) : undefined,
lastView: row.last_view ? new Date(row.last_view) : undefined,
lastUpdate: row.last_update ? new Date(row.last_update) : undefined
}
if (params.context != null) {
where.push(`n.context = $${index++}::uuid`)
values.push(params.context)
}
toNotification(row: any): Notification {
const lastPatch = row.patches?.[0]
const lastView = row.last_view ? new Date(row.last_view) : undefined
const archivedFrom = row.archived_from ? new Date(row.archived_from) : undefined
const created = new Date(row.message_created)
return {
message: {
id: row.id,
card: row.message_card,
content: lastPatch?.content ?? row.message_content,
creator: row.message_creator,
created,
edited: new Date(lastPatch?.created ?? row.message_created),
reactions: row.reactions ?? [],
attachments: row.attachments ?? []
},
context: row.context,
read: lastView != null && lastView >= created,
archived: archivedFrom != null && archivedFrom >= created
}
if (params.read === true) {
where.push('nc.last_view IS NOT NULL AND nc.last_view >= m.created')
}
if (params.read === false) {
where.push('(nc.last_view IS NULL OR nc.last_view > m.created)')
}
if (params.archived === true) {
where.push('nc.archived_from IS NOT NULL AND nc.archived_from >= m.created')
}
if (params.archived === false) {
where.push('(nc.archived_from IS NULL OR nc.archived_from > m.created)')
}
return { where: `WHERE ${where.join(' AND ')}`, values }
}
toNotificationContext (row: any): NotificationContext {
return {
id: row.id,
card: row.card_id,
workspace: row.workspace_id,
personalWorkspace: row.personal_workspace,
archivedFrom: row.archived_from != null ? new Date(row.archived_from) : undefined,
lastView: row.last_view != null ? new Date(row.last_view) : undefined,
lastUpdate: row.last_update != null ? new Date(row.last_update) : undefined
}
}
toNotification (row: any): Notification {
const lastPatch = row.patches?.[0]
const lastView = row.last_view != null ? new Date(row.last_view) : undefined
const archivedFrom = row.archived_from != null ? new Date(row.archived_from) : undefined
const created = new Date(row.message_created)
return {
message: {
id: row.id,
card: row.message_card,
content: lastPatch?.content ?? row.message_content,
creator: row.message_creator,
created,
edited: new Date(lastPatch?.created ?? row.message_created),
reactions: row.reactions ?? [],
attachments: row.attachments ?? []
},
context: row.context,
read: lastView != null && lastView >= created,
archived: archivedFrom != null && archivedFrom >= created
}
}
}
+148 -87
View File
@@ -1,122 +1,183 @@
import type {ContextID, MessageID, RichText, SocialID, CardID, BlobID, Message, Reaction, Attachment, MessagesGroup, WorkspaceID } from "@hcengineering/communication-types"
import {
type ContextID,
type MessageID,
type RichText,
type SocialID,
type CardID,
type BlobID,
type Message,
type Reaction,
type Attachment,
type MessagesGroup,
type WorkspaceID,
type PatchType,
type Patch,
type Thread
} from '@hcengineering/communication-types'
export enum TableName {
Message = 'communication.messages',
MessagesGroup = 'communication.messages_groups',
Patch = 'communication.patch',
Attachment = 'communication.attachments',
Reaction = 'communication.reactions',
Notification = 'communication.notifications',
NotificationContext = 'communication.notification_context'
Attachment = 'communication.attachments',
Message = 'communication.messages',
MessagesGroup = 'communication.messages_groups',
Notification = 'communication.notifications',
NotificationContext = 'communication.notification_context',
Patch = 'communication.patch',
Reaction = 'communication.reactions',
Thread = 'communication.thread'
}
export interface MessageDb {
id: MessageID,
workspace_id: WorkspaceID,
card_id: CardID,
content: RichText,
creator: SocialID,
created: Date,
id: MessageID
workspace_id: WorkspaceID
card_id: CardID
content: RichText
creator: SocialID
created: Date
thread_id?: CardID
replies_count?: number
last_reply?: Date
}
export interface MessagesGroupDb {
workspace_id: WorkspaceID,
card_id: CardID,
blob_id: BlobID,
from_id: MessageID,
to_id: MessageID,
from_date: Date,
to_date: Date,
count: number
workspace_id: WorkspaceID
card_id: CardID
blob_id: BlobID
from_date: Date
to_date: Date
from_id: MessageID
to_id: MessageID
count: number
patches?: PatchDb[]
}
export interface PatchDb {
workspace_id: WorkspaceID,
card_id: CardID,
message_id: MessageID,
content: RichText,
creator: SocialID,
created: Date,
workspace_id: WorkspaceID
card_id: CardID
message_id: MessageID
type: PatchType
content: RichText
creator: SocialID
created: Date
}
export interface ReactionDb {
workspace_id: WorkspaceID,
card_id: CardID,
message_id: MessageID,
reaction: string,
creator: SocialID
created: Date
workspace_id: WorkspaceID
card_id: CardID
message_id: MessageID
reaction: string
creator: SocialID
created: Date
}
export interface AttachmentDb {
message_id: MessageID,
card_id: CardID,
creator: SocialID
created: Date
message_id: MessageID
card_id: CardID
creator: SocialID
created: Date
}
export interface ThreadDb {
workspace_id: WorkspaceID
card_id: CardID
message_id: MessageID
thread_id: CardID
replies_count: number
last_reply: Date
}
export interface NotificationDb {
message_id: MessageID,
context: ContextID
message_id: MessageID
context: ContextID
}
export interface ContextDb {
workspace_id: WorkspaceID
card_id: CardID
personal_workspace: WorkspaceID
workspace_id: WorkspaceID
card_id: CardID
personal_workspace: WorkspaceID
archived_from?: Date
last_view?: Date
last_update?: Date
archived_from?: Date
last_view?: Date
last_update?: Date
}
interface RawMessage extends MessageDb {
patches?: PatchDb[]
attachments?: AttachmentDb[]
reactions?: ReactionDb[]
patches?: PatchDb[]
attachments?: AttachmentDb[]
reactions?: ReactionDb[]
}
export function toMessage(raw: RawMessage): Message {
const lastPatch = raw.patches?.[0]
return {
id: raw.id,
card: raw.card_id,
content: lastPatch?.content ?? raw.content,
creator: raw.creator,
created: new Date(raw.created),
edited: lastPatch?.created ? new Date(lastPatch.created) : undefined,
reactions: (raw.reactions ?? []).map(toReaction),
attachments: (raw.attachments ?? []).map(toAttachment)
}
export function toMessage (raw: RawMessage): Message {
const lastPatch = raw.patches?.[0]
return {
id: String(raw.id) as MessageID,
card: raw.card_id,
content: lastPatch?.content ?? raw.content,
creator: raw.creator,
created: raw.created,
edited: lastPatch?.created ?? undefined,
thread:
raw.thread_id != null
? {
card: raw.card_id,
message: String(raw.id) as MessageID,
thread: raw.thread_id,
repliesCount: raw.replies_count ?? 0,
lastReply: raw.last_reply ?? new Date()
}
: undefined,
reactions: (raw.reactions ?? []).map(toReaction),
attachments: (raw.attachments ?? []).map(toAttachment)
}
}
export function toReaction(raw: ReactionDb): Reaction {
return {
message: raw.message_id,
reaction: raw.reaction,
creator: raw.creator,
created: new Date(raw.created)
}
export function toReaction (raw: ReactionDb): Reaction {
return {
message: String(raw.message_id) as MessageID,
reaction: raw.reaction,
creator: raw.creator,
created: raw.created
}
}
export function toAttachment(raw: AttachmentDb): Attachment {
return {
message: raw.message_id,
card: raw.card_id,
creator: raw.creator,
created: new Date(raw.created)
}
export function toAttachment (raw: AttachmentDb): Attachment {
return {
message: String(raw.message_id) as MessageID,
card: raw.card_id,
creator: raw.creator,
created: raw.created
}
}
export function toMessagesGroup(raw: MessagesGroupDb): MessagesGroup {
return {
card: raw.card_id,
blobId: raw.blob_id,
fromId: raw.from_id,
toId: raw.to_id,
fromDate: new Date(raw.from_date),
toDate: new Date(raw.to_date),
count: raw.count
}
}
export function toMessagesGroup (raw: MessagesGroupDb): MessagesGroup {
return {
card: raw.card_id,
blobId: raw.blob_id,
fromDate: raw.from_date,
toDate: raw.to_date,
fromId: String(raw.from_id) as MessageID,
toId: String(raw.to_id) as MessageID,
count: raw.count,
patches: raw.patches == null ? [] : raw.patches.filter((it: any) => it.message_id != null).map(toPatch)
}
}
export function toPatch (raw: PatchDb): Patch {
return {
type: raw.type,
message: String(raw.message_id) as MessageID,
content: raw.content,
creator: raw.creator,
created: new Date(raw.created)
}
}
export function toThread (raw: ThreadDb): Thread {
return {
card: raw.card_id,
message: String(raw.message_id) as MessageID,
thread: raw.thread_id,
repliesCount: raw.replies_count,
lastReply: raw.last_reply
}
}
+25 -19
View File
@@ -1,22 +1,28 @@
export function getCondition(table: string, dbField: string, index: number, param: any): { where: string, value: any } | undefined {
if (typeof param === 'object') {
if (param.less != null) {
return {where: `${table}.${dbField} < $${index}`, value: param.less};
}
if (param.lessOrEqual != null) {
return {where: `${table}.${dbField} <= $${index}`, value: param.lessOrEqual};
}
if (param.greater != null) {
return {where: `${table}.${dbField} > $${index}`, value: param.greater};
}
if (param.greaterOrEqual != null) {
return {where: `${table}.${dbField} >= $${index}`, value: param.greaterOrEqual};
}
export function getCondition (
table: string,
dbField: string,
index: number,
param: any,
type: string
): { where: string, value: any } | undefined {
if (typeof param === 'object') {
if (param.less != null) {
return { where: `${table}.${dbField} < $${index}::${type}`, value: param.less }
}
if(param != null) {
return {where: `${table}.${dbField} = $${index}`, value: param};
if (param.lessOrEqual != null) {
return { where: `${table}.${dbField} <= $${index}::${type}`, value: param.lessOrEqual }
}
if (param.greater != null) {
return { where: `${table}.${dbField} > $${index}::${type}`, value: param.greater }
}
if (param.greaterOrEqual != null) {
return { where: `${table}.${dbField} >= $${index}::${type}`, value: param.greaterOrEqual }
}
}
return undefined
}
if (param != null) {
return { where: `${table}.${dbField} = $${index}::${type}`, value: param }
}
return undefined
}
+1 -1
View File
@@ -1 +1 @@
export * from './adapter.ts'
export * from './adapter'
+16
View File
@@ -0,0 +1,16 @@
import { type ParameterOrJSON, type Row } from 'postgres'
export interface Logger {
info: (message: string, data?: Record<string, any>) => void
warn: (message: string, data?: Record<string, any>) => void
error: (message: string, data?: Record<string, any>) => void
}
export interface Options {
withLogs?: boolean
}
export interface SqlClient {
execute: <T extends any[] = (Row & Iterable<Row>)[]>(query: string, params?: ParameterOrJSON<any>[]) => Promise<T>
close: () => void
}
-1
View File
@@ -1 +0,0 @@
export * from './message.ts'
-24
View File
@@ -1,24 +0,0 @@
import type { MessageID } from '@hcengineering/communication-types'
let lastTimestamp = Math.floor(Date.now() / 1000)
let counter = 0
export function generateMessageId(): MessageID {
const timestamp = Math.floor(Date.now() / 1000)
if (timestamp !== lastTimestamp) {
lastTimestamp = timestamp
counter = 0
}
counter++
return ((timestamp << 24) | counter) as MessageID
}
export function parseMessageId(id: MessageID): { timestamp: number; counter: number } {
const timestamp = id >> 24
const counter = id & 0xffffff
return { timestamp, counter }
}
+1 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-examples",
"version": "0.1.9",
"version": "0.1.47",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
@@ -13,9 +13,7 @@
"@types/bun": "^1.1.14"
},
"dependencies": {
"@hcengineering/communication-client-query": "workspace:*",
"@hcengineering/communication-client-ws": "workspace:*",
"@hcengineering/communication-client-sqlite": "workspace:*",
"@hcengineering/communication-types": "workspace:*"
},
"peerDependencies": {
+13 -19
View File
@@ -1,39 +1,33 @@
import {
type Message,
type SocialID,
SortOrder,
SortingOrder,
type CardID,
type Window,
type WorkspaceID
} from '@hcengineering/communication-types'
import { getWebsocketClient } from '@hcengineering/communication-client-ws'
import { getSqliteClient } from '@hcengineering/communication-client-sqlite'
import { createMessagesQuery, initLiveQueries } from '@hcengineering/communication-client-query'
import { getWebsocketClient, createMessagesQuery } from '@hcengineering/communication-client-ws'
const card = 'cd0aba36-1c4f-4170-95f2-27a12a5415f6' as CardID
const workspace = 'cd0aba36-1c4f-4170-95f2-27a12a5415f6' as WorkspaceID
const personalWorkspace = 'cd0aba36-1c4f-4170-95f2-27a12a5415f5' as WorkspaceID
// const personalWorkspace = 'cd0aba36-1c4f-4170-95f2-27a12a5415f5' as WorkspaceID
const creator1 = 'email:vasya@huly.com' as SocialID
async function getClient(type: 'ws' | 'sqlite') {
if (type === 'ws') {
const platformUrl = 'ws://localhost:8090'
const token = 'token'
return await getWebsocketClient(platformUrl, token)
}
return await getSqliteClient(workspace, personalWorkspace)
}
export async function example() {
const client = await getClient('sqlite')
initLiveQueries(client)
const platformUrl = 'ws://localhost:8090'
const token = 'token'
const client = await getWebsocketClient(
platformUrl,
token,
workspace,
'http://localhost:4022/blob/:workspace/:blobId/:filename'
)
const query1 = createMessagesQuery()
let window: Window<Message> | undefined = undefined
query1.query({ card, sort: SortOrder.Desc }, (res) => {
query1.query({ card, order: SortingOrder.Descending }, (res) => {
window = res
const r = window.getResult()
r.reverse()
@@ -51,7 +45,7 @@ export async function example() {
})
async function editMessage(message: Message) {
await client.createPatch(card, message.id, message.content + '_1_', creator1)
await client.updateMessage(card, message.id, message.content + '_1_', creator1)
}
async function deleteMessage(message: Message) {
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-query",
"version": "0.1.9",
"version": "0.1.47",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
@@ -11,11 +11,12 @@
],
"devDependencies": {
"@types/bun": "^1.1.14",
"@types/crypto-js": "^4.2.2"
"@types/crypto-js": "^4.2.2"
},
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-shared": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
"fast-equals": "^5.0.1"
},
"peerDependencies": {
+17 -12
View File
@@ -1,4 +1,8 @@
import { type FindMessagesParams, type FindNotificationsParams } from '@hcengineering/communication-types'
import {
type FindMessagesParams,
type FindNotificationsParams,
type WorkspaceID
} from '@hcengineering/communication-types'
import { deepEqual } from 'fast-equals'
import type {
MessagesQueryCallback,
@@ -7,7 +11,7 @@ import type {
QueryClient
} from '@hcengineering/communication-sdk-types'
import type { Query, QueryId } from './types'
import type { FindParams, PagedQuery, QueryId } from './types'
import { MessagesQuery } from './messages/query'
import { NotificationQuery } from './notifications/query'
@@ -18,14 +22,15 @@ interface CreateQueryResult {
const maxQueriesCache = 10
export class LiveQueries {
private readonly client: QueryClient
private readonly queries = new Map<QueryId, Query>()
private readonly queries = new Map<QueryId, PagedQuery>()
private readonly unsubscribed = new Set<QueryId>()
private counter: number = 0
constructor(client: QueryClient) {
this.client = client
}
constructor(
private readonly client: QueryClient,
private readonly workspace: WorkspaceID,
private readonly filesUrl: string
) {}
async onEvent(event: ResponseEvent): Promise<void> {
for (const q of this.queries.values()) {
@@ -66,11 +71,11 @@ export class LiveQueries {
return exists
} else {
const result = exists.copyResult()
return new MessagesQuery(this.client, id, params, callback, result)
return new MessagesQuery(this.client, this.workspace, this.filesUrl, id, params, callback, result)
}
}
return new MessagesQuery(this.client, id, params, callback)
return new MessagesQuery(this.client, this.workspace, this.filesUrl, id, params, callback)
}
private createNotificationQuery(
@@ -103,7 +108,7 @@ export class LiveQueries {
}
}
private findNotificationQuery(params: FindMessagesParams): NotificationQuery | undefined {
private findNotificationQuery(params: FindNotificationsParams): NotificationQuery | undefined {
for (const query of this.queries.values()) {
if (query instanceof NotificationQuery) {
if (!this.queryCompare(params, query.params)) continue
@@ -112,7 +117,7 @@ export class LiveQueries {
}
}
private queryCompare(q1: FindMessagesParams, q2: FindMessagesParams): boolean {
private queryCompare(q1: FindParams, q2: FindParams): boolean {
if (Object.keys(q1).length !== Object.keys(q2).length) {
return false
}
@@ -136,7 +141,7 @@ export class LiveQueries {
this.unsubscribed.delete(id)
}
private unsubscribeQuery(query: Query): void {
private unsubscribeQuery(query: PagedQuery): void {
this.unsubscribed.add(query.id)
query.removeCallback()
if (this.unsubscribed.size > maxQueriesCache) {
+648 -156
View File
@@ -1,201 +1,693 @@
import {
type FindMessagesParams,
type MessageID,
type Message,
type Patch,
SortOrder
type FindMessagesParams,
type Message,
type MessagesGroup,
type WorkspaceID,
type MessageID,
SortingOrder,
type Patch,
type ParsedFile,
PatchType,
type CardID
} from '@hcengineering/communication-types'
import {
type AttachmentCreatedEvent,
type MessageCreatedEvent,
type PatchCreatedEvent,
type ReactionCreatedEvent,
type ResponseEvent,
ResponseEventType,
type AttachmentRemovedEvent,
type MessageRemovedEvent,
type ReactionRemovedEvent
ResponseEventType,
type AttachmentCreatedEvent,
type AttachmentRemovedEvent,
type MessageCreatedEvent,
type MessageRemovedEvent,
type PatchCreatedEvent,
type QueryCallback,
type QueryClient,
type ReactionCreatedEvent,
type ReactionRemovedEvent,
type ResponseEvent,
type ThreadCreatedEvent
} from '@hcengineering/communication-sdk-types'
import { loadGroupFile, parseMessageId } from '@hcengineering/communication-shared'
import { BaseQuery } from '../query'
import { QueryResult } from '../result'
import { defaultQueryParams, Direction, type QueryId, type PagedQuery } from '../types'
import { WindowImpl } from '../window'
import { addReaction, addReply, removeReaction, removeReply } from './utils'
export class MessagesQuery extends BaseQuery<Message, FindMessagesParams> {
override async find(params: FindMessagesParams): Promise<Message[]> {
return this.client.findMessages(params, this.id)
const GROUPS_LIMIT = 20
export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
protected result: Promise<QueryResult<Message>> | QueryResult<Message>
private messagesFromFiles: Message[] = []
private readonly groupsBuffer: MessagesGroup[] = []
private firstGroup?: MessagesGroup
private lastGroup?: MessagesGroup
private readonly limit: number
private readonly next = {
hasMessages: true,
hasGroups: true
}
private readonly prev = {
hasMessages: true,
hasGroups: true
}
constructor (
protected readonly client: QueryClient,
private readonly workspace: WorkspaceID,
private readonly filesUrl: string,
public readonly id: QueryId,
public readonly params: FindMessagesParams,
private callback?: QueryCallback<Message>,
initialResult?: QueryResult<Message>
) {
const baseLimit = params.id != null ? 1 : this.params.limit ?? defaultQueryParams.limit
this.limit = baseLimit + 1
this.params = {
...params,
order: params.order ?? defaultQueryParams.order
}
if (initialResult !== undefined) {
const messages = initialResult.getResult()
const count = messages.length
override getObjectId(object: Message): MessageID {
return object.id
}
override getObjectDate(object: Message): Date {
return object.created
}
override async onEvent(event: ResponseEvent): Promise<void> {
switch (event.type) {
case ResponseEventType.MessageCreated:
return await this.onCreateMessageEvent(event)
case ResponseEventType.MessageRemoved:
return await this.onRemoveMessageEvent(event)
case ResponseEventType.PatchCreated:
return await this.onCreatePatchEvent(event)
case ResponseEventType.ReactionCreated:
return await this.onCreateReactionEvent(event)
case ResponseEventType.ReactionRemoved:
return await this.onRemoveReactionEvent(event)
case ResponseEventType.AttachmentCreated:
return await this.onCreateAttachmentEvent(event)
case ResponseEventType.AttachmentRemoved:
return await this.onRemoveAttachmentEvent(event)
if (count < this.limit) {
this.result = initialResult
} else {
if (this.params.order === SortingOrder.Ascending) {
this.result = new QueryResult(messages.slice(0, baseLimit), (x) => x.id)
this.result.setHead(true)
this.result.setTail(false)
} else {
this.result = new QueryResult(messages.slice(0, baseLimit), (x) => x.id)
this.result.setHead(false)
this.result.setTail(true)
}
}
void this.notify()
} else {
this.result = new QueryResult([] as Message[], (x) => x.id)
if (this.isInitLoadingForward()) {
this.result.setHead(true)
void this.requestLoadNextPage()
} else {
this.result.setTail(true)
void this.requestLoadPrevPage()
}
}
}
setCallback (callback: QueryCallback<Message>): void {
this.callback = callback
void this.notify()
}
removeCallback (): void {
this.callback = () => {}
}
async requestLoadNextPage (): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
if (!this.result.isTail()) {
this.result = this.loadPage(Direction.Forward, this.result)
void this.result
.then(() => this.notify())
.catch((error) => {
console.error('Failed to load messages', error)
})
}
}
async requestLoadPrevPage (): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
if (!this.result.isHead()) {
this.result = this.loadPage(Direction.Backward, this.result)
void this.result
.then(() => this.notify())
.catch((error) => {
console.error('Failed to load messages', error)
})
}
}
private isInitLoadingForward (): boolean {
const { order, created, id } = this.params
if (id != null) {
return false
}
async onCreateMessageEvent(event: MessageCreatedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
if (created == null) return order === SortingOrder.Ascending
if (created instanceof Date) return order === SortingOrder.Ascending
// TODO: fix me
if (created.less != null) return order !== SortingOrder.Ascending
if (created.lessOrEqual != null) return order !== SortingOrder.Ascending
if (created.greater != null) return order === SortingOrder.Ascending
if (created.greaterOrEqual != null) return order === SortingOrder.Ascending
const message = {
...event.message,
edited: event.message.edited,
created: event.message.created
}
const exists = this.result.get(message.id)
return false
}
if (exists !== undefined) return
if (!this.match(message)) return
private async loadPage (direction: Direction, result: QueryResult<Message>): Promise<QueryResult<Message>> {
const { messages, fromDb } =
direction === Direction.Forward ? await this.loadNextMessages(result) : await this.loadPrevMessages(result)
if (this.result.isTail()) {
if (this.params.sort === SortOrder.Asc) {
this.result.push(message)
} else {
this.result.unshift(message)
if (!result.isHead() && direction === Direction.Backward) {
result.setHead(messages.length < this.limit)
}
if (!result.isTail() && direction === Direction.Forward) {
result.setTail(messages.length < this.limit)
}
if (messages.length === this.limit && this.limit > 1) {
const lastMessage = messages.pop()
if (lastMessage != null && !fromDb) {
direction === Direction.Forward
? this.messagesFromFiles.unshift(lastMessage)
: this.messagesFromFiles.push(lastMessage)
}
}
result.append(messages)
return result
}
// Load next
private async loadNextMessages (result: QueryResult<Message>): Promise<{ messages: Message[], fromDb: boolean }> {
const messages: Message[] = this.messagesFromFiles.splice(0, this.limit)
if (messages.length >= this.limit) return { messages, fromDb: false }
while (this.next.hasGroups || this.groupsBuffer.length > 0) {
await this.loadGroups(Direction.Forward, result)
messages.push(...this.messagesFromFiles.splice(0, this.limit - messages.length))
if (messages.length >= this.limit) return { messages, fromDb: false }
}
const dbMessages = await this.findNextMessages(this.limit - messages.length, result)
this.next.hasMessages = dbMessages.length > 0
messages.push(...dbMessages)
return { messages, fromDb: dbMessages.length > 0 }
}
private async findNextMessages (limit: number, result: QueryResult<Message>): Promise<Message[]> {
if (this.next.hasGroups) {
return []
}
if (result.isTail()) return []
const last = result.getLast()
return await this.find({
...this.params,
created:
last != null
? {
greater: last?.created
}
await this.notify()
}
: undefined,
limit,
order: SortingOrder.Ascending
})
}
// Load prev
private async loadPrevMessages (result: QueryResult<Message>): Promise<{ messages: Message[], fromDb: boolean }> {
const messages: Message[] = []
if (this.prev.hasMessages) {
const prevMessages = await this.findPrevMessages(this.limit, result)
this.prev.hasMessages = prevMessages.length > 0
messages.push(...prevMessages)
}
private match(message: Message): boolean {
if (this.params.id != null && this.params.id !== message.id) {
return false
}
if (this.params.card != null && this.params.card !== message.card) {
return false
}
return true
if (messages.length >= this.limit) return { messages, fromDb: true }
const restLimit = this.limit - messages.length
const fromBuffer = this.messagesFromFiles.splice(-restLimit, restLimit).reverse()
messages.push(...fromBuffer)
if (messages.length >= this.limit) return { messages, fromDb: false }
while (this.prev.hasGroups || this.groupsBuffer.length > 0) {
await this.loadGroups(Direction.Backward, result)
const rest = this.limit - messages.length
const fromBuffer2 = this.messagesFromFiles.splice(-rest, rest).reverse()
messages.push(...fromBuffer2)
if (messages.length >= this.limit) return { messages, fromDb: false }
}
private async onCreatePatchEvent(event: PatchCreatedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
return { messages, fromDb: false }
}
private async findPrevMessages (limit: number, result: QueryResult<Message>): Promise<Message[]> {
if (!this.prev.hasMessages || result.isHead()) return []
const first = result.getLast()
return await this.find({
...this.params,
created:
first != null
? {
less: first?.created
}
: undefined,
limit,
order: SortingOrder.Descending
})
}
private async loadGroups (direction: Direction, result: QueryResult<Message>): Promise<void> {
let messagesCount = 0
const lastResult = result.getLast()
const toLoad: MessagesGroup[] = []
const toBuffer: MessagesGroup[] = []
while (messagesCount < this.limit) {
const currentGroups = this.groupsBuffer.splice(direction === Direction.Forward ? 0 : -GROUPS_LIMIT, GROUPS_LIMIT)
const hasGroups = direction === Direction.Forward ? this.next.hasGroups : this.prev.hasGroups
if (currentGroups.length === 0 && !hasGroups) break
const groups =
currentGroups.length > 0
? currentGroups
: await this.findGroups(
direction,
direction === Direction.Forward ? this.lastGroup?.fromDate : this.firstGroup?.fromDate
)
if (currentGroups.length === 0) {
this.firstGroup = direction === Direction.Forward ? this.firstGroup ?? groups[0] : groups[groups.length - 1]
this.lastGroup =
direction === Direction.Forward ? groups[groups.length - 1] ?? this.lastGroup : this.lastGroup ?? groups[0]
if (direction === Direction.Forward) {
this.next.hasGroups = groups.length >= GROUPS_LIMIT
} else {
this.prev.hasGroups = groups.length >= GROUPS_LIMIT
}
const patch = {
...event.patch,
created: event.patch.created
if (this.params.id != null) {
this.next.hasGroups = false
this.prev.hasGroups = false
}
}
const message = this.result.get(patch.message)
const orderedGroups = direction === Direction.Forward ? groups : groups.reverse()
while (messagesCount < this.limit && orderedGroups.length > 0) {
const group = direction === Direction.Forward ? orderedGroups.shift() : orderedGroups.pop()
if (group == null) break
toLoad.push(group)
messagesCount += group.count
}
if (message === undefined) return
if (message.created < patch.created) {
this.result.update(this.applyPatch(message, patch))
await this.notify()
}
while (orderedGroups.length > 0) {
const group = direction === Direction.Forward ? orderedGroups.shift() : orderedGroups.pop()
if (group == null) break
toBuffer.push(group)
messagesCount += group.count
}
}
private async onRemoveMessageEvent(event: MessageRemovedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
const deleted = this.result.delete(event.message)
if (deleted !== undefined) {
await this.notify()
}
if (direction === Direction.Forward) {
this.groupsBuffer.push(...toBuffer)
} else {
this.groupsBuffer.unshift(...toBuffer)
}
private async onCreateReactionEvent(event: ReactionCreatedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
const parsedFiles = await Promise.all(toLoad.map((group) => this.loadMessagesFromFiles(group)))
const reaction = {
...event.reaction,
created: event.reaction.created
}
const message = this.result.get(reaction.message)
if (message === undefined) return
for (const file of parsedFiles) {
if (file.messages.length === 0) continue
if (direction === Direction.Forward) {
const firstInFile = file.messages[0]
const queryDate =
lastResult != null && firstInFile.created < lastResult?.created ? lastResult?.created : undefined
this.messagesFromFiles.push(...this.matchFileMessages(file, queryDate))
} else {
const lastInFile = file.messages[file.messages.length - 1]
const queryDate =
lastResult != null && lastInFile.created > lastResult?.created ? lastResult?.created : undefined
this.messagesFromFiles.unshift(...this.matchFileMessages(file, queryDate))
}
}
}
message.reactions.push(reaction)
this.result.update(message)
await this.notify()
private matchFileMessages (file: ParsedFile, created?: Date): Message[] {
let result: Message[] = file.messages
if (this.params.id != null) {
const msg = file.messages.find((it) => it.id === this.params.id)
result = msg != null ? [msg] : []
}
private async onRemoveReactionEvent(event: ReactionRemovedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
const message = this.result.get(event.message)
if (message === undefined) return
const reactions = message.reactions.filter((it) => it.reaction !== event.reaction && it.creator !== event.creator)
if (reactions.length === message.reactions.length) return
const updated = {
...message,
reactions
}
this.result.update(updated)
await this.notify()
if (created != null) {
result =
this.params.order === SortingOrder.Ascending
? result.filter((it) => it.created > created)
: result.filter((it) => it.created < created)
}
private async onCreateAttachmentEvent(event: AttachmentCreatedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
return result
}
const attachment = {
...event.attachment,
created: event.attachment.created
}
const message = this.result.get(attachment.message)
if (message === undefined) return
private async loadMessagesFromFiles (group: MessagesGroup): Promise<ParsedFile> {
const parsedFile = await loadGroupFile(this.workspace, this.filesUrl, group, { retries: 5 })
message.attachments.push(attachment)
this.result.update(message)
await this.notify()
const patches = group.patches ?? []
const patchesMap = new Map<MessageID, Patch[]>()
for (const patch of patches) {
patchesMap.set(patch.message, [...(patchesMap.get(patch.message) ?? []), patch])
}
private async onRemoveAttachmentEvent(event: AttachmentRemovedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
return {
metadata: parsedFile.metadata,
messages:
patches.length > 0
? parsedFile.messages.map((message) => this.applyPatches(message, patchesMap.get(message.id) ?? []))
: parsedFile.messages
}
}
const message = this.result.get(event.message)
if (message === undefined) return
private async findGroupByMessage (id: MessageID): Promise<MessagesGroup | undefined> {
const date = parseMessageId(id)
const group1 = (await this.client.findMessagesGroups({
card: this.params.card,
limit: 1,
toDate: {
greaterOrEqual: date
},
order: SortingOrder.Descending,
orderBy: 'fromDate'
}))[0]
const attachments = message.attachments.filter((it) => it.card !== event.card)
if (attachments.length === message.attachments.length) return
const updated = {
...message,
attachments
}
this.result.update(updated)
await this.notify()
if (group1 !== undefined) {
return group1
}
private applyPatch(message: Message, patch: Patch): Message {
return (await this.client.findMessagesGroups({
card: this.params.card,
limit: 1,
fromDate: {
lessOrEqual: date
},
order: SortingOrder.Ascending,
orderBy: 'fromDate'
}))[0]
}
private async findGroups (direction: Direction, fromDate?: Date): Promise<MessagesGroup[]> {
if (this.params.id != null) {
const group = await this.findGroupByMessage(this.params.id)
return group !== undefined ? [group] : []
}
if (fromDate == null) {
return await this.client.findMessagesGroups({
card: this.params.card,
limit: GROUPS_LIMIT,
order: direction === Direction.Forward ? SortingOrder.Ascending : SortingOrder.Descending,
orderBy: 'fromDate'
})
}
return await this.client.findMessagesGroups({
card: this.params.card,
limit: GROUPS_LIMIT,
order: direction === Direction.Forward ? SortingOrder.Ascending : SortingOrder.Descending,
orderBy: 'fromDate',
fromDate:
direction === Direction.Forward
? {
greater: fromDate
}
: {
less: fromDate
}
})
}
private async find (params: FindMessagesParams): Promise<Message[]> {
return await this.client.findMessages(params, this.id)
}
private async notify (): Promise<void> {
if (this.callback == null) return
if (this.result instanceof Promise) this.result = await this.result
const result = this.result.getResult()
this.callback(new WindowImpl(result, this.result.isTail(), this.result.isHead(), this))
}
async unsubscribe (): Promise<void> {
await this.client.unsubscribeQuery(this.id)
}
async onEvent (event: ResponseEvent): Promise<void> {
switch (event.type) {
case ResponseEventType.MessageCreated: {
await this.onCreateMessageEvent(event)
return
}
case ResponseEventType.MessageRemoved: {
await this.onRemoveMessageEvent(event)
return
}
case ResponseEventType.PatchCreated: {
await this.onCreatePatchEvent(event)
return
}
case ResponseEventType.ReactionCreated: {
await this.onCreateReactionEvent(event)
return
}
case ResponseEventType.ReactionRemoved: {
await this.onRemoveReactionEvent(event)
return
}
case ResponseEventType.AttachmentCreated: {
await this.onCreateAttachmentEvent(event)
return
}
case ResponseEventType.AttachmentRemoved: {
await this.onRemoveAttachmentEvent(event)
return
}
case ResponseEventType.ThreadCreated: {
await this.onCreateThreadEvent(event)
}
}
}
async onCreateThreadEvent (event: ThreadCreatedEvent): Promise<void> {
if (this.params.card !== event.thread.card) return
if (this.result instanceof Promise) this.result = await this.result
const message = this.result.get(event.thread.message)
if (message !== undefined) {
const updated: Message = {
...message,
thread: event.thread
}
this.result.update(updated)
void this.notify()
}
this.messagesFromFiles = this.messagesFromFiles.map((it) => {
if (it.id === event.thread.message) {
return {
...message,
content: patch.content,
creator: patch.creator,
created: patch.created
...it,
thread: event.thread
}
}
return it
})
}
async onCreateMessageEvent (event: MessageCreatedEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
if (this.params.card !== event.message.card) return
const { message } = event
const exists = this.result.get(message.id)
if (exists !== undefined) return
if (!this.match(message)) return
if (this.result.isTail()) {
if (this.params.order === SortingOrder.Ascending) {
this.result.push(message)
} else {
this.result.unshift(message)
}
await this.notify()
}
}
private match (message: Message): boolean {
if (this.params.id != null && this.params.id !== message.id) {
return false
}
if (this.params.card !== message.card) {
return false
}
return true
}
private async onCreatePatchEvent (event: PatchCreatedEvent): Promise<void> {
if (this.params.card !== event.card) return
if (this.result instanceof Promise) this.result = await this.result
const { patch } = event
const messageId = BigInt(patch.message)
const group = this.groupsBuffer.find((it) => BigInt(it.fromId) <= messageId && BigInt(it.toId) >= messageId)
if (group != null) {
group.patches.push(patch)
}
const message = this.result.get(patch.message)
if (message === undefined) return
if (message.created < patch.created) {
this.result.update(this.applyPatch(message, patch))
await this.notify()
}
}
private async onRemoveMessageEvent (event: MessageRemovedEvent): Promise<void> {
if (this.params.card !== event.card) return
if (this.result instanceof Promise) this.result = await this.result
const deleted = this.result.delete(event.message)
if (deleted !== undefined) {
void this.notify()
}
this.messagesFromFiles = this.messagesFromFiles.filter((it) => it.id !== event.message)
}
private async onCreateReactionEvent (event: ReactionCreatedEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
if (this.params.card !== event.card) return
const reaction = {
...event.reaction,
created: event.reaction.created
}
const message = this.result.get(reaction.message)
if (message !== undefined) {
this.result.update(addReaction(message, reaction))
void this.notify()
}
const fromBuffer = this.messagesFromFiles.find((it) => it.id === reaction.message)
if (fromBuffer !== undefined) {
addReaction(fromBuffer, reaction)
}
}
private async onRemoveReactionEvent (event: ReactionRemovedEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
if (this.params.card !== event.card) return
const message = this.result.get(event.message)
if (message !== undefined) {
const updated = removeReaction(message, event.reaction, event.creator)
if (updated.reactions.length !== message.reactions.length) {
this.result.update(updated)
void this.notify()
}
}
this.messagesFromFiles = this.messagesFromFiles.map((it) =>
it.id === event.message ? removeReaction(it, event.reaction, event.creator) : it
)
}
private async onCreateAttachmentEvent (event: AttachmentCreatedEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
const attachment = {
...event.attachment,
created: event.attachment.created
}
const message = this.result.get(attachment.message)
if (message === undefined) return
message.attachments.push(attachment)
this.result.update(message)
await this.notify()
}
private async onRemoveAttachmentEvent (event: AttachmentRemovedEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
const message = this.result.get(event.message)
if (message === undefined) return
const attachments = message.attachments.filter((it) => it.card !== event.card)
if (attachments.length === message.attachments.length) return
const updated = {
...message,
attachments
}
this.result.update(updated)
await this.notify()
}
private applyPatch (message: Message, patch: Patch): Message {
switch (patch.type) {
case PatchType.update:
return {
...message,
edited: patch.created,
content: patch.content
}
case PatchType.addReaction:
return addReaction(message, {
message: message.id,
reaction: patch.content,
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 message
}
private applyPatches (message: Message, patches: Patch[]): Message {
if (patches.length === 0) return message
for (const p of patches) {
message = this.applyPatch(message, p)
}
return message
}
copyResult (): QueryResult<Message> | undefined {
if (this.result instanceof Promise) {
return undefined
}
return this.result.copy()
}
}
+54
View File
@@ -0,0 +1,54 @@
import type { CardID, Message, Reaction, SocialID } from '@hcengineering/communication-types'
export function addReaction (message: Message, reaction: Reaction): Message {
message.reactions.push(reaction)
return message
}
export function removeReaction (message: Message, emoji: string, creator: SocialID): Message {
const reactions = message.reactions.filter((it) => it.reaction !== emoji || it.creator !== creator)
if (reactions.length === message.reactions.length) return message
return {
...message,
reactions
}
}
export function addReply (message: Message, thread: CardID, created: Date): Message {
if (message.thread === undefined) {
return {
...message,
thread: {
card: message.card,
message: message.id,
thread,
repliesCount: 1,
lastReply: created
}
}
}
if (message.thread.thread !== thread) return message
return {
...message,
thread: {
...message.thread,
repliesCount: message.thread.repliesCount + 1,
lastReply: created
}
}
}
export 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
}
}
}
+119 -110
View File
@@ -1,129 +1,138 @@
import {
type FindNotificationsParams,
SortOrder,
type Notification,
type MessageID,
type FindNotificationsParams,
SortingOrder,
type Notification,
type MessageID
} from '@hcengineering/communication-types'
import {
type NotificationCreatedEvent,
ResponseEventType,
type ResponseEvent,
type NotificationContextRemovedEvent,
type NotificationRemovedEvent,
type NotificationContextUpdatedEvent,
type NotificationCreatedEvent,
ResponseEventType,
type ResponseEvent,
type NotificationContextRemovedEvent,
type NotificationRemovedEvent,
type NotificationContextUpdatedEvent
} from '@hcengineering/communication-sdk-types'
import {BaseQuery} from '../query.ts';
import { BaseQuery } from '../query'
export class NotificationQuery extends BaseQuery<Notification, FindNotificationsParams> {
override async find(params: FindNotificationsParams): Promise<Notification[]> {
return this.client.findNotifications(params, this.id)
override async find (params: FindNotificationsParams): Promise<Notification[]> {
return await this.client.findNotifications(params, this.id)
}
override getObjectId (object: Notification): MessageID {
return object.message.id
}
override getObjectDate (object: Notification): Date {
return object.message.created
}
override async onEvent (event: ResponseEvent): Promise<void> {
switch (event.type) {
case ResponseEventType.NotificationCreated: {
await this.onCreateNotificationEvent(event)
return
}
case ResponseEventType.NotificationRemoved: {
await this.onRemoveNotificationEvent(event)
return
}
case ResponseEventType.NotificationContextUpdated: {
await this.onUpdateNotificationContextEvent(event)
return
}
case ResponseEventType.NotificationContextRemoved: {
await this.onRemoveNotificationContextEvent(event)
}
}
}
async onCreateNotificationEvent (event: NotificationCreatedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
override getObjectId(object: Notification): MessageID {
return object.message.id
const exists = this.result.get(event.notification.message.id)
if (exists !== undefined) return
if (this.params.message != null && this.params.message !== event.notification.message.id) return
if (this.params.context != null && this.params.context !== event.notification.context) return
if (this.result.isTail()) {
if (this.params.order === SortingOrder.Ascending) {
this.result.push(event.notification)
} else {
this.result.unshift(event.notification)
}
await this.notify()
}
}
private async onUpdateNotificationContextEvent (event: NotificationContextUpdatedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
override getObjectDate(object: Notification): Date {
return object.message.created
}
if (this.params.context != null && this.params.context !== event.context) return
if (event.update.lastView === undefined && event.update.archivedFrom === undefined) return
override async onEvent(event: ResponseEvent): Promise<void> {
switch (event.type) {
case ResponseEventType.NotificationCreated:
return await this.onCreateNotificationEvent(event)
case ResponseEventType.NotificationRemoved:
return await this.onRemoveNotificationEvent(event)
case ResponseEventType.NotificationContextUpdated:
return await this.onUpdateNotificationContextEvent(event)
case ResponseEventType.NotificationContextRemoved:
return await this.onRemoveNotificationContextEvent(event)
}
}
const toUpdate =
this.params.context === event.context
? this.result.getResult()
: this.result.getResult().filter((it) => it.context === event.context)
if (toUpdate.length === 0) return
async onCreateNotificationEvent(event: NotificationCreatedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
const exists = this.result.get(event.notification.message.id)
if (exists !== undefined) return
if (this.params.message != null && this.params.message !== event.notification.message.id) return
if (this.params.context != null && this.params.context !== event.notification.context) return
if (this.result.isTail()) {
if (this.params.sort === SortOrder.Asc) {
this.result.push(event.notification)
} else {
this.result.unshift(event.notification)
for (const notification of toUpdate) {
this.result.update({
...notification,
...(event.update.lastView !== undefined
? {
read: event.update.lastView < notification.message.created
}
await this.notify()
}
}
private async onUpdateNotificationContextEvent(event: NotificationContextUpdatedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
if (this.params.context != null && this.params.context !== event.context) return
if (event.update.lastView === undefined && event.update.archivedFrom === undefined) return
const toUpdate = this.params.context === event.context ?
this.result.getResult()
: this.result.getResult().filter(it => it.context === event.context)
if (toUpdate.length === 0) return
for (const notification of toUpdate) {
this.result.update({
...notification,
...event.update.lastView !== undefined ? {
read: event.update.lastView < notification.message.created
} : {},
...event.update.archivedFrom !== undefined ? {
archived: event.update.archivedFrom < notification.message.created
} : {}
})
}
}
private async onRemoveNotificationEvent(event: NotificationRemovedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
const deleted = this.result.delete(event.message)
if (deleted !== undefined) {
await this.notify()
}
}
private async onRemoveNotificationContextEvent(event: NotificationContextRemovedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
if (this.params.context != null && this.params.context !== event.context) return
if (event.context === this.params.context) {
if (this.result.length === 0) return
this.result.deleteAll()
this.result.setHead(true)
this.result.setTail(true)
await this.notify()
} else {
const toRemove = this.result.getResult().filter(it => it.context === event.context)
if (toRemove.length === 0) return
for (const notification of toRemove) {
this.result.delete(notification.message.id)
: {}),
...(event.update.archivedFrom !== undefined
? {
archived: event.update.archivedFrom < notification.message.created
}
await this.notify()
}
: {})
})
}
}
private async onRemoveNotificationEvent (event: NotificationRemovedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
const deleted = this.result.delete(event.message)
if (deleted !== undefined) {
await this.notify()
}
}
private async onRemoveNotificationContextEvent (event: NotificationContextRemovedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
if (this.params.context != null && this.params.context !== event.context) return
if (event.context === this.params.context) {
if (this.result.length === 0) return
this.result.deleteAll()
this.result.setHead(true)
this.result.setTail(true)
await this.notify()
} else {
const toRemove = this.result.getResult().filter((it) => it.context === event.context)
if (toRemove.length === 0) return
for (const notification of toRemove) {
this.result.delete(notification.message.id)
}
await this.notify()
}
}
}
+18 -23
View File
@@ -1,11 +1,11 @@
import { Direction, type ID, SortOrder } from '@hcengineering/communication-types'
import { SortingOrder, type ID } from '@hcengineering/communication-types'
import { type ResponseEvent, type QueryCallback, type QueryClient } from '@hcengineering/communication-sdk-types'
import { QueryResult } from './result'
import { defaultQueryParams, type FindParams, type Query, type QueryId } from './types'
import { defaultQueryParams, type FindParams, type PagedQuery, type QueryId } from './types'
import { WindowImpl } from './window'
export class BaseQuery<T, P extends FindParams> implements Query<T, P> {
export class BaseQuery<T, P extends FindParams> implements PagedQuery<T, P> {
protected result: QueryResult<T> | Promise<QueryResult<T>>
private forward: Promise<T[]> | T[] = []
private backward: Promise<T[]> | T[] = []
@@ -24,20 +24,18 @@ export class BaseQuery<T, P extends FindParams> implements Query<T, P> {
const limit = this.params.limit ?? defaultQueryParams.limit
const findParams = {
...this.params,
excluded: this.params.excluded ?? defaultQueryParams.excluded,
direction: this.params.direction ?? defaultQueryParams.direction,
sort: this.params.sort ?? defaultQueryParams.sort,
sort: this.params.order ?? defaultQueryParams.order,
limit: limit + 1
}
const findPromise = this.find(findParams)
this.result = findPromise.then((res) => {
const isTail = params.from ? res.length <= limit : params.sort === SortOrder.Desc
const isHead = params.from === undefined && params.sort === SortOrder.Asc
const isTail = false
const isHead = false
if (!isTail) {
res.pop()
}
const qResult = new QueryResult(res, this.getObjectId)
const qResult = new QueryResult(res, (x) => this.getObjectId(x))
qResult.setTail(isTail)
qResult.setHead(isHead)
@@ -55,25 +53,25 @@ export class BaseQuery<T, P extends FindParams> implements Query<T, P> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async find(params: FindParams): Promise<T[]> {
/*Implement in subclass*/
/* Implement in subclass */
return [] as T[]
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected getObjectId(object: T): ID {
/*Implement in subclass*/
/* Implement in subclass */
return '' as ID
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected getObjectDate(object: T): Date {
/*Implement in subclass*/
return new Date(0) as Date
/* Implement in subclass */
return new Date(0)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async onEvent(event: ResponseEvent): Promise<void> {
/*Implement in subclass*/
/* Implement in subclass */
}
setCallback(callback: QueryCallback<T>): void {
@@ -99,7 +97,7 @@ export class BaseQuery<T, P extends FindParams> implements Query<T, P> {
this.callback(window)
}
async loadForward() {
async requestLoadNextPage(): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
@@ -117,9 +115,8 @@ export class BaseQuery<T, P extends FindParams> implements Query<T, P> {
...this.params,
from: this.getObjectDate(last),
excluded: true,
direction: Direction.Forward,
limit: limit + 1,
sort: SortOrder.Asc
order: SortingOrder.Ascending
}
const forward = this.find(findParams)
@@ -139,7 +136,7 @@ export class BaseQuery<T, P extends FindParams> implements Query<T, P> {
})
}
async loadBackward() {
async requestLoadPrevPage(): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
@@ -149,17 +146,15 @@ export class BaseQuery<T, P extends FindParams> implements Query<T, P> {
if (this.result.isHead()) return
const first = this.params.sort === SortOrder.Asc ? this.result.getFirst() : this.result.getLast()
const first = this.params.order === SortingOrder.Ascending ? this.result.getFirst() : this.result.getLast()
if (first === undefined) return
const limit = this.params.limit ?? defaultQueryParams.limit
const findParams: FindParams = {
...this.params,
from: this.getObjectDate(first),
excluded: true,
direction: Direction.Backward,
limit: limit + 1,
sort: SortOrder.Desc
order: SortingOrder.Descending
}
const backward = this.find(findParams)
@@ -172,7 +167,7 @@ export class BaseQuery<T, P extends FindParams> implements Query<T, P> {
res.pop()
}
if (this.params.sort === SortOrder.Asc) {
if (this.params.order === SortingOrder.Ascending) {
const reversed = res.reverse()
this.result.prepend(reversed)
} else {
+5 -5
View File
@@ -25,11 +25,11 @@ export class QueryResult<T> {
return this.head
}
setHead(head: boolean) {
setHead(head: boolean): void {
this.head = head
}
setTail(tail: boolean) {
setTail(tail: boolean): void {
this.tail = tail
}
@@ -47,7 +47,7 @@ export class QueryResult<T> {
return object
}
deleteAll() {
deleteAll(): void {
this.objectById.clear()
}
@@ -71,12 +71,12 @@ export class QueryResult<T> {
return Array.from(this.objectById.values())[this.objectById.size - 1]
}
prepend(objects: T[]) {
prepend(objects: T[]): void {
const current = Array.from(this.objectById.entries())
this.objectById = new Map([...objects.map<[ID, T]>((object) => [this.getId(object), object]), ...current])
}
append(objects: T[]) {
append(objects: T[]): void {
for (const object of objects) {
this.objectById.set(this.getId(object), object)
}
+16 -15
View File
@@ -1,33 +1,34 @@
import { type ResponseEvent } from '@hcengineering/communication-sdk-types'
import { Direction, SortOrder, type Window } from '@hcengineering/communication-types'
import { SortingOrder, type Window } from '@hcengineering/communication-types'
import { QueryResult } from './result.ts'
import { QueryResult } from './result'
export type QueryId = number
export const defaultQueryParams = {
limit: 50,
excluded: false,
direction: Direction.Forward,
sort: SortOrder.Desc
order: SortingOrder.Ascending
}
export type FindParams = Partial<typeof defaultQueryParams> & {
from?: Date
export enum Direction {
Forward = 1,
Backward = -1
}
export interface Query<R = any, P = FindParams> {
export type FindParams = Partial<typeof defaultQueryParams>
export interface PagedQuery<R = any, P = FindParams> {
readonly id: QueryId
readonly params: P
onEvent(event: ResponseEvent): Promise<void>
onEvent: (event: ResponseEvent) => Promise<void>
loadForward(): Promise<void>
loadBackward(): Promise<void>
requestLoadNextPage: () => Promise<void>
requestLoadPrevPage: () => Promise<void>
unsubscribe(): Promise<void>
unsubscribe: () => Promise<void>
setCallback(callback: (window: Window<R>) => void): void
removeCallback(): void
copyResult(): QueryResult<R> | undefined
setCallback: (callback: (window: Window<R>) => void) => void
removeCallback: () => void
copyResult: () => QueryResult<R> | undefined
}
+4 -4
View File
@@ -1,13 +1,13 @@
import type { Window } from '@hcengineering/communication-types'
import type { Query } from './types'
import type { PagedQuery } from './types'
export class WindowImpl<T> implements Window<T> {
constructor(
private readonly result: T[],
private readonly isTail: boolean,
private readonly isHead: boolean,
private readonly query: Query<T>
private readonly query: PagedQuery<T>
) {}
getResult(): T[] {
@@ -16,12 +16,12 @@ export class WindowImpl<T> implements Window<T> {
async loadNextPage(): Promise<void> {
if (!this.hasNextPage()) return
await this.query.loadForward()
await this.query.requestLoadNextPage()
}
async loadPrevPage(): Promise<void> {
if (!this.hasPrevPage()) return
await this.query.loadBackward()
await this.query.requestLoadPrevPage()
}
hasNextPage(): boolean {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-sdk-types",
"version": "0.1.9",
"version": "0.1.47",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./types/index.d.ts",
@@ -20,7 +20,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/hcengineering/communication.git",
"url": "git+https://github.com/hcengineering/communication.git"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com"
+7 -2
View File
@@ -9,7 +9,9 @@ import type {
NotificationContextUpdate,
RichText,
SocialID,
Notification
Notification,
FindMessagesGroupsParams,
MessagesGroup
} from '@hcengineering/communication-types'
import type { FindMessagesParams } from '@hcengineering/communication-types'
@@ -18,7 +20,7 @@ import type { ResponseEvent } from './responseEvent.ts'
export interface Client {
createMessage(card: CardID, content: RichText, creator: SocialID): Promise<MessageID>
removeMessage(card: CardID, id: MessageID): Promise<void>
createPatch(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise<void>
updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise<void>
createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void>
removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void>
@@ -26,6 +28,8 @@ export interface Client {
createAttachment(card: CardID, message: MessageID, attachment: CardID, creator: SocialID): Promise<void>
removeAttachment(card: CardID, message: MessageID, attachment: CardID): Promise<void>
createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void>
createNotification(message: MessageID, context: ContextID): Promise<void>
removeNotification(message: MessageID, context: ContextID): Promise<void>
@@ -36,6 +40,7 @@ export interface Client {
onEvent(event: ResponseEvent): void
findMessages(params: FindMessagesParams, queryId?: number): Promise<Message[]>
findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]>
findNotificationContexts(params: FindNotificationContextParams, queryId?: number): Promise<NotificationContext[]>
findNotifications(params: FindNotificationsParams, queryId?: number): Promise<Notification[]>
+24 -17
View File
@@ -14,47 +14,55 @@ import type {
BlobID,
FindMessagesGroupsParams,
MessagesGroup,
WorkspaceID
WorkspaceID,
PatchType,
Thread
} from '@hcengineering/communication-types'
export interface DbAdapter {
createMessage(card: CardID, content: RichText, creator: SocialID, created: Date): Promise<MessageID>
removeMessage(card: CardID, id: MessageID, socialIds?: SocialID[]): Promise<void>
removeMessages(card: CardID, fromId: MessageID, toId: MessageID): Promise<void>
removeMessage(card: CardID, id: MessageID): Promise<MessageID | undefined>
removeMessages(card: CardID, ids: MessageID[]): Promise<MessageID[]>
createPatch(card: CardID, message: MessageID, content: RichText, creator: SocialID, created: Date): Promise<void>
createPatch(
card: CardID,
message: MessageID,
type: PatchType,
content: RichText,
creator: SocialID,
created: Date
): Promise<void>
removePatches(card: CardID, fromId: MessageID, toId: MessageID): Promise<void>
createMessagesGroup(
card: CardID,
blobId: BlobID,
from_id: MessageID,
to_id: MessageID,
from_date: Date,
to_date: Date,
fromDate: Date,
toDate: Date,
fromID: MessageID,
toID: MessageID,
count: number
): Promise<void>
removeMessagesGroup(card: CardID, blobId: BlobID): Promise<void>
createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID, created: Date): Promise<void>
removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void>
createAttachment(message: MessageID, attachment: CardID, creator: SocialID, created: Date): Promise<void>
removeAttachment(message: MessageID, attachment: CardID): Promise<void>
findMessages(query: FindMessagesParams): Promise<Message[]>
createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void>
updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise<void>
findMessagesGroups(query: FindMessagesGroupsParams): Promise<MessagesGroup[]>
findMessages(params: FindMessagesParams): Promise<Message[]>
findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]>
findThread(thread: CardID): Promise<Thread | undefined>
createNotification(message: MessageID, context: ContextID): Promise<void>
removeNotification(message: MessageID, context: ContextID): Promise<void>
createContext(personalWorkspace: WorkspaceID, card: CardID, lastView?: Date, lastUpdate?: Date): Promise<ContextID>
updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void>
removeContext(context: ContextID): Promise<void>
findContexts(
@@ -62,7 +70,6 @@ export interface DbAdapter {
personalWorkspaces: WorkspaceID[],
workspace?: WorkspaceID
): Promise<NotificationContext[]>
findNotifications(
params: FindNotificationsParams,
personalWorkspace: WorkspaceID,
+7 -1
View File
@@ -4,7 +4,13 @@ import type { Client } from './client'
export type QueryClient = Pick<
Client,
'onEvent' | 'findMessages' | 'findNotificationContexts' | 'findNotifications' | 'unsubscribeQuery' | 'close'
| 'onEvent'
| 'findMessages'
| 'findMessagesGroups'
| 'findNotificationContexts'
| 'findNotifications'
| 'unsubscribeQuery'
| 'close'
>
export type QueryCallback<T> = (window: Window<T>) => void
+39 -5
View File
@@ -5,24 +5,35 @@ import type {
NotificationContextUpdate,
RichText,
SocialID,
MessagesGroup
MessagesGroup,
BlobID
} from '@hcengineering/communication-types'
export enum RequestEventType {
CreateMessage = 'createMessage',
RemoveMessage = 'removeMessage',
RemoveMessages = 'removeMessages',
CreatePatch = 'createPatch',
RemovePatches = 'removePatches',
CreateReaction = 'createReaction',
RemoveReaction = 'removeReaction',
CreateAttachment = 'createAttachment',
RemoveAttachment = 'removeAttachment',
CreateThread = 'createThread',
CreateMessagesGroup = 'createMessagesGroup',
RemoveMessagesGroup = 'removeMessagesGroup',
CreateNotification = 'createNotification',
RemoveNotification = 'removeNotification',
CreateNotificationContext = 'createNotificationContext',
RemoveNotificationContext = 'removeNotificationContext',
UpdateNotificationContext = 'updateNotificationContext',
CreateMessagesGroup = 'createMessagesGroup'
UpdateNotificationContext = 'updateNotificationContext'
}
export type RequestEvent =
@@ -30,6 +41,7 @@ export type RequestEvent =
| RemoveMessageEvent
| RemoveMessagesEvent
| CreatePatchEvent
| RemovePatchesEvent
| CreateReactionEvent
| RemoveReactionEvent
| CreateAttachmentEvent
@@ -40,6 +52,8 @@ export type RequestEvent =
| RemoveNotificationContextEvent
| UpdateNotificationContextEvent
| CreateMessagesGroupEvent
| RemoveMessagesGroupEvent
| CreateThreadEvent
export interface CreateMessageEvent {
type: RequestEventType.CreateMessage
@@ -57,8 +71,8 @@ export interface RemoveMessageEvent {
export interface RemoveMessagesEvent {
type: RequestEventType.RemoveMessages
card: CardID
messages: MessageID[]
silent?: boolean
fromId: MessageID
toId: MessageID
}
export interface CreatePatchEvent {
@@ -69,6 +83,13 @@ export interface CreatePatchEvent {
creator: SocialID
}
export interface RemovePatchesEvent {
type: RequestEventType.RemovePatches
card: CardID
fromId: MessageID
toId: MessageID
}
export interface CreateReactionEvent {
type: RequestEventType.CreateReaction
card: CardID
@@ -100,6 +121,13 @@ export interface RemoveAttachmentEvent {
attachment: CardID
}
export interface CreateThreadEvent {
type: RequestEventType.CreateThread
card: CardID
message: MessageID
thread: CardID
}
export interface CreateNotificationEvent {
type: RequestEventType.CreateNotification
message: MessageID
@@ -135,6 +163,12 @@ export interface CreateMessagesGroupEvent {
group: MessagesGroup
}
export interface RemoveMessagesGroupEvent {
type: RequestEventType.RemoveMessagesGroup
card: CardID
blobId: BlobID
}
export type EventResult = CreateMessageResult | CreateNotificationContextResult | {}
export interface CreateMessageResult {
+9 -9
View File
@@ -9,18 +9,19 @@ import type {
Patch,
Reaction,
SocialID,
Notification
Notification,
Thread
} from '@hcengineering/communication-types'
export enum ResponseEventType {
MessageCreated = 'messageCreated',
MessageRemoved = 'messageRemoved',
MessagesRemoved = 'messagesRemoved',
PatchCreated = 'patchCreated',
ReactionCreated = 'reactionCreated',
ReactionRemoved = 'reactionRemoved',
AttachmentCreated = 'attachmentCreated',
AttachmentRemoved = 'attachmentRemoved',
ThreadCreated = 'threadCreated',
NotificationCreated = 'notificationCreated',
NotificationRemoved = 'notificationRemoved',
NotificationContextCreated = 'notificationContextCreated',
@@ -31,7 +32,6 @@ export enum ResponseEventType {
export type ResponseEvent =
| MessageCreatedEvent
| MessageRemovedEvent
| MessagesRemovedEvent
| PatchCreatedEvent
| ReactionCreatedEvent
| ReactionRemovedEvent
@@ -42,6 +42,7 @@ export type ResponseEvent =
| NotificationContextCreatedEvent
| NotificationContextRemovedEvent
| NotificationContextUpdatedEvent
| ThreadCreatedEvent
export interface MessageCreatedEvent {
type: ResponseEventType.MessageCreated
@@ -54,12 +55,6 @@ export interface MessageRemovedEvent {
message: MessageID
}
export interface MessagesRemovedEvent {
type: ResponseEventType.MessagesRemoved
card: CardID
messages: MessageID[]
}
export interface PatchCreatedEvent {
type: ResponseEventType.PatchCreated
card: CardID
@@ -93,6 +88,11 @@ export interface AttachmentRemovedEvent {
attachment: CardID
}
export interface ThreadCreatedEvent {
type: ResponseEventType.ThreadCreated
thread: Thread
}
export interface NotificationCreatedEvent {
type: ResponseEventType.NotificationCreated
personalWorkspace: string
+1
View File
@@ -13,6 +13,7 @@ export interface ConnectionInfo {
sessionId: string
personalWorkspace: WorkspaceID
socialIds: SocialID[]
isSystem: boolean
}
export interface ServerApi {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-server",
"version": "0.1.9",
"version": "0.1.47",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
+132 -96
View File
@@ -4,7 +4,8 @@ import {
type Reaction,
type Attachment,
type SocialID,
type WorkspaceID
type WorkspaceID,
PatchType
} from '@hcengineering/communication-types'
import {
type CreateAttachmentEvent,
@@ -38,62 +39,65 @@ import {
ResponseEventType,
type CreateMessagesGroupEvent,
type RemoveMessagesEvent,
type MessagesRemovedEvent
type ThreadCreatedEvent,
type CreateThreadEvent,
type ConnectionInfo,
type RemovePatchesEvent,
type RemoveMessagesGroupEvent
} from '@hcengineering/communication-sdk-types'
export type Result = {
export interface Result {
responseEvent?: ResponseEvent
result: EventResult
}
export type UserInfo = {
personalWorkspace: WorkspaceID
socialIds: SocialID[]
}
export class EventProcessor {
constructor(
private readonly db: DbAdapter,
private readonly workspace: WorkspaceID
) {}
async process(user: UserInfo, event: RequestEvent): Promise<Result> {
async process(info: ConnectionInfo, event: RequestEvent): Promise<Result> {
switch (event.type) {
case RequestEventType.CreateMessage:
return await this.createMessage(event, user)
return await this.createMessage(event, info)
case RequestEventType.RemoveMessage:
return await this.removeMessage(event, user)
return await this.removeMessage(event, info)
case RequestEventType.RemoveMessages:
return await this.removeMessages(event, user)
return await this.removeMessages(event, info)
case RequestEventType.CreatePatch:
return await this.createPatch(event, user)
return await this.createPatch(event, info)
case RequestEventType.CreateReaction:
return await this.createReaction(event, user)
return await this.createReaction(event, info)
case RequestEventType.RemoveReaction:
return await this.removeReaction(event, user)
return await this.removeReaction(event, info)
case RequestEventType.CreateAttachment:
return await this.createAttachment(event, user)
return await this.createAttachment(event, info)
case RequestEventType.RemoveAttachment:
return await this.removeAttachment(event, user)
return await this.removeAttachment(event, info)
case RequestEventType.CreateNotification:
return await this.createNotification(event, user)
return await this.createNotification(event, info)
case RequestEventType.RemoveNotification:
return await this.removeNotification(event, user)
return await this.removeNotification(event, info)
case RequestEventType.CreateNotificationContext:
return await this.createNotificationContext(event, user)
return await this.createNotificationContext(event, info)
case RequestEventType.RemoveNotificationContext:
return await this.removeNotificationContext(event, user)
return await this.removeNotificationContext(event, info)
case RequestEventType.UpdateNotificationContext:
return await this.updateNotificationContext(event, user)
return await this.updateNotificationContext(event, info)
case RequestEventType.CreateMessagesGroup:
return await this.createMessagesGroup(event, user)
return await this.createMessagesGroup(event, info)
case RequestEventType.CreateThread:
return await this.createThread(event, info)
case RequestEventType.RemovePatches:
return await this.removePatches(event, info)
case RequestEventType.RemoveMessagesGroup:
return await this.removeMessagesGroup(event, info)
}
}
private async createMessage(event: CreateMessageEvent, user: UserInfo): Promise<Result> {
if (!user.socialIds.includes(event.creator)) {
throw new Error('Forbidden')
}
private async createMessage(event: CreateMessageEvent, info: ConnectionInfo): Promise<Result> {
this.checkCreator(info, event.creator)
const created = new Date()
const id = await this.db.createMessage(event.card, event.content, event.creator, created)
@@ -102,8 +106,7 @@ export class EventProcessor {
card: event.card,
content: event.content,
creator: event.creator,
created: created,
edited: created,
created,
reactions: [],
attachments: []
}
@@ -117,18 +120,17 @@ export class EventProcessor {
}
}
private async createPatch(event: CreatePatchEvent, user: UserInfo): Promise<Result> {
if (!user.socialIds.includes(event.creator)) {
throw new Error('Forbidden')
}
private async createPatch(event: CreatePatchEvent, info: ConnectionInfo): Promise<Result> {
this.checkCreator(info, event.creator)
const created = new Date()
await this.db.createPatch(event.card, event.message, event.content, event.creator, created)
await this.db.createPatch(event.card, event.message, PatchType.update, event.content, event.creator, created)
const patch: Patch = {
type: PatchType.update,
message: event.message,
content: event.content,
creator: event.creator,
created: created
created
}
const responseEvent: PatchCreatedEvent = {
type: ResponseEventType.PatchCreated,
@@ -141,16 +143,9 @@ export class EventProcessor {
}
}
//eslint-disable-next-line @typescript-eslint/no-unused-vars
private async removeMessage(event: RemoveMessageEvent, _: UserInfo): Promise<Result> {
const res = await this.db.removeMessage(event.card, event.message)
if (res === undefined) {
return {
responseEvent: undefined,
result: { id: res }
}
}
private async removeMessage(event: RemoveMessageEvent, info: ConnectionInfo): Promise<Result> {
const socialIds = info.isSystem ? undefined : info.socialIds
await this.db.removeMessage(event.card, event.message, socialIds)
const responseEvent: MessageRemovedEvent = {
type: ResponseEventType.MessageRemoved,
@@ -160,37 +155,34 @@ export class EventProcessor {
return {
responseEvent,
result: { id: res }
result: {}
}
}
//eslint-disable-next-line @typescript-eslint/no-unused-vars
private async removeMessages(event: RemoveMessagesEvent, _: UserInfo): Promise<Result> {
const ids = await this.db.removeMessages(event.card, event.messages)
if (event.silent === true) {
return {
responseEvent: undefined,
result: { ids }
}
}
const responseEvent: MessagesRemovedEvent = {
type: ResponseEventType.MessagesRemoved,
card: event.card,
messages: ids
}
return {
responseEvent,
result: { ids }
}
}
private async createReaction(event: CreateReactionEvent, user: UserInfo): Promise<Result> {
if (!user.socialIds.includes(event.creator)) {
private async removeMessages(event: RemoveMessagesEvent, info: ConnectionInfo): Promise<Result> {
if (!info.isSystem) {
throw new Error('Forbidden')
}
await this.db.removeMessages(event.card, event.fromId, event.toId)
return {
result: {}
}
}
private async removePatches(event: RemovePatchesEvent, info: ConnectionInfo): Promise<Result> {
if (!info.isSystem) {
throw new Error('Forbidden')
}
await this.db.removePatches(event.card, event.fromId, event.toId)
return {
result: {}
}
}
private async createReaction(event: CreateReactionEvent, info: ConnectionInfo): Promise<Result> {
this.checkCreator(info, event.creator)
const created = new Date()
await this.db.createReaction(event.card, event.message, event.reaction, event.creator, created)
@@ -198,7 +190,7 @@ export class EventProcessor {
message: event.message,
reaction: event.reaction,
creator: event.creator,
created: created
created
}
const responseEvent: ReactionCreatedEvent = {
type: ResponseEventType.ReactionCreated,
@@ -211,10 +203,8 @@ export class EventProcessor {
}
}
private async removeReaction(event: RemoveReactionEvent, user: UserInfo): Promise<Result> {
if (!user.socialIds.includes(event.creator)) {
throw new Error('Forbidden')
}
private async removeReaction(event: RemoveReactionEvent, info: ConnectionInfo): Promise<Result> {
this.checkCreator(info, event.creator)
await this.db.removeReaction(event.card, event.message, event.reaction, event.creator)
const responseEvent: ReactionRemovedEvent = {
type: ResponseEventType.ReactionRemoved,
@@ -229,8 +219,8 @@ export class EventProcessor {
}
}
//eslint-disable-next-line @typescript-eslint/no-unused-vars
private async createAttachment(event: CreateAttachmentEvent, _: UserInfo): Promise<Result> {
private async createAttachment(event: CreateAttachmentEvent, info: ConnectionInfo): Promise<Result> {
this.checkCreator(info, event.creator)
const created = new Date()
await this.db.createAttachment(event.message, event.card, event.creator, created)
@@ -238,7 +228,7 @@ export class EventProcessor {
message: event.message,
card: event.card,
creator: event.creator,
created: created
created
}
const responseEvent: AttachmentCreatedEvent = {
type: ResponseEventType.AttachmentCreated,
@@ -252,8 +242,8 @@ export class EventProcessor {
}
}
//eslint-disable-next-line @typescript-eslint/no-unused-vars
private async removeAttachment(event: RemoveAttachmentEvent, _: UserInfo): Promise<Result> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async removeAttachment(event: RemoveAttachmentEvent, _: ConnectionInfo): Promise<Result> {
await this.db.removeAttachment(event.message, event.card)
const responseEvent: AttachmentRemovedEvent = {
type: ResponseEventType.AttachmentRemoved,
@@ -267,8 +257,8 @@ export class EventProcessor {
}
}
//eslint-disable-next-line @typescript-eslint/no-unused-vars
private async createNotification(event: CreateNotificationEvent, _: UserInfo): Promise<Result> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async createNotification(event: CreateNotificationEvent, _: ConnectionInfo): Promise<Result> {
await this.db.createNotification(event.message, event.context)
return {
@@ -276,12 +266,12 @@ export class EventProcessor {
}
}
private async removeNotification(event: RemoveNotificationEvent, user: UserInfo): Promise<Result> {
private async removeNotification(event: RemoveNotificationEvent, info: ConnectionInfo): Promise<Result> {
await this.db.removeNotification(event.message, event.context)
const responseEvent: NotificationRemovedEvent = {
type: ResponseEventType.NotificationRemoved,
personalWorkspace: user.personalWorkspace,
personalWorkspace: info.personalWorkspace,
message: event.message,
context: event.context
}
@@ -291,14 +281,17 @@ export class EventProcessor {
}
}
private async createNotificationContext(event: CreateNotificationContextEvent, user: UserInfo): Promise<Result> {
const id = await this.db.createContext(user.personalWorkspace, event.card, event.lastView, event.lastUpdate)
private async createNotificationContext(
event: CreateNotificationContextEvent,
info: ConnectionInfo
): Promise<Result> {
const id = await this.db.createContext(info.personalWorkspace, event.card, event.lastView, event.lastUpdate)
const responseEvent: NotificationContextCreatedEvent = {
type: ResponseEventType.NotificationContextCreated,
context: {
id,
workspace: this.workspace,
personalWorkspace: user.personalWorkspace,
personalWorkspace: info.personalWorkspace,
card: event.card,
lastView: event.lastView,
lastUpdate: event.lastUpdate
@@ -310,11 +303,14 @@ export class EventProcessor {
}
}
private async removeNotificationContext(event: RemoveNotificationContextEvent, user: UserInfo): Promise<Result> {
private async removeNotificationContext(
event: RemoveNotificationContextEvent,
info: ConnectionInfo
): Promise<Result> {
await this.db.removeContext(event.context)
const responseEvent: NotificationContextRemovedEvent = {
type: ResponseEventType.NotificationContextRemoved,
personalWorkspace: user.personalWorkspace,
personalWorkspace: info.personalWorkspace,
context: event.context
}
return {
@@ -323,12 +319,12 @@ export class EventProcessor {
}
}
async updateNotificationContext(event: UpdateNotificationContextEvent, user: UserInfo): Promise<Result> {
async updateNotificationContext(event: UpdateNotificationContextEvent, info: ConnectionInfo): Promise<Result> {
await this.db.updateContext(event.context, event.update)
const responseEvent: NotificationContextUpdatedEvent = {
type: ResponseEventType.NotificationContextUpdated,
personalWorkspace: user.personalWorkspace,
personalWorkspace: info.personalWorkspace,
context: event.context,
update: event.update
}
@@ -338,14 +334,54 @@ export class EventProcessor {
}
}
//eslint-disable-next-line @typescript-eslint/no-unused-vars
async createMessagesGroup(event: CreateMessagesGroupEvent, _: UserInfo): Promise<Result> {
const { fromId, toId, fromDate, toDate, count } = event.group
await this.db.createMessagesGroup(event.group.card, event.group.blobId, fromId, toId, fromDate, toDate, count)
async createMessagesGroup(event: CreateMessagesGroupEvent, info: ConnectionInfo): Promise<Result> {
if (!info.isSystem) {
throw new Error('Forbidden')
}
const { fromDate, toDate, count, fromId, toId, card, blobId } = event.group
await this.db.createMessagesGroup(card, blobId, fromDate, toDate, fromId, toId, count)
return {
responseEvent: undefined,
result: {}
}
}
async removeMessagesGroup(event: RemoveMessagesGroupEvent, info: ConnectionInfo): Promise<Result> {
if (!info.isSystem) {
throw new Error('Forbidden')
}
await this.db.removeMessagesGroup(event.card, event.blobId)
return {
responseEvent: undefined,
result: {}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async createThread(event: CreateThreadEvent, _: ConnectionInfo): Promise<Result> {
const date = new Date()
await this.db.createThread(event.card, event.message, event.thread, date)
const responseEvent: ThreadCreatedEvent = {
type: ResponseEventType.ThreadCreated,
thread: {
card: event.card,
thread: event.thread,
message: event.message,
repliesCount: 0,
lastReply: date
}
}
return {
responseEvent,
result: {}
}
}
private checkCreator(info: ConnectionInfo, creator: SocialID): void {
if (!info.socialIds.includes(creator) && !info.isSystem) {
throw new Error('Forbidden')
}
}
}
+3 -3
View File
@@ -15,7 +15,7 @@ import type {
ServerApi
} from '@hcengineering/communication-sdk-types'
import { Manager, type BroadcastSessionsFunc } from './manager.ts'
import { Manager, type BroadcastSessionsFunc } from './manager'
export class Api implements ServerApi {
private readonly manager: Manager
@@ -26,7 +26,7 @@ export class Api implements ServerApi {
private readonly db: DbAdapter,
private readonly broadcast: BroadcastSessionsFunc
) {
this.manager = new Manager(this.ctx, db, this.workspace, broadcast)
this.manager = new Manager(this.ctx, this.db, this.workspace, this.broadcast)
}
static async create(
@@ -35,7 +35,7 @@ export class Api implements ServerApi {
dbUrl: string,
broadcast: BroadcastSessionsFunc
): Promise<Api> {
const db = await createDbAdapter(dbUrl, workspace)
const db = await createDbAdapter(dbUrl, workspace, ctx, { withLogs: true })
return new Api(ctx, workspace, db, broadcast)
}
+20 -18
View File
@@ -18,17 +18,17 @@ import type {
MessagesGroup,
WorkspaceID
} from '@hcengineering/communication-types'
import { Triggers } from './triggers.ts'
import { EventProcessor } from './eventProcessor.ts'
import type { MeasureContext } from '@hcengineering/core'
import { Triggers } from './triggers'
import { EventProcessor } from './eventProcessor'
type QueryId = number | string
type QueryType = 'message' | 'notification' | 'context'
export type BroadcastSessionsFunc = (ctx: MeasureContext, sessionIds: string[], result: any) => void
type SessionInfo = {
interface SessionInfo {
personalWorkspace: string
messageQueries: Map<QueryId, FindMessagesParams>
notificationQueries: Map<QueryId, FindNotificationsParams>
@@ -36,8 +36,8 @@ type SessionInfo = {
}
export class Manager {
private dataBySessionId: Map<string, SessionInfo> = new Map()
private triggers: Triggers
private readonly dataBySessionId = new Map<string, SessionInfo>()
private readonly triggers: Triggers
private readonly eventProcessor: EventProcessor
constructor(
@@ -58,17 +58,16 @@ export class Manager {
return result
}
async findMessagesGroups(info: ConnectionInfo, params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
async findMessagesGroups(_: ConnectionInfo, params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
return await this.db.findMessagesGroups(params)
}
async event(info: ConnectionInfo, event: RequestEvent): Promise<EventResult> {
const { result, responseEvent } = await this.eventProcessor.process(
{ personalWorkspace: info.personalWorkspace, socialIds: info.socialIds },
event
)
const eventResult = await this.eventProcessor.process(info, event)
const { result, responseEvent } = eventResult
if (responseEvent !== undefined) {
void this.next(responseEvent)
void this.next(info, responseEvent)
}
return result
}
@@ -86,7 +85,7 @@ export class Manager {
}
if (type === 'message') {
data.messageQueries.set(queryId, params)
data.messageQueries.set(queryId, params as FindMessagesParams)
} else if (type === 'notification') {
data.notificationQueries.set(queryId, params)
} else if (type === 'context') {
@@ -107,10 +106,10 @@ export class Manager {
this.dataBySessionId.delete(sessionId)
}
async next(event: ResponseEvent): Promise<void> {
async next(info: ConnectionInfo, event: ResponseEvent): Promise<void> {
await this.responseEvent(event)
const derived = await this.triggers.process(event)
await Promise.all(derived.map((it) => this.next(it)))
const derived = await this.triggers.process(event, info)
await Promise.all(derived.map((it) => this.next(info, it)))
}
private async responseEvent(event: ResponseEvent): Promise<void> {
@@ -143,8 +142,6 @@ export class Manager {
{ card: event.card, id: event.message },
Array.from(info.messageQueries.values())
)
case ResponseEventType.MessagesRemoved:
return this.matchMessagesQuery({ card: event.card }, Array.from(info.messageQueries.values()))
case ResponseEventType.ReactionCreated:
return this.matchMessagesQuery(
{ card: event.card, id: event.reaction.message },
@@ -165,6 +162,11 @@ export class Manager {
{ card: event.card, id: event.message },
Array.from(info.messageQueries.values())
)
case ResponseEventType.ThreadCreated:
return this.matchMessagesQuery(
{ card: event.thread.card, id: event.thread.message },
Array.from(info.messageQueries.values())
)
case ResponseEventType.NotificationCreated:
return (
info.personalWorkspace === event.personalWorkspace &&
+50 -88
View File
@@ -1,12 +1,12 @@
import {
ResponseEventType,
type DbAdapter,
type MessageCreatedEvent,
type NotificationContextCreatedEvent,
type NotificationCreatedEvent,
type ResponseEvent
type DbAdapter,
type ResponseEvent,
ResponseEventType,
type MessageRemovedEvent,
type ConnectionInfo
} from '@hcengineering/communication-sdk-types'
import type { NotificationContext, ContextID, CardID, WorkspaceID } from '@hcengineering/communication-types'
import { type WorkspaceID, PatchType, type Patch } from '@hcengineering/communication-types'
export class Triggers {
constructor(
@@ -14,103 +14,65 @@ export class Triggers {
private readonly workspace: WorkspaceID
) {}
async process(event: ResponseEvent): Promise<ResponseEvent[]> {
async process(event: ResponseEvent, info: ConnectionInfo): Promise<ResponseEvent[]> {
switch (event.type) {
case ResponseEventType.MessageCreated:
return this.createNotifications(event)
return await this.onMessageCreated(event)
case ResponseEventType.MessageRemoved:
return await this.onMessageRemoved(event, info)
}
return []
}
private async createNotifications(event: MessageCreatedEvent): Promise<ResponseEvent[]> {
const card = event.message.card as any as CardID
const subscribedPersonalWorkspaces = [
'cd0aba36-1c4f-4170-95f2-27a12a5415f7',
'cd0aba36-1c4f-4170-95f2-27a12a5415f8'
] as WorkspaceID[]
async onMessageRemoved(event: MessageRemovedEvent, info: ConnectionInfo): Promise<ResponseEvent[]> {
const { card } = event
const thread = await this.db.findThread(card)
if (thread === undefined) return []
const res: ResponseEvent[] = []
const contexts = await this.db.findContexts({ card }, [], this.workspace)
const date = new Date()
const socialId = info.socialIds[0]
res.push(...(await this.updateNotificationContexts(event.message.created, contexts)))
for (const personalWorkspace of subscribedPersonalWorkspaces) {
const existsContext = contexts.find(
(it) => it.card === card && it.personalWorkspace === personalWorkspace && this.workspace === it.workspace
)
const contextId = await this.getOrCreateContextId(
personalWorkspace,
card,
res,
event.message.created,
existsContext
)
await this.db.createNotification(event.message.id, contextId)
const resultEvent: NotificationCreatedEvent = {
type: ResponseEventType.NotificationCreated,
personalWorkspace,
notification: {
context: contextId,
message: event.message,
read: false,
archived: false
}
}
res.push(resultEvent)
const patch: Patch = {
message: thread.message,
type: PatchType.removeReply,
content: thread.thread,
creator: socialId,
created: date
}
await this.db.updateThread(thread.thread, date, 'decrement')
await this.db.createPatch(thread.card, patch.message, patch.type, patch.content, patch.creator, patch.created)
return res
return [
{
type: ResponseEventType.PatchCreated,
card: thread.card,
patch
}
]
}
private async getOrCreateContextId(
personalWorkspace: WorkspaceID,
card: CardID,
res: ResponseEvent[],
lastUpdate: Date,
context?: NotificationContext
): Promise<ContextID> {
if (context !== undefined) {
return context.id
} else {
const contextId = await this.db.createContext(personalWorkspace, card, undefined, lastUpdate)
const newContext = {
id: contextId,
card,
workspace: this.workspace,
personalWorkspace
}
const resultEvent: NotificationContextCreatedEvent = {
type: ResponseEventType.NotificationContextCreated,
context: newContext
}
async onMessageCreated(event: MessageCreatedEvent): Promise<ResponseEvent[]> {
const { message } = event
const thread = await this.db.findThread(message.card)
if (thread === undefined) return []
res.push(resultEvent)
return contextId
const date = new Date()
const patch: Patch = {
message: thread.message,
type: PatchType.addReply,
content: thread.thread,
creator: message.creator,
created: date
}
}
await this.db.updateThread(thread.thread, date, 'increment')
await this.db.createPatch(thread.card, patch.message, patch.type, patch.content, patch.creator, patch.created)
private async updateNotificationContexts(
lastUpdate: Date,
contexts: NotificationContext[]
): Promise<ResponseEvent[]> {
const res: ResponseEvent[] = []
for (const context of contexts) {
if (context.lastUpdate === undefined || context.lastUpdate < lastUpdate) {
await this.db.updateContext(context.id, { lastUpdate })
res.push({
type: ResponseEventType.NotificationContextUpdated,
personalWorkspace: context.personalWorkspace,
context: context.id,
update: {
lastUpdate
}
})
return [
{
type: ResponseEventType.PatchCreated,
card: thread.card,
patch
}
}
return res
]
}
}
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-core",
"version": "0.1.9",
"name": "@hcengineering/communication-shared",
"version": "0.1.47",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
@@ -10,13 +10,15 @@
"types/**/*.d.ts"
],
"devDependencies": {
"@types/bun": "^1.1.14"
"@types/bun": "^1.1.14",
"@types/js-yaml": "^4.0.9"
},
"peerDependencies": {
"typescript": "^5.6.3"
},
"dependencies": {
"@hcengineering/communication-types": "workspace:*"
"@hcengineering/communication-types": "workspace:*",
"js-yaml": "^4.1.0"
},
"repository": {
"type": "git",
+58
View File
@@ -0,0 +1,58 @@
import type {
FileMessage,
FileMetadata,
MessagesGroup,
ParsedFile,
WorkspaceID
} from '@hcengineering/communication-types'
import yaml from 'js-yaml'
import { retry, type RetryOptions } from './retry'
export async function loadGroupFile(
workspace: WorkspaceID,
filesUrl: string,
group: MessagesGroup,
options: RetryOptions
): Promise<ParsedFile> {
const url = getFileUrl(workspace, filesUrl, group.blobId)
const file = await retry(() => fetchFile(url), options)
const [metadata, messages] = yaml.loadAll(file) as [FileMetadata, FileMessage[]]
return {
metadata,
messages: messages.map((message) => ({
id: message.id,
card: metadata.card,
content: message.content,
edited: message.edited,
creator: message.creator,
created: message.created,
attachments: [],
reactions: message.reactions.map((reaction) => ({
message: message.id,
reaction: reaction.reaction,
creator: reaction.creator,
created: reaction.created
}))
}))
}
}
async function fetchFile(url: string): Promise<string> {
const res = await fetch(url)
if (!res.ok) {
throw new Error(`Failed to fetch file: ${res.statusText}`)
}
return await res.text()
}
function getFileUrl(workspace: WorkspaceID, urlTemplate: string, file: string): string {
return urlTemplate
.replaceAll(':filename', encodeURIComponent(file))
.replaceAll(':workspace', encodeURIComponent(workspace))
.replaceAll(':blobId', encodeURIComponent(file))
}
+3
View File
@@ -0,0 +1,3 @@
export * from './message.ts'
export * from './retry.ts'
export * from './files.ts'
+24
View File
@@ -0,0 +1,24 @@
import type { MessageID } from '@hcengineering/communication-types'
let lastTimestamp = 0
let counter = 0n
export function generateMessageId(): MessageID {
const timestamp = Math.floor(Date.now() / 1000)
if (timestamp !== lastTimestamp) {
lastTimestamp = timestamp
counter = 0n
}
const id = (BigInt(timestamp) << 20n) | counter
counter++
return id.toString() as MessageID
}
export function parseMessageId(messageId: MessageID): Date {
const timestamp = Number(BigInt(messageId) >> 20n)
return new Date(timestamp * 1000)
}
+20
View File
@@ -0,0 +1,20 @@
export interface RetryOptions {
retries: number
delay?: number
}
export async function retry<T>(op: () => Promise<T>, { retries, delay }: RetryOptions): Promise<T> {
let error: any
while (retries > 0) {
retries--
try {
return await op()
} catch (err: any) {
error = err
if (retries !== 0 && delay !== undefined && delay > 0) {
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
}
throw error
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-sqlite-wasm",
"version": "0.1.9",
"version": "0.1.47",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
-2
View File
@@ -73,8 +73,6 @@ export class SqliteAdapter {
workspace: string,
card: CardID,
blobId: BlobID,
from_id: MessageID,
to_id: MessageID,
from_date: Date,
to_date: Date,
count: number
+17 -16
View File
@@ -3,10 +3,10 @@ import {
type MessageID,
type CardID,
type FindMessagesParams,
SortOrder,
SortingOrder,
type SocialID,
type RichText,
Direction, type Reaction, type Attachment
type Reaction, type Attachment
} from '@hcengineering/communication-types'
import {BaseDb} from './base.ts'
@@ -124,7 +124,7 @@ export class MessagesDb extends BaseDb {
const where = this.buildMessageWhere(workspace, params)
const groupBy = `GROUP BY m.id`
const orderBy = params.sort ? `ORDER BY m.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` : ''
const orderBy = params.order ? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
const sql = [select, where, groupBy, orderBy, limit].join(' ')
@@ -143,19 +143,20 @@ export class MessagesDb extends BaseDb {
where.push(`m.id = '${params.id}'`)
}
if (params.from != null) {
const exclude = params.excluded ?? false
const direction = params.direction ?? Direction.Forward
const getOperator = () => {
if (exclude) {
return direction === Direction.Forward ? '>' : '<'
} else {
return direction === Direction.Forward ? '>=' : '<='
}
}
where.push(`m.created ${getOperator()} ${params.from}`)
}
//TODO: FIX ME
// if (params.from != null) {
// const exclude = params.excluded ?? false
// const direction = params.direction ?? Direction.Forward
// const getOperator = () => {
// if (exclude) {
// return direction === Direction.Forward ? '>' : '<'
// } else {
// return direction === Direction.Forward ? '>=' : '<='
// }
// }
//
// where.push(`m.created ${getOperator()} ${params.from}`)
// }
return `WHERE ${where.join(' AND ')}`
}
+2 -2
View File
@@ -3,7 +3,7 @@ import {
type ContextID,
type CardID,
type NotificationContext,
type FindNotificationContextParams, SortOrder,
type FindNotificationContextParams, SortingOrder,
type FindNotificationsParams, type Notification,
type NotificationContextUpdate
} from '@hcengineering/communication-types'
@@ -130,7 +130,7 @@ export class NotificationsDb extends BaseDb {
`;
const where = this.buildNotificationWhere(params, personalWorkspace, workspace)
const groupBy = `GROUP BY n.message_id, n.context_id, m.id, nc.card_id, nc.archived_from, nc.last_view, nc.last_update`;
const orderBy = `ORDER BY m.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}`
const orderBy = `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}`
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
const sql = [select, where, groupBy, orderBy, limit].join(' ')
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-types",
"version": "0.1.9",
"version": "0.1.47",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
+36
View File
@@ -0,0 +1,36 @@
import type { CardID, Message, MessageID, RichText, SocialID } from './message'
export interface FileMetadata {
card: CardID
title: string
fromDate: Date
toDate: Date
}
export interface FileMessage {
id: MessageID
content: RichText
edited?: Date
creator: SocialID
created: Date
reactions: FileReaction[]
thread?: FileThread
}
export interface FileReaction {
reaction: string
creator: SocialID
created: Date
}
export interface FileThread {
thread: CardID
repliesCount: number
lastReply: Date
replied: SocialID[]
}
export interface ParsedFile {
metadata: FileMetadata
messages: Message[]
}
+1
View File
@@ -1,3 +1,4 @@
export * from './message.ts'
export * from './notification.ts'
export * from './query.ts'
export * from './file.ts'
+35 -12
View File
@@ -7,19 +7,18 @@ export type SocialID = PersonId
export type WorkspaceID = WorkspaceUuid
export type RichText = string
export type ID = string | number
export type MessageID = number & { message: true }
export type ID = string
export type MessageID = string & { message: true }
interface Object {
creator: SocialID
created: Date
}
export interface Message extends Object {
export interface Message {
id: MessageID
card: CardID
content: RichText
creator: SocialID
created: Date
edited?: Date
thread?: Thread
reactions: Reaction[]
attachments: Attachment[]
}
@@ -32,19 +31,43 @@ export interface MessagesGroup {
fromDate: Date
toDate: Date
count: number
patches: Patch[]
}
export interface Patch extends Object {
export interface Patch {
message: MessageID
content: RichText
type: PatchType
content: string
creator: SocialID
created: Date
}
export interface Reaction extends Object {
export enum PatchType {
update = 'update',
addReaction = 'addReaction',
removeReaction = 'removeReaction',
addReply = 'addReply',
removeReply = 'removeReply'
}
export interface Reaction {
message: MessageID
reaction: string
creator: SocialID
created: Date
}
export interface Attachment extends Object {
export interface Attachment {
message: MessageID
card: CardID
creator: SocialID
created: Date
}
export interface Thread {
card: CardID
message: MessageID
thread: CardID
repliesCount: number
lastReply: Date
}
+16 -27
View File
@@ -1,15 +1,15 @@
import { SortingOrder } from '@hcengineering/core'
import type { BlobID, CardID, MessageID } from './message'
import type { ContextID } from './notification'
export enum SortOrder {
Asc = 1,
Desc = -1
}
export { SortingOrder }
export enum Direction {
Backward = 1,
Forward = -1
}
export type ComparisonOperator = 'less' | 'lessOrEqual' | 'greater' | 'greaterOrEqual' | 'notEqual'
type Exclusive<T> = {
[K in keyof T]: Record<K, T[K]> & Partial<Record<Exclude<keyof T, K>, never>>
}[keyof T]
export interface Window<T> {
getResult(): T[]
@@ -22,16 +22,14 @@ export interface Window<T> {
}
interface FindParams {
from?: Date
excluded?: boolean
direction?: Direction
sort?: SortOrder
order?: SortingOrder
limit?: number
}
export interface FindMessagesParams extends FindParams {
id?: MessageID
card?: CardID
card: CardID
created?: Exclusive<Record<ComparisonOperator, Date>> | Date
}
export interface FindNotificationsParams extends FindParams {
@@ -39,6 +37,7 @@ export interface FindNotificationsParams extends FindParams {
message?: MessageID
read?: boolean
archived?: boolean
created?: Exclusive<Record<ComparisonOperator, Date>> | Date
}
export interface FindNotificationContextParams extends FindParams {
@@ -46,20 +45,10 @@ export interface FindNotificationContextParams extends FindParams {
card?: CardID
}
export type ComparisonOperator = 'less' | 'lessOrEqual' | 'greater' | 'greaterOrEqual' | 'notEqual'
type Exclusive<T> = {
[K in keyof T]: Record<K, T[K]> & Partial<Record<Exclude<keyof T, K>, never>>
}[keyof T]
export interface FindMessagesGroupsParams {
export interface FindMessagesGroupsParams extends FindParams {
card?: CardID
blobId?: BlobID
fromId?: Exclusive<Record<ComparisonOperator, MessageID>> | MessageID
toId?: Exclusive<Record<ComparisonOperator, MessageID>> | MessageID
fromDate?: Exclusive<Record<ComparisonOperator, Date>> | Date
toDate?: Exclusive<Record<ComparisonOperator, Date>> | Date
limit?: number
sortBy?: 'fromId' | 'toId' | 'fromDate' | 'toDate'
sort?: SortOrder
fromDate?: Partial<Record<ComparisonOperator, Date>> | Date
toDate?: Partial<Record<ComparisonOperator, Date>> | Date
orderBy?: 'fromDate' | 'toDate'
}