Notifications, files, queries, rest-client (#36)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina
2025-03-24 10:03:47 +04:00
committed by GitHub
parent c891025321
commit bb2edae113
89 changed files with 4610 additions and 4191 deletions
+111 -134
View File
@@ -4,19 +4,19 @@
"": {
"name": "@hcengineering/communication",
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/bun": "^1.2.4",
"bun-types": "^1.2.4",
"eslint": "^9.21.0",
"@eslint/js": "^9.23.0",
"@types/bun": "^1.2.5",
"bun-types": "^1.2.5",
"eslint": "^9.23.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-prettier": "^5.2.4",
"prettier": "^3.5.3",
"typescript-eslint": "^8.25.0",
"typescript-eslint": "^8.27.0",
},
},
"packages/client-query": {
"name": "@hcengineering/communication-client-query",
"version": "0.1.61",
"version": "0.1.147",
"dependencies": {
"@hcengineering/communication-query": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
@@ -30,41 +30,9 @@
"typescript": "^5.6.3",
},
},
"packages/client-sqlite": {
"name": "@hcengineering/communication-client-sqlite",
"version": "0.1.61",
"dependencies": {
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-sqlite-wasm": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
"fast-equals": "^5.0.1",
},
"devDependencies": {
"@types/bun": "^1.1.14",
},
"peerDependencies": {
"typescript": "^5.6.3",
},
},
"packages/client-ws": {
"name": "@hcengineering/communication-client-ws",
"version": "0.1.61",
"dependencies": {
"@hcengineering/communication-client-query": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
"@msgpack/msgpack": "^3.0.0-beta2",
},
"devDependencies": {
"@types/bun": "^1.1.14",
},
"peerDependencies": {
"typescript": "^5.6.3",
},
},
"packages/cockroach": {
"name": "@hcengineering/communication-cockroach",
"version": "0.1.61",
"version": "0.1.147",
"dependencies": {
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-shared": "workspace:*",
@@ -79,27 +47,14 @@
"typescript": "^5.6.3",
},
},
"packages/examples": {
"name": "@hcengineering/communication-examples",
"version": "0.1.61",
"dependencies": {
"@hcengineering/communication-client-ws": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
},
"devDependencies": {
"@types/bun": "^1.1.14",
},
"peerDependencies": {
"typescript": "^5.6.3",
},
},
"packages/query": {
"name": "@hcengineering/communication-query",
"version": "0.1.61",
"version": "0.1.147",
"dependencies": {
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-shared": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/communication-yaml": "workspace:*",
"fast-equals": "^5.0.1",
},
"devDependencies": {
@@ -110,12 +65,30 @@
"typescript": "^5.6.3",
},
},
"packages/rest-client": {
"name": "@hcengineering/communication-rest-client",
"version": "0.1.147",
"dependencies": {
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-shared": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/core": "^0.7.28",
"snappyjs": "^0.7.0",
},
"devDependencies": {
"@types/bun": "^1.1.14",
"@types/snappyjs": "^0.7.1",
},
"peerDependencies": {
"typescript": "^5.6.3",
},
},
"packages/sdk-types": {
"name": "@hcengineering/communication-sdk-types",
"version": "0.1.61",
"version": "0.1.147",
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/core": "0.7.15",
"@hcengineering/core": "^0.7.28",
},
"devDependencies": {
"@types/bun": "^1.1.14",
@@ -126,13 +99,14 @@
},
"packages/server": {
"name": "@hcengineering/communication-server",
"version": "0.1.61",
"version": "0.1.147",
"dependencies": {
"@hcengineering/account-client": "^0.7.28",
"@hcengineering/communication-cockroach": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/core": "0.7.15",
"@hcengineering/server-token": "0.7.15",
"@hcengineering/core": "^0.7.28",
"@hcengineering/server-token": "^0.7.28",
},
"devDependencies": {
"@types/bun": "^1.1.14",
@@ -146,28 +120,9 @@
},
"packages/shared": {
"name": "@hcengineering/communication-shared",
"version": "0.1.61",
"version": "0.1.147",
"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.61",
"dependencies": {
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
"@sqlite.org/sqlite-wasm": "^3.47.1-build1",
"path": "^0.12.7",
"uuid": "^11.0.3",
},
"devDependencies": {
"@types/bun": "^1.1.14",
@@ -178,10 +133,10 @@
},
"packages/types": {
"name": "@hcengineering/communication-types",
"version": "0.1.61",
"version": "0.1.147",
"dependencies": {
"@hcengineering/card": "0.7.15",
"@hcengineering/core": "0.7.15",
"@hcengineering/card": "^0.7.28",
"@hcengineering/core": "^0.7.28",
},
"devDependencies": {
"@types/bun": "^1.1.14",
@@ -190,6 +145,22 @@
"typescript": "^5.6.3",
},
},
"packages/yaml": {
"name": "@hcengineering/communication-yaml",
"version": "0.1.147",
"dependencies": {
"@hcengineering/communication-shared": "workspace:*",
"@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": {
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
@@ -202,11 +173,13 @@
"@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/config-helpers": ["@eslint/config-helpers@0.2.0", "", {}, "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ=="],
"@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.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/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "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-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.21.0", "", {}, "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw=="],
"@eslint/js": ["@eslint/js@9.23.0", "", {}, "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
@@ -222,41 +195,39 @@
"@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.0", "", { "dependencies": { "tslib": "2" } }, "sha512-4rB4g+3hESy1bHSBG3tDFaMY2CH67iT7yne1e+0CLTsGLDcmoEWWpJjjpWVaYgYfYuohIRuo0E+N536gd2ZHZA=="],
"@hcengineering/analytics": ["@hcengineering/analytics@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/analytics/0.7.15/46f0006d8f4ba8ec04105770e6418251def15296", { "dependencies": { "@hcengineering/platform": "^0.7.15" } }, "sha512-U+tswirTFX0z7Sr51zfOEXbtrtfnf2xLzWTIrPip929g+oULJjCKMqoh+3HHVCF4HFm97x0P3qaRtW2hLgxyAQ=="],
"@hcengineering/account-client": ["@hcengineering/account-client@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/account-client/0.7.28/32c9bc0dd297f00f4d5461467f25e072d01b9c76", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28" } }, "sha512-k1dkioX+jYrLyAXLLLsBYVCdZKf3WjhnDHw/yeAQgLQQ/9W2ffLtDMZ8lYWV3XMQ3eWrUCodtTDOiAQgXd/LEQ=="],
"@hcengineering/card": ["@hcengineering/card@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/card/0.7.15/70ba947306289e9ec1719ec4670abb5abadd9a74", { "dependencies": { "@hcengineering/core": "^0.7.15", "@hcengineering/platform": "^0.7.15", "@hcengineering/ui": "^0.7.15" } }, "sha512-xIM9UwFqhyjD9EJs0egn4kf3Q9Br8+spYHkzNl+5IPdL5d5itDIuy5HS9ng6XZ1BXApauo8MTl1DcaqJMneNbw=="],
"@hcengineering/analytics": ["@hcengineering/analytics@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/analytics/0.7.28/a8faf3d575bb28b9682b659495e738f8e0ca596e", { "dependencies": { "@hcengineering/platform": "^0.7.28" } }, "sha512-+CI/wJ8zgop+3mEcvhlB1UJBU6tIKq2Y7qTichZjGli8BckEIEwbD4j2beu2qXRSoTT2yUGWM9zpS9+fmH/DAw=="],
"@hcengineering/card": ["@hcengineering/card@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/card/0.7.28/5348996f5b7b4a34517780a5bf324122cbddfbad", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "@hcengineering/ui": "^0.7.28" } }, "sha512-1d3allyx9jTpE/gr9kk/g5qo5848i4FKsRiHgCpJqzNEdRW7TAednoHAr2hKRU5DZC/i3Br56axSkUQzV2Idhg=="],
"@hcengineering/communication-client-query": ["@hcengineering/communication-client-query@workspace:packages/client-query"],
"@hcengineering/communication-client-sqlite": ["@hcengineering/communication-client-sqlite@workspace:packages/client-sqlite"],
"@hcengineering/communication-client-ws": ["@hcengineering/communication-client-ws@workspace:packages/client-ws"],
"@hcengineering/communication-cockroach": ["@hcengineering/communication-cockroach@workspace:packages/cockroach"],
"@hcengineering/communication-examples": ["@hcengineering/communication-examples@workspace:packages/examples"],
"@hcengineering/communication-query": ["@hcengineering/communication-query@workspace:packages/query"],
"@hcengineering/communication-rest-client": ["@hcengineering/communication-rest-client@workspace:packages/rest-client"],
"@hcengineering/communication-sdk-types": ["@hcengineering/communication-sdk-types@workspace:packages/sdk-types"],
"@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"],
"@hcengineering/core": ["@hcengineering/core@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/core/0.7.15/785cb20f9adc4a3a3c2179a0ae38316f4b4f933f", { "dependencies": { "@hcengineering/analytics": "^0.7.15", "@hcengineering/platform": "^0.7.15", "fast-equals": "^5.2.2" } }, "sha512-YhRRdtcSuNSQUdh4TO5u3u/w/vE7fcildc51wnL7Erw7VZ1rVANEx9a3mScnViVH89xw0oAcfDAm/AbH5+EZcQ=="],
"@hcengineering/communication-yaml": ["@hcengineering/communication-yaml@workspace:packages/yaml"],
"@hcengineering/platform": ["@hcengineering/platform@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/platform/0.7.15/3e6c41c4abfd6425be49d9b31afef627dcafa4cb", { "dependencies": { "intl-messageformat": "^10.7.14" } }, "sha512-zv6afaQVu/T0MXrWch8JaxdVjLSUHDpIx3jgY1JE5xSy+FN8bjC30FR/RGavuMtm4z25rFsZQRPfr4qmZYZirQ=="],
"@hcengineering/core": ["@hcengineering/core@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/core/0.7.28/43b84c63262e2b8fe5554c1967812e2ce6bb56e1", { "dependencies": { "@hcengineering/analytics": "^0.7.28", "@hcengineering/platform": "^0.7.28", "fast-equals": "^5.2.2" } }, "sha512-qv4QE7mkect6JYlGMljW5CQ2uU80hFGq0E17lZ5rO79h1boWEKLJpINv+w3J2BdR/nNG7taIsdZg3yfl9fY4bg=="],
"@hcengineering/server-token": ["@hcengineering/server-token@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/server-token/0.7.15/5443096926f96919ed8ed748e90e3474137519ec", { "dependencies": { "@hcengineering/core": "^0.7.15", "@hcengineering/platform": "^0.7.15", "jwt-simple": "^0.5.6" } }, "sha512-XEh0nDSSxMn34a9AHeXIgRh6iO025hKmGNXLZlXnm4kRIYODzMLhigds2H/RvmUjznfUTm9Bpy3EFLq9WFmzWg=="],
"@hcengineering/platform": ["@hcengineering/platform@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/platform/0.7.28/2126ff97284b679ceab3c86f24febd73a3a28712", { "dependencies": { "intl-messageformat": "^10.7.14" } }, "sha512-0zEifwsLdOs/M6TZ9xnPrQTTlhwW28sooiyL4ZfQsiC2AJUy05vLP5/W8V9eDLta+EiR5bjtlulGsg1GfPjUoQ=="],
"@hcengineering/theme": ["@hcengineering/theme@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/theme/0.7.15/f02e70ea8421f0cd3e5b52c34ee69488d361b3e6", { "dependencies": { "@hcengineering/analytics": "^0.7.15", "@hcengineering/platform": "^0.7.15", "svelte": "^4.2.19" } }, "sha512-d+rX4Jp4J4YELBi763M5Zfe+3x5gyQjV1NcuP1GIvSCe9tXNboKEp5X2SPdqFlP1sBfE5sgVECIAtmZkhMAqcQ=="],
"@hcengineering/server-token": ["@hcengineering/server-token@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/server-token/0.7.28/b1cffcb628aa0c6ac73171ef340c8ccf0f85ab07", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "jwt-simple": "^0.5.6" } }, "sha512-TaHwzxuoFKN6jnWHIZk0UcRmB+UQ1QE1HZzUh5doPSFUYOAtTrSb32iJDA8e8oEADcJpOp9RGIUP/EP/VED7BQ=="],
"@hcengineering/ui": ["@hcengineering/ui@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/ui/0.7.15/8462766246888da93d3f51e19e0b11d7c40312f6", { "dependencies": { "@hcengineering/analytics": "^0.7.15", "@hcengineering/core": "^0.7.15", "@hcengineering/platform": "^0.7.15", "@hcengineering/theme": "^0.7.15", "autolinker": "4.0.0", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "dompurify": "^3.1.6", "emojibase": "^16.0.0", "fast-equals": "^5.2.2", "hls.js": "^1.5.20", "svelte": "^4.2.19" } }, "sha512-OkllPX5wH3eJ3WSEzr+PJ0H8H2ZyXWGNHdUmwQSPW6UTZqAYia9WtCVO3sxR7SMtZtjAnKPitRycSG4Q/z8GVA=="],
"@hcengineering/theme": ["@hcengineering/theme@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/theme/0.7.28/557640a7ba2ffaa4d6e5411a54577f60dc528300", { "dependencies": { "@hcengineering/analytics": "^0.7.28", "@hcengineering/platform": "^0.7.28", "svelte": "^4.2.19" } }, "sha512-Hv3wQKHr327TC7wMHCBiG4bMtg8vSlb7+UFwdKI8ztEs3359meqkXu+exqi7Nl5334voXIz5ZFxjwZwOMzNSFA=="],
"@hcengineering/ui": ["@hcengineering/ui@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/ui/0.7.28/82025fc775702ea8c8569611356662bfbf28eb74", { "dependencies": { "@hcengineering/analytics": "^0.7.28", "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "@hcengineering/theme": "^0.7.28", "autolinker": "4.0.0", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "dompurify": "^3.1.6", "emojibase": "^16.0.0", "fast-equals": "^5.2.2", "hls.js": "^1.5.20", "plyr": "^3.7.8", "svelte": "^4.2.19" } }, "sha512-QsUpO0+Idu1n2dC6KtorPJ6SSp30It8FHSuG3Y+Awv4td/ftVocOSFqhskbk+us9Yw9KqOYgkqhGYEHbfRfw4Q=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
@@ -276,21 +247,17 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@msgpack/msgpack": ["@msgpack/msgpack@3.0.1", "", {}, "sha512-9qysoVTITLcOFIIJeXbdtUgvvY25ojUp+WWfLc0O4H4KKWeamUNAqkjS5mej/PnVDnH70llWKNa7pzv5U4TqVQ=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@pkgr/core": ["@pkgr/core@0.1.1", "", {}, "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA=="],
"@sqlite.org/sqlite-wasm": ["@sqlite.org/sqlite-wasm@3.47.1-build1", "", { "bin": { "sqlite-wasm": "bin/index.js" } }, "sha512-3qnVGab3sjJ8ov0ce1rQGZrMkglYEQ/q5fnq1s1BpRWFMYsiJVaLD1zKepcI9E9r3qx7929+2G27Hfsasvrm6Q=="],
"@pkgr/core": ["@pkgr/core@0.2.0", "", {}, "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ=="],
"@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="],
"@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="],
"@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
@@ -312,7 +279,7 @@
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
"@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
"@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/qs": ["@types/qs@6.9.18", "", {}, "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="],
@@ -322,27 +289,29 @@
"@types/serve-static": ["@types/serve-static@1.15.7", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw=="],
"@types/snappyjs": ["@types/snappyjs@0.7.1", "", {}, "sha512-OxjzJ6cQZstysMh6PEwZWmK9qlKZyezHJKOkcUkZDooSFuog2votUEKkxMaTq51UQF3cJkXKQ+XGlj4FSl8JQQ=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"@types/ws": ["@types/ws@8.18.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw=="],
"@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/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.27.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.27.0", "@typescript-eslint/type-utils": "8.27.0", "@typescript-eslint/utils": "8.27.0", "@typescript-eslint/visitor-keys": "8.27.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.9.0" } }, "sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA=="],
"@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/parser": ["@typescript-eslint/parser@8.27.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.27.0", "@typescript-eslint/types": "8.27.0", "@typescript-eslint/typescript-estree": "8.27.0", "@typescript-eslint/visitor-keys": "8.27.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA=="],
"@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/scope-manager": ["@typescript-eslint/scope-manager@8.27.0", "", { "dependencies": { "@typescript-eslint/types": "8.27.0", "@typescript-eslint/visitor-keys": "8.27.0" } }, "sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw=="],
"@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/type-utils": ["@typescript-eslint/type-utils@8.27.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.27.0", "@typescript-eslint/utils": "8.27.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-wVArTVcz1oJOIEJxui/nRhV0TXzD/zMSOYi/ggCfNq78EIszddXcJb7r4RCp/oBrjt8n9A0BSxRMKxHftpDxDA=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.25.0", "", {}, "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.27.0", "", {}, "sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A=="],
"@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/typescript-estree": ["@typescript-eslint/typescript-estree@8.27.0", "", { "dependencies": { "@typescript-eslint/types": "8.27.0", "@typescript-eslint/visitor-keys": "8.27.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.9.0" } }, "sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A=="],
"@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/utils": ["@typescript-eslint/utils@8.27.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.27.0", "@typescript-eslint/types": "8.27.0", "@typescript-eslint/typescript-estree": "8.27.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q=="],
"@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=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.27.0", "", { "dependencies": { "@typescript-eslint/types": "8.27.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ=="],
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
@@ -364,7 +333,7 @@
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="],
"bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
@@ -378,10 +347,14 @@
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"core-js": ["core-js@3.41.0", "", {}, "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
"custom-event-polyfill": ["custom-event-polyfill@1.0.7", "", {}, "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w=="],
"date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="],
"date-fns-tz": ["date-fns-tz@2.0.1", "", { "peerDependencies": { "date-fns": "2.x" } }, "sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA=="],
@@ -398,13 +371,13 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"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": ["eslint@9.23.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.23.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.3.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-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw=="],
"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=="],
"eslint-plugin-prettier": ["eslint-plugin-prettier@5.2.3", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.9.1" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw=="],
"eslint-plugin-prettier": ["eslint-plugin-prettier@5.2.4", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.10.2" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-SFtuYmnhwYCtuCDTKPoK+CEzCnEgKTU2qTLwoCxvrC0MFBTIXo1i6hDYOI4cwHaE5GZtlWmTN3YfucYi7KJwPw=="],
"eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="],
"eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
@@ -432,7 +405,7 @@
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
@@ -460,8 +433,6 @@
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
"intl-messageformat": ["intl-messageformat@10.7.15", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.3", "@formatjs/fast-memoize": "2.2.6", "@formatjs/icu-messageformat-parser": "2.11.1", "tslib": "2" } }, "sha512-LRyExsEsefQSBjU2p47oAheoKz+EOJxSLDdjOaEjdriajfHsMXOmV/EhMvYSg9bAgCUHasuAC+mcUBe/95PfIg=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
@@ -488,6 +459,8 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"loadjs": ["loadjs@4.3.0", "", {}, "sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
@@ -516,8 +489,6 @@
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"path": ["path@0.12.7", "", { "dependencies": { "process": "^0.11.1", "util": "^0.10.3" } }, "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -526,6 +497,8 @@
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"plyr": ["plyr@3.7.8", "", { "dependencies": { "core-js": "^3.26.1", "custom-event-polyfill": "^1.0.7", "loadjs": "^4.2.0", "rangetouch": "^2.0.1", "url-polyfill": "^1.1.12" } }, "sha512-yG/EHDobwbB/uP+4Bm6eUpJ93f8xxHjjk2dYcD1Oqpe1EcuQl5tzzw9Oq+uVAzd2lkM11qZfydSiyIpiB8pgdA=="],
"postgres": ["postgres@3.4.5", "", {}, "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
@@ -534,17 +507,17 @@
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"rangetouch": ["rangetouch@2.0.1", "", {}, "sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA=="],
"regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
@@ -554,6 +527,8 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"snappyjs": ["snappyjs@0.7.0", "", {}, "sha512-u5iEEXkMe2EInQio6Wv9LWHOQYRDbD2O9hzS27GpT/lwfIQhTCnHCTqedqHIHe9ZcvQo+9au6vngQayipz1NYw=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
@@ -562,7 +537,7 @@
"svelte": ["svelte@4.2.19", "", { "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/trace-mapping": "^0.3.18", "@types/estree": "^1.0.1", "acorn": "^8.9.0", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "code-red": "^1.0.3", "css-tree": "^2.3.1", "estree-walker": "^3.0.3", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", "periscopic": "^3.1.0" } }, "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw=="],
"synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw=="],
"synckit": ["synckit@0.10.2", "", { "dependencies": { "@pkgr/core": "^0.2.0", "tslib": "^2.8.1" } }, "sha512-cSGiaCPhFzeFIQY8KKEacv46LclENY4d60jgkwCrKomvRkIjtMyss1dPkHLp/62c1leuOjEedB1+lWcwqTJSvA=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
@@ -572,17 +547,17 @@
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
"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=="],
"typescript-eslint": ["typescript-eslint@8.27.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.27.0", "@typescript-eslint/parser": "8.27.0", "@typescript-eslint/utils": "8.27.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-ZZ/8+Y0rRUMuW1gJaPtLWe4ryHbsPLzzibk5Sq+IFa2aOH1Vo0gPr1fbA6pOnzBke7zC2Da4w8AyCgxKXo3lqA=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"util": ["util@0.10.4", "", { "dependencies": { "inherits": "2.0.3" } }, "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A=="],
"url-polyfill": ["url-polyfill@1.1.13", "", {}, "sha512-tXzkojrv2SujumYthZ/WjF7jaSfNhSXlYMpE5AYdL2I3D7DCeo+mch8KtW2rUuKjDg+3VXODXHVgipt8yGY/eQ=="],
"uuid": ["uuid@11.0.5", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA=="],
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -596,6 +571,8 @@
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"bun-types/@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
+6 -6
View File
@@ -8,13 +8,13 @@
"format": "prettier --write packages/**/src/*.ts && bun run lint"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/bun": "^1.2.4",
"bun-types": "^1.2.4",
"eslint": "^9.21.0",
"@eslint/js": "^9.23.0",
"@types/bun": "^1.2.5",
"bun-types": "^1.2.5",
"eslint": "^9.23.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-prettier": "^5.2.4",
"prettier": "^3.5.3",
"typescript-eslint": "^8.25.0"
"typescript-eslint": "^8.27.0"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-client-query",
"version": "0.1.61",
"version": "0.1.147",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
+19 -10
View File
@@ -13,30 +13,39 @@
// limitations under the License.
//
import { LiveQueries } from '@hcengineering/communication-query'
import type { QueryClient } from '@hcengineering/communication-sdk-types'
import { LiveQueries, type QueryClient } from '@hcengineering/communication-query'
import type { WorkspaceID } from '@hcengineering/communication-types'
import { MessagesQuery, NotificationsQuery } from './query'
import { MessagesQuery, NotificationContextsQuery, NotificationsQuery } from './query'
let lq: LiveQueries
let onDestroy: (fn: () => void) => void = () => {}
export function createMessagesQuery(): MessagesQuery {
return new MessagesQuery(lq)
return new MessagesQuery(lq, onDestroy)
}
export function createNotificationsQuery(): NotificationsQuery {
return new NotificationsQuery(lq)
return new NotificationsQuery(lq, onDestroy)
}
export function initLiveQueries(client: QueryClient, workspace: WorkspaceID, filesUrl: string): void {
export function createNotificationContextsQuery(): NotificationContextsQuery {
return new NotificationContextsQuery(lq, onDestroy)
}
export function initLiveQueries(
client: QueryClient,
workspace: WorkspaceID,
filesUrl: string,
destroyFn?: (fn: () => void) => void
): void {
if (lq != null) {
lq.close()
}
lq = new LiveQueries(client, workspace, filesUrl)
client.onEvent = (event) => {
void lq.onEvent(event)
if (destroyFn != null) {
onDestroy = destroyFn
}
lq = new LiveQueries(client, workspace, filesUrl)
}
+29 -3
View File
@@ -19,14 +19,26 @@ import type {
NotificationsQueryCallback,
QueryCallback
} from '@hcengineering/communication-sdk-types'
import { type FindMessagesParams, type FindNotificationsParams } from '@hcengineering/communication-types'
import {
type FindMessagesParams,
type FindNotificationContextParams,
type FindNotificationsParams,
type NotificationContext
} from '@hcengineering/communication-types'
import { deepEqual } from 'fast-equals'
class BaseQuery<P extends Record<string, any>, C extends QueryCallback<any>> {
private oldQuery: P | undefined
private oldCallback: QueryCallback<any> | undefined
private oldCallback: C | undefined
constructor(protected readonly lq: LiveQueries) {}
constructor(
protected readonly lq: LiveQueries,
onDestroy: (fn: () => void) => void
) {
onDestroy(() => {
this.unsubscribe()
})
}
unsubscribe: () => void = () => {}
@@ -82,3 +94,17 @@ export class NotificationsQuery extends BaseQuery<FindNotificationsParams, Notif
return this.lq.queryNotifications(params, callback)
}
}
export class NotificationContextsQuery extends BaseQuery<
FindNotificationContextParams,
QueryCallback<NotificationContext>
> {
override createQuery(
params: FindNotificationContextParams,
callback: QueryCallback<NotificationContext>
): {
unsubscribe: () => void
} {
return this.lq.queryNotificationContexts(params, callback)
}
}
-31
View File
@@ -1,31 +0,0 @@
{
"name": "@hcengineering/communication-client-sqlite",
"version": "0.1.61",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
"files": [
"dist/index.js",
"dist/index.cjs",
"types/**/*.d.ts"
],
"scripts": {
"bundle": "bun run bundle:browser",
"bundle:browser": "bun build src/index.ts --outdir dist --target browser"
},
"devDependencies": {
"@types/bun": "^1.1.14"
},
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-sqlite-wasm": "workspace:*",
"fast-equals": "^5.0.1"
},
"peerDependencies": {
"typescript": "^5.6.3"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
-215
View File
@@ -1,215 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type CardID,
type Message,
type FindMessagesParams,
type MessageID,
type RichText,
type SocialID,
type ContextID,
type NotificationContextUpdate,
type FindNotificationContextParams,
type NotificationContext,
type FindNotificationsParams,
type Notification,
type Attachment,
type Reaction,
type WorkspaceID,
type FindMessagesGroupsParams,
type MessagesGroup,
PatchType
} from '@hcengineering/communication-types'
import {
type Client,
type MessageCreatedEvent,
type DbAdapter,
type ResponseEvent,
ResponseEventType
} from '@hcengineering/communication-sdk-types'
import { createDbAdapter as createSqliteDbAdapter } from '@hcengineering/communication-sqlite-wasm'
//TODO: FIXME
class DbClient {
onEvent: (event: ResponseEvent) => void = () => {}
constructor(
private readonly db: DbAdapter,
private readonly workspace: WorkspaceID,
private readonly personalWorkspace: WorkspaceID
) {}
async createMessage(card: CardID, content: RichText, creator: SocialID): Promise<MessageID> {
const created = new Date()
const id = await this.db.createMessage(card, content, creator, created)
const event: MessageCreatedEvent = {
type: ResponseEventType.MessageCreated,
message: {
id,
card,
content,
creator,
created,
edited: created,
reactions: [],
attachments: []
}
}
this.onEvent(event)
return id
}
async removeMessage(card: CardID, message: MessageID) {
await this.db.removeMessage(card, message)
this.onEvent({ type: ResponseEventType.MessageRemoved, message, card })
}
async updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise<void> {
const created = new Date()
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> {
const created = new Date()
await this.db.createReaction(card, message, reaction, creator, created)
this.onEvent({ type: ResponseEventType.ReactionCreated, card, reaction: { message, reaction, creator, created } })
}
async removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void> {
await this.db.removeReaction(card, message, reaction, creator)
this.onEvent({ type: ResponseEventType.ReactionRemoved, card, message, reaction, creator })
}
async createAttachment(card: CardID, message: MessageID, attachment: CardID, creator: SocialID): Promise<void> {
const created = new Date()
await this.db.createAttachment(message, card, creator, created)
this.onEvent({
type: ResponseEventType.AttachmentCreated,
card,
attachment: { message, card: attachment, creator, created }
})
}
async removeAttachment(card: CardID, message: MessageID, attachment: CardID): Promise<void> {
await this.db.removeAttachment(message, card)
this.onEvent({ type: ResponseEventType.AttachmentRemoved, message, card, attachment })
}
async findMessages(params: FindMessagesParams): Promise<Message[]> {
const rawMessages = await this.db.findMessages(params)
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]
}
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 createNotification(message: MessageID, context: ContextID): Promise<void> {
await this.db.createNotification(message, context)
}
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
await this.db.removeNotification(message, context)
}
async createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise<ContextID> {
return await this.db.createContext(this.personalWorkspace, card, lastView, lastUpdate)
}
async updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
await this.db.updateContext(context, update)
}
async removeNotificationContext(context: ContextID): Promise<void> {
await this.db.removeContext(context)
}
async findNotificationContexts(params: FindNotificationContextParams): Promise<NotificationContext[]> {
//TODO: should we filter by workspace?
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
}
close() {
this.db.close()
}
}
export async function getSqliteClient(
workspace: WorkspaceID,
personalWorkspace: WorkspaceID,
dbUrl = 'file:communication.sqlite3?vfs=opfs'
): Promise<Client> {
const db = await createSqliteDbAdapter(dbUrl)
return new DbClient(db, workspace, personalWorkspace) as unknown as Client
}
-9
View File
@@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
-240
View File
@@ -1,240 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type CardID,
type ContextID,
type FindMessagesGroupsParams,
type FindMessagesParams,
type FindNotificationContextParams,
type FindNotificationsParams,
type Message,
type MessageID,
type MessagesGroup,
type Notification,
type NotificationContext,
type NotificationContextUpdate,
type RichText,
type SocialID,
type WorkspaceID
} from '@hcengineering/communication-types'
import {
RequestEventType,
type Client,
type CreateAttachmentEvent,
type CreateMessageEvent,
type CreateMessageResult,
type CreateNotificationContextEvent,
type CreateNotificationContextResult,
type CreateNotificationEvent,
type CreatePatchEvent,
type CreateReactionEvent,
type EventResult,
type RemoveAttachmentEvent,
type RemoveMessageEvent,
type RemoveNotificationContextEvent,
type RemoveNotificationEvent,
type RemoveReactionEvent,
type RequestEvent,
type ResponseEvent,
type UpdateNotificationContextEvent
} from '@hcengineering/communication-sdk-types'
import { initLiveQueries } from '@hcengineering/communication-client-query'
import { WebSocketConnection } from './connection'
class WsClient implements Client {
private readonly ws: WebSocketConnection
onEvent: (event: ResponseEvent) => void = () => {}
constructor(
private readonly url: string,
private readonly token: string,
private readonly binary: boolean = false
) {
const connectionUrl = this.url + '?token=' + this.token
this.ws = new WebSocketConnection(connectionUrl, this.binary)
this.ws.onEvent = (event) => {
void this.onEvent(event)
}
}
async createMessage(card: CardID, content: RichText, creator: SocialID): Promise<MessageID> {
const event: CreateMessageEvent = {
type: RequestEventType.CreateMessage,
card,
content,
creator
}
const result = await this.sendEvent(event)
return (result as CreateMessageResult).id
}
async removeMessage(card: CardID, message: MessageID): Promise<void> {
const event: RemoveMessageEvent = {
type: RequestEventType.RemoveMessage,
card,
message
}
await this.sendEvent(event)
}
async updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise<void> {
const event: CreatePatchEvent = {
type: RequestEventType.CreatePatch,
card,
message,
content,
creator
}
await this.sendEvent(event)
}
async createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void> {
const event: CreateReactionEvent = {
type: RequestEventType.CreateReaction,
card,
message,
reaction,
creator
}
await this.sendEvent(event)
}
async removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void> {
const event: RemoveReactionEvent = {
type: RequestEventType.RemoveReaction,
card,
message,
reaction,
creator
}
await this.sendEvent(event)
}
async createAttachment(card: CardID, message: MessageID, attachment: CardID, creator: SocialID): Promise<void> {
const event: CreateAttachmentEvent = {
type: RequestEventType.CreateAttachment,
card,
message,
attachment,
creator
}
await this.sendEvent(event)
}
async removeAttachment(card: CardID, message: MessageID, attachment: CardID): Promise<void> {
const event: RemoveAttachmentEvent = {
type: RequestEventType.RemoveAttachment,
card,
message,
attachment
}
await this.sendEvent(event)
}
async findMessages(params: FindMessagesParams, queryId?: number): Promise<Message[]> {
return await this.ws.send('findMessages', [params, queryId])
}
async findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
return await this.ws.send('findMessagesGroups', [params])
}
async createNotification(message: MessageID, context: ContextID): Promise<void> {
const event: CreateNotificationEvent = {
type: RequestEventType.CreateNotification,
message,
context
}
await this.sendEvent(event)
}
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
const event: RemoveNotificationEvent = {
type: RequestEventType.RemoveNotification,
message,
context
}
await this.sendEvent(event)
}
async createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise<ContextID> {
const event: CreateNotificationContextEvent = {
type: RequestEventType.CreateNotificationContext,
card,
lastView,
lastUpdate
}
const result = await this.sendEvent(event)
return (result as CreateNotificationContextResult).id
}
async removeNotificationContext(context: ContextID): Promise<void> {
const event: RemoveNotificationContextEvent = {
type: RequestEventType.RemoveNotificationContext,
context
}
await this.sendEvent(event)
}
async updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
const event: UpdateNotificationContextEvent = {
type: RequestEventType.UpdateNotificationContext,
context,
update
}
await this.sendEvent(event)
}
async findNotificationContexts(
params: FindNotificationContextParams,
queryId?: number
): Promise<NotificationContext[]> {
return await this.ws.send('findNotificationContexts', [params, queryId])
}
async findNotifications(params: FindNotificationsParams, queryId?: number): Promise<Notification[]> {
return await this.ws.send('findNotifications', [params, queryId])
}
async unsubscribeQuery(id: number): Promise<void> {
await this.ws.send('unsubscribeQuery', [id])
}
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,
workspace: WorkspaceID,
filesUrl: string
): Promise<Client> {
const client = new WsClient(url, token)
initLiveQueries(client, workspace, filesUrl)
return client
}
-156
View File
@@ -1,156 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { ResponseEvent } from '@hcengineering/communication-sdk-types'
import { encode, decode } from '@msgpack/msgpack'
const PING_TIMEOUT = 10000
const RECONNECT_TIMEOUT = 1000
export type RequestId = string
export interface Response {
id?: RequestId
result?: any
error?: string
}
export interface Request {
id?: RequestId
method: string
params: any[]
}
export interface HelloRequest extends Request {
binary?: boolean
}
export class WebSocketConnection {
private ws!: WebSocket | Promise<WebSocket>
private requests: { [key: RequestId]: { resolve: (response: any) => void; reject: (reason: any) => void } } = {}
private lastId: number = 0
private pingInterval: any
private reconnectTimeout: any
onEvent: (event: ResponseEvent) => void = () => {}
constructor(
private url: string,
private readonly binary: boolean = false
) {
this.connect()
}
private connect(): void {
const ws = new WebSocket(this.url)
ws.onmessage = (event: MessageEvent) => {
const response = deserializeResponse(event.data, this.binary)
if (response.id !== undefined) {
const handlers = this.requests[response.id]
if (handlers === undefined) return
delete this.requests[response.id]
if (response.error !== undefined) {
console.error('Websocket error', response.error)
handlers.reject(response.error)
} else {
handlers.resolve(response.result)
}
} else {
if (response.error !== undefined) {
console.error('Websocket error', response.error)
} else {
const event = response.result as ResponseEvent
this.onEvent(event)
}
}
}
ws.onclose = () => {
clearInterval(this.pingInterval)
this.handleReconnect()
}
this.ws = new Promise((resolve, reject) => {
ws.onopen = () => {
const request: HelloRequest = { id: 'hello', method: 'hello', params: [], binary: this.binary }
ws.send(serializeRequest(request, this.binary))
clearInterval(this.pingInterval)
this.pingInterval = setInterval(() => {
void this.sendRequest({ method: 'ping', params: [] })
}, PING_TIMEOUT)
resolve(ws)
}
ws.onerror = (event: any) => {
console.error('Websocket error', event)
reject(new Error('Websocket error'))
}
})
}
private handleReconnect() {
clearTimeout(this.reconnectTimeout)
this.reconnectTimeout = setTimeout(() => {
this.connect()
}, RECONNECT_TIMEOUT)
}
async waitWs(): Promise<WebSocket> {
if (this.ws instanceof Promise) {
this.ws = await this.ws
}
return this.ws
}
async send(method: string, params: any[]): Promise<any> {
const id = ++this.lastId
return await this.sendRequest({ id: id.toString(), method, params })
}
private async sendRequest(request: Request): Promise<any> {
const ws = await this.waitWs()
return new Promise((resolve, reject) => {
if (request.id !== undefined) {
this.requests[request.id] = { resolve, reject }
}
ws.send(serializeRequest(request, this.binary))
})
}
async close(): Promise<void> {
clearInterval(this.pingInterval)
clearTimeout(this.reconnectTimeout)
const ws = await this.waitWs()
ws.close()
}
}
function serializeRequest(request: Request, binary: boolean): any {
if (binary) {
return encode(request)
} else {
return JSON.stringify(request)
}
}
function deserializeResponse(data: any, binary: boolean): Response {
if (binary) {
return decode(data) as Response
} else {
return JSON.parse(data.toString())
}
}
+6 -4
View File
@@ -8,6 +8,10 @@ CREATE TABLE IF NOT EXISTS communication.messages
creator VARCHAR(255) NOT NULL,
created TIMESTAMPTZ NOT NULL,
type VARCHAR(255) NOT NULL,
data JSONB NOT NULL DEFAULT '{}',
PRIMARY KEY (workspace_id, card_id, id)
);
@@ -20,10 +24,8 @@ CREATE TABLE IF NOT EXISTS communication.messages_groups
card_id VARCHAR(255) NOT NULL,
blob_id UUID NOT NULL,
from_date TIMESTAMPTZ NOT NULL,
to_date TIMESTAMPTZ NOT NULL,
from_id INT8 NOT NULL,
to_id INT8 NOT NULL,
from_sec TIMESTAMPTZ(0) NOT NULL,
to_sec TIMESTAMPTZ(0) NOT NULL,
count INT NOT NULL,
PRIMARY KEY (workspace_id, card_id, blob_id)
+9 -8
View File
@@ -1,13 +1,14 @@
CREATE TABLE IF NOT EXISTS communication.patch
(
id INT8 NOT NULL DEFAULT unique_rowid(),
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
message_id INT8 NOT NULL,
type VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
creator VARCHAR(255) NOT NULL,
created TIMESTAMPTZ NOT NULL,
id INT8 NOT NULL DEFAULT unique_rowid(),
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
message_id INT8 NOT NULL,
type VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
creator VARCHAR(255) NOT NULL,
created TIMESTAMPTZ NOT NULL,
message_created_sec TIMESTAMPTZ(0) NOT NULL,
PRIMARY KEY (id)
);
@@ -1,11 +0,0 @@
CREATE TABLE IF NOT EXISTS communication.attachments
(
message_id INT8 NOT NULL,
card_id VARCHAR(255) NOT NULL,
creator VARCHAR(255) NOT NULL,
created TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (card_id, message_id)
);
CREATE INDEX IF NOT EXISTS attachment_message_idx ON communication.attachments (message_id);
@@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS communication.files
(
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
message_id INT8 NOT NULL,
blob_id UUID NOT NULL,
filename VARCHAR(255) NOT NULL,
type VARCHAR(255) NOT NULL,
size INT8 NOT NULL,
creator VARCHAR(255) NOT NULL,
created TIMESTAMPTZ NOT NULL DEFAULT now(),
message_created_sec TIMESTAMPTZ(0) NOT NULL,
PRIMARY KEY (workspace_id, card_id, message_id, blob_id)
);
CREATE INDEX IF NOT EXISTS files_workspace_card_message_idx ON communication.files (workspace_id, card_id, message_id);
@@ -1,27 +1,38 @@
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,
id INT8 NOT NULL DEFAULT unique_rowid(),
personal_workspace UUID NOT NULL,
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
account UUID NOT NULL,
archived_from TIMESTAMPTZ,
last_view TIMESTAMPTZ,
last_update TIMESTAMPTZ,
last_view TIMESTAMPTZ NOT NULL DEFAULT now(),
last_update TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (id),
UNIQUE (workspace_id, card_id, personal_workspace)
UNIQUE (workspace_id, card_id, account)
);
CREATE TABLE IF NOT EXISTS communication.notifications
(
message_id UUID NOT NULL,
context UUID NOT NULL,
id INT8 NOT NULL DEFAULT unique_rowid(),
context_id INT8 NOT NULL,
message_id INT8,
created TIMESTAMPTZ NOT NULL,
content JSONB NOT NULL DEFAULT '{}',
PRIMARY KEY (message_id, context),
FOREIGN KEY (context) REFERENCES communication.notification_context (id) ON DELETE CASCADE
PRIMARY KEY (id),
FOREIGN KEY (context_id) REFERENCES communication.notification_context (id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS notification_context_idx ON communication.notifications (context);
CREATE INDEX IF NOT EXISTS notification_context_idx ON communication.notifications (context_id);
CREATE TABLE IF NOT EXISTS communication.collaborators
(
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
account UUID NOT NULL,
date TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (workspace_id, card_id, account)
);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-cockroach",
"version": "0.1.61",
"version": "0.1.147",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
+106 -88
View File
@@ -13,11 +13,13 @@
// limitations under the License.
//
import type { ParameterOrJSON, Row } from 'postgres'
import type postgres from 'postgres'
import {
type FindCollaboratorsParams,
type AccountID,
type BlobID,
type CardID,
type Collaborator,
type ContextID,
type FindMessagesGroupsParams,
type FindMessagesParams,
@@ -26,14 +28,15 @@ import {
type Message,
type MessageID,
type MessagesGroup,
type MessageType,
type Notification,
type NotificationContext,
type NotificationContextUpdate,
type PatchType,
type RichText,
type SocialID,
type Thread,
type WorkspaceID,
type Thread
type NotificationID
} from '@hcengineering/communication-types'
import type { DbAdapter } from '@hcengineering/communication-sdk-types'
import { retry } from '@hcengineering/communication-shared'
@@ -41,8 +44,8 @@ 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'
import { injectVars } from './utils.ts'
import { type Logger, type Options, type SqlClient, type SqlParams, type SqlRow } from './types'
import { injectVars } from './utils'
export class CockroachAdapter implements DbAdapter {
private readonly message: MessagesDb
@@ -58,16 +61,19 @@ export class CockroachAdapter implements DbAdapter {
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 createMessage(
card: CardID,
type: MessageType,
content: RichText,
creator: SocialID,
created: Date,
data?: any
): Promise<MessageID> {
return await this.message.createMessage(card, type, content, creator, created, data)
}
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 removeMessages(card: CardID, messages: MessageID[], socialIds?: SocialID[]): Promise<MessageID[]> {
return await this.message.removeMessages(card, messages, socialIds)
}
async createPatch(
@@ -81,20 +87,8 @@ export class CockroachAdapter implements DbAdapter {
await this.message.createPatch(card, message, type, content, creator, created)
}
async removePatches(card: CardID, fromId: MessageID, toId: MessageID): Promise<void> {
await this.message.removePatches(card, fromId, toId)
}
async createMessagesGroup(
card: CardID,
blobId: BlobID,
fromDate: Date,
toDate: Date,
fromId: MessageID,
toId: MessageID,
count: number
): Promise<void> {
await this.message.createMessagesGroup(card, blobId, fromDate, toDate, fromId, toId, count)
async createMessagesGroup(card: CardID, blobId: BlobID, fromDate: Date, toDate: Date, count: number): Promise<void> {
await this.message.createMessagesGroup(card, blobId, fromDate, toDate, count)
}
async removeMessagesGroup(card: CardID, blobId: BlobID): Promise<void> {
@@ -115,48 +109,31 @@ export class CockroachAdapter implements DbAdapter {
await this.message.removeReaction(card, message, reaction, creator, new Date())
}
async createAttachment(message: MessageID, attachment: CardID, creator: SocialID, created: Date): Promise<void> {
await this.message.createAttachment(message, attachment, creator, created)
async createFile(
card: CardID,
message: MessageID,
blobId: BlobID,
fileType: string,
filename: string,
size: number,
creator: SocialID,
created: Date
): Promise<void> {
await this.message.createFile(card, message, blobId, fileType, filename, size, creator, created)
}
async removeAttachment(message: MessageID, attachment: CardID): Promise<void> {
await this.message.removeAttachment(message, attachment)
async removeFile(card: CardID, message: MessageID, blobId: BlobID): Promise<void> {
await this.message.removeFile(card, message, blobId)
}
async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void> {
await this.message.createThread(card, message, thread, created)
}
async updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise<void> {
await this.message.updateThread(thread, lastReply, op)
async updateThread(thread: CardID, op: 'increment' | 'decrement', lastReply?: Date): Promise<void> {
await this.message.updateThread(thread, op, lastReply)
}
async createNotification(message: MessageID, context: ContextID): Promise<void> {
await this.notification.createNotification(message, context)
}
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
await this.notification.removeNotification(message, context)
}
async createContext(
personalWorkspace: WorkspaceID,
card: CardID,
lastView?: Date,
lastUpdate?: Date
): Promise<ContextID> {
return await this.notification.createContext(personalWorkspace, card, lastView, lastUpdate)
}
async updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
await this.notification.updateContext(context, update)
}
async removeContext(context: ContextID): Promise<void> {
await this.notification.removeContext(context)
}
// Finds
async findMessages(params: FindMessagesParams): Promise<Message[]> {
return await this.message.find(params)
}
@@ -169,25 +146,53 @@ export class CockroachAdapter implements DbAdapter {
return await this.message.findThread(thread)
}
async findContexts(
params: FindNotificationContextParams,
personalWorkspaces: WorkspaceID[],
workspace?: WorkspaceID
): Promise<NotificationContext[]> {
return await this.notification.findContexts(params, personalWorkspaces, workspace)
async addCollaborators(card: CardID, collaborators: AccountID[], date?: Date): Promise<void> {
await this.notification.addCollaborators(card, collaborators, date)
}
async findNotifications(
params: FindNotificationsParams,
personalWorkspace: WorkspaceID,
workspace?: WorkspaceID
): Promise<Notification[]> {
return await this.notification.findNotifications(params, personalWorkspace, workspace)
async removeCollaborators(card: CardID, collaborators: AccountID[]): Promise<void> {
await this.notification.removeCollaborators(card, collaborators)
}
async createNotification(context: ContextID, message: MessageID, messageCreated: Date): Promise<NotificationID> {
return await this.notification.createNotification(context, message, messageCreated)
}
async removeNotification(context: ContextID, account: AccountID, untilDate: Date): Promise<void> {
await this.notification.removeNotifications(context, account, untilDate)
}
async createContext(account: AccountID, card: CardID, lastUpdate: Date, lastView: Date): Promise<ContextID> {
return await this.notification.createContext(account, card, lastUpdate, lastView)
}
async updateContext(context: ContextID, account: AccountID, lastUpdate?: Date, lastView?: Date): Promise<void> {
await this.notification.updateContext(context, account, lastUpdate, lastView)
}
async removeContext(context: ContextID, account: AccountID): Promise<void> {
await this.notification.removeContext(context, account)
}
async findContexts(params: FindNotificationContextParams): Promise<NotificationContext[]> {
return await this.notification.findContexts(params)
}
async findNotifications(params: FindNotificationsParams): Promise<Notification[]> {
return await this.notification.findNotifications(params)
}
async findCollaborators(params: FindCollaboratorsParams): Promise<Collaborator[]> {
return await this.notification.findCollaborators(params)
}
close(): void {
this.sql.close()
}
getCollaboratorsCursor(card: CardID, date: Date, size?: number): AsyncIterable<Collaborator[]> {
return this.notification.getCollaboratorsCursor(card, date, size)
}
}
export async function createDbAdapter(
@@ -197,12 +202,13 @@ export async function createDbAdapter(
options?: Options
): Promise<DbAdapter> {
const greenUrl = process.env.GREEN_URL ?? ''
const connection = connect(connectionString)
const sql = await connection.getClient()
if (greenUrl !== '') {
const client = new GreenClient(greenUrl)
const client = new GreenClient(greenUrl, sql)
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(client, workspace, logger, options)
@@ -212,8 +218,11 @@ export async function createDbAdapter(
class GreenClient implements SqlClient {
private readonly url: string
private readonly token: string
constructor(endpoint: string) {
const url = new URL(endpoint)
constructor(
private readonly endpoint: string,
private readonly sql: postgres.Sql
) {
const url = new URL(this.endpoint)
this.token = url.searchParams.get('token') ?? 'secret'
const compression = url.searchParams.get('compression') ?? ''
@@ -229,14 +238,21 @@ class GreenClient implements SqlClient {
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 })
async execute<T = SqlRow>(query: string, params?: SqlParams): 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> {
cursor<T = SqlRow>(query: string, params?: SqlParams, size?: number): AsyncIterable<NonNullable<T[][number]>[]> {
const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query
return this.sql.unsafe<T[]>(sql).cursor(size)
}
close(): void {
// do nothing
}
private async fetch<T = SqlRow>(query: string, params?: SqlParams): Promise<T> {
const url = this.url.endsWith('/') ? this.url + 'api/v1/sql' : this.url + '/api/v1/sql'
const response = await fetch(url, {
@@ -255,10 +271,6 @@ class GreenClient implements SqlClient {
return await response.json()
}
close(): void {
// do nothing
}
}
class CockroachClient implements SqlClient {
@@ -267,9 +279,15 @@ class CockroachClient implements SqlClient {
private readonly sql: postgres.Sql
) {}
async execute<T extends any[] = (Row & Iterable<Row>)[]>(query: string, params?: ParameterOrJSON<any>[]): Promise<T> {
async execute<T = SqlRow>(query: string, params?: SqlParams): Promise<T[]> {
const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query
return await this.sql.unsafe<T>(sql)
return await this.sql.unsafe<T[]>(sql)
}
cursor<T = SqlRow>(query: string, params?: SqlParams, size?: number): AsyncIterable<NonNullable<T[][number]>[]> {
const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query
return this.sql.unsafe<T[]>(sql).cursor(size)
}
close(): void {
+1 -1
View File
@@ -103,7 +103,7 @@ export function connect(connectionString: string, database?: string): PostgresCl
database,
max: 5,
fetch_types: false,
prepare: true,
prepare: false,
...extraOptions
})
+5 -9
View File
@@ -16,7 +16,7 @@
import { type ParameterOrJSON, type Row } from 'postgres'
import type { WorkspaceID } from '@hcengineering/communication-types'
import { type SqlClient, type Logger, type Options } from '../types'
import { type Logger, type Options, type SqlClient } from '../types'
export class BaseDb {
constructor (
@@ -26,11 +26,7 @@ export class BaseDb {
readonly options?: Options
) {}
async execute<T extends any[] = (Row & Iterable<Row>)[]>(
sql: string,
params?: ParameterOrJSON<any>[],
name?: string
): Promise<T> {
async execute<T = 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)
}
@@ -38,16 +34,16 @@ export class BaseDb {
return await this.client.execute(sql, params)
}
private async executeWithLogs<T extends any[] = (Row & Iterable<Row>)[]>(
private async executeWithLogs<T = Row & Iterable<Row>>(
name: string,
logger: Logger,
sql: string,
params?: ParameterOrJSON<any>[]
): Promise<T> {
): Promise<T[]> {
const start = performance.now()
try {
return await this.client.execute(sql, params)
return await this.client.execute<T>(sql, params)
} finally {
const time = performance.now() - start
logger.info(name, { time })
+251
View File
@@ -0,0 +1,251 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type File,
type BlobID,
type CardID,
type Collaborator,
type ContextID,
type Message,
type MessageID,
type MessagesGroup,
type MessageType,
type Notification,
type NotificationContext,
type NotificationID,
type Patch,
PatchType,
type Reaction,
type RichText,
type SocialID,
type Thread,
type MessageData
} from '@hcengineering/communication-types'
import {
type FileDb,
type CollaboratorDb,
type ContextDb,
type MessageDb,
type MessagesGroupDb,
type NotificationDb,
type PatchDb,
type ReactionDb,
type ThreadDb
} from './schema'
interface RawMessage extends MessageDb {
thread_id?: CardID
replies_count?: number
last_reply?: Date
patches?: PatchDb[]
files?: FileDb[]
reactions?: ReactionDb[]
}
interface RawNotification extends NotificationDb {
message_id: MessageID
message_type?: MessageType
message_content?: RichText
message_creator?: SocialID
message_data?: MessageData
message_created?: Date
message_group_blob_id?: BlobID
message_group_from_sec?: Date
message_group_to_sec?: Date
message_group_count?: number
message_patches?: {
patch_type: PatchType
patch_content: RichText
patch_creator: SocialID
patch_created: Date
}[]
}
type RawContext = ContextDb & { id: ContextID } & {
notifications?: RawNotification[]
}
export function toMessage (raw: RawMessage): Message {
const lastPatch = raw.patches?.[0]
return {
id: String(raw.id) as MessageID,
type: raw.type,
card: raw.card_id,
content: lastPatch?.content ?? raw.content,
creator: raw.creator,
created: raw.created,
data: raw.data,
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),
files: (raw.files ?? []).map(toFile)
}
}
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 toFile (raw: FileDb): File {
return {
card: raw.card_id,
message: String(raw.message_id) as MessageID,
blobId: raw.blob_id,
type: raw.type,
filename: raw.filename,
size: parseInt(raw.size as any),
creator: raw.creator,
created: raw.created
}
}
export function toMessagesGroup (raw: MessagesGroupDb): MessagesGroup {
return {
card: raw.card_id,
blobId: raw.blob_id,
fromSec: raw.from_sec,
toSec: raw.to_sec,
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
}
}
export function toNotificationContext (raw: RawContext): NotificationContext {
const lastView = new Date(raw.last_view)
return {
id: String(raw.id) as ContextID,
card: raw.card_id,
account: raw.account,
lastView,
lastUpdate: new Date(raw.last_update),
notifications: (raw.notifications ?? [])
.filter((it) => it.id != null)
.map((it) => toNotificationRaw(raw.id, raw.card_id, lastView, it))
}
}
function toNotificationRaw (
id: ContextID,
card: CardID,
lastView: Date | undefined,
raw: RawNotification
): Notification {
const created = new Date(raw.created)
const read = lastView != null && lastView >= created
let message: Message | undefined
if (
raw.message_content != null &&
raw.message_creator != null &&
raw.message_created != null &&
raw.message_type != null
) {
const lastPatch = (raw.message_patches ?? []).find((it) => it.patch_type === PatchType.update)
message = {
id: String(raw.message_id) as MessageID,
type: raw.message_type,
card,
content: lastPatch?.patch_content ?? raw.message_content,
data: raw.message_data,
creator: raw.message_creator,
created: new Date(raw.message_created),
edited: lastPatch?.patch_created != null ? new Date(lastPatch.patch_created) : undefined,
reactions: [],
files: []
}
}
if (message != null) {
return {
id: String(raw.id) as NotificationID,
read,
messageId: String(raw.message_id) as MessageID,
created,
context: String(id) as ContextID,
message
}
}
let messageGroup: MessagesGroup | undefined
if (raw.message_group_blob_id != null && raw.message_group_from_sec != null && raw.message_group_to_sec != null) {
messageGroup = {
card,
blobId: raw.message_group_blob_id,
fromSec: new Date(raw.message_group_from_sec),
toSec: new Date(raw.message_group_to_sec),
count: raw.message_group_count ?? 0
}
}
return {
id: String(raw.id) as NotificationID,
read,
messageId: raw.message_id,
created,
context: String(id) as ContextID,
messageGroup
}
}
export function toNotification (raw: RawNotification & { card_id: CardID, last_view?: Date }): Notification {
const lastView = raw.last_view != null ? new Date(raw.last_view) : undefined
return toNotificationRaw(raw.context_id, raw.card_id, lastView, raw)
}
export function toCollaborator (raw: CollaboratorDb): Collaborator {
return {
account: raw.account
}
}
+474 -390
View File
@@ -14,401 +14,487 @@
//
import {
type Message,
type MessageID,
type CardID,
type FindMessagesParams,
type SocialID,
type RichText,
SortingOrder,
PatchType,
type Thread,
type BlobID,
type FindMessagesGroupsParams,
type MessagesGroup
type BlobID,
type CardID,
type FindMessagesGroupsParams,
type FindMessagesParams,
type Message,
type MessageID,
type MessagesGroup,
type MessageType,
PatchType,
type RichText,
type SocialID,
SortingOrder,
type Thread
} from '@hcengineering/communication-types'
import {generateMessageId} from '@hcengineering/communication-shared'
import { generateMessageId, parseMessageId } from '@hcengineering/communication-shared'
import {BaseDb} from './base'
import {
TableName,
type MessageDb,
type AttachmentDb,
type ReactionDb,
type PatchDb,
toMessage,
type ThreadDb,
toThread,
type MessagesGroupDb,
toMessagesGroup
} from './schema'
import {getCondition} from './utils'
import { BaseDb } from './base'
import {type FileDb, type MessageDb, type MessagesGroupDb, type PatchDb, type ReactionDb, TableName, type ThreadDb } from './schema'
import { getCondition } from './utils'
import { toMessage, toMessagesGroup, toThread } from './mapping'
export class MessagesDb extends BaseDb {
// 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
}
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
// Message
async createMessage (
card: CardID,
type: MessageType,
content: RichText,
creator: SocialID,
created: Date,
data?: any
): Promise<MessageID> {
const id = generateMessageId()
const db: MessageDb = {
id,
type,
workspace_id: this.workspace,
card_id: card,
content,
creator,
created,
data
}
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[]);`
const sql = `INSERT INTO ${TableName.Message} (workspace_id, card_id, id, content, creator, created, type, data)
VALUES ($1::uuid, $2::varchar, $3::bigint, $4::text, $5::varchar, $6::timestamptz, $7::varchar, $8::jsonb)`
await this.execute(sql, [this.workspace, card, message, socialIds], 'remove message')
}
await this.execute(
sql,
[db.workspace_id, db.card_id, db.id, db.content, db.creator, db.created, db.type, db.data ?? {}],
'insert message'
)
return id
}
async removeMessages (card: CardID, messages: MessageID[], socialIds?: SocialID[]): Promise<MessageID[]> {
if (messages.length === 0) return []
const where: string[] = ['workspace_id = $1::uuid', 'card_id = $2::varchar']
const values: any[] = [this.workspace, card]
let index = values.length + 1
if (socialIds?.length === 1) {
where.push(`creator = $${index++}::varchar`)
values.push(socialIds[0])
}
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')
if (socialIds != null && socialIds.length > 1) {
where.push(`creator = ANY($${index++}::varchar[])`)
values.push(socialIds)
}
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
}
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'
)
if (messages.length === 1) {
where.push(`id = $${index++}::bigint`)
values.push(messages[0])
} else {
where.push(`id = ANY($${index++}::bigint[])`)
values.push(messages)
}
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;`
const sql = `DELETE FROM ${TableName.Message} WHERE ${where.join(' AND ')} RETURNING id`
await this.execute(sql, [this.workspace, card, BigInt(fromId), BigInt(toId)], 'remove patches')
const result = await this.execute(sql, values, 'remove messages')
return result.map((row: any) => row.id)
}
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,
message_created_sec: parseMessageId(message)
}
// 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)`
const sql = `INSERT INTO ${TableName.Patch} (workspace_id, card_id, message_id, type, content, creator, created, message_created_sec)
VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::text, $6::varchar, $7::timestamptz, $8::timestamptz)`
await this.execute(sql, [db.message_id, db.card_id, db.creator, db.created], 'insert attachment')
await this.execute(
sql,
[db.workspace_id, db.card_id, db.message_id, db.type, db.content, db.creator, db.created, db.message_created_sec],
'insert patch'
)
}
// File
async createFile (card: CardID, message: MessageID, blobId: BlobID, fileType: string, filename: string, size: number,creator: SocialID, created: Date): Promise<void> {
const db: FileDb = {
workspace_id: this.workspace,
card_id: card,
message_id: message,
blob_id: blobId,
type: fileType,
filename,
size,
creator,
created,
message_created_sec: parseMessageId(message)
}
const sql = `INSERT INTO ${TableName.File} (workspace_id, card_id, message_id, blob_id, type, filename, creator, created, message_created_sec, size)
VALUES ($1::uuid, $2::varchar, $3::int8, $4::uuid, $5::varchar, $6::varchar, $7::varchar, $8::timestamptz, $9::timestamptz, $10::int8)`
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')
}
await this.execute(sql, [db.workspace_id, db.card_id, db.message_id, db.blob_id, db.type, db.filename, db.creator, db.created, db.message_created_sec, db.size], 'insert file')
}
// Reaction
async createReaction(
card: CardID,
message: MessageID,
reaction: string,
creator: SocialID,
created: Date
): Promise<void> {
const select = `SELECT m.id
async removeFile (card: CardID, message: MessageID, blobId: BlobID): Promise<void> {
const sql = `DELETE
FROM ${TableName.File}
WHERE workspace_id = $1::uuid
AND card_id = $2::varchar
AND message_id = $3::bigint
AND blob_id = $4::uuid`
await this.execute(sql, [this.workspace, card, message, blobId], 'remove file')
}
// 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')
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)
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)
}
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
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')
const messageDb = await this.execute(select, [message], 'select message')
if (messageDb.length > 0) {
const sql = `DELETE
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)
}
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,
// 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'
)
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, op: 'increment' | 'decrement', lastReply?: Date): Promise<void> {
const set: string[] = []
const values: any[] = []
if (lastReply != null) {
set.push('last_reply = $3::timestamptz')
values.push(lastReply)
}
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')
}
if (op === 'increment') {
set.push('replies_count = replies_count + 1')
} else if (op === 'decrement') {
set.push('replies_count = GREATEST(replies_count - 1, 0)')
}
// 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
}
const update = `UPDATE ${TableName.Thread}`
const setSql = 'SET ' + set.join(', ')
const where = 'WHERE workspace_id = $1::uuid AND thread_id = $2::varchar'
const sql = [update, setSql, where].join(' ')
await this.execute(sql, [this.workspace, thread, ...values], 'update thread')
}
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'
)
// MessagesGroup
async createMessagesGroup (card: CardID, blobId: BlobID, fromSec: Date, toSec: Date, count: number): Promise<void> {
const db: MessagesGroupDb = {
workspace_id: this.workspace,
card_id: card,
blob_id: blobId,
from_sec: fromSec,
to_sec: toSec,
count
}
async removeMessagesGroup(card: CardID, blobId: BlobID): Promise<void> {
const sql = `DELETE
const sql = `INSERT INTO ${TableName.MessagesGroup} (workspace_id, card_id, blob_id, from_sec, to_sec, count)
VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz, $6::int)`
await this.execute(
sql,
[db.workspace_id, db.card_id, db.blob_id, db.from_sec, db.to_sec, db.count],
'insert messages group'
)
}
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')
await this.execute(sql, [this.workspace, card, blobId], 'remove messages group')
}
async find(params: FindMessagesParams): Promise<Message[]> {
const { where, values } = this.buildMessageWhere(params);
const orderBy = this.buildOrderBy(params);
const limit = this.buildLimit(params);
const sql = `
WITH
${this.buildCteLimitedMessages(where, orderBy, limit)}
${this.buildCteAggregatedFiles(params)}
${this.buildCteAggregatedReactions(params)}
${this.buildCteAggregatedPatches()}
${this.buildMainSelect(params)}
`;
const result = await this.execute(sql, values, 'find messages');
return result.map((it: any) => toMessage(it));
}
private buildOrderBy(params: FindMessagesParams): string {
return params.order != null
? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}`
: '';
}
private buildLimit(params: FindMessagesParams): string {
return params.limit != null ? `LIMIT ${params.limit}` : '';
}
private buildCteLimitedMessages(where: string, orderBy: string, limit: string): string {
return `
limited_messages AS (
SELECT *
FROM ${TableName.Message} m
${where}
${orderBy}
${limit}
)
`;
}
private buildCteAggregatedFiles(params: FindMessagesParams): string {
if (!params.files) return '';
return `,
agg_files AS (
SELECT
f.workspace_id,
f.card_id,
f.message_id,
jsonb_agg(jsonb_build_object(
'card_id', f.card_id,
'message_id', f.message_id,
'blob_id', f.blob_id,
'type', f.type,
'filename', f.filename,
'creator', f.creator,
'created', f.created
)) AS files
FROM ${TableName.File} f
INNER JOIN limited_messages m
ON m.workspace_id = f.workspace_id
AND m.card_id = f.card_id
AND m.id = f.message_id
GROUP BY f.workspace_id, f.card_id, f.message_id
)
`;
}
private buildCteAggregatedReactions(params: FindMessagesParams): string {
if (!params.reactions) return '';
return `,
agg_reactions AS (
SELECT
r.workspace_id,
r.card_id,
r.message_id,
jsonb_agg(jsonb_build_object(
'message_id', r.message_id,
'reaction', r.reaction,
'creator', r.creator,
'created', r.created
)) AS reactions
FROM ${TableName.Reaction} r
INNER JOIN limited_messages m
ON m.workspace_id = r.workspace_id
AND m.card_id = r.card_id
AND m.id = r.message_id
GROUP BY r.workspace_id, r.card_id, r.message_id
)
`;
}
private buildCteAggregatedPatches(): string {
return `,
agg_patches AS (
SELECT
p.workspace_id,
p.card_id,
p.message_id,
jsonb_agg(
jsonb_build_object(
'content', p.content,
'creator', p.creator,
'created', p.created
) ORDER BY p.created DESC
) AS patches
FROM ${TableName.Patch} p
INNER JOIN limited_messages m
ON m.workspace_id = p.workspace_id
AND m.card_id = p.card_id
AND m.id = p.message_id
WHERE p.type = 'update'
GROUP BY p.workspace_id, p.card_id, p.message_id
)
`;
}
private buildMainSelect(params: FindMessagesParams): string {
const orderBy = this.buildOrderBy(params);
const selectReplies = params.replies
? `t.thread_id as thread_id, t.replies_count as replies_count, t.last_reply as last_reply,`
: '';
const selectFiles = params.files
? `COALESCE(f.files, '[]'::jsonb) AS files,`
: `'[]'::jsonb AS files,`;
const selectReactions = params.reactions
? `COALESCE(r.reactions, '[]'::jsonb) AS reactions,`
: `'[]'::jsonb AS reactions,`;
const joinFiles = params.files ? `
LEFT JOIN agg_files f
ON f.workspace_id = m.workspace_id
AND f.card_id = m.card_id
AND f.message_id = m.id` : '';
const joinReactions = params.reactions ? `
LEFT JOIN agg_reactions r
ON r.workspace_id = m.workspace_id
AND r.card_id = m.card_id
AND r.message_id = m.id` : '';
return `
SELECT
m.id,
m.card_id,
m.type,
m.content,
m.creator,
m.created,
m.data,
${selectReplies}
${selectFiles}
${selectReactions}
COALESCE(p.patches, '[]'::jsonb) AS patches
FROM limited_messages m
LEFT JOIN ${TableName.Thread} t
ON t.workspace_id = m.workspace_id
AND t.card_id = m.card_id
AND t.message_id = m.id
${joinFiles}
${joinReactions}
LEFT JOIN agg_patches p
ON p.workspace_id = m.workspace_id
AND p.card_id = m.card_id
AND p.message_id = m.id
${orderBy}
`;
}
buildMessageWhere (params: FindMessagesParams): { where: string, values: any[] } {
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)
}
// 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))
if (params.card != null) {
where.push(`m.card_id = $${index++}::varchar`)
values.push(params.card)
}
buildMessageWhere(params: FindMessagesParams): { where: string, values: any[] } {
const where: string[] = ['m.workspace_id = $1::uuid']
const values: any[] = [this.workspace]
const createdCondition = getCondition('m', 'created', index, params.created, 'timestamptz')
let index = 2
if (params.id != null) {
where.push(`m.id = $${index++}::bigint`)
values.push(params.id)
}
if (params.card != null) {
where.push(`m.card_id = $${index++}::varchar`)
values.push(params.card)
}
const createdCondition = getCondition('m', 'created', index, params.created, 'timestamptz')
if (createdCondition != null) {
where.push(createdCondition.where)
values.push(createdCondition.value)
index++
}
return {where: `WHERE ${where.join(' AND ')}`, values}
if (createdCondition != null) {
where.push(createdCondition.where)
values.push(...createdCondition.values)
index = createdCondition.index
}
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`
}
return { where: `WHERE ${where.join(' AND ')}`, values }
}
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,
// Find thread
async findThread (thread: CardID): Promise<Thread | undefined> {
const sql = `SELECT t.card_id,
t.message_id,
t.thread_id,
t.replies_count,
@@ -418,19 +504,17 @@ export class MessagesDb extends BaseDb {
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]
}
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 = `
// 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.from_sec,
mg.to_sec,
mg.count,
patches
FROM ${TableName.MessagesGroup} mg
@@ -445,57 +529,57 @@ export class MessagesDb extends BaseDb {
FROM ${TableName.Patch} p
WHERE 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
AND p.message_created_sec BETWEEN mg.from_sec AND mg.to_sec
) sub`
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 limit = params.limit != null ? ` LIMIT ${params.limit}` : '';
const { where, values } = this.buildMessagesGroupWhere(params)
const orderBy =
params.orderBy === 'toSec'
? `ORDER BY mg.to_sec ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}`
: `ORDER BY mg.from_sec ${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 groups')
const sql = [select, where, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find messages groups')
return result.map((it: any) => toMessagesGroup(it))
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
where.push(`mg.card_id = $${index++}::varchar`)
values.push(params.card)
if (params.blobId != null) {
where.push(`mg.blob_id = $${index++}`)
values.push(params.blobId)
}
buildMessagesGroupWhere(params: FindMessagesGroupsParams): {
where: string
values: any[]
} {
const where: string[] = ['mg.workspace_id = $1::uuid']
const values: any[] = [this.workspace]
let index = 2
where.push(`mg.card_id = $${index++}::varchar`)
values.push(params.card)
if (params.blobId != null) {
where.push(`mg.blob_id = $${index++}`)
values.push(params.blobId)
}
const fromDateCondition = getCondition('mg', 'from_date', index, params.fromDate, 'timestamptz')
if (fromDateCondition != null) {
where.push(fromDateCondition.where)
values.push(fromDateCondition.value)
index++
}
const toDateCondition = getCondition('mg', 'to_date', index, params.toDate, 'timestamptz')
if (toDateCondition != null) {
where.push(toDateCondition.where)
values.push(toDateCondition.value)
index++
}
if (params.withPatches === true) {
where.push(`sub.patches IS NOT NULL`)
}
return {where: `WHERE ${where.join(' AND ')}`, values}
const fromDateCondition = getCondition('mg', 'from_sec', index, params.fromSec, 'timestamptz')
if (fromDateCondition != null) {
where.push(fromDateCondition.where)
values.push(...fromDateCondition.values)
index = fromDateCondition.index
}
const toDateCondition = getCondition('mg', 'to_sec', index, params.toSec, 'timestamptz')
if (toDateCondition != null) {
where.push(toDateCondition.where)
values.push(...toDateCondition.values)
index = toDateCondition.index
}
if (params.patches === true) {
where.push('sub.patches IS NOT NULL')
}
return { where: `WHERE ${where.join(' AND ')}`, values }
}
}
+361 -176
View File
@@ -14,82 +14,140 @@
//
import {
type MessageID,
type ContextID,
type AccountID,
type CardID,
type NotificationContext,
type Collaborator,
type ContextID,
type FindCollaboratorsParams,
type FindNotificationContextParams,
SortingOrder,
type FindNotificationsParams,
type MessageID,
type Notification,
type NotificationContextUpdate,
type WorkspaceID
type NotificationContext,
SortingOrder,
type NotificationID
} from '@hcengineering/communication-types'
import { BaseDb } from './base'
import { TableName, type ContextDb, type NotificationDb } from './schema'
import { type ContextDb, type NotificationDb, TableName } from './schema'
import { getCondition } from './utils'
import { toCollaborator, toNotification, toNotificationContext } from './mapping'
export class NotificationsDb extends BaseDb {
async createNotification (message: MessageID, context: ContextID): Promise<void> {
const db: NotificationDb = {
message_id: message,
context
async addCollaborators (card: CardID, collaborators: AccountID[], date?: Date): Promise<void> {
if (collaborators.length === 0) return
const values: any[] = []
const sqlValues = collaborators
.map((account, index) => {
const i = index * 3
values.push(this.workspace, card, account, date ?? new Date())
return `($${i + 1}::uuid, $${i + 2}::varchar, $${i + 3}::uuid, $${i + 4}::timestamptz)`
})
.join(', ')
const sql = `INSERT INTO ${TableName.Collaborators} (workspace_id, card_id, account, date) VALUES ${sqlValues} ON CONFLICT DO NOTHING`
await this.execute(sql, values, 'insert collaborators')
}
async removeCollaborators (card: CardID, collaborators: AccountID[]): Promise<void> {
if (collaborators.length === 0) return
if (collaborators.length === 1) {
const sql = `DELETE
FROM ${TableName.Collaborators}
WHERE workspace_id = $1::uuid
AND card_id = $2::varchar
AND account = $3::uuid`
await this.execute(sql, [this.workspace, card, collaborators[0]], 'remove collaborator')
} else {
const inValues = collaborators.map((_, index) => `$${index + 3}`).join(', ')
const sql = `DELETE
FROM ${TableName.Collaborators}
WHERE workspace_id = $1::uuid
AND card_id = $2::varchar
AND account IN (${inValues})`
await this.execute(sql, [this.workspace, card, ...collaborators], 'remove collaborators')
}
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,
getCollaboratorsCursor (
card: CardID,
lastView?: Date,
lastUpdate?: Date
): Promise<ContextID> {
date: Date,
size?: number
): AsyncIterable<NonNullable<Collaborator[][number]>[]> {
const sql = `
SELECT account
FROM ${TableName.Collaborators}
WHERE workspace_id = $1::uuid
AND card_id = $2::varchar
AND date <= $3::timestamptz
ORDER BY date ASC `
return this.client.cursor<Collaborator>(sql, [this.workspace, card, date], size)
}
async createNotification (context: ContextID, message: MessageID, created: Date): Promise<NotificationID> {
const db: Omit<NotificationDb, 'id'> = {
message_id: message,
context_id: context,
created
}
const sql = `INSERT INTO ${TableName.Notification} (message_id, context_id, created)
VALUES ($1::bigint, $2::int8, $3::timestamptz)
RETURNING id`
const result = await this.execute(sql, [db.message_id, db.context_id, db.created], 'insert notification')
return result[0].id as NotificationID
}
async removeNotifications (context: ContextID, account: AccountID, untilDate: Date): Promise<void> {
const sql = `
DELETE FROM ${TableName.Notification} n
USING ${TableName.NotificationContext} nc
WHERE n.context_id = $1::int8
AND nc.id = n.context_id
AND nc.account = $2::uuid
AND n.created < $3::timestamptz
`
await this.execute(sql, [context, account, untilDate], 'remove notification')
}
async createContext (account: AccountID, card: CardID, lastUpdate: Date, lastView: Date): Promise<ContextID> {
const db: ContextDb = {
workspace_id: this.workspace,
card_id: card,
personal_workspace: personalWorkspace,
account,
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 sql = `INSERT INTO ${TableName.NotificationContext} (workspace_id, card_id, account, 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],
[db.workspace_id, db.card_id, db.account, db.last_view, db.last_update],
'insert notification context'
)
return result[0].id as ContextID
}
async removeContext (context: ContextID): Promise<void> {
async removeContext (context: ContextID, account: AccountID): Promise<void> {
const sql = `DELETE
FROM ${TableName.Notification}
WHERE context = $1::uuid`
await this.execute(sql, [context], 'remove notification context')
FROM ${TableName.Notification}
WHERE context = $1::int8 AND account = $2::uuid`
await this.execute(sql, [context, account], 'remove notification context')
}
async updateContext (context: ContextID, update: NotificationContextUpdate): Promise<void> {
async updateContext (context: ContextID, account: AccountID, lastUpdate?: Date, lastView?: Date): Promise<void> {
const dbData: Partial<ContextDb> = {}
if (update.archivedFrom != null) {
dbData.archived_from = update.archivedFrom
if (lastView != null) {
dbData.last_view = lastView
}
if (update.lastView != null) {
dbData.last_view = update.lastView
}
if (update.lastUpdate != null) {
dbData.last_update = update.lastUpdate
if (lastUpdate != null) {
dbData.last_update = lastUpdate
}
if (Object.keys(dbData).length === 0) {
@@ -100,91 +158,236 @@ export class NotificationsDb extends BaseDb {
const values = Object.values(dbData)
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')
SET ${keys.map((k, idx) => `"${k}" = $${idx + 3}::timestamptz`).join(', ')}
WHERE id = $1::int8 AND account = $2::uuid;`
await this.execute(sql, [context, account, ...values], 'update notification context')
}
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 != null ? ` LIMIT ${params.limit}` : ''
const sql = [select, where, limit].join(' ')
async findContexts (params: FindNotificationContextParams): Promise<NotificationContext[]> {
const withNotifications = params.notifications != null
const withMessages = params.notifications?.message === true
const result = await this.execute(sql, values, 'find notification contexts')
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 = `
SELECT n.message_id,
n.context,
m.card_id AS message_card,
m.content AS message_content,
m.creator AS message_creator,
m.created AS message_created,
nc.card_id,
nc.archived_from,
nc.last_view,
nc.last_update,
(SELECT json_agg(
jsonb_build_object(
'id', p.id,
'content', p.content,
'creator', p.creator,
'created', p.created
)
)
FROM ${TableName.Patch} p
WHERE p.message_id = m.id) AS patches
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 { where, values, index } = this.buildContextWhere(params)
const limit = params.limit != null ? `LIMIT ${Number(params.limit)}` : ''
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(' ')
params.order != null ? `ORDER BY nc.last_update ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
let joinMessages = ''
let buildObject = `
JSONB_BUILD_OBJECT(
'id', n.id,
'created', n.created,
'message_id', n.message_id
)`
if (withMessages) {
joinMessages = `
LEFT JOIN ${TableName.Message} m
ON n.message_id = m.id
AND nc.workspace_id = m.workspace_id
AND nc.card_id = m.card_id
LEFT JOIN ${TableName.MessagesGroup} mg
ON nc.workspace_id = mg.workspace_id
AND nc.card_id = mg.card_id
AND n.created BETWEEN mg.from_sec AND mg.to_sec`
buildObject = `
JSONB_BUILD_OBJECT(
'id', n.id,
'created', n.created,
'message_id', n.message_id,
'message_type', m.type,
'message_content', m.content,
'message_data', m.data,
'message_creator', m.creator,
'message_created', m.created,
'message_group_blob_id', mg.blob_id,
'message_group_from_sec', mg.from_sec,
'message_group_to_sec', mg.to_sec,
'message_group_count', mg.count,
'message_patches', (
SELECT COALESCE(
JSON_AGG(
JSONB_BUILD_OBJECT(
'patch_type', p.type,
'patch_content', p.content,
'patch_creator', p.creator,
'patch_created', p.created
) ORDER BY p.created DESC
),
'[]'::JSONB
)
FROM ${TableName.Patch} p
WHERE p.message_id = n.message_id
)
)`
}
let joinNotifications = ''
let notificationsSelect = ''
let groupBy = ''
if (withNotifications) {
const { where: whereNotifications, values: valuesNotifications } = this.buildNotificationWhere(
{ read: params.notifications?.read },
index,
true
)
values.push(...valuesNotifications)
joinNotifications = `
LEFT JOIN LATERAL (
SELECT
n.*,
ROW_NUMBER() OVER (
PARTITION BY n.context_id
ORDER BY n.created ${params.notifications?.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}
) AS rn
FROM ${TableName.Notification} n
${whereNotifications} ${whereNotifications.length > 1 ? 'AND n.context_id = nc.id' : 'WHERE n.context_id = nc.id'}
) n ON n.rn <= ${params.notifications?.limit ?? 1}`
notificationsSelect = `,
COALESCE(
JSON_AGG(
${buildObject}
ORDER BY n.created ${params.notifications?.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}
),
'[]'::JSONB
) AS notifications`
groupBy = 'GROUP BY nc.id'
}
const sql = `
SELECT nc.id,
nc.card_id,
nc.account,
nc.last_view,
nc.last_update
${notificationsSelect}
FROM ${TableName.NotificationContext} nc
${joinNotifications}
${joinMessages}
${where}
${groupBy}
${orderBy}
${limit};
`
const result = await this.execute(sql, values, 'find contexts')
return result.map((it: any) => toNotificationContext(it))
}
async findNotifications (params: FindNotificationsParams): Promise<Notification[]> {
const withMessage = params.message === true
let select = 'SELECT n.id, n.created,n.message_id, n.context_id, nc.last_view '
let joinMessages = ''
if (withMessage) {
select += `,
m.card_id AS card_id,
m.type AS message_type,
m.content AS message_content,
m.creator AS message_creator,
m.created AS message_created,
m.data AS message_data,
mg.blob_id AS message_group_blob_id,
mg.from_sec AS message_group_from_sec,
mg.to_sec AS message_group_to_sec,
mg.count AS message_group_count,
(SELECT json_agg(
jsonb_build_object(
'id', p.id,
'content', p.content,
'creator', p.creator,
'created', p.created
)
)
FROM ${TableName.Patch} p
WHERE p.message_id = m.id) AS message_patches
`
joinMessages = `
LEFT JOIN ${TableName.Message} m
ON n.message_id = m.id
AND nc.workspace_id = m.workspace_id
AND nc.card_id = m.card_id
LEFT JOIN ${TableName.MessagesGroup} mg
ON nc.workspace_id = mg.workspace_id
AND nc.card_id = mg.card_id
AND n.created BETWEEN mg.from_sec AND mg.to_sec
`
}
select += ` FROM ${TableName.Notification} n
JOIN ${TableName.NotificationContext} nc ON n.context_id = nc.id`
const { where, values } = this.buildNotificationWhere(params)
const orderBy =
params.order != null ? `ORDER BY n.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
const limit = params.limit != null ? `LIMIT ${params.limit}` : ''
const sql = [select, joinMessages, where, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find notifications')
return result.map((it) => this.toNotification(it))
return result.map((it: any) => toNotification(it))
}
buildContextWhere (
params: FindNotificationContextParams,
personalWorkspaces: WorkspaceID[],
workspace?: WorkspaceID
): {
where: string
values: any[]
} {
const where: string[] = []
const values: any[] = []
let index = 1
async findCollaborators (params: FindCollaboratorsParams): Promise<Collaborator[]> {
const { where, values } = this.buildCollaboratorsWhere(params)
const select = `
SELECT c.account
FROM ${TableName.Collaborators} c
`
if (workspace != null) {
where.push(`nc.workspace_id = $${index++}::uuid`)
values.push(workspace)
const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''
const orderBy =
params.order != null ? `ORDER BY c.date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
const sql = [select, where, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find collaborators')
return result.map((it: any) => toCollaborator(it))
}
private buildCollaboratorsWhere (params: FindCollaboratorsParams): { where: string, values: any[] } {
const where: string[] = ['c.workspace_id = $1::uuid', 'c.card_id = $2::varchar']
const values: any[] = [this.workspace, params.card]
let index = values.length + 1
if (params.account != null) {
const accounts = Array.isArray(params.account) ? params.account : [params.account]
if (accounts.length === 1) {
where.push(`c.account = $${index++}::uuid`)
values.push(accounts[0])
} else if (accounts.length > 1) {
where.push(`c.account = ANY($${index++}::uuid[])`)
values.push(accounts)
}
}
if (personalWorkspaces.length > 0) {
where.push(`nc.personal_workspace IN (${personalWorkspaces.map((it) => `$${index++}::uuid`).join(', ')})`)
values.push(...personalWorkspaces)
return { where: `WHERE ${where.join(' AND ')}`, values }
}
private buildContextWhere (params: FindNotificationContextParams): {
where: string
values: any[]
index: number
} {
const where: string[] = ['nc.workspace_id = $1::uuid']
const values: any[] = [this.workspace]
let index = 2
if (params.id != null) {
where.push(`nc.id = $${index++}::int8`)
values.push(params.id)
}
if (params.card != null) {
@@ -192,82 +395,64 @@ export class NotificationsDb extends BaseDb {
values.push(params.card)
}
return { where: `WHERE ${where.join(' AND ')}`, values }
if (params.account != null) {
const accounts = Array.isArray(params.account) ? params.account : [params.account]
if (accounts.length === 1) {
where.push(`nc.account = $${index++}::uuid`)
values.push(accounts[0])
} else if (accounts.length > 1) {
where.push(`nc.account IN (${accounts.map((it) => `$${index++}::uuid`).join(', ')})`)
values.push(...accounts)
}
}
const lastUpdateCondition = getCondition('nc', 'last_update', index, params.lastUpdate, 'timestamptz')
if (lastUpdateCondition != null) {
where.push(lastUpdateCondition.where)
values.push(...lastUpdateCondition.values)
index= lastUpdateCondition.index
}
return { where: `WHERE ${where.join(' AND ')}`, values, index }
}
buildNotificationWhere (
private buildNotificationWhere (
params: FindNotificationsParams,
personalWorkspace: WorkspaceID,
workspace?: WorkspaceID
initialIndex?: number,
skipWorkspace?: boolean
): {
where: string
values: any[]
} {
const where: string[] = ['nc.personal_workspace = $1::uuid']
const values: any[] = [personalWorkspace]
let index = 2
if (workspace != null) {
where.push(`nc.workspace_id = $${index++}::uuid`)
values.push(workspace)
}
const where: string[] = skipWorkspace === true ? [] : ['nc.workspace_id = $1::uuid']
const values: any[] = skipWorkspace === true ? [] : [this.workspace]
let index = (initialIndex ?? 0) + values.length + 1
if (params.context != null) {
where.push(`n.context = $${index++}::uuid`)
where.push(`n.context_id = $${index++}::int8`)
values.push(params.context)
}
if (params.account != null) {
const accounts = Array.isArray(params.account) ? params.account : [params.account]
if (accounts.length === 1) {
where.push(`nc.account = $${index++}::uuid`)
values.push(accounts[0])
} else if (accounts.length > 1) {
where.push(`nc.account = ANY ($${index++}::uuid[])`)
values.push(accounts)
}
}
if (params.read === true) {
where.push('nc.last_view IS NOT NULL AND nc.last_view >= m.created')
where.push('nc.last_view IS NOT NULL AND nc.last_view >= n.created')
}
if (params.read === false) {
where.push('(nc.last_view IS NULL OR nc.last_view > m.created)')
where.push('(nc.last_view IS NULL OR nc.last_view < n.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
}
return { where: where.length > 0 ? `WHERE ${where.join(' AND ')}` : '', values }
}
}
+34 -107
View File
@@ -14,53 +14,48 @@
//
import {
type AccountID,
type BlobID,
type CardID,
type ContextID,
type MessageID,
type MessageType,
type PatchType,
type RichText,
type SocialID,
type CardID,
type BlobID,
type Message,
type Reaction,
type Attachment,
type MessagesGroup,
type WorkspaceID,
type PatchType,
type Patch,
type Thread
type NotificationID
} from '@hcengineering/communication-types'
export enum TableName {
Attachment = 'communication.attachments',
File = 'communication.files',
Message = 'communication.messages',
MessagesGroup = 'communication.messages_groups',
Notification = 'communication.notifications',
NotificationContext = 'communication.notification_context',
Patch = 'communication.patch',
Reaction = 'communication.reactions',
Thread = 'communication.thread'
Thread = 'communication.thread',
Collaborators = 'communication.collaborators'
}
export interface MessageDb {
id: MessageID
type: MessageType
workspace_id: WorkspaceID
card_id: CardID
content: RichText
creator: SocialID
created: Date
thread_id?: CardID
replies_count?: number
last_reply?: Date
data?: any
}
export interface MessagesGroupDb {
workspace_id: WorkspaceID
card_id: CardID
blob_id: BlobID
from_date: Date
to_date: Date
from_id: MessageID
to_id: MessageID
from_sec: Date
to_sec: Date
count: number
patches?: PatchDb[]
}
@@ -73,6 +68,7 @@ export interface PatchDb {
content: RichText
creator: SocialID
created: Date
message_created_sec: Date
}
export interface ReactionDb {
@@ -84,11 +80,17 @@ export interface ReactionDb {
created: Date
}
export interface AttachmentDb {
message_id: MessageID
export interface FileDb {
workspace_id: WorkspaceID
card_id: CardID
message_id: MessageID
blob_id: BlobID
filename: string
size: number
type: string
creator: SocialID
created: Date
message_created_sec: Date
}
export interface ThreadDb {
@@ -101,98 +103,23 @@ export interface ThreadDb {
}
export interface NotificationDb {
message_id: MessageID
context: ContextID
id: NotificationID
message_id: MessageID | null
context_id: ContextID
created: Date
}
export interface ContextDb {
workspace_id: WorkspaceID
card_id: CardID
personal_workspace: WorkspaceID
archived_from?: Date
last_view?: Date
last_update?: Date
account: AccountID
last_update: Date
last_view: Date
}
interface RawMessage extends MessageDb {
patches?: PatchDb[]
attachments?: AttachmentDb[]
reactions?: ReactionDb[]
}
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: String(raw.message_id) as MessageID,
reaction: raw.reaction,
creator: raw.creator,
created: 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,
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
}
export interface CollaboratorDb {
workspace_id: WorkspaceID
card_id: CardID
account: AccountID
date: Date
}
+32 -16
View File
@@ -13,31 +13,47 @@
// limitations under the License.
//
export function getCondition (
export function getCondition(
table: string,
dbField: string,
index: number,
startIndex: 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 }
): { where: string, values: any[], index: number } | undefined {
const conditions: string[] = [];
const values: any[] = [];
let index = startIndex;
if (param !== null && typeof param === 'object') {
if (param.less !== undefined) {
conditions.push(`${table}.${dbField} < $${index}::${type}`);
values.push(param.less);
index++;
}
if (param.lessOrEqual != null) {
return { where: `${table}.${dbField} <= $${index}::${type}`, value: param.lessOrEqual }
if (param.lessOrEqual !== undefined) {
conditions.push(`${table}.${dbField} <= $${index}::${type}`);
values.push(param.lessOrEqual);
index++;
}
if (param.greater != null) {
return { where: `${table}.${dbField} > $${index}::${type}`, value: param.greater }
if (param.greater !== undefined) {
conditions.push(`${table}.${dbField} > $${index}::${type}`);
values.push(param.greater);
index++;
}
if (param.greaterOrEqual != null) {
return { where: `${table}.${dbField} >= $${index}::${type}`, value: param.greaterOrEqual }
if (param.greaterOrEqual !== undefined) {
conditions.push(`${table}.${dbField} >= $${index}::${type}`);
values.push(param.greaterOrEqual);
index++;
}
}
if (param != null) {
return { where: `${table}.${dbField} = $${index}::${type}`, value: param }
if (param != null && conditions.length === 0) {
conditions.push(`${table}.${dbField} = $${index}::${type}`);
values.push(param);
index++;
}
return undefined
}
if (conditions.length === 0) return undefined;
return { where: conditions.join(' AND '), values, index };
}
+5 -1
View File
@@ -15,6 +15,9 @@
import { type ParameterOrJSON, type Row } from 'postgres'
export type SqlRow = Row & Iterable<Row>
export type SqlParams = ParameterOrJSON<any>[]
export interface Logger {
info: (message: string, data?: Record<string, any>) => void
warn: (message: string, data?: Record<string, any>) => void
@@ -26,6 +29,7 @@ export interface Options {
}
export interface SqlClient {
execute: <T extends any[] = (Row & Iterable<Row>)[]>(query: string, params?: ParameterOrJSON<any>[]) => Promise<T>
execute: <T = SqlRow>(query: string, params?: SqlParams) => Promise<T[]>
cursor: <T = SqlRow>(query: string, params?: SqlParams, size?: number) => AsyncIterable<NonNullable<T[][number]>[]>
close: () => void
}
+2
View File
@@ -44,6 +44,8 @@ function escape(value: any): string {
return `'${value.replace(/'/g, "''")}'`
case 'bigint':
return value.toString()
case 'object':
return `'${JSON.stringify(value)}'`
default:
throw new Error(`Unsupported value type: ${typeof value}`)
}
-26
View File
@@ -1,26 +0,0 @@
{
"name": "@hcengineering/communication-examples",
"version": "0.1.61",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
"files": [
"dist/index.js",
"dist/index.cjs",
"types/**/*.d.ts"
],
"scripts": {
"bundle": "bun run bundle:browser",
"bundle:browser": "bun build src/index.ts --outdir dist --target browser"
},
"devDependencies": {
"@types/bun": "^1.1.14"
},
"dependencies": {
"@hcengineering/communication-client-ws": "workspace:*",
"@hcengineering/communication-types": "workspace:*"
},
"peerDependencies": {
"typescript": "^5.6.3"
}
}
-70
View File
@@ -1,70 +0,0 @@
Bun.serve({
port: 3003,
fetch(req) {
const url = new URL(req.url)
const headers = {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp'
}
if (url.pathname === '/') {
return new Response(Bun.file('./index.html'), { headers })
}
if (url.pathname.endsWith('index.js')) {
return new Response(Bun.file('../dist/index.js'), { headers })
}
if (url.pathname.endsWith('sqlite3-opfs-async-proxy.js')) {
return new Response(
Bun.file('../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js'),
{
headers
}
)
}
if (url.pathname.endsWith('index.mjs')) {
return new Response(Bun.file('../../../node_modules/@sqlite.org/sqlite-wasm/index.mjs'), {
headers
})
}
if (url.pathname.endsWith('sqlite3-worker1-promiser.mjs')) {
return new Response(
Bun.file('../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-worker1-promiser.mjs'),
{
headers
}
)
}
if (url.pathname.endsWith('sqlite3-worker1-bundler-friendly.mjs')) {
return new Response(
Bun.file(
'../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-worker1-bundler-friendly.mjs'
),
{
headers
}
)
}
if (url.pathname.endsWith('sqlite3.js')) {
return new Response(Bun.file('../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.js'), {
headers
})
}
if (url.pathname.endsWith('.wasm')) {
return new Response(Bun.file('../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm'), {
headers: {
...headers,
ContentType: 'application/wasm'
}
})
}
return new Response('Not Found', { status: 404, headers })
}
})
-51
View File
@@ -1,51 +0,0 @@
<html>
<head>
<style>
.chat {
width: 100%;
height: 500px;
max-height: 500px;
display: flex;
flex-direction: column;
border: 1px solid black;
margin-bottom: 20px;
overflow: scroll;
}
.messages{
height: 100%;
display: flex;
flex-direction: column;
padding: 0 20px;
}
.message {
width: 100%;
height: 30px;
min-height: 30px;
display: flex;
align-items: center;
justify-content: space-between;
}
.buttons {
display: flex;
gap: 10px;
}
</style>
</head>
<body>
<div class="chat" id="chat">
<div class="messages" id="messages"></div>
</div>
<form class="chat-box" id="form">
<input name="text"/>
<button id='submit-button' type="submit">Send</button>
</form>
<button id='backward-button' type="button">Backward</button>
<button id='forward-button' type="button">Forward</button>
</body>
<script type="module" crossorigin src="../dist/index.js"></script>
</html>
-126
View File
@@ -1,126 +0,0 @@
import {
type Message,
type SocialID,
SortingOrder,
type CardID,
type Window,
type WorkspaceID
} from '@hcengineering/communication-types'
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 creator1 = 'email:vasya@huly.com' as SocialID
export async function example() {
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, order: SortingOrder.Descending }, (res) => {
window = res
const r = window.getResult()
r.reverse()
showMessages(r)
})
document.getElementById('forward-button')?.addEventListener('click', async () => {
if (window == null) return
await window.loadNextPage()
})
document.getElementById('backward-button')?.addEventListener('click', async () => {
if (window == null) return
await window.loadPrevPage()
})
async function editMessage(message: Message) {
await client.updateMessage(card, message.id, message.content + '_1_', creator1)
}
async function deleteMessage(message: Message) {
await client.removeMessage(card, message.id)
}
async function addReaction(message: Message) {
await client.createReaction(card, message.id, '👍', creator1)
}
async function removeReaction(message: Message) {
await client.removeReaction(card, message.id, '👍', creator1)
}
function scrollToBottom() {
const el = document.getElementById('chat')
if (el == null) return
el.scrollTo(0, el.scrollHeight)
}
async function showMessages(messages: ReadonlyArray<Message>) {
const el = document.getElementById('messages')
if (el == null) return
el.innerHTML = ''
for (const message of messages) {
const div = el.appendChild(document.createElement('div'))
div.className = 'message'
const messageContent = document.createElement('span')
messageContent.textContent = message.content + ' ' + message.reactions.map((it) => it.reaction).join(' ')
// + ' ' + messages.created.getTime()
div.appendChild(messageContent)
const buttonsDiv = document.createElement('div')
buttonsDiv.className = 'buttons'
const editButton = document.createElement('button')
editButton.textContent = 'Edit'
editButton.className = 'edit-button'
editButton.addEventListener('click', () => editMessage(message))
buttonsDiv.appendChild(editButton)
const deleteButton = document.createElement('button')
deleteButton.textContent = 'Remove'
deleteButton.className = 'delete-button'
deleteButton.addEventListener('click', () => deleteMessage(message))
buttonsDiv.appendChild(deleteButton)
const addReactionButton = document.createElement('button')
addReactionButton.textContent = '+R'
addReactionButton.className = 'add-reaction-button'
addReactionButton.addEventListener('click', () => addReaction(message))
buttonsDiv.appendChild(addReactionButton)
const removeReactionButton = document.createElement('button')
removeReactionButton.textContent = '-R'
removeReactionButton.className = 'remove-reaction-button'
removeReactionButton.addEventListener('click', () => removeReaction(message))
buttonsDiv.appendChild(removeReactionButton)
div.appendChild(buttonsDiv)
}
scrollToBottom()
}
document.getElementById('form')?.addEventListener('submit', async (event) => {
event.preventDefault()
// @ts-expect-error error
const el = event.target?.getElementsByTagName('input')[0] as HTMLInputElement
if (el.value == '' || el.value == null) return
await client.createMessage(card, el.value, creator1)
el.value = ''
})
}
void example()
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-query",
"version": "0.1.61",
"version": "0.1.147",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
@@ -21,6 +21,7 @@
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-shared": "workspace:*",
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/communication-yaml": "workspace:*",
"fast-equals": "^5.0.1"
},
"peerDependencies": {
+2 -1
View File
@@ -13,4 +13,5 @@
// limitations under the License.
//
export * from './lq.ts'
export * from './lq'
export type { QueryClient } from './types'
+79 -54
View File
@@ -15,26 +15,33 @@
import {
type FindMessagesParams,
type FindNotificationContextParams,
type FindNotificationsParams,
type WorkspaceID
type Message,
type WorkspaceID,
type Notification,
type NotificationContext
} from '@hcengineering/communication-types'
import { deepEqual } from 'fast-equals'
import type {
MessagesQueryCallback,
NotificationsQueryCallback,
ResponseEvent,
QueryClient
QueryCallback,
RequestEvent,
EventResult
} from '@hcengineering/communication-sdk-types'
import type { FindParams, PagedQuery, QueryId } from './types'
import type { PagedQuery, FindParams, QueryId, QueryClient } from './types'
import { MessagesQuery } from './messages/query'
import { NotificationQuery } from './notifications/query'
import { NotificationContextsQuery } from './notification-contexts/query'
interface CreateQueryResult {
unsubscribe: () => void
}
const maxQueriesCache = 10
const maxQueriesCache = 20
export class LiveQueries {
private readonly queries = new Map<QueryId, PagedQuery>()
@@ -45,27 +52,61 @@ export class LiveQueries {
private readonly client: QueryClient,
private readonly workspace: WorkspaceID,
private readonly filesUrl: string
) {}
) {
this.client.onEvent = (event) => {
void this.onEvent(event)
}
this.client.onRequest = (event, promise) => {
void this.onRequest(event, promise)
}
}
async onEvent(event: ResponseEvent): Promise<void> {
for (const q of this.queries.values()) {
await q.onEvent(event)
void q.onEvent(event)
}
}
async onRequest(event: RequestEvent, promise: Promise<EventResult>): Promise<void> {
for (const q of this.queries.values()) {
void q.onRequest(event, promise)
}
}
queryMessages(params: FindMessagesParams, callback: MessagesQueryCallback): CreateQueryResult {
const query = this.createMessagesQuery(params, callback)
this.queries.set(query.id, query)
return {
unsubscribe: () => {
this.unsubscribeQuery(query)
}
}
return this.createAndStoreQuery<Message, FindMessagesParams, MessagesQuery>(
params,
callback,
MessagesQuery,
(params) => this.findMessagesQuery(params)
)
}
queryNotifications(params: FindNotificationsParams, callback: NotificationsQueryCallback): CreateQueryResult {
const query = this.createNotificationQuery(params, callback)
return this.createAndStoreQuery<Notification, FindNotificationsParams, NotificationQuery>(
params,
callback,
NotificationQuery,
(params) => this.findNotificationQuery(params)
)
}
queryNotificationContexts(params: FindNotificationContextParams, callback: any): CreateQueryResult {
return this.createAndStoreQuery<NotificationContext, FindNotificationContextParams, NotificationContextsQuery>(
params,
callback,
NotificationContextsQuery,
(params) => this.findNotificationContextsQuery(params)
)
}
private createAndStoreQuery<T, P extends FindParams, Q extends PagedQuery<T, P>>(
params: P,
callback: QueryCallback<T>,
QueryClass: new (...args: any[]) => Q,
finder: (params: P) => Q | undefined
): CreateQueryResult {
const query = this.createQuery<T, P, Q>(params, callback, QueryClass, finder)
this.queries.set(query.id, query)
return {
@@ -75,68 +116,51 @@ export class LiveQueries {
}
}
private createMessagesQuery(params: FindMessagesParams, callback: MessagesQueryCallback): MessagesQuery {
private createQuery<T, P, Q extends PagedQuery<T, P>>(
params: P,
callback: any,
QueryClass: new (...args: any[]) => Q,
finder: (params: P) => Q | undefined
): Q {
const id = ++this.counter
const exists = this.findMessagesQuery(params)
const exists = finder(params)
if (exists !== undefined) {
if (this.unsubscribed.has(id)) {
this.unsubscribed.delete(id)
if (this.unsubscribed.has(exists.id)) {
this.unsubscribed.delete(exists.id)
exists.setCallback(callback)
return exists
} else {
const result = exists.copyResult()
return new MessagesQuery(this.client, this.workspace, this.filesUrl, id, params, callback, result)
return new QueryClass(this.client, this.workspace, this.filesUrl, id, params, callback, result)
}
}
return new MessagesQuery(this.client, this.workspace, this.filesUrl, id, params, callback)
return new QueryClass(this.client, this.workspace, this.filesUrl, id, params, callback)
}
private createNotificationQuery(
params: FindNotificationsParams,
callback: NotificationsQueryCallback
): NotificationQuery {
const id = ++this.counter
const exists = this.findNotificationQuery(params)
if (exists !== undefined) {
if (this.unsubscribed.has(id)) {
this.unsubscribed.delete(id)
exists.setCallback(callback)
return exists
} else {
const result = exists.copyResult()
return new NotificationQuery(this.client, id, params, callback, result)
private findQuery<T extends PagedQuery>(params: FindParams, QueryClass: new (...args: any[]) => T): T | undefined {
for (const query of this.queries.values()) {
if (query instanceof QueryClass && this.queryCompare(params, query.params)) {
return query as T
}
}
return new NotificationQuery(this.client, id, params, callback)
}
private findMessagesQuery(params: FindMessagesParams): MessagesQuery | undefined {
for (const query of this.queries.values()) {
if (query instanceof MessagesQuery) {
if (!this.queryCompare(params, query.params)) continue
return query
}
}
return this.findQuery(params, MessagesQuery)
}
private findNotificationQuery(params: FindNotificationsParams): NotificationQuery | undefined {
for (const query of this.queries.values()) {
if (query instanceof NotificationQuery) {
if (!this.queryCompare(params, query.params)) continue
return query
}
}
return this.findQuery(params, NotificationQuery)
}
private findNotificationContextsQuery(params: FindNotificationContextParams): NotificationContextsQuery | undefined {
return this.findQuery(params, NotificationContextsQuery)
}
private queryCompare(q1: FindParams, q2: FindParams): boolean {
if (Object.keys(q1).length !== Object.keys(q2).length) {
return false
}
return deepEqual(q1, q2)
return Object.keys(q1).length === Object.keys(q2).length && deepEqual(q1, q2)
}
private removeOldQueries(): void {
@@ -166,5 +190,6 @@ export class LiveQueries {
close(): void {
this.queries.clear()
this.unsubscribed.clear()
}
}
+306 -191
View File
@@ -16,40 +16,48 @@
import {
type FindMessagesParams,
type Message,
type MessagesGroup,
type WorkspaceID,
type MessageID,
SortingOrder,
type Patch,
type MessagesGroup,
type ParsedFile,
type Patch,
PatchType,
type CardID
type Reaction,
type SocialID,
SortingOrder,
type WorkspaceID,
type File,
type BlobID,
MessageType
} from '@hcengineering/communication-types'
import {
ResponseEventType,
type AttachmentCreatedEvent,
type AttachmentRemovedEvent,
type FileCreatedEvent,
type FileRemovedEvent,
type MessageCreatedEvent,
type MessageRemovedEvent,
type MessagesRemovedEvent,
type PatchCreatedEvent,
type QueryCallback,
type QueryClient,
type ReactionCreatedEvent,
type ReactionRemovedEvent,
type CreateMessageEvent,
type RequestEvent,
RequestEventType,
type ResponseEvent,
type ThreadCreatedEvent
ResponseEventType,
type ThreadCreatedEvent,
type EventResult,
type CreateMessageResult
} from '@hcengineering/communication-sdk-types'
import { loadGroupFile, parseMessageId } from '@hcengineering/communication-shared'
import { applyPatch, applyPatches, generateMessageId, parseMessageId } from '@hcengineering/communication-shared'
import { loadGroupFile } from '@hcengineering/communication-yaml'
import { QueryResult } from '../result'
import { defaultQueryParams, Direction, type QueryId, type PagedQuery } from '../types'
import { defaultQueryParams, Direction, type PagedQuery, type QueryId, type QueryClient } from '../types'
import { WindowImpl } from '../window'
import { addReaction, addReply, removeReaction, removeReply } from './utils'
const GROUPS_LIMIT = 20
export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
protected result: Promise<QueryResult<Message>> | QueryResult<Message>
private result: Promise<QueryResult<Message>> | QueryResult<Message>
private messagesFromFiles: Message[] = []
@@ -70,8 +78,10 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
hasGroups: true
}
private tmpMessages: Map<string, MessageID> = new Map()
constructor (
protected readonly client: QueryClient,
private readonly client: QueryClient,
private readonly workspace: WorkspaceID,
private readonly filesUrl: string,
public readonly id: QueryId,
@@ -107,22 +117,132 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
this.result = new QueryResult([] as Message[], (x) => x.id)
if (this.isInitLoadingForward()) {
this.result.setHead(true)
this.result.setHead(this.params.created == null)
void this.requestLoadNextPage()
} else {
this.result.setTail(true)
this.result.setTail(this.params.created == null)
void this.requestLoadPrevPage()
}
}
}
setCallback (callback: QueryCallback<Message>): void {
this.callback = callback
void this.notify()
async onEvent (event: ResponseEvent): Promise<void> {
switch (event.type) {
case ResponseEventType.MessageCreated: {
await this.onMessageCreatedEvent(event)
break
}
case ResponseEventType.MessagesRemoved: {
await this.onMessagesRemovedEvent(event)
break
}
case ResponseEventType.PatchCreated: {
await this.onPatchCreatedEvent(event)
break
}
case ResponseEventType.ReactionCreated: {
await this.onReactionCreatedEvent(event)
break
}
case ResponseEventType.ReactionRemoved: {
await this.onReactionRemovedEvent(event)
break
}
case ResponseEventType.FileCreated: {
await this.onFileCreatedEvent(event)
break
}
case ResponseEventType.FileRemoved: {
await this.onFileRemovedEvent(event)
break
}
case ResponseEventType.ThreadCreated: {
await this.onThreadCreatedEvent(event)
}
}
}
removeCallback (): void {
this.callback = () => {}
async onRequest (event: RequestEvent, promise: Promise<EventResult>): Promise<void> {
switch (event.type) {
case RequestEventType.CreateMessage: {
await this.onCreateMessageRequest(event, promise as Promise<CreateMessageResult>)
break
}
}
}
async onCreateMessageRequest(
event: CreateMessageEvent,
promise: Promise<CreateMessageResult>
): Promise<void> {
if (this.params.card !== event.card) return;
const eventId = event._id
if(eventId == null) return
const tmpId = generateMessageId();
let resultId: MessageID | undefined;
const tmpMessage: Message = {
id: tmpId,
type: MessageType.Message,
card: event.card,
content: event.content,
creator: event.creator,
created: new Date(),
data: event.data,
edited: undefined,
thread: undefined,
reactions: [],
files: []
};
if (!this.match(tmpMessage)) return;
promise
.then(async (result) => {
this.tmpMessages.delete(eventId)
resultId = result.id;
if (this.result instanceof Promise) this.result = await this.result
if (this.result.get(resultId)) {
if (this.result.delete(tmpId)) {
await this.notify();
}
} else {
const updatedMessage = { ...tmpMessage, id: resultId };
this.result.delete(tmpId);
this.insertMessage(this.result, updatedMessage);
void this.notify();
}
})
.catch(async () => {
if (this.result instanceof Promise) this.result = await this.result
this.tmpMessages.delete(eventId)
if (this.result.delete(tmpId)) {
void this.notify();
}
});
if (this.result instanceof Promise) this.result = await this.result;
if (resultId === undefined && this.result.isTail()) {
this.tmpMessages.set(eventId, tmpId)
this.insertMessage(this.result, tmpMessage);
void this.notify();
}
}
private insertMessage(result: QueryResult<Message>, message: Message): void {
if (this.params.order === SortingOrder.Ascending) {
result.push(message);
} else {
result.unshift(message);
}
}
async unsubscribe (): Promise<void> {
await this.client.unsubscribeQuery(this.id)
}
async requestLoadNextPage (): Promise<void> {
@@ -152,22 +272,31 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
}
}
private isInitLoadingForward (): boolean {
const { order, created, id } = this.params
removeCallback (): void {
this.callback = () => {}
}
if (id != null) {
return false
setCallback (callback: QueryCallback<Message>): void {
this.callback = callback
void this.notify()
}
copyResult (): QueryResult<Message> | undefined {
if (this.result instanceof Promise) {
return undefined
}
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
return this.result.copy()
}
return false
private isInitLoadingForward (): boolean {
const { order, id } = this.params
if (id != null) {
return true
}
return order === SortingOrder.Ascending
}
private async loadPage (direction: Direction, result: QueryResult<Message>): Promise<QueryResult<Message>> {
@@ -190,7 +319,11 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
}
}
result.append(messages)
if (this.params.order === SortingOrder.Ascending && direction === Direction.Backward) {
result.prepend(messages.reverse())
} else {
result.append(messages)
}
return result
}
@@ -229,9 +362,9 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
created:
last != null
? {
greater: last?.created
}
: undefined,
greater: last.created
}
: this.params.created,
limit,
order: SortingOrder.Ascending
})
@@ -271,16 +404,16 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
private async findPrevMessages (limit: number, result: QueryResult<Message>): Promise<Message[]> {
if (!this.prev.hasMessages || result.isHead()) return []
const first = result.getLast()
const first = this.params.order === SortingOrder.Ascending ? result.getFirst() : result.getLast()
return await this.find({
...this.params,
created:
first != null
? {
less: first?.created
}
: undefined,
less: first?.created
}
: this.params.created,
limit,
order: SortingOrder.Descending
})
@@ -302,7 +435,7 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
? currentGroups
: await this.findGroups(
direction,
direction === Direction.Forward ? this.lastGroup?.fromDate : this.firstGroup?.fromDate
direction === Direction.Forward ? this.lastGroup?.fromSec : this.firstGroup?.fromSec
)
if (currentGroups.length === 0) {
@@ -380,7 +513,6 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
private async loadMessagesFromFiles (group: MessagesGroup): Promise<ParsedFile> {
const parsedFile = await loadGroupFile(this.workspace, this.filesUrl, group, { retries: 5 })
const patches = group.patches ?? []
const patchesMap = new Map<MessageID, Patch[]>()
@@ -392,36 +524,24 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
metadata: parsedFile.metadata,
messages:
patches.length > 0
? parsedFile.messages.map((message) => this.applyPatches(message, patchesMap.get(message.id) ?? []))
? parsedFile.messages.map((message) => applyPatches(message, patchesMap.get(message.id) ?? [], this.allowedPatches()))
: parsedFile.messages
}
}
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]
private async findGroupByMessage(id: MessageID): Promise<MessagesGroup | undefined> {
const date = parseMessageId(id);
if (group1 !== undefined) {
return group1
}
return (await this.client.findMessagesGroups({
const groups = await this.client.findMessagesGroups({
card: this.params.card,
fromSec: { lessOrEqual: date },
toSec: { greaterOrEqual: date },
limit: 1,
fromDate: {
lessOrEqual: date
},
order: SortingOrder.Ascending,
orderBy: 'fromDate'
}))[0]
orderBy: 'fromSec'
});
return groups[0];
}
private async findGroups (direction: Direction, fromDate?: Date): Promise<MessagesGroup[]> {
@@ -435,7 +555,7 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
card: this.params.card,
limit: GROUPS_LIMIT,
order: direction === Direction.Forward ? SortingOrder.Ascending : SortingOrder.Descending,
orderBy: 'fromDate'
orderBy: 'fromSec'
})
}
@@ -443,15 +563,15 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
card: this.params.card,
limit: GROUPS_LIMIT,
order: direction === Direction.Forward ? SortingOrder.Ascending : SortingOrder.Descending,
orderBy: 'fromDate',
fromDate:
orderBy: 'fromSec',
fromSec:
direction === Direction.Forward
? {
greater: fromDate
}
greater: fromDate
}
: {
less: fromDate
}
less: fromDate
}
})
}
@@ -466,47 +586,18 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
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)
}
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
}
async onCreateThreadEvent (event: ThreadCreatedEvent): Promise<void> {
private async onThreadCreatedEvent (event: ThreadCreatedEvent): Promise<void> {
if(this.params.replies !== true) return
if (this.params.card !== event.thread.card) return
if (this.result instanceof Promise) this.result = await this.result
@@ -532,7 +623,7 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
})
}
async onCreateMessageEvent (event: MessageCreatedEvent): Promise<void> {
private async onMessageCreatedEvent (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
@@ -542,6 +633,12 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
if (!this.match(message)) return
if (this.result.isTail()) {
const eventId = event._id
if(eventId != null) {
const tmp = this.tmpMessages.get(eventId)
if (tmp) this.result.delete(tmp)
this.tmpMessages.delete(eventId)
}
if (this.params.order === SortingOrder.Ascending) {
this.result.push(message)
} else {
@@ -551,51 +648,50 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
}
}
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> {
private async onPatchCreatedEvent (event: PatchCreatedEvent): Promise<void> {
if (this.params.card !== event.card) return
if (!this.isAllowedPatch(event.patch.type)) 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)
const created = parseMessageId(patch.message)
const groups = this.groupsBuffer.filter((it) => it.fromSec <= created && it.toSec >= created)
if (group != null && group.patches != null) {
group.patches.push(patch)
for (const group of groups) {
if (group.patches != 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))
this.result.update(applyPatch(message, patch, this.allowedPatches()))
await this.notify()
}
}
private async onRemoveMessageEvent (event: MessageRemovedEvent): Promise<void> {
private async onMessagesRemovedEvent (event: MessagesRemovedEvent): 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)
let isDeleted = false
if (deleted !== undefined) {
for (const message of event.messages) {
const deleted = this.result.delete(message)
isDeleted = isDeleted || deleted !== undefined
}
if (isDeleted) {
void this.notify()
}
this.messagesFromFiles = this.messagesFromFiles.filter((it) => it.id !== event.message)
this.messagesFromFiles = this.messagesFromFiles.filter((it) => !event.messages.includes(it.id))
}
private async onCreateReactionEvent (event: ReactionCreatedEvent): Promise<void> {
private async onReactionCreatedEvent (event: ReactionCreatedEvent): Promise<void> {
if(this.params.reactions !== true) return
if (this.result instanceof Promise) this.result = await this.result
if (this.params.card !== event.card) return
@@ -616,7 +712,8 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
}
}
private async onRemoveReactionEvent (event: ReactionRemovedEvent): Promise<void> {
private async onReactionRemovedEvent (event: ReactionRemovedEvent): Promise<void> {
if(this.params.reactions !== true) return
if (this.result instanceof Promise) this.result = await this.result
if (this.params.card !== event.card) return
@@ -633,78 +730,96 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
)
}
private async onCreateAttachmentEvent (event: AttachmentCreatedEvent): Promise<void> {
private async onFileCreatedEvent (event: FileCreatedEvent): Promise<void> {
if(this.params.files !== true) return
if (this.result instanceof Promise) this.result = await this.result
const attachment = {
...event.attachment,
created: event.attachment.created
const { file } = event
const message = this.result.get(file.message)
if (message !== undefined) {
message.files.push(file)
this.result.update(message)
await this.notify()
}
const message = this.result.get(attachment.message)
if (message === undefined) return
message.attachments.push(attachment)
this.result.update(message)
await this.notify()
const fromBuffer = this.messagesFromFiles.find((it) => it.id === file.message)
if (fromBuffer !== undefined) {
addFile(fromBuffer, file)
}
}
private async onRemoveAttachmentEvent (event: AttachmentRemovedEvent): Promise<void> {
private async onFileRemovedEvent (event: FileRemovedEvent): Promise<void> {
if(this.params.files !== true) return
if(this.params.card !== event.card) return
if (this.result instanceof Promise) this.result = await this.result
const message = this.result.get(event.message)
if (message === undefined) return
if (message !== undefined) {
const attachments = message.attachments.filter((it) => it.card !== event.card)
if (attachments.length === message.attachments.length) return
const files = message.files.filter((it) => it.blobId !== event.blobId)
if (files.length === message.files.length) return
const updated = {
...message,
attachments
const updated = {
...message,
files
}
this.result.update(updated)
await this.notify()
}
this.result.update(updated)
await this.notify()
this.messagesFromFiles = this.messagesFromFiles.map((it) =>
it.id === event.message ? removeFile(it, event.blobId) : it
)
}
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)
}
private allowedPatches (): PatchType[] {
const result = [PatchType.update]
return message
if(this.params.reactions === true) {
result.push(PatchType.addReaction, PatchType.removeReaction)
}
if(this.params.files === true) {
result.push(PatchType.addFile, PatchType.removeFile)
}
if(this.params.replies === true) {
result.push(PatchType.addReply, PatchType.removeReply)
}
return result
}
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()
private isAllowedPatch (type: PatchType): boolean {
return this.allowedPatches().includes(type)
}
}
function addFile (message: Message, file:File): Message {
message.files.push(file)
return message
}
function removeFile (message: Message, blobId: BlobID): Message {
const files = message.files.filter((it) => it.blobId !== blobId)
if (files.length === message.files.length) return message
return {
...message,
files
}
}
function addReaction (message: Message, reaction: Reaction): Message {
message.reactions.push(reaction)
return message
}
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
}
}
-69
View File
@@ -1,69 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { CardID, 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
}
}
}
@@ -0,0 +1,510 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type FindNotificationContextParams,
type Notification,
type NotificationContext,
PatchType,
SortingOrder,
type WorkspaceID
} from '@hcengineering/communication-types'
import {
type MessagesRemovedEvent,
type NotificationContextCreatedEvent,
type NotificationContextRemovedEvent,
type NotificationContextUpdatedEvent,
type NotificationCreatedEvent,
type NotificationsRemovedEvent,
type PatchCreatedEvent,
type QueryCallback,
type RequestEvent,
type ResponseEvent,
ResponseEventType
} from '@hcengineering/communication-sdk-types'
import { applyPatch } from '@hcengineering/communication-shared'
import { defaultQueryParams, type PagedQuery, type QueryId, type QueryClient } from '../types'
import { QueryResult } from '../result'
import { WindowImpl } from '../window'
import { loadMessageFromGroup } from '../utils'
const allowedPatchTypes = [PatchType.update, PatchType.addReaction, PatchType.removeReaction]
export class NotificationContextsQuery implements PagedQuery<NotificationContext, FindNotificationContextParams> {
private result: QueryResult<NotificationContext> | Promise<QueryResult<NotificationContext>>
private forward: Promise<NotificationContext[]> | NotificationContext[] = []
private backward: Promise<NotificationContext[]> | NotificationContext[] = []
constructor (
private readonly client: QueryClient,
private readonly workspace: WorkspaceID,
private readonly filesUrl: string,
public readonly id: QueryId,
public readonly params: FindNotificationContextParams,
private callback?: QueryCallback<NotificationContext>,
initialResult?: QueryResult<NotificationContext>
) {
this.params = {
...params,
limit: params.limit,
order: params.order ?? defaultQueryParams.order
}
const limit = params.limit != null ? params.limit + 1 : undefined
const findParams = {
...this.params,
sort: this.params.order ?? defaultQueryParams.order,
limit
}
if (initialResult !== undefined) {
this.result = initialResult
void this.notify()
} else {
const findPromise = this.find(findParams)
this.result = findPromise.then((res) => {
const allLoaded = limit == null || res.length <= limit
const isTail = allLoaded || params.lastUpdate == null
const isHead = allLoaded
if (limit != null && res.length > limit) {
res.pop()
}
const qResult = new QueryResult(res, (x) => x.id)
qResult.setTail(isTail)
qResult.setHead(isHead)
return qResult
})
this.result
.then(async () => {
await this.notify()
})
.catch((err: any) => {
console.error('Failed to update Live query: ', err)
})
}
}
async onEvent (event: ResponseEvent): Promise<void> {
switch (event.type) {
case ResponseEventType.PatchCreated: {
await this.onCreatePatchEvent(event)
break
}
case ResponseEventType.MessagesRemoved: {
await this.onMessagesRemovedEvent(event)
break
}
case ResponseEventType.NotificationCreated: {
await this.onCreateNotificationEvent(event)
break
}
case ResponseEventType.NotificationsRemoved: {
await this.onRemoveNotificationEvent(event)
break
}
case ResponseEventType.NotificationContextCreated: {
await this.onCreateNotificationContextEvent(event)
break
}
case ResponseEventType.NotificationContextUpdated: {
await this.onUpdateNotificationContextEvent(event)
break
}
case ResponseEventType.NotificationContextRemoved: {
await this.onRemoveNotificationContextEvent(event)
}
}
}
async onRequest (event: RequestEvent): Promise<void> {}
async unsubscribe (): Promise<void> {
await this.client.unsubscribeQuery(this.id)
}
async requestLoadNextPage (): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
if (this.forward instanceof Promise) {
this.forward = await this.forward
}
if (this.result.isTail()) return
const last = this.result.getLast()
if (last === undefined) return
const limit = this.params.limit ?? defaultQueryParams.limit
const findParams: FindNotificationContextParams = {
...this.params,
lastUpdate: {
greater: last.lastUpdate
},
limit: limit + 1,
order: SortingOrder.Ascending
}
const forward = this.find(findParams)
this.forward = forward.then(async (res) => {
if (this.result instanceof Promise) {
this.result = await this.result
}
const isTail = res.length <= limit
if (!isTail) {
res.pop()
}
this.result.append(res)
this.result.setTail(isTail)
await this.notify()
return res
})
}
async requestLoadPrevPage (): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
if (this.backward instanceof Promise) {
this.backward = await this.backward
}
if (this.result.isHead()) return
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: FindNotificationContextParams = {
...this.params,
lastUpdate: {
less: first.lastUpdate
},
limit: limit + 1,
order: SortingOrder.Descending
}
const backward = this.find(findParams)
this.backward = backward.then(async (res) => {
if (this.result instanceof Promise) {
this.result = await this.result
}
const isHead = res.length <= limit
if (!isHead) {
res.pop()
}
if (this.params.order === SortingOrder.Ascending) {
const reversed = res.reverse()
this.result.prepend(reversed)
} else {
this.result.append(res)
}
this.result.setHead(isHead)
await this.notify()
return res
})
}
removeCallback (): void {
this.callback = () => {}
}
setCallback (callback: QueryCallback<NotificationContext>): void {
this.callback = callback
void this.notify()
}
copyResult (): QueryResult<NotificationContext> | undefined {
if (this.result instanceof Promise) {
return undefined
}
return this.result.copy()
}
private async find (params: FindNotificationContextParams): Promise<NotificationContext[]> {
const contexts = await this.client.findNotificationContexts(params, this.id)
if (params.notifications?.message !== true) return contexts
await Promise.all(
contexts.map(async (context) => {
const notifications = context.notifications ?? []
context.notifications = await Promise.all(
notifications.map(async (notification) => {
if (notification.message != null || notification.messageId == null) return notification
const message = await loadMessageFromGroup(
notification.messageId,
this.workspace,
this.filesUrl,
notification.messageGroup,
notification.patches
)
if (message !== undefined) {
return {
...notification,
message
}
}
return notification
})
)
return context
})
)
return contexts
}
private async onCreateNotificationContextEvent (event: NotificationContextCreatedEvent): Promise<void> {
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const context = event.context
if (this.result.get(context.id) !== undefined) {
return
}
await this.addContext(context)
void this.notify()
}
private async onCreatePatchEvent (event: PatchCreatedEvent): Promise<void> {
if (this.params.notifications == null) return
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const context = this.result.getResult().find((it) => it.card === event.card)
if (context === undefined || (context.notifications ?? []).length === 0) return
const hasMessage = context.notifications?.some((it) => it.messageId === event.patch.message) ?? false
if (!hasMessage) return
this.result.update({
...context,
notifications: context.notifications?.map((it) => ({
...it,
message:
it.messageId === event.patch.message && it.message != null
? applyPatch(it.message, event.patch, allowedPatchTypes)
: it.message
}))
})
void this.notify()
}
private async onMessagesRemovedEvent (event: MessagesRemovedEvent): Promise<void> {
if (this.params.notifications == null) return
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const context = this.result.getResult().find((it) => it.card === event.card)
if (context === undefined) return
const filtered = (context.notifications ?? []).filter(
(it) => it.messageId == null || !event.messages.includes(it.messageId)
)
if (filtered.length === (context.notifications?.length ?? 0)) return
const contextUpdated = (await this.find({ id: context.id, limit: 1, notifications: this.params.notifications }))[0]
if (contextUpdated !== undefined) {
this.result.update(contextUpdated)
} else {
this.result.update({
...context,
notifications: filtered
})
}
void this.notify()
}
private async onRemoveNotificationEvent (event: NotificationsRemovedEvent): Promise<void> {
if (this.params.notifications == null) return
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const context = this.result.get(event.context)
if (context === undefined) return
const filtered = (context.notifications ?? []).filter((it) => it.created > event.untilDate)
if (filtered.length === (context.notifications?.length ?? 0)) return
const contextUpdated = (await this.find({ id: context.id, limit: 1, notifications: this.params.notifications }))[0]
if (contextUpdated !== undefined) {
this.result.update(contextUpdated)
} else {
this.result.update({
...context,
notifications: filtered
})
}
void this.notify()
}
private async onCreateNotificationEvent (event: NotificationCreatedEvent): Promise<void> {
if (this.params.notifications == null) return
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const context = this.result.get(event.notification.context)
if (context !== undefined) {
const message =
this.params.notifications.message === true
? (
await this.client.findMessages({
card: context.card,
id: event.notification.messageId
})
)[0]
: undefined
const notifications = [
{
...event.notification,
message
},
...(context.notifications ?? [])
]
if (notifications.length > this.params.notifications.limit) {
notifications.pop()
}
this.result.update({
...context,
notifications
})
void this.notify()
} else {
const newContext = (
await this.find({ id: event.notification.context, notifications: this.params.notifications, limit: 1 })
)[0]
if (newContext !== undefined) {
await this.addContext(newContext)
void this.notify()
}
}
}
private async onRemoveNotificationContextEvent (event: NotificationContextRemovedEvent): Promise<void> {
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
this.result.delete(event.context)
}
private async onUpdateNotificationContextEvent (event: NotificationContextUpdatedEvent): Promise<void> {
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const toUpdate = this.result.get(event.context)
if (toUpdate !== undefined) {
const notifications = this.filterNotifications(
event.lastView != null
? (toUpdate.notifications ?? []).map((it) => ({
...it,
read: event.lastView != null && event.lastView >= it.created
}))
: toUpdate.notifications ?? []
)
if (
notifications.length < (toUpdate.notifications?.length ?? 0) &&
this.params.notifications?.order !== SortingOrder.Descending
) {
const updated: NotificationContext = (
await this.find({ id: event.context, limit: 1, notifications: this.params.notifications })
)[0]
if (updated !== undefined) {
this.result.update(updated)
} else {
const updated: NotificationContext = {
...toUpdate,
lastUpdate: event.lastUpdate ?? toUpdate.lastUpdate,
lastView: event.lastView ?? toUpdate.lastView,
notifications
}
this.result.update(updated)
}
} else {
const updated: NotificationContext = {
...toUpdate,
lastUpdate: event.lastUpdate ?? toUpdate.lastUpdate,
lastView: event.lastView ?? toUpdate.lastView,
notifications
}
this.result.update(updated)
}
if (event.lastUpdate != null) {
this.result.sort((a, b) =>
this.params.order === SortingOrder.Descending
? b.lastUpdate.getTime() - a.lastUpdate.getTime()
: a.lastUpdate.getTime() - b.lastUpdate.getTime()
)
}
void this.notify()
}
}
private filterNotifications (notifications: Notification[]): Notification[] {
if (this.params.notifications == null) return notifications
const read = this.params.notifications.read
if (read == null) return notifications
return notifications.filter((it) => it.read === read)
}
private async addContext (context: NotificationContext): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
if (this.result.get(context.id) !== undefined) return
if (this.result.isTail()) {
if (this.params.order === SortingOrder.Ascending) {
this.result.push(context)
} else {
this.result.unshift(context)
}
}
if (this.params.limit != null && this.result.length > this.params.limit) {
this.result.pop()
}
}
private async notify (): Promise<void> {
if (this.callback === undefined) return
if (this.result instanceof Promise) {
this.result = await this.result
}
const result = this.result.getResult()
const isTail = this.result.isTail()
const isHead = this.result.isHead()
const window = new WindowImpl(result, isTail, isHead, this)
this.callback(window)
}
}
+244 -82
View File
@@ -2,7 +2,7 @@
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// You may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
@@ -15,122 +15,239 @@
import {
type FindNotificationsParams,
SortingOrder,
type Notification,
type MessageID
SortingOrder,
type WorkspaceID
} from '@hcengineering/communication-types'
import {
type NotificationCreatedEvent,
ResponseEventType,
type ResponseEvent,
type NotificationContextRemovedEvent,
type NotificationRemovedEvent,
type NotificationContextUpdatedEvent
type NotificationContextUpdatedEvent,
type NotificationCreatedEvent,
type NotificationsRemovedEvent,
type QueryCallback,
type RequestEvent,
type ResponseEvent,
ResponseEventType
} from '@hcengineering/communication-sdk-types'
import { BaseQuery } from '../query'
import { defaultQueryParams, type PagedQuery, type QueryId, type QueryClient } from '../types'
import { QueryResult } from '../result'
import { WindowImpl } from '../window'
import { loadMessageFromGroup } from '../utils'
export class NotificationQuery extends BaseQuery<Notification, FindNotificationsParams> {
override async find (params: FindNotificationsParams): Promise<Notification[]> {
return await this.client.findNotifications(params, this.id)
export class NotificationQuery implements PagedQuery<Notification, FindNotificationsParams> {
private result: QueryResult<Notification> | Promise<QueryResult<Notification>>
constructor (
private readonly client: QueryClient,
private readonly workspace: WorkspaceID,
private readonly filesUrl: string,
public readonly id: QueryId,
public readonly params: FindNotificationsParams,
private callback?: QueryCallback<Notification>,
initialResult?: QueryResult<Notification>
) {
const limit = this.params.limit ?? defaultQueryParams.limit
const findParams = { ...this.params, sort: this.params.order ?? defaultQueryParams.order, limit: limit + 1 }
if (initialResult !== undefined) {
this.result = initialResult
void this.notify()
} else {
this.result = this.initResult(findParams, limit)
}
}
override getObjectId (object: Notification): MessageID {
return object.message.id
private async initResult (findParams: FindNotificationsParams, limit: number): Promise<QueryResult<Notification>> {
try {
const res = await this.find(findParams)
const isComplete = res.length <= limit
if (!isComplete) res.pop()
const result = new QueryResult(res, (it) => it.id)
result.setTail(isComplete)
result.setHead(isComplete)
void this.notify()
return result
} catch (error) {
console.error('Failed to initialize query:', error)
return new QueryResult([] as Notification[], (it) => it.id)
}
}
override getObjectDate (object: Notification): Date {
return object.message.created
}
override async onEvent (event: ResponseEvent): Promise<void> {
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)
}
case ResponseEventType.NotificationCreated:
{ await this.onCreateNotificationEvent(event); break }
case ResponseEventType.NotificationsRemoved:
{ await this.onRemoveNotificationsEvent(event); break }
case ResponseEventType.NotificationContextUpdated:
{ await this.onUpdateNotificationContextEvent(event); break }
case ResponseEventType.NotificationContextRemoved:
{ await this.onRemoveNotificationContextEvent(event) }
}
}
async onCreateNotificationEvent (event: NotificationCreatedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
async onRequest (event: RequestEvent): Promise<void> {}
async unsubscribe (): Promise<void> {
await this.client.unsubscribeQuery(this.id)
}
async requestLoadNextPage (): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
await this.loadPage(SortingOrder.Ascending, this.result.getLast()?.created)
}
async requestLoadPrevPage (): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
await this.loadPage(SortingOrder.Descending, this.result.getFirst()?.created)
}
private async loadPage (order: SortingOrder, created?: Date): Promise<void> {
if (!created) return
if (this.result instanceof Promise) this.result = await this.result
const limit = this.getLimit()
const findParams: FindNotificationsParams = {
...this.params,
created: order === SortingOrder.Ascending ? { greater: created } : { less: created },
limit: limit + 1,
order
}
const exists = this.result.get(event.notification.message.id)
if (exists !== undefined) return
try {
const res = await this.find(findParams)
const isComplete = res.length <= limit
if (!isComplete) res.pop()
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)
if (order === SortingOrder.Ascending) {
this.result.append(res)
this.result.setTail(isComplete)
} else {
this.result.unshift(event.notification)
this.result.prepend(res)
this.result.setHead(isComplete)
}
await this.notify()
} catch (error) {
console.error(`Failed to load ${order === SortingOrder.Ascending ? 'next' : 'previous'} page:`, error)
}
}
removeCallback (): void {
this.callback = () => {}
}
setCallback (callback: QueryCallback<Notification>): void {
this.callback = callback
void this.notify()
}
copyResult (): QueryResult<Notification> | undefined {
return this.result instanceof Promise ? undefined : this.result.copy()
}
private async find (params: FindNotificationsParams): Promise<Notification[]> {
const notifications = await this.client.findNotifications(params, this.id)
if (!params.message) return notifications
return await Promise.all(
notifications.map(async (notification) => {
if (notification.message || !notification.messageId) return notification
const message = await loadMessageFromGroup(
notification.messageId,
this.workspace,
this.filesUrl,
notification.messageGroup,
notification.patches
)
return message ? { ...notification, message } : notification
})
)
}
private async onCreateNotificationEvent (event: NotificationCreatedEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
if (this.result.get(event.notification.id)) return
if (!this.result.isTail()) return
if (this.params.context && this.params.context !== event.notification.context) return
const notifications = this.filterNotifications([event.notification])
if (notifications.length === 0) return
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
}
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)
const lastView = event.lastView
if (lastView === undefined) return
const toUpdate = 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
}
: {})
})
const updated: Notification[] = toUpdate.map((it) => ({
...it,
read: lastView >= it.created
}))
const filtered = this.filterNotifications(updated)
if (filtered.length < this.getLimit() && filtered.length < toUpdate.length) {
if (this.result.length < this.getLimit()) {
for (const notification of updated) {
const allowed = filtered.some((it) => it.messageId === notification.messageId)
if (allowed) {
this.result.update(notification)
} else {
this.result.delete(notification.id)
}
}
void this.notify()
} else {
await this.reinit(this.result.length)
}
} else {
for (const notification of filtered) {
this.result.update(notification)
}
void this.notify()
}
}
private async onRemoveNotificationEvent (event: NotificationRemovedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
private async onRemoveNotificationsEvent (event: NotificationsRemovedEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
const notifications = this.result.getResult()
const length = this.result.length
let isDeleted = false
for (const notification of notifications) {
if (notification.created <= event.untilDate) {
isDeleted = true
this.result.delete(notification.id)
}
}
const deleted = this.result.delete(event.message)
if (deleted !== undefined) {
await this.notify()
if (length >= this.getLimit() && this.result.length < this.getLimit()) {
void this.reinit(this.result.length)
} else if (isDeleted) {
void this.notify()
}
}
private async onRemoveNotificationContextEvent (event: NotificationContextRemovedEvent): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
if (this.result instanceof Promise) this.result = await this.result
if (this.params.context != null && this.params.context !== event.context) return
@@ -139,15 +256,60 @@ export class NotificationQuery extends BaseQuery<Notification, FindNotifications
this.result.deleteAll()
this.result.setHead(true)
this.result.setTail(true)
await this.notify()
void this.notify()
} else {
const toRemove = this.result.getResult().filter((it) => it.context === event.context)
if (toRemove.length === 0) return
const length = this.result.length
for (const notification of toRemove) {
this.result.delete(notification.message.id)
this.result.delete(notification.id)
}
if (length >= this.getLimit() && this.result.length < this.getLimit()) {
void this.reinit(this.result.length)
} else {
void this.notify()
}
await this.notify()
}
}
private async notify (): Promise<void> {
if (!this.callback) return
if (this.result instanceof Promise) this.result = await this.result
const window = new WindowImpl(this.result.getResult(), this.result.isTail(), this.result.isHead(), this)
this.callback(window)
}
private getLimit (): number {
return this.params.limit ?? defaultQueryParams.limit
}
private filterNotifications (notifications: Notification[]): Notification[] {
const read = this.params.read
if (read == null) return notifications
return notifications.filter((it) => it.read === read)
}
private async reinit (limit: number): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
this.result = this.find({ ...this.params, limit: limit + 1 }).then((res) => {
const isTail = res.length <= limit
const isHead = res.length <= limit
if (res.length > limit) {
res.pop()
}
const result = new QueryResult(res, (it) => it.id)
result.setHead(isHead)
result.setTail(isTail)
return result
})
void this.result.then((res) => {
void this.notify()
return res
})
}
}
-208
View File
@@ -1,208 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { 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 PagedQuery, type QueryId } from './types'
import { WindowImpl } from './window'
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[] = []
constructor(
protected readonly client: QueryClient,
public readonly id: QueryId,
public readonly params: P,
private callback?: QueryCallback<T>,
initialResult?: QueryResult<T>
) {
if (initialResult !== undefined) {
this.result = initialResult
void this.notify()
} else {
const limit = this.params.limit ?? defaultQueryParams.limit
const findParams = {
...this.params,
sort: this.params.order ?? defaultQueryParams.order,
limit: limit + 1
}
const findPromise = this.find(findParams)
this.result = findPromise.then((res) => {
const isTail = false
const isHead = false
if (!isTail) {
res.pop()
}
const qResult = new QueryResult(res, (x) => this.getObjectId(x))
qResult.setTail(isTail)
qResult.setHead(isHead)
return qResult
})
this.result
.then(async () => {
await this.notify()
})
.catch((err: any) => {
console.error('Failed to update Live query: ', err)
})
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async find(params: FindParams): Promise<T[]> {
/* Implement in subclass */
return [] as T[]
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected getObjectId(object: T): ID {
/* 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)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async onEvent(event: ResponseEvent): Promise<void> {
/* Implement in subclass */
}
setCallback(callback: QueryCallback<T>): void {
this.callback = callback
void this.notify()
}
removeCallback(): void {
this.callback = () => {}
}
protected async notify(): Promise<void> {
if (this.callback === undefined) return
if (this.result instanceof Promise) {
this.result = await this.result
}
const result = this.result.getResult()
const isTail = this.result.isTail()
const isHead = this.result.isHead()
const window = new WindowImpl(result, isTail, isHead, this)
this.callback(window)
}
async requestLoadNextPage(): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
if (this.forward instanceof Promise) {
this.forward = await this.forward
}
if (this.result.isTail()) return
const last = this.result.getLast()
if (last === undefined) return
const limit = this.params.limit ?? defaultQueryParams.limit
const findParams: FindParams = {
...this.params,
from: this.getObjectDate(last),
excluded: true,
limit: limit + 1,
order: SortingOrder.Ascending
}
const forward = this.find(findParams)
this.forward = forward.then(async (res) => {
if (this.result instanceof Promise) {
this.result = await this.result
}
const isTail = res.length <= limit
if (!isTail) {
res.pop()
}
this.result.append(res)
this.result.setTail(isTail)
await this.notify()
return res
})
}
async requestLoadPrevPage(): Promise<void> {
if (this.result instanceof Promise) {
this.result = await this.result
}
if (this.backward instanceof Promise) {
this.backward = await this.backward
}
if (this.result.isHead()) return
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),
limit: limit + 1,
order: SortingOrder.Descending
}
const backward = this.find(findParams)
this.backward = backward.then(async (res) => {
if (this.result instanceof Promise) {
this.result = await this.result
}
const isHead = res.length <= limit
if (!isHead) {
res.pop()
}
if (this.params.order === SortingOrder.Ascending) {
const reversed = res.reverse()
this.result.prepend(reversed)
} else {
this.result.append(res)
}
this.result.setHead(isHead)
await this.notify()
return res
})
}
copyResult(): QueryResult<T> | undefined {
if (this.result instanceof Promise) {
return undefined
}
return this.result.copy()
}
async unsubscribe(): Promise<void> {
await this.client.unsubscribeQuery(this.id)
}
}
+14
View File
@@ -74,6 +74,14 @@ export class QueryResult<T> {
this.objectById = new Map([[this.getId(object), object], ...this.objectById])
}
pop(): T | undefined {
const array = Array.from(this.objectById.values())
const last = array[array.length - 1]
if (last === undefined) return
this.objectById.delete(this.getId(last))
return last
}
update(object: T): void {
this.objectById.set(this.getId(object), object)
}
@@ -97,6 +105,12 @@ export class QueryResult<T> {
}
}
sort(compare: (a: T, b: T) => number): void {
const current = Array.from(this.objectById.values())
const sorted = current.sort(compare)
this.objectById = new Map(sorted.map<[ID, T]>((object) => [this.getId(object), object]))
}
copy(): QueryResult<T> {
const copy = new QueryResult(Array.from(this.objectById.values()), this.getId)
+33 -4
View File
@@ -14,7 +14,19 @@
//
import { type ResponseEvent } from '@hcengineering/communication-sdk-types'
import { SortingOrder, type Window } from '@hcengineering/communication-types'
import {
SortingOrder,
type FindMessagesGroupsParams,
type FindMessagesParams,
type FindNotificationContextParams,
type FindNotificationsParams,
type Message,
type MessagesGroup,
type NotificationContext,
type Window,
type Notification
} from '@hcengineering/communication-types'
import type { EventResult, RequestEvent } from '@hcengineering/communication-sdk-types'
import { QueryResult } from './result'
@@ -37,13 +49,30 @@ export interface PagedQuery<R = any, P = FindParams> {
readonly params: P
onEvent: (event: ResponseEvent) => Promise<void>
onRequest: (event: RequestEvent, promise: Promise<EventResult>) => Promise<void>
unsubscribe: () => Promise<void>
requestLoadNextPage: () => Promise<void>
requestLoadPrevPage: () => Promise<void>
unsubscribe: () => Promise<void>
setCallback: (callback: (window: Window<R>) => void) => void
removeCallback: () => void
setCallback: (callback: (window: Window<R>) => void) => void
copyResult: () => QueryResult<R> | undefined
}
export interface QueryClient {
onEvent(event: ResponseEvent): void
onRequest(event: RequestEvent, promise: Promise<EventResult>): 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[]>
unsubscribeQuery(id: number): Promise<void>
}
+35
View File
@@ -0,0 +1,35 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { applyPatches } from '@hcengineering/communication-shared'
import type { Message, MessageID, MessagesGroup, Patch, WorkspaceID } from '@hcengineering/communication-types'
import { loadGroupFile } from '@hcengineering/communication-yaml'
export async function loadMessageFromGroup(
id: MessageID,
workspace: WorkspaceID,
filesUrl: string,
group?: MessagesGroup,
patches: Patch[] = []
): Promise<Message | undefined> {
if (group == null) return
const parsedFile = await loadGroupFile(workspace, filesUrl, group, { retries: 5 })
const message = parsedFile.messages.find((it) => it.id === id)
if (message == null) return
return applyPatches(message, patches)
}
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@hcengineering/communication-rest-client",
"version": "0.1.147",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "./types/index.d.ts",
"files": [
"dist/index.js",
"dist/index.cjs",
"types/**/*.d.ts"
],
"scripts": {
"bundle": "bun run bundle:browser && bun run bundle:node",
"bundle:browser": "bun build src/index.ts --outdir dist --target browser",
"bundle:node": "bun build src/index.ts --target node --format cjs --outfile dist/index.cjs"
},
"devDependencies": {
"@types/bun": "^1.1.14",
"@types/snappyjs": "^0.7.1"
},
"peerDependencies": {
"typescript": "^5.6.3"
},
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/communication-shared": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/core": "^0.7.28",
"snappyjs": "^0.7.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/hcengineering/communication.git"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
@@ -13,5 +13,5 @@
// limitations under the License.
//
export { type Client } from '@hcengineering/communication-sdk-types'
export * from './client'
export { createRestClient } from './rest'
export type { RestClient } from './types'
+164
View File
@@ -0,0 +1,164 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { concatLink } from '@hcengineering/core'
import { PlatformError, unknownError } from '@hcengineering/platform'
import type { EventResult, RequestEvent } from '@hcengineering/communication-sdk-types'
import type {
FindMessagesGroupsParams,
FindMessagesParams,
FindNotificationContextParams,
Message,
MessagesGroup,
NotificationContext,
FindNotificationsParams,
Notification
} from '@hcengineering/communication-types'
import { retry } from '@hcengineering/communication-shared'
import { extractJson } from './utils'
import type { RestClient } from './types'
const retries = 3
export function createRestClient(endpoint: string, workspaceId: string, token: string): RestClient {
return new RestClientImpl(endpoint, workspaceId, token)
}
class RestClientImpl implements RestClient {
endpoint: string
constructor(
endpoint: string,
readonly workspace: string,
readonly token: string
) {
this.endpoint = endpoint.replace('ws', 'http')
}
private jsonHeaders(): Record<string, string> {
return {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.token,
'accept-encoding': 'snappy, gzip'
}
}
private requestInit(): RequestInit {
return {
method: 'GET',
keepalive: true,
headers: this.jsonHeaders()
}
}
async event(event: RequestEvent): Promise<EventResult> {
const response = await fetch(concatLink(this.endpoint, `/api/v1/event/${this.workspace}`), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.token
},
keepalive: true,
body: JSON.stringify(event)
})
if (!response.ok) {
throw new PlatformError(unknownError(response.statusText))
}
return (await response.json()) as EventResult
}
async findMessages(params: FindMessagesParams): Promise<Message[]> {
const searchParams = new URLSearchParams()
if (Object.keys(params).length > 0) {
searchParams.append('params', JSON.stringify(params))
}
const requestUrl = concatLink(this.endpoint, `/api/v1/find-messages/${this.workspace}?${searchParams.toString()}`)
return await retry(
async () => {
const response = await fetch(requestUrl, this.requestInit())
if (!response.ok) {
throw new PlatformError(unknownError(response.statusText))
}
return await extractJson<MessagesGroup[]>(response)
},
{ retries }
)
}
async findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
const searchParams = new URLSearchParams()
if (Object.keys(params).length > 0) {
searchParams.append('params', JSON.stringify(params))
}
const requestUrl = concatLink(
this.endpoint,
`/api/v1/find-messages-groups/${this.workspace}?${searchParams.toString()}`
)
return await retry(
async () => {
const response = await fetch(requestUrl, this.requestInit())
if (!response.ok) {
throw new PlatformError(unknownError(response.statusText))
}
return await extractJson<MessagesGroup[]>(response)
},
{ retries }
)
}
async findNotificationContexts(params: FindNotificationContextParams): Promise<NotificationContext[]> {
const searchParams = new URLSearchParams()
if (Object.keys(params).length > 0) {
searchParams.append('params', JSON.stringify(params))
}
const requestUrl = concatLink(
this.endpoint,
`/api/v1/find-notification-contexts/${this.workspace}?${searchParams.toString()}`
)
return await retry(
async () => {
const response = await fetch(requestUrl, this.requestInit())
if (!response.ok) {
throw new PlatformError(unknownError(response.statusText))
}
return await extractJson<NotificationContext[]>(response)
},
{ retries }
)
}
async findNotifications(params: FindNotificationsParams): Promise<Notification[]> {
const searchParams = new URLSearchParams()
if (Object.keys(params).length > 0) {
searchParams.append('params', JSON.stringify(params))
}
const requestUrl = concatLink(
this.endpoint,
`/api/v1/find-notifications/${this.workspace}?${searchParams.toString()}`
)
return await retry(
async () => {
const response = await fetch(requestUrl, this.requestInit())
if (!response.ok) {
throw new PlatformError(unknownError(response.statusText))
}
return await extractJson<Notification[]>(response)
},
{ retries }
)
}
}
+35
View File
@@ -0,0 +1,35 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { EventResult, RequestEvent } from '@hcengineering/communication-sdk-types'
import type {
FindMessagesGroupsParams,
FindMessagesParams,
Message,
MessagesGroup,
FindNotificationsParams,
FindNotificationContextParams,
NotificationContext,
Notification
} from '@hcengineering/communication-types'
export interface RestClient {
findMessages: (params: FindMessagesParams) => Promise<Message[]>
findMessagesGroups: (params: FindMessagesGroupsParams) => Promise<MessagesGroup[]>
findNotificationContexts: (params: FindNotificationContextParams) => Promise<NotificationContext[]>
findNotifications: (params: FindNotificationsParams) => Promise<Notification[]>
event: (event: RequestEvent) => Promise<EventResult>
}
+41
View File
@@ -0,0 +1,41 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { uncompress } from 'snappyjs'
function isDateString(value: any) {
if (typeof value !== 'string') return false
const dateStringRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/
return dateStringRegex.test(value)
}
function reviver(key: string, value: any) {
if (isDateString(value)) return new Date(value)
return value
}
export async function extractJson<T>(response: Response): Promise<any> {
const encoding = response.headers.get('content-encoding')
if (encoding === 'snappy') {
const buffer = await response.arrayBuffer()
const decompressed = uncompress(buffer)
const decoder = new TextDecoder()
const jsonString = decoder.decode(decompressed)
return JSON.parse(jsonString, reviver) as T
}
const jsonString = await response.text()
return JSON.parse(jsonString, reviver) as T
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-sdk-types",
"version": "0.1.61",
"version": "0.1.147",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./types/index.d.ts",
@@ -19,7 +19,7 @@
},
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/core": "0.7.15"
"@hcengineering/core": "^0.7.28"
},
"peerDependencies": {
"typescript": "^5.6.3"
+13 -6
View File
@@ -21,12 +21,12 @@ import type {
Message,
MessageID,
NotificationContext,
NotificationContextUpdate,
RichText,
SocialID,
Notification,
FindMessagesGroupsParams,
MessagesGroup
MessagesGroup,
BlobID
} from '@hcengineering/communication-types'
import type { FindMessagesParams } from '@hcengineering/communication-types'
@@ -40,17 +40,24 @@ export interface Client {
createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void>
removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void>
createAttachment(card: CardID, message: MessageID, attachment: CardID, creator: SocialID): Promise<void>
removeAttachment(card: CardID, message: MessageID, attachment: CardID): Promise<void>
createFile(
card: CardID,
message: MessageID,
blobId: BlobID,
fileType: string,
filename: string,
creator: SocialID
): Promise<void>
removeFile(card: CardID, message: MessageID, blobId: BlobID): 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>
createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise<ContextID>
createNotificationContext(card: CardID, lastView: Date, lastUpdate: Date): Promise<ContextID>
removeNotificationContext(context: ContextID): Promise<void>
updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise<void>
updateNotificationContext(context: ContextID, lastView?: Date, lastUpdate?: Date): Promise<void>
onEvent(event: ResponseEvent): void
+42 -34
View File
@@ -22,22 +22,32 @@ import type {
Message,
MessageID,
NotificationContext,
NotificationContextUpdate,
RichText,
SocialID,
Notification,
BlobID,
FindMessagesGroupsParams,
MessagesGroup,
WorkspaceID,
PatchType,
Thread
Thread,
AccountID,
Collaborator,
MessageType,
FindCollaboratorsParams,
NotificationID,
MessageData
} 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>
createMessage(
card: CardID,
type: MessageType,
content: RichText,
creator: SocialID,
created: Date,
data?: MessageData
): Promise<MessageID>
removeMessages(card: CardID, ids: MessageID[], socialIds?: SocialID[]): Promise<MessageID[]>
createPatch(
card: CardID,
@@ -47,49 +57,47 @@ export interface DbAdapter {
creator: SocialID,
created: Date
): Promise<void>
removePatches(card: CardID, fromId: MessageID, toId: MessageID): Promise<void>
createMessagesGroup(
card: CardID,
blobId: BlobID,
fromDate: Date,
toDate: Date,
fromID: MessageID,
toID: MessageID,
count: number
): Promise<void>
createMessagesGroup(card: CardID, blobId: BlobID, fromSec: Date, toSec: Date, 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>
createFile(
card: CardID,
message: MessageID,
blobId: BlobID,
fileType: string,
filename: string,
size: number,
creator: SocialID,
created: Date
): Promise<void>
removeFile(card: CardID, message: MessageID, blobId: BlobID): Promise<void>
createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void>
updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise<void>
updateThread(thread: CardID, op: 'increment' | 'decrement', lastReply?: Date): Promise<void>
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>
addCollaborators(card: CardID, collaborators: AccountID[], date?: Date): Promise<void>
removeCollaborators(card: CardID, collaborators: AccountID[]): Promise<void>
getCollaboratorsCursor(card: CardID, date: Date, size?: number): AsyncIterable<Collaborator[]>
createContext(personalWorkspace: WorkspaceID, card: CardID, lastView?: Date, lastUpdate?: Date): Promise<ContextID>
updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void>
removeContext(context: ContextID): Promise<void>
findCollaborators(params: FindCollaboratorsParams): Promise<Collaborator[]>
findContexts(
params: FindNotificationContextParams,
personalWorkspaces: WorkspaceID[],
workspace?: WorkspaceID
): Promise<NotificationContext[]>
findNotifications(
params: FindNotificationsParams,
personalWorkspace: WorkspaceID,
workspace?: WorkspaceID
): Promise<Notification[]>
createNotification(context: ContextID, message: MessageID, created: Date): Promise<NotificationID>
removeNotification(context: ContextID, account: AccountID, untilDate: Date): Promise<void>
createContext(account: AccountID, card: CardID, lastUpdate: Date, lastView: Date): Promise<ContextID>
updateContext(context: ContextID, account: AccountID, lastUpdate?: Date, lastView?: Date): Promise<void>
removeContext(context: ContextID, account: AccountID): Promise<void>
findContexts(params: FindNotificationContextParams): Promise<NotificationContext[]>
findNotifications(params: FindNotificationsParams): Promise<Notification[]>
close(): void
}
-13
View File
@@ -15,19 +15,6 @@
import type { Message, Window, Notification } from '@hcengineering/communication-types'
import type { Client } from './client'
export type QueryClient = Pick<
Client,
| 'onEvent'
| 'findMessages'
| 'findMessagesGroups'
| 'findNotificationContexts'
| 'findNotifications'
| 'unsubscribeQuery'
| 'close'
>
export type QueryCallback<T> = (window: Window<T>) => void
export type MessagesQueryCallback = QueryCallback<Message>
+101 -71
View File
@@ -17,95 +17,94 @@ import type {
CardID,
ContextID,
MessageID,
NotificationContextUpdate,
RichText,
SocialID,
MessagesGroup,
BlobID
BlobID,
AccountID,
PatchType,
MessageType,
MessageData
} 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',
CreateFile = 'createFile',
RemoveFile = 'removeFile',
CreateThread = 'createThread',
UpdateThread = 'updateThread',
CreateMessagesGroup = 'createMessagesGroup',
RemoveMessagesGroup = 'removeMessagesGroup',
AddCollaborators = 'addCollaborators',
RemoveCollaborators = 'removeCollaborators',
CreateNotification = 'createNotification',
RemoveNotification = 'removeNotification',
RemoveNotifications = 'removeNotifications',
CreateNotificationContext = 'createNotificationContext',
RemoveNotificationContext = 'removeNotificationContext',
UpdateNotificationContext = 'updateNotificationContext'
}
export type RequestEvent =
| CreateMessageEvent
| RemoveMessageEvent
| RemoveMessagesEvent
| CreatePatchEvent
| RemovePatchesEvent
| CreateReactionEvent
| RemoveReactionEvent
| CreateAttachmentEvent
| RemoveAttachmentEvent
| CreateNotificationEvent
| RemoveNotificationEvent
| CreateNotificationContextEvent
| RemoveNotificationContextEvent
| UpdateNotificationContextEvent
| CreateMessagesGroupEvent
| RemoveMessagesGroupEvent
| CreateThreadEvent
type BaseRequestEvent = {
_id?: string
}
export interface CreateMessageEvent {
export type RequestEvent =
| AddCollaboratorsEvent
| CreateFileEvent
| CreateMessageEvent
| CreateMessagesGroupEvent
| CreateNotificationContextEvent
| CreateNotificationEvent
| CreatePatchEvent
| CreateReactionEvent
| CreateThreadEvent
| RemoveFileEvent
| RemoveCollaboratorsEvent
| RemoveMessagesEvent
| RemoveMessagesGroupEvent
| RemoveNotificationContextEvent
| RemoveNotificationsEvent
| RemoveReactionEvent
| UpdateNotificationContextEvent
| UpdateThreadEvent
export interface CreateMessageEvent extends BaseRequestEvent {
type: RequestEventType.CreateMessage
messageType: MessageType
card: CardID
content: RichText
creator: SocialID
data?: MessageData
}
export interface RemoveMessageEvent {
type: RequestEventType.RemoveMessage
card: CardID
message: MessageID
}
export interface RemoveMessagesEvent {
export interface RemoveMessagesEvent extends BaseRequestEvent {
type: RequestEventType.RemoveMessages
card: CardID
fromId: MessageID
toId: MessageID
messages: MessageID[]
}
export interface CreatePatchEvent {
export interface CreatePatchEvent extends BaseRequestEvent {
type: RequestEventType.CreatePatch
patchType: PatchType
card: CardID
message: MessageID
content: RichText
creator: SocialID
}
export interface RemovePatchesEvent {
type: RequestEventType.RemovePatches
card: CardID
fromId: MessageID
toId: MessageID
}
export interface CreateReactionEvent {
export interface CreateReactionEvent extends BaseRequestEvent {
type: RequestEventType.CreateReaction
card: CardID
message: MessageID
@@ -113,7 +112,7 @@ export interface CreateReactionEvent {
creator: SocialID
}
export interface RemoveReactionEvent {
export interface RemoveReactionEvent extends BaseRequestEvent {
type: RequestEventType.RemoveReaction
card: CardID
message: MessageID
@@ -121,69 +120,100 @@ export interface RemoveReactionEvent {
creator: SocialID
}
export interface CreateAttachmentEvent {
type: RequestEventType.CreateAttachment
export interface CreateFileEvent extends BaseRequestEvent {
type: RequestEventType.CreateFile
card: CardID
message: MessageID
attachment: CardID
blobId: BlobID
size: number
fileType: string
filename: string
creator: SocialID
}
export interface RemoveAttachmentEvent {
type: RequestEventType.RemoveAttachment
export interface RemoveFileEvent extends BaseRequestEvent {
type: RequestEventType.RemoveFile
card: CardID
message: MessageID
attachment: CardID
blobId: BlobID
creator: SocialID
}
export interface CreateThreadEvent {
export interface CreateThreadEvent extends BaseRequestEvent {
type: RequestEventType.CreateThread
card: CardID
message: MessageID
thread: CardID
}
export interface CreateNotificationEvent {
export interface UpdateThreadEvent extends BaseRequestEvent {
type: RequestEventType.UpdateThread
thread: CardID
replies: 'increment' | 'decrement'
lastReply?: Date
}
export interface CreateNotificationEvent extends BaseRequestEvent {
type: RequestEventType.CreateNotification
message: MessageID
context: ContextID
message: MessageID
created: Date
account: AccountID
}
export interface RemoveNotificationEvent {
type: RequestEventType.RemoveNotification
message: MessageID
export interface RemoveNotificationsEvent extends BaseRequestEvent {
type: RequestEventType.RemoveNotifications
context: ContextID
account: AccountID
untilDate: Date
}
export interface CreateNotificationContextEvent {
export interface CreateNotificationContextEvent extends BaseRequestEvent {
type: RequestEventType.CreateNotificationContext
card: CardID
account: AccountID
lastView: Date
lastUpdate: Date
}
export interface RemoveNotificationContextEvent extends BaseRequestEvent {
type: RequestEventType.RemoveNotificationContext
context: ContextID
account: AccountID
}
export interface UpdateNotificationContextEvent extends BaseRequestEvent {
type: RequestEventType.UpdateNotificationContext
context: ContextID
account: AccountID
lastView?: Date
lastUpdate?: Date
}
export interface RemoveNotificationContextEvent {
type: RequestEventType.RemoveNotificationContext
context: ContextID
}
export interface UpdateNotificationContextEvent {
type: RequestEventType.UpdateNotificationContext
context: ContextID
update: NotificationContextUpdate
}
export interface CreateMessagesGroupEvent {
export interface CreateMessagesGroupEvent extends BaseRequestEvent {
type: RequestEventType.CreateMessagesGroup
group: MessagesGroup
}
export interface RemoveMessagesGroupEvent {
export interface RemoveMessagesGroupEvent extends BaseRequestEvent {
type: RequestEventType.RemoveMessagesGroup
card: CardID
blobId: BlobID
}
export interface AddCollaboratorsEvent extends BaseRequestEvent {
type: RequestEventType.AddCollaborators
card: CardID
collaborators: AccountID[]
date?: Date
}
export interface RemoveCollaboratorsEvent extends BaseRequestEvent {
type: RequestEventType.RemoveCollaborators
card: CardID
collaborators: AccountID[]
}
export type EventResult = CreateMessageResult | CreateNotificationContextResult | {}
export interface CreateMessageResult {
+79 -39
View File
@@ -14,78 +14,97 @@
//
import type {
Attachment,
File,
CardID,
ContextID,
Message,
MessageID,
NotificationContext,
NotificationContextUpdate,
Patch,
Reaction,
SocialID,
Notification,
Thread,
MessagesGroup
MessagesGroup,
AccountID,
BlobID
} from '@hcengineering/communication-types'
export enum ResponseEventType {
MessageCreated = 'messageCreated',
MessageRemoved = 'messageRemoved',
MessagesRemoved = 'messagesRemoved',
PatchCreated = 'patchCreated',
ReactionCreated = 'reactionCreated',
ReactionRemoved = 'reactionRemoved',
AttachmentCreated = 'attachmentCreated',
AttachmentRemoved = 'attachmentRemoved',
FileCreated = 'fileCreated',
FileRemoved = 'fileRemoved',
ThreadCreated = 'threadCreated',
ThreadUpdated = 'threadUpdated',
MessagesGroupCreated = 'messagesGroupCreated',
NotificationCreated = 'notificationCreated',
NotificationRemoved = 'notificationRemoved',
NotificationsRemoved = 'notificationsRemoved',
NotificationContextCreated = 'notificationContextCreated',
NotificationContextRemoved = 'notificationContextRemoved',
NotificationContextUpdated = 'notificationContextUpdated'
NotificationContextUpdated = 'notificationContextUpdated',
AddedCollaborators = 'addedCollaborators',
RemovedCollaborators = 'removedCollaborators'
}
type BaseResponseEvent = {
_id?: string
}
export type ResponseEvent =
| MessageCreatedEvent
| MessageRemovedEvent
| MessagesRemovedEvent
| PatchCreatedEvent
| ReactionCreatedEvent
| ReactionRemovedEvent
| AttachmentCreatedEvent
| AttachmentRemovedEvent
| FileCreatedEvent
| FileRemovedEvent
| NotificationCreatedEvent
| NotificationRemovedEvent
| NotificationsRemovedEvent
| NotificationContextCreatedEvent
| NotificationContextRemovedEvent
| NotificationContextUpdatedEvent
| ThreadCreatedEvent
| MessagesGroupCreatedEvent
| AddedCollaboratorsEvent
| RemovedCollaboratorsEvent
| ThreadUpdatedEvent
export interface MessageCreatedEvent {
export interface MessageCreatedEvent extends BaseResponseEvent {
type: ResponseEventType.MessageCreated
message: Message
}
export interface MessageRemovedEvent {
type: ResponseEventType.MessageRemoved
export interface MessagesRemovedEvent extends BaseResponseEvent {
type: ResponseEventType.MessagesRemoved
card: CardID
message: MessageID
messages: MessageID[]
}
export interface PatchCreatedEvent {
export interface PatchCreatedEvent extends BaseResponseEvent {
type: ResponseEventType.PatchCreated
card: CardID
patch: Patch
}
export interface ReactionCreatedEvent {
export interface ReactionCreatedEvent extends BaseResponseEvent {
type: ResponseEventType.ReactionCreated
card: CardID
reaction: Reaction
}
export interface ReactionRemovedEvent {
export interface ReactionRemovedEvent extends BaseResponseEvent {
type: ResponseEventType.ReactionRemoved
card: CardID
message: MessageID
@@ -93,56 +112,77 @@ export interface ReactionRemovedEvent {
creator: SocialID
}
export interface AttachmentCreatedEvent {
type: ResponseEventType.AttachmentCreated
export interface FileCreatedEvent extends BaseResponseEvent {
type: ResponseEventType.FileCreated
card: CardID
attachment: Attachment
file: File
}
export interface AttachmentRemovedEvent {
type: ResponseEventType.AttachmentRemoved
export interface FileRemovedEvent extends BaseResponseEvent {
type: ResponseEventType.FileRemoved
card: CardID
message: MessageID
attachment: CardID
blobId: BlobID
creator: SocialID
}
export interface MessagesGroupCreatedEvent {
export interface MessagesGroupCreatedEvent extends BaseResponseEvent {
type: ResponseEventType.MessagesGroupCreated
group: MessagesGroup
}
export interface ThreadCreatedEvent {
export interface ThreadCreatedEvent extends BaseResponseEvent {
type: ResponseEventType.ThreadCreated
thread: Thread
}
export interface NotificationCreatedEvent {
export interface NotificationCreatedEvent extends BaseResponseEvent {
type: ResponseEventType.NotificationCreated
personalWorkspace: string
notification: Notification
account: AccountID
}
export interface NotificationRemovedEvent {
type: ResponseEventType.NotificationRemoved
personalWorkspace: string
message: MessageID
export interface NotificationsRemovedEvent extends BaseResponseEvent {
type: ResponseEventType.NotificationsRemoved
untilDate: Date
context: ContextID
account: AccountID
}
export interface NotificationContextCreatedEvent {
export interface NotificationContextCreatedEvent extends BaseResponseEvent {
type: ResponseEventType.NotificationContextCreated
context: NotificationContext
}
export interface NotificationContextRemovedEvent {
export interface NotificationContextRemovedEvent extends BaseResponseEvent {
type: ResponseEventType.NotificationContextRemoved
personalWorkspace: string
context: ContextID
account: AccountID
}
export interface NotificationContextUpdatedEvent {
export interface NotificationContextUpdatedEvent extends BaseResponseEvent {
type: ResponseEventType.NotificationContextUpdated
personalWorkspace: string
context: ContextID
update: NotificationContextUpdate
account: AccountID
lastView?: Date
lastUpdate?: Date
}
export interface AddedCollaboratorsEvent extends BaseResponseEvent {
type: ResponseEventType.AddedCollaborators
card: CardID
collaborators: AccountID[]
}
export interface RemovedCollaboratorsEvent extends BaseResponseEvent {
type: ResponseEventType.RemovedCollaborators
card: CardID
collaborators: AccountID[]
}
export interface ThreadUpdatedEvent extends BaseResponseEvent {
type: ResponseEventType.ThreadUpdated
thread: CardID
replies: 'increment' | 'decrement'
lastReply?: Date
}
+17 -3
View File
@@ -16,21 +16,35 @@
import type {
FindMessagesGroupsParams,
FindMessagesParams,
FindNotificationContextParams,
FindNotificationsParams,
Message,
MessagesGroup
MessagesGroup,
NotificationContext,
Notification
} from '@hcengineering/communication-types'
import type { Account } from '@hcengineering/core'
import type { EventResult, RequestEvent } from './requestEvent.ts'
export interface ConnectionInfo {
sessionId: string
sessionId?: string
account: Account
}
export interface ServerApi {
findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: number): Promise<Message[]>
findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: number | string): Promise<Message[]>
findMessagesGroups(info: ConnectionInfo, params: FindMessagesGroupsParams): Promise<MessagesGroup[]>
findNotificationContexts(
info: ConnectionInfo,
params: FindNotificationContextParams,
queryId?: number | string
): Promise<NotificationContext[]>
findNotifications(
info: ConnectionInfo,
params: FindNotificationsParams,
queryId?: number | string
): Promise<Notification[]>
event(info: ConnectionInfo, event: RequestEvent): Promise<EventResult>
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-server",
"version": "0.1.61",
"version": "0.1.147",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
@@ -20,11 +20,12 @@
"@types/ws": "^8.5.13"
},
"dependencies": {
"@hcengineering/core": "0.7.15",
"@hcengineering/server-token": "0.7.15",
"@hcengineering/account-client": "^0.7.28",
"@hcengineering/communication-cockroach": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
"@hcengineering/communication-types": "workspace:*"
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/core": "^0.7.28",
"@hcengineering/server-token": "^0.7.28"
},
"peerDependencies": {
"typescript": "^5.6.3"
+221 -203
View File
@@ -13,73 +13,61 @@
// limitations under the License.
//
import { type Message, type Patch, type Reaction } from '@hcengineering/communication-types'
import {
type Message,
type Patch,
type Reaction,
type Attachment,
type SocialID,
type WorkspaceID,
PatchType
} from '@hcengineering/communication-types'
import {
type CreateAttachmentEvent,
type AttachmentCreatedEvent,
type AddCollaboratorsEvent,
type ConnectionInfo,
type CreateFileEvent,
type CreateMessageEvent,
type MessageCreatedEvent,
type CreatePatchEvent,
type PatchCreatedEvent,
type CreateReactionEvent,
type ReactionCreatedEvent,
type RemoveAttachmentEvent,
type AttachmentRemovedEvent,
type RemoveMessageEvent,
type MessageRemovedEvent,
type RemoveReactionEvent,
type ReactionRemovedEvent,
type EventResult,
type DbAdapter,
type CreateNotificationEvent,
type RemoveNotificationEvent,
type CreateMessagesGroupEvent,
type CreateNotificationContextEvent,
type RemoveNotificationContextEvent,
type UpdateNotificationContextEvent,
type NotificationRemovedEvent,
type CreateNotificationEvent,
type CreatePatchEvent,
type CreateReactionEvent,
type CreateThreadEvent,
type DbAdapter,
type EventResult,
type FileCreatedEvent,
type FileRemovedEvent,
type MessageCreatedEvent,
type MessagesGroupCreatedEvent,
type MessagesRemovedEvent,
type NotificationContextCreatedEvent,
type NotificationContextRemovedEvent,
type NotificationContextUpdatedEvent,
type ResponseEvent,
RequestEventType,
type RequestEvent,
ResponseEventType,
type CreateMessagesGroupEvent,
type NotificationsRemovedEvent,
type PatchCreatedEvent,
type ReactionCreatedEvent,
type ReactionRemovedEvent,
type RemoveCollaboratorsEvent,
type RemoveFileEvent,
type RemoveMessagesEvent,
type ThreadCreatedEvent,
type CreateThreadEvent,
type ConnectionInfo,
type RemovePatchesEvent,
type RemoveMessagesGroupEvent,
type MessagesGroupCreatedEvent
type RemoveNotificationContextEvent,
type RemoveNotificationsEvent,
type RemoveReactionEvent,
type RequestEvent,
RequestEventType,
type ResponseEvent,
ResponseEventType,
type ThreadCreatedEvent,
type UpdateNotificationContextEvent,
type UpdateThreadEvent
} from '@hcengineering/communication-sdk-types'
import { systemAccountUuid, type Account } from '@hcengineering/core'
import { systemAccountUuid } from '@hcengineering/core'
export interface Result {
responseEvent?: ResponseEvent
result: EventResult
result?: EventResult
}
export class EventProcessor {
constructor(
private readonly db: DbAdapter,
private readonly workspace: WorkspaceID
) {}
constructor(private readonly db: DbAdapter) {}
async process(info: ConnectionInfo, event: RequestEvent): Promise<Result> {
switch (event.type) {
case RequestEventType.CreateMessage:
return await this.createMessage(event, info)
case RequestEventType.RemoveMessage:
return await this.removeMessage(event, info)
case RequestEventType.RemoveMessages:
return await this.removeMessages(event, info)
case RequestEventType.CreatePatch:
@@ -88,14 +76,14 @@ export class EventProcessor {
return await this.createReaction(event, info)
case RequestEventType.RemoveReaction:
return await this.removeReaction(event, info)
case RequestEventType.CreateAttachment:
return await this.createAttachment(event, info)
case RequestEventType.RemoveAttachment:
return await this.removeAttachment(event, info)
case RequestEventType.CreateFile:
return await this.createFile(event, info)
case RequestEventType.RemoveFile:
return await this.removeFile(event, info)
case RequestEventType.CreateNotification:
return await this.createNotification(event, info)
case RequestEventType.RemoveNotification:
return await this.removeNotification(event, info)
case RequestEventType.RemoveNotifications:
return await this.removeNotifications(event, info)
case RequestEventType.CreateNotificationContext:
return await this.createNotificationContext(event, info)
case RequestEventType.RemoveNotificationContext:
@@ -106,28 +94,82 @@ export class EventProcessor {
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)
case RequestEventType.AddCollaborators:
return await this.addCollaborators(event, info)
case RequestEventType.RemoveCollaborators:
return await this.removeCollaborators(event, info)
case RequestEventType.UpdateThread:
return await this.updateThread(event, info)
}
}
private async createMessage(event: CreateMessageEvent, info: ConnectionInfo): Promise<Result> {
this.checkCreator(info.account, event.creator)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async updateThread(event: UpdateThreadEvent, _: ConnectionInfo): Promise<Result> {
await this.db.updateThread(event.thread, event.replies, event.lastReply)
return {
responseEvent: {
_id: event._id,
type: ResponseEventType.ThreadUpdated,
thread: event.thread,
replies: event.replies,
lastReply: event.lastReply
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async addCollaborators(event: AddCollaboratorsEvent, _: ConnectionInfo): Promise<Result> {
await this.db.addCollaborators(event.card, event.collaborators, event.date)
return {
responseEvent: {
_id: event._id,
type: ResponseEventType.AddedCollaborators,
card: event.card,
collaborators: event.collaborators
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async removeCollaborators(event: RemoveCollaboratorsEvent, _: ConnectionInfo): Promise<Result> {
await this.db.removeCollaborators(event.card, event.collaborators)
return {
responseEvent: {
_id: event._id,
type: ResponseEventType.RemovedCollaborators,
card: event.card,
collaborators: event.collaborators
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async createMessage(event: CreateMessageEvent, _: ConnectionInfo): Promise<Result> {
const created = new Date()
const id = await this.db.createMessage(event.card, event.content, event.creator, created)
const id = await this.db.createMessage(
event.card,
event.messageType,
event.content,
event.creator,
created,
event.data
)
const message: Message = {
id,
type: event.messageType,
card: event.card,
content: event.content,
creator: event.creator,
created,
data: event.data,
reactions: [],
attachments: []
files: []
}
const responseEvent: MessageCreatedEvent = {
_id: event._id,
type: ResponseEventType.MessageCreated,
message
}
@@ -137,69 +179,48 @@ export class EventProcessor {
}
}
private async createPatch(event: CreatePatchEvent, info: ConnectionInfo): Promise<Result> {
this.checkCreator(info.account, event.creator)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async createPatch(event: CreatePatchEvent, _: ConnectionInfo): Promise<Result> {
const created = new Date()
await this.db.createPatch(event.card, event.message, PatchType.update, event.content, event.creator, created)
await this.db.createPatch(event.card, event.message, event.patchType, event.content, event.creator, created)
const patch: Patch = {
type: PatchType.update,
type: event.patchType,
message: event.message,
content: event.content,
creator: event.creator,
created
}
const responseEvent: PatchCreatedEvent = {
_id: event._id,
type: ResponseEventType.PatchCreated,
card: event.card,
patch
}
return {
responseEvent,
result: {}
}
}
private async removeMessage(event: RemoveMessageEvent, info: ConnectionInfo): Promise<Result> {
const socialIds = systemAccountUuid === info.account.uuid ? undefined : info.account.socialIds
await this.db.removeMessage(event.card, event.message, socialIds)
const responseEvent: MessageRemovedEvent = {
type: ResponseEventType.MessageRemoved,
card: event.card,
message: event.message
}
return {
responseEvent,
result: {}
responseEvent
}
}
private async removeMessages(event: RemoveMessagesEvent, info: ConnectionInfo): Promise<Result> {
if (systemAccountUuid !== info.account.uuid) {
throw new Error('Forbidden')
const socialIds = systemAccountUuid === info.account.uuid ? undefined : info.account.socialIds
const deleted = await this.db.removeMessages(event.card, event.messages, socialIds)
const responseEvent: MessagesRemovedEvent = {
_id: event._id,
type: ResponseEventType.MessagesRemoved,
card: event.card,
messages: deleted
}
await this.db.removeMessages(event.card, event.fromId, event.toId)
return {
result: {}
responseEvent,
result: { messages: deleted }
}
}
private async removePatches(event: RemovePatchesEvent, info: ConnectionInfo): Promise<Result> {
if (systemAccountUuid !== info.account.uuid) {
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.account, event.creator)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async createReaction(event: CreateReactionEvent, _: ConnectionInfo): Promise<Result> {
const created = new Date()
await this.db.createReaction(event.card, event.message, event.reaction, event.creator, created)
@@ -210,20 +231,21 @@ export class EventProcessor {
created
}
const responseEvent: ReactionCreatedEvent = {
_id: event._id,
type: ResponseEventType.ReactionCreated,
card: event.card,
reaction
}
return {
responseEvent,
result: {}
responseEvent
}
}
private async removeReaction(event: RemoveReactionEvent, info: ConnectionInfo): Promise<Result> {
this.checkCreator(info.account, event.creator)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async removeReaction(event: RemoveReactionEvent, _: ConnectionInfo): Promise<Result> {
await this.db.removeReaction(event.card, event.message, event.reaction, event.creator)
const responseEvent: ReactionRemovedEvent = {
_id: event._id,
type: ResponseEventType.ReactionRemoved,
card: event.card,
message: event.message,
@@ -231,90 +253,104 @@ export class EventProcessor {
creator: event.creator
}
return {
responseEvent,
result: {}
}
}
private async createAttachment(event: CreateAttachmentEvent, info: ConnectionInfo): Promise<Result> {
this.checkCreator(info.account, event.creator)
const created = new Date()
await this.db.createAttachment(event.message, event.card, event.creator, created)
const attachment: Attachment = {
message: event.message,
card: event.card,
creator: event.creator,
created
}
const responseEvent: AttachmentCreatedEvent = {
type: ResponseEventType.AttachmentCreated,
card: event.card,
attachment
}
return {
responseEvent,
result: {}
responseEvent
}
}
// 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,
private async createFile(event: CreateFileEvent, _: ConnectionInfo): Promise<Result> {
const created = new Date()
await this.db.createFile(
event.card,
event.message,
event.blobId,
event.fileType,
event.filename,
event.size,
event.creator,
created
)
const responseEvent: FileCreatedEvent = {
_id: event._id,
type: ResponseEventType.FileCreated,
card: event.card,
message: event.message,
attachment: event.attachment
file: {
card: event.card,
message: event.message,
blobId: event.blobId,
type: event.fileType,
filename: event.filename,
size: event.size,
creator: event.creator,
created
}
}
return {
responseEvent,
result: {}
responseEvent
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async removeFile(event: RemoveFileEvent, _: ConnectionInfo): Promise<Result> {
await this.db.removeFile(event.card, event.message, event.blobId)
const responseEvent: FileRemovedEvent = {
_id: event._id,
type: ResponseEventType.FileRemoved,
card: event.card,
message: event.message,
blobId: event.blobId,
creator: event.creator
}
return {
responseEvent
}
}
// 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)
const id = await this.db.createNotification(event.context, event.message, event.created)
return {
result: {}
responseEvent: {
_id: event._id,
type: ResponseEventType.NotificationCreated,
notification: {
id,
context: event.context,
messageId: event.message,
read: false,
created: event.created
},
account: event.account
}
}
}
//eslint-disable-next-line @typescript-eslint/no-unused-vars
private async removeNotification(event: RemoveNotificationEvent, info: ConnectionInfo): Promise<Result> {
await this.db.removeNotification(event.message, event.context)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async removeNotifications(event: RemoveNotificationsEvent, _: ConnectionInfo): Promise<Result> {
await this.db.removeNotification(event.context, event.account, event.untilDate)
const responseEvent: NotificationRemovedEvent = {
type: ResponseEventType.NotificationRemoved,
// personalWorkspace: info.personalWorkspace,
// TODO: add personal workspace
personalWorkspace: '' as WorkspaceID,
message: event.message,
context: event.context
const responseEvent: NotificationsRemovedEvent = {
_id: event._id,
type: ResponseEventType.NotificationsRemoved,
context: event.context,
account: event.account,
untilDate: event.untilDate
}
return {
responseEvent,
result: {}
responseEvent
}
}
private async createNotificationContext(
event: CreateNotificationContextEvent,
//eslint-disable-next-line @typescript-eslint/no-unused-vars
info: ConnectionInfo
): Promise<Result> {
// TODO: add personal workspace
const personalWorkspace = '' as WorkspaceID
const id = await this.db.createContext(personalWorkspace, event.card, event.lastView, event.lastUpdate)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async createNotificationContext(event: CreateNotificationContextEvent, _: ConnectionInfo): Promise<Result> {
const id = await this.db.createContext(event.account, event.card, event.lastUpdate, event.lastView)
const responseEvent: NotificationContextCreatedEvent = {
_id: event._id,
type: ResponseEventType.NotificationContextCreated,
context: {
id,
workspace: this.workspace,
personalWorkspace,
account: event.account,
card: event.card,
lastView: event.lastView,
lastUpdate: event.lastUpdate
@@ -329,75 +365,63 @@ export class EventProcessor {
private async removeNotificationContext(
event: RemoveNotificationContextEvent,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: ConnectionInfo
_: ConnectionInfo
): Promise<Result> {
await this.db.removeContext(event.context)
await this.db.removeContext(event.context, event.account)
const responseEvent: NotificationContextRemovedEvent = {
_id: event._id,
type: ResponseEventType.NotificationContextRemoved,
// personalWorkspace: info.personalWorkspace,
// TODO: add personal workspace
personalWorkspace: '' as WorkspaceID,
context: event.context
context: event.context,
account: event.account
}
return {
responseEvent,
result: {}
responseEvent
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async updateNotificationContext(event: UpdateNotificationContextEvent, info: ConnectionInfo): Promise<Result> {
await this.db.updateContext(event.context, event.update)
async updateNotificationContext(event: UpdateNotificationContextEvent, _: ConnectionInfo): Promise<Result> {
await this.db.updateContext(event.context, event.account, event.lastUpdate, event.lastView)
const responseEvent: NotificationContextUpdatedEvent = {
_id: event._id,
type: ResponseEventType.NotificationContextUpdated,
// personalWorkspace: info.personalWorkspace,
// TODO: add personal workspace
personalWorkspace: '' as WorkspaceID,
context: event.context,
update: event.update
account: event.account,
lastView: event.lastView,
lastUpdate: event.lastUpdate
}
return {
responseEvent,
result: {}
responseEvent
}
}
async createMessagesGroup(event: CreateMessagesGroupEvent, info: ConnectionInfo): Promise<Result> {
if (systemAccountUuid !== info.account.uuid) {
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)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async createMessagesGroup(event: CreateMessagesGroupEvent, _: ConnectionInfo): Promise<Result> {
const { fromSec, toSec, count, card, blobId } = event.group
await this.db.createMessagesGroup(card, blobId, fromSec, toSec, count)
const responseEvent: MessagesGroupCreatedEvent = {
_id: event._id,
type: ResponseEventType.MessagesGroupCreated,
group: {
card,
blobId,
fromDate,
toDate,
fromId,
toId,
fromSec,
toSec,
count
}
}
return {
responseEvent,
result: {}
responseEvent
}
}
async removeMessagesGroup(event: RemoveMessagesGroupEvent, info: ConnectionInfo): Promise<Result> {
if (systemAccountUuid !== info.account.uuid) {
throw new Error('Forbidden')
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async removeMessagesGroup(event: RemoveMessagesGroupEvent, _: ConnectionInfo): Promise<Result> {
await this.db.removeMessagesGroup(event.card, event.blobId)
return {
responseEvent: undefined,
result: {}
}
return {}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -405,6 +429,7 @@ export class EventProcessor {
const date = new Date()
await this.db.createThread(event.card, event.message, event.thread, date)
const responseEvent: ThreadCreatedEvent = {
_id: event._id,
type: ResponseEventType.ThreadCreated,
thread: {
card: event.card,
@@ -415,14 +440,7 @@ export class EventProcessor {
}
}
return {
responseEvent,
result: {}
}
}
private checkCreator(account: Account, creator: SocialID): void {
if (!account.socialIds.includes(creator) && systemAccountUuid !== account.uuid) {
throw new Error('Forbidden')
responseEvent
}
}
}
+68 -5
View File
@@ -13,13 +13,17 @@
// limitations under the License.
//
import type { MeasureContext } from '@hcengineering/core'
import { type MeasureContext, systemAccountUuid } from '@hcengineering/core'
import type {
FindMessagesGroupsParams,
FindMessagesParams,
FindNotificationContextParams,
FindNotificationsParams,
Message,
MessagesGroup,
WorkspaceID
NotificationContext,
WorkspaceID,
Notification
} from '@hcengineering/communication-types'
import { createDbAdapter } from '@hcengineering/communication-cockroach'
import type {
@@ -30,8 +34,9 @@ import type {
ServerApi
} from '@hcengineering/communication-sdk-types'
import { Manager, type BroadcastSessionsFunc } from './manager'
import { type BroadcastSessionsFunc, Manager } from './manager'
import { getMetadata, type Metadata } from './metadata'
import type { QueryId } from './types'
export class Api implements ServerApi {
private readonly manager: Manager
@@ -52,12 +57,14 @@ export class Api implements ServerApi {
dbUrl: string,
broadcast: BroadcastSessionsFunc
): Promise<Api> {
const db = await createDbAdapter(dbUrl, workspace, ctx, { withLogs: true })
const db = await createDbAdapter(dbUrl, workspace, ctx, {
withLogs: process.env.COMMUNICATION_TIME_LOGGING_ENABLED === 'true'
})
const metadata = getMetadata()
return new Api(ctx, metadata, workspace, db, broadcast)
}
async findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: number): Promise<Message[]> {
async findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: QueryId): Promise<Message[]> {
return await this.manager.findMessages(info, params, queryId)
}
@@ -65,6 +72,62 @@ export class Api implements ServerApi {
return await this.manager.findMessagesGroups(info, params)
}
async findNotificationContexts(
info: ConnectionInfo,
params: FindNotificationContextParams,
queryId?: QueryId
): Promise<NotificationContext[]> {
const isSystem = info.account.uuid === systemAccountUuid
if (isSystem) {
return await this.manager.findNotificationContexts(info, params, queryId)
}
const accounts = params.account == null || Array.isArray(params.account) ? params.account : [params.account]
const withMe = accounts == null || accounts.includes(info.account.uuid)
if (withMe) {
return await this.manager.findNotificationContexts(
info,
{
...params,
account: info.account.uuid
},
queryId
)
}
return []
}
async findNotifications(
info: ConnectionInfo,
params: FindNotificationsParams,
queryId?: QueryId
): Promise<Notification[]> {
const isSystem = info.account.uuid === systemAccountUuid
if (isSystem) {
return await this.manager.findNotifications(info, params, queryId)
}
const accounts = params.account == null || Array.isArray(params.account) ? params.account : [params.account]
const withMe = accounts == null || accounts.includes(info.account.uuid)
if (withMe) {
return await this.manager.findNotifications(
info,
{
...params,
account: info.account.uuid
},
queryId
)
}
return []
}
async unsubscribeQuery(info: ConnectionInfo, id: number): Promise<void> {
this.manager.unsubscribeQuery(info, id)
}
+144 -101
View File
@@ -17,13 +17,13 @@ import {
type ConnectionInfo,
type DbAdapter,
type EventResult,
type NotificationContextCreatedEvent,
type NotificationCreatedEvent,
type RequestEvent,
type ResponseEvent,
ResponseEventType
} from '@hcengineering/communication-sdk-types'
import type {
AccountID,
CardID,
FindMessagesGroupsParams,
FindMessagesParams,
FindNotificationContextParams,
@@ -31,29 +31,32 @@ import type {
Message,
MessageID,
MessagesGroup,
WorkspaceID
NotificationContext,
WorkspaceID,
Notification
} from '@hcengineering/communication-types'
import type { MeasureContext } from '@hcengineering/core'
import { Triggers } from './triggers'
import { Permissions } from './permissions'
import { EventProcessor } from './eventProcessor'
import type { Metadata } from './metadata.ts'
type QueryId = number | string
type QueryType = 'message' | 'notification' | 'context'
export type BroadcastSessionsFunc = (ctx: MeasureContext, sessionIds: string[], result: any) => void
interface SessionInfo {
account: AccountID
messageQueries: Map<QueryId, FindMessagesParams>
notificationQueries: Map<QueryId, FindNotificationsParams>
contextQueries: Map<QueryId, FindNotificationContextParams>
contextQueries: Map<QueryId, Set<CardID>>
}
export class Manager {
private readonly dataBySessionId = new Map<string, SessionInfo>()
private readonly triggers: Triggers
private readonly eventProcessor: EventProcessor
private readonly permissions: Permissions = new Permissions()
constructor(
private readonly ctx: MeasureContext,
@@ -62,58 +65,98 @@ export class Manager {
private readonly workspace: WorkspaceID,
private readonly broadcast: BroadcastSessionsFunc
) {
this.eventProcessor = new EventProcessor(db, this.workspace)
this.triggers = new Triggers(this.metadata, db, this.workspace)
this.eventProcessor = new EventProcessor(db)
this.triggers = new Triggers(ctx.newChild('triggers', {}), this.metadata, db, this.workspace)
}
async findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: number): Promise<Message[]> {
async findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: QueryId): Promise<Message[]> {
this.createSession(info)
const result = await this.db.findMessages(params)
if (queryId != null && info.sessionId != null && info.sessionId !== '') {
this.subscribeQuery(info, 'message', queryId, params)
this.subscribeMessageQuery(info, queryId, params)
}
return result
}
async findMessagesGroups(_: ConnectionInfo, params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
async findMessagesGroups(info: ConnectionInfo, params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
this.createSession(info)
return await this.db.findMessagesGroups(params)
}
async findNotificationContexts(
info: ConnectionInfo,
params: FindNotificationContextParams,
queryId?: QueryId
): Promise<NotificationContext[]> {
this.createSession(info)
const result = await this.db.findContexts(params)
if (queryId != null && info.sessionId != null && info.sessionId !== '') {
this.subscribeContextQuery(info, queryId, params, result)
}
return result
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async findNotifications(info: ConnectionInfo, params: FindNotificationsParams, _?: QueryId): Promise<Notification[]> {
this.createSession(info)
return await this.db.findNotifications(params)
}
async event(info: ConnectionInfo, event: RequestEvent): Promise<EventResult> {
this.permissions.validate(info, event)
this.createSession(info)
const eventResult = await this.eventProcessor.process(info, event)
const { result, responseEvent } = eventResult
if (responseEvent !== undefined) {
void this.next(info, responseEvent)
}
return result
return result ?? {}
}
subscribeQuery(info: ConnectionInfo, type: QueryType, queryId: number, params: Record<string, any>): void {
const { sessionId } = info
const data = this.dataBySessionId.get(sessionId) ?? {
messageQueries: new Map(),
notificationQueries: new Map(),
contextQueries: new Map()
}
if (!this.dataBySessionId.has(sessionId)) {
this.dataBySessionId.set(sessionId, data)
private subscribeMessageQuery(info: ConnectionInfo, queryId: QueryId, params: Record<string, any>): void {
const data = this.createSession(info)
if (data == null) return
data.messageQueries.set(queryId, params as FindMessagesParams)
}
private subscribeContextQuery(
info: ConnectionInfo,
queryId: QueryId,
params: FindNotificationContextParams,
result: NotificationContext[]
): void {
if (params.notifications == null) return
const data = this.createSession(info)
if (data == null) return
const cards = new Set(result.map((it) => it.card))
const current = data.contextQueries.get(queryId) ?? new Set()
data.contextQueries.set(queryId, new Set([...current, ...cards]))
}
private createSession(info: ConnectionInfo): SessionInfo | undefined {
if (info.sessionId == null) return
if (!this.dataBySessionId.has(info.sessionId)) {
this.dataBySessionId.set(info.sessionId, {
account: info.account.uuid,
messageQueries: new Map(),
contextQueries: new Map()
})
}
if (type === 'message') {
data.messageQueries.set(queryId, params as FindMessagesParams)
} else if (type === 'notification') {
data.notificationQueries.set(queryId, params)
} else if (type === 'context') {
data.contextQueries.set(queryId, params)
}
return this.dataBySessionId.get(info.sessionId)
}
unsubscribeQuery(info: ConnectionInfo, queryId: number): void {
if (info.sessionId == null) return
const data = this.dataBySessionId.get(info.sessionId)
if (data == null) return
data.messageQueries.delete(queryId)
data.notificationQueries.delete(queryId)
data.contextQueries.delete(queryId)
}
@@ -121,13 +164,35 @@ export class Manager {
this.dataBySessionId.delete(sessionId)
}
async next(info: ConnectionInfo, event: ResponseEvent): Promise<void> {
await this.responseEvent(event)
const derived = await this.triggers.process(event, info)
close(): void {
this.db.close()
}
private async apply(info: ConnectionInfo, derivedRequests: RequestEvent[]): Promise<void> {
const result = await Promise.all(derivedRequests.map((it) => this.eventProcessor.process(info, it)))
const derived = result.flatMap((it) => it.responseEvent).filter((it): it is ResponseEvent => it !== undefined)
await Promise.all(derived.map((it) => this.next(info, it)))
}
private async responseEvent(event: ResponseEvent): Promise<void> {
private async execute(info: ConnectionInfo, request: RequestEvent): Promise<EventResult> {
const result = await this.eventProcessor.process(info, request)
if (result.responseEvent != null) {
void this.next(info, result.responseEvent)
}
return result.result ?? {}
}
private async next(info: ConnectionInfo, event: ResponseEvent): Promise<void> {
await this.broadcastEvent(event)
await this.triggers.process(
event,
info,
(events) => this.apply(info, events),
(event) => this.execute(info, event)
)
}
private async broadcastEvent(event: ResponseEvent): Promise<void> {
const sessionIds: string[] = []
for (const [sessionId, session] of this.dataBySessionId.entries()) {
if (this.match(event, session)) {
@@ -148,109 +213,87 @@ export class Manager {
switch (event.type) {
case ResponseEventType.MessageCreated:
return this.matchMessagesQuery(
{ id: event.message.id, card: event.message.card },
Array.from(info.messageQueries.values())
{ ids: [event.message.id], card: event.message.card },
Array.from(info.messageQueries.values()),
new Set(info.contextQueries.values().flatMap((it) => Array.from(it)))
)
case ResponseEventType.PatchCreated:
return this.matchMessagesQuery(
{ card: event.card, id: event.patch.message },
Array.from(info.messageQueries.values())
{ card: event.card, ids: [event.patch.message] },
Array.from(info.messageQueries.values()),
new Set(info.contextQueries.values().flatMap((it) => Array.from(it)))
)
case ResponseEventType.MessageRemoved:
case ResponseEventType.MessagesRemoved:
return this.matchMessagesQuery(
{ card: event.card, id: event.message },
Array.from(info.messageQueries.values())
{ card: event.card, ids: event.messages },
Array.from(info.messageQueries.values()),
new Set(info.contextQueries.values().flatMap((it) => Array.from(it)))
)
case ResponseEventType.ReactionCreated:
return this.matchMessagesQuery(
{ card: event.card, id: event.reaction.message },
Array.from(info.messageQueries.values())
{ card: event.card, ids: [event.reaction.message] },
Array.from(info.messageQueries.values()),
new Set()
)
case ResponseEventType.ReactionRemoved:
return this.matchMessagesQuery(
{ card: event.card, id: event.message },
Array.from(info.messageQueries.values())
{ card: event.card, ids: [event.message] },
Array.from(info.messageQueries.values()),
new Set()
)
case ResponseEventType.AttachmentCreated:
case ResponseEventType.FileCreated:
return this.matchMessagesQuery(
{ card: event.card, id: event.attachment.message },
Array.from(info.messageQueries.values())
{ card: event.card, ids: [event.file.message] },
Array.from(info.messageQueries.values()),
new Set()
)
case ResponseEventType.AttachmentRemoved:
case ResponseEventType.FileRemoved:
return this.matchMessagesQuery(
{ card: event.card, id: event.message },
Array.from(info.messageQueries.values())
{ card: event.card, ids: [event.message] },
Array.from(info.messageQueries.values()),
new Set()
)
case ResponseEventType.ThreadCreated:
return this.matchMessagesQuery(
{ card: event.thread.card, id: event.thread.message },
Array.from(info.messageQueries.values())
{ card: event.thread.card, ids: [event.thread.message] },
Array.from(info.messageQueries.values()),
new Set()
)
case ResponseEventType.NotificationCreated:
// return (
// info.personalWorkspace === event.personalWorkspace &&
// this.matchNotificationQuery(event, Array.from(info.notificationQueries.values()))
// )
return false
case ResponseEventType.NotificationRemoved:
// return info.personalWorkspace === event.personalWorkspace && info.notificationQueries.size > 0
return false
return info.account === event.account
case ResponseEventType.NotificationsRemoved:
return info.account === event.account
case ResponseEventType.NotificationContextCreated:
// return (
// info.personalWorkspace === event.context.personalWorkspace &&
// this.matchContextQuery(event, Array.from(info.contextQueries.values()))
// )
return false
return info.account === event.context.account
case ResponseEventType.NotificationContextRemoved:
// return info.personalWorkspace === event.personalWorkspace && info.contextQueries.size > 0
return false
return info.account === event.account
case ResponseEventType.NotificationContextUpdated:
// return info.personalWorkspace === event.personalWorkspace && info.contextQueries.size > 0
return false
return info.account === event.account
case ResponseEventType.MessagesGroupCreated:
return false
case ResponseEventType.AddedCollaborators:
return true
case ResponseEventType.RemovedCollaborators:
return true
case ResponseEventType.ThreadUpdated:
return false
}
}
private matchMessagesQuery(params: { id?: MessageID; card?: string }, queries: FindMessagesParams[]): boolean {
private matchMessagesQuery(
params: { ids: MessageID[]; card: CardID },
queries: FindMessagesParams[],
cards: Set<CardID>
): boolean {
if (cards.has(params.card)) return true
if (queries.length === 0) return false
for (const query of queries) {
if (query.id != null && query.id !== params.id) continue
if (query.id != null && !params.ids.includes(query.id)) continue
if (query.card != null && query.card !== params.card) continue
return true
}
return false
}
private matchNotificationQuery(event: NotificationCreatedEvent, queries: FindNotificationsParams[]): boolean {
if (queries.length === 0) return false
for (const query of queries) {
if (query.context != null && query.context !== event.notification.context) continue
if (query.message != null && query.message !== event.notification.message.id) continue
if (query.read != null && query.read !== event.notification.read) continue
if (query.archived != null && query.archived !== event.notification.archived) continue
return true
}
return false
}
private matchContextQuery(event: NotificationContextCreatedEvent, queries: FindNotificationContextParams[]): boolean {
if (queries.length === 0) return false
for (const query of queries) {
if (query.id != null && query.id !== event.context.id) continue
if (query.card != null && query.card !== event.context.card) continue
return true
}
return false
}
close(): void {
this.db.close()
}
}
+18 -1
View File
@@ -1,9 +1,26 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
export interface Metadata {
msg2fileUrl: string
accountsUrl: string
}
export function getMetadata(): Metadata {
return {
msg2fileUrl: process.env.MSG2FILE_URL ?? ''
msg2fileUrl: process.env.MSG2FILE_URL ?? '',
accountsUrl: process.env.ACCOUNTS_URL ?? ''
}
}
@@ -0,0 +1,174 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type CreateNotificationContextResult,
type RequestEvent,
RequestEventType,
type ResponseEvent,
ResponseEventType
} from '@hcengineering/communication-sdk-types'
import {MessageType, type AccountID, type CardID, type ContextID, type Message, type NotificationContext } from '@hcengineering/communication-types'
import type { TriggerCtx } from '../types'
import { findAccount } from '../utils'
const BATCH_SIZE = 500
export async function notify (ctx: TriggerCtx, event: ResponseEvent): Promise<RequestEvent[]> {
switch (event.type) {
case ResponseEventType.MessageCreated: {
return await notifyMessage(ctx, event.message)
}
}
return []
}
async function notifyMessage (ctx: TriggerCtx, message: Message): Promise<RequestEvent[]> {
const cursor = ctx.db.getCollaboratorsCursor(message.card, message.created, BATCH_SIZE)
const creatorAccount = await findAccount(ctx, message.creator)
const result: RequestEvent[] = []
let isFirstBatch = true
for await (const dbCollaborators of cursor) {
const collaborators: AccountID[] = dbCollaborators.map((it) => it.account)
const contexts: NotificationContext[] = await ctx.db.findContexts({
card: message.card,
account: isFirstBatch && collaborators.length < BATCH_SIZE ? undefined : collaborators
})
for (const collaborator of collaborators) {
try {
const context = contexts.find((it) => it.account === collaborator)
const res = await processCollaborator(ctx, message, collaborator, creatorAccount, context)
result.push(...res)
} catch (e) {
ctx.ctx.error('Error on create notification', { collaborator, error: e })
}
}
isFirstBatch = false
}
return result
}
async function processCollaborator (
ctx: TriggerCtx,
message: Message,
collaborator: AccountID,
creatorAccount?: AccountID,
context?: NotificationContext
): Promise<RequestEvent[]> {
const result: RequestEvent[] = []
const isOwn = creatorAccount === collaborator
const { contextId, events } = await createOrUpdateContext(ctx, message, collaborator, isOwn, context)
result.push(...events)
if (contextId == null || isOwn) return result
if(message.type !== MessageType.Message) return result
result.push({
type: RequestEventType.CreateNotification,
account: collaborator,
context: contextId,
message: message.id,
created: message.created
})
return result
}
async function createOrUpdateContext (
ctx: TriggerCtx,
message: Message,
collaborator: AccountID,
isOwn: boolean,
context?: NotificationContext
): Promise<{
contextId: ContextID | undefined
events: RequestEvent[]
}> {
if (context == null) {
const contextId = await createContext(
ctx,
collaborator,
message.card,
message.created,
isOwn ? message.created : undefined
)
return {
contextId,
events: []
}
}
const lastUpdate =
context.lastUpdate == null || message.created > context.lastUpdate ? message.created : context.lastUpdate
const lastView = isOwn && isContextRead(context) ? message.created : undefined
return {
contextId: context.id,
events: [
{
type: RequestEventType.UpdateNotificationContext,
context: context.id,
account: collaborator,
lastView,
lastUpdate
}
]
}
}
async function createContext (
ctx: TriggerCtx,
account: AccountID,
card: CardID,
lastUpdate: Date,
lastView?: Date
): Promise<ContextID | undefined> {
try {
const result = (await ctx.execute({
type: RequestEventType.CreateNotificationContext,
account,
card,
lastUpdate,
lastView: lastView ?? new Date(lastUpdate.getTime() - 1)
})) as CreateNotificationContextResult
return result.id
} catch (e) {
return (
await ctx.db.findContexts({
account,
card
})
)[0]?.id
}
}
function isContextRead (context: NotificationContext): boolean {
const { lastView, lastUpdate } = context
if (lastView == null) {
return false
}
return lastView >= lastUpdate
}
+66
View File
@@ -0,0 +1,66 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type AccountID, type SocialID } from '@hcengineering/communication-types'
import { type ConnectionInfo, type RequestEvent, RequestEventType } from '@hcengineering/communication-sdk-types'
import { type Account, systemAccountUuid } from '@hcengineering/core'
export class Permissions {
validate(info: ConnectionInfo, event: RequestEvent): void {
switch (event.type) {
case RequestEventType.CreateMessage:
case RequestEventType.CreatePatch:
case RequestEventType.CreateReaction:
case RequestEventType.RemoveReaction:
case RequestEventType.RemoveFile:
case RequestEventType.CreateFile: {
this.checkSocialId(info.account, event.creator)
return
}
case RequestEventType.RemoveNotifications:
case RequestEventType.CreateNotificationContext:
case RequestEventType.UpdateNotificationContext:
case RequestEventType.RemoveNotificationContext: {
this.checkAccount(info.account, event.account)
return
}
case RequestEventType.CreateMessagesGroup:
case RequestEventType.RemoveMessagesGroup: {
this.onlySystemAccount(info.account)
break
}
default:
break
}
}
private checkSocialId(account: Account, creator: SocialID): void {
if (!account.socialIds.includes(creator) && systemAccountUuid !== account.uuid) {
throw new Error('Forbidden')
}
}
private checkAccount(account: Account, creator: AccountID): void {
if (account.uuid !== creator && systemAccountUuid !== account.uuid) {
throw new Error('Forbidden')
}
}
private onlySystemAccount(account: Account): void {
if (systemAccountUuid !== account.uuid) {
throw new Error('Forbidden')
}
}
}
+193 -66
View File
@@ -14,118 +14,247 @@
//
import {
type MessageCreatedEvent,
type ConnectionInfo,
type CreatePatchEvent,
type DbAdapter,
type EventResult,
type FileCreatedEvent,
type FileRemovedEvent,
type MessageCreatedEvent,
type MessagesGroupCreatedEvent,
type MessagesRemovedEvent,
type PatchCreatedEvent,
type RequestEvent,
RequestEventType,
type ResponseEvent,
ResponseEventType,
type MessageRemovedEvent,
type ConnectionInfo,
type PatchCreatedEvent,
type MessagesGroupCreatedEvent
type UpdateThreadEvent
} from '@hcengineering/communication-sdk-types'
import { type WorkspaceID, PatchType, type Patch, type CardID } from '@hcengineering/communication-types'
import { concatLink, systemAccountUuid } from '@hcengineering/core'
import { type CardID, PatchType, type WorkspaceID, type File } from '@hcengineering/communication-types'
import { concatLink, type MeasureContext, systemAccountUuid } from '@hcengineering/core'
import { generateToken } from '@hcengineering/server-token'
import type { Metadata } from './metadata.ts'
import { notify } from './notification/notification'
import { type TriggerCtx } from './types.js'
import { findAccount } from './utils'
export class Triggers {
private readonly registeredCards: Set<CardID> = new Set()
private readonly registeredCards = new Set<CardID>()
constructor(
private readonly ctx: MeasureContext,
private readonly metadata: Metadata,
private readonly db: DbAdapter,
private readonly workspace: WorkspaceID
) {}
async process(event: ResponseEvent, info: ConnectionInfo): Promise<ResponseEvent[]> {
try {
switch (event.type) {
case ResponseEventType.MessageCreated:
return await this.onMessageCreated(event)
case ResponseEventType.MessageRemoved:
return await this.onMessageRemoved(event, info)
case ResponseEventType.PatchCreated:
return await this.onPatchCreated(event, info)
case ResponseEventType.MessagesGroupCreated:
return await this.onMessagesGroupCreated(event)
}
} catch (err: any) {
console.error(err)
return []
async process(
event: ResponseEvent,
info: ConnectionInfo,
apply: (events: RequestEvent[]) => Promise<void>,
execute: (event: RequestEvent) => Promise<EventResult>
): Promise<void> {
const ctx: TriggerCtx = {
ctx: this.ctx,
metadata: this.metadata,
db: this.db,
workspace: this.workspace,
account: info.account,
execute
}
return []
await this.applySyncTriggers(ctx, event, apply)
void this.createNotification(ctx, event).then((it) => {
void apply(it)
})
}
async onMessagesGroupCreated(event: MessagesGroupCreatedEvent): Promise<ResponseEvent[]> {
private async applySyncTriggers(
ctx: TriggerCtx,
event: ResponseEvent,
apply: (events: RequestEvent[]) => Promise<void>
): Promise<void> {
let events: RequestEvent[] = []
try {
switch (event.type) {
case ResponseEventType.MessageCreated: {
events = await this.onMessageCreated(ctx, event)
break
}
case ResponseEventType.MessagesRemoved: {
events = await this.onMessagesRemoved(ctx, event)
break
}
case ResponseEventType.PatchCreated: {
events = await this.onPatchCreated(ctx, event)
break
}
case ResponseEventType.FileCreated: {
events = await this.onFileCreated(ctx, event)
break
}
case ResponseEventType.FileRemoved: {
events = await this.onFileRemoved(ctx, event)
break
}
case ResponseEventType.MessagesGroupCreated: {
events = await this.onMessagesGroupCreated(ctx, event)
break
}
}
await apply(events)
} catch (err: any) {
console.error(err)
}
}
private async createNotification(ctx: TriggerCtx, event: ResponseEvent): Promise<RequestEvent[]> {
return await notify(ctx, event)
}
private async onMessagesGroupCreated(_: TriggerCtx, event: MessagesGroupCreatedEvent): Promise<RequestEvent[]> {
this.registeredCards.delete(event.group.card)
return []
}
async onMessageRemoved(event: MessageRemovedEvent, info: ConnectionInfo): Promise<ResponseEvent[]> {
private async onMessagesRemoved(ctx: TriggerCtx, event: MessagesRemovedEvent): Promise<RequestEvent[]> {
const { card } = event
const thread = await this.db.findThread(card)
if (thread === undefined) return []
const date = new Date()
const socialId = info.account.primarySocialId
const socialId = ctx.account.primarySocialId
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 [
{
type: ResponseEventType.PatchCreated,
return event.messages.flatMap(() => {
const patchEvent: CreatePatchEvent = {
type: RequestEventType.CreatePatch,
patchType: PatchType.removeReply,
card: thread.card,
patch
message: thread.message,
content: thread.thread,
creator: socialId
}
]
const threadEvent: UpdateThreadEvent = {
type: RequestEventType.UpdateThread,
thread: thread.thread,
replies: 'decrement'
}
return [patchEvent, threadEvent]
})
}
async onMessageCreated(event: MessageCreatedEvent): Promise<ResponseEvent[]> {
return (await Promise.all([this.registerCard(event.message.card), this.updateThread(event)])).flat()
private async onMessageCreated(ctx: TriggerCtx, event: MessageCreatedEvent): Promise<RequestEvent[]> {
void this.registerCard(event.message.card)
return [...(await this.addCollaborators(ctx, event)), ...(await this.addThreadReply(event))]
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async onPatchCreated(event: PatchCreatedEvent, info: ConnectionInfo): Promise<ResponseEvent[]> {
return this.registerCard(event.card)
private async addCollaborators(ctx: TriggerCtx, event: MessageCreatedEvent): Promise<RequestEvent[]> {
const { creator } = event.message
const account = await findAccount(ctx, creator)
if (account === undefined) {
return []
}
const collaborator = (
await this.db.findCollaborators({
card: event.message.card,
account,
limit: 1
})
)[0]
return collaborator != null
? []
: [
{
type: RequestEventType.AddCollaborators,
card: event.message.card,
collaborators: [account],
date: event.message.created
}
]
}
async updateThread(event: MessageCreatedEvent): Promise<ResponseEvent[]> {
private async onPatchCreated(_: TriggerCtx, event: PatchCreatedEvent): Promise<RequestEvent[]> {
void this.registerCard(event.card)
return []
}
private async addThreadReply(event: MessageCreatedEvent): Promise<RequestEvent[]> {
const { message } = event
const thread = await this.db.findThread(message.card)
if (thread === undefined) return []
const date = new Date()
const patch: Patch = {
message: thread.message,
type: PatchType.addReply,
content: thread.thread,
creator: message.creator,
created: date
if (thread === undefined) {
return []
}
await this.db.updateThread(thread.thread, date, 'increment')
await this.db.createPatch(thread.card, patch.message, patch.type, patch.content, patch.creator, patch.created)
return [
{
type: ResponseEventType.PatchCreated,
type: RequestEventType.CreatePatch,
patchType: PatchType.addReply,
card: thread.card,
patch
message: thread.message,
content: thread.thread,
creator: message.creator
},
{
type: RequestEventType.UpdateThread,
thread: thread.thread,
lastReply: event.message.created,
replies: 'increment'
}
]
}
async registerCard(card: CardID): Promise<ResponseEvent[]> {
if (this.registeredCards.has(card) || this.metadata.msg2fileUrl === '') return []
private async onFileCreated(_: TriggerCtx, event: FileCreatedEvent): Promise<RequestEvent[]> {
const message = (await this.db.findMessages({ card: event.card, id: event.file.message, limit: 1 }))[0]
if (message !== undefined) return []
const { file } = event
const patchContent: Omit<File, 'card' | 'message' | 'created' | 'creator'> = {
blobId: file.blobId,
type: file.type,
filename: file.filename,
size: file.size
}
return [
{
type: RequestEventType.CreatePatch,
patchType: PatchType.addFile,
card: event.card,
message: file.message,
content: JSON.stringify(patchContent),
creator: file.creator
}
]
}
private async onFileRemoved(_: TriggerCtx, event: FileRemovedEvent): Promise<RequestEvent[]> {
const message = (await this.db.findMessages({ card: event.card, id: event.message, limit: 1 }))[0]
if (message !== undefined) return []
const { blobId } = event
return [
{
type: RequestEventType.CreatePatch,
patchType: PatchType.removeFile,
card: event.card,
message: event.message,
content: JSON.stringify({ blobId }),
creator: event.creator
}
]
}
private async registerCard(card: CardID): Promise<void> {
if (this.registeredCards.has(card) || this.metadata.msg2fileUrl === '') {
return
}
try {
const token = generateToken(systemAccountUuid, this.workspace)
@@ -140,7 +269,5 @@ export class Triggers {
} catch (e) {
console.error(e)
}
return []
}
}
+31
View File
@@ -0,0 +1,31 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { Account, MeasureContext } from '@hcengineering/core'
import type { DbAdapter, EventResult, RequestEvent } from '@hcengineering/communication-sdk-types'
import type { WorkspaceID } from '@hcengineering/communication-types'
import type { Metadata } from './metadata'
export interface TriggerCtx {
ctx: MeasureContext
metadata: Metadata
db: DbAdapter
workspace: WorkspaceID
account: Account
execute: (event: RequestEvent) => Promise<EventResult>
}
export type QueryId = string | number
+71
View File
@@ -0,0 +1,71 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { AccountID, SocialID } from '@hcengineering/communication-types'
import { generateToken } from '@hcengineering/server-token'
import { systemAccountUuid } from '@hcengineering/core'
import type { TriggerCtx } from './types'
export async function findAccount(ctx: TriggerCtx, socialString: SocialID): Promise<AccountID | undefined> {
if (ctx.account.socialIds.includes(socialString)) {
return ctx.account.uuid
}
const token = generateToken(systemAccountUuid)
// const account = getAccountClient(ctx.metadata.accountsUrl, token)
try {
//TODO: FIXME
return await fetchAccount(socialString, ctx.metadata.accountsUrl, token)
} catch (err: any) {
ctx.ctx.warn('Cannot find account', { socialString, err })
}
}
//TODO: replace with AccountClient
async function fetchAccount(socialId: SocialID, url: string, token: string): Promise<AccountID | undefined> {
const body = {
method: 'findPersonBySocialId' as const,
params: { socialId, requireAccount: true }
}
const request: RequestInit = {
keepalive: true,
headers: {
...(token === undefined
? {}
: {
Authorization: 'Bearer ' + token
})
}
}
const response = await fetch(url, {
...request,
headers: {
...request.headers,
'Content-Type': 'application/json'
},
method: 'POST',
body: JSON.stringify(body)
})
const result = await response.json()
if (result.error != null) {
throw Error(result.error)
}
return result.result as AccountID | undefined
}
+3 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-shared",
"version": "0.1.61",
"version": "0.1.147",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "./types/index.d.ts",
@@ -15,15 +15,13 @@
"bundle:node": "bun build src/index.ts --target node --format cjs --outfile dist/index.cjs"
},
"devDependencies": {
"@types/bun": "^1.1.14",
"@types/js-yaml": "^4.0.9"
"@types/bun": "^1.1.14"
},
"peerDependencies": {
"typescript": "^5.6.3"
},
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
"js-yaml": "^4.1.0"
"@hcengineering/communication-types": "workspace:*"
},
"repository": {
"type": "git",
+1 -1
View File
@@ -15,4 +15,4 @@
export * from './message.ts'
export * from './retry.ts'
export * from './files.ts'
export * from './patch.ts'
+5 -6
View File
@@ -15,25 +15,24 @@
import type { MessageID } from '@hcengineering/communication-types'
let lastTimestamp = 0
let lastTimestamp = 0n
let counter = 0n
export function generateMessageId(): MessageID {
const timestamp = Math.floor(Date.now() / 1000)
const timestamp = BigInt(Math.floor(Date.now() / 1000))
if (timestamp !== lastTimestamp) {
lastTimestamp = timestamp
counter = 0n
}
const id = (BigInt(timestamp) << 20n) | counter
counter++
const id = (timestamp << 20n) | (counter << 10n) | BigInt(Math.floor(Math.random() * 1024))
return id.toString() as MessageID
}
export function parseMessageId(messageId: MessageID): Date {
const timestamp = Number(BigInt(messageId) >> 20n)
return new Date(timestamp * 1000)
return new Date(Number(BigInt(messageId) >> 20n) * 1000)
}
+135
View File
@@ -0,0 +1,135 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
PatchType,
type BlobID,
type CardID,
type Message,
type Patch,
type Reaction,
type SocialID,
type File
} from '@hcengineering/communication-types'
type PatchFile = Pick<File, 'blobId' | 'type' | 'filename' | 'size'>
export function applyPatches(message: Message, patches: Patch[], allowedPatchTypes: PatchType[] = []): Message {
if (patches.length === 0) return message
for (const p of patches) {
message = applyPatch(message, p, allowedPatchTypes)
}
return message
}
export function applyPatch(message: Message, patch: Patch, allowedPatchTypes: PatchType[] = []): Message {
if (allowedPatchTypes.length > 0 && !allowedPatchTypes.includes(patch.type)) return 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)
case PatchType.addFile:
return addFile(message, JSON.parse(patch.content) as PatchFile, patch.created, patch.creator)
case PatchType.removeFile:
return removeFile(message, patch.content as BlobID)
}
return message
}
function addReaction(message: Message, reaction: Reaction): Message {
message.reactions.push(reaction)
return message
}
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
}
}
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
}
}
}
function addFile(message: Message, file: PatchFile, created: Date, creator: SocialID): Message {
message.files.push({ ...file, card: message.card, message: message.id, created, creator })
return message
}
function removeFile(message: Message, blobId: BlobID): Message {
const files = message.files.filter((it) => it.blobId !== blobId)
if (files.length === message.files.length) return message
return {
...message,
files
}
}
function removeReply(message: Message, thread: CardID): Message {
if (message.thread === undefined || message.thread.thread !== thread) return message
return {
...message,
thread: {
...message.thread,
repliesCount: message.thread.repliesCount - 1
}
}
}
-29
View File
@@ -1,29 +0,0 @@
{
"name": "@hcengineering/communication-sqlite-wasm",
"version": "0.1.61",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./types/index.d.ts",
"files": [
"dist/index.js",
"dist/index.cjs",
"types/**/*.d.ts"
],
"scripts": {
"bundle": "bun run bundle:browser",
"bundle:browser": "bun build src/index.ts --outdir dist --target browser"
},
"devDependencies": {
"@types/bun": "^1.1.14"
},
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
"@sqlite.org/sqlite-wasm": "^3.47.1-build1",
"path": "^0.12.7",
"uuid": "^11.0.3"
},
"peerDependencies": {
"typescript": "^5.6.3"
}
}
-190
View File
@@ -1,190 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type Message,
type FindMessagesParams,
type CardID,
type RichText,
type SocialID,
type MessageID,
type ContextID,
type NotificationContextUpdate,
type FindNotificationsParams,
type FindNotificationContextParams,
type NotificationContext,
type Notification,
type BlobID,
type MessagesGroup,
type FindMessagesGroupsParams
} from '@hcengineering/communication-types'
import type { DbAdapter } from '@hcengineering/communication-sdk-types'
import { initializeSQLite, type Sqlite3Worker1Promiser } from './connection'
import { applyMigrations } from './migrations.ts'
import { MessagesDb } from './db/message.ts'
import { NotificationsDb } from './db/notification.ts'
//TODO: FIXME
//export class SqliteAdapter implements DbAdapter
export class SqliteAdapter {
private readonly message: MessagesDb
private readonly notification: NotificationsDb
constructor(
private readonly worker: Sqlite3Worker1Promiser,
private readonly dbId: string
) {
this.message = new MessagesDb(worker, dbId)
this.notification = new NotificationsDb(worker, dbId)
}
async createMessage(
workspace: string,
card: CardID,
content: RichText,
creator: SocialID,
created: Date
): Promise<MessageID> {
return await this.message.createMessage(workspace, card, content, creator, created)
}
async createPatch(
workspace: string,
card: CardID,
message: MessageID,
content: RichText,
creator: SocialID,
created: Date
): Promise<void> {
//TODO: FIXME
return await this.message.createPatch(message, content, creator, created)
}
async removeMessage(workspace: string, card: CardID, id: MessageID): Promise<MessageID> {
await this.message.removeMessage(id)
return id
}
async removeMessages(workspace: string, card: CardID, ids: MessageID[]): Promise<MessageID[]> {
//TODO: implement
return ids
}
/* eslint-disable @typescript-eslint/no-unused-vars */
async createMessagesGroup(
workspace: string,
card: CardID,
blobId: BlobID,
from_date: Date,
to_date: Date,
count: number
): Promise<void> {
//TODO: implement
}
async findMessagesGroups(workspace: string, params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
//TODO: implement
return []
}
async createReaction(
workspace: string,
card: CardID,
message: MessageID,
reaction: string,
creator: SocialID,
created: Date
): Promise<void> {
//TODO: FIXME
return await this.message.createReaction(message, reaction, creator, created)
}
async removeReaction(
workspace: string,
card: CardID,
message: MessageID,
reaction: string,
creator: SocialID
): Promise<void> {
//TODO: FIXME
return await this.message.removeReaction(message, reaction, creator)
}
async createAttachment(message: MessageID, attachment: CardID, creator: SocialID, created: Date): Promise<void> {
return await this.message.createAttachment(message, attachment, creator, created)
}
async removeAttachment(message: MessageID, attachment: CardID): Promise<void> {
return await this.message.removeAttachment(message, attachment)
}
async findMessages(workspace: string, params: FindMessagesParams): Promise<Message[]> {
return await this.message.find(workspace, params)
}
async createNotification(message: MessageID, context: ContextID): Promise<void> {
return await this.notification.createNotification(message, context)
}
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
return await this.notification.removeNotification(message, context)
}
async createContext(
workspace: string,
card: CardID,
personalWorkspace: string,
lastView?: Date,
lastUpdate?: Date
): Promise<ContextID> {
return await this.notification.createContext(workspace, card, personalWorkspace, lastView, lastUpdate)
}
async removeContext(context: ContextID): Promise<void> {
return await this.notification.removeContext(context)
}
async updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
return await this.notification.updateContext(context, update)
}
async findContexts(
params: FindNotificationContextParams,
personalWorkspaces: string[],
workspace?: string
): Promise<NotificationContext[]> {
return await this.notification.findContexts(params, personalWorkspaces, workspace)
}
async findNotifications(
params: FindNotificationsParams,
personalWorkspace: string,
workspace?: string
): Promise<Notification[]> {
return await this.notification.findNotifications(params, personalWorkspace, workspace)
}
close(): void {
void this.worker('close')
}
}
export async function createDbAdapter(connectionString: string): Promise<DbAdapter> {
const { worker, dbId } = await initializeSQLite(connectionString)
await applyMigrations(worker, dbId)
//TODO: FIXME
return new SqliteAdapter(worker, dbId) as unknown as DbAdapter
}
-71
View File
@@ -1,71 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
// @ts-expect-error error
import { sqlite3Worker1Promiser } from '@sqlite.org/sqlite-wasm'
export type Sqlite3Worker1Promiser = {
(
command: 'config-get',
params: object
): Promise<{
result: {
version: {
libVersion: string
}
}
}>
(
command: 'open',
params: { filename: string }
): Promise<{
dbId: string
}>
(
command: 'exec',
params: {
dbId: string
sql: string
callback?: (row: SqlResult | null | undefined) => void
}
): Promise<void>
(command: 'close'): Promise<void>
}
type SqlResult = {
columnNames: string[]
row: any[] | null | undefined
rowNumber: number | null | undefined
}
export async function initializeSQLite(
connectionString: string
): Promise<{ worker: Sqlite3Worker1Promiser; dbId: string }> {
const promiser: Sqlite3Worker1Promiser = await new Promise((resolve) => {
const _promiser = sqlite3Worker1Promiser({
onready: () => resolve(_promiser)
})
})
const configResponse = await promiser('config-get', {})
console.log('SQLite3 config', configResponse.result)
const { dbId } = await promiser('open', { filename: connectionString })
return { worker: promiser, dbId }
}
-77
View File
@@ -1,77 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type {Sqlite3Worker1Promiser} from "../connection";
export class BaseDb {
constructor(protected readonly worker: Sqlite3Worker1Promiser, protected readonly dbId: string) {
}
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 (${values.map((value) => `'${value}'`).join(', ')});
`
await this.worker('exec', {
dbId: this.dbId,
sql
});
}
async remove(table: string, where: Record<string, any>): Promise<void> {
const keys = Object.keys(where)
const values = Object.values(where)
const sql = `
DELETE
FROM ${table}
WHERE ${keys.map((k, idx) => `${k} = '${values[idx]}'`).join(' AND ')};`
await this.worker('exec', {
dbId: this.dbId,
sql
});
}
async select(sql: string): Promise<Record<string, any>[]> {
return new Promise(async (resolve) => {
const rows: Record<string, any>[] = [];
await this.worker('exec', {
dbId: this.dbId,
sql,
callback: (res) => {
if (res == null) {
resolve(rows);
return;
}
if (res.row == null || res.rowNumber == null) {
resolve(rows);
} else {
const rowObject: Record<string, any> = {};
res.columnNames.forEach((columnName, index) => {
rowObject[columnName] = res.row?.[index] ?? undefined
});
rows.push(rowObject);
}
},
});
});
}
}
-216
View File
@@ -1,216 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type Message,
type MessageID,
type CardID,
type FindMessagesParams,
SortingOrder,
type SocialID,
type RichText,
type Reaction, type Attachment
} from '@hcengineering/communication-types'
import {BaseDb} from './base.ts'
import {
TableName,
type MessageDb,
type AttachmentDb,
type ReactionDb,
type PatchDb
} from './types.ts'
export class MessagesDb extends BaseDb {
//Message
async createMessage(workspace: string, card: CardID, content: RichText, creator: SocialID, created: Date): Promise<MessageID> {
const dbData: MessageDb = {
id: self.crypto.randomUUID(),
workspace_id: workspace,
card_id: card,
content: content,
creator: creator,
created: created,
}
await this.insert(TableName.Message, dbData)
//TODO: FIX ME
return dbData.id as any as MessageID
}
async removeMessage(message: MessageID): Promise<void> {
await this.remove(TableName.Message, {id: message})
}
async createPatch(message: MessageID, content: RichText, creator: SocialID, created: Date): Promise<void> {
const dbData: PatchDb = {
id: self.crypto.randomUUID(),
message_id: message,
content: content,
creator: creator,
created: created
}
await this.insert(TableName.Patch, dbData)
}
//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)
}
async removeAttachment(message: MessageID, card: CardID): Promise<void> {
await this.remove(TableName.Attachment, {
message_id: message,
card_id: card
})
}
//Reaction
async createReaction(message: MessageID, reaction: string, creator: SocialID, created: Date): Promise<void> {
const dbData: ReactionDb = {
message_id: message,
reaction: reaction,
creator: creator,
created: created
}
await this.insert(TableName.Reaction, dbData)
}
async removeReaction(message: MessageID, reaction: string, creator: SocialID): Promise<void> {
await this.remove(TableName.Reaction, {
message_id: message,
reaction: reaction,
creator: creator
})
}
//Find messages
async find(workspace: string, params: FindMessagesParams): Promise<Message[]> {
const select = `SELECT m.id,
m.card_id,
m.content,
m.creator,
m.created,
json_group_array(
json_object(
'content', p.content,
'creator', p.creator,
'created', p.created
)
) AS patches,
json_group_array(
json_object(
'card_id', a.card_id,
'message_id', a.message_id,
'creator', a.creator,
'created', a.created
)
) AS attachments,
json_group_array(
json_object(
'message_id', r.message_id,
'reaction', r.reaction,
'creator', r.creator,
'created', r.created
)
) AS reactions
FROM ${TableName.Message} m
LEFT JOIN ${TableName.Patch} p ON p.message_id = m.id
LEFT JOIN ${TableName.Attachment} a ON a.message_id = m.id
LEFT JOIN ${TableName.Reaction} r ON r.message_id = m.id`
const where = this.buildMessageWhere(workspace, params)
const groupBy = `GROUP BY m.id`
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(' ')
const result = await this.select(sql)
return result.map(it => this.toMessage(it))
}
buildMessageWhere(workspace: string, params: FindMessagesParams): string {
const where: string[] = [`m.workspace_id = '${workspace}'`]
if (params.card != null) {
where.push(`m.card_id = '${params.card}'`)
}
if (params.id != null) {
where.push(`m.id = '${params.id}'`)
}
//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 ')}`
}
private toMessage(row: any): Message {
const patches = JSON.parse(row.patches).filter((it: any) => it.created != null)
const attachments = JSON.parse(row.attachments).filter((it: any) => it.created != null)
const reactions = JSON.parse(row.reactions).filter((it: any) => it.created != null)
const lastPatch = patches?.[0]
return {
id: row.id,
card: row.card_id,
content: lastPatch?.content ?? row.content,
creator: row.creator,
created: new Date(row.created),
edited: (lastPatch?.created ?? row.created),
reactions: (reactions ?? []).map((it: any) => this.toReaction(it)),
attachments: (attachments ?? []).map((it: any) => this.toAttachment(it))
}
}
private toReaction(row: any): Reaction {
return {
message: row.message_id,
reaction: row.reaction,
creator: row.creator,
created: new Date(row.created)
}
}
private toAttachment(row: any): Attachment {
return {
message: row.message_id,
card: row.card,
creator: row.creator,
created: new Date(row.created)
}
}
}
-238
View File
@@ -1,238 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type MessageID,
type ContextID,
type CardID,
type NotificationContext,
type FindNotificationContextParams, SortingOrder,
type FindNotificationsParams, type Notification,
type NotificationContextUpdate
} from '@hcengineering/communication-types'
import {BaseDb} from './base.ts'
import {TableName, type ContextDb, type NotificationDb} from './types.ts'
export class NotificationsDb extends BaseDb {
async createNotification(message: MessageID, context: ContextID): Promise<void> {
const dbData: NotificationDb = {
message_id: message,
context_id: context
}
await this.insert(TableName.Notification, dbData)
}
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
await this.remove(TableName.Notification, {
message_id: message,
context
})
}
async createContext(workspace: string, card: CardID, personalWorkspace: string, lastView?: Date, lastUpdate?: Date): Promise<ContextID> {
const dbData: ContextDb = {
id: self.crypto.randomUUID(),
workspace_id: workspace,
card_id: card,
personal_workspace: personalWorkspace,
last_view: lastView,
last_update: lastUpdate
}
await this.insert(TableName.NotificationContext, dbData)
return dbData.id as ContextID
}
async removeContext(context: ContextID): Promise<void> {
await this.remove(TableName.NotificationContext, {
id: 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}" = '${values[idx]}'`).join(', ')}
WHERE id = '${context}'
`;
await this.worker('exec', {
dbId: this.dbId,
sql
});
}
async findContexts(params: FindNotificationContextParams, personalWorkspaces: string[], workspace?: string,): Promise<NotificationContext[]> {
const select = `
SELECT nc.id,
nc.card_id,
nc.archived_from,
nc.last_view,
nc.last_update,
nc.workspace_id,
nc.personal_workspace
FROM ${TableName.NotificationContext} nc`;
const where = 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(' ')
const result = await this.select(sql)
return result.map(it => this.toNotificationContext(it));
}
async findNotifications(params: FindNotificationsParams, personalWorkspace: string, workspace?: string): Promise<Notification[]> {
//TODO: should join with attachments and reactions?
const select = `
SELECT n.message_id,
n.context_id,
m.card_id AS message_card,
m.content AS message_content,
m.creator AS message_creator,
m.created AS message_created,
nc.card_id,
nc.archived_from,
nc.last_view,
nc.last_update,
json_group_array(
json_object(
'id', p.id,
'content', p.content,
'creator', p.creator,
'created', p.created
)
) AS patches
FROM ${TableName.Notification} n
JOIN
${TableName.NotificationContext} nc ON n.context_id = nc.id
JOIN
${TableName.Message} m ON n.message_id = m.id
LEFT JOIN
${TableName.Patch} p ON p.message_id = m.id
`;
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.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}`
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
const sql = [select, where, groupBy, orderBy, limit].join(' ')
const result = await this.select(sql)
return result.map(it => this.toNotification(it));
}
buildContextWhere(params: FindNotificationContextParams, personalWorkspaces: string[], workspace?: string,): string {
const where: string[] = []
if (workspace != null) {
where.push(`nc.workspace_id = '${workspace}'`)
}
if (personalWorkspaces.length > 0) {
where.push(`nc.personal_workspace IN (${personalWorkspaces.map(it => `'${it}'`).join(', ')})`)
}
if (params.card != null) {
where.push(`nc.card_id = '${params.card}'`)
}
return `WHERE ${where.join(' AND ')}`
}
buildNotificationWhere(params: FindNotificationsParams, personalWorkspace: string, workspace?: string): string {
const where: string[] = [`nc.personal_workspace = '${personalWorkspace}'`]
if (workspace != null) {
where.push(`nc.workspace_id = '${workspace}'`)
}
if (params.context != null) {
where.push(`n.context_id = '${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.join(' AND ')}`
}
toNotificationContext(row: any): NotificationContext {
return {
id: row.id,
card: row.card_id,
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,
workspace: row.workspace,
personalWorkspace: row.personal_workspace
}
}
toNotification(row: any): Notification {
const patches = JSON.parse(row.patches).filter((p: any) => p.created != null)
const lastPatch = patches[patches.length - 1]
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.message_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: [],
attachments: []
},
context: row.context_id,
read: lastView != null && lastView >= created,
archived: archivedFrom != null && archivedFrom >= created
}
}
}
-72
View File
@@ -1,72 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type {CardID, ContextID, MessageID, RichText, SocialID } from "@hcengineering/communication-types"
export enum TableName {
Message = 'message',
Patch = 'patch',
Attachment = 'attachment',
Reaction = 'reaction',
Notification = 'notification',
NotificationContext = 'notification_context'
}
export interface MessageDb {
id: string
workspace_id: string,
card_id: string,
content: RichText,
creator: SocialID,
created: Date,
}
export interface PatchDb {
id: string,
message_id: MessageID,
content: RichText,
creator: SocialID,
created: Date,
}
export interface ReactionDb {
message_id: MessageID,
reaction: string,
creator: SocialID
created: Date
}
export interface AttachmentDb {
message_id: MessageID,
card_id: CardID,
creator: SocialID
created: Date
}
export interface NotificationDb {
message_id: MessageID,
context_id: ContextID
}
export interface ContextDb {
id: string
workspace_id: string
card_id: CardID
personal_workspace: string
archived_from?: Date
last_view?: Date
last_update?: Date
}
-130
View File
@@ -1,130 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { Sqlite3Worker1Promiser } from './connection'
export async function applyMigrations(worker: Sqlite3Worker1Promiser, dbId: string): Promise<void> {
await migrationV1(worker, dbId)
}
async function migrationV1(worker: Sqlite3Worker1Promiser, dbId: string): Promise<void> {
await worker('exec', {
dbId,
sql: `
CREATE TABLE IF NOT EXISTS message
(
id TEXT NOT NULL,
workspace_id TEXT NOT NULL,
card_id TEXT NOT NULL,
content TEXT NOT NULL,
creator TEXT NOT NULL,
created DATETIME NOT NULL,
PRIMARY KEY (id)
)
`
})
await worker('exec', {
dbId,
sql: `
CREATE TABLE IF NOT EXISTS patch
(
id TEXT NOT NULL,
message_id TEXT NOT NULL,
content TEXT NOT NULL,
creator VARCHAR(255) NOT NULL,
created DATETIME NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE
)
`
})
await worker('exec', {
dbId,
sql: `CREATE INDEX IF NOT EXISTS idx_patch_message_id ON patch (message_id)`
})
await worker('exec', {
dbId,
sql: `
CREATE TABLE IF NOT EXISTS attachment
(
message_id TEXT NOT NULL,
card_id TEXT NOT NULL,
creator VARCHAR(255) NOT NULL,
created DATETIME NOT NULL,
PRIMARY KEY (message_id, card_id),
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE
)
`
})
await worker('exec', {
dbId,
sql: `CREATE INDEX IF NOT EXISTS attachment_message_idx ON attachment (message_id)`
})
await worker('exec', {
dbId,
sql: `
CREATE TABLE IF NOT EXISTS reaction
(
message_id TEXT NOT NULL,
reaction TEXT NOT NULL,
creator VARCHAR(255) NOT NULL,
created DATETIME NOT NULL,
PRIMARY KEY (message_id, creator, reaction),
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE
)
`
})
await worker('exec', {
dbId,
sql: `CREATE INDEX IF NOT EXISTS reaction_message_idx ON reaction (message_id)`
})
await worker('exec', {
dbId,
sql: `
CREATE TABLE IF NOT EXISTS notification_context
(
id TEXT NOT NULL,
workspace_id TEXT NOT NULL,
card_id TEXT NOT NULL,
personal_workspace TEXT NOT NULL,
archived_from DATETIME,
last_view DATETIME,
last_update DATETIME,
PRIMARY KEY (id),
UNIQUE (workspace_id, card_id, personal_workspace)
);
CREATE TABLE IF NOT EXISTS notification
(
message_id TEXT NOT NULL,
context_id TEXT NOT NULL,
PRIMARY KEY (message_id, context_id),
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE,
FOREIGN KEY (context_id) REFERENCES notification_context (id) ON DELETE CASCADE
);
`
})
}
-10
View File
@@ -1,10 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declarationDir": "./types",
"emitDeclarationOnly": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/communication-types",
"version": "0.1.61",
"version": "0.1.147",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "./types/index.d.ts",
@@ -21,8 +21,8 @@
"typescript": "^5.6.3"
},
"dependencies": {
"@hcengineering/core": "0.7.15",
"@hcengineering/card": "0.7.15"
"@hcengineering/core": "^0.7.28",
"@hcengineering/card": "^0.7.28"
},
"repository": {
"type": "git",
@@ -13,7 +13,14 @@
// limitations under the License.
//
import { createMessagesQuery, createNotificationsQuery } from '@hcengineering/communication-client-query'
import type { Ref, Blob, AccountUuid, WorkspaceUuid, PersonId } from '@hcengineering/core'
import type { Card } from '@hcengineering/card'
export * from './client'
export { createMessagesQuery, createNotificationsQuery }
export type BlobID = Ref<Blob>
export type CardID = Ref<Card>
export type SocialID = PersonId
export type WorkspaceID = WorkspaceUuid
export type RichText = string
export type AccountID = AccountUuid
export type ID = string
+16 -3
View File
@@ -13,7 +13,8 @@
// limitations under the License.
//
import type { CardID, Message, MessageID, RichText, SocialID } from './message'
import type { BlobID, CardID, RichText, SocialID } from './core'
import type { Message, MessageID, MessageType, MessageData } from './message'
export interface FileMetadata {
card: CardID
@@ -24,12 +25,24 @@ export interface FileMetadata {
export interface FileMessage {
id: MessageID
type: MessageType
content: RichText
creator: SocialID
data?: MessageData
created: Date
edited?: Date
reactions: FileReaction[]
files: FileBlob[]
thread?: FileThread
}
export interface FileBlob {
blobId: BlobID
type: string
filename: string
size: number
creator: SocialID
created: Date
reactions: FileReaction[]
thread?: FileThread
}
export interface FileReaction {
+2 -1
View File
@@ -13,7 +13,8 @@
// limitations under the License.
//
export * from './core.ts'
export * from './file.ts'
export * from './message.ts'
export * from './notification.ts'
export * from './query.ts'
export * from './file.ts'
+59 -18
View File
@@ -13,38 +13,73 @@
// limitations under the License.
//
import type { Ref, Blob, PersonId, WorkspaceUuid } from '@hcengineering/core'
import type { Card } from '@hcengineering/card'
import type { Attribute, Class, Mixin, Ref } from '@hcengineering/core'
import type { BlobID, CardID, ID, RichText, SocialID } from './core'
import type { Card, Tag } from '@hcengineering/card'
export type BlobID = Ref<Blob>
export type CardID = Ref<Card>
export type SocialID = PersonId
export type WorkspaceID = WorkspaceUuid
export type RichText = string
export type ID = string
export type MessageID = string & { message: true }
export type MessageID = ID & { message: true }
export interface Message {
id: MessageID
card: CardID
type: MessageType
content: RichText
creator: SocialID
created: Date
data?: MessageData
edited?: Date
thread?: Thread
reactions: Reaction[]
attachments: Attachment[]
files: File[]
}
export enum MessageType {
Message = 'message',
Activity = 'activity'
}
export type MessageData = ActivityMessageData | any
export interface ActivityMessage extends Message {
type: MessageType.Activity
data: ActivityMessageData
}
export interface ActivityMessageData {
action: 'create' | 'remove' | 'update'
update?: ActivityUpdate
}
export type ActivityUpdate = ActivityAttributeUpdate | ActivityTagUpdate
export enum ActivityUpdateType {
Attribute = 'attribute',
Tag = 'tag'
}
export interface ActivityTagUpdate {
type: ActivityUpdateType.Tag
tag: Ref<Tag>
action: 'add' | 'remove'
}
type AttributeValue = string | number | null
export interface ActivityAttributeUpdate {
type: ActivityUpdateType.Attribute
attrKey: string
attrClass: Ref<Class<Attribute<Card>>>
mixin?: Ref<Mixin<Card>>
set?: AttributeValue | AttributeValue[]
added?: AttributeValue[]
removed?: AttributeValue[]
}
export interface MessagesGroup {
card: CardID
blobId: BlobID
fromId: MessageID
toId: MessageID
fromDate: Date
toDate: Date
fromSec: Date
toSec: Date
count: number
patches?: Patch[]
}
@@ -62,7 +97,9 @@ export enum PatchType {
addReaction = 'addReaction',
removeReaction = 'removeReaction',
addReply = 'addReply',
removeReply = 'removeReply'
removeReply = 'removeReply',
addFile = 'addFile',
removeFile = 'removeFile'
}
export interface Reaction {
@@ -72,9 +109,13 @@ export interface Reaction {
created: Date
}
export interface Attachment {
message: MessageID
export interface File {
card: CardID
message: MessageID
blobId: BlobID
type: string
filename: string
size: number
creator: SocialID
created: Date
}
+18 -15
View File
@@ -13,29 +13,32 @@
// limitations under the License.
//
import type { Message, CardID, WorkspaceID } from './message'
import type { AccountID, CardID, ID } from './core'
import type { Message, MessageID, MessagesGroup, Patch } from './message'
export type ContextID = string & { context: true }
export type ContextID = ID & { context: true }
export type NotificationID = ID & { notification: true }
export interface Collaborator {
account: AccountID
}
export interface Notification {
message: Message
id: NotificationID
context: ContextID
read: boolean
archived: boolean
created: Date
messageId?: MessageID
message?: Message
messageGroup?: MessagesGroup
patches?: Patch[]
}
export interface NotificationContext {
id: ContextID
card: CardID
workspace: WorkspaceID
personalWorkspace: WorkspaceID
archivedFrom?: Date
lastView?: Date
lastUpdate?: Date
}
export interface NotificationContextUpdate {
archivedFrom?: Date
lastView?: Date
lastUpdate?: Date
account: AccountID
lastUpdate: Date
lastView: Date
notifications?: Notification[]
}
+38 -23
View File
@@ -15,24 +15,23 @@
import { SortingOrder } from '@hcengineering/core'
import type { BlobID, CardID, MessageID } from './message'
import type { MessageID } from './message'
import type { ContextID } from './notification'
import type { AccountID, BlobID, CardID } from './core'
export { SortingOrder }
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[]
loadNextPage(): Promise<void>
loadPrevPage(): Promise<void>
hasNextPage(): boolean
hasPrevPage(): boolean
}
@@ -44,27 +43,43 @@ interface FindParams {
export interface FindMessagesParams extends FindParams {
id?: MessageID
card: CardID
created?: Exclusive<Record<ComparisonOperator, Date>> | Date
}
export interface FindNotificationsParams extends FindParams {
context?: ContextID
message?: MessageID
read?: boolean
archived?: boolean
created?: Exclusive<Record<ComparisonOperator, Date>> | Date
}
export interface FindNotificationContextParams extends FindParams {
id?: ContextID
card?: CardID
files?: boolean
reactions?: boolean
replies?: boolean
created?: Partial<Record<ComparisonOperator, Date>> | Date
}
export interface FindMessagesGroupsParams extends FindParams {
card: CardID
blobId?: BlobID
withPatches?: boolean
fromDate?: Partial<Record<ComparisonOperator, Date>> | Date
toDate?: Partial<Record<ComparisonOperator, Date>> | Date
orderBy?: 'fromDate' | 'toDate'
patches?: boolean
fromSec?: Partial<Record<ComparisonOperator, Date>> | Date
toSec?: Partial<Record<ComparisonOperator, Date>> | Date
orderBy?: 'fromSec' | 'toSec'
}
export interface FindNotificationContextParams extends FindParams {
id?: ContextID
card?: CardID
lastUpdate?: Partial<Record<ComparisonOperator, Date>> | Date
account?: AccountID | AccountID[]
notifications?: {
message?: boolean
limit: number
order: SortingOrder
read?: boolean
}
}
export interface FindNotificationsParams extends FindParams {
context?: ContextID
read?: boolean
created?: Partial<Record<ComparisonOperator, Date>> | Date
account?: AccountID | AccountID[]
message?: boolean
}
export interface FindCollaboratorsParams extends FindParams {
card: CardID
account?: AccountID | AccountID[]
}
@@ -1,7 +1,7 @@
{
"name": "@hcengineering/communication-client-ws",
"version": "0.1.61",
"main": "dist/index.js",
"name": "@hcengineering/communication-yaml",
"version": "0.1.147",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "./types/index.d.ts",
"files": [
@@ -10,21 +10,22 @@
"types/**/*.d.ts"
],
"scripts": {
"bundle": "bun run bundle:browser",
"bundle:browser": "bun build src/index.ts --outdir dist --target browser"
"bundle": "bun run bundle:browser && bun run bundle:node",
"bundle:browser": "bun build src/index.ts --outdir dist --target browser",
"bundle:node": "bun build src/index.ts --target node --format cjs --outfile dist/index.cjs"
},
"devDependencies": {
"@types/bun": "^1.1.14"
},
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/communication-client-query": "workspace:*",
"@hcengineering/communication-sdk-types": "workspace:*",
"@msgpack/msgpack": "^3.0.0-beta2"
"@types/bun": "^1.1.14",
"@types/js-yaml": "^4.0.9"
},
"peerDependencies": {
"typescript": "^5.6.3"
},
"dependencies": {
"@hcengineering/communication-types": "workspace:*",
"@hcengineering/communication-shared": "workspace:*",
"js-yaml": "^4.1.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/hcengineering/communication.git"
+49
View File
@@ -0,0 +1,49 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { FileMessage, Message } from '@hcengineering/communication-types'
export function deserializeMessage(message: Message): FileMessage {
return {
id: message.id,
type: message.type,
content: message.content,
edited: message.edited,
creator: message.creator,
created: message.created,
data: message.data,
thread:
message.thread != null
? {
thread: message.thread.thread,
repliesCount: message.thread.repliesCount,
lastReply: message.thread.lastReply
}
: undefined,
files: message.files.map((file) => ({
blobId: file.blobId,
type: file.type,
filename: file.filename,
size: file.size,
created: file.created,
creator: file.creator
})),
reactions: message.reactions.map((reaction) => ({
reaction: reaction.reaction,
creator: reaction.creator,
created: reaction.created
}))
}
}
@@ -13,4 +13,5 @@
// limitations under the License.
//
export * from './adapter.ts'
export * from './parse.ts'
export * from './deserialize.ts'
@@ -13,6 +13,7 @@
// limitations under the License.
//
import { retry, type RetryOptions } from '@hcengineering/communication-shared'
import type {
FileMessage,
FileMetadata,
@@ -22,8 +23,6 @@ import type {
} from '@hcengineering/communication-types'
import yaml from 'js-yaml'
import { retry, type RetryOptions } from './retry'
export async function loadGroupFile(
workspace: WorkspaceID,
filesUrl: string,
@@ -64,11 +63,13 @@ export function parseYaml(data: string): ParsedFile {
metadata,
messages: messages.map((message) => ({
id: message.id,
type: message.type,
card: metadata.card,
content: message.content,
edited: message.edited,
creator: message.creator,
created: message.created,
data: message.data,
thread: message.thread
? {
card: metadata.card,
@@ -78,7 +79,11 @@ export function parseYaml(data: string): ParsedFile {
lastReply: message.thread.lastReply
}
: undefined,
attachments: [],
files: message.files.map((file) => ({
...file,
message: message.id,
card: metadata.card
})),
reactions: message.reactions.map((reaction) => ({
message: message.id,
reaction: reaction.reaction,