diff --git a/bun.lock b/bun.lock
index e341aec615..fc721f8680 100644
--- a/bun.lock
+++ b/bun.lock
@@ -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=="],
diff --git a/package.json b/package.json
index 7e3a9002f7..5e298dc5c5 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/packages/client-query/package.json b/packages/client-query/package.json
index 853d4cd8a9..430773b9b1 100644
--- a/packages/client-query/package.json
+++ b/packages/client-query/package.json
@@ -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",
diff --git a/packages/client-query/src/index.ts b/packages/client-query/src/index.ts
index ee5d04972e..5ebcd40b15 100644
--- a/packages/client-query/src/index.ts
+++ b/packages/client-query/src/index.ts
@@ -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)
}
diff --git a/packages/client-query/src/query.ts b/packages/client-query/src/query.ts
index 76a7df614e..45331d3155 100644
--- a/packages/client-query/src/query.ts
+++ b/packages/client-query/src/query.ts
@@ -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
, C extends QueryCallback> {
private oldQuery: P | undefined
- private oldCallback: QueryCallback | 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
+> {
+ override createQuery(
+ params: FindNotificationContextParams,
+ callback: QueryCallback
+ ): {
+ unsubscribe: () => void
+ } {
+ return this.lq.queryNotificationContexts(params, callback)
+ }
+}
diff --git a/packages/client-sqlite/package.json b/packages/client-sqlite/package.json
deleted file mode 100644
index fb27de5dd5..0000000000
--- a/packages/client-sqlite/package.json
+++ /dev/null
@@ -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"
- }
-}
diff --git a/packages/client-sqlite/src/client.ts b/packages/client-sqlite/src/client.ts
deleted file mode 100644
index 7884b77a4d..0000000000
--- a/packages/client-sqlite/src/client.ts
+++ /dev/null
@@ -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 {
- 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 {
- 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 {
- 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 {
- 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 {
- 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 {
- await this.db.removeAttachment(message, card)
- this.onEvent({ type: ResponseEventType.AttachmentRemoved, message, card, attachment })
- }
-
- async findMessages(params: FindMessagesParams): Promise {
- const rawMessages = await this.db.findMessages(params)
- return rawMessages.map((it) => this.toMessage(it))
- }
-
- async findMessagesGroups(params: FindMessagesGroupsParams): Promise {
- return await this.db.findMessagesGroups(params)
- }
-
- async findMessage(params: FindMessagesParams): Promise {
- 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 {
- await this.db.createNotification(message, context)
- }
-
- async removeNotification(message: MessageID, context: ContextID): Promise {
- await this.db.removeNotification(message, context)
- }
-
- async createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise {
- return await this.db.createContext(this.personalWorkspace, card, lastView, lastUpdate)
- }
-
- async updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise {
- await this.db.updateContext(context, update)
- }
-
- async removeNotificationContext(context: ContextID): Promise {
- await this.db.removeContext(context)
- }
-
- async findNotificationContexts(params: FindNotificationContextParams): Promise {
- //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 {
- //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 {
- //TODO: implement
- }
-
- async unsubscribeQuery() {
- //ignore
- }
-
- close() {
- this.db.close()
- }
-}
-
-export async function getSqliteClient(
- workspace: WorkspaceID,
- personalWorkspace: WorkspaceID,
- dbUrl = 'file:communication.sqlite3?vfs=opfs'
-): Promise {
- const db = await createSqliteDbAdapter(dbUrl)
- return new DbClient(db, workspace, personalWorkspace) as unknown as Client
-}
diff --git a/packages/client-sqlite/tsconfig.json b/packages/client-sqlite/tsconfig.json
deleted file mode 100644
index 3ae07cd3fa..0000000000
--- a/packages/client-sqlite/tsconfig.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "extends": "../../tsconfig.json",
- "compilerOptions": {
- "jsx": "react-jsx",
- "outDir": "./dist",
- "rootDir": "./src"
- },
- "include": ["src"]
-}
diff --git a/packages/client-ws/src/client.ts b/packages/client-ws/src/client.ts
deleted file mode 100644
index 3fa8e0101d..0000000000
--- a/packages/client-ws/src/client.ts
+++ /dev/null
@@ -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 {
- 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 {
- const event: RemoveMessageEvent = {
- type: RequestEventType.RemoveMessage,
- card,
- message
- }
- await this.sendEvent(event)
- }
-
- async updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise {
- 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 {
- 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 {
- 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 {
- const event: CreateAttachmentEvent = {
- type: RequestEventType.CreateAttachment,
- card,
- message,
- attachment,
- creator
- }
- await this.sendEvent(event)
- }
-
- async removeAttachment(card: CardID, message: MessageID, attachment: CardID): Promise {
- const event: RemoveAttachmentEvent = {
- type: RequestEventType.RemoveAttachment,
- card,
- message,
- attachment
- }
- await this.sendEvent(event)
- }
-
- async findMessages(params: FindMessagesParams, queryId?: number): Promise {
- return await this.ws.send('findMessages', [params, queryId])
- }
-
- async findMessagesGroups(params: FindMessagesGroupsParams): Promise {
- return await this.ws.send('findMessagesGroups', [params])
- }
-
- async createNotification(message: MessageID, context: ContextID): Promise {
- const event: CreateNotificationEvent = {
- type: RequestEventType.CreateNotification,
- message,
- context
- }
- await this.sendEvent(event)
- }
-
- async removeNotification(message: MessageID, context: ContextID): Promise {
- const event: RemoveNotificationEvent = {
- type: RequestEventType.RemoveNotification,
- message,
- context
- }
- await this.sendEvent(event)
- }
-
- async createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise {
- 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 {
- const event: RemoveNotificationContextEvent = {
- type: RequestEventType.RemoveNotificationContext,
- context
- }
- await this.sendEvent(event)
- }
-
- async updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise {
- const event: UpdateNotificationContextEvent = {
- type: RequestEventType.UpdateNotificationContext,
- context,
- update
- }
- await this.sendEvent(event)
- }
-
- async findNotificationContexts(
- params: FindNotificationContextParams,
- queryId?: number
- ): Promise {
- return await this.ws.send('findNotificationContexts', [params, queryId])
- }
-
- async findNotifications(params: FindNotificationsParams, queryId?: number): Promise {
- return await this.ws.send('findNotifications', [params, queryId])
- }
-
- async unsubscribeQuery(id: number): Promise {
- await this.ws.send('unsubscribeQuery', [id])
- }
-
- private async sendEvent(event: RequestEvent): Promise {
- 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 {
- //TODO: implement
- }
-
- close() {
- void this.ws.close()
- }
-}
-
-export async function getWebsocketClient(
- url: string,
- token: string,
- workspace: WorkspaceID,
- filesUrl: string
-): Promise {
- const client = new WsClient(url, token)
- initLiveQueries(client, workspace, filesUrl)
- return client
-}
diff --git a/packages/client-ws/src/connection.ts b/packages/client-ws/src/connection.ts
deleted file mode 100644
index 9d0a8a8054..0000000000
--- a/packages/client-ws/src/connection.ts
+++ /dev/null
@@ -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
- 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 {
- if (this.ws instanceof Promise) {
- this.ws = await this.ws
- }
- return this.ws
- }
-
- async send(method: string, params: any[]): Promise {
- const id = ++this.lastId
- return await this.sendRequest({ id: id.toString(), method, params })
- }
-
- private async sendRequest(request: Request): Promise {
- 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 {
- 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())
- }
-}
diff --git a/packages/cockroach/migrations/01_message.sql b/packages/cockroach/migrations/01_message.sql
index d113fe2fd3..37f45dbef7 100644
--- a/packages/cockroach/migrations/01_message.sql
+++ b/packages/cockroach/migrations/01_message.sql
@@ -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)
diff --git a/packages/cockroach/migrations/02_patch.sql b/packages/cockroach/migrations/02_patch.sql
index 527df14bf0..1c2037b046 100644
--- a/packages/cockroach/migrations/02_patch.sql
+++ b/packages/cockroach/migrations/02_patch.sql
@@ -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)
);
diff --git a/packages/cockroach/migrations/03_attachment.sql b/packages/cockroach/migrations/03_attachment.sql
deleted file mode 100644
index 780002f335..0000000000
--- a/packages/cockroach/migrations/03_attachment.sql
+++ /dev/null
@@ -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);
diff --git a/packages/cockroach/migrations/03_files.sql b/packages/cockroach/migrations/03_files.sql
new file mode 100644
index 0000000000..0689245041
--- /dev/null
+++ b/packages/cockroach/migrations/03_files.sql
@@ -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);
diff --git a/packages/cockroach/migrations/06_notification.sql b/packages/cockroach/migrations/06_notification.sql
index df565632a1..88d60cd21f 100644
--- a/packages/cockroach/migrations/06_notification.sql
+++ b/packages/cockroach/migrations/06_notification.sql
@@ -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);
\ No newline at end of file
+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)
+);
\ No newline at end of file
diff --git a/packages/cockroach/package.json b/packages/cockroach/package.json
index 095bdf4c0d..4971913475 100644
--- a/packages/cockroach/package.json
+++ b/packages/cockroach/package.json
@@ -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",
diff --git a/packages/cockroach/src/adapter.ts b/packages/cockroach/src/adapter.ts
index f5d9a846a6..c471d49093 100644
--- a/packages/cockroach/src/adapter.ts
+++ b/packages/cockroach/src/adapter.ts
@@ -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 {
- return await this.message.createMessage(card, content, creator, created)
+ async createMessage(
+ card: CardID,
+ type: MessageType,
+ content: RichText,
+ creator: SocialID,
+ created: Date,
+ data?: any
+ ): Promise {
+ return await this.message.createMessage(card, type, content, creator, created, data)
}
- async removeMessage(card: CardID, message: MessageID, socialIds?: SocialID[]): Promise {
- await this.message.removeMessage(card, message, socialIds)
- }
-
- async removeMessages(card: CardID, fromId: MessageID, toId: MessageID): Promise {
- await this.message.removeMessages(card, fromId, toId)
+ async removeMessages(card: CardID, messages: MessageID[], socialIds?: SocialID[]): Promise {
+ 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 {
- await this.message.removePatches(card, fromId, toId)
- }
-
- async createMessagesGroup(
- card: CardID,
- blobId: BlobID,
- fromDate: Date,
- toDate: Date,
- fromId: MessageID,
- toId: MessageID,
- count: number
- ): Promise {
- await this.message.createMessagesGroup(card, blobId, fromDate, toDate, fromId, toId, count)
+ async createMessagesGroup(card: CardID, blobId: BlobID, fromDate: Date, toDate: Date, count: number): Promise {
+ await this.message.createMessagesGroup(card, blobId, fromDate, toDate, count)
}
async removeMessagesGroup(card: CardID, blobId: BlobID): Promise {
@@ -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 {
- 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 {
+ await this.message.createFile(card, message, blobId, fileType, filename, size, creator, created)
}
- async removeAttachment(message: MessageID, attachment: CardID): Promise {
- await this.message.removeAttachment(message, attachment)
+ async removeFile(card: CardID, message: MessageID, blobId: BlobID): Promise {
+ await this.message.removeFile(card, message, blobId)
}
async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise {
await this.message.createThread(card, message, thread, created)
}
- async updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise {
- await this.message.updateThread(thread, lastReply, op)
+ async updateThread(thread: CardID, op: 'increment' | 'decrement', lastReply?: Date): Promise {
+ await this.message.updateThread(thread, op, lastReply)
}
- async createNotification(message: MessageID, context: ContextID): Promise {
- await this.notification.createNotification(message, context)
- }
-
- async removeNotification(message: MessageID, context: ContextID): Promise {
- await this.notification.removeNotification(message, context)
- }
-
- async createContext(
- personalWorkspace: WorkspaceID,
- card: CardID,
- lastView?: Date,
- lastUpdate?: Date
- ): Promise {
- return await this.notification.createContext(personalWorkspace, card, lastView, lastUpdate)
- }
-
- async updateContext(context: ContextID, update: NotificationContextUpdate): Promise {
- await this.notification.updateContext(context, update)
- }
-
- async removeContext(context: ContextID): Promise {
- await this.notification.removeContext(context)
- }
-
- // Finds
async findMessages(params: FindMessagesParams): Promise {
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 {
- return await this.notification.findContexts(params, personalWorkspaces, workspace)
+ async addCollaborators(card: CardID, collaborators: AccountID[], date?: Date): Promise {
+ await this.notification.addCollaborators(card, collaborators, date)
}
- async findNotifications(
- params: FindNotificationsParams,
- personalWorkspace: WorkspaceID,
- workspace?: WorkspaceID
- ): Promise {
- return await this.notification.findNotifications(params, personalWorkspace, workspace)
+ async removeCollaborators(card: CardID, collaborators: AccountID[]): Promise {
+ await this.notification.removeCollaborators(card, collaborators)
+ }
+
+ async createNotification(context: ContextID, message: MessageID, messageCreated: Date): Promise {
+ return await this.notification.createNotification(context, message, messageCreated)
+ }
+
+ async removeNotification(context: ContextID, account: AccountID, untilDate: Date): Promise {
+ await this.notification.removeNotifications(context, account, untilDate)
+ }
+
+ async createContext(account: AccountID, card: CardID, lastUpdate: Date, lastView: Date): Promise {
+ return await this.notification.createContext(account, card, lastUpdate, lastView)
+ }
+
+ async updateContext(context: ContextID, account: AccountID, lastUpdate?: Date, lastView?: Date): Promise {
+ await this.notification.updateContext(context, account, lastUpdate, lastView)
+ }
+
+ async removeContext(context: ContextID, account: AccountID): Promise {
+ await this.notification.removeContext(context, account)
+ }
+
+ async findContexts(params: FindNotificationContextParams): Promise {
+ return await this.notification.findContexts(params)
+ }
+
+ async findNotifications(params: FindNotificationsParams): Promise {
+ return await this.notification.findNotifications(params)
+ }
+
+ async findCollaborators(params: FindCollaboratorsParams): Promise {
+ return await this.notification.findCollaborators(params)
}
close(): void {
this.sql.close()
}
+
+ getCollaboratorsCursor(card: CardID, date: Date, size?: number): AsyncIterable {
+ return this.notification.getCollaboratorsCursor(card, date, size)
+ }
}
export async function createDbAdapter(
@@ -197,12 +202,13 @@ export async function createDbAdapter(
options?: Options
): Promise {
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)[]>(query: string, params?: ParameterOrJSON[]): Promise {
- return await retry(() => this.fetch(query, params), { retries: 5 })
+ async execute(query: string, params?: SqlParams): Promise {
+ return await retry(() => this.fetch(query, params), { retries: 5 })
}
- private async fetch)[]>(
- query: string,
- params?: ParameterOrJSON[]
- ): Promise {
+ cursor(query: string, params?: SqlParams, size?: number): AsyncIterable[]> {
+ const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query
+
+ return this.sql.unsafe(sql).cursor(size)
+ }
+
+ close(): void {
+ // do nothing
+ }
+
+ private async fetch(query: string, params?: SqlParams): Promise {
const url = this.url.endsWith('/') ? this.url + 'api/v1/sql' : this.url + '/api/v1/sql'
const response = await fetch(url, {
@@ -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)[]>(query: string, params?: ParameterOrJSON[]): Promise {
+ async execute(query: string, params?: SqlParams): Promise {
const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query
- return await this.sql.unsafe(sql)
+ return await this.sql.unsafe(sql)
+ }
+
+ cursor(query: string, params?: SqlParams, size?: number): AsyncIterable[]> {
+ const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query
+
+ return this.sql.unsafe(sql).cursor(size)
}
close(): void {
diff --git a/packages/cockroach/src/connection.ts b/packages/cockroach/src/connection.ts
index f7223da5a7..7480097f54 100644
--- a/packages/cockroach/src/connection.ts
+++ b/packages/cockroach/src/connection.ts
@@ -103,7 +103,7 @@ export function connect(connectionString: string, database?: string): PostgresCl
database,
max: 5,
fetch_types: false,
- prepare: true,
+ prepare: false,
...extraOptions
})
diff --git a/packages/cockroach/src/db/base.ts b/packages/cockroach/src/db/base.ts
index 572c5c3e43..5f63b6b0e7 100644
--- a/packages/cockroach/src/db/base.ts
+++ b/packages/cockroach/src/db/base.ts
@@ -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)[]>(
- sql: string,
- params?: ParameterOrJSON[],
- name?: string
- ): Promise {
+ async execute>(sql: string, params?: ParameterOrJSON[], name?: string): Promise {
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)[]>(
+ private async executeWithLogs>(
name: string,
logger: Logger,
sql: string,
params?: ParameterOrJSON[]
- ): Promise {
+ ): Promise {
const start = performance.now()
try {
- return await this.client.execute(sql, params)
+ return await this.client.execute(sql, params)
} finally {
const time = performance.now() - start
logger.info(name, { time })
diff --git a/packages/cockroach/src/db/mapping.ts b/packages/cockroach/src/db/mapping.ts
new file mode 100644
index 0000000000..ee43477dc4
--- /dev/null
+++ b/packages/cockroach/src/db/mapping.ts
@@ -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
+ }
+}
diff --git a/packages/cockroach/src/db/message.ts b/packages/cockroach/src/db/message.ts
index e94d089650..5d020122b9 100644
--- a/packages/cockroach/src/db/message.ts
+++ b/packages/cockroach/src/db/message.ts
@@ -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 {
- 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 {
+ 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 {
- 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 {
+ 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 {
- 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 {
- 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 {
- 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 {
+ 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 {
- 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 {
+ 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 {
- 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 {
- const select = `SELECT m.id
+ async removeFile (card: CardID, message: MessageID, blobId: BlobID): Promise {
+ 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 {
+ 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 {
- const select = `SELECT m.id
+ async removeReaction (
+ card: CardID,
+ message: MessageID,
+ reaction: string,
+ creator: SocialID,
+ created: Date
+ ): Promise {
+ 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 {
- 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 {
+ 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 {
+ 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 {
- 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 {
- 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 {
+ 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 {
- 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 {
+ 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 {
+ 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 {
- // 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 {
- const sql = `SELECT t.card_id,
+ // Find thread
+ async findThread (thread: CardID): Promise {
+ 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 {
- const select = `
+ // Find messages groups
+ async findMessagesGroups (params: FindMessagesGroupsParams): Promise {
+ 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 }
+ }
}
diff --git a/packages/cockroach/src/db/notification.ts b/packages/cockroach/src/db/notification.ts
index 5f6a0e2dc5..94258d5927 100644
--- a/packages/cockroach/src/db/notification.ts
+++ b/packages/cockroach/src/db/notification.ts
@@ -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 {
- const db: NotificationDb = {
- message_id: message,
- context
+ async addCollaborators (card: CardID, collaborators: AccountID[], date?: Date): Promise {
+ 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 {
+ 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 {
- // 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 {
+ date: Date,
+ size?: number
+ ): AsyncIterable[]> {
+ 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(sql, [this.workspace, card, date], size)
+ }
+
+ async createNotification (context: ContextID, message: MessageID, created: Date): Promise {
+ const db: Omit = {
+ 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 {
+ 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 {
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 {
+ async removeContext (context: ContextID, account: AccountID): Promise {
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 {
+ async updateContext (context: ContextID, account: AccountID, lastUpdate?: Date, lastView?: Date): Promise {
const dbData: Partial = {}
- 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 {
- 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 {
+ 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 {
- // 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 {
+ 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 {
+ 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 }
}
}
diff --git a/packages/cockroach/src/db/schema.ts b/packages/cockroach/src/db/schema.ts
index 298a35ba9d..b09e608c0e 100644
--- a/packages/cockroach/src/db/schema.ts
+++ b/packages/cockroach/src/db/schema.ts
@@ -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
}
diff --git a/packages/cockroach/src/db/utils.ts b/packages/cockroach/src/db/utils.ts
index 950ef1bb5a..17a6fb9265 100644
--- a/packages/cockroach/src/db/utils.ts
+++ b/packages/cockroach/src/db/utils.ts
@@ -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 };
+}
\ No newline at end of file
diff --git a/packages/cockroach/src/types.ts b/packages/cockroach/src/types.ts
index f8545d4129..1e7a5280e9 100644
--- a/packages/cockroach/src/types.ts
+++ b/packages/cockroach/src/types.ts
@@ -15,6 +15,9 @@
import { type ParameterOrJSON, type Row } from 'postgres'
+export type SqlRow = Row & Iterable
+export type SqlParams = ParameterOrJSON[]
+
export interface Logger {
info: (message: string, data?: Record) => void
warn: (message: string, data?: Record) => void
@@ -26,6 +29,7 @@ export interface Options {
}
export interface SqlClient {
- execute: )[]>(query: string, params?: ParameterOrJSON[]) => Promise
+ execute: (query: string, params?: SqlParams) => Promise
+ cursor: (query: string, params?: SqlParams, size?: number) => AsyncIterable[]>
close: () => void
}
diff --git a/packages/cockroach/src/utils.ts b/packages/cockroach/src/utils.ts
index 6ffc522d3f..213f6d7f3f 100644
--- a/packages/cockroach/src/utils.ts
+++ b/packages/cockroach/src/utils.ts
@@ -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}`)
}
diff --git a/packages/examples/package.json b/packages/examples/package.json
deleted file mode 100644
index c83e4869f4..0000000000
--- a/packages/examples/package.json
+++ /dev/null
@@ -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"
- }
-}
diff --git a/packages/examples/src/httpServer.ts b/packages/examples/src/httpServer.ts
deleted file mode 100644
index 4320997f2b..0000000000
--- a/packages/examples/src/httpServer.ts
+++ /dev/null
@@ -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 })
- }
-})
diff --git a/packages/examples/src/index.html b/packages/examples/src/index.html
deleted file mode 100644
index 1eae9ec030..0000000000
--- a/packages/examples/src/index.html
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts
deleted file mode 100644
index d72db680f7..0000000000
--- a/packages/examples/src/index.ts
+++ /dev/null
@@ -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 | 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) {
- 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()
diff --git a/packages/query/package.json b/packages/query/package.json
index d2f5783f9c..0afa492501 100644
--- a/packages/query/package.json
+++ b/packages/query/package.json
@@ -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": {
diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts
index 45ba576822..d5362634f0 100644
--- a/packages/query/src/index.ts
+++ b/packages/query/src/index.ts
@@ -13,4 +13,5 @@
// limitations under the License.
//
-export * from './lq.ts'
+export * from './lq'
+export type { QueryClient } from './types'
diff --git a/packages/query/src/lq.ts b/packages/query/src/lq.ts
index 5ace34515d..e1b333a1cd 100644
--- a/packages/query/src/lq.ts
+++ b/packages/query/src/lq.ts
@@ -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()
@@ -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 {
for (const q of this.queries.values()) {
- await q.onEvent(event)
+ void q.onEvent(event)
+ }
+ }
+
+ async onRequest(event: RequestEvent, promise: Promise): Promise {
+ 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(
+ params,
+ callback,
+ MessagesQuery,
+ (params) => this.findMessagesQuery(params)
+ )
}
queryNotifications(params: FindNotificationsParams, callback: NotificationsQueryCallback): CreateQueryResult {
- const query = this.createNotificationQuery(params, callback)
+ return this.createAndStoreQuery(
+ params,
+ callback,
+ NotificationQuery,
+ (params) => this.findNotificationQuery(params)
+ )
+ }
+
+ queryNotificationContexts(params: FindNotificationContextParams, callback: any): CreateQueryResult {
+ return this.createAndStoreQuery(
+ params,
+ callback,
+ NotificationContextsQuery,
+ (params) => this.findNotificationContextsQuery(params)
+ )
+ }
+
+ private createAndStoreQuery>(
+ params: P,
+ callback: QueryCallback,
+ QueryClass: new (...args: any[]) => Q,
+ finder: (params: P) => Q | undefined
+ ): CreateQueryResult {
+ const query = this.createQuery(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>(
+ 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(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()
}
}
diff --git a/packages/query/src/messages/query.ts b/packages/query/src/messages/query.ts
index 877fe3363e..8097f79a34 100644
--- a/packages/query/src/messages/query.ts
+++ b/packages/query/src/messages/query.ts
@@ -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 {
- protected result: Promise> | QueryResult
+ private result: Promise> | QueryResult
private messagesFromFiles: Message[] = []
@@ -70,8 +78,10 @@ export class MessagesQuery implements PagedQuery {
hasGroups: true
}
+ private tmpMessages: Map = 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 {
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): void {
- this.callback = callback
- void this.notify()
+ async onEvent (event: ResponseEvent): Promise {
+ 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): Promise {
+ switch (event.type) {
+ case RequestEventType.CreateMessage: {
+ await this.onCreateMessageRequest(event, promise as Promise)
+ break
+ }
+ }
+ }
+
+ async onCreateMessageRequest(
+ event: CreateMessageEvent,
+ promise: Promise
+ ): Promise {
+ 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): void {
+ if (this.params.order === SortingOrder.Ascending) {
+ result.push(message);
+ } else {
+ result.unshift(message);
+ }
+ }
+
+ async unsubscribe (): Promise {
+ await this.client.unsubscribeQuery(this.id)
}
async requestLoadNextPage (): Promise {
@@ -152,22 +272,31 @@ export class MessagesQuery implements PagedQuery {
}
}
- private isInitLoadingForward (): boolean {
- const { order, created, id } = this.params
+ removeCallback (): void {
+ this.callback = () => {}
+ }
- if (id != null) {
- return false
+ setCallback (callback: QueryCallback): void {
+ this.callback = callback
+ void this.notify()
+ }
+
+ copyResult (): QueryResult | 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): Promise> {
@@ -190,7 +319,11 @@ export class MessagesQuery implements PagedQuery {
}
}
- 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 {
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 {
private async findPrevMessages (limit: number, result: QueryResult): Promise {
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 {
? 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 {
private async loadMessagesFromFiles (group: MessagesGroup): Promise {
const parsedFile = await loadGroupFile(this.workspace, this.filesUrl, group, { retries: 5 })
-
const patches = group.patches ?? []
const patchesMap = new Map()
@@ -392,36 +524,24 @@ export class MessagesQuery implements PagedQuery {
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 {
- 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 {
+ 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 {
@@ -435,7 +555,7 @@ export class MessagesQuery implements PagedQuery {
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 {
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 {
this.callback(new WindowImpl(result, this.result.isTail(), this.result.isHead(), this))
}
- async unsubscribe (): Promise {
- await this.client.unsubscribeQuery(this.id)
- }
-
- async onEvent (event: ResponseEvent): Promise {
- 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 {
+ private async onThreadCreatedEvent (event: ThreadCreatedEvent): Promise {
+ 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 {
})
}
- async onCreateMessageEvent (event: MessageCreatedEvent): Promise {
+ private async onMessageCreatedEvent (event: MessageCreatedEvent): Promise {
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 {
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 {
}
}
- 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 {
+ private async onPatchCreatedEvent (event: PatchCreatedEvent): Promise {
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 {
+ private async onMessagesRemovedEvent (event: MessagesRemovedEvent): Promise {
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 {
+ private async onReactionCreatedEvent (event: ReactionCreatedEvent): Promise {
+ 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 {
}
}
- private async onRemoveReactionEvent (event: ReactionRemovedEvent): Promise {
+ private async onReactionRemovedEvent (event: ReactionRemovedEvent): Promise