mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-11 04:07:43 +02:00
Notifications, files, queries, rest-client (#36)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
@@ -4,19 +4,19 @@
|
||||
"": {
|
||||
"name": "@hcengineering/communication",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/bun": "^1.2.4",
|
||||
"bun-types": "^1.2.4",
|
||||
"eslint": "^9.21.0",
|
||||
"@eslint/js": "^9.23.0",
|
||||
"@types/bun": "^1.2.5",
|
||||
"bun-types": "^1.2.5",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-prettier": "^5.2.4",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript-eslint": "^8.25.0",
|
||||
"typescript-eslint": "^8.27.0",
|
||||
},
|
||||
},
|
||||
"packages/client-query": {
|
||||
"name": "@hcengineering/communication-client-query",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.147",
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-query": "workspace:*",
|
||||
"@hcengineering/communication-sdk-types": "workspace:*",
|
||||
@@ -30,41 +30,9 @@
|
||||
"typescript": "^5.6.3",
|
||||
},
|
||||
},
|
||||
"packages/client-sqlite": {
|
||||
"name": "@hcengineering/communication-client-sqlite",
|
||||
"version": "0.1.61",
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-sdk-types": "workspace:*",
|
||||
"@hcengineering/communication-sqlite-wasm": "workspace:*",
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"fast-equals": "^5.0.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3",
|
||||
},
|
||||
},
|
||||
"packages/client-ws": {
|
||||
"name": "@hcengineering/communication-client-ws",
|
||||
"version": "0.1.61",
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-client-query": "workspace:*",
|
||||
"@hcengineering/communication-sdk-types": "workspace:*",
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3",
|
||||
},
|
||||
},
|
||||
"packages/cockroach": {
|
||||
"name": "@hcengineering/communication-cockroach",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.147",
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-sdk-types": "workspace:*",
|
||||
"@hcengineering/communication-shared": "workspace:*",
|
||||
@@ -79,27 +47,14 @@
|
||||
"typescript": "^5.6.3",
|
||||
},
|
||||
},
|
||||
"packages/examples": {
|
||||
"name": "@hcengineering/communication-examples",
|
||||
"version": "0.1.61",
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-client-ws": "workspace:*",
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3",
|
||||
},
|
||||
},
|
||||
"packages/query": {
|
||||
"name": "@hcengineering/communication-query",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.147",
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-sdk-types": "workspace:*",
|
||||
"@hcengineering/communication-shared": "workspace:*",
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"@hcengineering/communication-yaml": "workspace:*",
|
||||
"fast-equals": "^5.0.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -110,12 +65,30 @@
|
||||
"typescript": "^5.6.3",
|
||||
},
|
||||
},
|
||||
"packages/rest-client": {
|
||||
"name": "@hcengineering/communication-rest-client",
|
||||
"version": "0.1.147",
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-sdk-types": "workspace:*",
|
||||
"@hcengineering/communication-shared": "workspace:*",
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"@hcengineering/core": "^0.7.28",
|
||||
"snappyjs": "^0.7.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/snappyjs": "^0.7.1",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3",
|
||||
},
|
||||
},
|
||||
"packages/sdk-types": {
|
||||
"name": "@hcengineering/communication-sdk-types",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.147",
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"@hcengineering/core": "0.7.15",
|
||||
"@hcengineering/core": "^0.7.28",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
@@ -126,13 +99,14 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@hcengineering/communication-server",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.147",
|
||||
"dependencies": {
|
||||
"@hcengineering/account-client": "^0.7.28",
|
||||
"@hcengineering/communication-cockroach": "workspace:*",
|
||||
"@hcengineering/communication-sdk-types": "workspace:*",
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"@hcengineering/core": "0.7.15",
|
||||
"@hcengineering/server-token": "0.7.15",
|
||||
"@hcengineering/core": "^0.7.28",
|
||||
"@hcengineering/server-token": "^0.7.28",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
@@ -146,28 +120,9 @@
|
||||
},
|
||||
"packages/shared": {
|
||||
"name": "@hcengineering/communication-shared",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.147",
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"js-yaml": "^4.1.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3",
|
||||
},
|
||||
},
|
||||
"packages/sqlite-wasm": {
|
||||
"name": "@hcengineering/communication-sqlite-wasm",
|
||||
"version": "0.1.61",
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-sdk-types": "workspace:*",
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"@sqlite.org/sqlite-wasm": "^3.47.1-build1",
|
||||
"path": "^0.12.7",
|
||||
"uuid": "^11.0.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
@@ -178,10 +133,10 @@
|
||||
},
|
||||
"packages/types": {
|
||||
"name": "@hcengineering/communication-types",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.147",
|
||||
"dependencies": {
|
||||
"@hcengineering/card": "0.7.15",
|
||||
"@hcengineering/core": "0.7.15",
|
||||
"@hcengineering/card": "^0.7.28",
|
||||
"@hcengineering/core": "^0.7.28",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
@@ -190,6 +145,22 @@
|
||||
"typescript": "^5.6.3",
|
||||
},
|
||||
},
|
||||
"packages/yaml": {
|
||||
"name": "@hcengineering/communication-yaml",
|
||||
"version": "0.1.147",
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-shared": "workspace:*",
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"js-yaml": "^4.1.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
@@ -202,11 +173,13 @@
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.0", "", {}, "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ=="],
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.21.0", "", {}, "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw=="],
|
||||
"@eslint/js": ["@eslint/js@9.23.0", "", {}, "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
|
||||
|
||||
@@ -222,41 +195,39 @@
|
||||
|
||||
"@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.0", "", { "dependencies": { "tslib": "2" } }, "sha512-4rB4g+3hESy1bHSBG3tDFaMY2CH67iT7yne1e+0CLTsGLDcmoEWWpJjjpWVaYgYfYuohIRuo0E+N536gd2ZHZA=="],
|
||||
|
||||
"@hcengineering/analytics": ["@hcengineering/analytics@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/analytics/0.7.15/46f0006d8f4ba8ec04105770e6418251def15296", { "dependencies": { "@hcengineering/platform": "^0.7.15" } }, "sha512-U+tswirTFX0z7Sr51zfOEXbtrtfnf2xLzWTIrPip929g+oULJjCKMqoh+3HHVCF4HFm97x0P3qaRtW2hLgxyAQ=="],
|
||||
"@hcengineering/account-client": ["@hcengineering/account-client@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/account-client/0.7.28/32c9bc0dd297f00f4d5461467f25e072d01b9c76", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28" } }, "sha512-k1dkioX+jYrLyAXLLLsBYVCdZKf3WjhnDHw/yeAQgLQQ/9W2ffLtDMZ8lYWV3XMQ3eWrUCodtTDOiAQgXd/LEQ=="],
|
||||
|
||||
"@hcengineering/card": ["@hcengineering/card@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/card/0.7.15/70ba947306289e9ec1719ec4670abb5abadd9a74", { "dependencies": { "@hcengineering/core": "^0.7.15", "@hcengineering/platform": "^0.7.15", "@hcengineering/ui": "^0.7.15" } }, "sha512-xIM9UwFqhyjD9EJs0egn4kf3Q9Br8+spYHkzNl+5IPdL5d5itDIuy5HS9ng6XZ1BXApauo8MTl1DcaqJMneNbw=="],
|
||||
"@hcengineering/analytics": ["@hcengineering/analytics@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/analytics/0.7.28/a8faf3d575bb28b9682b659495e738f8e0ca596e", { "dependencies": { "@hcengineering/platform": "^0.7.28" } }, "sha512-+CI/wJ8zgop+3mEcvhlB1UJBU6tIKq2Y7qTichZjGli8BckEIEwbD4j2beu2qXRSoTT2yUGWM9zpS9+fmH/DAw=="],
|
||||
|
||||
"@hcengineering/card": ["@hcengineering/card@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/card/0.7.28/5348996f5b7b4a34517780a5bf324122cbddfbad", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "@hcengineering/ui": "^0.7.28" } }, "sha512-1d3allyx9jTpE/gr9kk/g5qo5848i4FKsRiHgCpJqzNEdRW7TAednoHAr2hKRU5DZC/i3Br56axSkUQzV2Idhg=="],
|
||||
|
||||
"@hcengineering/communication-client-query": ["@hcengineering/communication-client-query@workspace:packages/client-query"],
|
||||
|
||||
"@hcengineering/communication-client-sqlite": ["@hcengineering/communication-client-sqlite@workspace:packages/client-sqlite"],
|
||||
|
||||
"@hcengineering/communication-client-ws": ["@hcengineering/communication-client-ws@workspace:packages/client-ws"],
|
||||
|
||||
"@hcengineering/communication-cockroach": ["@hcengineering/communication-cockroach@workspace:packages/cockroach"],
|
||||
|
||||
"@hcengineering/communication-examples": ["@hcengineering/communication-examples@workspace:packages/examples"],
|
||||
|
||||
"@hcengineering/communication-query": ["@hcengineering/communication-query@workspace:packages/query"],
|
||||
|
||||
"@hcengineering/communication-rest-client": ["@hcengineering/communication-rest-client@workspace:packages/rest-client"],
|
||||
|
||||
"@hcengineering/communication-sdk-types": ["@hcengineering/communication-sdk-types@workspace:packages/sdk-types"],
|
||||
|
||||
"@hcengineering/communication-server": ["@hcengineering/communication-server@workspace:packages/server"],
|
||||
|
||||
"@hcengineering/communication-shared": ["@hcengineering/communication-shared@workspace:packages/shared"],
|
||||
|
||||
"@hcengineering/communication-sqlite-wasm": ["@hcengineering/communication-sqlite-wasm@workspace:packages/sqlite-wasm"],
|
||||
|
||||
"@hcengineering/communication-types": ["@hcengineering/communication-types@workspace:packages/types"],
|
||||
|
||||
"@hcengineering/core": ["@hcengineering/core@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/core/0.7.15/785cb20f9adc4a3a3c2179a0ae38316f4b4f933f", { "dependencies": { "@hcengineering/analytics": "^0.7.15", "@hcengineering/platform": "^0.7.15", "fast-equals": "^5.2.2" } }, "sha512-YhRRdtcSuNSQUdh4TO5u3u/w/vE7fcildc51wnL7Erw7VZ1rVANEx9a3mScnViVH89xw0oAcfDAm/AbH5+EZcQ=="],
|
||||
"@hcengineering/communication-yaml": ["@hcengineering/communication-yaml@workspace:packages/yaml"],
|
||||
|
||||
"@hcengineering/platform": ["@hcengineering/platform@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/platform/0.7.15/3e6c41c4abfd6425be49d9b31afef627dcafa4cb", { "dependencies": { "intl-messageformat": "^10.7.14" } }, "sha512-zv6afaQVu/T0MXrWch8JaxdVjLSUHDpIx3jgY1JE5xSy+FN8bjC30FR/RGavuMtm4z25rFsZQRPfr4qmZYZirQ=="],
|
||||
"@hcengineering/core": ["@hcengineering/core@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/core/0.7.28/43b84c63262e2b8fe5554c1967812e2ce6bb56e1", { "dependencies": { "@hcengineering/analytics": "^0.7.28", "@hcengineering/platform": "^0.7.28", "fast-equals": "^5.2.2" } }, "sha512-qv4QE7mkect6JYlGMljW5CQ2uU80hFGq0E17lZ5rO79h1boWEKLJpINv+w3J2BdR/nNG7taIsdZg3yfl9fY4bg=="],
|
||||
|
||||
"@hcengineering/server-token": ["@hcengineering/server-token@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/server-token/0.7.15/5443096926f96919ed8ed748e90e3474137519ec", { "dependencies": { "@hcengineering/core": "^0.7.15", "@hcengineering/platform": "^0.7.15", "jwt-simple": "^0.5.6" } }, "sha512-XEh0nDSSxMn34a9AHeXIgRh6iO025hKmGNXLZlXnm4kRIYODzMLhigds2H/RvmUjznfUTm9Bpy3EFLq9WFmzWg=="],
|
||||
"@hcengineering/platform": ["@hcengineering/platform@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/platform/0.7.28/2126ff97284b679ceab3c86f24febd73a3a28712", { "dependencies": { "intl-messageformat": "^10.7.14" } }, "sha512-0zEifwsLdOs/M6TZ9xnPrQTTlhwW28sooiyL4ZfQsiC2AJUy05vLP5/W8V9eDLta+EiR5bjtlulGsg1GfPjUoQ=="],
|
||||
|
||||
"@hcengineering/theme": ["@hcengineering/theme@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/theme/0.7.15/f02e70ea8421f0cd3e5b52c34ee69488d361b3e6", { "dependencies": { "@hcengineering/analytics": "^0.7.15", "@hcengineering/platform": "^0.7.15", "svelte": "^4.2.19" } }, "sha512-d+rX4Jp4J4YELBi763M5Zfe+3x5gyQjV1NcuP1GIvSCe9tXNboKEp5X2SPdqFlP1sBfE5sgVECIAtmZkhMAqcQ=="],
|
||||
"@hcengineering/server-token": ["@hcengineering/server-token@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/server-token/0.7.28/b1cffcb628aa0c6ac73171ef340c8ccf0f85ab07", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "jwt-simple": "^0.5.6" } }, "sha512-TaHwzxuoFKN6jnWHIZk0UcRmB+UQ1QE1HZzUh5doPSFUYOAtTrSb32iJDA8e8oEADcJpOp9RGIUP/EP/VED7BQ=="],
|
||||
|
||||
"@hcengineering/ui": ["@hcengineering/ui@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/ui/0.7.15/8462766246888da93d3f51e19e0b11d7c40312f6", { "dependencies": { "@hcengineering/analytics": "^0.7.15", "@hcengineering/core": "^0.7.15", "@hcengineering/platform": "^0.7.15", "@hcengineering/theme": "^0.7.15", "autolinker": "4.0.0", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "dompurify": "^3.1.6", "emojibase": "^16.0.0", "fast-equals": "^5.2.2", "hls.js": "^1.5.20", "svelte": "^4.2.19" } }, "sha512-OkllPX5wH3eJ3WSEzr+PJ0H8H2ZyXWGNHdUmwQSPW6UTZqAYia9WtCVO3sxR7SMtZtjAnKPitRycSG4Q/z8GVA=="],
|
||||
"@hcengineering/theme": ["@hcengineering/theme@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/theme/0.7.28/557640a7ba2ffaa4d6e5411a54577f60dc528300", { "dependencies": { "@hcengineering/analytics": "^0.7.28", "@hcengineering/platform": "^0.7.28", "svelte": "^4.2.19" } }, "sha512-Hv3wQKHr327TC7wMHCBiG4bMtg8vSlb7+UFwdKI8ztEs3359meqkXu+exqi7Nl5334voXIz5ZFxjwZwOMzNSFA=="],
|
||||
|
||||
"@hcengineering/ui": ["@hcengineering/ui@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/ui/0.7.28/82025fc775702ea8c8569611356662bfbf28eb74", { "dependencies": { "@hcengineering/analytics": "^0.7.28", "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "@hcengineering/theme": "^0.7.28", "autolinker": "4.0.0", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "dompurify": "^3.1.6", "emojibase": "^16.0.0", "fast-equals": "^5.2.2", "hls.js": "^1.5.20", "plyr": "^3.7.8", "svelte": "^4.2.19" } }, "sha512-QsUpO0+Idu1n2dC6KtorPJ6SSp30It8FHSuG3Y+Awv4td/ftVocOSFqhskbk+us9Yw9KqOYgkqhGYEHbfRfw4Q=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
@@ -276,21 +247,17 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@msgpack/msgpack": ["@msgpack/msgpack@3.0.1", "", {}, "sha512-9qysoVTITLcOFIIJeXbdtUgvvY25ojUp+WWfLc0O4H4KKWeamUNAqkjS5mej/PnVDnH70llWKNa7pzv5U4TqVQ=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@pkgr/core": ["@pkgr/core@0.1.1", "", {}, "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA=="],
|
||||
|
||||
"@sqlite.org/sqlite-wasm": ["@sqlite.org/sqlite-wasm@3.47.1-build1", "", { "bin": { "sqlite-wasm": "bin/index.js" } }, "sha512-3qnVGab3sjJ8ov0ce1rQGZrMkglYEQ/q5fnq1s1BpRWFMYsiJVaLD1zKepcI9E9r3qx7929+2G27Hfsasvrm6Q=="],
|
||||
"@pkgr/core": ["@pkgr/core@0.2.0", "", {}, "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ=="],
|
||||
|
||||
"@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="],
|
||||
"@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
@@ -312,7 +279,7 @@
|
||||
|
||||
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
|
||||
"@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.9.18", "", {}, "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="],
|
||||
|
||||
@@ -322,27 +289,29 @@
|
||||
|
||||
"@types/serve-static": ["@types/serve-static@1.15.7", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw=="],
|
||||
|
||||
"@types/snappyjs": ["@types/snappyjs@0.7.1", "", {}, "sha512-OxjzJ6cQZstysMh6PEwZWmK9qlKZyezHJKOkcUkZDooSFuog2votUEKkxMaTq51UQF3cJkXKQ+XGlj4FSl8JQQ=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
|
||||
"@types/ws": ["@types/ws@8.18.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.25.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/type-utils": "8.25.0", "@typescript-eslint/utils": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA=="],
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.27.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.27.0", "@typescript-eslint/type-utils": "8.27.0", "@typescript-eslint/utils": "8.27.0", "@typescript-eslint/visitor-keys": "8.27.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.25.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/types": "8.25.0", "@typescript-eslint/typescript-estree": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg=="],
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.27.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.27.0", "@typescript-eslint/types": "8.27.0", "@typescript-eslint/typescript-estree": "8.27.0", "@typescript-eslint/visitor-keys": "8.27.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0" } }, "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg=="],
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.27.0", "", { "dependencies": { "@typescript-eslint/types": "8.27.0", "@typescript-eslint/visitor-keys": "8.27.0" } }, "sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.25.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.25.0", "@typescript-eslint/utils": "8.25.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g=="],
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.27.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.27.0", "@typescript-eslint/utils": "8.27.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-wVArTVcz1oJOIEJxui/nRhV0TXzD/zMSOYi/ggCfNq78EIszddXcJb7r4RCp/oBrjt8n9A0BSxRMKxHftpDxDA=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.25.0", "", {}, "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw=="],
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.27.0", "", {}, "sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q=="],
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.27.0", "", { "dependencies": { "@typescript-eslint/types": "8.27.0", "@typescript-eslint/visitor-keys": "8.27.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.25.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/types": "8.25.0", "@typescript-eslint/typescript-estree": "8.25.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA=="],
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.27.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.27.0", "@typescript-eslint/types": "8.27.0", "@typescript-eslint/typescript-estree": "8.27.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ=="],
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.27.0", "", { "dependencies": { "@typescript-eslint/types": "8.27.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ=="],
|
||||
|
||||
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
@@ -364,7 +333,7 @@
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="],
|
||||
"bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
@@ -378,10 +347,14 @@
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"core-js": ["core-js@3.41.0", "", {}, "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
|
||||
|
||||
"custom-event-polyfill": ["custom-event-polyfill@1.0.7", "", {}, "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w=="],
|
||||
|
||||
"date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="],
|
||||
|
||||
"date-fns-tz": ["date-fns-tz@2.0.1", "", { "peerDependencies": { "date-fns": "2.x" } }, "sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA=="],
|
||||
@@ -398,13 +371,13 @@
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.21.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg=="],
|
||||
"eslint": ["eslint@9.23.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.23.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw=="],
|
||||
|
||||
"eslint-config-prettier": ["eslint-config-prettier@9.1.0", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw=="],
|
||||
|
||||
"eslint-plugin-prettier": ["eslint-plugin-prettier@5.2.3", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.9.1" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw=="],
|
||||
"eslint-plugin-prettier": ["eslint-plugin-prettier@5.2.4", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.10.2" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-SFtuYmnhwYCtuCDTKPoK+CEzCnEgKTU2qTLwoCxvrC0MFBTIXo1i6hDYOI4cwHaE5GZtlWmTN3YfucYi7KJwPw=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="],
|
||||
"eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||
|
||||
@@ -432,7 +405,7 @@
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
@@ -460,8 +433,6 @@
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
|
||||
|
||||
"intl-messageformat": ["intl-messageformat@10.7.15", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.3", "@formatjs/fast-memoize": "2.2.6", "@formatjs/icu-messageformat-parser": "2.11.1", "tslib": "2" } }, "sha512-LRyExsEsefQSBjU2p47oAheoKz+EOJxSLDdjOaEjdriajfHsMXOmV/EhMvYSg9bAgCUHasuAC+mcUBe/95PfIg=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
@@ -488,6 +459,8 @@
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"loadjs": ["loadjs@4.3.0", "", {}, "sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ=="],
|
||||
|
||||
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
@@ -516,8 +489,6 @@
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"path": ["path@0.12.7", "", { "dependencies": { "process": "^0.11.1", "util": "^0.10.3" } }, "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
@@ -526,6 +497,8 @@
|
||||
|
||||
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"plyr": ["plyr@3.7.8", "", { "dependencies": { "core-js": "^3.26.1", "custom-event-polyfill": "^1.0.7", "loadjs": "^4.2.0", "rangetouch": "^2.0.1", "url-polyfill": "^1.1.12" } }, "sha512-yG/EHDobwbB/uP+4Bm6eUpJ93f8xxHjjk2dYcD1Oqpe1EcuQl5tzzw9Oq+uVAzd2lkM11qZfydSiyIpiB8pgdA=="],
|
||||
|
||||
"postgres": ["postgres@3.4.5", "", {}, "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
@@ -534,17 +507,17 @@
|
||||
|
||||
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
|
||||
|
||||
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"rangetouch": ["rangetouch@2.0.1", "", {}, "sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA=="],
|
||||
|
||||
"regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
@@ -554,6 +527,8 @@
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"snappyjs": ["snappyjs@0.7.0", "", {}, "sha512-u5iEEXkMe2EInQio6Wv9LWHOQYRDbD2O9hzS27GpT/lwfIQhTCnHCTqedqHIHe9ZcvQo+9au6vngQayipz1NYw=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
@@ -562,7 +537,7 @@
|
||||
|
||||
"svelte": ["svelte@4.2.19", "", { "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/trace-mapping": "^0.3.18", "@types/estree": "^1.0.1", "acorn": "^8.9.0", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "code-red": "^1.0.3", "css-tree": "^2.3.1", "estree-walker": "^3.0.3", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", "periscopic": "^3.1.0" } }, "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw=="],
|
||||
|
||||
"synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw=="],
|
||||
"synckit": ["synckit@0.10.2", "", { "dependencies": { "@pkgr/core": "^0.2.0", "tslib": "^2.8.1" } }, "sha512-cSGiaCPhFzeFIQY8KKEacv46LclENY4d60jgkwCrKomvRkIjtMyss1dPkHLp/62c1leuOjEedB1+lWcwqTJSvA=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
@@ -572,17 +547,17 @@
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
||||
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.25.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.25.0", "@typescript-eslint/parser": "8.25.0", "@typescript-eslint/utils": "8.25.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q=="],
|
||||
"typescript-eslint": ["typescript-eslint@8.27.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.27.0", "@typescript-eslint/parser": "8.27.0", "@typescript-eslint/utils": "8.27.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-ZZ/8+Y0rRUMuW1gJaPtLWe4ryHbsPLzzibk5Sq+IFa2aOH1Vo0gPr1fbA6pOnzBke7zC2Da4w8AyCgxKXo3lqA=="],
|
||||
|
||||
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"util": ["util@0.10.4", "", { "dependencies": { "inherits": "2.0.3" } }, "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A=="],
|
||||
"url-polyfill": ["url-polyfill@1.1.13", "", {}, "sha512-tXzkojrv2SujumYthZ/WjF7jaSfNhSXlYMpE5AYdL2I3D7DCeo+mch8KtW2rUuKjDg+3VXODXHVgipt8yGY/eQ=="],
|
||||
|
||||
"uuid": ["uuid@11.0.5", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA=="],
|
||||
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
@@ -596,6 +571,8 @@
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"bun-types/@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
+6
-6
@@ -8,13 +8,13 @@
|
||||
"format": "prettier --write packages/**/src/*.ts && bun run lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/bun": "^1.2.4",
|
||||
"bun-types": "^1.2.4",
|
||||
"eslint": "^9.21.0",
|
||||
"@eslint/js": "^9.23.0",
|
||||
"@types/bun": "^1.2.5",
|
||||
"bun-types": "^1.2.5",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-prettier": "^5.2.4",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript-eslint": "^8.25.0"
|
||||
"typescript-eslint": "^8.27.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -19,14 +19,26 @@ import type {
|
||||
NotificationsQueryCallback,
|
||||
QueryCallback
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
import { type FindMessagesParams, type FindNotificationsParams } from '@hcengineering/communication-types'
|
||||
import {
|
||||
type FindMessagesParams,
|
||||
type FindNotificationContextParams,
|
||||
type FindNotificationsParams,
|
||||
type NotificationContext
|
||||
} from '@hcengineering/communication-types'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
class BaseQuery<P extends Record<string, any>, C extends QueryCallback<any>> {
|
||||
private oldQuery: P | undefined
|
||||
private oldCallback: QueryCallback<any> | undefined
|
||||
private oldCallback: C | undefined
|
||||
|
||||
constructor(protected readonly lq: LiveQueries) {}
|
||||
constructor(
|
||||
protected readonly lq: LiveQueries,
|
||||
onDestroy: (fn: () => void) => void
|
||||
) {
|
||||
onDestroy(() => {
|
||||
this.unsubscribe()
|
||||
})
|
||||
}
|
||||
|
||||
unsubscribe: () => void = () => {}
|
||||
|
||||
@@ -82,3 +94,17 @@ export class NotificationsQuery extends BaseQuery<FindNotificationsParams, Notif
|
||||
return this.lq.queryNotifications(params, callback)
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationContextsQuery extends BaseQuery<
|
||||
FindNotificationContextParams,
|
||||
QueryCallback<NotificationContext>
|
||||
> {
|
||||
override createQuery(
|
||||
params: FindNotificationContextParams,
|
||||
callback: QueryCallback<NotificationContext>
|
||||
): {
|
||||
unsubscribe: () => void
|
||||
} {
|
||||
return this.lq.queryNotificationContexts(params, callback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
type CardID,
|
||||
type Message,
|
||||
type FindMessagesParams,
|
||||
type MessageID,
|
||||
type RichText,
|
||||
type SocialID,
|
||||
type ContextID,
|
||||
type NotificationContextUpdate,
|
||||
type FindNotificationContextParams,
|
||||
type NotificationContext,
|
||||
type FindNotificationsParams,
|
||||
type Notification,
|
||||
type Attachment,
|
||||
type Reaction,
|
||||
type WorkspaceID,
|
||||
type FindMessagesGroupsParams,
|
||||
type MessagesGroup,
|
||||
PatchType
|
||||
} from '@hcengineering/communication-types'
|
||||
import {
|
||||
type Client,
|
||||
type MessageCreatedEvent,
|
||||
type DbAdapter,
|
||||
type ResponseEvent,
|
||||
ResponseEventType
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
import { createDbAdapter as createSqliteDbAdapter } from '@hcengineering/communication-sqlite-wasm'
|
||||
|
||||
//TODO: FIXME
|
||||
class DbClient {
|
||||
onEvent: (event: ResponseEvent) => void = () => {}
|
||||
|
||||
constructor(
|
||||
private readonly db: DbAdapter,
|
||||
private readonly workspace: WorkspaceID,
|
||||
private readonly personalWorkspace: WorkspaceID
|
||||
) {}
|
||||
|
||||
async createMessage(card: CardID, content: RichText, creator: SocialID): Promise<MessageID> {
|
||||
const created = new Date()
|
||||
const id = await this.db.createMessage(card, content, creator, created)
|
||||
|
||||
const event: MessageCreatedEvent = {
|
||||
type: ResponseEventType.MessageCreated,
|
||||
message: {
|
||||
id,
|
||||
card,
|
||||
content,
|
||||
creator,
|
||||
created,
|
||||
edited: created,
|
||||
reactions: [],
|
||||
attachments: []
|
||||
}
|
||||
}
|
||||
|
||||
this.onEvent(event)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
async removeMessage(card: CardID, message: MessageID) {
|
||||
await this.db.removeMessage(card, message)
|
||||
this.onEvent({ type: ResponseEventType.MessageRemoved, message, card })
|
||||
}
|
||||
|
||||
async updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise<void> {
|
||||
const created = new Date()
|
||||
await this.db.createPatch(card, message, PatchType.update, content, creator, created)
|
||||
this.onEvent({
|
||||
type: ResponseEventType.PatchCreated,
|
||||
card,
|
||||
patch: { message, type: PatchType.update, content, creator, created }
|
||||
})
|
||||
}
|
||||
|
||||
async createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void> {
|
||||
const created = new Date()
|
||||
await this.db.createReaction(card, message, reaction, creator, created)
|
||||
this.onEvent({ type: ResponseEventType.ReactionCreated, card, reaction: { message, reaction, creator, created } })
|
||||
}
|
||||
|
||||
async removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void> {
|
||||
await this.db.removeReaction(card, message, reaction, creator)
|
||||
this.onEvent({ type: ResponseEventType.ReactionRemoved, card, message, reaction, creator })
|
||||
}
|
||||
|
||||
async createAttachment(card: CardID, message: MessageID, attachment: CardID, creator: SocialID): Promise<void> {
|
||||
const created = new Date()
|
||||
await this.db.createAttachment(message, card, creator, created)
|
||||
this.onEvent({
|
||||
type: ResponseEventType.AttachmentCreated,
|
||||
card,
|
||||
attachment: { message, card: attachment, creator, created }
|
||||
})
|
||||
}
|
||||
|
||||
async removeAttachment(card: CardID, message: MessageID, attachment: CardID): Promise<void> {
|
||||
await this.db.removeAttachment(message, card)
|
||||
this.onEvent({ type: ResponseEventType.AttachmentRemoved, message, card, attachment })
|
||||
}
|
||||
|
||||
async findMessages(params: FindMessagesParams): Promise<Message[]> {
|
||||
const rawMessages = await this.db.findMessages(params)
|
||||
return rawMessages.map((it) => this.toMessage(it))
|
||||
}
|
||||
|
||||
async findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
|
||||
return await this.db.findMessagesGroups(params)
|
||||
}
|
||||
|
||||
async findMessage(params: FindMessagesParams): Promise<Message | undefined> {
|
||||
return (await this.findMessages({ ...params, limit: 1 }))[0]
|
||||
}
|
||||
|
||||
toMessage(raw: any): Message {
|
||||
return {
|
||||
id: raw.id,
|
||||
card: raw.card,
|
||||
content: raw.content,
|
||||
creator: raw.creator,
|
||||
created: new Date(raw.created),
|
||||
edited: new Date(raw.edited),
|
||||
reactions: raw.reactions.map((it: any) => this.toReaction(it)),
|
||||
attachments: raw.attachments.map((it: any) => this.toAttachment(it))
|
||||
}
|
||||
}
|
||||
|
||||
toAttachment(raw: any): Attachment {
|
||||
return {
|
||||
message: raw.message,
|
||||
card: raw.card,
|
||||
creator: raw.creator,
|
||||
created: new Date(raw.created)
|
||||
}
|
||||
}
|
||||
|
||||
toReaction(raw: any): Reaction {
|
||||
return {
|
||||
message: raw.message,
|
||||
reaction: raw.reaction,
|
||||
creator: raw.creator,
|
||||
created: new Date(raw.created)
|
||||
}
|
||||
}
|
||||
|
||||
async createNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
await this.db.createNotification(message, context)
|
||||
}
|
||||
|
||||
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
await this.db.removeNotification(message, context)
|
||||
}
|
||||
|
||||
async createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise<ContextID> {
|
||||
return await this.db.createContext(this.personalWorkspace, card, lastView, lastUpdate)
|
||||
}
|
||||
|
||||
async updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
|
||||
await this.db.updateContext(context, update)
|
||||
}
|
||||
|
||||
async removeNotificationContext(context: ContextID): Promise<void> {
|
||||
await this.db.removeContext(context)
|
||||
}
|
||||
|
||||
async findNotificationContexts(params: FindNotificationContextParams): Promise<NotificationContext[]> {
|
||||
//TODO: should we filter by workspace?
|
||||
return await this.db.findContexts(params, [this.personalWorkspace])
|
||||
}
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async findNotifications(params: FindNotificationsParams): Promise<Notification[]> {
|
||||
//TODO: should we filter by workspace?
|
||||
return await this.db.findNotifications(params, this.personalWorkspace)
|
||||
}
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void> {
|
||||
//TODO: implement
|
||||
}
|
||||
|
||||
async unsubscribeQuery() {
|
||||
//ignore
|
||||
}
|
||||
|
||||
close() {
|
||||
this.db.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSqliteClient(
|
||||
workspace: WorkspaceID,
|
||||
personalWorkspace: WorkspaceID,
|
||||
dbUrl = 'file:communication.sqlite3?vfs=opfs'
|
||||
): Promise<Client> {
|
||||
const db = await createSqliteDbAdapter(dbUrl)
|
||||
return new DbClient(db, workspace, personalWorkspace) as unknown as Client
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
type CardID,
|
||||
type ContextID,
|
||||
type FindMessagesGroupsParams,
|
||||
type FindMessagesParams,
|
||||
type FindNotificationContextParams,
|
||||
type FindNotificationsParams,
|
||||
type Message,
|
||||
type MessageID,
|
||||
type MessagesGroup,
|
||||
type Notification,
|
||||
type NotificationContext,
|
||||
type NotificationContextUpdate,
|
||||
type RichText,
|
||||
type SocialID,
|
||||
type WorkspaceID
|
||||
} from '@hcengineering/communication-types'
|
||||
import {
|
||||
RequestEventType,
|
||||
type Client,
|
||||
type CreateAttachmentEvent,
|
||||
type CreateMessageEvent,
|
||||
type CreateMessageResult,
|
||||
type CreateNotificationContextEvent,
|
||||
type CreateNotificationContextResult,
|
||||
type CreateNotificationEvent,
|
||||
type CreatePatchEvent,
|
||||
type CreateReactionEvent,
|
||||
type EventResult,
|
||||
type RemoveAttachmentEvent,
|
||||
type RemoveMessageEvent,
|
||||
type RemoveNotificationContextEvent,
|
||||
type RemoveNotificationEvent,
|
||||
type RemoveReactionEvent,
|
||||
type RequestEvent,
|
||||
type ResponseEvent,
|
||||
type UpdateNotificationContextEvent
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
import { initLiveQueries } from '@hcengineering/communication-client-query'
|
||||
|
||||
import { WebSocketConnection } from './connection'
|
||||
|
||||
class WsClient implements Client {
|
||||
private readonly ws: WebSocketConnection
|
||||
|
||||
onEvent: (event: ResponseEvent) => void = () => {}
|
||||
|
||||
constructor(
|
||||
private readonly url: string,
|
||||
private readonly token: string,
|
||||
private readonly binary: boolean = false
|
||||
) {
|
||||
const connectionUrl = this.url + '?token=' + this.token
|
||||
this.ws = new WebSocketConnection(connectionUrl, this.binary)
|
||||
this.ws.onEvent = (event) => {
|
||||
void this.onEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
async createMessage(card: CardID, content: RichText, creator: SocialID): Promise<MessageID> {
|
||||
const event: CreateMessageEvent = {
|
||||
type: RequestEventType.CreateMessage,
|
||||
card,
|
||||
content,
|
||||
creator
|
||||
}
|
||||
const result = await this.sendEvent(event)
|
||||
return (result as CreateMessageResult).id
|
||||
}
|
||||
|
||||
async removeMessage(card: CardID, message: MessageID): Promise<void> {
|
||||
const event: RemoveMessageEvent = {
|
||||
type: RequestEventType.RemoveMessage,
|
||||
card,
|
||||
message
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise<void> {
|
||||
const event: CreatePatchEvent = {
|
||||
type: RequestEventType.CreatePatch,
|
||||
card,
|
||||
message,
|
||||
content,
|
||||
creator
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void> {
|
||||
const event: CreateReactionEvent = {
|
||||
type: RequestEventType.CreateReaction,
|
||||
card,
|
||||
message,
|
||||
reaction,
|
||||
creator
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void> {
|
||||
const event: RemoveReactionEvent = {
|
||||
type: RequestEventType.RemoveReaction,
|
||||
card,
|
||||
message,
|
||||
reaction,
|
||||
creator
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async createAttachment(card: CardID, message: MessageID, attachment: CardID, creator: SocialID): Promise<void> {
|
||||
const event: CreateAttachmentEvent = {
|
||||
type: RequestEventType.CreateAttachment,
|
||||
card,
|
||||
message,
|
||||
attachment,
|
||||
creator
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async removeAttachment(card: CardID, message: MessageID, attachment: CardID): Promise<void> {
|
||||
const event: RemoveAttachmentEvent = {
|
||||
type: RequestEventType.RemoveAttachment,
|
||||
card,
|
||||
message,
|
||||
attachment
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async findMessages(params: FindMessagesParams, queryId?: number): Promise<Message[]> {
|
||||
return await this.ws.send('findMessages', [params, queryId])
|
||||
}
|
||||
|
||||
async findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
|
||||
return await this.ws.send('findMessagesGroups', [params])
|
||||
}
|
||||
|
||||
async createNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
const event: CreateNotificationEvent = {
|
||||
type: RequestEventType.CreateNotification,
|
||||
message,
|
||||
context
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
const event: RemoveNotificationEvent = {
|
||||
type: RequestEventType.RemoveNotification,
|
||||
message,
|
||||
context
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise<ContextID> {
|
||||
const event: CreateNotificationContextEvent = {
|
||||
type: RequestEventType.CreateNotificationContext,
|
||||
card,
|
||||
lastView,
|
||||
lastUpdate
|
||||
}
|
||||
const result = await this.sendEvent(event)
|
||||
return (result as CreateNotificationContextResult).id
|
||||
}
|
||||
|
||||
async removeNotificationContext(context: ContextID): Promise<void> {
|
||||
const event: RemoveNotificationContextEvent = {
|
||||
type: RequestEventType.RemoveNotificationContext,
|
||||
context
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
|
||||
const event: UpdateNotificationContextEvent = {
|
||||
type: RequestEventType.UpdateNotificationContext,
|
||||
context,
|
||||
update
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async findNotificationContexts(
|
||||
params: FindNotificationContextParams,
|
||||
queryId?: number
|
||||
): Promise<NotificationContext[]> {
|
||||
return await this.ws.send('findNotificationContexts', [params, queryId])
|
||||
}
|
||||
|
||||
async findNotifications(params: FindNotificationsParams, queryId?: number): Promise<Notification[]> {
|
||||
return await this.ws.send('findNotifications', [params, queryId])
|
||||
}
|
||||
|
||||
async unsubscribeQuery(id: number): Promise<void> {
|
||||
await this.ws.send('unsubscribeQuery', [id])
|
||||
}
|
||||
|
||||
private async sendEvent(event: RequestEvent): Promise<EventResult> {
|
||||
return await this.ws.send('event', [event])
|
||||
}
|
||||
//eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void> {
|
||||
//TODO: implement
|
||||
}
|
||||
|
||||
close() {
|
||||
void this.ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWebsocketClient(
|
||||
url: string,
|
||||
token: string,
|
||||
workspace: WorkspaceID,
|
||||
filesUrl: string
|
||||
): Promise<Client> {
|
||||
const client = new WsClient(url, token)
|
||||
initLiveQueries(client, workspace, filesUrl)
|
||||
return client
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { ResponseEvent } from '@hcengineering/communication-sdk-types'
|
||||
import { encode, decode } from '@msgpack/msgpack'
|
||||
|
||||
const PING_TIMEOUT = 10000
|
||||
const RECONNECT_TIMEOUT = 1000
|
||||
|
||||
export type RequestId = string
|
||||
|
||||
export interface Response {
|
||||
id?: RequestId
|
||||
result?: any
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface Request {
|
||||
id?: RequestId
|
||||
method: string
|
||||
params: any[]
|
||||
}
|
||||
|
||||
export interface HelloRequest extends Request {
|
||||
binary?: boolean
|
||||
}
|
||||
|
||||
export class WebSocketConnection {
|
||||
private ws!: WebSocket | Promise<WebSocket>
|
||||
private requests: { [key: RequestId]: { resolve: (response: any) => void; reject: (reason: any) => void } } = {}
|
||||
private lastId: number = 0
|
||||
|
||||
private pingInterval: any
|
||||
private reconnectTimeout: any
|
||||
|
||||
onEvent: (event: ResponseEvent) => void = () => {}
|
||||
|
||||
constructor(
|
||||
private url: string,
|
||||
private readonly binary: boolean = false
|
||||
) {
|
||||
this.connect()
|
||||
}
|
||||
|
||||
private connect(): void {
|
||||
const ws = new WebSocket(this.url)
|
||||
|
||||
ws.onmessage = (event: MessageEvent) => {
|
||||
const response = deserializeResponse(event.data, this.binary)
|
||||
if (response.id !== undefined) {
|
||||
const handlers = this.requests[response.id]
|
||||
if (handlers === undefined) return
|
||||
delete this.requests[response.id]
|
||||
if (response.error !== undefined) {
|
||||
console.error('Websocket error', response.error)
|
||||
handlers.reject(response.error)
|
||||
} else {
|
||||
handlers.resolve(response.result)
|
||||
}
|
||||
} else {
|
||||
if (response.error !== undefined) {
|
||||
console.error('Websocket error', response.error)
|
||||
} else {
|
||||
const event = response.result as ResponseEvent
|
||||
this.onEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
clearInterval(this.pingInterval)
|
||||
this.handleReconnect()
|
||||
}
|
||||
|
||||
this.ws = new Promise((resolve, reject) => {
|
||||
ws.onopen = () => {
|
||||
const request: HelloRequest = { id: 'hello', method: 'hello', params: [], binary: this.binary }
|
||||
ws.send(serializeRequest(request, this.binary))
|
||||
clearInterval(this.pingInterval)
|
||||
this.pingInterval = setInterval(() => {
|
||||
void this.sendRequest({ method: 'ping', params: [] })
|
||||
}, PING_TIMEOUT)
|
||||
resolve(ws)
|
||||
}
|
||||
ws.onerror = (event: any) => {
|
||||
console.error('Websocket error', event)
|
||||
reject(new Error('Websocket error'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private handleReconnect() {
|
||||
clearTimeout(this.reconnectTimeout)
|
||||
this.reconnectTimeout = setTimeout(() => {
|
||||
this.connect()
|
||||
}, RECONNECT_TIMEOUT)
|
||||
}
|
||||
|
||||
async waitWs(): Promise<WebSocket> {
|
||||
if (this.ws instanceof Promise) {
|
||||
this.ws = await this.ws
|
||||
}
|
||||
return this.ws
|
||||
}
|
||||
|
||||
async send(method: string, params: any[]): Promise<any> {
|
||||
const id = ++this.lastId
|
||||
return await this.sendRequest({ id: id.toString(), method, params })
|
||||
}
|
||||
|
||||
private async sendRequest(request: Request): Promise<any> {
|
||||
const ws = await this.waitWs()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (request.id !== undefined) {
|
||||
this.requests[request.id] = { resolve, reject }
|
||||
}
|
||||
ws.send(serializeRequest(request, this.binary))
|
||||
})
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
clearInterval(this.pingInterval)
|
||||
clearTimeout(this.reconnectTimeout)
|
||||
const ws = await this.waitWs()
|
||||
ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
function serializeRequest(request: Request, binary: boolean): any {
|
||||
if (binary) {
|
||||
return encode(request)
|
||||
} else {
|
||||
return JSON.stringify(request)
|
||||
}
|
||||
}
|
||||
|
||||
function deserializeResponse(data: any, binary: boolean): Response {
|
||||
if (binary) {
|
||||
return decode(data) as Response
|
||||
} else {
|
||||
return JSON.parse(data.toString())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
CREATE TABLE IF NOT EXISTS communication.patch
|
||||
(
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
workspace_id UUID NOT NULL,
|
||||
card_id VARCHAR(255) NOT NULL,
|
||||
message_id INT8 NOT NULL,
|
||||
type VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
creator VARCHAR(255) NOT NULL,
|
||||
created TIMESTAMPTZ NOT NULL,
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
workspace_id UUID NOT NULL,
|
||||
card_id VARCHAR(255) NOT NULL,
|
||||
message_id INT8 NOT NULL,
|
||||
type VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
creator VARCHAR(255) NOT NULL,
|
||||
created TIMESTAMPTZ NOT NULL,
|
||||
message_created_sec TIMESTAMPTZ(0) NOT NULL,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS communication.attachments
|
||||
(
|
||||
message_id INT8 NOT NULL,
|
||||
card_id VARCHAR(255) NOT NULL,
|
||||
creator VARCHAR(255) NOT NULL,
|
||||
created TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
PRIMARY KEY (card_id, message_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS attachment_message_idx ON communication.attachments (message_id);
|
||||
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE IF NOT EXISTS communication.files
|
||||
(
|
||||
workspace_id UUID NOT NULL,
|
||||
card_id VARCHAR(255) NOT NULL,
|
||||
message_id INT8 NOT NULL,
|
||||
|
||||
blob_id UUID NOT NULL,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(255) NOT NULL,
|
||||
size INT8 NOT NULL,
|
||||
|
||||
creator VARCHAR(255) NOT NULL,
|
||||
created TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
message_created_sec TIMESTAMPTZ(0) NOT NULL,
|
||||
|
||||
PRIMARY KEY (workspace_id, card_id, message_id, blob_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS files_workspace_card_message_idx ON communication.files (workspace_id, card_id, message_id);
|
||||
@@ -1,27 +1,38 @@
|
||||
CREATE TABLE IF NOT EXISTS communication.notification_context
|
||||
(
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL,
|
||||
card_id VARCHAR(255) NOT NULL,
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
|
||||
personal_workspace UUID NOT NULL,
|
||||
workspace_id UUID NOT NULL,
|
||||
card_id VARCHAR(255) NOT NULL,
|
||||
account UUID NOT NULL,
|
||||
|
||||
archived_from TIMESTAMPTZ,
|
||||
last_view TIMESTAMPTZ,
|
||||
last_update TIMESTAMPTZ,
|
||||
last_view TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_update TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (workspace_id, card_id, personal_workspace)
|
||||
UNIQUE (workspace_id, card_id, account)
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS communication.notifications
|
||||
(
|
||||
message_id UUID NOT NULL,
|
||||
context UUID NOT NULL,
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
context_id INT8 NOT NULL,
|
||||
message_id INT8,
|
||||
created TIMESTAMPTZ NOT NULL,
|
||||
content JSONB NOT NULL DEFAULT '{}',
|
||||
|
||||
PRIMARY KEY (message_id, context),
|
||||
FOREIGN KEY (context) REFERENCES communication.notification_context (id) ON DELETE CASCADE
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (context_id) REFERENCES communication.notification_context (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS notification_context_idx ON communication.notifications (context);
|
||||
CREATE INDEX IF NOT EXISTS notification_context_idx ON communication.notifications (context_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS communication.collaborators
|
||||
(
|
||||
workspace_id UUID NOT NULL,
|
||||
card_id VARCHAR(255) NOT NULL,
|
||||
account UUID NOT NULL,
|
||||
date TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
PRIMARY KEY (workspace_id, card_id, account)
|
||||
);
|
||||
@@ -1,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",
|
||||
|
||||
@@ -13,11 +13,13 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { ParameterOrJSON, Row } from 'postgres'
|
||||
import type postgres from 'postgres'
|
||||
import {
|
||||
type FindCollaboratorsParams,
|
||||
type AccountID,
|
||||
type BlobID,
|
||||
type CardID,
|
||||
type Collaborator,
|
||||
type ContextID,
|
||||
type FindMessagesGroupsParams,
|
||||
type FindMessagesParams,
|
||||
@@ -26,14 +28,15 @@ import {
|
||||
type Message,
|
||||
type MessageID,
|
||||
type MessagesGroup,
|
||||
type MessageType,
|
||||
type Notification,
|
||||
type NotificationContext,
|
||||
type NotificationContextUpdate,
|
||||
type PatchType,
|
||||
type RichText,
|
||||
type SocialID,
|
||||
type Thread,
|
||||
type WorkspaceID,
|
||||
type Thread
|
||||
type NotificationID
|
||||
} from '@hcengineering/communication-types'
|
||||
import type { DbAdapter } from '@hcengineering/communication-sdk-types'
|
||||
import { retry } from '@hcengineering/communication-shared'
|
||||
@@ -41,8 +44,8 @@ import { retry } from '@hcengineering/communication-shared'
|
||||
import { MessagesDb } from './db/message'
|
||||
import { NotificationsDb } from './db/notification'
|
||||
import { connect, type PostgresClientReference } from './connection'
|
||||
import { type Options, type Logger, type SqlClient } from './types'
|
||||
import { injectVars } from './utils.ts'
|
||||
import { type Logger, type Options, type SqlClient, type SqlParams, type SqlRow } from './types'
|
||||
import { injectVars } from './utils'
|
||||
|
||||
export class CockroachAdapter implements DbAdapter {
|
||||
private readonly message: MessagesDb
|
||||
@@ -58,16 +61,19 @@ export class CockroachAdapter implements DbAdapter {
|
||||
this.notification = new NotificationsDb(this.sql, this.workspace, logger, options)
|
||||
}
|
||||
|
||||
async createMessage(card: CardID, content: RichText, creator: SocialID, created: Date): Promise<MessageID> {
|
||||
return await this.message.createMessage(card, content, creator, created)
|
||||
async createMessage(
|
||||
card: CardID,
|
||||
type: MessageType,
|
||||
content: RichText,
|
||||
creator: SocialID,
|
||||
created: Date,
|
||||
data?: any
|
||||
): Promise<MessageID> {
|
||||
return await this.message.createMessage(card, type, content, creator, created, data)
|
||||
}
|
||||
|
||||
async removeMessage(card: CardID, message: MessageID, socialIds?: SocialID[]): Promise<void> {
|
||||
await this.message.removeMessage(card, message, socialIds)
|
||||
}
|
||||
|
||||
async removeMessages(card: CardID, fromId: MessageID, toId: MessageID): Promise<void> {
|
||||
await this.message.removeMessages(card, fromId, toId)
|
||||
async removeMessages(card: CardID, messages: MessageID[], socialIds?: SocialID[]): Promise<MessageID[]> {
|
||||
return await this.message.removeMessages(card, messages, socialIds)
|
||||
}
|
||||
|
||||
async createPatch(
|
||||
@@ -81,20 +87,8 @@ export class CockroachAdapter implements DbAdapter {
|
||||
await this.message.createPatch(card, message, type, content, creator, created)
|
||||
}
|
||||
|
||||
async removePatches(card: CardID, fromId: MessageID, toId: MessageID): Promise<void> {
|
||||
await this.message.removePatches(card, fromId, toId)
|
||||
}
|
||||
|
||||
async createMessagesGroup(
|
||||
card: CardID,
|
||||
blobId: BlobID,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
fromId: MessageID,
|
||||
toId: MessageID,
|
||||
count: number
|
||||
): Promise<void> {
|
||||
await this.message.createMessagesGroup(card, blobId, fromDate, toDate, fromId, toId, count)
|
||||
async createMessagesGroup(card: CardID, blobId: BlobID, fromDate: Date, toDate: Date, count: number): Promise<void> {
|
||||
await this.message.createMessagesGroup(card, blobId, fromDate, toDate, count)
|
||||
}
|
||||
|
||||
async removeMessagesGroup(card: CardID, blobId: BlobID): Promise<void> {
|
||||
@@ -115,48 +109,31 @@ export class CockroachAdapter implements DbAdapter {
|
||||
await this.message.removeReaction(card, message, reaction, creator, new Date())
|
||||
}
|
||||
|
||||
async createAttachment(message: MessageID, attachment: CardID, creator: SocialID, created: Date): Promise<void> {
|
||||
await this.message.createAttachment(message, attachment, creator, created)
|
||||
async createFile(
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
blobId: BlobID,
|
||||
fileType: string,
|
||||
filename: string,
|
||||
size: number,
|
||||
creator: SocialID,
|
||||
created: Date
|
||||
): Promise<void> {
|
||||
await this.message.createFile(card, message, blobId, fileType, filename, size, creator, created)
|
||||
}
|
||||
|
||||
async removeAttachment(message: MessageID, attachment: CardID): Promise<void> {
|
||||
await this.message.removeAttachment(message, attachment)
|
||||
async removeFile(card: CardID, message: MessageID, blobId: BlobID): Promise<void> {
|
||||
await this.message.removeFile(card, message, blobId)
|
||||
}
|
||||
|
||||
async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void> {
|
||||
await this.message.createThread(card, message, thread, created)
|
||||
}
|
||||
|
||||
async updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise<void> {
|
||||
await this.message.updateThread(thread, lastReply, op)
|
||||
async updateThread(thread: CardID, op: 'increment' | 'decrement', lastReply?: Date): Promise<void> {
|
||||
await this.message.updateThread(thread, op, lastReply)
|
||||
}
|
||||
|
||||
async createNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
await this.notification.createNotification(message, context)
|
||||
}
|
||||
|
||||
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
await this.notification.removeNotification(message, context)
|
||||
}
|
||||
|
||||
async createContext(
|
||||
personalWorkspace: WorkspaceID,
|
||||
card: CardID,
|
||||
lastView?: Date,
|
||||
lastUpdate?: Date
|
||||
): Promise<ContextID> {
|
||||
return await this.notification.createContext(personalWorkspace, card, lastView, lastUpdate)
|
||||
}
|
||||
|
||||
async updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
|
||||
await this.notification.updateContext(context, update)
|
||||
}
|
||||
|
||||
async removeContext(context: ContextID): Promise<void> {
|
||||
await this.notification.removeContext(context)
|
||||
}
|
||||
|
||||
// Finds
|
||||
async findMessages(params: FindMessagesParams): Promise<Message[]> {
|
||||
return await this.message.find(params)
|
||||
}
|
||||
@@ -169,25 +146,53 @@ export class CockroachAdapter implements DbAdapter {
|
||||
return await this.message.findThread(thread)
|
||||
}
|
||||
|
||||
async findContexts(
|
||||
params: FindNotificationContextParams,
|
||||
personalWorkspaces: WorkspaceID[],
|
||||
workspace?: WorkspaceID
|
||||
): Promise<NotificationContext[]> {
|
||||
return await this.notification.findContexts(params, personalWorkspaces, workspace)
|
||||
async addCollaborators(card: CardID, collaborators: AccountID[], date?: Date): Promise<void> {
|
||||
await this.notification.addCollaborators(card, collaborators, date)
|
||||
}
|
||||
|
||||
async findNotifications(
|
||||
params: FindNotificationsParams,
|
||||
personalWorkspace: WorkspaceID,
|
||||
workspace?: WorkspaceID
|
||||
): Promise<Notification[]> {
|
||||
return await this.notification.findNotifications(params, personalWorkspace, workspace)
|
||||
async removeCollaborators(card: CardID, collaborators: AccountID[]): Promise<void> {
|
||||
await this.notification.removeCollaborators(card, collaborators)
|
||||
}
|
||||
|
||||
async createNotification(context: ContextID, message: MessageID, messageCreated: Date): Promise<NotificationID> {
|
||||
return await this.notification.createNotification(context, message, messageCreated)
|
||||
}
|
||||
|
||||
async removeNotification(context: ContextID, account: AccountID, untilDate: Date): Promise<void> {
|
||||
await this.notification.removeNotifications(context, account, untilDate)
|
||||
}
|
||||
|
||||
async createContext(account: AccountID, card: CardID, lastUpdate: Date, lastView: Date): Promise<ContextID> {
|
||||
return await this.notification.createContext(account, card, lastUpdate, lastView)
|
||||
}
|
||||
|
||||
async updateContext(context: ContextID, account: AccountID, lastUpdate?: Date, lastView?: Date): Promise<void> {
|
||||
await this.notification.updateContext(context, account, lastUpdate, lastView)
|
||||
}
|
||||
|
||||
async removeContext(context: ContextID, account: AccountID): Promise<void> {
|
||||
await this.notification.removeContext(context, account)
|
||||
}
|
||||
|
||||
async findContexts(params: FindNotificationContextParams): Promise<NotificationContext[]> {
|
||||
return await this.notification.findContexts(params)
|
||||
}
|
||||
|
||||
async findNotifications(params: FindNotificationsParams): Promise<Notification[]> {
|
||||
return await this.notification.findNotifications(params)
|
||||
}
|
||||
|
||||
async findCollaborators(params: FindCollaboratorsParams): Promise<Collaborator[]> {
|
||||
return await this.notification.findCollaborators(params)
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.sql.close()
|
||||
}
|
||||
|
||||
getCollaboratorsCursor(card: CardID, date: Date, size?: number): AsyncIterable<Collaborator[]> {
|
||||
return this.notification.getCollaboratorsCursor(card, date, size)
|
||||
}
|
||||
}
|
||||
|
||||
export async function createDbAdapter(
|
||||
@@ -197,12 +202,13 @@ export async function createDbAdapter(
|
||||
options?: Options
|
||||
): Promise<DbAdapter> {
|
||||
const greenUrl = process.env.GREEN_URL ?? ''
|
||||
const connection = connect(connectionString)
|
||||
const sql = await connection.getClient()
|
||||
|
||||
if (greenUrl !== '') {
|
||||
const client = new GreenClient(greenUrl)
|
||||
const client = new GreenClient(greenUrl, sql)
|
||||
return new CockroachAdapter(client, workspace, logger, options)
|
||||
} else {
|
||||
const connection = connect(connectionString)
|
||||
const sql = await connection.getClient()
|
||||
const client = new CockroachClient(connection, sql)
|
||||
|
||||
return new CockroachAdapter(client, workspace, logger, options)
|
||||
@@ -212,8 +218,11 @@ export async function createDbAdapter(
|
||||
class GreenClient implements SqlClient {
|
||||
private readonly url: string
|
||||
private readonly token: string
|
||||
constructor(endpoint: string) {
|
||||
const url = new URL(endpoint)
|
||||
constructor(
|
||||
private readonly endpoint: string,
|
||||
private readonly sql: postgres.Sql
|
||||
) {
|
||||
const url = new URL(this.endpoint)
|
||||
this.token = url.searchParams.get('token') ?? 'secret'
|
||||
|
||||
const compression = url.searchParams.get('compression') ?? ''
|
||||
@@ -229,14 +238,21 @@ class GreenClient implements SqlClient {
|
||||
this.url = `${url.protocol}//${newHost}${newPathname}${newSearchParams.size > 0 ? '?' + newSearchParams.toString() : ''}`
|
||||
}
|
||||
|
||||
async execute<T extends any[] = (Row & Iterable<Row>)[]>(query: string, params?: ParameterOrJSON<any>[]): Promise<T> {
|
||||
return await retry(() => this.fetch<T>(query, params), { retries: 5 })
|
||||
async execute<T = SqlRow>(query: string, params?: SqlParams): Promise<T[]> {
|
||||
return await retry(() => this.fetch<T[]>(query, params), { retries: 5 })
|
||||
}
|
||||
|
||||
private async fetch<T extends any[] = (Row & Iterable<Row>)[]>(
|
||||
query: string,
|
||||
params?: ParameterOrJSON<any>[]
|
||||
): Promise<T> {
|
||||
cursor<T = SqlRow>(query: string, params?: SqlParams, size?: number): AsyncIterable<NonNullable<T[][number]>[]> {
|
||||
const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query
|
||||
|
||||
return this.sql.unsafe<T[]>(sql).cursor(size)
|
||||
}
|
||||
|
||||
close(): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
private async fetch<T = SqlRow>(query: string, params?: SqlParams): Promise<T> {
|
||||
const url = this.url.endsWith('/') ? this.url + 'api/v1/sql' : this.url + '/api/v1/sql'
|
||||
|
||||
const response = await fetch(url, {
|
||||
@@ -255,10 +271,6 @@ class GreenClient implements SqlClient {
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
close(): void {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
class CockroachClient implements SqlClient {
|
||||
@@ -267,9 +279,15 @@ class CockroachClient implements SqlClient {
|
||||
private readonly sql: postgres.Sql
|
||||
) {}
|
||||
|
||||
async execute<T extends any[] = (Row & Iterable<Row>)[]>(query: string, params?: ParameterOrJSON<any>[]): Promise<T> {
|
||||
async execute<T = SqlRow>(query: string, params?: SqlParams): Promise<T[]> {
|
||||
const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query
|
||||
return await this.sql.unsafe<T>(sql)
|
||||
return await this.sql.unsafe<T[]>(sql)
|
||||
}
|
||||
|
||||
cursor<T = SqlRow>(query: string, params?: SqlParams, size?: number): AsyncIterable<NonNullable<T[][number]>[]> {
|
||||
const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query
|
||||
|
||||
return this.sql.unsafe<T[]>(sql).cursor(size)
|
||||
}
|
||||
|
||||
close(): void {
|
||||
|
||||
@@ -103,7 +103,7 @@ export function connect(connectionString: string, database?: string): PostgresCl
|
||||
database,
|
||||
max: 5,
|
||||
fetch_types: false,
|
||||
prepare: true,
|
||||
prepare: false,
|
||||
...extraOptions
|
||||
})
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { type ParameterOrJSON, type Row } from 'postgres'
|
||||
import type { WorkspaceID } from '@hcengineering/communication-types'
|
||||
|
||||
import { type SqlClient, type Logger, type Options } from '../types'
|
||||
import { type Logger, type Options, type SqlClient } from '../types'
|
||||
|
||||
export class BaseDb {
|
||||
constructor (
|
||||
@@ -26,11 +26,7 @@ export class BaseDb {
|
||||
readonly options?: Options
|
||||
) {}
|
||||
|
||||
async execute<T extends any[] = (Row & Iterable<Row>)[]>(
|
||||
sql: string,
|
||||
params?: ParameterOrJSON<any>[],
|
||||
name?: string
|
||||
): Promise<T> {
|
||||
async execute<T = Row & Iterable<Row>>(sql: string, params?: ParameterOrJSON<any>[], name?: string): Promise<T[]> {
|
||||
if (this.options?.withLogs === true && this.logger !== undefined) {
|
||||
return await this.executeWithLogs(name ?? 'execute sql', this.logger, sql, params)
|
||||
}
|
||||
@@ -38,16 +34,16 @@ export class BaseDb {
|
||||
return await this.client.execute(sql, params)
|
||||
}
|
||||
|
||||
private async executeWithLogs<T extends any[] = (Row & Iterable<Row>)[]>(
|
||||
private async executeWithLogs<T = Row & Iterable<Row>>(
|
||||
name: string,
|
||||
logger: Logger,
|
||||
sql: string,
|
||||
params?: ParameterOrJSON<any>[]
|
||||
): Promise<T> {
|
||||
): Promise<T[]> {
|
||||
const start = performance.now()
|
||||
|
||||
try {
|
||||
return await this.client.execute(sql, params)
|
||||
return await this.client.execute<T>(sql, params)
|
||||
} finally {
|
||||
const time = performance.now() - start
|
||||
logger.info(name, { time })
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -14,401 +14,487 @@
|
||||
//
|
||||
|
||||
import {
|
||||
type Message,
|
||||
type MessageID,
|
||||
type CardID,
|
||||
type FindMessagesParams,
|
||||
type SocialID,
|
||||
type RichText,
|
||||
SortingOrder,
|
||||
PatchType,
|
||||
type Thread,
|
||||
type BlobID,
|
||||
type FindMessagesGroupsParams,
|
||||
type MessagesGroup
|
||||
type BlobID,
|
||||
type CardID,
|
||||
type FindMessagesGroupsParams,
|
||||
type FindMessagesParams,
|
||||
type Message,
|
||||
type MessageID,
|
||||
type MessagesGroup,
|
||||
type MessageType,
|
||||
PatchType,
|
||||
type RichText,
|
||||
type SocialID,
|
||||
SortingOrder,
|
||||
type Thread
|
||||
} from '@hcengineering/communication-types'
|
||||
import {generateMessageId} from '@hcengineering/communication-shared'
|
||||
import { generateMessageId, parseMessageId } from '@hcengineering/communication-shared'
|
||||
|
||||
import {BaseDb} from './base'
|
||||
import {
|
||||
TableName,
|
||||
type MessageDb,
|
||||
type AttachmentDb,
|
||||
type ReactionDb,
|
||||
type PatchDb,
|
||||
toMessage,
|
||||
type ThreadDb,
|
||||
toThread,
|
||||
type MessagesGroupDb,
|
||||
toMessagesGroup
|
||||
} from './schema'
|
||||
import {getCondition} from './utils'
|
||||
import { BaseDb } from './base'
|
||||
import {type FileDb, type MessageDb, type MessagesGroupDb, type PatchDb, type ReactionDb, TableName, type ThreadDb } from './schema'
|
||||
import { getCondition } from './utils'
|
||||
import { toMessage, toMessagesGroup, toThread } from './mapping'
|
||||
|
||||
export class MessagesDb extends BaseDb {
|
||||
// Message
|
||||
async createMessage(card: CardID, content: RichText, creator: SocialID, created: Date): Promise<MessageID> {
|
||||
const id = generateMessageId()
|
||||
const db: MessageDb = {
|
||||
id,
|
||||
workspace_id: this.workspace,
|
||||
card_id: card,
|
||||
content,
|
||||
creator,
|
||||
created
|
||||
}
|
||||
|
||||
const sql = `INSERT INTO ${TableName.Message} (workspace_id, card_id, id, content, creator, created)
|
||||
VALUES ($1::uuid, $2::varchar, $3::bigint, $4::text, $5::varchar, $6::timestamptz)`
|
||||
|
||||
await this.execute(sql, [db.workspace_id, db.card_id, db.id, db.content, db.creator, db.created], 'insert message')
|
||||
|
||||
return id
|
||||
// Message
|
||||
async createMessage (
|
||||
card: CardID,
|
||||
type: MessageType,
|
||||
content: RichText,
|
||||
creator: SocialID,
|
||||
created: Date,
|
||||
data?: any
|
||||
): Promise<MessageID> {
|
||||
const id = generateMessageId()
|
||||
const db: MessageDb = {
|
||||
id,
|
||||
type,
|
||||
workspace_id: this.workspace,
|
||||
card_id: card,
|
||||
content,
|
||||
creator,
|
||||
created,
|
||||
data
|
||||
}
|
||||
|
||||
async removeMessage(card: CardID, message: MessageID, socialIds?: SocialID[]): Promise<void> {
|
||||
if (socialIds === undefined || socialIds.length === 0) {
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.Message}
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND card_id = $2::varchar
|
||||
AND id = $2::bigint;`
|
||||
await this.execute(sql, [this.workspace, card, message], 'remove message')
|
||||
} else if (socialIds.length === 1) {
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.Message}
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND card_id = $2::varchar
|
||||
AND id = $2::bigint
|
||||
AND creator = $3::varchar;`
|
||||
await this.execute(sql, [this.workspace, card, message, socialIds[0]], 'remove message')
|
||||
} else {
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.Message}
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND card_id = $2::varchar
|
||||
AND id = $2::bigint
|
||||
AND creator = ANY ($3::varchar[]);`
|
||||
const sql = `INSERT INTO ${TableName.Message} (workspace_id, card_id, id, content, creator, created, type, data)
|
||||
VALUES ($1::uuid, $2::varchar, $3::bigint, $4::text, $5::varchar, $6::timestamptz, $7::varchar, $8::jsonb)`
|
||||
|
||||
await this.execute(sql, [this.workspace, card, message, socialIds], 'remove message')
|
||||
}
|
||||
await this.execute(
|
||||
sql,
|
||||
[db.workspace_id, db.card_id, db.id, db.content, db.creator, db.created, db.type, db.data ?? {}],
|
||||
'insert message'
|
||||
)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
async removeMessages (card: CardID, messages: MessageID[], socialIds?: SocialID[]): Promise<MessageID[]> {
|
||||
if (messages.length === 0) return []
|
||||
|
||||
const where: string[] = ['workspace_id = $1::uuid', 'card_id = $2::varchar']
|
||||
const values: any[] = [this.workspace, card]
|
||||
|
||||
let index = values.length + 1
|
||||
|
||||
if (socialIds?.length === 1) {
|
||||
where.push(`creator = $${index++}::varchar`)
|
||||
values.push(socialIds[0])
|
||||
}
|
||||
|
||||
async removeMessages(card: CardID, fromId: MessageID, toId: MessageID): Promise<void> {
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.Message}
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND card_id = $2::varchar
|
||||
AND id >= $3::bigint
|
||||
AND id <= $4::bigint;`
|
||||
|
||||
await this.execute(sql, [this.workspace, card, BigInt(fromId), BigInt(toId)], 'remove messages')
|
||||
if (socialIds != null && socialIds.length > 1) {
|
||||
where.push(`creator = ANY($${index++}::varchar[])`)
|
||||
values.push(socialIds)
|
||||
}
|
||||
|
||||
async createPatch(
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
type: PatchType,
|
||||
content: string,
|
||||
creator: SocialID,
|
||||
created: Date
|
||||
): Promise<void> {
|
||||
const db: PatchDb = {
|
||||
workspace_id: this.workspace,
|
||||
card_id: card,
|
||||
message_id: message,
|
||||
type,
|
||||
content,
|
||||
creator,
|
||||
created
|
||||
}
|
||||
|
||||
const sql = `INSERT INTO ${TableName.Patch} (workspace_id, card_id, message_id, type, content, creator, created)
|
||||
VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::text, $6::varchar, $7::timestamptz)`
|
||||
|
||||
await this.execute(
|
||||
sql,
|
||||
[db.workspace_id, db.card_id, db.message_id, db.type, db.content, db.creator, db.created],
|
||||
'insert patch'
|
||||
)
|
||||
if (messages.length === 1) {
|
||||
where.push(`id = $${index++}::bigint`)
|
||||
values.push(messages[0])
|
||||
} else {
|
||||
where.push(`id = ANY($${index++}::bigint[])`)
|
||||
values.push(messages)
|
||||
}
|
||||
|
||||
async removePatches(card: CardID, fromId: MessageID, toId: MessageID): Promise<void> {
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.Patch}
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND card_id = $2::varchar
|
||||
AND message_id >= $3::bigint
|
||||
AND message_id <= $4::bigint;`
|
||||
const sql = `DELETE FROM ${TableName.Message} WHERE ${where.join(' AND ')} RETURNING id`
|
||||
|
||||
await this.execute(sql, [this.workspace, card, BigInt(fromId), BigInt(toId)], 'remove patches')
|
||||
const result = await this.execute(sql, values, 'remove messages')
|
||||
|
||||
return result.map((row: any) => row.id)
|
||||
}
|
||||
|
||||
async createPatch (
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
type: PatchType,
|
||||
content: string,
|
||||
creator: SocialID,
|
||||
created: Date
|
||||
): Promise<void> {
|
||||
const db: PatchDb = {
|
||||
workspace_id: this.workspace,
|
||||
card_id: card,
|
||||
message_id: message,
|
||||
type,
|
||||
content,
|
||||
creator,
|
||||
created,
|
||||
message_created_sec: parseMessageId(message)
|
||||
}
|
||||
|
||||
// Attachment
|
||||
async createAttachment(message: MessageID, card: CardID, creator: SocialID, created: Date): Promise<void> {
|
||||
const db: AttachmentDb = {
|
||||
message_id: message,
|
||||
card_id: card,
|
||||
creator,
|
||||
created
|
||||
}
|
||||
const sql = `INSERT INTO ${TableName.Attachment} (message_id, card_id, creator, created)
|
||||
VALUES ($1::bigint, $2::varchar, $3::varchar, $4::timestamptz)`
|
||||
const sql = `INSERT INTO ${TableName.Patch} (workspace_id, card_id, message_id, type, content, creator, created, message_created_sec)
|
||||
VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::text, $6::varchar, $7::timestamptz, $8::timestamptz)`
|
||||
|
||||
await this.execute(sql, [db.message_id, db.card_id, db.creator, db.created], 'insert attachment')
|
||||
await this.execute(
|
||||
sql,
|
||||
[db.workspace_id, db.card_id, db.message_id, db.type, db.content, db.creator, db.created, db.message_created_sec],
|
||||
'insert patch'
|
||||
)
|
||||
}
|
||||
|
||||
// File
|
||||
async createFile (card: CardID, message: MessageID, blobId: BlobID, fileType: string, filename: string, size: number,creator: SocialID, created: Date): Promise<void> {
|
||||
const db: FileDb = {
|
||||
workspace_id: this.workspace,
|
||||
card_id: card,
|
||||
message_id: message,
|
||||
blob_id: blobId,
|
||||
type: fileType,
|
||||
filename,
|
||||
size,
|
||||
creator,
|
||||
created,
|
||||
message_created_sec: parseMessageId(message)
|
||||
}
|
||||
const sql = `INSERT INTO ${TableName.File} (workspace_id, card_id, message_id, blob_id, type, filename, creator, created, message_created_sec, size)
|
||||
VALUES ($1::uuid, $2::varchar, $3::int8, $4::uuid, $5::varchar, $6::varchar, $7::varchar, $8::timestamptz, $9::timestamptz, $10::int8)`
|
||||
|
||||
async removeAttachment(message: MessageID, card: CardID): Promise<void> {
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.Attachment}
|
||||
WHERE message_id = $1::bigint
|
||||
AND card_id = $2::varchar`
|
||||
await this.execute(sql, [message, card], 'remove attachment')
|
||||
}
|
||||
await this.execute(sql, [db.workspace_id, db.card_id, db.message_id, db.blob_id, db.type, db.filename, db.creator, db.created, db.message_created_sec, db.size], 'insert file')
|
||||
}
|
||||
|
||||
// Reaction
|
||||
async createReaction(
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
reaction: string,
|
||||
creator: SocialID,
|
||||
created: Date
|
||||
): Promise<void> {
|
||||
const select = `SELECT m.id
|
||||
async removeFile (card: CardID, message: MessageID, blobId: BlobID): Promise<void> {
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.File}
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND card_id = $2::varchar
|
||||
AND message_id = $3::bigint
|
||||
AND blob_id = $4::uuid`
|
||||
await this.execute(sql, [this.workspace, card, message, blobId], 'remove file')
|
||||
}
|
||||
|
||||
// Reaction
|
||||
async createReaction (
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
reaction: string,
|
||||
creator: SocialID,
|
||||
created: Date
|
||||
): Promise<void> {
|
||||
const select = `SELECT m.id
|
||||
FROM ${TableName.Message} m
|
||||
WHERE m.id = $1::bigint`
|
||||
|
||||
const messageDb = await this.execute(select, [message], 'select message')
|
||||
const messageDb = await this.execute(select, [message], 'select message')
|
||||
|
||||
if (messageDb.length > 0) {
|
||||
const db: ReactionDb = {
|
||||
workspace_id: this.workspace,
|
||||
card_id: card,
|
||||
message_id: message,
|
||||
reaction,
|
||||
creator,
|
||||
created
|
||||
}
|
||||
const sql = `INSERT INTO ${TableName.Reaction} (workspace_id, card_id, message_id, reaction, creator, created)
|
||||
if (messageDb.length > 0) {
|
||||
const db: ReactionDb = {
|
||||
workspace_id: this.workspace,
|
||||
card_id: card,
|
||||
message_id: message,
|
||||
reaction,
|
||||
creator,
|
||||
created
|
||||
}
|
||||
const sql = `INSERT INTO ${TableName.Reaction} (workspace_id, card_id, message_id, reaction, creator, created)
|
||||
VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::varchar, $6::timestamptz)`
|
||||
|
||||
await this.execute(
|
||||
sql,
|
||||
[db.workspace_id, db.card_id, db.message_id, db.reaction, db.creator, db.created],
|
||||
'insert reaction'
|
||||
)
|
||||
} else {
|
||||
await this.createPatch(card, message, PatchType.addReaction, reaction, creator, created)
|
||||
}
|
||||
await this.execute(
|
||||
sql,
|
||||
[db.workspace_id, db.card_id, db.message_id, db.reaction, db.creator, db.created],
|
||||
'insert reaction'
|
||||
)
|
||||
} else {
|
||||
await this.createPatch(card, message, PatchType.addReaction, reaction, creator, created)
|
||||
}
|
||||
}
|
||||
|
||||
async removeReaction(
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
reaction: string,
|
||||
creator: SocialID,
|
||||
created: Date
|
||||
): Promise<void> {
|
||||
const select = `SELECT m.id
|
||||
async removeReaction (
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
reaction: string,
|
||||
creator: SocialID,
|
||||
created: Date
|
||||
): Promise<void> {
|
||||
const select = `SELECT m.id
|
||||
FROM ${TableName.Message} m
|
||||
WHERE m.id = $1::bigint`
|
||||
|
||||
const messageDb = await this.execute(select, [message], 'select message')
|
||||
const messageDb = await this.execute(select, [message], 'select message')
|
||||
|
||||
if (messageDb.length > 0) {
|
||||
const sql = `DELETE
|
||||
if (messageDb.length > 0) {
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.Reaction}
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND card_id = $2::varchar
|
||||
AND message_id = $3::bigint
|
||||
AND reaction = $4::varchar
|
||||
AND creator = $5::varchar`
|
||||
await this.execute(sql, [this.workspace, card, message, reaction, creator], 'remove reaction')
|
||||
} else {
|
||||
await this.createPatch(card, message, PatchType.removeReaction, reaction, creator, created)
|
||||
}
|
||||
await this.execute(sql, [this.workspace, card, message, reaction, creator], 'remove reaction')
|
||||
} else {
|
||||
await this.createPatch(card, message, PatchType.removeReaction, reaction, creator, created)
|
||||
}
|
||||
}
|
||||
|
||||
// Thread
|
||||
async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void> {
|
||||
const db: ThreadDb = {
|
||||
workspace_id: this.workspace,
|
||||
card_id: card,
|
||||
message_id: message,
|
||||
thread_id: thread,
|
||||
replies_count: 0,
|
||||
last_reply: created
|
||||
}
|
||||
const sql = `INSERT INTO ${TableName.Thread} (workspace_id, card_id, message_id, thread_id, replies_count,
|
||||
// Thread
|
||||
async createThread (card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void> {
|
||||
const db: ThreadDb = {
|
||||
workspace_id: this.workspace,
|
||||
card_id: card,
|
||||
message_id: message,
|
||||
thread_id: thread,
|
||||
replies_count: 0,
|
||||
last_reply: created
|
||||
}
|
||||
const sql = `INSERT INTO ${TableName.Thread} (workspace_id, card_id, message_id, thread_id, replies_count,
|
||||
last_reply)
|
||||
VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::int, $6::timestamptz)`
|
||||
await this.execute(
|
||||
sql,
|
||||
[db.workspace_id, db.card_id, db.message_id, db.thread_id, db.replies_count, db.last_reply],
|
||||
'insert thread'
|
||||
)
|
||||
await this.execute(
|
||||
sql,
|
||||
[db.workspace_id, db.card_id, db.message_id, db.thread_id, db.replies_count, db.last_reply],
|
||||
'insert thread'
|
||||
)
|
||||
}
|
||||
|
||||
async updateThread (thread: CardID, op: 'increment' | 'decrement', lastReply?: Date): Promise<void> {
|
||||
const set: string[] = []
|
||||
const values: any[] = []
|
||||
|
||||
if (lastReply != null) {
|
||||
set.push('last_reply = $3::timestamptz')
|
||||
values.push(lastReply)
|
||||
}
|
||||
|
||||
async updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise<void> {
|
||||
if (op === 'increment') {
|
||||
const sql = `UPDATE ${TableName.Thread}
|
||||
SET replies_count = replies_count + 1,
|
||||
last_reply = $3::timestamptz
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND thread_id = $2::varchar`
|
||||
await this.execute(sql, [this.workspace, thread, lastReply], 'update thread')
|
||||
} else if (op === 'decrement') {
|
||||
const sql = `UPDATE ${TableName.Thread}
|
||||
SET replies_count = GREATEST(replies_count - 1, 0)
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND thread_id = $2::varchar`
|
||||
await this.execute(sql, [this.workspace, thread], 'update thread')
|
||||
}
|
||||
if (op === 'increment') {
|
||||
set.push('replies_count = replies_count + 1')
|
||||
} else if (op === 'decrement') {
|
||||
set.push('replies_count = GREATEST(replies_count - 1, 0)')
|
||||
}
|
||||
|
||||
// MessagesGroup
|
||||
async createMessagesGroup(
|
||||
card: CardID,
|
||||
blobId: BlobID,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
fromId: MessageID,
|
||||
toId: MessageID,
|
||||
count: number
|
||||
): Promise<void> {
|
||||
const db: MessagesGroupDb = {
|
||||
workspace_id: this.workspace,
|
||||
card_id: card,
|
||||
blob_id: blobId,
|
||||
from_date: fromDate,
|
||||
to_date: toDate,
|
||||
from_id: fromId,
|
||||
to_id: toId,
|
||||
count
|
||||
}
|
||||
const update = `UPDATE ${TableName.Thread}`
|
||||
const setSql = 'SET ' + set.join(', ')
|
||||
const where = 'WHERE workspace_id = $1::uuid AND thread_id = $2::varchar'
|
||||
const sql = [update, setSql, where].join(' ')
|
||||
await this.execute(sql, [this.workspace, thread, ...values], 'update thread')
|
||||
}
|
||||
|
||||
const sql = `INSERT INTO ${TableName.MessagesGroup} (workspace_id, card_id, blob_id, from_date, to_date,
|
||||
from_id,
|
||||
to_id, count)
|
||||
VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz, $6::bigint, $7::bigint,
|
||||
$8::int)`
|
||||
await this.execute(
|
||||
sql,
|
||||
[db.workspace_id, db.card_id, db.blob_id, db.from_date, db.to_date, db.from_id, db.to_id, db.count],
|
||||
'insert messages group'
|
||||
)
|
||||
// MessagesGroup
|
||||
async createMessagesGroup (card: CardID, blobId: BlobID, fromSec: Date, toSec: Date, count: number): Promise<void> {
|
||||
const db: MessagesGroupDb = {
|
||||
workspace_id: this.workspace,
|
||||
card_id: card,
|
||||
blob_id: blobId,
|
||||
from_sec: fromSec,
|
||||
to_sec: toSec,
|
||||
count
|
||||
}
|
||||
|
||||
async removeMessagesGroup(card: CardID, blobId: BlobID): Promise<void> {
|
||||
const sql = `DELETE
|
||||
const sql = `INSERT INTO ${TableName.MessagesGroup} (workspace_id, card_id, blob_id, from_sec, to_sec, count)
|
||||
VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz, $6::int)`
|
||||
await this.execute(
|
||||
sql,
|
||||
[db.workspace_id, db.card_id, db.blob_id, db.from_sec, db.to_sec, db.count],
|
||||
'insert messages group'
|
||||
)
|
||||
}
|
||||
|
||||
async removeMessagesGroup (card: CardID, blobId: BlobID): Promise<void> {
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.MessagesGroup}
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND card_id = $2::varchar
|
||||
AND blob_id = $3::uuid`
|
||||
await this.execute(sql, [this.workspace, card, blobId], 'remove messages group')
|
||||
await this.execute(sql, [this.workspace, card, blobId], 'remove messages group')
|
||||
}
|
||||
|
||||
async find(params: FindMessagesParams): Promise<Message[]> {
|
||||
const { where, values } = this.buildMessageWhere(params);
|
||||
const orderBy = this.buildOrderBy(params);
|
||||
const limit = this.buildLimit(params);
|
||||
|
||||
const sql = `
|
||||
WITH
|
||||
${this.buildCteLimitedMessages(where, orderBy, limit)}
|
||||
${this.buildCteAggregatedFiles(params)}
|
||||
${this.buildCteAggregatedReactions(params)}
|
||||
${this.buildCteAggregatedPatches()}
|
||||
${this.buildMainSelect(params)}
|
||||
`;
|
||||
|
||||
const result = await this.execute(sql, values, 'find messages');
|
||||
return result.map((it: any) => toMessage(it));
|
||||
}
|
||||
|
||||
private buildOrderBy(params: FindMessagesParams): string {
|
||||
return params.order != null
|
||||
? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}`
|
||||
: '';
|
||||
}
|
||||
|
||||
private buildLimit(params: FindMessagesParams): string {
|
||||
return params.limit != null ? `LIMIT ${params.limit}` : '';
|
||||
}
|
||||
|
||||
private buildCteLimitedMessages(where: string, orderBy: string, limit: string): string {
|
||||
return `
|
||||
limited_messages AS (
|
||||
SELECT *
|
||||
FROM ${TableName.Message} m
|
||||
${where}
|
||||
${orderBy}
|
||||
${limit}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
private buildCteAggregatedFiles(params: FindMessagesParams): string {
|
||||
if (!params.files) return '';
|
||||
return `,
|
||||
agg_files AS (
|
||||
SELECT
|
||||
f.workspace_id,
|
||||
f.card_id,
|
||||
f.message_id,
|
||||
jsonb_agg(jsonb_build_object(
|
||||
'card_id', f.card_id,
|
||||
'message_id', f.message_id,
|
||||
'blob_id', f.blob_id,
|
||||
'type', f.type,
|
||||
'filename', f.filename,
|
||||
'creator', f.creator,
|
||||
'created', f.created
|
||||
)) AS files
|
||||
FROM ${TableName.File} f
|
||||
INNER JOIN limited_messages m
|
||||
ON m.workspace_id = f.workspace_id
|
||||
AND m.card_id = f.card_id
|
||||
AND m.id = f.message_id
|
||||
GROUP BY f.workspace_id, f.card_id, f.message_id
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
private buildCteAggregatedReactions(params: FindMessagesParams): string {
|
||||
if (!params.reactions) return '';
|
||||
return `,
|
||||
agg_reactions AS (
|
||||
SELECT
|
||||
r.workspace_id,
|
||||
r.card_id,
|
||||
r.message_id,
|
||||
jsonb_agg(jsonb_build_object(
|
||||
'message_id', r.message_id,
|
||||
'reaction', r.reaction,
|
||||
'creator', r.creator,
|
||||
'created', r.created
|
||||
)) AS reactions
|
||||
FROM ${TableName.Reaction} r
|
||||
INNER JOIN limited_messages m
|
||||
ON m.workspace_id = r.workspace_id
|
||||
AND m.card_id = r.card_id
|
||||
AND m.id = r.message_id
|
||||
GROUP BY r.workspace_id, r.card_id, r.message_id
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
private buildCteAggregatedPatches(): string {
|
||||
return `,
|
||||
agg_patches AS (
|
||||
SELECT
|
||||
p.workspace_id,
|
||||
p.card_id,
|
||||
p.message_id,
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'content', p.content,
|
||||
'creator', p.creator,
|
||||
'created', p.created
|
||||
) ORDER BY p.created DESC
|
||||
) AS patches
|
||||
FROM ${TableName.Patch} p
|
||||
INNER JOIN limited_messages m
|
||||
ON m.workspace_id = p.workspace_id
|
||||
AND m.card_id = p.card_id
|
||||
AND m.id = p.message_id
|
||||
WHERE p.type = 'update'
|
||||
GROUP BY p.workspace_id, p.card_id, p.message_id
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
private buildMainSelect(params: FindMessagesParams): string {
|
||||
const orderBy = this.buildOrderBy(params);
|
||||
const selectReplies = params.replies
|
||||
? `t.thread_id as thread_id, t.replies_count as replies_count, t.last_reply as last_reply,`
|
||||
: '';
|
||||
|
||||
const selectFiles = params.files
|
||||
? `COALESCE(f.files, '[]'::jsonb) AS files,`
|
||||
: `'[]'::jsonb AS files,`;
|
||||
|
||||
const selectReactions = params.reactions
|
||||
? `COALESCE(r.reactions, '[]'::jsonb) AS reactions,`
|
||||
: `'[]'::jsonb AS reactions,`;
|
||||
|
||||
const joinFiles = params.files ? `
|
||||
LEFT JOIN agg_files f
|
||||
ON f.workspace_id = m.workspace_id
|
||||
AND f.card_id = m.card_id
|
||||
AND f.message_id = m.id` : '';
|
||||
|
||||
const joinReactions = params.reactions ? `
|
||||
LEFT JOIN agg_reactions r
|
||||
ON r.workspace_id = m.workspace_id
|
||||
AND r.card_id = m.card_id
|
||||
AND r.message_id = m.id` : '';
|
||||
|
||||
return `
|
||||
SELECT
|
||||
m.id,
|
||||
m.card_id,
|
||||
m.type,
|
||||
m.content,
|
||||
m.creator,
|
||||
m.created,
|
||||
m.data,
|
||||
${selectReplies}
|
||||
${selectFiles}
|
||||
${selectReactions}
|
||||
COALESCE(p.patches, '[]'::jsonb) AS patches
|
||||
FROM limited_messages m
|
||||
LEFT JOIN ${TableName.Thread} t
|
||||
ON t.workspace_id = m.workspace_id
|
||||
AND t.card_id = m.card_id
|
||||
AND t.message_id = m.id
|
||||
${joinFiles}
|
||||
${joinReactions}
|
||||
LEFT JOIN agg_patches p
|
||||
ON p.workspace_id = m.workspace_id
|
||||
AND p.card_id = m.card_id
|
||||
AND p.message_id = m.id
|
||||
${orderBy}
|
||||
`;
|
||||
}
|
||||
|
||||
buildMessageWhere (params: FindMessagesParams): { where: string, values: any[] } {
|
||||
const where: string[] = ['m.workspace_id = $1::uuid']
|
||||
const values: any[] = [this.workspace]
|
||||
|
||||
let index = 2
|
||||
|
||||
if (params.id != null) {
|
||||
where.push(`m.id = $${index++}::bigint`)
|
||||
values.push(params.id)
|
||||
}
|
||||
|
||||
// Find messages
|
||||
async find(params: FindMessagesParams): Promise<Message[]> {
|
||||
// TODO: experiment with select to improve performance
|
||||
const select = `SELECT m.id,
|
||||
m.card_id,
|
||||
m.content,
|
||||
m.creator,
|
||||
m.created,
|
||||
t.thread_id as thread_id,
|
||||
t.replies_count as replies_count,
|
||||
t.last_reply as last_reply,
|
||||
${this.subSelectPatches()},
|
||||
${this.subSelectReactions()}
|
||||
FROM ${TableName.Message} m
|
||||
LEFT JOIN ${TableName.Thread} t
|
||||
ON t.workspace_id = m.workspace_id AND t.card_id = m.card_id AND
|
||||
t.message_id = m.id`
|
||||
|
||||
const {where, values} = this.buildMessageWhere(params)
|
||||
const orderBy =
|
||||
params.order != null ? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
|
||||
const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''
|
||||
const sql = [select, where, orderBy, limit].join(' ')
|
||||
|
||||
const result = await this.execute(sql, values, 'find messages')
|
||||
|
||||
return result.map((it: any) => toMessage(it))
|
||||
if (params.card != null) {
|
||||
where.push(`m.card_id = $${index++}::varchar`)
|
||||
values.push(params.card)
|
||||
}
|
||||
|
||||
buildMessageWhere(params: FindMessagesParams): { where: string, values: any[] } {
|
||||
const where: string[] = ['m.workspace_id = $1::uuid']
|
||||
const values: any[] = [this.workspace]
|
||||
const createdCondition = getCondition('m', 'created', index, params.created, 'timestamptz')
|
||||
|
||||
let index = 2
|
||||
|
||||
if (params.id != null) {
|
||||
where.push(`m.id = $${index++}::bigint`)
|
||||
values.push(params.id)
|
||||
}
|
||||
|
||||
if (params.card != null) {
|
||||
where.push(`m.card_id = $${index++}::varchar`)
|
||||
values.push(params.card)
|
||||
}
|
||||
|
||||
const createdCondition = getCondition('m', 'created', index, params.created, 'timestamptz')
|
||||
|
||||
if (createdCondition != null) {
|
||||
where.push(createdCondition.where)
|
||||
values.push(createdCondition.value)
|
||||
index++
|
||||
}
|
||||
|
||||
return {where: `WHERE ${where.join(' AND ')}`, values}
|
||||
if (createdCondition != null) {
|
||||
where.push(createdCondition.where)
|
||||
values.push(...createdCondition.values)
|
||||
index = createdCondition.index
|
||||
}
|
||||
|
||||
subSelectPatches(): string {
|
||||
return `COALESCE(
|
||||
(SELECT jsonb_agg(jsonb_build_object(
|
||||
'content', p.content,
|
||||
'creator', p.creator,
|
||||
'created', p.created
|
||||
) ORDER BY p.created DESC)
|
||||
FROM ${TableName.Patch} p
|
||||
WHERE p.message_id = m.id
|
||||
AND p.workspace_id = m.workspace_id
|
||||
AND p.card_id = m.card_id
|
||||
AND p.type = 'update'
|
||||
), '[]'::jsonb) AS patches`
|
||||
}
|
||||
return { where: `WHERE ${where.join(' AND ')}`, values }
|
||||
}
|
||||
|
||||
subSelectAttachments(): string {
|
||||
return `COALESCE(
|
||||
(SELECT jsonb_agg(jsonb_build_object(
|
||||
'card_id', a.card_id,
|
||||
'message_id', a.message_id,
|
||||
'creator', a.creator,
|
||||
'created', a.created
|
||||
))
|
||||
FROM ${TableName.Attachment} a
|
||||
WHERE a.message_id = m.id
|
||||
), '[]'::jsonb) AS attachments`
|
||||
}
|
||||
|
||||
subSelectReactions(): string {
|
||||
return `COALESCE(
|
||||
(SELECT jsonb_agg(jsonb_build_object(
|
||||
'message_id', r.message_id,
|
||||
'reaction', r.reaction,
|
||||
'creator', r.creator,
|
||||
'created', r.created
|
||||
))
|
||||
FROM ${TableName.Reaction} r
|
||||
WHERE r.workspace_id = m.workspace_id
|
||||
AND r.card_id = m.card_id
|
||||
AND r.message_id = m.id
|
||||
), '[]'::jsonb) AS reactions`
|
||||
}
|
||||
|
||||
// Find thread
|
||||
async findThread(thread: CardID): Promise<Thread | undefined> {
|
||||
const sql = `SELECT t.card_id,
|
||||
// Find thread
|
||||
async findThread (thread: CardID): Promise<Thread | undefined> {
|
||||
const sql = `SELECT t.card_id,
|
||||
t.message_id,
|
||||
t.thread_id,
|
||||
t.replies_count,
|
||||
@@ -418,19 +504,17 @@ export class MessagesDb extends BaseDb {
|
||||
AND t.thread_id = $2::varchar
|
||||
LIMIT 1;`
|
||||
|
||||
const result = await this.execute(sql, [this.workspace, thread], 'find thread')
|
||||
return result.map((it: any) => toThread(it))[0]
|
||||
}
|
||||
const result = await this.execute(sql, [this.workspace, thread], 'find thread')
|
||||
return result.map((it: any) => toThread(it))[0]
|
||||
}
|
||||
|
||||
// Find messages groups
|
||||
async findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
|
||||
const select = `
|
||||
// Find messages groups
|
||||
async findMessagesGroups (params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
|
||||
const select = `
|
||||
SELECT mg.card_id,
|
||||
mg.blob_id,
|
||||
mg.from_date,
|
||||
mg.to_date,
|
||||
mg.from_id,
|
||||
mg.to_id,
|
||||
mg.from_sec,
|
||||
mg.to_sec,
|
||||
mg.count,
|
||||
patches
|
||||
FROM ${TableName.MessagesGroup} mg
|
||||
@@ -445,57 +529,57 @@ export class MessagesDb extends BaseDb {
|
||||
FROM ${TableName.Patch} p
|
||||
WHERE p.workspace_id = mg.workspace_id
|
||||
AND p.card_id = mg.card_id
|
||||
AND p.message_id BETWEEN mg.from_id AND mg.to_id
|
||||
AND p.message_created_sec BETWEEN mg.from_sec AND mg.to_sec
|
||||
) sub`
|
||||
|
||||
const {where, values} = this.buildMessagesGroupWhere(params)
|
||||
const orderBy =
|
||||
params.orderBy === 'toDate'
|
||||
? `ORDER BY mg.to_date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}`
|
||||
: `ORDER BY mg.from_date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}`
|
||||
const limit = params.limit != null ? ` LIMIT ${params.limit}` : '';
|
||||
const { where, values } = this.buildMessagesGroupWhere(params)
|
||||
const orderBy =
|
||||
params.orderBy === 'toSec'
|
||||
? `ORDER BY mg.to_sec ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}`
|
||||
: `ORDER BY mg.from_sec ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}`
|
||||
const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''
|
||||
|
||||
const sql = [select, where, orderBy, limit].join(' ')
|
||||
const result = await this.execute(sql, values, 'find messages groups')
|
||||
const sql = [select, where, orderBy, limit].join(' ')
|
||||
const result = await this.execute(sql, values, 'find messages groups')
|
||||
|
||||
return result.map((it: any) => toMessagesGroup(it))
|
||||
return result.map((it: any) => toMessagesGroup(it))
|
||||
}
|
||||
|
||||
buildMessagesGroupWhere (params: FindMessagesGroupsParams): {
|
||||
where: string
|
||||
values: any[]
|
||||
} {
|
||||
const where: string[] = ['mg.workspace_id = $1::uuid']
|
||||
const values: any[] = [this.workspace]
|
||||
|
||||
let index = 2
|
||||
|
||||
where.push(`mg.card_id = $${index++}::varchar`)
|
||||
values.push(params.card)
|
||||
|
||||
if (params.blobId != null) {
|
||||
where.push(`mg.blob_id = $${index++}`)
|
||||
values.push(params.blobId)
|
||||
}
|
||||
|
||||
buildMessagesGroupWhere(params: FindMessagesGroupsParams): {
|
||||
where: string
|
||||
values: any[]
|
||||
} {
|
||||
const where: string[] = ['mg.workspace_id = $1::uuid']
|
||||
const values: any[] = [this.workspace]
|
||||
|
||||
let index = 2
|
||||
|
||||
where.push(`mg.card_id = $${index++}::varchar`)
|
||||
values.push(params.card)
|
||||
|
||||
if (params.blobId != null) {
|
||||
where.push(`mg.blob_id = $${index++}`)
|
||||
values.push(params.blobId)
|
||||
}
|
||||
|
||||
const fromDateCondition = getCondition('mg', 'from_date', index, params.fromDate, 'timestamptz')
|
||||
if (fromDateCondition != null) {
|
||||
where.push(fromDateCondition.where)
|
||||
values.push(fromDateCondition.value)
|
||||
index++
|
||||
}
|
||||
|
||||
const toDateCondition = getCondition('mg', 'to_date', index, params.toDate, 'timestamptz')
|
||||
if (toDateCondition != null) {
|
||||
where.push(toDateCondition.where)
|
||||
values.push(toDateCondition.value)
|
||||
index++
|
||||
}
|
||||
|
||||
if (params.withPatches === true) {
|
||||
where.push(`sub.patches IS NOT NULL`)
|
||||
}
|
||||
|
||||
return {where: `WHERE ${where.join(' AND ')}`, values}
|
||||
const fromDateCondition = getCondition('mg', 'from_sec', index, params.fromSec, 'timestamptz')
|
||||
if (fromDateCondition != null) {
|
||||
where.push(fromDateCondition.where)
|
||||
values.push(...fromDateCondition.values)
|
||||
index = fromDateCondition.index
|
||||
}
|
||||
|
||||
const toDateCondition = getCondition('mg', 'to_sec', index, params.toSec, 'timestamptz')
|
||||
if (toDateCondition != null) {
|
||||
where.push(toDateCondition.where)
|
||||
values.push(...toDateCondition.values)
|
||||
index = toDateCondition.index
|
||||
}
|
||||
|
||||
if (params.patches === true) {
|
||||
where.push('sub.patches IS NOT NULL')
|
||||
}
|
||||
|
||||
return { where: `WHERE ${where.join(' AND ')}`, values }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,82 +14,140 @@
|
||||
//
|
||||
|
||||
import {
|
||||
type MessageID,
|
||||
type ContextID,
|
||||
type AccountID,
|
||||
type CardID,
|
||||
type NotificationContext,
|
||||
type Collaborator,
|
||||
type ContextID,
|
||||
type FindCollaboratorsParams,
|
||||
type FindNotificationContextParams,
|
||||
SortingOrder,
|
||||
type FindNotificationsParams,
|
||||
type MessageID,
|
||||
type Notification,
|
||||
type NotificationContextUpdate,
|
||||
type WorkspaceID
|
||||
type NotificationContext,
|
||||
SortingOrder,
|
||||
type NotificationID
|
||||
} from '@hcengineering/communication-types'
|
||||
|
||||
import { BaseDb } from './base'
|
||||
import { TableName, type ContextDb, type NotificationDb } from './schema'
|
||||
import { type ContextDb, type NotificationDb, TableName } from './schema'
|
||||
import { getCondition } from './utils'
|
||||
import { toCollaborator, toNotification, toNotificationContext } from './mapping'
|
||||
|
||||
export class NotificationsDb extends BaseDb {
|
||||
async createNotification (message: MessageID, context: ContextID): Promise<void> {
|
||||
const db: NotificationDb = {
|
||||
message_id: message,
|
||||
context
|
||||
async addCollaborators (card: CardID, collaborators: AccountID[], date?: Date): Promise<void> {
|
||||
if (collaborators.length === 0) return
|
||||
const values: any[] = []
|
||||
|
||||
const sqlValues = collaborators
|
||||
.map((account, index) => {
|
||||
const i = index * 3
|
||||
values.push(this.workspace, card, account, date ?? new Date())
|
||||
return `($${i + 1}::uuid, $${i + 2}::varchar, $${i + 3}::uuid, $${i + 4}::timestamptz)`
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
const sql = `INSERT INTO ${TableName.Collaborators} (workspace_id, card_id, account, date) VALUES ${sqlValues} ON CONFLICT DO NOTHING`
|
||||
|
||||
await this.execute(sql, values, 'insert collaborators')
|
||||
}
|
||||
|
||||
async removeCollaborators (card: CardID, collaborators: AccountID[]): Promise<void> {
|
||||
if (collaborators.length === 0) return
|
||||
|
||||
if (collaborators.length === 1) {
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.Collaborators}
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND card_id = $2::varchar
|
||||
AND account = $3::uuid`
|
||||
await this.execute(sql, [this.workspace, card, collaborators[0]], 'remove collaborator')
|
||||
} else {
|
||||
const inValues = collaborators.map((_, index) => `$${index + 3}`).join(', ')
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.Collaborators}
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND card_id = $2::varchar
|
||||
AND account IN (${inValues})`
|
||||
|
||||
await this.execute(sql, [this.workspace, card, ...collaborators], 'remove collaborators')
|
||||
}
|
||||
const sql = `INSERT INTO ${TableName.Notification} (message_id, context_id)
|
||||
VALUES ($1::bigint, $2::uuid)`
|
||||
await this.execute(sql, [db.message_id, db.context], 'insert notification')
|
||||
}
|
||||
|
||||
async removeNotification (message: MessageID, context: ContextID): Promise<void> {
|
||||
// TODO: do we need to remove notifications?
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.NotificationContext}
|
||||
WHERE id = $1::uuid`
|
||||
await this.execute(sql, [context], 'remove notification')
|
||||
}
|
||||
|
||||
async createContext (
|
||||
personalWorkspace: WorkspaceID,
|
||||
getCollaboratorsCursor (
|
||||
card: CardID,
|
||||
lastView?: Date,
|
||||
lastUpdate?: Date
|
||||
): Promise<ContextID> {
|
||||
date: Date,
|
||||
size?: number
|
||||
): AsyncIterable<NonNullable<Collaborator[][number]>[]> {
|
||||
const sql = `
|
||||
SELECT account
|
||||
FROM ${TableName.Collaborators}
|
||||
WHERE workspace_id = $1::uuid
|
||||
AND card_id = $2::varchar
|
||||
AND date <= $3::timestamptz
|
||||
ORDER BY date ASC `
|
||||
|
||||
return this.client.cursor<Collaborator>(sql, [this.workspace, card, date], size)
|
||||
}
|
||||
|
||||
async createNotification (context: ContextID, message: MessageID, created: Date): Promise<NotificationID> {
|
||||
const db: Omit<NotificationDb, 'id'> = {
|
||||
message_id: message,
|
||||
context_id: context,
|
||||
created
|
||||
}
|
||||
const sql = `INSERT INTO ${TableName.Notification} (message_id, context_id, created)
|
||||
VALUES ($1::bigint, $2::int8, $3::timestamptz)
|
||||
RETURNING id`
|
||||
const result = await this.execute(sql, [db.message_id, db.context_id, db.created], 'insert notification')
|
||||
return result[0].id as NotificationID
|
||||
}
|
||||
|
||||
async removeNotifications (context: ContextID, account: AccountID, untilDate: Date): Promise<void> {
|
||||
const sql = `
|
||||
DELETE FROM ${TableName.Notification} n
|
||||
USING ${TableName.NotificationContext} nc
|
||||
WHERE n.context_id = $1::int8
|
||||
AND nc.id = n.context_id
|
||||
AND nc.account = $2::uuid
|
||||
AND n.created < $3::timestamptz
|
||||
`
|
||||
await this.execute(sql, [context, account, untilDate], 'remove notification')
|
||||
}
|
||||
|
||||
async createContext (account: AccountID, card: CardID, lastUpdate: Date, lastView: Date): Promise<ContextID> {
|
||||
const db: ContextDb = {
|
||||
workspace_id: this.workspace,
|
||||
card_id: card,
|
||||
personal_workspace: personalWorkspace,
|
||||
account,
|
||||
last_view: lastView,
|
||||
last_update: lastUpdate
|
||||
}
|
||||
const sql = `INSERT INTO ${TableName.NotificationContext} (workspace_id, card_id, personal_workspace, last_view, last_update)
|
||||
VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz)
|
||||
RETURNING id`
|
||||
const sql = `INSERT INTO ${TableName.NotificationContext} (workspace_id, card_id, account, last_view, last_update)
|
||||
VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz)
|
||||
RETURNING id`
|
||||
const result = await this.execute(
|
||||
sql,
|
||||
[db.workspace_id, db.card_id, db.personal_workspace, db.last_view, db.last_update],
|
||||
[db.workspace_id, db.card_id, db.account, db.last_view, db.last_update],
|
||||
'insert notification context'
|
||||
)
|
||||
return result[0].id as ContextID
|
||||
}
|
||||
|
||||
async removeContext (context: ContextID): Promise<void> {
|
||||
async removeContext (context: ContextID, account: AccountID): Promise<void> {
|
||||
const sql = `DELETE
|
||||
FROM ${TableName.Notification}
|
||||
WHERE context = $1::uuid`
|
||||
await this.execute(sql, [context], 'remove notification context')
|
||||
FROM ${TableName.Notification}
|
||||
WHERE context = $1::int8 AND account = $2::uuid`
|
||||
await this.execute(sql, [context, account], 'remove notification context')
|
||||
}
|
||||
|
||||
async updateContext (context: ContextID, update: NotificationContextUpdate): Promise<void> {
|
||||
async updateContext (context: ContextID, account: AccountID, lastUpdate?: Date, lastView?: Date): Promise<void> {
|
||||
const dbData: Partial<ContextDb> = {}
|
||||
|
||||
if (update.archivedFrom != null) {
|
||||
dbData.archived_from = update.archivedFrom
|
||||
if (lastView != null) {
|
||||
dbData.last_view = lastView
|
||||
}
|
||||
if (update.lastView != null) {
|
||||
dbData.last_view = update.lastView
|
||||
}
|
||||
if (update.lastUpdate != null) {
|
||||
dbData.last_update = update.lastUpdate
|
||||
if (lastUpdate != null) {
|
||||
dbData.last_update = lastUpdate
|
||||
}
|
||||
|
||||
if (Object.keys(dbData).length === 0) {
|
||||
@@ -100,91 +158,236 @@ export class NotificationsDb extends BaseDb {
|
||||
const values = Object.values(dbData)
|
||||
|
||||
const sql = `UPDATE ${TableName.NotificationContext}
|
||||
SET ${keys.map((k, idx) => `"${k}" = $${idx + 1}::timestamptz`).join(', ')}
|
||||
WHERE id = $${keys.length + 1}::uuid;`
|
||||
await this.execute(sql, [values, context], 'update notification context')
|
||||
SET ${keys.map((k, idx) => `"${k}" = $${idx + 3}::timestamptz`).join(', ')}
|
||||
WHERE id = $1::int8 AND account = $2::uuid;`
|
||||
|
||||
await this.execute(sql, [context, account, ...values], 'update notification context')
|
||||
}
|
||||
|
||||
async findContexts (
|
||||
params: FindNotificationContextParams,
|
||||
personalWorkspaces: WorkspaceID[],
|
||||
workspace?: WorkspaceID
|
||||
): Promise<NotificationContext[]> {
|
||||
const select = `
|
||||
SELECT nc.id, nc.card_id, nc.archived_from, nc.last_view, nc.last_update
|
||||
FROM ${TableName.NotificationContext} nc`
|
||||
const { where, values } = this.buildContextWhere(params, personalWorkspaces, workspace)
|
||||
// const orderSql = `ORDER BY nc.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}`
|
||||
const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''
|
||||
const sql = [select, where, limit].join(' ')
|
||||
async findContexts (params: FindNotificationContextParams): Promise<NotificationContext[]> {
|
||||
const withNotifications = params.notifications != null
|
||||
const withMessages = params.notifications?.message === true
|
||||
|
||||
const result = await this.execute(sql, values, 'find notification contexts')
|
||||
|
||||
return result.map((it) => this.toNotificationContext(it))
|
||||
}
|
||||
|
||||
async findNotifications (
|
||||
params: FindNotificationsParams,
|
||||
personalWorkspace: WorkspaceID,
|
||||
workspace?: WorkspaceID
|
||||
): Promise<Notification[]> {
|
||||
// TODO: experiment with select to improve performance, should join with attachments and reactions?
|
||||
const select = `
|
||||
SELECT n.message_id,
|
||||
n.context,
|
||||
m.card_id AS message_card,
|
||||
m.content AS message_content,
|
||||
m.creator AS message_creator,
|
||||
m.created AS message_created,
|
||||
nc.card_id,
|
||||
nc.archived_from,
|
||||
nc.last_view,
|
||||
nc.last_update,
|
||||
(SELECT json_agg(
|
||||
jsonb_build_object(
|
||||
'id', p.id,
|
||||
'content', p.content,
|
||||
'creator', p.creator,
|
||||
'created', p.created
|
||||
)
|
||||
)
|
||||
FROM ${TableName.Patch} p
|
||||
WHERE p.message_id = m.id) AS patches
|
||||
FROM ${TableName.Notification} n
|
||||
JOIN ${TableName.NotificationContext} nc ON n.context = nc.id
|
||||
JOIN ${TableName.Message} m ON n.message_id = m.id
|
||||
`
|
||||
const { where, values } = this.buildNotificationWhere(params, personalWorkspace, workspace)
|
||||
const { where, values, index } = this.buildContextWhere(params)
|
||||
const limit = params.limit != null ? `LIMIT ${Number(params.limit)}` : ''
|
||||
const orderBy =
|
||||
params.order != null ? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
|
||||
const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''
|
||||
const sql = [select, where, orderBy, limit].join(' ')
|
||||
params.order != null ? `ORDER BY nc.last_update ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
|
||||
|
||||
let joinMessages = ''
|
||||
let buildObject = `
|
||||
JSONB_BUILD_OBJECT(
|
||||
'id', n.id,
|
||||
'created', n.created,
|
||||
'message_id', n.message_id
|
||||
)`
|
||||
|
||||
if (withMessages) {
|
||||
joinMessages = `
|
||||
LEFT JOIN ${TableName.Message} m
|
||||
ON n.message_id = m.id
|
||||
AND nc.workspace_id = m.workspace_id
|
||||
AND nc.card_id = m.card_id
|
||||
LEFT JOIN ${TableName.MessagesGroup} mg
|
||||
ON nc.workspace_id = mg.workspace_id
|
||||
AND nc.card_id = mg.card_id
|
||||
AND n.created BETWEEN mg.from_sec AND mg.to_sec`
|
||||
|
||||
buildObject = `
|
||||
JSONB_BUILD_OBJECT(
|
||||
'id', n.id,
|
||||
'created', n.created,
|
||||
'message_id', n.message_id,
|
||||
'message_type', m.type,
|
||||
'message_content', m.content,
|
||||
'message_data', m.data,
|
||||
'message_creator', m.creator,
|
||||
'message_created', m.created,
|
||||
'message_group_blob_id', mg.blob_id,
|
||||
'message_group_from_sec', mg.from_sec,
|
||||
'message_group_to_sec', mg.to_sec,
|
||||
'message_group_count', mg.count,
|
||||
'message_patches', (
|
||||
SELECT COALESCE(
|
||||
JSON_AGG(
|
||||
JSONB_BUILD_OBJECT(
|
||||
'patch_type', p.type,
|
||||
'patch_content', p.content,
|
||||
'patch_creator', p.creator,
|
||||
'patch_created', p.created
|
||||
) ORDER BY p.created DESC
|
||||
),
|
||||
'[]'::JSONB
|
||||
)
|
||||
FROM ${TableName.Patch} p
|
||||
WHERE p.message_id = n.message_id
|
||||
)
|
||||
)`
|
||||
}
|
||||
|
||||
let joinNotifications = ''
|
||||
let notificationsSelect = ''
|
||||
let groupBy = ''
|
||||
|
||||
if (withNotifications) {
|
||||
const { where: whereNotifications, values: valuesNotifications } = this.buildNotificationWhere(
|
||||
{ read: params.notifications?.read },
|
||||
index,
|
||||
true
|
||||
)
|
||||
|
||||
values.push(...valuesNotifications)
|
||||
|
||||
joinNotifications = `
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
n.*,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY n.context_id
|
||||
ORDER BY n.created ${params.notifications?.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}
|
||||
) AS rn
|
||||
FROM ${TableName.Notification} n
|
||||
${whereNotifications} ${whereNotifications.length > 1 ? 'AND n.context_id = nc.id' : 'WHERE n.context_id = nc.id'}
|
||||
) n ON n.rn <= ${params.notifications?.limit ?? 1}`
|
||||
|
||||
notificationsSelect = `,
|
||||
COALESCE(
|
||||
JSON_AGG(
|
||||
${buildObject}
|
||||
ORDER BY n.created ${params.notifications?.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}
|
||||
),
|
||||
'[]'::JSONB
|
||||
) AS notifications`
|
||||
|
||||
groupBy = 'GROUP BY nc.id'
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT nc.id,
|
||||
nc.card_id,
|
||||
nc.account,
|
||||
nc.last_view,
|
||||
nc.last_update
|
||||
${notificationsSelect}
|
||||
FROM ${TableName.NotificationContext} nc
|
||||
${joinNotifications}
|
||||
${joinMessages}
|
||||
${where}
|
||||
${groupBy}
|
||||
${orderBy}
|
||||
${limit};
|
||||
`
|
||||
|
||||
const result = await this.execute(sql, values, 'find contexts')
|
||||
|
||||
return result.map((it: any) => toNotificationContext(it))
|
||||
}
|
||||
|
||||
async findNotifications (params: FindNotificationsParams): Promise<Notification[]> {
|
||||
const withMessage = params.message === true
|
||||
|
||||
let select = 'SELECT n.id, n.created,n.message_id, n.context_id, nc.last_view '
|
||||
|
||||
let joinMessages = ''
|
||||
|
||||
if (withMessage) {
|
||||
select += `,
|
||||
m.card_id AS card_id,
|
||||
m.type AS message_type,
|
||||
m.content AS message_content,
|
||||
m.creator AS message_creator,
|
||||
m.created AS message_created,
|
||||
m.data AS message_data,
|
||||
mg.blob_id AS message_group_blob_id,
|
||||
mg.from_sec AS message_group_from_sec,
|
||||
mg.to_sec AS message_group_to_sec,
|
||||
mg.count AS message_group_count,
|
||||
(SELECT json_agg(
|
||||
jsonb_build_object(
|
||||
'id', p.id,
|
||||
'content', p.content,
|
||||
'creator', p.creator,
|
||||
'created', p.created
|
||||
)
|
||||
)
|
||||
FROM ${TableName.Patch} p
|
||||
WHERE p.message_id = m.id) AS message_patches
|
||||
`
|
||||
|
||||
joinMessages = `
|
||||
LEFT JOIN ${TableName.Message} m
|
||||
ON n.message_id = m.id
|
||||
AND nc.workspace_id = m.workspace_id
|
||||
AND nc.card_id = m.card_id
|
||||
LEFT JOIN ${TableName.MessagesGroup} mg
|
||||
ON nc.workspace_id = mg.workspace_id
|
||||
AND nc.card_id = mg.card_id
|
||||
AND n.created BETWEEN mg.from_sec AND mg.to_sec
|
||||
`
|
||||
}
|
||||
|
||||
select += ` FROM ${TableName.Notification} n
|
||||
JOIN ${TableName.NotificationContext} nc ON n.context_id = nc.id`
|
||||
|
||||
const { where, values } = this.buildNotificationWhere(params)
|
||||
const orderBy =
|
||||
params.order != null ? `ORDER BY n.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
|
||||
const limit = params.limit != null ? `LIMIT ${params.limit}` : ''
|
||||
|
||||
const sql = [select, joinMessages, where, orderBy, limit].join(' ')
|
||||
|
||||
const result = await this.execute(sql, values, 'find notifications')
|
||||
|
||||
return result.map((it) => this.toNotification(it))
|
||||
return result.map((it: any) => toNotification(it))
|
||||
}
|
||||
|
||||
buildContextWhere (
|
||||
params: FindNotificationContextParams,
|
||||
personalWorkspaces: WorkspaceID[],
|
||||
workspace?: WorkspaceID
|
||||
): {
|
||||
where: string
|
||||
values: any[]
|
||||
} {
|
||||
const where: string[] = []
|
||||
const values: any[] = []
|
||||
let index = 1
|
||||
async findCollaborators (params: FindCollaboratorsParams): Promise<Collaborator[]> {
|
||||
const { where, values } = this.buildCollaboratorsWhere(params)
|
||||
const select = `
|
||||
SELECT c.account
|
||||
FROM ${TableName.Collaborators} c
|
||||
`
|
||||
|
||||
if (workspace != null) {
|
||||
where.push(`nc.workspace_id = $${index++}::uuid`)
|
||||
values.push(workspace)
|
||||
const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''
|
||||
const orderBy =
|
||||
params.order != null ? `ORDER BY c.date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
|
||||
|
||||
const sql = [select, where, orderBy, limit].join(' ')
|
||||
const result = await this.execute(sql, values, 'find collaborators')
|
||||
|
||||
return result.map((it: any) => toCollaborator(it))
|
||||
}
|
||||
|
||||
private buildCollaboratorsWhere (params: FindCollaboratorsParams): { where: string, values: any[] } {
|
||||
const where: string[] = ['c.workspace_id = $1::uuid', 'c.card_id = $2::varchar']
|
||||
const values: any[] = [this.workspace, params.card]
|
||||
let index = values.length + 1
|
||||
|
||||
if (params.account != null) {
|
||||
const accounts = Array.isArray(params.account) ? params.account : [params.account]
|
||||
if (accounts.length === 1) {
|
||||
where.push(`c.account = $${index++}::uuid`)
|
||||
values.push(accounts[0])
|
||||
} else if (accounts.length > 1) {
|
||||
where.push(`c.account = ANY($${index++}::uuid[])`)
|
||||
values.push(accounts)
|
||||
}
|
||||
}
|
||||
|
||||
if (personalWorkspaces.length > 0) {
|
||||
where.push(`nc.personal_workspace IN (${personalWorkspaces.map((it) => `$${index++}::uuid`).join(', ')})`)
|
||||
values.push(...personalWorkspaces)
|
||||
return { where: `WHERE ${where.join(' AND ')}`, values }
|
||||
}
|
||||
|
||||
private buildContextWhere (params: FindNotificationContextParams): {
|
||||
where: string
|
||||
values: any[]
|
||||
index: number
|
||||
} {
|
||||
const where: string[] = ['nc.workspace_id = $1::uuid']
|
||||
const values: any[] = [this.workspace]
|
||||
let index = 2
|
||||
|
||||
if (params.id != null) {
|
||||
where.push(`nc.id = $${index++}::int8`)
|
||||
values.push(params.id)
|
||||
}
|
||||
|
||||
if (params.card != null) {
|
||||
@@ -192,82 +395,64 @@ export class NotificationsDb extends BaseDb {
|
||||
values.push(params.card)
|
||||
}
|
||||
|
||||
return { where: `WHERE ${where.join(' AND ')}`, values }
|
||||
if (params.account != null) {
|
||||
const accounts = Array.isArray(params.account) ? params.account : [params.account]
|
||||
if (accounts.length === 1) {
|
||||
where.push(`nc.account = $${index++}::uuid`)
|
||||
values.push(accounts[0])
|
||||
} else if (accounts.length > 1) {
|
||||
where.push(`nc.account IN (${accounts.map((it) => `$${index++}::uuid`).join(', ')})`)
|
||||
values.push(...accounts)
|
||||
}
|
||||
}
|
||||
|
||||
const lastUpdateCondition = getCondition('nc', 'last_update', index, params.lastUpdate, 'timestamptz')
|
||||
|
||||
if (lastUpdateCondition != null) {
|
||||
where.push(lastUpdateCondition.where)
|
||||
values.push(...lastUpdateCondition.values)
|
||||
index= lastUpdateCondition.index
|
||||
}
|
||||
|
||||
return { where: `WHERE ${where.join(' AND ')}`, values, index }
|
||||
}
|
||||
|
||||
buildNotificationWhere (
|
||||
private buildNotificationWhere (
|
||||
params: FindNotificationsParams,
|
||||
personalWorkspace: WorkspaceID,
|
||||
workspace?: WorkspaceID
|
||||
initialIndex?: number,
|
||||
skipWorkspace?: boolean
|
||||
): {
|
||||
where: string
|
||||
values: any[]
|
||||
} {
|
||||
const where: string[] = ['nc.personal_workspace = $1::uuid']
|
||||
const values: any[] = [personalWorkspace]
|
||||
let index = 2
|
||||
|
||||
if (workspace != null) {
|
||||
where.push(`nc.workspace_id = $${index++}::uuid`)
|
||||
values.push(workspace)
|
||||
}
|
||||
const where: string[] = skipWorkspace === true ? [] : ['nc.workspace_id = $1::uuid']
|
||||
const values: any[] = skipWorkspace === true ? [] : [this.workspace]
|
||||
let index = (initialIndex ?? 0) + values.length + 1
|
||||
|
||||
if (params.context != null) {
|
||||
where.push(`n.context = $${index++}::uuid`)
|
||||
where.push(`n.context_id = $${index++}::int8`)
|
||||
values.push(params.context)
|
||||
}
|
||||
|
||||
if (params.account != null) {
|
||||
const accounts = Array.isArray(params.account) ? params.account : [params.account]
|
||||
if (accounts.length === 1) {
|
||||
where.push(`nc.account = $${index++}::uuid`)
|
||||
values.push(accounts[0])
|
||||
} else if (accounts.length > 1) {
|
||||
where.push(`nc.account = ANY ($${index++}::uuid[])`)
|
||||
values.push(accounts)
|
||||
}
|
||||
}
|
||||
|
||||
if (params.read === true) {
|
||||
where.push('nc.last_view IS NOT NULL AND nc.last_view >= m.created')
|
||||
where.push('nc.last_view IS NOT NULL AND nc.last_view >= n.created')
|
||||
}
|
||||
|
||||
if (params.read === false) {
|
||||
where.push('(nc.last_view IS NULL OR nc.last_view > m.created)')
|
||||
where.push('(nc.last_view IS NULL OR nc.last_view < n.created)')
|
||||
}
|
||||
|
||||
if (params.archived === true) {
|
||||
where.push('nc.archived_from IS NOT NULL AND nc.archived_from >= m.created')
|
||||
}
|
||||
|
||||
if (params.archived === false) {
|
||||
where.push('(nc.archived_from IS NULL OR nc.archived_from > m.created)')
|
||||
}
|
||||
|
||||
return { where: `WHERE ${where.join(' AND ')}`, values }
|
||||
}
|
||||
|
||||
toNotificationContext (row: any): NotificationContext {
|
||||
return {
|
||||
id: row.id,
|
||||
card: row.card_id,
|
||||
workspace: row.workspace_id,
|
||||
personalWorkspace: row.personal_workspace,
|
||||
archivedFrom: row.archived_from != null ? new Date(row.archived_from) : undefined,
|
||||
lastView: row.last_view != null ? new Date(row.last_view) : undefined,
|
||||
lastUpdate: row.last_update != null ? new Date(row.last_update) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
toNotification (row: any): Notification {
|
||||
const lastPatch = row.patches?.[0]
|
||||
const lastView = row.last_view != null ? new Date(row.last_view) : undefined
|
||||
const archivedFrom = row.archived_from != null ? new Date(row.archived_from) : undefined
|
||||
const created = new Date(row.message_created)
|
||||
|
||||
return {
|
||||
message: {
|
||||
id: row.id,
|
||||
card: row.message_card,
|
||||
content: lastPatch?.content ?? row.message_content,
|
||||
creator: row.message_creator,
|
||||
created,
|
||||
edited: new Date(lastPatch?.created ?? row.message_created),
|
||||
reactions: row.reactions ?? [],
|
||||
attachments: row.attachments ?? []
|
||||
},
|
||||
context: row.context,
|
||||
read: lastView != null && lastView >= created,
|
||||
archived: archivedFrom != null && archivedFrom >= created
|
||||
}
|
||||
return { where: where.length > 0 ? `WHERE ${where.join(' AND ')}` : '', values }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -15,6 +15,9 @@
|
||||
|
||||
import { type ParameterOrJSON, type Row } from 'postgres'
|
||||
|
||||
export type SqlRow = Row & Iterable<Row>
|
||||
export type SqlParams = ParameterOrJSON<any>[]
|
||||
|
||||
export interface Logger {
|
||||
info: (message: string, data?: Record<string, any>) => void
|
||||
warn: (message: string, data?: Record<string, any>) => void
|
||||
@@ -26,6 +29,7 @@ export interface Options {
|
||||
}
|
||||
|
||||
export interface SqlClient {
|
||||
execute: <T extends any[] = (Row & Iterable<Row>)[]>(query: string, params?: ParameterOrJSON<any>[]) => Promise<T>
|
||||
execute: <T = SqlRow>(query: string, params?: SqlParams) => Promise<T[]>
|
||||
cursor: <T = SqlRow>(query: string, params?: SqlParams, size?: number) => AsyncIterable<NonNullable<T[][number]>[]>
|
||||
close: () => void
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
})
|
||||
@@ -1,51 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.chat {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
max-height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid black;
|
||||
margin-bottom: 20px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.messages{
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="chat" id="chat">
|
||||
<div class="messages" id="messages"></div>
|
||||
</div>
|
||||
<form class="chat-box" id="form">
|
||||
<input name="text"/>
|
||||
<button id='submit-button' type="submit">Send</button>
|
||||
</form>
|
||||
|
||||
<button id='backward-button' type="button">Backward</button>
|
||||
<button id='forward-button' type="button">Forward</button>
|
||||
|
||||
</body>
|
||||
<script type="module" crossorigin src="../dist/index.js"></script>
|
||||
</html>
|
||||
@@ -1,126 +0,0 @@
|
||||
import {
|
||||
type Message,
|
||||
type SocialID,
|
||||
SortingOrder,
|
||||
type CardID,
|
||||
type Window,
|
||||
type WorkspaceID
|
||||
} from '@hcengineering/communication-types'
|
||||
import { getWebsocketClient, createMessagesQuery } from '@hcengineering/communication-client-ws'
|
||||
|
||||
const card = 'cd0aba36-1c4f-4170-95f2-27a12a5415f6' as CardID
|
||||
const workspace = 'cd0aba36-1c4f-4170-95f2-27a12a5415f6' as WorkspaceID
|
||||
// const personalWorkspace = 'cd0aba36-1c4f-4170-95f2-27a12a5415f5' as WorkspaceID
|
||||
const creator1 = 'email:vasya@huly.com' as SocialID
|
||||
|
||||
export async function example() {
|
||||
const platformUrl = 'ws://localhost:8090'
|
||||
const token = 'token'
|
||||
const client = await getWebsocketClient(
|
||||
platformUrl,
|
||||
token,
|
||||
workspace,
|
||||
'http://localhost:4022/blob/:workspace/:blobId/:filename'
|
||||
)
|
||||
|
||||
const query1 = createMessagesQuery()
|
||||
|
||||
let window: Window<Message> | undefined = undefined
|
||||
|
||||
query1.query({ card, order: SortingOrder.Descending }, (res) => {
|
||||
window = res
|
||||
const r = window.getResult()
|
||||
r.reverse()
|
||||
showMessages(r)
|
||||
})
|
||||
|
||||
document.getElementById('forward-button')?.addEventListener('click', async () => {
|
||||
if (window == null) return
|
||||
await window.loadNextPage()
|
||||
})
|
||||
|
||||
document.getElementById('backward-button')?.addEventListener('click', async () => {
|
||||
if (window == null) return
|
||||
await window.loadPrevPage()
|
||||
})
|
||||
|
||||
async function editMessage(message: Message) {
|
||||
await client.updateMessage(card, message.id, message.content + '_1_', creator1)
|
||||
}
|
||||
|
||||
async function deleteMessage(message: Message) {
|
||||
await client.removeMessage(card, message.id)
|
||||
}
|
||||
|
||||
async function addReaction(message: Message) {
|
||||
await client.createReaction(card, message.id, '👍', creator1)
|
||||
}
|
||||
|
||||
async function removeReaction(message: Message) {
|
||||
await client.removeReaction(card, message.id, '👍', creator1)
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
const el = document.getElementById('chat')
|
||||
if (el == null) return
|
||||
el.scrollTo(0, el.scrollHeight)
|
||||
}
|
||||
|
||||
async function showMessages(messages: ReadonlyArray<Message>) {
|
||||
const el = document.getElementById('messages')
|
||||
if (el == null) return
|
||||
el.innerHTML = ''
|
||||
for (const message of messages) {
|
||||
const div = el.appendChild(document.createElement('div'))
|
||||
div.className = 'message'
|
||||
|
||||
const messageContent = document.createElement('span')
|
||||
messageContent.textContent = message.content + ' ' + message.reactions.map((it) => it.reaction).join(' ')
|
||||
// + ' ' + messages.created.getTime()
|
||||
div.appendChild(messageContent)
|
||||
|
||||
const buttonsDiv = document.createElement('div')
|
||||
buttonsDiv.className = 'buttons'
|
||||
|
||||
const editButton = document.createElement('button')
|
||||
editButton.textContent = 'Edit'
|
||||
editButton.className = 'edit-button'
|
||||
editButton.addEventListener('click', () => editMessage(message))
|
||||
buttonsDiv.appendChild(editButton)
|
||||
|
||||
const deleteButton = document.createElement('button')
|
||||
deleteButton.textContent = 'Remove'
|
||||
deleteButton.className = 'delete-button'
|
||||
deleteButton.addEventListener('click', () => deleteMessage(message))
|
||||
buttonsDiv.appendChild(deleteButton)
|
||||
|
||||
const addReactionButton = document.createElement('button')
|
||||
addReactionButton.textContent = '+R'
|
||||
addReactionButton.className = 'add-reaction-button'
|
||||
addReactionButton.addEventListener('click', () => addReaction(message))
|
||||
buttonsDiv.appendChild(addReactionButton)
|
||||
|
||||
const removeReactionButton = document.createElement('button')
|
||||
removeReactionButton.textContent = '-R'
|
||||
removeReactionButton.className = 'remove-reaction-button'
|
||||
removeReactionButton.addEventListener('click', () => removeReaction(message))
|
||||
buttonsDiv.appendChild(removeReactionButton)
|
||||
|
||||
div.appendChild(buttonsDiv)
|
||||
}
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
document.getElementById('form')?.addEventListener('submit', async (event) => {
|
||||
event.preventDefault()
|
||||
// @ts-expect-error error
|
||||
const el = event.target?.getElementsByTagName('input')[0] as HTMLInputElement
|
||||
if (el.value == '' || el.value == null) return
|
||||
|
||||
await client.createMessage(card, el.value, creator1)
|
||||
|
||||
el.value = ''
|
||||
})
|
||||
}
|
||||
|
||||
void example()
|
||||
@@ -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": {
|
||||
|
||||
@@ -13,4 +13,5 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
export * from './lq.ts'
|
||||
export * from './lq'
|
||||
export type { QueryClient } from './types'
|
||||
|
||||
+79
-54
@@ -15,26 +15,33 @@
|
||||
|
||||
import {
|
||||
type FindMessagesParams,
|
||||
type FindNotificationContextParams,
|
||||
type FindNotificationsParams,
|
||||
type WorkspaceID
|
||||
type Message,
|
||||
type WorkspaceID,
|
||||
type Notification,
|
||||
type NotificationContext
|
||||
} from '@hcengineering/communication-types'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import type {
|
||||
MessagesQueryCallback,
|
||||
NotificationsQueryCallback,
|
||||
ResponseEvent,
|
||||
QueryClient
|
||||
QueryCallback,
|
||||
RequestEvent,
|
||||
EventResult
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
|
||||
import type { FindParams, PagedQuery, QueryId } from './types'
|
||||
import type { PagedQuery, FindParams, QueryId, QueryClient } from './types'
|
||||
import { MessagesQuery } from './messages/query'
|
||||
import { NotificationQuery } from './notifications/query'
|
||||
import { NotificationContextsQuery } from './notification-contexts/query'
|
||||
|
||||
interface CreateQueryResult {
|
||||
unsubscribe: () => void
|
||||
}
|
||||
|
||||
const maxQueriesCache = 10
|
||||
const maxQueriesCache = 20
|
||||
|
||||
export class LiveQueries {
|
||||
private readonly queries = new Map<QueryId, PagedQuery>()
|
||||
@@ -45,27 +52,61 @@ export class LiveQueries {
|
||||
private readonly client: QueryClient,
|
||||
private readonly workspace: WorkspaceID,
|
||||
private readonly filesUrl: string
|
||||
) {}
|
||||
) {
|
||||
this.client.onEvent = (event) => {
|
||||
void this.onEvent(event)
|
||||
}
|
||||
this.client.onRequest = (event, promise) => {
|
||||
void this.onRequest(event, promise)
|
||||
}
|
||||
}
|
||||
|
||||
async onEvent(event: ResponseEvent): Promise<void> {
|
||||
for (const q of this.queries.values()) {
|
||||
await q.onEvent(event)
|
||||
void q.onEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
async onRequest(event: RequestEvent, promise: Promise<EventResult>): Promise<void> {
|
||||
for (const q of this.queries.values()) {
|
||||
void q.onRequest(event, promise)
|
||||
}
|
||||
}
|
||||
|
||||
queryMessages(params: FindMessagesParams, callback: MessagesQueryCallback): CreateQueryResult {
|
||||
const query = this.createMessagesQuery(params, callback)
|
||||
this.queries.set(query.id, query)
|
||||
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
this.unsubscribeQuery(query)
|
||||
}
|
||||
}
|
||||
return this.createAndStoreQuery<Message, FindMessagesParams, MessagesQuery>(
|
||||
params,
|
||||
callback,
|
||||
MessagesQuery,
|
||||
(params) => this.findMessagesQuery(params)
|
||||
)
|
||||
}
|
||||
|
||||
queryNotifications(params: FindNotificationsParams, callback: NotificationsQueryCallback): CreateQueryResult {
|
||||
const query = this.createNotificationQuery(params, callback)
|
||||
return this.createAndStoreQuery<Notification, FindNotificationsParams, NotificationQuery>(
|
||||
params,
|
||||
callback,
|
||||
NotificationQuery,
|
||||
(params) => this.findNotificationQuery(params)
|
||||
)
|
||||
}
|
||||
|
||||
queryNotificationContexts(params: FindNotificationContextParams, callback: any): CreateQueryResult {
|
||||
return this.createAndStoreQuery<NotificationContext, FindNotificationContextParams, NotificationContextsQuery>(
|
||||
params,
|
||||
callback,
|
||||
NotificationContextsQuery,
|
||||
(params) => this.findNotificationContextsQuery(params)
|
||||
)
|
||||
}
|
||||
|
||||
private createAndStoreQuery<T, P extends FindParams, Q extends PagedQuery<T, P>>(
|
||||
params: P,
|
||||
callback: QueryCallback<T>,
|
||||
QueryClass: new (...args: any[]) => Q,
|
||||
finder: (params: P) => Q | undefined
|
||||
): CreateQueryResult {
|
||||
const query = this.createQuery<T, P, Q>(params, callback, QueryClass, finder)
|
||||
this.queries.set(query.id, query)
|
||||
|
||||
return {
|
||||
@@ -75,68 +116,51 @@ export class LiveQueries {
|
||||
}
|
||||
}
|
||||
|
||||
private createMessagesQuery(params: FindMessagesParams, callback: MessagesQueryCallback): MessagesQuery {
|
||||
private createQuery<T, P, Q extends PagedQuery<T, P>>(
|
||||
params: P,
|
||||
callback: any,
|
||||
QueryClass: new (...args: any[]) => Q,
|
||||
finder: (params: P) => Q | undefined
|
||||
): Q {
|
||||
const id = ++this.counter
|
||||
const exists = this.findMessagesQuery(params)
|
||||
const exists = finder(params)
|
||||
|
||||
if (exists !== undefined) {
|
||||
if (this.unsubscribed.has(id)) {
|
||||
this.unsubscribed.delete(id)
|
||||
if (this.unsubscribed.has(exists.id)) {
|
||||
this.unsubscribed.delete(exists.id)
|
||||
exists.setCallback(callback)
|
||||
return exists
|
||||
} else {
|
||||
const result = exists.copyResult()
|
||||
return new MessagesQuery(this.client, this.workspace, this.filesUrl, id, params, callback, result)
|
||||
return new QueryClass(this.client, this.workspace, this.filesUrl, id, params, callback, result)
|
||||
}
|
||||
}
|
||||
|
||||
return new MessagesQuery(this.client, this.workspace, this.filesUrl, id, params, callback)
|
||||
return new QueryClass(this.client, this.workspace, this.filesUrl, id, params, callback)
|
||||
}
|
||||
|
||||
private createNotificationQuery(
|
||||
params: FindNotificationsParams,
|
||||
callback: NotificationsQueryCallback
|
||||
): NotificationQuery {
|
||||
const id = ++this.counter
|
||||
const exists = this.findNotificationQuery(params)
|
||||
|
||||
if (exists !== undefined) {
|
||||
if (this.unsubscribed.has(id)) {
|
||||
this.unsubscribed.delete(id)
|
||||
exists.setCallback(callback)
|
||||
return exists
|
||||
} else {
|
||||
const result = exists.copyResult()
|
||||
return new NotificationQuery(this.client, id, params, callback, result)
|
||||
private findQuery<T extends PagedQuery>(params: FindParams, QueryClass: new (...args: any[]) => T): T | undefined {
|
||||
for (const query of this.queries.values()) {
|
||||
if (query instanceof QueryClass && this.queryCompare(params, query.params)) {
|
||||
return query as T
|
||||
}
|
||||
}
|
||||
|
||||
return new NotificationQuery(this.client, id, params, callback)
|
||||
}
|
||||
|
||||
private findMessagesQuery(params: FindMessagesParams): MessagesQuery | undefined {
|
||||
for (const query of this.queries.values()) {
|
||||
if (query instanceof MessagesQuery) {
|
||||
if (!this.queryCompare(params, query.params)) continue
|
||||
return query
|
||||
}
|
||||
}
|
||||
return this.findQuery(params, MessagesQuery)
|
||||
}
|
||||
|
||||
private findNotificationQuery(params: FindNotificationsParams): NotificationQuery | undefined {
|
||||
for (const query of this.queries.values()) {
|
||||
if (query instanceof NotificationQuery) {
|
||||
if (!this.queryCompare(params, query.params)) continue
|
||||
return query
|
||||
}
|
||||
}
|
||||
return this.findQuery(params, NotificationQuery)
|
||||
}
|
||||
|
||||
private findNotificationContextsQuery(params: FindNotificationContextParams): NotificationContextsQuery | undefined {
|
||||
return this.findQuery(params, NotificationContextsQuery)
|
||||
}
|
||||
|
||||
private queryCompare(q1: FindParams, q2: FindParams): boolean {
|
||||
if (Object.keys(q1).length !== Object.keys(q2).length) {
|
||||
return false
|
||||
}
|
||||
return deepEqual(q1, q2)
|
||||
return Object.keys(q1).length === Object.keys(q2).length && deepEqual(q1, q2)
|
||||
}
|
||||
|
||||
private removeOldQueries(): void {
|
||||
@@ -166,5 +190,6 @@ export class LiveQueries {
|
||||
|
||||
close(): void {
|
||||
this.queries.clear()
|
||||
this.unsubscribed.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,40 +16,48 @@
|
||||
import {
|
||||
type FindMessagesParams,
|
||||
type Message,
|
||||
type MessagesGroup,
|
||||
type WorkspaceID,
|
||||
type MessageID,
|
||||
SortingOrder,
|
||||
type Patch,
|
||||
type MessagesGroup,
|
||||
type ParsedFile,
|
||||
type Patch,
|
||||
PatchType,
|
||||
type CardID
|
||||
type Reaction,
|
||||
type SocialID,
|
||||
SortingOrder,
|
||||
type WorkspaceID,
|
||||
type File,
|
||||
type BlobID,
|
||||
MessageType
|
||||
} from '@hcengineering/communication-types'
|
||||
import {
|
||||
ResponseEventType,
|
||||
type AttachmentCreatedEvent,
|
||||
type AttachmentRemovedEvent,
|
||||
type FileCreatedEvent,
|
||||
type FileRemovedEvent,
|
||||
type MessageCreatedEvent,
|
||||
type MessageRemovedEvent,
|
||||
type MessagesRemovedEvent,
|
||||
type PatchCreatedEvent,
|
||||
type QueryCallback,
|
||||
type QueryClient,
|
||||
type ReactionCreatedEvent,
|
||||
type ReactionRemovedEvent,
|
||||
type CreateMessageEvent,
|
||||
type RequestEvent,
|
||||
RequestEventType,
|
||||
type ResponseEvent,
|
||||
type ThreadCreatedEvent
|
||||
ResponseEventType,
|
||||
type ThreadCreatedEvent,
|
||||
type EventResult,
|
||||
type CreateMessageResult
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
import { loadGroupFile, parseMessageId } from '@hcengineering/communication-shared'
|
||||
import { applyPatch, applyPatches, generateMessageId, parseMessageId } from '@hcengineering/communication-shared'
|
||||
import { loadGroupFile } from '@hcengineering/communication-yaml'
|
||||
|
||||
import { QueryResult } from '../result'
|
||||
import { defaultQueryParams, Direction, type QueryId, type PagedQuery } from '../types'
|
||||
import { defaultQueryParams, Direction, type PagedQuery, type QueryId, type QueryClient } from '../types'
|
||||
import { WindowImpl } from '../window'
|
||||
import { addReaction, addReply, removeReaction, removeReply } from './utils'
|
||||
|
||||
const GROUPS_LIMIT = 20
|
||||
|
||||
export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
protected result: Promise<QueryResult<Message>> | QueryResult<Message>
|
||||
private result: Promise<QueryResult<Message>> | QueryResult<Message>
|
||||
|
||||
private messagesFromFiles: Message[] = []
|
||||
|
||||
@@ -70,8 +78,10 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
hasGroups: true
|
||||
}
|
||||
|
||||
private tmpMessages: Map<string, MessageID> = new Map()
|
||||
|
||||
constructor (
|
||||
protected readonly client: QueryClient,
|
||||
private readonly client: QueryClient,
|
||||
private readonly workspace: WorkspaceID,
|
||||
private readonly filesUrl: string,
|
||||
public readonly id: QueryId,
|
||||
@@ -107,22 +117,132 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
this.result = new QueryResult([] as Message[], (x) => x.id)
|
||||
|
||||
if (this.isInitLoadingForward()) {
|
||||
this.result.setHead(true)
|
||||
this.result.setHead(this.params.created == null)
|
||||
void this.requestLoadNextPage()
|
||||
} else {
|
||||
this.result.setTail(true)
|
||||
this.result.setTail(this.params.created == null)
|
||||
void this.requestLoadPrevPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCallback (callback: QueryCallback<Message>): void {
|
||||
this.callback = callback
|
||||
void this.notify()
|
||||
async onEvent (event: ResponseEvent): Promise<void> {
|
||||
switch (event.type) {
|
||||
case ResponseEventType.MessageCreated: {
|
||||
await this.onMessageCreatedEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.MessagesRemoved: {
|
||||
await this.onMessagesRemovedEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.PatchCreated: {
|
||||
await this.onPatchCreatedEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.ReactionCreated: {
|
||||
await this.onReactionCreatedEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.ReactionRemoved: {
|
||||
await this.onReactionRemovedEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.FileCreated: {
|
||||
await this.onFileCreatedEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.FileRemoved: {
|
||||
await this.onFileRemovedEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.ThreadCreated: {
|
||||
await this.onThreadCreatedEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeCallback (): void {
|
||||
this.callback = () => {}
|
||||
async onRequest (event: RequestEvent, promise: Promise<EventResult>): Promise<void> {
|
||||
switch (event.type) {
|
||||
case RequestEventType.CreateMessage: {
|
||||
await this.onCreateMessageRequest(event, promise as Promise<CreateMessageResult>)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onCreateMessageRequest(
|
||||
event: CreateMessageEvent,
|
||||
promise: Promise<CreateMessageResult>
|
||||
): Promise<void> {
|
||||
if (this.params.card !== event.card) return;
|
||||
const eventId = event._id
|
||||
if(eventId == null) return
|
||||
|
||||
const tmpId = generateMessageId();
|
||||
let resultId: MessageID | undefined;
|
||||
const tmpMessage: Message = {
|
||||
id: tmpId,
|
||||
type: MessageType.Message,
|
||||
card: event.card,
|
||||
content: event.content,
|
||||
creator: event.creator,
|
||||
created: new Date(),
|
||||
data: event.data,
|
||||
edited: undefined,
|
||||
thread: undefined,
|
||||
reactions: [],
|
||||
files: []
|
||||
};
|
||||
|
||||
if (!this.match(tmpMessage)) return;
|
||||
|
||||
promise
|
||||
.then(async (result) => {
|
||||
this.tmpMessages.delete(eventId)
|
||||
resultId = result.id;
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
if (this.result.get(resultId)) {
|
||||
if (this.result.delete(tmpId)) {
|
||||
await this.notify();
|
||||
}
|
||||
} else {
|
||||
const updatedMessage = { ...tmpMessage, id: resultId };
|
||||
this.result.delete(tmpId);
|
||||
|
||||
this.insertMessage(this.result, updatedMessage);
|
||||
|
||||
void this.notify();
|
||||
}
|
||||
})
|
||||
.catch(async () => {
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
this.tmpMessages.delete(eventId)
|
||||
if (this.result.delete(tmpId)) {
|
||||
void this.notify();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.result instanceof Promise) this.result = await this.result;
|
||||
|
||||
if (resultId === undefined && this.result.isTail()) {
|
||||
this.tmpMessages.set(eventId, tmpId)
|
||||
this.insertMessage(this.result, tmpMessage);
|
||||
void this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
private insertMessage(result: QueryResult<Message>, message: Message): void {
|
||||
if (this.params.order === SortingOrder.Ascending) {
|
||||
result.push(message);
|
||||
} else {
|
||||
result.unshift(message);
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribe (): Promise<void> {
|
||||
await this.client.unsubscribeQuery(this.id)
|
||||
}
|
||||
|
||||
async requestLoadNextPage (): Promise<void> {
|
||||
@@ -152,22 +272,31 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
}
|
||||
}
|
||||
|
||||
private isInitLoadingForward (): boolean {
|
||||
const { order, created, id } = this.params
|
||||
removeCallback (): void {
|
||||
this.callback = () => {}
|
||||
}
|
||||
|
||||
if (id != null) {
|
||||
return false
|
||||
setCallback (callback: QueryCallback<Message>): void {
|
||||
this.callback = callback
|
||||
void this.notify()
|
||||
}
|
||||
|
||||
copyResult (): QueryResult<Message> | undefined {
|
||||
if (this.result instanceof Promise) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (created == null) return order === SortingOrder.Ascending
|
||||
if (created instanceof Date) return order === SortingOrder.Ascending
|
||||
// TODO: fix me
|
||||
if (created.less != null) return order !== SortingOrder.Ascending
|
||||
if (created.lessOrEqual != null) return order !== SortingOrder.Ascending
|
||||
if (created.greater != null) return order === SortingOrder.Ascending
|
||||
if (created.greaterOrEqual != null) return order === SortingOrder.Ascending
|
||||
return this.result.copy()
|
||||
}
|
||||
|
||||
return false
|
||||
private isInitLoadingForward (): boolean {
|
||||
const { order, id } = this.params
|
||||
|
||||
if (id != null) {
|
||||
return true
|
||||
}
|
||||
|
||||
return order === SortingOrder.Ascending
|
||||
}
|
||||
|
||||
private async loadPage (direction: Direction, result: QueryResult<Message>): Promise<QueryResult<Message>> {
|
||||
@@ -190,7 +319,11 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
}
|
||||
}
|
||||
|
||||
result.append(messages)
|
||||
if (this.params.order === SortingOrder.Ascending && direction === Direction.Backward) {
|
||||
result.prepend(messages.reverse())
|
||||
} else {
|
||||
result.append(messages)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -229,9 +362,9 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
created:
|
||||
last != null
|
||||
? {
|
||||
greater: last?.created
|
||||
}
|
||||
: undefined,
|
||||
greater: last.created
|
||||
}
|
||||
: this.params.created,
|
||||
limit,
|
||||
order: SortingOrder.Ascending
|
||||
})
|
||||
@@ -271,16 +404,16 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
private async findPrevMessages (limit: number, result: QueryResult<Message>): Promise<Message[]> {
|
||||
if (!this.prev.hasMessages || result.isHead()) return []
|
||||
|
||||
const first = result.getLast()
|
||||
const first = this.params.order === SortingOrder.Ascending ? result.getFirst() : result.getLast()
|
||||
|
||||
return await this.find({
|
||||
...this.params,
|
||||
created:
|
||||
first != null
|
||||
? {
|
||||
less: first?.created
|
||||
}
|
||||
: undefined,
|
||||
less: first?.created
|
||||
}
|
||||
: this.params.created,
|
||||
limit,
|
||||
order: SortingOrder.Descending
|
||||
})
|
||||
@@ -302,7 +435,7 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
? currentGroups
|
||||
: await this.findGroups(
|
||||
direction,
|
||||
direction === Direction.Forward ? this.lastGroup?.fromDate : this.firstGroup?.fromDate
|
||||
direction === Direction.Forward ? this.lastGroup?.fromSec : this.firstGroup?.fromSec
|
||||
)
|
||||
|
||||
if (currentGroups.length === 0) {
|
||||
@@ -380,7 +513,6 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
|
||||
private async loadMessagesFromFiles (group: MessagesGroup): Promise<ParsedFile> {
|
||||
const parsedFile = await loadGroupFile(this.workspace, this.filesUrl, group, { retries: 5 })
|
||||
|
||||
const patches = group.patches ?? []
|
||||
|
||||
const patchesMap = new Map<MessageID, Patch[]>()
|
||||
@@ -392,36 +524,24 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
metadata: parsedFile.metadata,
|
||||
messages:
|
||||
patches.length > 0
|
||||
? parsedFile.messages.map((message) => this.applyPatches(message, patchesMap.get(message.id) ?? []))
|
||||
? parsedFile.messages.map((message) => applyPatches(message, patchesMap.get(message.id) ?? [], this.allowedPatches()))
|
||||
: parsedFile.messages
|
||||
}
|
||||
}
|
||||
|
||||
private async findGroupByMessage (id: MessageID): Promise<MessagesGroup | undefined> {
|
||||
const date = parseMessageId(id)
|
||||
const group1 = (await this.client.findMessagesGroups({
|
||||
card: this.params.card,
|
||||
limit: 1,
|
||||
toDate: {
|
||||
greaterOrEqual: date
|
||||
},
|
||||
order: SortingOrder.Descending,
|
||||
orderBy: 'fromDate'
|
||||
}))[0]
|
||||
private async findGroupByMessage(id: MessageID): Promise<MessagesGroup | undefined> {
|
||||
const date = parseMessageId(id);
|
||||
|
||||
if (group1 !== undefined) {
|
||||
return group1
|
||||
}
|
||||
|
||||
return (await this.client.findMessagesGroups({
|
||||
const groups = await this.client.findMessagesGroups({
|
||||
card: this.params.card,
|
||||
fromSec: { lessOrEqual: date },
|
||||
toSec: { greaterOrEqual: date },
|
||||
limit: 1,
|
||||
fromDate: {
|
||||
lessOrEqual: date
|
||||
},
|
||||
order: SortingOrder.Ascending,
|
||||
orderBy: 'fromDate'
|
||||
}))[0]
|
||||
orderBy: 'fromSec'
|
||||
});
|
||||
|
||||
return groups[0];
|
||||
}
|
||||
|
||||
private async findGroups (direction: Direction, fromDate?: Date): Promise<MessagesGroup[]> {
|
||||
@@ -435,7 +555,7 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
card: this.params.card,
|
||||
limit: GROUPS_LIMIT,
|
||||
order: direction === Direction.Forward ? SortingOrder.Ascending : SortingOrder.Descending,
|
||||
orderBy: 'fromDate'
|
||||
orderBy: 'fromSec'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -443,15 +563,15 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
card: this.params.card,
|
||||
limit: GROUPS_LIMIT,
|
||||
order: direction === Direction.Forward ? SortingOrder.Ascending : SortingOrder.Descending,
|
||||
orderBy: 'fromDate',
|
||||
fromDate:
|
||||
orderBy: 'fromSec',
|
||||
fromSec:
|
||||
direction === Direction.Forward
|
||||
? {
|
||||
greater: fromDate
|
||||
}
|
||||
greater: fromDate
|
||||
}
|
||||
: {
|
||||
less: fromDate
|
||||
}
|
||||
less: fromDate
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -466,47 +586,18 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
this.callback(new WindowImpl(result, this.result.isTail(), this.result.isHead(), this))
|
||||
}
|
||||
|
||||
async unsubscribe (): Promise<void> {
|
||||
await this.client.unsubscribeQuery(this.id)
|
||||
}
|
||||
|
||||
async onEvent (event: ResponseEvent): Promise<void> {
|
||||
switch (event.type) {
|
||||
case ResponseEventType.MessageCreated: {
|
||||
await this.onCreateMessageEvent(event)
|
||||
return
|
||||
}
|
||||
case ResponseEventType.MessageRemoved: {
|
||||
await this.onRemoveMessageEvent(event)
|
||||
return
|
||||
}
|
||||
case ResponseEventType.PatchCreated: {
|
||||
await this.onCreatePatchEvent(event)
|
||||
return
|
||||
}
|
||||
case ResponseEventType.ReactionCreated: {
|
||||
await this.onCreateReactionEvent(event)
|
||||
return
|
||||
}
|
||||
case ResponseEventType.ReactionRemoved: {
|
||||
await this.onRemoveReactionEvent(event)
|
||||
return
|
||||
}
|
||||
case ResponseEventType.AttachmentCreated: {
|
||||
await this.onCreateAttachmentEvent(event)
|
||||
return
|
||||
}
|
||||
case ResponseEventType.AttachmentRemoved: {
|
||||
await this.onRemoveAttachmentEvent(event)
|
||||
return
|
||||
}
|
||||
case ResponseEventType.ThreadCreated: {
|
||||
await this.onCreateThreadEvent(event)
|
||||
}
|
||||
private match (message: Message): boolean {
|
||||
if (this.params.id != null && this.params.id !== message.id) {
|
||||
return false
|
||||
}
|
||||
if (this.params.card !== message.card) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async onCreateThreadEvent (event: ThreadCreatedEvent): Promise<void> {
|
||||
private async onThreadCreatedEvent (event: ThreadCreatedEvent): Promise<void> {
|
||||
if(this.params.replies !== true) return
|
||||
if (this.params.card !== event.thread.card) return
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
@@ -532,7 +623,7 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
})
|
||||
}
|
||||
|
||||
async onCreateMessageEvent (event: MessageCreatedEvent): Promise<void> {
|
||||
private async onMessageCreatedEvent (event: MessageCreatedEvent): Promise<void> {
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
if (this.params.card !== event.message.card) return
|
||||
const { message } = event
|
||||
@@ -542,6 +633,12 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
if (!this.match(message)) return
|
||||
|
||||
if (this.result.isTail()) {
|
||||
const eventId = event._id
|
||||
if(eventId != null) {
|
||||
const tmp = this.tmpMessages.get(eventId)
|
||||
if (tmp) this.result.delete(tmp)
|
||||
this.tmpMessages.delete(eventId)
|
||||
}
|
||||
if (this.params.order === SortingOrder.Ascending) {
|
||||
this.result.push(message)
|
||||
} else {
|
||||
@@ -551,51 +648,50 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
}
|
||||
}
|
||||
|
||||
private match (message: Message): boolean {
|
||||
if (this.params.id != null && this.params.id !== message.id) {
|
||||
return false
|
||||
}
|
||||
if (this.params.card !== message.card) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private async onCreatePatchEvent (event: PatchCreatedEvent): Promise<void> {
|
||||
private async onPatchCreatedEvent (event: PatchCreatedEvent): Promise<void> {
|
||||
if (this.params.card !== event.card) return
|
||||
if (!this.isAllowedPatch(event.patch.type)) return
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const { patch } = event
|
||||
const messageId = BigInt(patch.message)
|
||||
const group = this.groupsBuffer.find((it) => BigInt(it.fromId) <= messageId && BigInt(it.toId) >= messageId)
|
||||
const created = parseMessageId(patch.message)
|
||||
const groups = this.groupsBuffer.filter((it) => it.fromSec <= created && it.toSec >= created)
|
||||
|
||||
if (group != null && group.patches != null) {
|
||||
group.patches.push(patch)
|
||||
for (const group of groups) {
|
||||
if (group.patches != null) {
|
||||
group.patches.push(patch)
|
||||
}
|
||||
}
|
||||
|
||||
const message = this.result.get(patch.message)
|
||||
if (message === undefined) return
|
||||
|
||||
if (message.created < patch.created) {
|
||||
this.result.update(this.applyPatch(message, patch))
|
||||
this.result.update(applyPatch(message, patch, this.allowedPatches()))
|
||||
await this.notify()
|
||||
}
|
||||
}
|
||||
|
||||
private async onRemoveMessageEvent (event: MessageRemovedEvent): Promise<void> {
|
||||
private async onMessagesRemovedEvent (event: MessagesRemovedEvent): Promise<void> {
|
||||
if (this.params.card !== event.card) return
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const deleted = this.result.delete(event.message)
|
||||
let isDeleted = false
|
||||
|
||||
if (deleted !== undefined) {
|
||||
for (const message of event.messages) {
|
||||
const deleted = this.result.delete(message)
|
||||
isDeleted = isDeleted || deleted !== undefined
|
||||
}
|
||||
|
||||
if (isDeleted) {
|
||||
void this.notify()
|
||||
}
|
||||
|
||||
this.messagesFromFiles = this.messagesFromFiles.filter((it) => it.id !== event.message)
|
||||
this.messagesFromFiles = this.messagesFromFiles.filter((it) => !event.messages.includes(it.id))
|
||||
}
|
||||
|
||||
private async onCreateReactionEvent (event: ReactionCreatedEvent): Promise<void> {
|
||||
private async onReactionCreatedEvent (event: ReactionCreatedEvent): Promise<void> {
|
||||
if(this.params.reactions !== true) return
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
if (this.params.card !== event.card) return
|
||||
|
||||
@@ -616,7 +712,8 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
}
|
||||
}
|
||||
|
||||
private async onRemoveReactionEvent (event: ReactionRemovedEvent): Promise<void> {
|
||||
private async onReactionRemovedEvent (event: ReactionRemovedEvent): Promise<void> {
|
||||
if(this.params.reactions !== true) return
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
if (this.params.card !== event.card) return
|
||||
|
||||
@@ -633,78 +730,96 @@ export class MessagesQuery implements PagedQuery<Message, FindMessagesParams> {
|
||||
)
|
||||
}
|
||||
|
||||
private async onCreateAttachmentEvent (event: AttachmentCreatedEvent): Promise<void> {
|
||||
private async onFileCreatedEvent (event: FileCreatedEvent): Promise<void> {
|
||||
if(this.params.files !== true) return
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const attachment = {
|
||||
...event.attachment,
|
||||
created: event.attachment.created
|
||||
const { file } = event
|
||||
const message = this.result.get(file.message)
|
||||
if (message !== undefined) {
|
||||
message.files.push(file)
|
||||
this.result.update(message)
|
||||
await this.notify()
|
||||
}
|
||||
const message = this.result.get(attachment.message)
|
||||
if (message === undefined) return
|
||||
|
||||
message.attachments.push(attachment)
|
||||
this.result.update(message)
|
||||
await this.notify()
|
||||
const fromBuffer = this.messagesFromFiles.find((it) => it.id === file.message)
|
||||
if (fromBuffer !== undefined) {
|
||||
addFile(fromBuffer, file)
|
||||
}
|
||||
}
|
||||
|
||||
private async onRemoveAttachmentEvent (event: AttachmentRemovedEvent): Promise<void> {
|
||||
private async onFileRemovedEvent (event: FileRemovedEvent): Promise<void> {
|
||||
if(this.params.files !== true) return
|
||||
if(this.params.card !== event.card) return
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const message = this.result.get(event.message)
|
||||
if (message === undefined) return
|
||||
if (message !== undefined) {
|
||||
|
||||
const attachments = message.attachments.filter((it) => it.card !== event.card)
|
||||
if (attachments.length === message.attachments.length) return
|
||||
const files = message.files.filter((it) => it.blobId !== event.blobId)
|
||||
if (files.length === message.files.length) return
|
||||
|
||||
const updated = {
|
||||
...message,
|
||||
attachments
|
||||
const updated = {
|
||||
...message,
|
||||
files
|
||||
}
|
||||
this.result.update(updated)
|
||||
await this.notify()
|
||||
}
|
||||
this.result.update(updated)
|
||||
await this.notify()
|
||||
|
||||
this.messagesFromFiles = this.messagesFromFiles.map((it) =>
|
||||
it.id === event.message ? removeFile(it, event.blobId) : it
|
||||
)
|
||||
}
|
||||
|
||||
private applyPatch (message: Message, patch: Patch): Message {
|
||||
switch (patch.type) {
|
||||
case PatchType.update:
|
||||
return {
|
||||
...message,
|
||||
edited: patch.created,
|
||||
content: patch.content
|
||||
}
|
||||
case PatchType.addReaction:
|
||||
return addReaction(message, {
|
||||
message: message.id,
|
||||
reaction: patch.content,
|
||||
creator: patch.creator,
|
||||
created: patch.created
|
||||
})
|
||||
case PatchType.removeReaction:
|
||||
return removeReaction(message, patch.content, patch.creator)
|
||||
case PatchType.addReply:
|
||||
return addReply(message, patch.content as CardID, patch.created)
|
||||
case PatchType.removeReply:
|
||||
return removeReply(message, patch.content as CardID)
|
||||
}
|
||||
private allowedPatches (): PatchType[] {
|
||||
const result = [PatchType.update]
|
||||
|
||||
return message
|
||||
if(this.params.reactions === true) {
|
||||
result.push(PatchType.addReaction, PatchType.removeReaction)
|
||||
}
|
||||
if(this.params.files === true) {
|
||||
result.push(PatchType.addFile, PatchType.removeFile)
|
||||
}
|
||||
if(this.params.replies === true) {
|
||||
result.push(PatchType.addReply, PatchType.removeReply)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private applyPatches (message: Message, patches: Patch[]): Message {
|
||||
if (patches.length === 0) return message
|
||||
|
||||
for (const p of patches) {
|
||||
message = this.applyPatch(message, p)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
copyResult (): QueryResult<Message> | undefined {
|
||||
if (this.result instanceof Promise) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return this.result.copy()
|
||||
private isAllowedPatch (type: PatchType): boolean {
|
||||
return this.allowedPatches().includes(type)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function addFile (message: Message, file:File): Message {
|
||||
message.files.push(file)
|
||||
return message
|
||||
}
|
||||
|
||||
function removeFile (message: Message, blobId: BlobID): Message {
|
||||
const files = message.files.filter((it) => it.blobId !== blobId)
|
||||
if (files.length === message.files.length) return message
|
||||
|
||||
return {
|
||||
...message,
|
||||
files
|
||||
}
|
||||
}
|
||||
|
||||
function addReaction (message: Message, reaction: Reaction): Message {
|
||||
message.reactions.push(reaction)
|
||||
return message
|
||||
}
|
||||
|
||||
|
||||
function removeReaction (message: Message, emoji: string, creator: SocialID): Message {
|
||||
const reactions = message.reactions.filter((it) => it.reaction !== emoji || it.creator !== creator)
|
||||
if (reactions.length === message.reactions.length) return message
|
||||
|
||||
return {
|
||||
...message,
|
||||
reactions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { CardID, Message, Reaction, SocialID } from '@hcengineering/communication-types'
|
||||
|
||||
export function addReaction (message: Message, reaction: Reaction): Message {
|
||||
message.reactions.push(reaction)
|
||||
return message
|
||||
}
|
||||
|
||||
export function removeReaction (message: Message, emoji: string, creator: SocialID): Message {
|
||||
const reactions = message.reactions.filter((it) => it.reaction !== emoji || it.creator !== creator)
|
||||
if (reactions.length === message.reactions.length) return message
|
||||
|
||||
return {
|
||||
...message,
|
||||
reactions
|
||||
}
|
||||
}
|
||||
|
||||
export function addReply (message: Message, thread: CardID, created: Date): Message {
|
||||
if (message.thread === undefined) {
|
||||
return {
|
||||
...message,
|
||||
thread: {
|
||||
card: message.card,
|
||||
message: message.id,
|
||||
thread,
|
||||
repliesCount: 1,
|
||||
lastReply: created
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.thread.thread !== thread) return message
|
||||
|
||||
return {
|
||||
...message,
|
||||
thread: {
|
||||
...message.thread,
|
||||
repliesCount: message.thread.repliesCount + 1,
|
||||
lastReply: created
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function removeReply (message: Message, thread: CardID): Message {
|
||||
if (message.thread === undefined || message.thread.thread !== thread) return message
|
||||
|
||||
return {
|
||||
...message,
|
||||
thread: {
|
||||
...message.thread,
|
||||
repliesCount: message.thread.repliesCount - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
type FindNotificationContextParams,
|
||||
type Notification,
|
||||
type NotificationContext,
|
||||
PatchType,
|
||||
SortingOrder,
|
||||
type WorkspaceID
|
||||
} from '@hcengineering/communication-types'
|
||||
import {
|
||||
type MessagesRemovedEvent,
|
||||
type NotificationContextCreatedEvent,
|
||||
type NotificationContextRemovedEvent,
|
||||
type NotificationContextUpdatedEvent,
|
||||
type NotificationCreatedEvent,
|
||||
type NotificationsRemovedEvent,
|
||||
type PatchCreatedEvent,
|
||||
type QueryCallback,
|
||||
type RequestEvent,
|
||||
type ResponseEvent,
|
||||
ResponseEventType
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
import { applyPatch } from '@hcengineering/communication-shared'
|
||||
|
||||
import { defaultQueryParams, type PagedQuery, type QueryId, type QueryClient } from '../types'
|
||||
import { QueryResult } from '../result'
|
||||
import { WindowImpl } from '../window'
|
||||
import { loadMessageFromGroup } from '../utils'
|
||||
|
||||
const allowedPatchTypes = [PatchType.update, PatchType.addReaction, PatchType.removeReaction]
|
||||
|
||||
export class NotificationContextsQuery implements PagedQuery<NotificationContext, FindNotificationContextParams> {
|
||||
private result: QueryResult<NotificationContext> | Promise<QueryResult<NotificationContext>>
|
||||
private forward: Promise<NotificationContext[]> | NotificationContext[] = []
|
||||
private backward: Promise<NotificationContext[]> | NotificationContext[] = []
|
||||
|
||||
constructor (
|
||||
private readonly client: QueryClient,
|
||||
private readonly workspace: WorkspaceID,
|
||||
private readonly filesUrl: string,
|
||||
public readonly id: QueryId,
|
||||
public readonly params: FindNotificationContextParams,
|
||||
private callback?: QueryCallback<NotificationContext>,
|
||||
initialResult?: QueryResult<NotificationContext>
|
||||
) {
|
||||
this.params = {
|
||||
...params,
|
||||
limit: params.limit,
|
||||
order: params.order ?? defaultQueryParams.order
|
||||
}
|
||||
const limit = params.limit != null ? params.limit + 1 : undefined
|
||||
const findParams = {
|
||||
...this.params,
|
||||
sort: this.params.order ?? defaultQueryParams.order,
|
||||
limit
|
||||
}
|
||||
|
||||
if (initialResult !== undefined) {
|
||||
this.result = initialResult
|
||||
void this.notify()
|
||||
} else {
|
||||
const findPromise = this.find(findParams)
|
||||
this.result = findPromise.then((res) => {
|
||||
const allLoaded = limit == null || res.length <= limit
|
||||
const isTail = allLoaded || params.lastUpdate == null
|
||||
const isHead = allLoaded
|
||||
if (limit != null && res.length > limit) {
|
||||
res.pop()
|
||||
}
|
||||
const qResult = new QueryResult(res, (x) => x.id)
|
||||
qResult.setTail(isTail)
|
||||
qResult.setHead(isHead)
|
||||
|
||||
return qResult
|
||||
})
|
||||
this.result
|
||||
.then(async () => {
|
||||
await this.notify()
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.error('Failed to update Live query: ', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async onEvent (event: ResponseEvent): Promise<void> {
|
||||
switch (event.type) {
|
||||
case ResponseEventType.PatchCreated: {
|
||||
await this.onCreatePatchEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.MessagesRemoved: {
|
||||
await this.onMessagesRemovedEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.NotificationCreated: {
|
||||
await this.onCreateNotificationEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.NotificationsRemoved: {
|
||||
await this.onRemoveNotificationEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.NotificationContextCreated: {
|
||||
await this.onCreateNotificationContextEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.NotificationContextUpdated: {
|
||||
await this.onUpdateNotificationContextEvent(event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.NotificationContextRemoved: {
|
||||
await this.onRemoveNotificationContextEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onRequest (event: RequestEvent): Promise<void> {}
|
||||
|
||||
async unsubscribe (): Promise<void> {
|
||||
await this.client.unsubscribeQuery(this.id)
|
||||
}
|
||||
|
||||
async requestLoadNextPage (): Promise<void> {
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
}
|
||||
if (this.forward instanceof Promise) {
|
||||
this.forward = await this.forward
|
||||
}
|
||||
|
||||
if (this.result.isTail()) return
|
||||
|
||||
const last = this.result.getLast()
|
||||
if (last === undefined) return
|
||||
|
||||
const limit = this.params.limit ?? defaultQueryParams.limit
|
||||
const findParams: FindNotificationContextParams = {
|
||||
...this.params,
|
||||
lastUpdate: {
|
||||
greater: last.lastUpdate
|
||||
},
|
||||
limit: limit + 1,
|
||||
order: SortingOrder.Ascending
|
||||
}
|
||||
|
||||
const forward = this.find(findParams)
|
||||
|
||||
this.forward = forward.then(async (res) => {
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
}
|
||||
const isTail = res.length <= limit
|
||||
if (!isTail) {
|
||||
res.pop()
|
||||
}
|
||||
this.result.append(res)
|
||||
this.result.setTail(isTail)
|
||||
await this.notify()
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
async requestLoadPrevPage (): Promise<void> {
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
}
|
||||
if (this.backward instanceof Promise) {
|
||||
this.backward = await this.backward
|
||||
}
|
||||
|
||||
if (this.result.isHead()) return
|
||||
|
||||
const first = this.params.order === SortingOrder.Ascending ? this.result.getFirst() : this.result.getLast()
|
||||
if (first === undefined) return
|
||||
|
||||
const limit = this.params.limit ?? defaultQueryParams.limit
|
||||
const findParams: FindNotificationContextParams = {
|
||||
...this.params,
|
||||
lastUpdate: {
|
||||
less: first.lastUpdate
|
||||
},
|
||||
limit: limit + 1,
|
||||
order: SortingOrder.Descending
|
||||
}
|
||||
|
||||
const backward = this.find(findParams)
|
||||
this.backward = backward.then(async (res) => {
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
}
|
||||
const isHead = res.length <= limit
|
||||
if (!isHead) {
|
||||
res.pop()
|
||||
}
|
||||
|
||||
if (this.params.order === SortingOrder.Ascending) {
|
||||
const reversed = res.reverse()
|
||||
this.result.prepend(reversed)
|
||||
} else {
|
||||
this.result.append(res)
|
||||
}
|
||||
this.result.setHead(isHead)
|
||||
await this.notify()
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
removeCallback (): void {
|
||||
this.callback = () => {}
|
||||
}
|
||||
|
||||
setCallback (callback: QueryCallback<NotificationContext>): void {
|
||||
this.callback = callback
|
||||
void this.notify()
|
||||
}
|
||||
|
||||
copyResult (): QueryResult<NotificationContext> | undefined {
|
||||
if (this.result instanceof Promise) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return this.result.copy()
|
||||
}
|
||||
|
||||
private async find (params: FindNotificationContextParams): Promise<NotificationContext[]> {
|
||||
const contexts = await this.client.findNotificationContexts(params, this.id)
|
||||
if (params.notifications?.message !== true) return contexts
|
||||
|
||||
await Promise.all(
|
||||
contexts.map(async (context) => {
|
||||
const notifications = context.notifications ?? []
|
||||
|
||||
context.notifications = await Promise.all(
|
||||
notifications.map(async (notification) => {
|
||||
if (notification.message != null || notification.messageId == null) return notification
|
||||
|
||||
const message = await loadMessageFromGroup(
|
||||
notification.messageId,
|
||||
this.workspace,
|
||||
this.filesUrl,
|
||||
notification.messageGroup,
|
||||
notification.patches
|
||||
)
|
||||
if (message !== undefined) {
|
||||
return {
|
||||
...notification,
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
return notification
|
||||
})
|
||||
)
|
||||
return context
|
||||
})
|
||||
)
|
||||
|
||||
return contexts
|
||||
}
|
||||
|
||||
private async onCreateNotificationContextEvent (event: NotificationContextCreatedEvent): Promise<void> {
|
||||
if (this.forward instanceof Promise) this.forward = await this.forward
|
||||
if (this.backward instanceof Promise) this.backward = await this.backward
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const context = event.context
|
||||
|
||||
if (this.result.get(context.id) !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.addContext(context)
|
||||
void this.notify()
|
||||
}
|
||||
|
||||
private async onCreatePatchEvent (event: PatchCreatedEvent): Promise<void> {
|
||||
if (this.params.notifications == null) return
|
||||
if (this.forward instanceof Promise) this.forward = await this.forward
|
||||
if (this.backward instanceof Promise) this.backward = await this.backward
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const context = this.result.getResult().find((it) => it.card === event.card)
|
||||
if (context === undefined || (context.notifications ?? []).length === 0) return
|
||||
|
||||
const hasMessage = context.notifications?.some((it) => it.messageId === event.patch.message) ?? false
|
||||
if (!hasMessage) return
|
||||
|
||||
this.result.update({
|
||||
...context,
|
||||
notifications: context.notifications?.map((it) => ({
|
||||
...it,
|
||||
message:
|
||||
it.messageId === event.patch.message && it.message != null
|
||||
? applyPatch(it.message, event.patch, allowedPatchTypes)
|
||||
: it.message
|
||||
}))
|
||||
})
|
||||
|
||||
void this.notify()
|
||||
}
|
||||
|
||||
private async onMessagesRemovedEvent (event: MessagesRemovedEvent): Promise<void> {
|
||||
if (this.params.notifications == null) return
|
||||
if (this.forward instanceof Promise) this.forward = await this.forward
|
||||
if (this.backward instanceof Promise) this.backward = await this.backward
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const context = this.result.getResult().find((it) => it.card === event.card)
|
||||
|
||||
if (context === undefined) return
|
||||
const filtered = (context.notifications ?? []).filter(
|
||||
(it) => it.messageId == null || !event.messages.includes(it.messageId)
|
||||
)
|
||||
if (filtered.length === (context.notifications?.length ?? 0)) return
|
||||
const contextUpdated = (await this.find({ id: context.id, limit: 1, notifications: this.params.notifications }))[0]
|
||||
if (contextUpdated !== undefined) {
|
||||
this.result.update(contextUpdated)
|
||||
} else {
|
||||
this.result.update({
|
||||
...context,
|
||||
notifications: filtered
|
||||
})
|
||||
}
|
||||
void this.notify()
|
||||
}
|
||||
|
||||
private async onRemoveNotificationEvent (event: NotificationsRemovedEvent): Promise<void> {
|
||||
if (this.params.notifications == null) return
|
||||
if (this.forward instanceof Promise) this.forward = await this.forward
|
||||
if (this.backward instanceof Promise) this.backward = await this.backward
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const context = this.result.get(event.context)
|
||||
if (context === undefined) return
|
||||
|
||||
const filtered = (context.notifications ?? []).filter((it) => it.created > event.untilDate)
|
||||
if (filtered.length === (context.notifications?.length ?? 0)) return
|
||||
|
||||
const contextUpdated = (await this.find({ id: context.id, limit: 1, notifications: this.params.notifications }))[0]
|
||||
if (contextUpdated !== undefined) {
|
||||
this.result.update(contextUpdated)
|
||||
} else {
|
||||
this.result.update({
|
||||
...context,
|
||||
notifications: filtered
|
||||
})
|
||||
}
|
||||
void this.notify()
|
||||
}
|
||||
|
||||
private async onCreateNotificationEvent (event: NotificationCreatedEvent): Promise<void> {
|
||||
if (this.params.notifications == null) return
|
||||
if (this.forward instanceof Promise) this.forward = await this.forward
|
||||
if (this.backward instanceof Promise) this.backward = await this.backward
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const context = this.result.get(event.notification.context)
|
||||
if (context !== undefined) {
|
||||
const message =
|
||||
this.params.notifications.message === true
|
||||
? (
|
||||
await this.client.findMessages({
|
||||
card: context.card,
|
||||
id: event.notification.messageId
|
||||
})
|
||||
)[0]
|
||||
: undefined
|
||||
|
||||
const notifications = [
|
||||
{
|
||||
...event.notification,
|
||||
message
|
||||
},
|
||||
...(context.notifications ?? [])
|
||||
]
|
||||
if (notifications.length > this.params.notifications.limit) {
|
||||
notifications.pop()
|
||||
}
|
||||
this.result.update({
|
||||
...context,
|
||||
notifications
|
||||
})
|
||||
void this.notify()
|
||||
} else {
|
||||
const newContext = (
|
||||
await this.find({ id: event.notification.context, notifications: this.params.notifications, limit: 1 })
|
||||
)[0]
|
||||
if (newContext !== undefined) {
|
||||
await this.addContext(newContext)
|
||||
void this.notify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async onRemoveNotificationContextEvent (event: NotificationContextRemovedEvent): Promise<void> {
|
||||
if (this.forward instanceof Promise) this.forward = await this.forward
|
||||
if (this.backward instanceof Promise) this.backward = await this.backward
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
this.result.delete(event.context)
|
||||
}
|
||||
|
||||
private async onUpdateNotificationContextEvent (event: NotificationContextUpdatedEvent): Promise<void> {
|
||||
if (this.forward instanceof Promise) this.forward = await this.forward
|
||||
if (this.backward instanceof Promise) this.backward = await this.backward
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const toUpdate = this.result.get(event.context)
|
||||
|
||||
if (toUpdate !== undefined) {
|
||||
const notifications = this.filterNotifications(
|
||||
event.lastView != null
|
||||
? (toUpdate.notifications ?? []).map((it) => ({
|
||||
...it,
|
||||
read: event.lastView != null && event.lastView >= it.created
|
||||
}))
|
||||
: toUpdate.notifications ?? []
|
||||
)
|
||||
|
||||
if (
|
||||
notifications.length < (toUpdate.notifications?.length ?? 0) &&
|
||||
this.params.notifications?.order !== SortingOrder.Descending
|
||||
) {
|
||||
const updated: NotificationContext = (
|
||||
await this.find({ id: event.context, limit: 1, notifications: this.params.notifications })
|
||||
)[0]
|
||||
if (updated !== undefined) {
|
||||
this.result.update(updated)
|
||||
} else {
|
||||
const updated: NotificationContext = {
|
||||
...toUpdate,
|
||||
lastUpdate: event.lastUpdate ?? toUpdate.lastUpdate,
|
||||
lastView: event.lastView ?? toUpdate.lastView,
|
||||
notifications
|
||||
}
|
||||
this.result.update(updated)
|
||||
}
|
||||
} else {
|
||||
const updated: NotificationContext = {
|
||||
...toUpdate,
|
||||
lastUpdate: event.lastUpdate ?? toUpdate.lastUpdate,
|
||||
lastView: event.lastView ?? toUpdate.lastView,
|
||||
notifications
|
||||
}
|
||||
this.result.update(updated)
|
||||
}
|
||||
if (event.lastUpdate != null) {
|
||||
this.result.sort((a, b) =>
|
||||
this.params.order === SortingOrder.Descending
|
||||
? b.lastUpdate.getTime() - a.lastUpdate.getTime()
|
||||
: a.lastUpdate.getTime() - b.lastUpdate.getTime()
|
||||
)
|
||||
}
|
||||
void this.notify()
|
||||
}
|
||||
}
|
||||
|
||||
private filterNotifications (notifications: Notification[]): Notification[] {
|
||||
if (this.params.notifications == null) return notifications
|
||||
const read = this.params.notifications.read
|
||||
if (read == null) return notifications
|
||||
|
||||
return notifications.filter((it) => it.read === read)
|
||||
}
|
||||
|
||||
private async addContext (context: NotificationContext): Promise<void> {
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
if (this.result.get(context.id) !== undefined) return
|
||||
if (this.result.isTail()) {
|
||||
if (this.params.order === SortingOrder.Ascending) {
|
||||
this.result.push(context)
|
||||
} else {
|
||||
this.result.unshift(context)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.params.limit != null && this.result.length > this.params.limit) {
|
||||
this.result.pop()
|
||||
}
|
||||
}
|
||||
|
||||
private async notify (): Promise<void> {
|
||||
if (this.callback === undefined) return
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
}
|
||||
|
||||
const result = this.result.getResult()
|
||||
const isTail = this.result.isTail()
|
||||
const isHead = this.result.isHead()
|
||||
|
||||
const window = new WindowImpl(result, isTail, isHead, this)
|
||||
this.callback(window)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// You may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
@@ -15,122 +15,239 @@
|
||||
|
||||
import {
|
||||
type FindNotificationsParams,
|
||||
SortingOrder,
|
||||
type Notification,
|
||||
type MessageID
|
||||
SortingOrder,
|
||||
type WorkspaceID
|
||||
} from '@hcengineering/communication-types'
|
||||
import {
|
||||
type NotificationCreatedEvent,
|
||||
ResponseEventType,
|
||||
type ResponseEvent,
|
||||
type NotificationContextRemovedEvent,
|
||||
type NotificationRemovedEvent,
|
||||
type NotificationContextUpdatedEvent
|
||||
type NotificationContextUpdatedEvent,
|
||||
type NotificationCreatedEvent,
|
||||
type NotificationsRemovedEvent,
|
||||
type QueryCallback,
|
||||
type RequestEvent,
|
||||
type ResponseEvent,
|
||||
ResponseEventType
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
|
||||
import { BaseQuery } from '../query'
|
||||
import { defaultQueryParams, type PagedQuery, type QueryId, type QueryClient } from '../types'
|
||||
import { QueryResult } from '../result'
|
||||
import { WindowImpl } from '../window'
|
||||
import { loadMessageFromGroup } from '../utils'
|
||||
|
||||
export class NotificationQuery extends BaseQuery<Notification, FindNotificationsParams> {
|
||||
override async find (params: FindNotificationsParams): Promise<Notification[]> {
|
||||
return await this.client.findNotifications(params, this.id)
|
||||
export class NotificationQuery implements PagedQuery<Notification, FindNotificationsParams> {
|
||||
private result: QueryResult<Notification> | Promise<QueryResult<Notification>>
|
||||
|
||||
constructor (
|
||||
private readonly client: QueryClient,
|
||||
private readonly workspace: WorkspaceID,
|
||||
private readonly filesUrl: string,
|
||||
public readonly id: QueryId,
|
||||
public readonly params: FindNotificationsParams,
|
||||
private callback?: QueryCallback<Notification>,
|
||||
initialResult?: QueryResult<Notification>
|
||||
) {
|
||||
const limit = this.params.limit ?? defaultQueryParams.limit
|
||||
const findParams = { ...this.params, sort: this.params.order ?? defaultQueryParams.order, limit: limit + 1 }
|
||||
|
||||
if (initialResult !== undefined) {
|
||||
this.result = initialResult
|
||||
void this.notify()
|
||||
} else {
|
||||
this.result = this.initResult(findParams, limit)
|
||||
}
|
||||
}
|
||||
|
||||
override getObjectId (object: Notification): MessageID {
|
||||
return object.message.id
|
||||
private async initResult (findParams: FindNotificationsParams, limit: number): Promise<QueryResult<Notification>> {
|
||||
try {
|
||||
const res = await this.find(findParams)
|
||||
const isComplete = res.length <= limit
|
||||
if (!isComplete) res.pop()
|
||||
|
||||
const result = new QueryResult(res, (it) => it.id)
|
||||
result.setTail(isComplete)
|
||||
result.setHead(isComplete)
|
||||
|
||||
void this.notify()
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize query:', error)
|
||||
return new QueryResult([] as Notification[], (it) => it.id)
|
||||
}
|
||||
}
|
||||
|
||||
override getObjectDate (object: Notification): Date {
|
||||
return object.message.created
|
||||
}
|
||||
|
||||
override async onEvent (event: ResponseEvent): Promise<void> {
|
||||
async onEvent (event: ResponseEvent): Promise<void> {
|
||||
switch (event.type) {
|
||||
case ResponseEventType.NotificationCreated: {
|
||||
await this.onCreateNotificationEvent(event)
|
||||
return
|
||||
}
|
||||
case ResponseEventType.NotificationRemoved: {
|
||||
await this.onRemoveNotificationEvent(event)
|
||||
return
|
||||
}
|
||||
case ResponseEventType.NotificationContextUpdated: {
|
||||
await this.onUpdateNotificationContextEvent(event)
|
||||
return
|
||||
}
|
||||
case ResponseEventType.NotificationContextRemoved: {
|
||||
await this.onRemoveNotificationContextEvent(event)
|
||||
}
|
||||
case ResponseEventType.NotificationCreated:
|
||||
{ await this.onCreateNotificationEvent(event); break }
|
||||
case ResponseEventType.NotificationsRemoved:
|
||||
{ await this.onRemoveNotificationsEvent(event); break }
|
||||
case ResponseEventType.NotificationContextUpdated:
|
||||
{ await this.onUpdateNotificationContextEvent(event); break }
|
||||
case ResponseEventType.NotificationContextRemoved:
|
||||
{ await this.onRemoveNotificationContextEvent(event) }
|
||||
}
|
||||
}
|
||||
|
||||
async onCreateNotificationEvent (event: NotificationCreatedEvent): Promise<void> {
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
async onRequest (event: RequestEvent): Promise<void> {}
|
||||
|
||||
async unsubscribe (): Promise<void> {
|
||||
await this.client.unsubscribeQuery(this.id)
|
||||
}
|
||||
|
||||
async requestLoadNextPage (): Promise<void> {
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
await this.loadPage(SortingOrder.Ascending, this.result.getLast()?.created)
|
||||
}
|
||||
|
||||
async requestLoadPrevPage (): Promise<void> {
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
await this.loadPage(SortingOrder.Descending, this.result.getFirst()?.created)
|
||||
}
|
||||
|
||||
private async loadPage (order: SortingOrder, created?: Date): Promise<void> {
|
||||
if (!created) return
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const limit = this.getLimit()
|
||||
const findParams: FindNotificationsParams = {
|
||||
...this.params,
|
||||
created: order === SortingOrder.Ascending ? { greater: created } : { less: created },
|
||||
limit: limit + 1,
|
||||
order
|
||||
}
|
||||
|
||||
const exists = this.result.get(event.notification.message.id)
|
||||
if (exists !== undefined) return
|
||||
try {
|
||||
const res = await this.find(findParams)
|
||||
const isComplete = res.length <= limit
|
||||
if (!isComplete) res.pop()
|
||||
|
||||
if (this.params.message != null && this.params.message !== event.notification.message.id) return
|
||||
if (this.params.context != null && this.params.context !== event.notification.context) return
|
||||
|
||||
if (this.result.isTail()) {
|
||||
if (this.params.order === SortingOrder.Ascending) {
|
||||
this.result.push(event.notification)
|
||||
if (order === SortingOrder.Ascending) {
|
||||
this.result.append(res)
|
||||
this.result.setTail(isComplete)
|
||||
} else {
|
||||
this.result.unshift(event.notification)
|
||||
this.result.prepend(res)
|
||||
this.result.setHead(isComplete)
|
||||
}
|
||||
|
||||
await this.notify()
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${order === SortingOrder.Ascending ? 'next' : 'previous'} page:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
removeCallback (): void {
|
||||
this.callback = () => {}
|
||||
}
|
||||
|
||||
setCallback (callback: QueryCallback<Notification>): void {
|
||||
this.callback = callback
|
||||
void this.notify()
|
||||
}
|
||||
|
||||
copyResult (): QueryResult<Notification> | undefined {
|
||||
return this.result instanceof Promise ? undefined : this.result.copy()
|
||||
}
|
||||
|
||||
private async find (params: FindNotificationsParams): Promise<Notification[]> {
|
||||
const notifications = await this.client.findNotifications(params, this.id)
|
||||
if (!params.message) return notifications
|
||||
|
||||
return await Promise.all(
|
||||
notifications.map(async (notification) => {
|
||||
if (notification.message || !notification.messageId) return notification
|
||||
const message = await loadMessageFromGroup(
|
||||
notification.messageId,
|
||||
this.workspace,
|
||||
this.filesUrl,
|
||||
notification.messageGroup,
|
||||
notification.patches
|
||||
)
|
||||
return message ? { ...notification, message } : notification
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private async onCreateNotificationEvent (event: NotificationCreatedEvent): Promise<void> {
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
if (this.result.get(event.notification.id)) return
|
||||
if (!this.result.isTail()) return
|
||||
if (this.params.context && this.params.context !== event.notification.context) return
|
||||
|
||||
const notifications = this.filterNotifications([event.notification])
|
||||
if (notifications.length === 0) return
|
||||
|
||||
if (this.params.order === SortingOrder.Ascending) {
|
||||
this.result.push(event.notification)
|
||||
} else {
|
||||
this.result.unshift(event.notification)
|
||||
}
|
||||
|
||||
await this.notify()
|
||||
}
|
||||
|
||||
private async onUpdateNotificationContextEvent (event: NotificationContextUpdatedEvent): Promise<void> {
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
}
|
||||
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
if (this.params.context != null && this.params.context !== event.context) return
|
||||
if (event.update.lastView === undefined && event.update.archivedFrom === undefined) return
|
||||
|
||||
const toUpdate =
|
||||
this.params.context === event.context
|
||||
? this.result.getResult()
|
||||
: this.result.getResult().filter((it) => it.context === event.context)
|
||||
const lastView = event.lastView
|
||||
if (lastView === undefined) return
|
||||
|
||||
const toUpdate = this.result.getResult().filter((it) => it.context === event.context)
|
||||
if (toUpdate.length === 0) return
|
||||
|
||||
for (const notification of toUpdate) {
|
||||
this.result.update({
|
||||
...notification,
|
||||
...(event.update.lastView !== undefined
|
||||
? {
|
||||
read: event.update.lastView < notification.message.created
|
||||
}
|
||||
: {}),
|
||||
...(event.update.archivedFrom !== undefined
|
||||
? {
|
||||
archived: event.update.archivedFrom < notification.message.created
|
||||
}
|
||||
: {})
|
||||
})
|
||||
const updated: Notification[] = toUpdate.map((it) => ({
|
||||
...it,
|
||||
read: lastView >= it.created
|
||||
}))
|
||||
const filtered = this.filterNotifications(updated)
|
||||
|
||||
if (filtered.length < this.getLimit() && filtered.length < toUpdate.length) {
|
||||
if (this.result.length < this.getLimit()) {
|
||||
for (const notification of updated) {
|
||||
const allowed = filtered.some((it) => it.messageId === notification.messageId)
|
||||
if (allowed) {
|
||||
this.result.update(notification)
|
||||
} else {
|
||||
this.result.delete(notification.id)
|
||||
}
|
||||
}
|
||||
void this.notify()
|
||||
} else {
|
||||
await this.reinit(this.result.length)
|
||||
}
|
||||
} else {
|
||||
for (const notification of filtered) {
|
||||
this.result.update(notification)
|
||||
}
|
||||
void this.notify()
|
||||
}
|
||||
}
|
||||
|
||||
private async onRemoveNotificationEvent (event: NotificationRemovedEvent): Promise<void> {
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
private async onRemoveNotificationsEvent (event: NotificationsRemovedEvent): Promise<void> {
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const notifications = this.result.getResult()
|
||||
const length = this.result.length
|
||||
let isDeleted = false
|
||||
|
||||
for (const notification of notifications) {
|
||||
if (notification.created <= event.untilDate) {
|
||||
isDeleted = true
|
||||
this.result.delete(notification.id)
|
||||
}
|
||||
}
|
||||
|
||||
const deleted = this.result.delete(event.message)
|
||||
|
||||
if (deleted !== undefined) {
|
||||
await this.notify()
|
||||
if (length >= this.getLimit() && this.result.length < this.getLimit()) {
|
||||
void this.reinit(this.result.length)
|
||||
} else if (isDeleted) {
|
||||
void this.notify()
|
||||
}
|
||||
}
|
||||
|
||||
private async onRemoveNotificationContextEvent (event: NotificationContextRemovedEvent): Promise<void> {
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
}
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
if (this.params.context != null && this.params.context !== event.context) return
|
||||
|
||||
@@ -139,15 +256,60 @@ export class NotificationQuery extends BaseQuery<Notification, FindNotifications
|
||||
this.result.deleteAll()
|
||||
this.result.setHead(true)
|
||||
this.result.setTail(true)
|
||||
await this.notify()
|
||||
void this.notify()
|
||||
} else {
|
||||
const toRemove = this.result.getResult().filter((it) => it.context === event.context)
|
||||
if (toRemove.length === 0) return
|
||||
const length = this.result.length
|
||||
|
||||
for (const notification of toRemove) {
|
||||
this.result.delete(notification.message.id)
|
||||
this.result.delete(notification.id)
|
||||
}
|
||||
|
||||
if (length >= this.getLimit() && this.result.length < this.getLimit()) {
|
||||
void this.reinit(this.result.length)
|
||||
} else {
|
||||
void this.notify()
|
||||
}
|
||||
await this.notify()
|
||||
}
|
||||
}
|
||||
|
||||
private async notify (): Promise<void> {
|
||||
if (!this.callback) return
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
|
||||
const window = new WindowImpl(this.result.getResult(), this.result.isTail(), this.result.isHead(), this)
|
||||
this.callback(window)
|
||||
}
|
||||
|
||||
private getLimit (): number {
|
||||
return this.params.limit ?? defaultQueryParams.limit
|
||||
}
|
||||
|
||||
private filterNotifications (notifications: Notification[]): Notification[] {
|
||||
const read = this.params.read
|
||||
if (read == null) return notifications
|
||||
|
||||
return notifications.filter((it) => it.read === read)
|
||||
}
|
||||
|
||||
private async reinit (limit: number): Promise<void> {
|
||||
if (this.result instanceof Promise) this.result = await this.result
|
||||
this.result = this.find({ ...this.params, limit: limit + 1 }).then((res) => {
|
||||
const isTail = res.length <= limit
|
||||
const isHead = res.length <= limit
|
||||
if (res.length > limit) {
|
||||
res.pop()
|
||||
}
|
||||
|
||||
const result = new QueryResult(res, (it) => it.id)
|
||||
result.setHead(isHead)
|
||||
result.setTail(isTail)
|
||||
return result
|
||||
})
|
||||
void this.result.then((res) => {
|
||||
void this.notify()
|
||||
return res
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { SortingOrder, type ID } from '@hcengineering/communication-types'
|
||||
import { type ResponseEvent, type QueryCallback, type QueryClient } from '@hcengineering/communication-sdk-types'
|
||||
|
||||
import { QueryResult } from './result'
|
||||
import { defaultQueryParams, type FindParams, type PagedQuery, type QueryId } from './types'
|
||||
import { WindowImpl } from './window'
|
||||
|
||||
export class BaseQuery<T, P extends FindParams> implements PagedQuery<T, P> {
|
||||
protected result: QueryResult<T> | Promise<QueryResult<T>>
|
||||
private forward: Promise<T[]> | T[] = []
|
||||
private backward: Promise<T[]> | T[] = []
|
||||
|
||||
constructor(
|
||||
protected readonly client: QueryClient,
|
||||
public readonly id: QueryId,
|
||||
public readonly params: P,
|
||||
private callback?: QueryCallback<T>,
|
||||
initialResult?: QueryResult<T>
|
||||
) {
|
||||
if (initialResult !== undefined) {
|
||||
this.result = initialResult
|
||||
void this.notify()
|
||||
} else {
|
||||
const limit = this.params.limit ?? defaultQueryParams.limit
|
||||
const findParams = {
|
||||
...this.params,
|
||||
sort: this.params.order ?? defaultQueryParams.order,
|
||||
limit: limit + 1
|
||||
}
|
||||
|
||||
const findPromise = this.find(findParams)
|
||||
this.result = findPromise.then((res) => {
|
||||
const isTail = false
|
||||
const isHead = false
|
||||
if (!isTail) {
|
||||
res.pop()
|
||||
}
|
||||
const qResult = new QueryResult(res, (x) => this.getObjectId(x))
|
||||
qResult.setTail(isTail)
|
||||
qResult.setHead(isHead)
|
||||
|
||||
return qResult
|
||||
})
|
||||
this.result
|
||||
.then(async () => {
|
||||
await this.notify()
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.error('Failed to update Live query: ', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected async find(params: FindParams): Promise<T[]> {
|
||||
/* Implement in subclass */
|
||||
return [] as T[]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected getObjectId(object: T): ID {
|
||||
/* Implement in subclass */
|
||||
return '' as ID
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected getObjectDate(object: T): Date {
|
||||
/* Implement in subclass */
|
||||
return new Date(0)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async onEvent(event: ResponseEvent): Promise<void> {
|
||||
/* Implement in subclass */
|
||||
}
|
||||
|
||||
setCallback(callback: QueryCallback<T>): void {
|
||||
this.callback = callback
|
||||
void this.notify()
|
||||
}
|
||||
|
||||
removeCallback(): void {
|
||||
this.callback = () => {}
|
||||
}
|
||||
|
||||
protected async notify(): Promise<void> {
|
||||
if (this.callback === undefined) return
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
}
|
||||
|
||||
const result = this.result.getResult()
|
||||
const isTail = this.result.isTail()
|
||||
const isHead = this.result.isHead()
|
||||
|
||||
const window = new WindowImpl(result, isTail, isHead, this)
|
||||
this.callback(window)
|
||||
}
|
||||
|
||||
async requestLoadNextPage(): Promise<void> {
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
}
|
||||
if (this.forward instanceof Promise) {
|
||||
this.forward = await this.forward
|
||||
}
|
||||
|
||||
if (this.result.isTail()) return
|
||||
|
||||
const last = this.result.getLast()
|
||||
if (last === undefined) return
|
||||
|
||||
const limit = this.params.limit ?? defaultQueryParams.limit
|
||||
const findParams: FindParams = {
|
||||
...this.params,
|
||||
from: this.getObjectDate(last),
|
||||
excluded: true,
|
||||
limit: limit + 1,
|
||||
order: SortingOrder.Ascending
|
||||
}
|
||||
|
||||
const forward = this.find(findParams)
|
||||
|
||||
this.forward = forward.then(async (res) => {
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
}
|
||||
const isTail = res.length <= limit
|
||||
if (!isTail) {
|
||||
res.pop()
|
||||
}
|
||||
this.result.append(res)
|
||||
this.result.setTail(isTail)
|
||||
await this.notify()
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
async requestLoadPrevPage(): Promise<void> {
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
}
|
||||
if (this.backward instanceof Promise) {
|
||||
this.backward = await this.backward
|
||||
}
|
||||
|
||||
if (this.result.isHead()) return
|
||||
|
||||
const first = this.params.order === SortingOrder.Ascending ? this.result.getFirst() : this.result.getLast()
|
||||
if (first === undefined) return
|
||||
|
||||
const limit = this.params.limit ?? defaultQueryParams.limit
|
||||
const findParams: FindParams = {
|
||||
...this.params,
|
||||
from: this.getObjectDate(first),
|
||||
limit: limit + 1,
|
||||
order: SortingOrder.Descending
|
||||
}
|
||||
|
||||
const backward = this.find(findParams)
|
||||
this.backward = backward.then(async (res) => {
|
||||
if (this.result instanceof Promise) {
|
||||
this.result = await this.result
|
||||
}
|
||||
const isHead = res.length <= limit
|
||||
if (!isHead) {
|
||||
res.pop()
|
||||
}
|
||||
|
||||
if (this.params.order === SortingOrder.Ascending) {
|
||||
const reversed = res.reverse()
|
||||
this.result.prepend(reversed)
|
||||
} else {
|
||||
this.result.append(res)
|
||||
}
|
||||
this.result.setHead(isHead)
|
||||
await this.notify()
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
copyResult(): QueryResult<T> | undefined {
|
||||
if (this.result instanceof Promise) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return this.result.copy()
|
||||
}
|
||||
|
||||
async unsubscribe(): Promise<void> {
|
||||
await this.client.unsubscribeQuery(this.id)
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,14 @@ export class QueryResult<T> {
|
||||
this.objectById = new Map([[this.getId(object), object], ...this.objectById])
|
||||
}
|
||||
|
||||
pop(): T | undefined {
|
||||
const array = Array.from(this.objectById.values())
|
||||
const last = array[array.length - 1]
|
||||
if (last === undefined) return
|
||||
this.objectById.delete(this.getId(last))
|
||||
return last
|
||||
}
|
||||
|
||||
update(object: T): void {
|
||||
this.objectById.set(this.getId(object), object)
|
||||
}
|
||||
@@ -97,6 +105,12 @@ export class QueryResult<T> {
|
||||
}
|
||||
}
|
||||
|
||||
sort(compare: (a: T, b: T) => number): void {
|
||||
const current = Array.from(this.objectById.values())
|
||||
const sorted = current.sort(compare)
|
||||
this.objectById = new Map(sorted.map<[ID, T]>((object) => [this.getId(object), object]))
|
||||
}
|
||||
|
||||
copy(): QueryResult<T> {
|
||||
const copy = new QueryResult(Array.from(this.objectById.values()), this.getId)
|
||||
|
||||
|
||||
@@ -14,7 +14,19 @@
|
||||
//
|
||||
|
||||
import { type ResponseEvent } from '@hcengineering/communication-sdk-types'
|
||||
import { SortingOrder, type Window } from '@hcengineering/communication-types'
|
||||
import {
|
||||
SortingOrder,
|
||||
type FindMessagesGroupsParams,
|
||||
type FindMessagesParams,
|
||||
type FindNotificationContextParams,
|
||||
type FindNotificationsParams,
|
||||
type Message,
|
||||
type MessagesGroup,
|
||||
type NotificationContext,
|
||||
type Window,
|
||||
type Notification
|
||||
} from '@hcengineering/communication-types'
|
||||
import type { EventResult, RequestEvent } from '@hcengineering/communication-sdk-types'
|
||||
|
||||
import { QueryResult } from './result'
|
||||
|
||||
@@ -37,13 +49,30 @@ export interface PagedQuery<R = any, P = FindParams> {
|
||||
readonly params: P
|
||||
|
||||
onEvent: (event: ResponseEvent) => Promise<void>
|
||||
onRequest: (event: RequestEvent, promise: Promise<EventResult>) => Promise<void>
|
||||
|
||||
unsubscribe: () => Promise<void>
|
||||
|
||||
requestLoadNextPage: () => Promise<void>
|
||||
requestLoadPrevPage: () => Promise<void>
|
||||
|
||||
unsubscribe: () => Promise<void>
|
||||
|
||||
setCallback: (callback: (window: Window<R>) => void) => void
|
||||
removeCallback: () => void
|
||||
setCallback: (callback: (window: Window<R>) => void) => void
|
||||
copyResult: () => QueryResult<R> | undefined
|
||||
}
|
||||
|
||||
export interface QueryClient {
|
||||
onEvent(event: ResponseEvent): void
|
||||
|
||||
onRequest(event: RequestEvent, promise: Promise<EventResult>): void
|
||||
|
||||
findMessages(params: FindMessagesParams, queryId?: number): Promise<Message[]>
|
||||
|
||||
findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]>
|
||||
|
||||
findNotificationContexts(params: FindNotificationContextParams, queryId?: number): Promise<NotificationContext[]>
|
||||
|
||||
findNotifications(params: FindNotificationsParams, queryId?: number): Promise<Notification[]>
|
||||
|
||||
unsubscribeQuery(id: number): Promise<void>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { applyPatches } from '@hcengineering/communication-shared'
|
||||
import type { Message, MessageID, MessagesGroup, Patch, WorkspaceID } from '@hcengineering/communication-types'
|
||||
import { loadGroupFile } from '@hcengineering/communication-yaml'
|
||||
|
||||
export async function loadMessageFromGroup(
|
||||
id: MessageID,
|
||||
workspace: WorkspaceID,
|
||||
filesUrl: string,
|
||||
group?: MessagesGroup,
|
||||
patches: Patch[] = []
|
||||
): Promise<Message | undefined> {
|
||||
if (group == null) return
|
||||
|
||||
const parsedFile = await loadGroupFile(workspace, filesUrl, group, { retries: 5 })
|
||||
|
||||
const message = parsedFile.messages.find((it) => it.id === id)
|
||||
if (message == null) return
|
||||
|
||||
return applyPatches(message, patches)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@hcengineering/communication-rest-client",
|
||||
"version": "0.1.147",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "./types/index.d.ts",
|
||||
"files": [
|
||||
"dist/index.js",
|
||||
"dist/index.cjs",
|
||||
"types/**/*.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"bundle": "bun run bundle:browser && bun run bundle:node",
|
||||
"bundle:browser": "bun build src/index.ts --outdir dist --target browser",
|
||||
"bundle:node": "bun build src/index.ts --target node --format cjs --outfile dist/index.cjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/snappyjs": "^0.7.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"@hcengineering/communication-shared": "workspace:*",
|
||||
"@hcengineering/communication-sdk-types": "workspace:*",
|
||||
"@hcengineering/core": "^0.7.28",
|
||||
"snappyjs": "^0.7.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hcengineering/communication.git"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com"
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,5 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
export { type Client } from '@hcengineering/communication-sdk-types'
|
||||
export * from './client'
|
||||
export { createRestClient } from './rest'
|
||||
export type { RestClient } from './types'
|
||||
@@ -0,0 +1,164 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { concatLink } from '@hcengineering/core'
|
||||
import { PlatformError, unknownError } from '@hcengineering/platform'
|
||||
import type { EventResult, RequestEvent } from '@hcengineering/communication-sdk-types'
|
||||
import type {
|
||||
FindMessagesGroupsParams,
|
||||
FindMessagesParams,
|
||||
FindNotificationContextParams,
|
||||
Message,
|
||||
MessagesGroup,
|
||||
NotificationContext,
|
||||
FindNotificationsParams,
|
||||
Notification
|
||||
} from '@hcengineering/communication-types'
|
||||
import { retry } from '@hcengineering/communication-shared'
|
||||
|
||||
import { extractJson } from './utils'
|
||||
import type { RestClient } from './types'
|
||||
|
||||
const retries = 3
|
||||
|
||||
export function createRestClient(endpoint: string, workspaceId: string, token: string): RestClient {
|
||||
return new RestClientImpl(endpoint, workspaceId, token)
|
||||
}
|
||||
|
||||
class RestClientImpl implements RestClient {
|
||||
endpoint: string
|
||||
|
||||
constructor(
|
||||
endpoint: string,
|
||||
readonly workspace: string,
|
||||
readonly token: string
|
||||
) {
|
||||
this.endpoint = endpoint.replace('ws', 'http')
|
||||
}
|
||||
|
||||
private jsonHeaders(): Record<string, string> {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + this.token,
|
||||
'accept-encoding': 'snappy, gzip'
|
||||
}
|
||||
}
|
||||
|
||||
private requestInit(): RequestInit {
|
||||
return {
|
||||
method: 'GET',
|
||||
keepalive: true,
|
||||
headers: this.jsonHeaders()
|
||||
}
|
||||
}
|
||||
|
||||
async event(event: RequestEvent): Promise<EventResult> {
|
||||
const response = await fetch(concatLink(this.endpoint, `/api/v1/event/${this.workspace}`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + this.token
|
||||
},
|
||||
keepalive: true,
|
||||
body: JSON.stringify(event)
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new PlatformError(unknownError(response.statusText))
|
||||
}
|
||||
return (await response.json()) as EventResult
|
||||
}
|
||||
|
||||
async findMessages(params: FindMessagesParams): Promise<Message[]> {
|
||||
const searchParams = new URLSearchParams()
|
||||
if (Object.keys(params).length > 0) {
|
||||
searchParams.append('params', JSON.stringify(params))
|
||||
}
|
||||
const requestUrl = concatLink(this.endpoint, `/api/v1/find-messages/${this.workspace}?${searchParams.toString()}`)
|
||||
|
||||
return await retry(
|
||||
async () => {
|
||||
const response = await fetch(requestUrl, this.requestInit())
|
||||
if (!response.ok) {
|
||||
throw new PlatformError(unknownError(response.statusText))
|
||||
}
|
||||
return await extractJson<MessagesGroup[]>(response)
|
||||
},
|
||||
{ retries }
|
||||
)
|
||||
}
|
||||
|
||||
async findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
|
||||
const searchParams = new URLSearchParams()
|
||||
if (Object.keys(params).length > 0) {
|
||||
searchParams.append('params', JSON.stringify(params))
|
||||
}
|
||||
const requestUrl = concatLink(
|
||||
this.endpoint,
|
||||
`/api/v1/find-messages-groups/${this.workspace}?${searchParams.toString()}`
|
||||
)
|
||||
return await retry(
|
||||
async () => {
|
||||
const response = await fetch(requestUrl, this.requestInit())
|
||||
if (!response.ok) {
|
||||
throw new PlatformError(unknownError(response.statusText))
|
||||
}
|
||||
return await extractJson<MessagesGroup[]>(response)
|
||||
},
|
||||
{ retries }
|
||||
)
|
||||
}
|
||||
|
||||
async findNotificationContexts(params: FindNotificationContextParams): Promise<NotificationContext[]> {
|
||||
const searchParams = new URLSearchParams()
|
||||
if (Object.keys(params).length > 0) {
|
||||
searchParams.append('params', JSON.stringify(params))
|
||||
}
|
||||
const requestUrl = concatLink(
|
||||
this.endpoint,
|
||||
`/api/v1/find-notification-contexts/${this.workspace}?${searchParams.toString()}`
|
||||
)
|
||||
return await retry(
|
||||
async () => {
|
||||
const response = await fetch(requestUrl, this.requestInit())
|
||||
if (!response.ok) {
|
||||
throw new PlatformError(unknownError(response.statusText))
|
||||
}
|
||||
return await extractJson<NotificationContext[]>(response)
|
||||
},
|
||||
{ retries }
|
||||
)
|
||||
}
|
||||
|
||||
async findNotifications(params: FindNotificationsParams): Promise<Notification[]> {
|
||||
const searchParams = new URLSearchParams()
|
||||
if (Object.keys(params).length > 0) {
|
||||
searchParams.append('params', JSON.stringify(params))
|
||||
}
|
||||
const requestUrl = concatLink(
|
||||
this.endpoint,
|
||||
`/api/v1/find-notifications/${this.workspace}?${searchParams.toString()}`
|
||||
)
|
||||
return await retry(
|
||||
async () => {
|
||||
const response = await fetch(requestUrl, this.requestInit())
|
||||
if (!response.ok) {
|
||||
throw new PlatformError(unknownError(response.statusText))
|
||||
}
|
||||
return await extractJson<Notification[]>(response)
|
||||
},
|
||||
{ retries }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { EventResult, RequestEvent } from '@hcengineering/communication-sdk-types'
|
||||
import type {
|
||||
FindMessagesGroupsParams,
|
||||
FindMessagesParams,
|
||||
Message,
|
||||
MessagesGroup,
|
||||
FindNotificationsParams,
|
||||
FindNotificationContextParams,
|
||||
NotificationContext,
|
||||
Notification
|
||||
} from '@hcengineering/communication-types'
|
||||
|
||||
export interface RestClient {
|
||||
findMessages: (params: FindMessagesParams) => Promise<Message[]>
|
||||
findMessagesGroups: (params: FindMessagesGroupsParams) => Promise<MessagesGroup[]>
|
||||
findNotificationContexts: (params: FindNotificationContextParams) => Promise<NotificationContext[]>
|
||||
findNotifications: (params: FindNotificationsParams) => Promise<Notification[]>
|
||||
|
||||
event: (event: RequestEvent) => Promise<EventResult>
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { uncompress } from 'snappyjs'
|
||||
|
||||
function isDateString(value: any) {
|
||||
if (typeof value !== 'string') return false
|
||||
const dateStringRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/
|
||||
|
||||
return dateStringRegex.test(value)
|
||||
}
|
||||
|
||||
function reviver(key: string, value: any) {
|
||||
if (isDateString(value)) return new Date(value)
|
||||
return value
|
||||
}
|
||||
|
||||
export async function extractJson<T>(response: Response): Promise<any> {
|
||||
const encoding = response.headers.get('content-encoding')
|
||||
if (encoding === 'snappy') {
|
||||
const buffer = await response.arrayBuffer()
|
||||
const decompressed = uncompress(buffer)
|
||||
const decoder = new TextDecoder()
|
||||
const jsonString = decoder.decode(decompressed)
|
||||
return JSON.parse(jsonString, reviver) as T
|
||||
}
|
||||
const jsonString = await response.text()
|
||||
return JSON.parse(jsonString, reviver) as T
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/communication-sdk-types",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.147",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./types/index.d.ts",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"@hcengineering/core": "0.7.15"
|
||||
"@hcengineering/core": "^0.7.28"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3"
|
||||
|
||||
@@ -21,12 +21,12 @@ import type {
|
||||
Message,
|
||||
MessageID,
|
||||
NotificationContext,
|
||||
NotificationContextUpdate,
|
||||
RichText,
|
||||
SocialID,
|
||||
Notification,
|
||||
FindMessagesGroupsParams,
|
||||
MessagesGroup
|
||||
MessagesGroup,
|
||||
BlobID
|
||||
} from '@hcengineering/communication-types'
|
||||
import type { FindMessagesParams } from '@hcengineering/communication-types'
|
||||
|
||||
@@ -40,17 +40,24 @@ export interface Client {
|
||||
createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void>
|
||||
removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void>
|
||||
|
||||
createAttachment(card: CardID, message: MessageID, attachment: CardID, creator: SocialID): Promise<void>
|
||||
removeAttachment(card: CardID, message: MessageID, attachment: CardID): Promise<void>
|
||||
createFile(
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
blobId: BlobID,
|
||||
fileType: string,
|
||||
filename: string,
|
||||
creator: SocialID
|
||||
): Promise<void>
|
||||
removeFile(card: CardID, message: MessageID, blobId: BlobID): Promise<void>
|
||||
|
||||
createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void>
|
||||
|
||||
createNotification(message: MessageID, context: ContextID): Promise<void>
|
||||
removeNotification(message: MessageID, context: ContextID): Promise<void>
|
||||
|
||||
createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise<ContextID>
|
||||
createNotificationContext(card: CardID, lastView: Date, lastUpdate: Date): Promise<ContextID>
|
||||
removeNotificationContext(context: ContextID): Promise<void>
|
||||
updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise<void>
|
||||
updateNotificationContext(context: ContextID, lastView?: Date, lastUpdate?: Date): Promise<void>
|
||||
|
||||
onEvent(event: ResponseEvent): void
|
||||
|
||||
|
||||
@@ -22,22 +22,32 @@ import type {
|
||||
Message,
|
||||
MessageID,
|
||||
NotificationContext,
|
||||
NotificationContextUpdate,
|
||||
RichText,
|
||||
SocialID,
|
||||
Notification,
|
||||
BlobID,
|
||||
FindMessagesGroupsParams,
|
||||
MessagesGroup,
|
||||
WorkspaceID,
|
||||
PatchType,
|
||||
Thread
|
||||
Thread,
|
||||
AccountID,
|
||||
Collaborator,
|
||||
MessageType,
|
||||
FindCollaboratorsParams,
|
||||
NotificationID,
|
||||
MessageData
|
||||
} from '@hcengineering/communication-types'
|
||||
|
||||
export interface DbAdapter {
|
||||
createMessage(card: CardID, content: RichText, creator: SocialID, created: Date): Promise<MessageID>
|
||||
removeMessage(card: CardID, id: MessageID, socialIds?: SocialID[]): Promise<void>
|
||||
removeMessages(card: CardID, fromId: MessageID, toId: MessageID): Promise<void>
|
||||
createMessage(
|
||||
card: CardID,
|
||||
type: MessageType,
|
||||
content: RichText,
|
||||
creator: SocialID,
|
||||
created: Date,
|
||||
data?: MessageData
|
||||
): Promise<MessageID>
|
||||
removeMessages(card: CardID, ids: MessageID[], socialIds?: SocialID[]): Promise<MessageID[]>
|
||||
|
||||
createPatch(
|
||||
card: CardID,
|
||||
@@ -47,49 +57,47 @@ export interface DbAdapter {
|
||||
creator: SocialID,
|
||||
created: Date
|
||||
): Promise<void>
|
||||
removePatches(card: CardID, fromId: MessageID, toId: MessageID): Promise<void>
|
||||
|
||||
createMessagesGroup(
|
||||
card: CardID,
|
||||
blobId: BlobID,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
fromID: MessageID,
|
||||
toID: MessageID,
|
||||
count: number
|
||||
): Promise<void>
|
||||
createMessagesGroup(card: CardID, blobId: BlobID, fromSec: Date, toSec: Date, count: number): Promise<void>
|
||||
removeMessagesGroup(card: CardID, blobId: BlobID): Promise<void>
|
||||
|
||||
createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID, created: Date): Promise<void>
|
||||
removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise<void>
|
||||
|
||||
createAttachment(message: MessageID, attachment: CardID, creator: SocialID, created: Date): Promise<void>
|
||||
removeAttachment(message: MessageID, attachment: CardID): Promise<void>
|
||||
createFile(
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
blobId: BlobID,
|
||||
fileType: string,
|
||||
filename: string,
|
||||
size: number,
|
||||
creator: SocialID,
|
||||
created: Date
|
||||
): Promise<void>
|
||||
removeFile(card: CardID, message: MessageID, blobId: BlobID): Promise<void>
|
||||
|
||||
createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise<void>
|
||||
updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise<void>
|
||||
updateThread(thread: CardID, op: 'increment' | 'decrement', lastReply?: Date): Promise<void>
|
||||
|
||||
findMessages(params: FindMessagesParams): Promise<Message[]>
|
||||
findMessagesGroups(params: FindMessagesGroupsParams): Promise<MessagesGroup[]>
|
||||
findThread(thread: CardID): Promise<Thread | undefined>
|
||||
|
||||
createNotification(message: MessageID, context: ContextID): Promise<void>
|
||||
removeNotification(message: MessageID, context: ContextID): Promise<void>
|
||||
addCollaborators(card: CardID, collaborators: AccountID[], date?: Date): Promise<void>
|
||||
removeCollaborators(card: CardID, collaborators: AccountID[]): Promise<void>
|
||||
getCollaboratorsCursor(card: CardID, date: Date, size?: number): AsyncIterable<Collaborator[]>
|
||||
|
||||
createContext(personalWorkspace: WorkspaceID, card: CardID, lastView?: Date, lastUpdate?: Date): Promise<ContextID>
|
||||
updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void>
|
||||
removeContext(context: ContextID): Promise<void>
|
||||
findCollaborators(params: FindCollaboratorsParams): Promise<Collaborator[]>
|
||||
|
||||
findContexts(
|
||||
params: FindNotificationContextParams,
|
||||
personalWorkspaces: WorkspaceID[],
|
||||
workspace?: WorkspaceID
|
||||
): Promise<NotificationContext[]>
|
||||
findNotifications(
|
||||
params: FindNotificationsParams,
|
||||
personalWorkspace: WorkspaceID,
|
||||
workspace?: WorkspaceID
|
||||
): Promise<Notification[]>
|
||||
createNotification(context: ContextID, message: MessageID, created: Date): Promise<NotificationID>
|
||||
removeNotification(context: ContextID, account: AccountID, untilDate: Date): Promise<void>
|
||||
|
||||
createContext(account: AccountID, card: CardID, lastUpdate: Date, lastView: Date): Promise<ContextID>
|
||||
updateContext(context: ContextID, account: AccountID, lastUpdate?: Date, lastView?: Date): Promise<void>
|
||||
removeContext(context: ContextID, account: AccountID): Promise<void>
|
||||
|
||||
findContexts(params: FindNotificationContextParams): Promise<NotificationContext[]>
|
||||
findNotifications(params: FindNotificationsParams): Promise<Notification[]>
|
||||
|
||||
close(): void
|
||||
}
|
||||
|
||||
@@ -15,19 +15,6 @@
|
||||
|
||||
import type { Message, Window, Notification } from '@hcengineering/communication-types'
|
||||
|
||||
import type { Client } from './client'
|
||||
|
||||
export type QueryClient = Pick<
|
||||
Client,
|
||||
| 'onEvent'
|
||||
| 'findMessages'
|
||||
| 'findMessagesGroups'
|
||||
| 'findNotificationContexts'
|
||||
| 'findNotifications'
|
||||
| 'unsubscribeQuery'
|
||||
| 'close'
|
||||
>
|
||||
|
||||
export type QueryCallback<T> = (window: Window<T>) => void
|
||||
|
||||
export type MessagesQueryCallback = QueryCallback<Message>
|
||||
|
||||
@@ -17,95 +17,94 @@ import type {
|
||||
CardID,
|
||||
ContextID,
|
||||
MessageID,
|
||||
NotificationContextUpdate,
|
||||
RichText,
|
||||
SocialID,
|
||||
MessagesGroup,
|
||||
BlobID
|
||||
BlobID,
|
||||
AccountID,
|
||||
PatchType,
|
||||
MessageType,
|
||||
MessageData
|
||||
} from '@hcengineering/communication-types'
|
||||
|
||||
export enum RequestEventType {
|
||||
CreateMessage = 'createMessage',
|
||||
RemoveMessage = 'removeMessage',
|
||||
RemoveMessages = 'removeMessages',
|
||||
|
||||
CreatePatch = 'createPatch',
|
||||
RemovePatches = 'removePatches',
|
||||
|
||||
CreateReaction = 'createReaction',
|
||||
RemoveReaction = 'removeReaction',
|
||||
|
||||
CreateAttachment = 'createAttachment',
|
||||
RemoveAttachment = 'removeAttachment',
|
||||
CreateFile = 'createFile',
|
||||
RemoveFile = 'removeFile',
|
||||
|
||||
CreateThread = 'createThread',
|
||||
UpdateThread = 'updateThread',
|
||||
|
||||
CreateMessagesGroup = 'createMessagesGroup',
|
||||
RemoveMessagesGroup = 'removeMessagesGroup',
|
||||
|
||||
AddCollaborators = 'addCollaborators',
|
||||
RemoveCollaborators = 'removeCollaborators',
|
||||
|
||||
CreateNotification = 'createNotification',
|
||||
RemoveNotification = 'removeNotification',
|
||||
RemoveNotifications = 'removeNotifications',
|
||||
|
||||
CreateNotificationContext = 'createNotificationContext',
|
||||
RemoveNotificationContext = 'removeNotificationContext',
|
||||
UpdateNotificationContext = 'updateNotificationContext'
|
||||
}
|
||||
|
||||
export type RequestEvent =
|
||||
| CreateMessageEvent
|
||||
| RemoveMessageEvent
|
||||
| RemoveMessagesEvent
|
||||
| CreatePatchEvent
|
||||
| RemovePatchesEvent
|
||||
| CreateReactionEvent
|
||||
| RemoveReactionEvent
|
||||
| CreateAttachmentEvent
|
||||
| RemoveAttachmentEvent
|
||||
| CreateNotificationEvent
|
||||
| RemoveNotificationEvent
|
||||
| CreateNotificationContextEvent
|
||||
| RemoveNotificationContextEvent
|
||||
| UpdateNotificationContextEvent
|
||||
| CreateMessagesGroupEvent
|
||||
| RemoveMessagesGroupEvent
|
||||
| CreateThreadEvent
|
||||
type BaseRequestEvent = {
|
||||
_id?: string
|
||||
}
|
||||
|
||||
export interface CreateMessageEvent {
|
||||
export type RequestEvent =
|
||||
| AddCollaboratorsEvent
|
||||
| CreateFileEvent
|
||||
| CreateMessageEvent
|
||||
| CreateMessagesGroupEvent
|
||||
| CreateNotificationContextEvent
|
||||
| CreateNotificationEvent
|
||||
| CreatePatchEvent
|
||||
| CreateReactionEvent
|
||||
| CreateThreadEvent
|
||||
| RemoveFileEvent
|
||||
| RemoveCollaboratorsEvent
|
||||
| RemoveMessagesEvent
|
||||
| RemoveMessagesGroupEvent
|
||||
| RemoveNotificationContextEvent
|
||||
| RemoveNotificationsEvent
|
||||
| RemoveReactionEvent
|
||||
| UpdateNotificationContextEvent
|
||||
| UpdateThreadEvent
|
||||
|
||||
export interface CreateMessageEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.CreateMessage
|
||||
messageType: MessageType
|
||||
card: CardID
|
||||
content: RichText
|
||||
creator: SocialID
|
||||
data?: MessageData
|
||||
}
|
||||
|
||||
export interface RemoveMessageEvent {
|
||||
type: RequestEventType.RemoveMessage
|
||||
card: CardID
|
||||
message: MessageID
|
||||
}
|
||||
|
||||
export interface RemoveMessagesEvent {
|
||||
export interface RemoveMessagesEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.RemoveMessages
|
||||
card: CardID
|
||||
fromId: MessageID
|
||||
toId: MessageID
|
||||
messages: MessageID[]
|
||||
}
|
||||
|
||||
export interface CreatePatchEvent {
|
||||
export interface CreatePatchEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.CreatePatch
|
||||
patchType: PatchType
|
||||
card: CardID
|
||||
message: MessageID
|
||||
content: RichText
|
||||
creator: SocialID
|
||||
}
|
||||
|
||||
export interface RemovePatchesEvent {
|
||||
type: RequestEventType.RemovePatches
|
||||
card: CardID
|
||||
fromId: MessageID
|
||||
toId: MessageID
|
||||
}
|
||||
|
||||
export interface CreateReactionEvent {
|
||||
export interface CreateReactionEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.CreateReaction
|
||||
card: CardID
|
||||
message: MessageID
|
||||
@@ -113,7 +112,7 @@ export interface CreateReactionEvent {
|
||||
creator: SocialID
|
||||
}
|
||||
|
||||
export interface RemoveReactionEvent {
|
||||
export interface RemoveReactionEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.RemoveReaction
|
||||
card: CardID
|
||||
message: MessageID
|
||||
@@ -121,69 +120,100 @@ export interface RemoveReactionEvent {
|
||||
creator: SocialID
|
||||
}
|
||||
|
||||
export interface CreateAttachmentEvent {
|
||||
type: RequestEventType.CreateAttachment
|
||||
export interface CreateFileEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.CreateFile
|
||||
card: CardID
|
||||
message: MessageID
|
||||
attachment: CardID
|
||||
blobId: BlobID
|
||||
size: number
|
||||
fileType: string
|
||||
filename: string
|
||||
creator: SocialID
|
||||
}
|
||||
|
||||
export interface RemoveAttachmentEvent {
|
||||
type: RequestEventType.RemoveAttachment
|
||||
export interface RemoveFileEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.RemoveFile
|
||||
card: CardID
|
||||
message: MessageID
|
||||
attachment: CardID
|
||||
blobId: BlobID
|
||||
creator: SocialID
|
||||
}
|
||||
|
||||
export interface CreateThreadEvent {
|
||||
export interface CreateThreadEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.CreateThread
|
||||
card: CardID
|
||||
message: MessageID
|
||||
thread: CardID
|
||||
}
|
||||
|
||||
export interface CreateNotificationEvent {
|
||||
export interface UpdateThreadEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.UpdateThread
|
||||
thread: CardID
|
||||
replies: 'increment' | 'decrement'
|
||||
lastReply?: Date
|
||||
}
|
||||
|
||||
export interface CreateNotificationEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.CreateNotification
|
||||
message: MessageID
|
||||
context: ContextID
|
||||
message: MessageID
|
||||
created: Date
|
||||
account: AccountID
|
||||
}
|
||||
|
||||
export interface RemoveNotificationEvent {
|
||||
type: RequestEventType.RemoveNotification
|
||||
message: MessageID
|
||||
export interface RemoveNotificationsEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.RemoveNotifications
|
||||
context: ContextID
|
||||
account: AccountID
|
||||
untilDate: Date
|
||||
}
|
||||
|
||||
export interface CreateNotificationContextEvent {
|
||||
export interface CreateNotificationContextEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.CreateNotificationContext
|
||||
card: CardID
|
||||
account: AccountID
|
||||
lastView: Date
|
||||
lastUpdate: Date
|
||||
}
|
||||
|
||||
export interface RemoveNotificationContextEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.RemoveNotificationContext
|
||||
context: ContextID
|
||||
account: AccountID
|
||||
}
|
||||
|
||||
export interface UpdateNotificationContextEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.UpdateNotificationContext
|
||||
context: ContextID
|
||||
account: AccountID
|
||||
lastView?: Date
|
||||
lastUpdate?: Date
|
||||
}
|
||||
|
||||
export interface RemoveNotificationContextEvent {
|
||||
type: RequestEventType.RemoveNotificationContext
|
||||
context: ContextID
|
||||
}
|
||||
|
||||
export interface UpdateNotificationContextEvent {
|
||||
type: RequestEventType.UpdateNotificationContext
|
||||
context: ContextID
|
||||
update: NotificationContextUpdate
|
||||
}
|
||||
|
||||
export interface CreateMessagesGroupEvent {
|
||||
export interface CreateMessagesGroupEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.CreateMessagesGroup
|
||||
group: MessagesGroup
|
||||
}
|
||||
|
||||
export interface RemoveMessagesGroupEvent {
|
||||
export interface RemoveMessagesGroupEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.RemoveMessagesGroup
|
||||
card: CardID
|
||||
blobId: BlobID
|
||||
}
|
||||
|
||||
export interface AddCollaboratorsEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.AddCollaborators
|
||||
card: CardID
|
||||
collaborators: AccountID[]
|
||||
date?: Date
|
||||
}
|
||||
|
||||
export interface RemoveCollaboratorsEvent extends BaseRequestEvent {
|
||||
type: RequestEventType.RemoveCollaborators
|
||||
card: CardID
|
||||
collaborators: AccountID[]
|
||||
}
|
||||
|
||||
export type EventResult = CreateMessageResult | CreateNotificationContextResult | {}
|
||||
|
||||
export interface CreateMessageResult {
|
||||
|
||||
@@ -14,78 +14,97 @@
|
||||
//
|
||||
|
||||
import type {
|
||||
Attachment,
|
||||
File,
|
||||
CardID,
|
||||
ContextID,
|
||||
Message,
|
||||
MessageID,
|
||||
NotificationContext,
|
||||
NotificationContextUpdate,
|
||||
Patch,
|
||||
Reaction,
|
||||
SocialID,
|
||||
Notification,
|
||||
Thread,
|
||||
MessagesGroup
|
||||
MessagesGroup,
|
||||
AccountID,
|
||||
BlobID
|
||||
} from '@hcengineering/communication-types'
|
||||
|
||||
export enum ResponseEventType {
|
||||
MessageCreated = 'messageCreated',
|
||||
MessageRemoved = 'messageRemoved',
|
||||
MessagesRemoved = 'messagesRemoved',
|
||||
|
||||
PatchCreated = 'patchCreated',
|
||||
|
||||
ReactionCreated = 'reactionCreated',
|
||||
ReactionRemoved = 'reactionRemoved',
|
||||
AttachmentCreated = 'attachmentCreated',
|
||||
AttachmentRemoved = 'attachmentRemoved',
|
||||
|
||||
FileCreated = 'fileCreated',
|
||||
FileRemoved = 'fileRemoved',
|
||||
|
||||
ThreadCreated = 'threadCreated',
|
||||
ThreadUpdated = 'threadUpdated',
|
||||
|
||||
MessagesGroupCreated = 'messagesGroupCreated',
|
||||
|
||||
NotificationCreated = 'notificationCreated',
|
||||
NotificationRemoved = 'notificationRemoved',
|
||||
NotificationsRemoved = 'notificationsRemoved',
|
||||
|
||||
NotificationContextCreated = 'notificationContextCreated',
|
||||
NotificationContextRemoved = 'notificationContextRemoved',
|
||||
NotificationContextUpdated = 'notificationContextUpdated'
|
||||
NotificationContextUpdated = 'notificationContextUpdated',
|
||||
|
||||
AddedCollaborators = 'addedCollaborators',
|
||||
RemovedCollaborators = 'removedCollaborators'
|
||||
}
|
||||
|
||||
type BaseResponseEvent = {
|
||||
_id?: string
|
||||
}
|
||||
|
||||
export type ResponseEvent =
|
||||
| MessageCreatedEvent
|
||||
| MessageRemovedEvent
|
||||
| MessagesRemovedEvent
|
||||
| PatchCreatedEvent
|
||||
| ReactionCreatedEvent
|
||||
| ReactionRemovedEvent
|
||||
| AttachmentCreatedEvent
|
||||
| AttachmentRemovedEvent
|
||||
| FileCreatedEvent
|
||||
| FileRemovedEvent
|
||||
| NotificationCreatedEvent
|
||||
| NotificationRemovedEvent
|
||||
| NotificationsRemovedEvent
|
||||
| NotificationContextCreatedEvent
|
||||
| NotificationContextRemovedEvent
|
||||
| NotificationContextUpdatedEvent
|
||||
| ThreadCreatedEvent
|
||||
| MessagesGroupCreatedEvent
|
||||
| AddedCollaboratorsEvent
|
||||
| RemovedCollaboratorsEvent
|
||||
| ThreadUpdatedEvent
|
||||
|
||||
export interface MessageCreatedEvent {
|
||||
export interface MessageCreatedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.MessageCreated
|
||||
message: Message
|
||||
}
|
||||
|
||||
export interface MessageRemovedEvent {
|
||||
type: ResponseEventType.MessageRemoved
|
||||
export interface MessagesRemovedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.MessagesRemoved
|
||||
card: CardID
|
||||
message: MessageID
|
||||
messages: MessageID[]
|
||||
}
|
||||
|
||||
export interface PatchCreatedEvent {
|
||||
export interface PatchCreatedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.PatchCreated
|
||||
card: CardID
|
||||
patch: Patch
|
||||
}
|
||||
|
||||
export interface ReactionCreatedEvent {
|
||||
export interface ReactionCreatedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.ReactionCreated
|
||||
card: CardID
|
||||
reaction: Reaction
|
||||
}
|
||||
|
||||
export interface ReactionRemovedEvent {
|
||||
export interface ReactionRemovedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.ReactionRemoved
|
||||
card: CardID
|
||||
message: MessageID
|
||||
@@ -93,56 +112,77 @@ export interface ReactionRemovedEvent {
|
||||
creator: SocialID
|
||||
}
|
||||
|
||||
export interface AttachmentCreatedEvent {
|
||||
type: ResponseEventType.AttachmentCreated
|
||||
export interface FileCreatedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.FileCreated
|
||||
card: CardID
|
||||
attachment: Attachment
|
||||
file: File
|
||||
}
|
||||
|
||||
export interface AttachmentRemovedEvent {
|
||||
type: ResponseEventType.AttachmentRemoved
|
||||
export interface FileRemovedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.FileRemoved
|
||||
card: CardID
|
||||
message: MessageID
|
||||
attachment: CardID
|
||||
blobId: BlobID
|
||||
creator: SocialID
|
||||
}
|
||||
|
||||
export interface MessagesGroupCreatedEvent {
|
||||
export interface MessagesGroupCreatedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.MessagesGroupCreated
|
||||
group: MessagesGroup
|
||||
}
|
||||
|
||||
export interface ThreadCreatedEvent {
|
||||
export interface ThreadCreatedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.ThreadCreated
|
||||
thread: Thread
|
||||
}
|
||||
|
||||
export interface NotificationCreatedEvent {
|
||||
export interface NotificationCreatedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.NotificationCreated
|
||||
personalWorkspace: string
|
||||
notification: Notification
|
||||
account: AccountID
|
||||
}
|
||||
|
||||
export interface NotificationRemovedEvent {
|
||||
type: ResponseEventType.NotificationRemoved
|
||||
personalWorkspace: string
|
||||
message: MessageID
|
||||
export interface NotificationsRemovedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.NotificationsRemoved
|
||||
untilDate: Date
|
||||
context: ContextID
|
||||
account: AccountID
|
||||
}
|
||||
|
||||
export interface NotificationContextCreatedEvent {
|
||||
export interface NotificationContextCreatedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.NotificationContextCreated
|
||||
context: NotificationContext
|
||||
}
|
||||
|
||||
export interface NotificationContextRemovedEvent {
|
||||
export interface NotificationContextRemovedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.NotificationContextRemoved
|
||||
personalWorkspace: string
|
||||
context: ContextID
|
||||
account: AccountID
|
||||
}
|
||||
|
||||
export interface NotificationContextUpdatedEvent {
|
||||
export interface NotificationContextUpdatedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.NotificationContextUpdated
|
||||
personalWorkspace: string
|
||||
context: ContextID
|
||||
update: NotificationContextUpdate
|
||||
account: AccountID
|
||||
lastView?: Date
|
||||
lastUpdate?: Date
|
||||
}
|
||||
|
||||
export interface AddedCollaboratorsEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.AddedCollaborators
|
||||
card: CardID
|
||||
collaborators: AccountID[]
|
||||
}
|
||||
|
||||
export interface RemovedCollaboratorsEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.RemovedCollaborators
|
||||
card: CardID
|
||||
collaborators: AccountID[]
|
||||
}
|
||||
|
||||
export interface ThreadUpdatedEvent extends BaseResponseEvent {
|
||||
type: ResponseEventType.ThreadUpdated
|
||||
thread: CardID
|
||||
replies: 'increment' | 'decrement'
|
||||
lastReply?: Date
|
||||
}
|
||||
|
||||
@@ -16,21 +16,35 @@
|
||||
import type {
|
||||
FindMessagesGroupsParams,
|
||||
FindMessagesParams,
|
||||
FindNotificationContextParams,
|
||||
FindNotificationsParams,
|
||||
Message,
|
||||
MessagesGroup
|
||||
MessagesGroup,
|
||||
NotificationContext,
|
||||
Notification
|
||||
} from '@hcengineering/communication-types'
|
||||
import type { Account } from '@hcengineering/core'
|
||||
|
||||
import type { EventResult, RequestEvent } from './requestEvent.ts'
|
||||
|
||||
export interface ConnectionInfo {
|
||||
sessionId: string
|
||||
sessionId?: string
|
||||
account: Account
|
||||
}
|
||||
|
||||
export interface ServerApi {
|
||||
findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: number): Promise<Message[]>
|
||||
findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: number | string): Promise<Message[]>
|
||||
findMessagesGroups(info: ConnectionInfo, params: FindMessagesGroupsParams): Promise<MessagesGroup[]>
|
||||
findNotificationContexts(
|
||||
info: ConnectionInfo,
|
||||
params: FindNotificationContextParams,
|
||||
queryId?: number | string
|
||||
): Promise<NotificationContext[]>
|
||||
findNotifications(
|
||||
info: ConnectionInfo,
|
||||
params: FindNotificationsParams,
|
||||
queryId?: number | string
|
||||
): Promise<Notification[]>
|
||||
|
||||
event(info: ConnectionInfo, event: RequestEvent): Promise<EventResult>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/communication-server",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.147",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "./types/index.d.ts",
|
||||
@@ -20,11 +20,12 @@
|
||||
"@types/ws": "^8.5.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "0.7.15",
|
||||
"@hcengineering/server-token": "0.7.15",
|
||||
"@hcengineering/account-client": "^0.7.28",
|
||||
"@hcengineering/communication-cockroach": "workspace:*",
|
||||
"@hcengineering/communication-sdk-types": "workspace:*",
|
||||
"@hcengineering/communication-types": "workspace:*"
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"@hcengineering/core": "^0.7.28",
|
||||
"@hcengineering/server-token": "^0.7.28"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3"
|
||||
|
||||
@@ -13,73 +13,61 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Message, type Patch, type Reaction } from '@hcengineering/communication-types'
|
||||
import {
|
||||
type Message,
|
||||
type Patch,
|
||||
type Reaction,
|
||||
type Attachment,
|
||||
type SocialID,
|
||||
type WorkspaceID,
|
||||
PatchType
|
||||
} from '@hcengineering/communication-types'
|
||||
import {
|
||||
type CreateAttachmentEvent,
|
||||
type AttachmentCreatedEvent,
|
||||
type AddCollaboratorsEvent,
|
||||
type ConnectionInfo,
|
||||
type CreateFileEvent,
|
||||
type CreateMessageEvent,
|
||||
type MessageCreatedEvent,
|
||||
type CreatePatchEvent,
|
||||
type PatchCreatedEvent,
|
||||
type CreateReactionEvent,
|
||||
type ReactionCreatedEvent,
|
||||
type RemoveAttachmentEvent,
|
||||
type AttachmentRemovedEvent,
|
||||
type RemoveMessageEvent,
|
||||
type MessageRemovedEvent,
|
||||
type RemoveReactionEvent,
|
||||
type ReactionRemovedEvent,
|
||||
type EventResult,
|
||||
type DbAdapter,
|
||||
type CreateNotificationEvent,
|
||||
type RemoveNotificationEvent,
|
||||
type CreateMessagesGroupEvent,
|
||||
type CreateNotificationContextEvent,
|
||||
type RemoveNotificationContextEvent,
|
||||
type UpdateNotificationContextEvent,
|
||||
type NotificationRemovedEvent,
|
||||
type CreateNotificationEvent,
|
||||
type CreatePatchEvent,
|
||||
type CreateReactionEvent,
|
||||
type CreateThreadEvent,
|
||||
type DbAdapter,
|
||||
type EventResult,
|
||||
type FileCreatedEvent,
|
||||
type FileRemovedEvent,
|
||||
type MessageCreatedEvent,
|
||||
type MessagesGroupCreatedEvent,
|
||||
type MessagesRemovedEvent,
|
||||
type NotificationContextCreatedEvent,
|
||||
type NotificationContextRemovedEvent,
|
||||
type NotificationContextUpdatedEvent,
|
||||
type ResponseEvent,
|
||||
RequestEventType,
|
||||
type RequestEvent,
|
||||
ResponseEventType,
|
||||
type CreateMessagesGroupEvent,
|
||||
type NotificationsRemovedEvent,
|
||||
type PatchCreatedEvent,
|
||||
type ReactionCreatedEvent,
|
||||
type ReactionRemovedEvent,
|
||||
type RemoveCollaboratorsEvent,
|
||||
type RemoveFileEvent,
|
||||
type RemoveMessagesEvent,
|
||||
type ThreadCreatedEvent,
|
||||
type CreateThreadEvent,
|
||||
type ConnectionInfo,
|
||||
type RemovePatchesEvent,
|
||||
type RemoveMessagesGroupEvent,
|
||||
type MessagesGroupCreatedEvent
|
||||
type RemoveNotificationContextEvent,
|
||||
type RemoveNotificationsEvent,
|
||||
type RemoveReactionEvent,
|
||||
type RequestEvent,
|
||||
RequestEventType,
|
||||
type ResponseEvent,
|
||||
ResponseEventType,
|
||||
type ThreadCreatedEvent,
|
||||
type UpdateNotificationContextEvent,
|
||||
type UpdateThreadEvent
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
import { systemAccountUuid, type Account } from '@hcengineering/core'
|
||||
import { systemAccountUuid } from '@hcengineering/core'
|
||||
|
||||
export interface Result {
|
||||
responseEvent?: ResponseEvent
|
||||
result: EventResult
|
||||
result?: EventResult
|
||||
}
|
||||
|
||||
export class EventProcessor {
|
||||
constructor(
|
||||
private readonly db: DbAdapter,
|
||||
private readonly workspace: WorkspaceID
|
||||
) {}
|
||||
constructor(private readonly db: DbAdapter) {}
|
||||
|
||||
async process(info: ConnectionInfo, event: RequestEvent): Promise<Result> {
|
||||
switch (event.type) {
|
||||
case RequestEventType.CreateMessage:
|
||||
return await this.createMessage(event, info)
|
||||
case RequestEventType.RemoveMessage:
|
||||
return await this.removeMessage(event, info)
|
||||
case RequestEventType.RemoveMessages:
|
||||
return await this.removeMessages(event, info)
|
||||
case RequestEventType.CreatePatch:
|
||||
@@ -88,14 +76,14 @@ export class EventProcessor {
|
||||
return await this.createReaction(event, info)
|
||||
case RequestEventType.RemoveReaction:
|
||||
return await this.removeReaction(event, info)
|
||||
case RequestEventType.CreateAttachment:
|
||||
return await this.createAttachment(event, info)
|
||||
case RequestEventType.RemoveAttachment:
|
||||
return await this.removeAttachment(event, info)
|
||||
case RequestEventType.CreateFile:
|
||||
return await this.createFile(event, info)
|
||||
case RequestEventType.RemoveFile:
|
||||
return await this.removeFile(event, info)
|
||||
case RequestEventType.CreateNotification:
|
||||
return await this.createNotification(event, info)
|
||||
case RequestEventType.RemoveNotification:
|
||||
return await this.removeNotification(event, info)
|
||||
case RequestEventType.RemoveNotifications:
|
||||
return await this.removeNotifications(event, info)
|
||||
case RequestEventType.CreateNotificationContext:
|
||||
return await this.createNotificationContext(event, info)
|
||||
case RequestEventType.RemoveNotificationContext:
|
||||
@@ -106,28 +94,82 @@ export class EventProcessor {
|
||||
return await this.createMessagesGroup(event, info)
|
||||
case RequestEventType.CreateThread:
|
||||
return await this.createThread(event, info)
|
||||
case RequestEventType.RemovePatches:
|
||||
return await this.removePatches(event, info)
|
||||
case RequestEventType.RemoveMessagesGroup:
|
||||
return await this.removeMessagesGroup(event, info)
|
||||
case RequestEventType.AddCollaborators:
|
||||
return await this.addCollaborators(event, info)
|
||||
case RequestEventType.RemoveCollaborators:
|
||||
return await this.removeCollaborators(event, info)
|
||||
case RequestEventType.UpdateThread:
|
||||
return await this.updateThread(event, info)
|
||||
}
|
||||
}
|
||||
|
||||
private async createMessage(event: CreateMessageEvent, info: ConnectionInfo): Promise<Result> {
|
||||
this.checkCreator(info.account, event.creator)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async updateThread(event: UpdateThreadEvent, _: ConnectionInfo): Promise<Result> {
|
||||
await this.db.updateThread(event.thread, event.replies, event.lastReply)
|
||||
return {
|
||||
responseEvent: {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.ThreadUpdated,
|
||||
thread: event.thread,
|
||||
replies: event.replies,
|
||||
lastReply: event.lastReply
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async addCollaborators(event: AddCollaboratorsEvent, _: ConnectionInfo): Promise<Result> {
|
||||
await this.db.addCollaborators(event.card, event.collaborators, event.date)
|
||||
return {
|
||||
responseEvent: {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.AddedCollaborators,
|
||||
card: event.card,
|
||||
collaborators: event.collaborators
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async removeCollaborators(event: RemoveCollaboratorsEvent, _: ConnectionInfo): Promise<Result> {
|
||||
await this.db.removeCollaborators(event.card, event.collaborators)
|
||||
|
||||
return {
|
||||
responseEvent: {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.RemovedCollaborators,
|
||||
card: event.card,
|
||||
collaborators: event.collaborators
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async createMessage(event: CreateMessageEvent, _: ConnectionInfo): Promise<Result> {
|
||||
const created = new Date()
|
||||
const id = await this.db.createMessage(event.card, event.content, event.creator, created)
|
||||
const id = await this.db.createMessage(
|
||||
event.card,
|
||||
event.messageType,
|
||||
event.content,
|
||||
event.creator,
|
||||
created,
|
||||
event.data
|
||||
)
|
||||
const message: Message = {
|
||||
id,
|
||||
type: event.messageType,
|
||||
card: event.card,
|
||||
content: event.content,
|
||||
creator: event.creator,
|
||||
created,
|
||||
data: event.data,
|
||||
reactions: [],
|
||||
attachments: []
|
||||
files: []
|
||||
}
|
||||
const responseEvent: MessageCreatedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.MessageCreated,
|
||||
message
|
||||
}
|
||||
@@ -137,69 +179,48 @@ export class EventProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private async createPatch(event: CreatePatchEvent, info: ConnectionInfo): Promise<Result> {
|
||||
this.checkCreator(info.account, event.creator)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async createPatch(event: CreatePatchEvent, _: ConnectionInfo): Promise<Result> {
|
||||
const created = new Date()
|
||||
await this.db.createPatch(event.card, event.message, PatchType.update, event.content, event.creator, created)
|
||||
await this.db.createPatch(event.card, event.message, event.patchType, event.content, event.creator, created)
|
||||
|
||||
const patch: Patch = {
|
||||
type: PatchType.update,
|
||||
type: event.patchType,
|
||||
message: event.message,
|
||||
content: event.content,
|
||||
creator: event.creator,
|
||||
created
|
||||
}
|
||||
const responseEvent: PatchCreatedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.PatchCreated,
|
||||
card: event.card,
|
||||
patch
|
||||
}
|
||||
return {
|
||||
responseEvent,
|
||||
result: {}
|
||||
}
|
||||
}
|
||||
|
||||
private async removeMessage(event: RemoveMessageEvent, info: ConnectionInfo): Promise<Result> {
|
||||
const socialIds = systemAccountUuid === info.account.uuid ? undefined : info.account.socialIds
|
||||
await this.db.removeMessage(event.card, event.message, socialIds)
|
||||
|
||||
const responseEvent: MessageRemovedEvent = {
|
||||
type: ResponseEventType.MessageRemoved,
|
||||
card: event.card,
|
||||
message: event.message
|
||||
}
|
||||
|
||||
return {
|
||||
responseEvent,
|
||||
result: {}
|
||||
responseEvent
|
||||
}
|
||||
}
|
||||
|
||||
private async removeMessages(event: RemoveMessagesEvent, info: ConnectionInfo): Promise<Result> {
|
||||
if (systemAccountUuid !== info.account.uuid) {
|
||||
throw new Error('Forbidden')
|
||||
const socialIds = systemAccountUuid === info.account.uuid ? undefined : info.account.socialIds
|
||||
const deleted = await this.db.removeMessages(event.card, event.messages, socialIds)
|
||||
|
||||
const responseEvent: MessagesRemovedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.MessagesRemoved,
|
||||
card: event.card,
|
||||
messages: deleted
|
||||
}
|
||||
await this.db.removeMessages(event.card, event.fromId, event.toId)
|
||||
|
||||
return {
|
||||
result: {}
|
||||
responseEvent,
|
||||
result: { messages: deleted }
|
||||
}
|
||||
}
|
||||
|
||||
private async removePatches(event: RemovePatchesEvent, info: ConnectionInfo): Promise<Result> {
|
||||
if (systemAccountUuid !== info.account.uuid) {
|
||||
throw new Error('Forbidden')
|
||||
}
|
||||
await this.db.removePatches(event.card, event.fromId, event.toId)
|
||||
|
||||
return {
|
||||
result: {}
|
||||
}
|
||||
}
|
||||
|
||||
private async createReaction(event: CreateReactionEvent, info: ConnectionInfo): Promise<Result> {
|
||||
this.checkCreator(info.account, event.creator)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async createReaction(event: CreateReactionEvent, _: ConnectionInfo): Promise<Result> {
|
||||
const created = new Date()
|
||||
await this.db.createReaction(event.card, event.message, event.reaction, event.creator, created)
|
||||
|
||||
@@ -210,20 +231,21 @@ export class EventProcessor {
|
||||
created
|
||||
}
|
||||
const responseEvent: ReactionCreatedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.ReactionCreated,
|
||||
card: event.card,
|
||||
reaction
|
||||
}
|
||||
return {
|
||||
responseEvent,
|
||||
result: {}
|
||||
responseEvent
|
||||
}
|
||||
}
|
||||
|
||||
private async removeReaction(event: RemoveReactionEvent, info: ConnectionInfo): Promise<Result> {
|
||||
this.checkCreator(info.account, event.creator)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async removeReaction(event: RemoveReactionEvent, _: ConnectionInfo): Promise<Result> {
|
||||
await this.db.removeReaction(event.card, event.message, event.reaction, event.creator)
|
||||
const responseEvent: ReactionRemovedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.ReactionRemoved,
|
||||
card: event.card,
|
||||
message: event.message,
|
||||
@@ -231,90 +253,104 @@ export class EventProcessor {
|
||||
creator: event.creator
|
||||
}
|
||||
return {
|
||||
responseEvent,
|
||||
result: {}
|
||||
}
|
||||
}
|
||||
|
||||
private async createAttachment(event: CreateAttachmentEvent, info: ConnectionInfo): Promise<Result> {
|
||||
this.checkCreator(info.account, event.creator)
|
||||
const created = new Date()
|
||||
await this.db.createAttachment(event.message, event.card, event.creator, created)
|
||||
|
||||
const attachment: Attachment = {
|
||||
message: event.message,
|
||||
card: event.card,
|
||||
creator: event.creator,
|
||||
created
|
||||
}
|
||||
const responseEvent: AttachmentCreatedEvent = {
|
||||
type: ResponseEventType.AttachmentCreated,
|
||||
card: event.card,
|
||||
attachment
|
||||
}
|
||||
|
||||
return {
|
||||
responseEvent,
|
||||
result: {}
|
||||
responseEvent
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async removeAttachment(event: RemoveAttachmentEvent, _: ConnectionInfo): Promise<Result> {
|
||||
await this.db.removeAttachment(event.message, event.card)
|
||||
const responseEvent: AttachmentRemovedEvent = {
|
||||
type: ResponseEventType.AttachmentRemoved,
|
||||
private async createFile(event: CreateFileEvent, _: ConnectionInfo): Promise<Result> {
|
||||
const created = new Date()
|
||||
await this.db.createFile(
|
||||
event.card,
|
||||
event.message,
|
||||
event.blobId,
|
||||
event.fileType,
|
||||
event.filename,
|
||||
event.size,
|
||||
event.creator,
|
||||
created
|
||||
)
|
||||
const responseEvent: FileCreatedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.FileCreated,
|
||||
card: event.card,
|
||||
message: event.message,
|
||||
attachment: event.attachment
|
||||
file: {
|
||||
card: event.card,
|
||||
message: event.message,
|
||||
blobId: event.blobId,
|
||||
type: event.fileType,
|
||||
filename: event.filename,
|
||||
size: event.size,
|
||||
creator: event.creator,
|
||||
created
|
||||
}
|
||||
}
|
||||
return {
|
||||
responseEvent,
|
||||
result: {}
|
||||
responseEvent
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async removeFile(event: RemoveFileEvent, _: ConnectionInfo): Promise<Result> {
|
||||
await this.db.removeFile(event.card, event.message, event.blobId)
|
||||
const responseEvent: FileRemovedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.FileRemoved,
|
||||
card: event.card,
|
||||
message: event.message,
|
||||
blobId: event.blobId,
|
||||
creator: event.creator
|
||||
}
|
||||
return {
|
||||
responseEvent
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async createNotification(event: CreateNotificationEvent, _: ConnectionInfo): Promise<Result> {
|
||||
await this.db.createNotification(event.message, event.context)
|
||||
const id = await this.db.createNotification(event.context, event.message, event.created)
|
||||
|
||||
return {
|
||||
result: {}
|
||||
responseEvent: {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.NotificationCreated,
|
||||
notification: {
|
||||
id,
|
||||
context: event.context,
|
||||
messageId: event.message,
|
||||
read: false,
|
||||
created: event.created
|
||||
},
|
||||
account: event.account
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async removeNotification(event: RemoveNotificationEvent, info: ConnectionInfo): Promise<Result> {
|
||||
await this.db.removeNotification(event.message, event.context)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async removeNotifications(event: RemoveNotificationsEvent, _: ConnectionInfo): Promise<Result> {
|
||||
await this.db.removeNotification(event.context, event.account, event.untilDate)
|
||||
|
||||
const responseEvent: NotificationRemovedEvent = {
|
||||
type: ResponseEventType.NotificationRemoved,
|
||||
// personalWorkspace: info.personalWorkspace,
|
||||
// TODO: add personal workspace
|
||||
personalWorkspace: '' as WorkspaceID,
|
||||
message: event.message,
|
||||
context: event.context
|
||||
const responseEvent: NotificationsRemovedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.NotificationsRemoved,
|
||||
context: event.context,
|
||||
account: event.account,
|
||||
untilDate: event.untilDate
|
||||
}
|
||||
return {
|
||||
responseEvent,
|
||||
result: {}
|
||||
responseEvent
|
||||
}
|
||||
}
|
||||
|
||||
private async createNotificationContext(
|
||||
event: CreateNotificationContextEvent,
|
||||
//eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
info: ConnectionInfo
|
||||
): Promise<Result> {
|
||||
// TODO: add personal workspace
|
||||
const personalWorkspace = '' as WorkspaceID
|
||||
const id = await this.db.createContext(personalWorkspace, event.card, event.lastView, event.lastUpdate)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async createNotificationContext(event: CreateNotificationContextEvent, _: ConnectionInfo): Promise<Result> {
|
||||
const id = await this.db.createContext(event.account, event.card, event.lastUpdate, event.lastView)
|
||||
const responseEvent: NotificationContextCreatedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.NotificationContextCreated,
|
||||
context: {
|
||||
id,
|
||||
workspace: this.workspace,
|
||||
personalWorkspace,
|
||||
account: event.account,
|
||||
card: event.card,
|
||||
lastView: event.lastView,
|
||||
lastUpdate: event.lastUpdate
|
||||
@@ -329,75 +365,63 @@ export class EventProcessor {
|
||||
private async removeNotificationContext(
|
||||
event: RemoveNotificationContextEvent,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
info: ConnectionInfo
|
||||
_: ConnectionInfo
|
||||
): Promise<Result> {
|
||||
await this.db.removeContext(event.context)
|
||||
await this.db.removeContext(event.context, event.account)
|
||||
const responseEvent: NotificationContextRemovedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.NotificationContextRemoved,
|
||||
// personalWorkspace: info.personalWorkspace,
|
||||
// TODO: add personal workspace
|
||||
personalWorkspace: '' as WorkspaceID,
|
||||
context: event.context
|
||||
context: event.context,
|
||||
account: event.account
|
||||
}
|
||||
return {
|
||||
responseEvent,
|
||||
result: {}
|
||||
responseEvent
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async updateNotificationContext(event: UpdateNotificationContextEvent, info: ConnectionInfo): Promise<Result> {
|
||||
await this.db.updateContext(event.context, event.update)
|
||||
async updateNotificationContext(event: UpdateNotificationContextEvent, _: ConnectionInfo): Promise<Result> {
|
||||
await this.db.updateContext(event.context, event.account, event.lastUpdate, event.lastView)
|
||||
|
||||
const responseEvent: NotificationContextUpdatedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.NotificationContextUpdated,
|
||||
// personalWorkspace: info.personalWorkspace,
|
||||
// TODO: add personal workspace
|
||||
personalWorkspace: '' as WorkspaceID,
|
||||
context: event.context,
|
||||
update: event.update
|
||||
account: event.account,
|
||||
lastView: event.lastView,
|
||||
lastUpdate: event.lastUpdate
|
||||
}
|
||||
return {
|
||||
responseEvent,
|
||||
result: {}
|
||||
responseEvent
|
||||
}
|
||||
}
|
||||
|
||||
async createMessagesGroup(event: CreateMessagesGroupEvent, info: ConnectionInfo): Promise<Result> {
|
||||
if (systemAccountUuid !== info.account.uuid) {
|
||||
throw new Error('Forbidden')
|
||||
}
|
||||
const { fromDate, toDate, count, fromId, toId, card, blobId } = event.group
|
||||
await this.db.createMessagesGroup(card, blobId, fromDate, toDate, fromId, toId, count)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async createMessagesGroup(event: CreateMessagesGroupEvent, _: ConnectionInfo): Promise<Result> {
|
||||
const { fromSec, toSec, count, card, blobId } = event.group
|
||||
await this.db.createMessagesGroup(card, blobId, fromSec, toSec, count)
|
||||
|
||||
const responseEvent: MessagesGroupCreatedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.MessagesGroupCreated,
|
||||
group: {
|
||||
card,
|
||||
blobId,
|
||||
fromDate,
|
||||
toDate,
|
||||
fromId,
|
||||
toId,
|
||||
fromSec,
|
||||
toSec,
|
||||
count
|
||||
}
|
||||
}
|
||||
return {
|
||||
responseEvent,
|
||||
result: {}
|
||||
responseEvent
|
||||
}
|
||||
}
|
||||
|
||||
async removeMessagesGroup(event: RemoveMessagesGroupEvent, info: ConnectionInfo): Promise<Result> {
|
||||
if (systemAccountUuid !== info.account.uuid) {
|
||||
throw new Error('Forbidden')
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async removeMessagesGroup(event: RemoveMessagesGroupEvent, _: ConnectionInfo): Promise<Result> {
|
||||
await this.db.removeMessagesGroup(event.card, event.blobId)
|
||||
|
||||
return {
|
||||
responseEvent: undefined,
|
||||
result: {}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -405,6 +429,7 @@ export class EventProcessor {
|
||||
const date = new Date()
|
||||
await this.db.createThread(event.card, event.message, event.thread, date)
|
||||
const responseEvent: ThreadCreatedEvent = {
|
||||
_id: event._id,
|
||||
type: ResponseEventType.ThreadCreated,
|
||||
thread: {
|
||||
card: event.card,
|
||||
@@ -415,14 +440,7 @@ export class EventProcessor {
|
||||
}
|
||||
}
|
||||
return {
|
||||
responseEvent,
|
||||
result: {}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCreator(account: Account, creator: SocialID): void {
|
||||
if (!account.socialIds.includes(creator) && systemAccountUuid !== account.uuid) {
|
||||
throw new Error('Forbidden')
|
||||
responseEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,17 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { MeasureContext } from '@hcengineering/core'
|
||||
import { type MeasureContext, systemAccountUuid } from '@hcengineering/core'
|
||||
import type {
|
||||
FindMessagesGroupsParams,
|
||||
FindMessagesParams,
|
||||
FindNotificationContextParams,
|
||||
FindNotificationsParams,
|
||||
Message,
|
||||
MessagesGroup,
|
||||
WorkspaceID
|
||||
NotificationContext,
|
||||
WorkspaceID,
|
||||
Notification
|
||||
} from '@hcengineering/communication-types'
|
||||
import { createDbAdapter } from '@hcengineering/communication-cockroach'
|
||||
import type {
|
||||
@@ -30,8 +34,9 @@ import type {
|
||||
ServerApi
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
|
||||
import { Manager, type BroadcastSessionsFunc } from './manager'
|
||||
import { type BroadcastSessionsFunc, Manager } from './manager'
|
||||
import { getMetadata, type Metadata } from './metadata'
|
||||
import type { QueryId } from './types'
|
||||
|
||||
export class Api implements ServerApi {
|
||||
private readonly manager: Manager
|
||||
@@ -52,12 +57,14 @@ export class Api implements ServerApi {
|
||||
dbUrl: string,
|
||||
broadcast: BroadcastSessionsFunc
|
||||
): Promise<Api> {
|
||||
const db = await createDbAdapter(dbUrl, workspace, ctx, { withLogs: true })
|
||||
const db = await createDbAdapter(dbUrl, workspace, ctx, {
|
||||
withLogs: process.env.COMMUNICATION_TIME_LOGGING_ENABLED === 'true'
|
||||
})
|
||||
const metadata = getMetadata()
|
||||
return new Api(ctx, metadata, workspace, db, broadcast)
|
||||
}
|
||||
|
||||
async findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: number): Promise<Message[]> {
|
||||
async findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: QueryId): Promise<Message[]> {
|
||||
return await this.manager.findMessages(info, params, queryId)
|
||||
}
|
||||
|
||||
@@ -65,6 +72,62 @@ export class Api implements ServerApi {
|
||||
return await this.manager.findMessagesGroups(info, params)
|
||||
}
|
||||
|
||||
async findNotificationContexts(
|
||||
info: ConnectionInfo,
|
||||
params: FindNotificationContextParams,
|
||||
queryId?: QueryId
|
||||
): Promise<NotificationContext[]> {
|
||||
const isSystem = info.account.uuid === systemAccountUuid
|
||||
|
||||
if (isSystem) {
|
||||
return await this.manager.findNotificationContexts(info, params, queryId)
|
||||
}
|
||||
|
||||
const accounts = params.account == null || Array.isArray(params.account) ? params.account : [params.account]
|
||||
const withMe = accounts == null || accounts.includes(info.account.uuid)
|
||||
|
||||
if (withMe) {
|
||||
return await this.manager.findNotificationContexts(
|
||||
info,
|
||||
{
|
||||
...params,
|
||||
account: info.account.uuid
|
||||
},
|
||||
queryId
|
||||
)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async findNotifications(
|
||||
info: ConnectionInfo,
|
||||
params: FindNotificationsParams,
|
||||
queryId?: QueryId
|
||||
): Promise<Notification[]> {
|
||||
const isSystem = info.account.uuid === systemAccountUuid
|
||||
|
||||
if (isSystem) {
|
||||
return await this.manager.findNotifications(info, params, queryId)
|
||||
}
|
||||
|
||||
const accounts = params.account == null || Array.isArray(params.account) ? params.account : [params.account]
|
||||
const withMe = accounts == null || accounts.includes(info.account.uuid)
|
||||
|
||||
if (withMe) {
|
||||
return await this.manager.findNotifications(
|
||||
info,
|
||||
{
|
||||
...params,
|
||||
account: info.account.uuid
|
||||
},
|
||||
queryId
|
||||
)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async unsubscribeQuery(info: ConnectionInfo, id: number): Promise<void> {
|
||||
this.manager.unsubscribeQuery(info, id)
|
||||
}
|
||||
|
||||
+144
-101
@@ -17,13 +17,13 @@ import {
|
||||
type ConnectionInfo,
|
||||
type DbAdapter,
|
||||
type EventResult,
|
||||
type NotificationContextCreatedEvent,
|
||||
type NotificationCreatedEvent,
|
||||
type RequestEvent,
|
||||
type ResponseEvent,
|
||||
ResponseEventType
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
import type {
|
||||
AccountID,
|
||||
CardID,
|
||||
FindMessagesGroupsParams,
|
||||
FindMessagesParams,
|
||||
FindNotificationContextParams,
|
||||
@@ -31,29 +31,32 @@ import type {
|
||||
Message,
|
||||
MessageID,
|
||||
MessagesGroup,
|
||||
WorkspaceID
|
||||
NotificationContext,
|
||||
WorkspaceID,
|
||||
Notification
|
||||
} from '@hcengineering/communication-types'
|
||||
import type { MeasureContext } from '@hcengineering/core'
|
||||
|
||||
import { Triggers } from './triggers'
|
||||
import { Permissions } from './permissions'
|
||||
import { EventProcessor } from './eventProcessor'
|
||||
import type { Metadata } from './metadata.ts'
|
||||
|
||||
type QueryId = number | string
|
||||
type QueryType = 'message' | 'notification' | 'context'
|
||||
|
||||
export type BroadcastSessionsFunc = (ctx: MeasureContext, sessionIds: string[], result: any) => void
|
||||
|
||||
interface SessionInfo {
|
||||
account: AccountID
|
||||
messageQueries: Map<QueryId, FindMessagesParams>
|
||||
notificationQueries: Map<QueryId, FindNotificationsParams>
|
||||
contextQueries: Map<QueryId, FindNotificationContextParams>
|
||||
contextQueries: Map<QueryId, Set<CardID>>
|
||||
}
|
||||
|
||||
export class Manager {
|
||||
private readonly dataBySessionId = new Map<string, SessionInfo>()
|
||||
private readonly triggers: Triggers
|
||||
private readonly eventProcessor: EventProcessor
|
||||
private readonly permissions: Permissions = new Permissions()
|
||||
|
||||
constructor(
|
||||
private readonly ctx: MeasureContext,
|
||||
@@ -62,58 +65,98 @@ export class Manager {
|
||||
private readonly workspace: WorkspaceID,
|
||||
private readonly broadcast: BroadcastSessionsFunc
|
||||
) {
|
||||
this.eventProcessor = new EventProcessor(db, this.workspace)
|
||||
this.triggers = new Triggers(this.metadata, db, this.workspace)
|
||||
this.eventProcessor = new EventProcessor(db)
|
||||
this.triggers = new Triggers(ctx.newChild('triggers', {}), this.metadata, db, this.workspace)
|
||||
}
|
||||
|
||||
async findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: number): Promise<Message[]> {
|
||||
async findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: QueryId): Promise<Message[]> {
|
||||
this.createSession(info)
|
||||
const result = await this.db.findMessages(params)
|
||||
if (queryId != null && info.sessionId != null && info.sessionId !== '') {
|
||||
this.subscribeQuery(info, 'message', queryId, params)
|
||||
this.subscribeMessageQuery(info, queryId, params)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async findMessagesGroups(_: ConnectionInfo, params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
|
||||
async findMessagesGroups(info: ConnectionInfo, params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
|
||||
this.createSession(info)
|
||||
return await this.db.findMessagesGroups(params)
|
||||
}
|
||||
|
||||
async findNotificationContexts(
|
||||
info: ConnectionInfo,
|
||||
params: FindNotificationContextParams,
|
||||
queryId?: QueryId
|
||||
): Promise<NotificationContext[]> {
|
||||
this.createSession(info)
|
||||
|
||||
const result = await this.db.findContexts(params)
|
||||
if (queryId != null && info.sessionId != null && info.sessionId !== '') {
|
||||
this.subscribeContextQuery(info, queryId, params, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async findNotifications(info: ConnectionInfo, params: FindNotificationsParams, _?: QueryId): Promise<Notification[]> {
|
||||
this.createSession(info)
|
||||
return await this.db.findNotifications(params)
|
||||
}
|
||||
|
||||
async event(info: ConnectionInfo, event: RequestEvent): Promise<EventResult> {
|
||||
this.permissions.validate(info, event)
|
||||
this.createSession(info)
|
||||
const eventResult = await this.eventProcessor.process(info, event)
|
||||
|
||||
const { result, responseEvent } = eventResult
|
||||
if (responseEvent !== undefined) {
|
||||
void this.next(info, responseEvent)
|
||||
}
|
||||
return result
|
||||
return result ?? {}
|
||||
}
|
||||
|
||||
subscribeQuery(info: ConnectionInfo, type: QueryType, queryId: number, params: Record<string, any>): void {
|
||||
const { sessionId } = info
|
||||
const data = this.dataBySessionId.get(sessionId) ?? {
|
||||
messageQueries: new Map(),
|
||||
notificationQueries: new Map(),
|
||||
contextQueries: new Map()
|
||||
}
|
||||
if (!this.dataBySessionId.has(sessionId)) {
|
||||
this.dataBySessionId.set(sessionId, data)
|
||||
private subscribeMessageQuery(info: ConnectionInfo, queryId: QueryId, params: Record<string, any>): void {
|
||||
const data = this.createSession(info)
|
||||
if (data == null) return
|
||||
|
||||
data.messageQueries.set(queryId, params as FindMessagesParams)
|
||||
}
|
||||
|
||||
private subscribeContextQuery(
|
||||
info: ConnectionInfo,
|
||||
queryId: QueryId,
|
||||
params: FindNotificationContextParams,
|
||||
result: NotificationContext[]
|
||||
): void {
|
||||
if (params.notifications == null) return
|
||||
const data = this.createSession(info)
|
||||
if (data == null) return
|
||||
|
||||
const cards = new Set(result.map((it) => it.card))
|
||||
const current = data.contextQueries.get(queryId) ?? new Set()
|
||||
|
||||
data.contextQueries.set(queryId, new Set([...current, ...cards]))
|
||||
}
|
||||
|
||||
private createSession(info: ConnectionInfo): SessionInfo | undefined {
|
||||
if (info.sessionId == null) return
|
||||
if (!this.dataBySessionId.has(info.sessionId)) {
|
||||
this.dataBySessionId.set(info.sessionId, {
|
||||
account: info.account.uuid,
|
||||
messageQueries: new Map(),
|
||||
contextQueries: new Map()
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'message') {
|
||||
data.messageQueries.set(queryId, params as FindMessagesParams)
|
||||
} else if (type === 'notification') {
|
||||
data.notificationQueries.set(queryId, params)
|
||||
} else if (type === 'context') {
|
||||
data.contextQueries.set(queryId, params)
|
||||
}
|
||||
return this.dataBySessionId.get(info.sessionId)
|
||||
}
|
||||
|
||||
unsubscribeQuery(info: ConnectionInfo, queryId: number): void {
|
||||
if (info.sessionId == null) return
|
||||
const data = this.dataBySessionId.get(info.sessionId)
|
||||
if (data == null) return
|
||||
|
||||
data.messageQueries.delete(queryId)
|
||||
data.notificationQueries.delete(queryId)
|
||||
data.contextQueries.delete(queryId)
|
||||
}
|
||||
|
||||
@@ -121,13 +164,35 @@ export class Manager {
|
||||
this.dataBySessionId.delete(sessionId)
|
||||
}
|
||||
|
||||
async next(info: ConnectionInfo, event: ResponseEvent): Promise<void> {
|
||||
await this.responseEvent(event)
|
||||
const derived = await this.triggers.process(event, info)
|
||||
close(): void {
|
||||
this.db.close()
|
||||
}
|
||||
|
||||
private async apply(info: ConnectionInfo, derivedRequests: RequestEvent[]): Promise<void> {
|
||||
const result = await Promise.all(derivedRequests.map((it) => this.eventProcessor.process(info, it)))
|
||||
const derived = result.flatMap((it) => it.responseEvent).filter((it): it is ResponseEvent => it !== undefined)
|
||||
await Promise.all(derived.map((it) => this.next(info, it)))
|
||||
}
|
||||
|
||||
private async responseEvent(event: ResponseEvent): Promise<void> {
|
||||
private async execute(info: ConnectionInfo, request: RequestEvent): Promise<EventResult> {
|
||||
const result = await this.eventProcessor.process(info, request)
|
||||
if (result.responseEvent != null) {
|
||||
void this.next(info, result.responseEvent)
|
||||
}
|
||||
return result.result ?? {}
|
||||
}
|
||||
|
||||
private async next(info: ConnectionInfo, event: ResponseEvent): Promise<void> {
|
||||
await this.broadcastEvent(event)
|
||||
await this.triggers.process(
|
||||
event,
|
||||
info,
|
||||
(events) => this.apply(info, events),
|
||||
(event) => this.execute(info, event)
|
||||
)
|
||||
}
|
||||
|
||||
private async broadcastEvent(event: ResponseEvent): Promise<void> {
|
||||
const sessionIds: string[] = []
|
||||
for (const [sessionId, session] of this.dataBySessionId.entries()) {
|
||||
if (this.match(event, session)) {
|
||||
@@ -148,109 +213,87 @@ export class Manager {
|
||||
switch (event.type) {
|
||||
case ResponseEventType.MessageCreated:
|
||||
return this.matchMessagesQuery(
|
||||
{ id: event.message.id, card: event.message.card },
|
||||
Array.from(info.messageQueries.values())
|
||||
{ ids: [event.message.id], card: event.message.card },
|
||||
Array.from(info.messageQueries.values()),
|
||||
new Set(info.contextQueries.values().flatMap((it) => Array.from(it)))
|
||||
)
|
||||
case ResponseEventType.PatchCreated:
|
||||
return this.matchMessagesQuery(
|
||||
{ card: event.card, id: event.patch.message },
|
||||
Array.from(info.messageQueries.values())
|
||||
{ card: event.card, ids: [event.patch.message] },
|
||||
Array.from(info.messageQueries.values()),
|
||||
new Set(info.contextQueries.values().flatMap((it) => Array.from(it)))
|
||||
)
|
||||
case ResponseEventType.MessageRemoved:
|
||||
case ResponseEventType.MessagesRemoved:
|
||||
return this.matchMessagesQuery(
|
||||
{ card: event.card, id: event.message },
|
||||
Array.from(info.messageQueries.values())
|
||||
{ card: event.card, ids: event.messages },
|
||||
Array.from(info.messageQueries.values()),
|
||||
new Set(info.contextQueries.values().flatMap((it) => Array.from(it)))
|
||||
)
|
||||
case ResponseEventType.ReactionCreated:
|
||||
return this.matchMessagesQuery(
|
||||
{ card: event.card, id: event.reaction.message },
|
||||
Array.from(info.messageQueries.values())
|
||||
{ card: event.card, ids: [event.reaction.message] },
|
||||
Array.from(info.messageQueries.values()),
|
||||
new Set()
|
||||
)
|
||||
case ResponseEventType.ReactionRemoved:
|
||||
return this.matchMessagesQuery(
|
||||
{ card: event.card, id: event.message },
|
||||
Array.from(info.messageQueries.values())
|
||||
{ card: event.card, ids: [event.message] },
|
||||
Array.from(info.messageQueries.values()),
|
||||
new Set()
|
||||
)
|
||||
case ResponseEventType.AttachmentCreated:
|
||||
case ResponseEventType.FileCreated:
|
||||
return this.matchMessagesQuery(
|
||||
{ card: event.card, id: event.attachment.message },
|
||||
Array.from(info.messageQueries.values())
|
||||
{ card: event.card, ids: [event.file.message] },
|
||||
Array.from(info.messageQueries.values()),
|
||||
new Set()
|
||||
)
|
||||
case ResponseEventType.AttachmentRemoved:
|
||||
case ResponseEventType.FileRemoved:
|
||||
return this.matchMessagesQuery(
|
||||
{ card: event.card, id: event.message },
|
||||
Array.from(info.messageQueries.values())
|
||||
{ card: event.card, ids: [event.message] },
|
||||
Array.from(info.messageQueries.values()),
|
||||
new Set()
|
||||
)
|
||||
case ResponseEventType.ThreadCreated:
|
||||
return this.matchMessagesQuery(
|
||||
{ card: event.thread.card, id: event.thread.message },
|
||||
Array.from(info.messageQueries.values())
|
||||
{ card: event.thread.card, ids: [event.thread.message] },
|
||||
Array.from(info.messageQueries.values()),
|
||||
new Set()
|
||||
)
|
||||
case ResponseEventType.NotificationCreated:
|
||||
// return (
|
||||
// info.personalWorkspace === event.personalWorkspace &&
|
||||
// this.matchNotificationQuery(event, Array.from(info.notificationQueries.values()))
|
||||
// )
|
||||
return false
|
||||
case ResponseEventType.NotificationRemoved:
|
||||
// return info.personalWorkspace === event.personalWorkspace && info.notificationQueries.size > 0
|
||||
return false
|
||||
return info.account === event.account
|
||||
case ResponseEventType.NotificationsRemoved:
|
||||
return info.account === event.account
|
||||
case ResponseEventType.NotificationContextCreated:
|
||||
// return (
|
||||
// info.personalWorkspace === event.context.personalWorkspace &&
|
||||
// this.matchContextQuery(event, Array.from(info.contextQueries.values()))
|
||||
// )
|
||||
return false
|
||||
return info.account === event.context.account
|
||||
case ResponseEventType.NotificationContextRemoved:
|
||||
// return info.personalWorkspace === event.personalWorkspace && info.contextQueries.size > 0
|
||||
return false
|
||||
return info.account === event.account
|
||||
case ResponseEventType.NotificationContextUpdated:
|
||||
// return info.personalWorkspace === event.personalWorkspace && info.contextQueries.size > 0
|
||||
return false
|
||||
return info.account === event.account
|
||||
case ResponseEventType.MessagesGroupCreated:
|
||||
return false
|
||||
case ResponseEventType.AddedCollaborators:
|
||||
return true
|
||||
case ResponseEventType.RemovedCollaborators:
|
||||
return true
|
||||
case ResponseEventType.ThreadUpdated:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private matchMessagesQuery(params: { id?: MessageID; card?: string }, queries: FindMessagesParams[]): boolean {
|
||||
private matchMessagesQuery(
|
||||
params: { ids: MessageID[]; card: CardID },
|
||||
queries: FindMessagesParams[],
|
||||
cards: Set<CardID>
|
||||
): boolean {
|
||||
if (cards.has(params.card)) return true
|
||||
if (queries.length === 0) return false
|
||||
|
||||
for (const query of queries) {
|
||||
if (query.id != null && query.id !== params.id) continue
|
||||
if (query.id != null && !params.ids.includes(query.id)) continue
|
||||
if (query.card != null && query.card !== params.card) continue
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private matchNotificationQuery(event: NotificationCreatedEvent, queries: FindNotificationsParams[]): boolean {
|
||||
if (queries.length === 0) return false
|
||||
|
||||
for (const query of queries) {
|
||||
if (query.context != null && query.context !== event.notification.context) continue
|
||||
if (query.message != null && query.message !== event.notification.message.id) continue
|
||||
if (query.read != null && query.read !== event.notification.read) continue
|
||||
if (query.archived != null && query.archived !== event.notification.archived) continue
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private matchContextQuery(event: NotificationContextCreatedEvent, queries: FindNotificationContextParams[]): boolean {
|
||||
if (queries.length === 0) return false
|
||||
|
||||
for (const query of queries) {
|
||||
if (query.id != null && query.id !== event.context.id) continue
|
||||
if (query.card != null && query.card !== event.context.card) continue
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.db.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
export interface Metadata {
|
||||
msg2fileUrl: string
|
||||
accountsUrl: string
|
||||
}
|
||||
|
||||
export function getMetadata(): Metadata {
|
||||
return {
|
||||
msg2fileUrl: process.env.MSG2FILE_URL ?? ''
|
||||
msg2fileUrl: process.env.MSG2FILE_URL ?? '',
|
||||
accountsUrl: process.env.ACCOUNTS_URL ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
type CreateNotificationContextResult,
|
||||
type RequestEvent,
|
||||
RequestEventType,
|
||||
type ResponseEvent,
|
||||
ResponseEventType
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
import {MessageType, type AccountID, type CardID, type ContextID, type Message, type NotificationContext } from '@hcengineering/communication-types'
|
||||
|
||||
import type { TriggerCtx } from '../types'
|
||||
import { findAccount } from '../utils'
|
||||
|
||||
const BATCH_SIZE = 500
|
||||
|
||||
export async function notify (ctx: TriggerCtx, event: ResponseEvent): Promise<RequestEvent[]> {
|
||||
switch (event.type) {
|
||||
case ResponseEventType.MessageCreated: {
|
||||
return await notifyMessage(ctx, event.message)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async function notifyMessage (ctx: TriggerCtx, message: Message): Promise<RequestEvent[]> {
|
||||
const cursor = ctx.db.getCollaboratorsCursor(message.card, message.created, BATCH_SIZE)
|
||||
const creatorAccount = await findAccount(ctx, message.creator)
|
||||
const result: RequestEvent[] = []
|
||||
|
||||
let isFirstBatch = true
|
||||
|
||||
for await (const dbCollaborators of cursor) {
|
||||
const collaborators: AccountID[] = dbCollaborators.map((it) => it.account)
|
||||
const contexts: NotificationContext[] = await ctx.db.findContexts({
|
||||
card: message.card,
|
||||
account: isFirstBatch && collaborators.length < BATCH_SIZE ? undefined : collaborators
|
||||
})
|
||||
|
||||
for (const collaborator of collaborators) {
|
||||
try {
|
||||
const context = contexts.find((it) => it.account === collaborator)
|
||||
const res = await processCollaborator(ctx, message, collaborator, creatorAccount, context)
|
||||
result.push(...res)
|
||||
} catch (e) {
|
||||
ctx.ctx.error('Error on create notification', { collaborator, error: e })
|
||||
}
|
||||
}
|
||||
|
||||
isFirstBatch = false
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function processCollaborator (
|
||||
ctx: TriggerCtx,
|
||||
message: Message,
|
||||
collaborator: AccountID,
|
||||
creatorAccount?: AccountID,
|
||||
context?: NotificationContext
|
||||
): Promise<RequestEvent[]> {
|
||||
const result: RequestEvent[] = []
|
||||
const isOwn = creatorAccount === collaborator
|
||||
const { contextId, events } = await createOrUpdateContext(ctx, message, collaborator, isOwn, context)
|
||||
|
||||
result.push(...events)
|
||||
|
||||
if (contextId == null || isOwn) return result
|
||||
if(message.type !== MessageType.Message) return result
|
||||
|
||||
result.push({
|
||||
type: RequestEventType.CreateNotification,
|
||||
account: collaborator,
|
||||
context: contextId,
|
||||
message: message.id,
|
||||
created: message.created
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
async function createOrUpdateContext (
|
||||
ctx: TriggerCtx,
|
||||
message: Message,
|
||||
collaborator: AccountID,
|
||||
isOwn: boolean,
|
||||
context?: NotificationContext
|
||||
): Promise<{
|
||||
contextId: ContextID | undefined
|
||||
events: RequestEvent[]
|
||||
}> {
|
||||
if (context == null) {
|
||||
const contextId = await createContext(
|
||||
ctx,
|
||||
collaborator,
|
||||
message.card,
|
||||
message.created,
|
||||
isOwn ? message.created : undefined
|
||||
)
|
||||
|
||||
return {
|
||||
contextId,
|
||||
events: []
|
||||
}
|
||||
}
|
||||
|
||||
const lastUpdate =
|
||||
context.lastUpdate == null || message.created > context.lastUpdate ? message.created : context.lastUpdate
|
||||
const lastView = isOwn && isContextRead(context) ? message.created : undefined
|
||||
|
||||
return {
|
||||
contextId: context.id,
|
||||
events: [
|
||||
{
|
||||
type: RequestEventType.UpdateNotificationContext,
|
||||
context: context.id,
|
||||
account: collaborator,
|
||||
lastView,
|
||||
lastUpdate
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async function createContext (
|
||||
ctx: TriggerCtx,
|
||||
account: AccountID,
|
||||
card: CardID,
|
||||
lastUpdate: Date,
|
||||
lastView?: Date
|
||||
): Promise<ContextID | undefined> {
|
||||
try {
|
||||
const result = (await ctx.execute({
|
||||
type: RequestEventType.CreateNotificationContext,
|
||||
account,
|
||||
card,
|
||||
lastUpdate,
|
||||
lastView: lastView ?? new Date(lastUpdate.getTime() - 1)
|
||||
})) as CreateNotificationContextResult
|
||||
|
||||
return result.id
|
||||
} catch (e) {
|
||||
return (
|
||||
await ctx.db.findContexts({
|
||||
account,
|
||||
card
|
||||
})
|
||||
)[0]?.id
|
||||
}
|
||||
}
|
||||
|
||||
function isContextRead (context: NotificationContext): boolean {
|
||||
const { lastView, lastUpdate } = context
|
||||
|
||||
if (lastView == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return lastView >= lastUpdate
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type AccountID, type SocialID } from '@hcengineering/communication-types'
|
||||
import { type ConnectionInfo, type RequestEvent, RequestEventType } from '@hcengineering/communication-sdk-types'
|
||||
import { type Account, systemAccountUuid } from '@hcengineering/core'
|
||||
|
||||
export class Permissions {
|
||||
validate(info: ConnectionInfo, event: RequestEvent): void {
|
||||
switch (event.type) {
|
||||
case RequestEventType.CreateMessage:
|
||||
case RequestEventType.CreatePatch:
|
||||
case RequestEventType.CreateReaction:
|
||||
case RequestEventType.RemoveReaction:
|
||||
case RequestEventType.RemoveFile:
|
||||
case RequestEventType.CreateFile: {
|
||||
this.checkSocialId(info.account, event.creator)
|
||||
return
|
||||
}
|
||||
case RequestEventType.RemoveNotifications:
|
||||
case RequestEventType.CreateNotificationContext:
|
||||
case RequestEventType.UpdateNotificationContext:
|
||||
case RequestEventType.RemoveNotificationContext: {
|
||||
this.checkAccount(info.account, event.account)
|
||||
return
|
||||
}
|
||||
case RequestEventType.CreateMessagesGroup:
|
||||
case RequestEventType.RemoveMessagesGroup: {
|
||||
this.onlySystemAccount(info.account)
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private checkSocialId(account: Account, creator: SocialID): void {
|
||||
if (!account.socialIds.includes(creator) && systemAccountUuid !== account.uuid) {
|
||||
throw new Error('Forbidden')
|
||||
}
|
||||
}
|
||||
|
||||
private checkAccount(account: Account, creator: AccountID): void {
|
||||
if (account.uuid !== creator && systemAccountUuid !== account.uuid) {
|
||||
throw new Error('Forbidden')
|
||||
}
|
||||
}
|
||||
|
||||
private onlySystemAccount(account: Account): void {
|
||||
if (systemAccountUuid !== account.uuid) {
|
||||
throw new Error('Forbidden')
|
||||
}
|
||||
}
|
||||
}
|
||||
+193
-66
@@ -14,118 +14,247 @@
|
||||
//
|
||||
|
||||
import {
|
||||
type MessageCreatedEvent,
|
||||
type ConnectionInfo,
|
||||
type CreatePatchEvent,
|
||||
type DbAdapter,
|
||||
type EventResult,
|
||||
type FileCreatedEvent,
|
||||
type FileRemovedEvent,
|
||||
type MessageCreatedEvent,
|
||||
type MessagesGroupCreatedEvent,
|
||||
type MessagesRemovedEvent,
|
||||
type PatchCreatedEvent,
|
||||
type RequestEvent,
|
||||
RequestEventType,
|
||||
type ResponseEvent,
|
||||
ResponseEventType,
|
||||
type MessageRemovedEvent,
|
||||
type ConnectionInfo,
|
||||
type PatchCreatedEvent,
|
||||
type MessagesGroupCreatedEvent
|
||||
type UpdateThreadEvent
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
import { type WorkspaceID, PatchType, type Patch, type CardID } from '@hcengineering/communication-types'
|
||||
import { concatLink, systemAccountUuid } from '@hcengineering/core'
|
||||
import { type CardID, PatchType, type WorkspaceID, type File } from '@hcengineering/communication-types'
|
||||
import { concatLink, type MeasureContext, systemAccountUuid } from '@hcengineering/core'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
|
||||
import type { Metadata } from './metadata.ts'
|
||||
import { notify } from './notification/notification'
|
||||
import { type TriggerCtx } from './types.js'
|
||||
import { findAccount } from './utils'
|
||||
|
||||
export class Triggers {
|
||||
private readonly registeredCards: Set<CardID> = new Set()
|
||||
private readonly registeredCards = new Set<CardID>()
|
||||
|
||||
constructor(
|
||||
private readonly ctx: MeasureContext,
|
||||
private readonly metadata: Metadata,
|
||||
private readonly db: DbAdapter,
|
||||
private readonly workspace: WorkspaceID
|
||||
) {}
|
||||
|
||||
async process(event: ResponseEvent, info: ConnectionInfo): Promise<ResponseEvent[]> {
|
||||
try {
|
||||
switch (event.type) {
|
||||
case ResponseEventType.MessageCreated:
|
||||
return await this.onMessageCreated(event)
|
||||
case ResponseEventType.MessageRemoved:
|
||||
return await this.onMessageRemoved(event, info)
|
||||
case ResponseEventType.PatchCreated:
|
||||
return await this.onPatchCreated(event, info)
|
||||
case ResponseEventType.MessagesGroupCreated:
|
||||
return await this.onMessagesGroupCreated(event)
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
return []
|
||||
async process(
|
||||
event: ResponseEvent,
|
||||
info: ConnectionInfo,
|
||||
apply: (events: RequestEvent[]) => Promise<void>,
|
||||
execute: (event: RequestEvent) => Promise<EventResult>
|
||||
): Promise<void> {
|
||||
const ctx: TriggerCtx = {
|
||||
ctx: this.ctx,
|
||||
metadata: this.metadata,
|
||||
db: this.db,
|
||||
workspace: this.workspace,
|
||||
account: info.account,
|
||||
execute
|
||||
}
|
||||
|
||||
return []
|
||||
await this.applySyncTriggers(ctx, event, apply)
|
||||
|
||||
void this.createNotification(ctx, event).then((it) => {
|
||||
void apply(it)
|
||||
})
|
||||
}
|
||||
|
||||
async onMessagesGroupCreated(event: MessagesGroupCreatedEvent): Promise<ResponseEvent[]> {
|
||||
private async applySyncTriggers(
|
||||
ctx: TriggerCtx,
|
||||
event: ResponseEvent,
|
||||
apply: (events: RequestEvent[]) => Promise<void>
|
||||
): Promise<void> {
|
||||
let events: RequestEvent[] = []
|
||||
|
||||
try {
|
||||
switch (event.type) {
|
||||
case ResponseEventType.MessageCreated: {
|
||||
events = await this.onMessageCreated(ctx, event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.MessagesRemoved: {
|
||||
events = await this.onMessagesRemoved(ctx, event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.PatchCreated: {
|
||||
events = await this.onPatchCreated(ctx, event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.FileCreated: {
|
||||
events = await this.onFileCreated(ctx, event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.FileRemoved: {
|
||||
events = await this.onFileRemoved(ctx, event)
|
||||
break
|
||||
}
|
||||
case ResponseEventType.MessagesGroupCreated: {
|
||||
events = await this.onMessagesGroupCreated(ctx, event)
|
||||
break
|
||||
}
|
||||
}
|
||||
await apply(events)
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
private async createNotification(ctx: TriggerCtx, event: ResponseEvent): Promise<RequestEvent[]> {
|
||||
return await notify(ctx, event)
|
||||
}
|
||||
|
||||
private async onMessagesGroupCreated(_: TriggerCtx, event: MessagesGroupCreatedEvent): Promise<RequestEvent[]> {
|
||||
this.registeredCards.delete(event.group.card)
|
||||
return []
|
||||
}
|
||||
|
||||
async onMessageRemoved(event: MessageRemovedEvent, info: ConnectionInfo): Promise<ResponseEvent[]> {
|
||||
private async onMessagesRemoved(ctx: TriggerCtx, event: MessagesRemovedEvent): Promise<RequestEvent[]> {
|
||||
const { card } = event
|
||||
const thread = await this.db.findThread(card)
|
||||
if (thread === undefined) return []
|
||||
|
||||
const date = new Date()
|
||||
const socialId = info.account.primarySocialId
|
||||
const socialId = ctx.account.primarySocialId
|
||||
|
||||
const patch: Patch = {
|
||||
message: thread.message,
|
||||
type: PatchType.removeReply,
|
||||
content: thread.thread,
|
||||
creator: socialId,
|
||||
created: date
|
||||
}
|
||||
await this.db.updateThread(thread.thread, date, 'decrement')
|
||||
await this.db.createPatch(thread.card, patch.message, patch.type, patch.content, patch.creator, patch.created)
|
||||
|
||||
return [
|
||||
{
|
||||
type: ResponseEventType.PatchCreated,
|
||||
return event.messages.flatMap(() => {
|
||||
const patchEvent: CreatePatchEvent = {
|
||||
type: RequestEventType.CreatePatch,
|
||||
patchType: PatchType.removeReply,
|
||||
card: thread.card,
|
||||
patch
|
||||
message: thread.message,
|
||||
content: thread.thread,
|
||||
creator: socialId
|
||||
}
|
||||
]
|
||||
const threadEvent: UpdateThreadEvent = {
|
||||
type: RequestEventType.UpdateThread,
|
||||
thread: thread.thread,
|
||||
replies: 'decrement'
|
||||
}
|
||||
|
||||
return [patchEvent, threadEvent]
|
||||
})
|
||||
}
|
||||
|
||||
async onMessageCreated(event: MessageCreatedEvent): Promise<ResponseEvent[]> {
|
||||
return (await Promise.all([this.registerCard(event.message.card), this.updateThread(event)])).flat()
|
||||
private async onMessageCreated(ctx: TriggerCtx, event: MessageCreatedEvent): Promise<RequestEvent[]> {
|
||||
void this.registerCard(event.message.card)
|
||||
|
||||
return [...(await this.addCollaborators(ctx, event)), ...(await this.addThreadReply(event))]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async onPatchCreated(event: PatchCreatedEvent, info: ConnectionInfo): Promise<ResponseEvent[]> {
|
||||
return this.registerCard(event.card)
|
||||
private async addCollaborators(ctx: TriggerCtx, event: MessageCreatedEvent): Promise<RequestEvent[]> {
|
||||
const { creator } = event.message
|
||||
const account = await findAccount(ctx, creator)
|
||||
|
||||
if (account === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const collaborator = (
|
||||
await this.db.findCollaborators({
|
||||
card: event.message.card,
|
||||
account,
|
||||
limit: 1
|
||||
})
|
||||
)[0]
|
||||
|
||||
return collaborator != null
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: RequestEventType.AddCollaborators,
|
||||
card: event.message.card,
|
||||
collaborators: [account],
|
||||
date: event.message.created
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async updateThread(event: MessageCreatedEvent): Promise<ResponseEvent[]> {
|
||||
private async onPatchCreated(_: TriggerCtx, event: PatchCreatedEvent): Promise<RequestEvent[]> {
|
||||
void this.registerCard(event.card)
|
||||
return []
|
||||
}
|
||||
|
||||
private async addThreadReply(event: MessageCreatedEvent): Promise<RequestEvent[]> {
|
||||
const { message } = event
|
||||
const thread = await this.db.findThread(message.card)
|
||||
if (thread === undefined) return []
|
||||
|
||||
const date = new Date()
|
||||
const patch: Patch = {
|
||||
message: thread.message,
|
||||
type: PatchType.addReply,
|
||||
content: thread.thread,
|
||||
creator: message.creator,
|
||||
created: date
|
||||
if (thread === undefined) {
|
||||
return []
|
||||
}
|
||||
await this.db.updateThread(thread.thread, date, 'increment')
|
||||
await this.db.createPatch(thread.card, patch.message, patch.type, patch.content, patch.creator, patch.created)
|
||||
|
||||
return [
|
||||
{
|
||||
type: ResponseEventType.PatchCreated,
|
||||
type: RequestEventType.CreatePatch,
|
||||
patchType: PatchType.addReply,
|
||||
card: thread.card,
|
||||
patch
|
||||
message: thread.message,
|
||||
content: thread.thread,
|
||||
creator: message.creator
|
||||
},
|
||||
{
|
||||
type: RequestEventType.UpdateThread,
|
||||
thread: thread.thread,
|
||||
lastReply: event.message.created,
|
||||
replies: 'increment'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async registerCard(card: CardID): Promise<ResponseEvent[]> {
|
||||
if (this.registeredCards.has(card) || this.metadata.msg2fileUrl === '') return []
|
||||
private async onFileCreated(_: TriggerCtx, event: FileCreatedEvent): Promise<RequestEvent[]> {
|
||||
const message = (await this.db.findMessages({ card: event.card, id: event.file.message, limit: 1 }))[0]
|
||||
if (message !== undefined) return []
|
||||
|
||||
const { file } = event
|
||||
const patchContent: Omit<File, 'card' | 'message' | 'created' | 'creator'> = {
|
||||
blobId: file.blobId,
|
||||
type: file.type,
|
||||
filename: file.filename,
|
||||
size: file.size
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: RequestEventType.CreatePatch,
|
||||
patchType: PatchType.addFile,
|
||||
card: event.card,
|
||||
message: file.message,
|
||||
content: JSON.stringify(patchContent),
|
||||
creator: file.creator
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
private async onFileRemoved(_: TriggerCtx, event: FileRemovedEvent): Promise<RequestEvent[]> {
|
||||
const message = (await this.db.findMessages({ card: event.card, id: event.message, limit: 1 }))[0]
|
||||
if (message !== undefined) return []
|
||||
const { blobId } = event
|
||||
|
||||
return [
|
||||
{
|
||||
type: RequestEventType.CreatePatch,
|
||||
patchType: PatchType.removeFile,
|
||||
card: event.card,
|
||||
message: event.message,
|
||||
content: JSON.stringify({ blobId }),
|
||||
creator: event.creator
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
private async registerCard(card: CardID): Promise<void> {
|
||||
if (this.registeredCards.has(card) || this.metadata.msg2fileUrl === '') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const token = generateToken(systemAccountUuid, this.workspace)
|
||||
@@ -140,7 +269,5 @@ export class Triggers {
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Account, MeasureContext } from '@hcengineering/core'
|
||||
import type { DbAdapter, EventResult, RequestEvent } from '@hcengineering/communication-sdk-types'
|
||||
import type { WorkspaceID } from '@hcengineering/communication-types'
|
||||
|
||||
import type { Metadata } from './metadata'
|
||||
|
||||
export interface TriggerCtx {
|
||||
ctx: MeasureContext
|
||||
metadata: Metadata
|
||||
db: DbAdapter
|
||||
workspace: WorkspaceID
|
||||
account: Account
|
||||
execute: (event: RequestEvent) => Promise<EventResult>
|
||||
}
|
||||
|
||||
export type QueryId = string | number
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { AccountID, SocialID } from '@hcengineering/communication-types'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import { systemAccountUuid } from '@hcengineering/core'
|
||||
|
||||
import type { TriggerCtx } from './types'
|
||||
|
||||
export async function findAccount(ctx: TriggerCtx, socialString: SocialID): Promise<AccountID | undefined> {
|
||||
if (ctx.account.socialIds.includes(socialString)) {
|
||||
return ctx.account.uuid
|
||||
}
|
||||
|
||||
const token = generateToken(systemAccountUuid)
|
||||
// const account = getAccountClient(ctx.metadata.accountsUrl, token)
|
||||
|
||||
try {
|
||||
//TODO: FIXME
|
||||
return await fetchAccount(socialString, ctx.metadata.accountsUrl, token)
|
||||
} catch (err: any) {
|
||||
ctx.ctx.warn('Cannot find account', { socialString, err })
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: replace with AccountClient
|
||||
async function fetchAccount(socialId: SocialID, url: string, token: string): Promise<AccountID | undefined> {
|
||||
const body = {
|
||||
method: 'findPersonBySocialId' as const,
|
||||
params: { socialId, requireAccount: true }
|
||||
}
|
||||
const request: RequestInit = {
|
||||
keepalive: true,
|
||||
headers: {
|
||||
...(token === undefined
|
||||
? {}
|
||||
: {
|
||||
Authorization: 'Bearer ' + token
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...request,
|
||||
headers: {
|
||||
...request.headers,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
if (result.error != null) {
|
||||
throw Error(result.error)
|
||||
}
|
||||
|
||||
return result.result as AccountID | undefined
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/communication-shared",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.147",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "./types/index.d.ts",
|
||||
@@ -15,15 +15,13 @@
|
||||
"bundle:node": "bun build src/index.ts --target node --format cjs --outfile dist/index.cjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/js-yaml": "^4.0.9"
|
||||
"@types/bun": "^1.1.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"js-yaml": "^4.1.0"
|
||||
"@hcengineering/communication-types": "workspace:*"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
|
||||
export * from './message.ts'
|
||||
export * from './retry.ts'
|
||||
export * from './files.ts'
|
||||
export * from './patch.ts'
|
||||
|
||||
@@ -15,25 +15,24 @@
|
||||
|
||||
import type { MessageID } from '@hcengineering/communication-types'
|
||||
|
||||
let lastTimestamp = 0
|
||||
let lastTimestamp = 0n
|
||||
let counter = 0n
|
||||
|
||||
export function generateMessageId(): MessageID {
|
||||
const timestamp = Math.floor(Date.now() / 1000)
|
||||
const timestamp = BigInt(Math.floor(Date.now() / 1000))
|
||||
|
||||
if (timestamp !== lastTimestamp) {
|
||||
lastTimestamp = timestamp
|
||||
counter = 0n
|
||||
}
|
||||
|
||||
const id = (BigInt(timestamp) << 20n) | counter
|
||||
counter++
|
||||
|
||||
const id = (timestamp << 20n) | (counter << 10n) | BigInt(Math.floor(Math.random() * 1024))
|
||||
|
||||
return id.toString() as MessageID
|
||||
}
|
||||
|
||||
export function parseMessageId(messageId: MessageID): Date {
|
||||
const timestamp = Number(BigInt(messageId) >> 20n)
|
||||
|
||||
return new Date(timestamp * 1000)
|
||||
return new Date(Number(BigInt(messageId) >> 20n) * 1000)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
PatchType,
|
||||
type BlobID,
|
||||
type CardID,
|
||||
type Message,
|
||||
type Patch,
|
||||
type Reaction,
|
||||
type SocialID,
|
||||
type File
|
||||
} from '@hcengineering/communication-types'
|
||||
|
||||
type PatchFile = Pick<File, 'blobId' | 'type' | 'filename' | 'size'>
|
||||
|
||||
export function applyPatches(message: Message, patches: Patch[], allowedPatchTypes: PatchType[] = []): Message {
|
||||
if (patches.length === 0) return message
|
||||
|
||||
for (const p of patches) {
|
||||
message = applyPatch(message, p, allowedPatchTypes)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
export function applyPatch(message: Message, patch: Patch, allowedPatchTypes: PatchType[] = []): Message {
|
||||
if (allowedPatchTypes.length > 0 && !allowedPatchTypes.includes(patch.type)) return message
|
||||
switch (patch.type) {
|
||||
case PatchType.update:
|
||||
return {
|
||||
...message,
|
||||
edited: patch.created,
|
||||
content: patch.content
|
||||
}
|
||||
case PatchType.addReaction:
|
||||
return addReaction(message, {
|
||||
message: message.id,
|
||||
reaction: patch.content,
|
||||
creator: patch.creator,
|
||||
created: patch.created
|
||||
})
|
||||
case PatchType.removeReaction:
|
||||
return removeReaction(message, patch.content, patch.creator)
|
||||
case PatchType.addReply:
|
||||
return addReply(message, patch.content as CardID, patch.created)
|
||||
case PatchType.removeReply:
|
||||
return removeReply(message, patch.content as CardID)
|
||||
case PatchType.addFile:
|
||||
return addFile(message, JSON.parse(patch.content) as PatchFile, patch.created, patch.creator)
|
||||
case PatchType.removeFile:
|
||||
return removeFile(message, patch.content as BlobID)
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
function addReaction(message: Message, reaction: Reaction): Message {
|
||||
message.reactions.push(reaction)
|
||||
return message
|
||||
}
|
||||
|
||||
function removeReaction(message: Message, emoji: string, creator: SocialID): Message {
|
||||
const reactions = message.reactions.filter((it) => it.reaction !== emoji || it.creator !== creator)
|
||||
if (reactions.length === message.reactions.length) return message
|
||||
|
||||
return {
|
||||
...message,
|
||||
reactions
|
||||
}
|
||||
}
|
||||
|
||||
function addReply(message: Message, thread: CardID, created: Date): Message {
|
||||
if (message.thread === undefined) {
|
||||
return {
|
||||
...message,
|
||||
thread: {
|
||||
card: message.card,
|
||||
message: message.id,
|
||||
thread,
|
||||
repliesCount: 1,
|
||||
lastReply: created
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.thread.thread !== thread) return message
|
||||
|
||||
return {
|
||||
...message,
|
||||
thread: {
|
||||
...message.thread,
|
||||
repliesCount: message.thread.repliesCount + 1,
|
||||
lastReply: created
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addFile(message: Message, file: PatchFile, created: Date, creator: SocialID): Message {
|
||||
message.files.push({ ...file, card: message.card, message: message.id, created, creator })
|
||||
return message
|
||||
}
|
||||
|
||||
function removeFile(message: Message, blobId: BlobID): Message {
|
||||
const files = message.files.filter((it) => it.blobId !== blobId)
|
||||
if (files.length === message.files.length) return message
|
||||
|
||||
return {
|
||||
...message,
|
||||
files
|
||||
}
|
||||
}
|
||||
|
||||
function removeReply(message: Message, thread: CardID): Message {
|
||||
if (message.thread === undefined || message.thread.thread !== thread) return message
|
||||
|
||||
return {
|
||||
...message,
|
||||
thread: {
|
||||
...message.thread,
|
||||
repliesCount: message.thread.repliesCount - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "@hcengineering/communication-sqlite-wasm",
|
||||
"version": "0.1.61",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "./types/index.d.ts",
|
||||
"files": [
|
||||
"dist/index.js",
|
||||
"dist/index.cjs",
|
||||
"types/**/*.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"bundle": "bun run bundle:browser",
|
||||
"bundle:browser": "bun build src/index.ts --outdir dist --target browser"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"@hcengineering/communication-sdk-types": "workspace:*",
|
||||
"@sqlite.org/sqlite-wasm": "^3.47.1-build1",
|
||||
"path": "^0.12.7",
|
||||
"uuid": "^11.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
type Message,
|
||||
type FindMessagesParams,
|
||||
type CardID,
|
||||
type RichText,
|
||||
type SocialID,
|
||||
type MessageID,
|
||||
type ContextID,
|
||||
type NotificationContextUpdate,
|
||||
type FindNotificationsParams,
|
||||
type FindNotificationContextParams,
|
||||
type NotificationContext,
|
||||
type Notification,
|
||||
type BlobID,
|
||||
type MessagesGroup,
|
||||
type FindMessagesGroupsParams
|
||||
} from '@hcengineering/communication-types'
|
||||
import type { DbAdapter } from '@hcengineering/communication-sdk-types'
|
||||
|
||||
import { initializeSQLite, type Sqlite3Worker1Promiser } from './connection'
|
||||
import { applyMigrations } from './migrations.ts'
|
||||
import { MessagesDb } from './db/message.ts'
|
||||
import { NotificationsDb } from './db/notification.ts'
|
||||
|
||||
//TODO: FIXME
|
||||
//export class SqliteAdapter implements DbAdapter
|
||||
export class SqliteAdapter {
|
||||
private readonly message: MessagesDb
|
||||
private readonly notification: NotificationsDb
|
||||
|
||||
constructor(
|
||||
private readonly worker: Sqlite3Worker1Promiser,
|
||||
private readonly dbId: string
|
||||
) {
|
||||
this.message = new MessagesDb(worker, dbId)
|
||||
this.notification = new NotificationsDb(worker, dbId)
|
||||
}
|
||||
|
||||
async createMessage(
|
||||
workspace: string,
|
||||
card: CardID,
|
||||
content: RichText,
|
||||
creator: SocialID,
|
||||
created: Date
|
||||
): Promise<MessageID> {
|
||||
return await this.message.createMessage(workspace, card, content, creator, created)
|
||||
}
|
||||
|
||||
async createPatch(
|
||||
workspace: string,
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
content: RichText,
|
||||
creator: SocialID,
|
||||
created: Date
|
||||
): Promise<void> {
|
||||
//TODO: FIXME
|
||||
return await this.message.createPatch(message, content, creator, created)
|
||||
}
|
||||
|
||||
async removeMessage(workspace: string, card: CardID, id: MessageID): Promise<MessageID> {
|
||||
await this.message.removeMessage(id)
|
||||
return id
|
||||
}
|
||||
|
||||
async removeMessages(workspace: string, card: CardID, ids: MessageID[]): Promise<MessageID[]> {
|
||||
//TODO: implement
|
||||
return ids
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
async createMessagesGroup(
|
||||
workspace: string,
|
||||
card: CardID,
|
||||
blobId: BlobID,
|
||||
from_date: Date,
|
||||
to_date: Date,
|
||||
count: number
|
||||
): Promise<void> {
|
||||
//TODO: implement
|
||||
}
|
||||
|
||||
async findMessagesGroups(workspace: string, params: FindMessagesGroupsParams): Promise<MessagesGroup[]> {
|
||||
//TODO: implement
|
||||
return []
|
||||
}
|
||||
|
||||
async createReaction(
|
||||
workspace: string,
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
reaction: string,
|
||||
creator: SocialID,
|
||||
created: Date
|
||||
): Promise<void> {
|
||||
//TODO: FIXME
|
||||
return await this.message.createReaction(message, reaction, creator, created)
|
||||
}
|
||||
|
||||
async removeReaction(
|
||||
workspace: string,
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
reaction: string,
|
||||
creator: SocialID
|
||||
): Promise<void> {
|
||||
//TODO: FIXME
|
||||
return await this.message.removeReaction(message, reaction, creator)
|
||||
}
|
||||
|
||||
async createAttachment(message: MessageID, attachment: CardID, creator: SocialID, created: Date): Promise<void> {
|
||||
return await this.message.createAttachment(message, attachment, creator, created)
|
||||
}
|
||||
|
||||
async removeAttachment(message: MessageID, attachment: CardID): Promise<void> {
|
||||
return await this.message.removeAttachment(message, attachment)
|
||||
}
|
||||
|
||||
async findMessages(workspace: string, params: FindMessagesParams): Promise<Message[]> {
|
||||
return await this.message.find(workspace, params)
|
||||
}
|
||||
|
||||
async createNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
return await this.notification.createNotification(message, context)
|
||||
}
|
||||
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
return await this.notification.removeNotification(message, context)
|
||||
}
|
||||
|
||||
async createContext(
|
||||
workspace: string,
|
||||
card: CardID,
|
||||
personalWorkspace: string,
|
||||
lastView?: Date,
|
||||
lastUpdate?: Date
|
||||
): Promise<ContextID> {
|
||||
return await this.notification.createContext(workspace, card, personalWorkspace, lastView, lastUpdate)
|
||||
}
|
||||
|
||||
async removeContext(context: ContextID): Promise<void> {
|
||||
return await this.notification.removeContext(context)
|
||||
}
|
||||
|
||||
async updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
|
||||
return await this.notification.updateContext(context, update)
|
||||
}
|
||||
|
||||
async findContexts(
|
||||
params: FindNotificationContextParams,
|
||||
personalWorkspaces: string[],
|
||||
workspace?: string
|
||||
): Promise<NotificationContext[]> {
|
||||
return await this.notification.findContexts(params, personalWorkspaces, workspace)
|
||||
}
|
||||
|
||||
async findNotifications(
|
||||
params: FindNotificationsParams,
|
||||
personalWorkspace: string,
|
||||
workspace?: string
|
||||
): Promise<Notification[]> {
|
||||
return await this.notification.findNotifications(params, personalWorkspace, workspace)
|
||||
}
|
||||
|
||||
close(): void {
|
||||
void this.worker('close')
|
||||
}
|
||||
}
|
||||
|
||||
export async function createDbAdapter(connectionString: string): Promise<DbAdapter> {
|
||||
const { worker, dbId } = await initializeSQLite(connectionString)
|
||||
|
||||
await applyMigrations(worker, dbId)
|
||||
//TODO: FIXME
|
||||
return new SqliteAdapter(worker, dbId) as unknown as DbAdapter
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// @ts-expect-error error
|
||||
import { sqlite3Worker1Promiser } from '@sqlite.org/sqlite-wasm'
|
||||
|
||||
export type Sqlite3Worker1Promiser = {
|
||||
(
|
||||
command: 'config-get',
|
||||
params: object
|
||||
): Promise<{
|
||||
result: {
|
||||
version: {
|
||||
libVersion: string
|
||||
}
|
||||
}
|
||||
}>
|
||||
|
||||
(
|
||||
command: 'open',
|
||||
params: { filename: string }
|
||||
): Promise<{
|
||||
dbId: string
|
||||
}>
|
||||
|
||||
(
|
||||
command: 'exec',
|
||||
params: {
|
||||
dbId: string
|
||||
sql: string
|
||||
callback?: (row: SqlResult | null | undefined) => void
|
||||
}
|
||||
): Promise<void>
|
||||
|
||||
(command: 'close'): Promise<void>
|
||||
}
|
||||
|
||||
type SqlResult = {
|
||||
columnNames: string[]
|
||||
row: any[] | null | undefined
|
||||
rowNumber: number | null | undefined
|
||||
}
|
||||
|
||||
export async function initializeSQLite(
|
||||
connectionString: string
|
||||
): Promise<{ worker: Sqlite3Worker1Promiser; dbId: string }> {
|
||||
const promiser: Sqlite3Worker1Promiser = await new Promise((resolve) => {
|
||||
const _promiser = sqlite3Worker1Promiser({
|
||||
onready: () => resolve(_promiser)
|
||||
})
|
||||
})
|
||||
|
||||
const configResponse = await promiser('config-get', {})
|
||||
console.log('SQLite3 config', configResponse.result)
|
||||
|
||||
const { dbId } = await promiser('open', { filename: connectionString })
|
||||
|
||||
return { worker: promiser, dbId }
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type {Sqlite3Worker1Promiser} from "../connection";
|
||||
|
||||
export class BaseDb {
|
||||
constructor(protected readonly worker: Sqlite3Worker1Promiser, protected readonly dbId: string) {
|
||||
}
|
||||
|
||||
async insert( table: string, data: Record<string, any>): Promise<void> {
|
||||
const keys = Object.keys(data)
|
||||
const values = Object.values(data)
|
||||
const sql = `
|
||||
INSERT INTO ${table} (${keys.map((k) => `${k}`).join(', ')})
|
||||
VALUES (${values.map((value) => `'${value}'`).join(', ')});
|
||||
`
|
||||
await this.worker('exec', {
|
||||
dbId: this.dbId,
|
||||
sql
|
||||
});
|
||||
}
|
||||
|
||||
async remove(table: string, where: Record<string, any>): Promise<void> {
|
||||
const keys = Object.keys(where)
|
||||
const values = Object.values(where)
|
||||
|
||||
const sql = `
|
||||
DELETE
|
||||
FROM ${table}
|
||||
WHERE ${keys.map((k, idx) => `${k} = '${values[idx]}'`).join(' AND ')};`
|
||||
|
||||
await this.worker('exec', {
|
||||
dbId: this.dbId,
|
||||
sql
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async select(sql: string): Promise<Record<string, any>[]> {
|
||||
return new Promise(async (resolve) => {
|
||||
const rows: Record<string, any>[] = [];
|
||||
|
||||
await this.worker('exec', {
|
||||
dbId: this.dbId,
|
||||
sql,
|
||||
callback: (res) => {
|
||||
if (res == null) {
|
||||
resolve(rows);
|
||||
return;
|
||||
}
|
||||
if (res.row == null || res.rowNumber == null) {
|
||||
resolve(rows);
|
||||
} else {
|
||||
const rowObject: Record<string, any> = {};
|
||||
res.columnNames.forEach((columnName, index) => {
|
||||
rowObject[columnName] = res.row?.[index] ?? undefined
|
||||
});
|
||||
|
||||
rows.push(rowObject);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
type Message,
|
||||
type MessageID,
|
||||
type CardID,
|
||||
type FindMessagesParams,
|
||||
SortingOrder,
|
||||
type SocialID,
|
||||
type RichText,
|
||||
type Reaction, type Attachment
|
||||
} from '@hcengineering/communication-types'
|
||||
|
||||
import {BaseDb} from './base.ts'
|
||||
import {
|
||||
TableName,
|
||||
type MessageDb,
|
||||
type AttachmentDb,
|
||||
type ReactionDb,
|
||||
type PatchDb
|
||||
} from './types.ts'
|
||||
|
||||
export class MessagesDb extends BaseDb {
|
||||
//Message
|
||||
async createMessage(workspace: string, card: CardID, content: RichText, creator: SocialID, created: Date): Promise<MessageID> {
|
||||
const dbData: MessageDb = {
|
||||
id: self.crypto.randomUUID(),
|
||||
workspace_id: workspace,
|
||||
card_id: card,
|
||||
content: content,
|
||||
creator: creator,
|
||||
created: created,
|
||||
}
|
||||
await this.insert(TableName.Message, dbData)
|
||||
//TODO: FIX ME
|
||||
return dbData.id as any as MessageID
|
||||
}
|
||||
|
||||
async removeMessage(message: MessageID): Promise<void> {
|
||||
await this.remove(TableName.Message, {id: message})
|
||||
}
|
||||
|
||||
async createPatch(message: MessageID, content: RichText, creator: SocialID, created: Date): Promise<void> {
|
||||
const dbData: PatchDb = {
|
||||
id: self.crypto.randomUUID(),
|
||||
message_id: message,
|
||||
content: content,
|
||||
creator: creator,
|
||||
created: created
|
||||
}
|
||||
|
||||
await this.insert(TableName.Patch, dbData)
|
||||
}
|
||||
|
||||
//Attachment
|
||||
async createAttachment(message: MessageID, card: CardID, creator: SocialID, created: Date): Promise<void> {
|
||||
const dbData: AttachmentDb = {
|
||||
message_id: message,
|
||||
card_id: card,
|
||||
creator: creator,
|
||||
created: created
|
||||
}
|
||||
await this.insert(TableName.Attachment, dbData)
|
||||
}
|
||||
|
||||
async removeAttachment(message: MessageID, card: CardID): Promise<void> {
|
||||
await this.remove(TableName.Attachment, {
|
||||
message_id: message,
|
||||
card_id: card
|
||||
})
|
||||
}
|
||||
|
||||
//Reaction
|
||||
async createReaction(message: MessageID, reaction: string, creator: SocialID, created: Date): Promise<void> {
|
||||
const dbData: ReactionDb = {
|
||||
message_id: message,
|
||||
reaction: reaction,
|
||||
creator: creator,
|
||||
created: created
|
||||
}
|
||||
await this.insert(TableName.Reaction, dbData)
|
||||
}
|
||||
|
||||
async removeReaction(message: MessageID, reaction: string, creator: SocialID): Promise<void> {
|
||||
await this.remove(TableName.Reaction, {
|
||||
message_id: message,
|
||||
reaction: reaction,
|
||||
creator: creator
|
||||
})
|
||||
}
|
||||
|
||||
//Find messages
|
||||
async find(workspace: string, params: FindMessagesParams): Promise<Message[]> {
|
||||
const select = `SELECT m.id,
|
||||
m.card_id,
|
||||
m.content,
|
||||
m.creator,
|
||||
m.created,
|
||||
json_group_array(
|
||||
json_object(
|
||||
'content', p.content,
|
||||
'creator', p.creator,
|
||||
'created', p.created
|
||||
)
|
||||
) AS patches,
|
||||
json_group_array(
|
||||
json_object(
|
||||
'card_id', a.card_id,
|
||||
'message_id', a.message_id,
|
||||
'creator', a.creator,
|
||||
'created', a.created
|
||||
)
|
||||
) AS attachments,
|
||||
json_group_array(
|
||||
json_object(
|
||||
'message_id', r.message_id,
|
||||
'reaction', r.reaction,
|
||||
'creator', r.creator,
|
||||
'created', r.created
|
||||
)
|
||||
) AS reactions
|
||||
FROM ${TableName.Message} m
|
||||
LEFT JOIN ${TableName.Patch} p ON p.message_id = m.id
|
||||
LEFT JOIN ${TableName.Attachment} a ON a.message_id = m.id
|
||||
LEFT JOIN ${TableName.Reaction} r ON r.message_id = m.id`
|
||||
|
||||
const where = this.buildMessageWhere(workspace, params)
|
||||
const groupBy = `GROUP BY m.id`
|
||||
const orderBy = params.order ? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
|
||||
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
|
||||
const sql = [select, where, groupBy, orderBy, limit].join(' ')
|
||||
|
||||
const result = await this.select(sql)
|
||||
|
||||
return result.map(it => this.toMessage(it))
|
||||
}
|
||||
|
||||
buildMessageWhere(workspace: string, params: FindMessagesParams): string {
|
||||
const where: string[] = [`m.workspace_id = '${workspace}'`]
|
||||
|
||||
if (params.card != null) {
|
||||
where.push(`m.card_id = '${params.card}'`)
|
||||
}
|
||||
if (params.id != null) {
|
||||
where.push(`m.id = '${params.id}'`)
|
||||
}
|
||||
|
||||
//TODO: FIX ME
|
||||
// if (params.from != null) {
|
||||
// const exclude = params.excluded ?? false
|
||||
// const direction = params.direction ?? Direction.Forward
|
||||
// const getOperator = () => {
|
||||
// if (exclude) {
|
||||
// return direction === Direction.Forward ? '>' : '<'
|
||||
// } else {
|
||||
// return direction === Direction.Forward ? '>=' : '<='
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// where.push(`m.created ${getOperator()} ${params.from}`)
|
||||
// }
|
||||
|
||||
return `WHERE ${where.join(' AND ')}`
|
||||
}
|
||||
|
||||
private toMessage(row: any): Message {
|
||||
const patches = JSON.parse(row.patches).filter((it: any) => it.created != null)
|
||||
const attachments = JSON.parse(row.attachments).filter((it: any) => it.created != null)
|
||||
const reactions = JSON.parse(row.reactions).filter((it: any) => it.created != null)
|
||||
|
||||
const lastPatch = patches?.[0]
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
card: row.card_id,
|
||||
content: lastPatch?.content ?? row.content,
|
||||
creator: row.creator,
|
||||
created: new Date(row.created),
|
||||
edited: (lastPatch?.created ?? row.created),
|
||||
reactions: (reactions ?? []).map((it: any) => this.toReaction(it)),
|
||||
attachments: (attachments ?? []).map((it: any) => this.toAttachment(it))
|
||||
}
|
||||
}
|
||||
|
||||
private toReaction(row: any): Reaction {
|
||||
return {
|
||||
message: row.message_id,
|
||||
reaction: row.reaction,
|
||||
creator: row.creator,
|
||||
created: new Date(row.created)
|
||||
}
|
||||
}
|
||||
|
||||
private toAttachment(row: any): Attachment {
|
||||
return {
|
||||
message: row.message_id,
|
||||
card: row.card,
|
||||
creator: row.creator,
|
||||
created: new Date(row.created)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
type MessageID,
|
||||
type ContextID,
|
||||
type CardID,
|
||||
type NotificationContext,
|
||||
type FindNotificationContextParams, SortingOrder,
|
||||
type FindNotificationsParams, type Notification,
|
||||
type NotificationContextUpdate
|
||||
} from '@hcengineering/communication-types'
|
||||
|
||||
import {BaseDb} from './base.ts'
|
||||
import {TableName, type ContextDb, type NotificationDb} from './types.ts'
|
||||
|
||||
export class NotificationsDb extends BaseDb {
|
||||
async createNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
const dbData: NotificationDb = {
|
||||
message_id: message,
|
||||
context_id: context
|
||||
}
|
||||
await this.insert(TableName.Notification, dbData)
|
||||
}
|
||||
|
||||
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
await this.remove(TableName.Notification, {
|
||||
message_id: message,
|
||||
context
|
||||
})
|
||||
}
|
||||
|
||||
async createContext(workspace: string, card: CardID, personalWorkspace: string, lastView?: Date, lastUpdate?: Date): Promise<ContextID> {
|
||||
const dbData: ContextDb = {
|
||||
id: self.crypto.randomUUID(),
|
||||
workspace_id: workspace,
|
||||
card_id: card,
|
||||
personal_workspace: personalWorkspace,
|
||||
last_view: lastView,
|
||||
last_update: lastUpdate
|
||||
}
|
||||
await this.insert(TableName.NotificationContext, dbData)
|
||||
return dbData.id as ContextID
|
||||
}
|
||||
|
||||
async removeContext(context: ContextID): Promise<void> {
|
||||
await this.remove(TableName.NotificationContext, {
|
||||
id: context
|
||||
})
|
||||
}
|
||||
|
||||
async updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
|
||||
const dbData: Partial<ContextDb> = {}
|
||||
|
||||
if (update.archivedFrom != null) {
|
||||
dbData.archived_from = update.archivedFrom
|
||||
}
|
||||
if (update.lastView != null) {
|
||||
dbData.last_view = update.lastView
|
||||
}
|
||||
if (update.lastUpdate != null) {
|
||||
dbData.last_update = update.lastUpdate
|
||||
}
|
||||
|
||||
if (Object.keys(dbData).length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const keys = Object.keys(dbData)
|
||||
const values = Object.values(dbData)
|
||||
|
||||
const sql = `
|
||||
UPDATE ${TableName.NotificationContext}
|
||||
SET ${keys.map((k, idx) => `"${k}" = '${values[idx]}'`).join(', ')}
|
||||
WHERE id = '${context}'
|
||||
`;
|
||||
|
||||
await this.worker('exec', {
|
||||
dbId: this.dbId,
|
||||
sql
|
||||
});
|
||||
}
|
||||
|
||||
async findContexts(params: FindNotificationContextParams, personalWorkspaces: string[], workspace?: string,): Promise<NotificationContext[]> {
|
||||
const select = `
|
||||
SELECT nc.id,
|
||||
nc.card_id,
|
||||
nc.archived_from,
|
||||
nc.last_view,
|
||||
nc.last_update,
|
||||
nc.workspace_id,
|
||||
nc.personal_workspace
|
||||
FROM ${TableName.NotificationContext} nc`;
|
||||
const where = this.buildContextWhere(params, personalWorkspaces, workspace);
|
||||
// const orderSql = `ORDER BY nc.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}`
|
||||
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
|
||||
const sql = [select, where, limit].join(' ')
|
||||
|
||||
const result = await this.select(sql)
|
||||
|
||||
return result.map(it => this.toNotificationContext(it));
|
||||
}
|
||||
|
||||
|
||||
async findNotifications(params: FindNotificationsParams, personalWorkspace: string, workspace?: string): Promise<Notification[]> {
|
||||
//TODO: should join with attachments and reactions?
|
||||
const select = `
|
||||
SELECT n.message_id,
|
||||
n.context_id,
|
||||
m.card_id AS message_card,
|
||||
m.content AS message_content,
|
||||
m.creator AS message_creator,
|
||||
m.created AS message_created,
|
||||
nc.card_id,
|
||||
nc.archived_from,
|
||||
nc.last_view,
|
||||
nc.last_update,
|
||||
json_group_array(
|
||||
json_object(
|
||||
'id', p.id,
|
||||
'content', p.content,
|
||||
'creator', p.creator,
|
||||
'created', p.created
|
||||
)
|
||||
) AS patches
|
||||
FROM ${TableName.Notification} n
|
||||
JOIN
|
||||
${TableName.NotificationContext} nc ON n.context_id = nc.id
|
||||
JOIN
|
||||
${TableName.Message} m ON n.message_id = m.id
|
||||
LEFT JOIN
|
||||
${TableName.Patch} p ON p.message_id = m.id
|
||||
`;
|
||||
const where = this.buildNotificationWhere(params, personalWorkspace, workspace)
|
||||
const groupBy = `GROUP BY n.message_id, n.context_id, m.id, nc.card_id, nc.archived_from, nc.last_view, nc.last_update`;
|
||||
const orderBy = `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}`
|
||||
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
|
||||
const sql = [select, where, groupBy, orderBy, limit].join(' ')
|
||||
|
||||
const result = await this.select(sql)
|
||||
|
||||
return result.map(it => this.toNotification(it));
|
||||
}
|
||||
|
||||
buildContextWhere(params: FindNotificationContextParams, personalWorkspaces: string[], workspace?: string,): string {
|
||||
const where: string[] = []
|
||||
|
||||
if (workspace != null) {
|
||||
where.push(`nc.workspace_id = '${workspace}'`)
|
||||
}
|
||||
if (personalWorkspaces.length > 0) {
|
||||
where.push(`nc.personal_workspace IN (${personalWorkspaces.map(it => `'${it}'`).join(', ')})`)
|
||||
}
|
||||
|
||||
if (params.card != null) {
|
||||
where.push(`nc.card_id = '${params.card}'`)
|
||||
}
|
||||
|
||||
return `WHERE ${where.join(' AND ')}`
|
||||
}
|
||||
|
||||
buildNotificationWhere(params: FindNotificationsParams, personalWorkspace: string, workspace?: string): string {
|
||||
const where: string[] = [`nc.personal_workspace = '${personalWorkspace}'`]
|
||||
if (workspace != null) {
|
||||
where.push(`nc.workspace_id = '${workspace}'`)
|
||||
}
|
||||
|
||||
if (params.context != null) {
|
||||
where.push(`n.context_id = '${params.context}'`)
|
||||
}
|
||||
|
||||
if (params.read === true) {
|
||||
where.push(`nc.last_view IS NOT NULL AND nc.last_view >= m.created`)
|
||||
}
|
||||
|
||||
if (params.read === false) {
|
||||
where.push(`(nc.last_view IS NULL OR nc.last_view > m.created)`)
|
||||
}
|
||||
|
||||
if (params.archived === true) {
|
||||
where.push(`nc.archived_from IS NOT NULL AND nc.archived_from >= m.created`)
|
||||
}
|
||||
|
||||
if (params.archived === false) {
|
||||
where.push(`(nc.archived_from IS NULL OR nc.archived_from > m.created)`)
|
||||
}
|
||||
|
||||
return `WHERE ${where.join(' AND ')}`
|
||||
}
|
||||
|
||||
toNotificationContext(row: any): NotificationContext {
|
||||
return {
|
||||
id: row.id,
|
||||
card: row.card_id,
|
||||
archivedFrom: row.archived_from ? new Date(row.archived_from) : undefined,
|
||||
lastView: row.last_view ? new Date(row.last_view) : undefined,
|
||||
lastUpdate: row.last_update ? new Date(row.last_update) : undefined,
|
||||
workspace: row.workspace,
|
||||
personalWorkspace: row.personal_workspace
|
||||
}
|
||||
}
|
||||
|
||||
toNotification(row: any): Notification {
|
||||
const patches = JSON.parse(row.patches).filter((p: any) => p.created != null)
|
||||
const lastPatch = patches[patches.length - 1]
|
||||
const lastView = row.last_view ? new Date(row.last_view) : undefined
|
||||
const archivedFrom = row.archived_from ? new Date(row.archived_from) : undefined
|
||||
const created = new Date(row.message_created)
|
||||
return {
|
||||
message: {
|
||||
id: row.message_id,
|
||||
card: row.message_card,
|
||||
content: lastPatch?.content ?? row.message_content,
|
||||
creator: row.message_creator,
|
||||
created,
|
||||
edited: new Date(lastPatch?.created ?? row.message_created),
|
||||
reactions: [],
|
||||
attachments: []
|
||||
},
|
||||
context: row.context_id,
|
||||
read: lastView != null && lastView >= created,
|
||||
archived: archivedFrom != null && archivedFrom >= created
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type {CardID, ContextID, MessageID, RichText, SocialID } from "@hcengineering/communication-types"
|
||||
|
||||
export enum TableName {
|
||||
Message = 'message',
|
||||
Patch = 'patch',
|
||||
Attachment = 'attachment',
|
||||
Reaction = 'reaction',
|
||||
Notification = 'notification',
|
||||
NotificationContext = 'notification_context'
|
||||
}
|
||||
|
||||
export interface MessageDb {
|
||||
id: string
|
||||
workspace_id: string,
|
||||
card_id: string,
|
||||
content: RichText,
|
||||
creator: SocialID,
|
||||
created: Date,
|
||||
}
|
||||
|
||||
export interface PatchDb {
|
||||
id: string,
|
||||
message_id: MessageID,
|
||||
content: RichText,
|
||||
creator: SocialID,
|
||||
created: Date,
|
||||
}
|
||||
|
||||
export interface ReactionDb {
|
||||
message_id: MessageID,
|
||||
reaction: string,
|
||||
creator: SocialID
|
||||
created: Date
|
||||
}
|
||||
|
||||
export interface AttachmentDb {
|
||||
message_id: MessageID,
|
||||
card_id: CardID,
|
||||
creator: SocialID
|
||||
created: Date
|
||||
}
|
||||
|
||||
export interface NotificationDb {
|
||||
message_id: MessageID,
|
||||
context_id: ContextID
|
||||
}
|
||||
|
||||
export interface ContextDb {
|
||||
id: string
|
||||
workspace_id: string
|
||||
card_id: CardID
|
||||
personal_workspace: string
|
||||
|
||||
archived_from?: Date
|
||||
last_view?: Date
|
||||
last_update?: Date
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Sqlite3Worker1Promiser } from './connection'
|
||||
|
||||
export async function applyMigrations(worker: Sqlite3Worker1Promiser, dbId: string): Promise<void> {
|
||||
await migrationV1(worker, dbId)
|
||||
}
|
||||
|
||||
async function migrationV1(worker: Sqlite3Worker1Promiser, dbId: string): Promise<void> {
|
||||
await worker('exec', {
|
||||
dbId,
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS message
|
||||
(
|
||||
id TEXT NOT NULL,
|
||||
workspace_id TEXT NOT NULL,
|
||||
card_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
creator TEXT NOT NULL,
|
||||
created DATETIME NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
`
|
||||
})
|
||||
|
||||
await worker('exec', {
|
||||
dbId,
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS patch
|
||||
(
|
||||
id TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
creator VARCHAR(255) NOT NULL,
|
||||
created DATETIME NOT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE
|
||||
)
|
||||
`
|
||||
})
|
||||
await worker('exec', {
|
||||
dbId,
|
||||
sql: `CREATE INDEX IF NOT EXISTS idx_patch_message_id ON patch (message_id)`
|
||||
})
|
||||
|
||||
await worker('exec', {
|
||||
dbId,
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS attachment
|
||||
(
|
||||
message_id TEXT NOT NULL,
|
||||
card_id TEXT NOT NULL,
|
||||
creator VARCHAR(255) NOT NULL,
|
||||
created DATETIME NOT NULL,
|
||||
|
||||
PRIMARY KEY (message_id, card_id),
|
||||
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE
|
||||
)
|
||||
`
|
||||
})
|
||||
|
||||
await worker('exec', {
|
||||
dbId,
|
||||
sql: `CREATE INDEX IF NOT EXISTS attachment_message_idx ON attachment (message_id)`
|
||||
})
|
||||
|
||||
await worker('exec', {
|
||||
dbId,
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS reaction
|
||||
(
|
||||
message_id TEXT NOT NULL,
|
||||
reaction TEXT NOT NULL,
|
||||
creator VARCHAR(255) NOT NULL,
|
||||
created DATETIME NOT NULL,
|
||||
|
||||
PRIMARY KEY (message_id, creator, reaction),
|
||||
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE
|
||||
)
|
||||
`
|
||||
})
|
||||
|
||||
await worker('exec', {
|
||||
dbId,
|
||||
sql: `CREATE INDEX IF NOT EXISTS reaction_message_idx ON reaction (message_id)`
|
||||
})
|
||||
|
||||
await worker('exec', {
|
||||
dbId,
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS notification_context
|
||||
(
|
||||
id TEXT NOT NULL,
|
||||
workspace_id TEXT NOT NULL,
|
||||
card_id TEXT NOT NULL,
|
||||
personal_workspace TEXT NOT NULL,
|
||||
archived_from DATETIME,
|
||||
last_view DATETIME,
|
||||
last_update DATETIME,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (workspace_id, card_id, personal_workspace)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notification
|
||||
(
|
||||
message_id TEXT NOT NULL,
|
||||
context_id TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY (message_id, context_id),
|
||||
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (context_id) REFERENCES notification_context (id) ON DELETE CASCADE
|
||||
);
|
||||
`
|
||||
})
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declarationDir": "./types",
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/communication-types",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.147",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "./types/index.d.ts",
|
||||
@@ -21,8 +21,8 @@
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "0.7.15",
|
||||
"@hcengineering/card": "0.7.15"
|
||||
"@hcengineering/core": "^0.7.28",
|
||||
"@hcengineering/card": "^0.7.28"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -13,7 +13,14 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { createMessagesQuery, createNotificationsQuery } from '@hcengineering/communication-client-query'
|
||||
import type { Ref, Blob, AccountUuid, WorkspaceUuid, PersonId } from '@hcengineering/core'
|
||||
import type { Card } from '@hcengineering/card'
|
||||
|
||||
export * from './client'
|
||||
export { createMessagesQuery, createNotificationsQuery }
|
||||
export type BlobID = Ref<Blob>
|
||||
export type CardID = Ref<Card>
|
||||
export type SocialID = PersonId
|
||||
export type WorkspaceID = WorkspaceUuid
|
||||
export type RichText = string
|
||||
export type AccountID = AccountUuid
|
||||
|
||||
export type ID = string
|
||||
@@ -13,7 +13,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { CardID, Message, MessageID, RichText, SocialID } from './message'
|
||||
import type { BlobID, CardID, RichText, SocialID } from './core'
|
||||
import type { Message, MessageID, MessageType, MessageData } from './message'
|
||||
|
||||
export interface FileMetadata {
|
||||
card: CardID
|
||||
@@ -24,12 +25,24 @@ export interface FileMetadata {
|
||||
|
||||
export interface FileMessage {
|
||||
id: MessageID
|
||||
type: MessageType
|
||||
content: RichText
|
||||
creator: SocialID
|
||||
data?: MessageData
|
||||
created: Date
|
||||
edited?: Date
|
||||
reactions: FileReaction[]
|
||||
files: FileBlob[]
|
||||
thread?: FileThread
|
||||
}
|
||||
|
||||
export interface FileBlob {
|
||||
blobId: BlobID
|
||||
type: string
|
||||
filename: string
|
||||
size: number
|
||||
creator: SocialID
|
||||
created: Date
|
||||
reactions: FileReaction[]
|
||||
thread?: FileThread
|
||||
}
|
||||
|
||||
export interface FileReaction {
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
export * from './core.ts'
|
||||
export * from './file.ts'
|
||||
export * from './message.ts'
|
||||
export * from './notification.ts'
|
||||
export * from './query.ts'
|
||||
export * from './file.ts'
|
||||
|
||||
@@ -13,38 +13,73 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Ref, Blob, PersonId, WorkspaceUuid } from '@hcengineering/core'
|
||||
import type { Card } from '@hcengineering/card'
|
||||
import type { Attribute, Class, Mixin, Ref } from '@hcengineering/core'
|
||||
import type { BlobID, CardID, ID, RichText, SocialID } from './core'
|
||||
import type { Card, Tag } from '@hcengineering/card'
|
||||
|
||||
export type BlobID = Ref<Blob>
|
||||
export type CardID = Ref<Card>
|
||||
export type SocialID = PersonId
|
||||
export type WorkspaceID = WorkspaceUuid
|
||||
export type RichText = string
|
||||
|
||||
export type ID = string
|
||||
export type MessageID = string & { message: true }
|
||||
export type MessageID = ID & { message: true }
|
||||
|
||||
export interface Message {
|
||||
id: MessageID
|
||||
card: CardID
|
||||
type: MessageType
|
||||
content: RichText
|
||||
creator: SocialID
|
||||
created: Date
|
||||
data?: MessageData
|
||||
|
||||
edited?: Date
|
||||
thread?: Thread
|
||||
reactions: Reaction[]
|
||||
attachments: Attachment[]
|
||||
files: File[]
|
||||
}
|
||||
|
||||
export enum MessageType {
|
||||
Message = 'message',
|
||||
Activity = 'activity'
|
||||
}
|
||||
|
||||
export type MessageData = ActivityMessageData | any
|
||||
|
||||
export interface ActivityMessage extends Message {
|
||||
type: MessageType.Activity
|
||||
data: ActivityMessageData
|
||||
}
|
||||
|
||||
export interface ActivityMessageData {
|
||||
action: 'create' | 'remove' | 'update'
|
||||
update?: ActivityUpdate
|
||||
}
|
||||
|
||||
export type ActivityUpdate = ActivityAttributeUpdate | ActivityTagUpdate
|
||||
export enum ActivityUpdateType {
|
||||
Attribute = 'attribute',
|
||||
Tag = 'tag'
|
||||
}
|
||||
|
||||
export interface ActivityTagUpdate {
|
||||
type: ActivityUpdateType.Tag
|
||||
tag: Ref<Tag>
|
||||
action: 'add' | 'remove'
|
||||
}
|
||||
|
||||
type AttributeValue = string | number | null
|
||||
|
||||
export interface ActivityAttributeUpdate {
|
||||
type: ActivityUpdateType.Attribute
|
||||
attrKey: string
|
||||
attrClass: Ref<Class<Attribute<Card>>>
|
||||
mixin?: Ref<Mixin<Card>>
|
||||
set?: AttributeValue | AttributeValue[]
|
||||
added?: AttributeValue[]
|
||||
removed?: AttributeValue[]
|
||||
}
|
||||
|
||||
export interface MessagesGroup {
|
||||
card: CardID
|
||||
blobId: BlobID
|
||||
fromId: MessageID
|
||||
toId: MessageID
|
||||
fromDate: Date
|
||||
toDate: Date
|
||||
fromSec: Date
|
||||
toSec: Date
|
||||
count: number
|
||||
patches?: Patch[]
|
||||
}
|
||||
@@ -62,7 +97,9 @@ export enum PatchType {
|
||||
addReaction = 'addReaction',
|
||||
removeReaction = 'removeReaction',
|
||||
addReply = 'addReply',
|
||||
removeReply = 'removeReply'
|
||||
removeReply = 'removeReply',
|
||||
addFile = 'addFile',
|
||||
removeFile = 'removeFile'
|
||||
}
|
||||
|
||||
export interface Reaction {
|
||||
@@ -72,9 +109,13 @@ export interface Reaction {
|
||||
created: Date
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
message: MessageID
|
||||
export interface File {
|
||||
card: CardID
|
||||
message: MessageID
|
||||
blobId: BlobID
|
||||
type: string
|
||||
filename: string
|
||||
size: number
|
||||
creator: SocialID
|
||||
created: Date
|
||||
}
|
||||
|
||||
@@ -13,29 +13,32 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Message, CardID, WorkspaceID } from './message'
|
||||
import type { AccountID, CardID, ID } from './core'
|
||||
import type { Message, MessageID, MessagesGroup, Patch } from './message'
|
||||
|
||||
export type ContextID = string & { context: true }
|
||||
export type ContextID = ID & { context: true }
|
||||
export type NotificationID = ID & { notification: true }
|
||||
|
||||
export interface Collaborator {
|
||||
account: AccountID
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
message: Message
|
||||
id: NotificationID
|
||||
context: ContextID
|
||||
read: boolean
|
||||
archived: boolean
|
||||
created: Date
|
||||
messageId?: MessageID
|
||||
message?: Message
|
||||
messageGroup?: MessagesGroup
|
||||
patches?: Patch[]
|
||||
}
|
||||
|
||||
export interface NotificationContext {
|
||||
id: ContextID
|
||||
card: CardID
|
||||
workspace: WorkspaceID
|
||||
personalWorkspace: WorkspaceID
|
||||
archivedFrom?: Date
|
||||
lastView?: Date
|
||||
lastUpdate?: Date
|
||||
}
|
||||
|
||||
export interface NotificationContextUpdate {
|
||||
archivedFrom?: Date
|
||||
lastView?: Date
|
||||
lastUpdate?: Date
|
||||
account: AccountID
|
||||
lastUpdate: Date
|
||||
lastView: Date
|
||||
notifications?: Notification[]
|
||||
}
|
||||
|
||||
+38
-23
@@ -15,24 +15,23 @@
|
||||
|
||||
import { SortingOrder } from '@hcengineering/core'
|
||||
|
||||
import type { BlobID, CardID, MessageID } from './message'
|
||||
import type { MessageID } from './message'
|
||||
import type { ContextID } from './notification'
|
||||
import type { AccountID, BlobID, CardID } from './core'
|
||||
|
||||
export { SortingOrder }
|
||||
|
||||
export type ComparisonOperator = 'less' | 'lessOrEqual' | 'greater' | 'greaterOrEqual' | 'notEqual'
|
||||
|
||||
type Exclusive<T> = {
|
||||
[K in keyof T]: Record<K, T[K]> & Partial<Record<Exclude<keyof T, K>, never>>
|
||||
}[keyof T]
|
||||
|
||||
export interface Window<T> {
|
||||
getResult(): T[]
|
||||
|
||||
loadNextPage(): Promise<void>
|
||||
|
||||
loadPrevPage(): Promise<void>
|
||||
|
||||
hasNextPage(): boolean
|
||||
|
||||
hasPrevPage(): boolean
|
||||
}
|
||||
|
||||
@@ -44,27 +43,43 @@ interface FindParams {
|
||||
export interface FindMessagesParams extends FindParams {
|
||||
id?: MessageID
|
||||
card: CardID
|
||||
created?: Exclusive<Record<ComparisonOperator, Date>> | Date
|
||||
}
|
||||
|
||||
export interface FindNotificationsParams extends FindParams {
|
||||
context?: ContextID
|
||||
message?: MessageID
|
||||
read?: boolean
|
||||
archived?: boolean
|
||||
created?: Exclusive<Record<ComparisonOperator, Date>> | Date
|
||||
}
|
||||
|
||||
export interface FindNotificationContextParams extends FindParams {
|
||||
id?: ContextID
|
||||
card?: CardID
|
||||
files?: boolean
|
||||
reactions?: boolean
|
||||
replies?: boolean
|
||||
created?: Partial<Record<ComparisonOperator, Date>> | Date
|
||||
}
|
||||
|
||||
export interface FindMessagesGroupsParams extends FindParams {
|
||||
card: CardID
|
||||
blobId?: BlobID
|
||||
withPatches?: boolean
|
||||
fromDate?: Partial<Record<ComparisonOperator, Date>> | Date
|
||||
toDate?: Partial<Record<ComparisonOperator, Date>> | Date
|
||||
orderBy?: 'fromDate' | 'toDate'
|
||||
patches?: boolean
|
||||
fromSec?: Partial<Record<ComparisonOperator, Date>> | Date
|
||||
toSec?: Partial<Record<ComparisonOperator, Date>> | Date
|
||||
orderBy?: 'fromSec' | 'toSec'
|
||||
}
|
||||
|
||||
export interface FindNotificationContextParams extends FindParams {
|
||||
id?: ContextID
|
||||
card?: CardID
|
||||
lastUpdate?: Partial<Record<ComparisonOperator, Date>> | Date
|
||||
account?: AccountID | AccountID[]
|
||||
notifications?: {
|
||||
message?: boolean
|
||||
limit: number
|
||||
order: SortingOrder
|
||||
read?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface FindNotificationsParams extends FindParams {
|
||||
context?: ContextID
|
||||
read?: boolean
|
||||
created?: Partial<Record<ComparisonOperator, Date>> | Date
|
||||
account?: AccountID | AccountID[]
|
||||
message?: boolean
|
||||
}
|
||||
|
||||
export interface FindCollaboratorsParams extends FindParams {
|
||||
card: CardID
|
||||
account?: AccountID | AccountID[]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hcengineering/communication-client-ws",
|
||||
"version": "0.1.61",
|
||||
"main": "dist/index.js",
|
||||
"name": "@hcengineering/communication-yaml",
|
||||
"version": "0.1.147",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "./types/index.d.ts",
|
||||
"files": [
|
||||
@@ -10,21 +10,22 @@
|
||||
"types/**/*.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"bundle": "bun run bundle:browser",
|
||||
"bundle:browser": "bun build src/index.ts --outdir dist --target browser"
|
||||
"bundle": "bun run bundle:browser && bun run bundle:node",
|
||||
"bundle:browser": "bun build src/index.ts --outdir dist --target browser",
|
||||
"bundle:node": "bun build src/index.ts --target node --format cjs --outfile dist/index.cjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"@hcengineering/communication-client-query": "workspace:*",
|
||||
"@hcengineering/communication-sdk-types": "workspace:*",
|
||||
"@msgpack/msgpack": "^3.0.0-beta2"
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/js-yaml": "^4.0.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-types": "workspace:*",
|
||||
"@hcengineering/communication-shared": "workspace:*",
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hcengineering/communication.git"
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { FileMessage, Message } from '@hcengineering/communication-types'
|
||||
|
||||
export function deserializeMessage(message: Message): FileMessage {
|
||||
return {
|
||||
id: message.id,
|
||||
type: message.type,
|
||||
content: message.content,
|
||||
edited: message.edited,
|
||||
creator: message.creator,
|
||||
created: message.created,
|
||||
data: message.data,
|
||||
thread:
|
||||
message.thread != null
|
||||
? {
|
||||
thread: message.thread.thread,
|
||||
repliesCount: message.thread.repliesCount,
|
||||
lastReply: message.thread.lastReply
|
||||
}
|
||||
: undefined,
|
||||
files: message.files.map((file) => ({
|
||||
blobId: file.blobId,
|
||||
type: file.type,
|
||||
filename: file.filename,
|
||||
size: file.size,
|
||||
created: file.created,
|
||||
creator: file.creator
|
||||
})),
|
||||
reactions: message.reactions.map((reaction) => ({
|
||||
reaction: reaction.reaction,
|
||||
creator: reaction.creator,
|
||||
created: reaction.created
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,5 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
export * from './adapter.ts'
|
||||
export * from './parse.ts'
|
||||
export * from './deserialize.ts'
|
||||
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { retry, type RetryOptions } from '@hcengineering/communication-shared'
|
||||
import type {
|
||||
FileMessage,
|
||||
FileMetadata,
|
||||
@@ -22,8 +23,6 @@ import type {
|
||||
} from '@hcengineering/communication-types'
|
||||
import yaml from 'js-yaml'
|
||||
|
||||
import { retry, type RetryOptions } from './retry'
|
||||
|
||||
export async function loadGroupFile(
|
||||
workspace: WorkspaceID,
|
||||
filesUrl: string,
|
||||
@@ -64,11 +63,13 @@ export function parseYaml(data: string): ParsedFile {
|
||||
metadata,
|
||||
messages: messages.map((message) => ({
|
||||
id: message.id,
|
||||
type: message.type,
|
||||
card: metadata.card,
|
||||
content: message.content,
|
||||
edited: message.edited,
|
||||
creator: message.creator,
|
||||
created: message.created,
|
||||
data: message.data,
|
||||
thread: message.thread
|
||||
? {
|
||||
card: metadata.card,
|
||||
@@ -78,7 +79,11 @@ export function parseYaml(data: string): ParsedFile {
|
||||
lastReply: message.thread.lastReply
|
||||
}
|
||||
: undefined,
|
||||
attachments: [],
|
||||
files: message.files.map((file) => ({
|
||||
...file,
|
||||
message: message.id,
|
||||
card: metadata.card
|
||||
})),
|
||||
reactions: message.reactions.map((reaction) => ({
|
||||
message: message.id,
|
||||
reaction: reaction.reaction,
|
||||
Reference in New Issue
Block a user