diff --git a/bun.lock b/bun.lock index e341aec615..fc721f8680 100644 --- a/bun.lock +++ b/bun.lock @@ -4,19 +4,19 @@ "": { "name": "@hcengineering/communication", "devDependencies": { - "@eslint/js": "^9.21.0", - "@types/bun": "^1.2.4", - "bun-types": "^1.2.4", - "eslint": "^9.21.0", + "@eslint/js": "^9.23.0", + "@types/bun": "^1.2.5", + "bun-types": "^1.2.5", + "eslint": "^9.23.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-prettier": "^5.2.4", "prettier": "^3.5.3", - "typescript-eslint": "^8.25.0", + "typescript-eslint": "^8.27.0", }, }, "packages/client-query": { "name": "@hcengineering/communication-client-query", - "version": "0.1.61", + "version": "0.1.147", "dependencies": { "@hcengineering/communication-query": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", @@ -30,41 +30,9 @@ "typescript": "^5.6.3", }, }, - "packages/client-sqlite": { - "name": "@hcengineering/communication-client-sqlite", - "version": "0.1.61", - "dependencies": { - "@hcengineering/communication-sdk-types": "workspace:*", - "@hcengineering/communication-sqlite-wasm": "workspace:*", - "@hcengineering/communication-types": "workspace:*", - "fast-equals": "^5.0.1", - }, - "devDependencies": { - "@types/bun": "^1.1.14", - }, - "peerDependencies": { - "typescript": "^5.6.3", - }, - }, - "packages/client-ws": { - "name": "@hcengineering/communication-client-ws", - "version": "0.1.61", - "dependencies": { - "@hcengineering/communication-client-query": "workspace:*", - "@hcengineering/communication-sdk-types": "workspace:*", - "@hcengineering/communication-types": "workspace:*", - "@msgpack/msgpack": "^3.0.0-beta2", - }, - "devDependencies": { - "@types/bun": "^1.1.14", - }, - "peerDependencies": { - "typescript": "^5.6.3", - }, - }, "packages/cockroach": { "name": "@hcengineering/communication-cockroach", - "version": "0.1.61", + "version": "0.1.147", "dependencies": { "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-shared": "workspace:*", @@ -79,27 +47,14 @@ "typescript": "^5.6.3", }, }, - "packages/examples": { - "name": "@hcengineering/communication-examples", - "version": "0.1.61", - "dependencies": { - "@hcengineering/communication-client-ws": "workspace:*", - "@hcengineering/communication-types": "workspace:*", - }, - "devDependencies": { - "@types/bun": "^1.1.14", - }, - "peerDependencies": { - "typescript": "^5.6.3", - }, - }, "packages/query": { "name": "@hcengineering/communication-query", - "version": "0.1.61", + "version": "0.1.147", "dependencies": { "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-shared": "workspace:*", "@hcengineering/communication-types": "workspace:*", + "@hcengineering/communication-yaml": "workspace:*", "fast-equals": "^5.0.1", }, "devDependencies": { @@ -110,12 +65,30 @@ "typescript": "^5.6.3", }, }, + "packages/rest-client": { + "name": "@hcengineering/communication-rest-client", + "version": "0.1.147", + "dependencies": { + "@hcengineering/communication-sdk-types": "workspace:*", + "@hcengineering/communication-shared": "workspace:*", + "@hcengineering/communication-types": "workspace:*", + "@hcengineering/core": "^0.7.28", + "snappyjs": "^0.7.0", + }, + "devDependencies": { + "@types/bun": "^1.1.14", + "@types/snappyjs": "^0.7.1", + }, + "peerDependencies": { + "typescript": "^5.6.3", + }, + }, "packages/sdk-types": { "name": "@hcengineering/communication-sdk-types", - "version": "0.1.61", + "version": "0.1.147", "dependencies": { "@hcengineering/communication-types": "workspace:*", - "@hcengineering/core": "0.7.15", + "@hcengineering/core": "^0.7.28", }, "devDependencies": { "@types/bun": "^1.1.14", @@ -126,13 +99,14 @@ }, "packages/server": { "name": "@hcengineering/communication-server", - "version": "0.1.61", + "version": "0.1.147", "dependencies": { + "@hcengineering/account-client": "^0.7.28", "@hcengineering/communication-cockroach": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-types": "workspace:*", - "@hcengineering/core": "0.7.15", - "@hcengineering/server-token": "0.7.15", + "@hcengineering/core": "^0.7.28", + "@hcengineering/server-token": "^0.7.28", }, "devDependencies": { "@types/bun": "^1.1.14", @@ -146,28 +120,9 @@ }, "packages/shared": { "name": "@hcengineering/communication-shared", - "version": "0.1.61", + "version": "0.1.147", "dependencies": { "@hcengineering/communication-types": "workspace:*", - "js-yaml": "^4.1.0", - }, - "devDependencies": { - "@types/bun": "^1.1.14", - "@types/js-yaml": "^4.0.9", - }, - "peerDependencies": { - "typescript": "^5.6.3", - }, - }, - "packages/sqlite-wasm": { - "name": "@hcengineering/communication-sqlite-wasm", - "version": "0.1.61", - "dependencies": { - "@hcengineering/communication-sdk-types": "workspace:*", - "@hcengineering/communication-types": "workspace:*", - "@sqlite.org/sqlite-wasm": "^3.47.1-build1", - "path": "^0.12.7", - "uuid": "^11.0.3", }, "devDependencies": { "@types/bun": "^1.1.14", @@ -178,10 +133,10 @@ }, "packages/types": { "name": "@hcengineering/communication-types", - "version": "0.1.61", + "version": "0.1.147", "dependencies": { - "@hcengineering/card": "0.7.15", - "@hcengineering/core": "0.7.15", + "@hcengineering/card": "^0.7.28", + "@hcengineering/core": "^0.7.28", }, "devDependencies": { "@types/bun": "^1.1.14", @@ -190,6 +145,22 @@ "typescript": "^5.6.3", }, }, + "packages/yaml": { + "name": "@hcengineering/communication-yaml", + "version": "0.1.147", + "dependencies": { + "@hcengineering/communication-shared": "workspace:*", + "@hcengineering/communication-types": "workspace:*", + "js-yaml": "^4.1.0", + }, + "devDependencies": { + "@types/bun": "^1.1.14", + "@types/js-yaml": "^4.0.9", + }, + "peerDependencies": { + "typescript": "^5.6.3", + }, + }, }, "packages": { "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], @@ -202,11 +173,13 @@ "@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.2.0", "", {}, "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ=="], + "@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ=="], + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], - "@eslint/js": ["@eslint/js@9.21.0", "", {}, "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw=="], + "@eslint/js": ["@eslint/js@9.23.0", "", {}, "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], @@ -222,41 +195,39 @@ "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.0", "", { "dependencies": { "tslib": "2" } }, "sha512-4rB4g+3hESy1bHSBG3tDFaMY2CH67iT7yne1e+0CLTsGLDcmoEWWpJjjpWVaYgYfYuohIRuo0E+N536gd2ZHZA=="], - "@hcengineering/analytics": ["@hcengineering/analytics@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/analytics/0.7.15/46f0006d8f4ba8ec04105770e6418251def15296", { "dependencies": { "@hcengineering/platform": "^0.7.15" } }, "sha512-U+tswirTFX0z7Sr51zfOEXbtrtfnf2xLzWTIrPip929g+oULJjCKMqoh+3HHVCF4HFm97x0P3qaRtW2hLgxyAQ=="], + "@hcengineering/account-client": ["@hcengineering/account-client@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/account-client/0.7.28/32c9bc0dd297f00f4d5461467f25e072d01b9c76", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28" } }, "sha512-k1dkioX+jYrLyAXLLLsBYVCdZKf3WjhnDHw/yeAQgLQQ/9W2ffLtDMZ8lYWV3XMQ3eWrUCodtTDOiAQgXd/LEQ=="], - "@hcengineering/card": ["@hcengineering/card@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/card/0.7.15/70ba947306289e9ec1719ec4670abb5abadd9a74", { "dependencies": { "@hcengineering/core": "^0.7.15", "@hcengineering/platform": "^0.7.15", "@hcengineering/ui": "^0.7.15" } }, "sha512-xIM9UwFqhyjD9EJs0egn4kf3Q9Br8+spYHkzNl+5IPdL5d5itDIuy5HS9ng6XZ1BXApauo8MTl1DcaqJMneNbw=="], + "@hcengineering/analytics": ["@hcengineering/analytics@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/analytics/0.7.28/a8faf3d575bb28b9682b659495e738f8e0ca596e", { "dependencies": { "@hcengineering/platform": "^0.7.28" } }, "sha512-+CI/wJ8zgop+3mEcvhlB1UJBU6tIKq2Y7qTichZjGli8BckEIEwbD4j2beu2qXRSoTT2yUGWM9zpS9+fmH/DAw=="], + + "@hcengineering/card": ["@hcengineering/card@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/card/0.7.28/5348996f5b7b4a34517780a5bf324122cbddfbad", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "@hcengineering/ui": "^0.7.28" } }, "sha512-1d3allyx9jTpE/gr9kk/g5qo5848i4FKsRiHgCpJqzNEdRW7TAednoHAr2hKRU5DZC/i3Br56axSkUQzV2Idhg=="], "@hcengineering/communication-client-query": ["@hcengineering/communication-client-query@workspace:packages/client-query"], - "@hcengineering/communication-client-sqlite": ["@hcengineering/communication-client-sqlite@workspace:packages/client-sqlite"], - - "@hcengineering/communication-client-ws": ["@hcengineering/communication-client-ws@workspace:packages/client-ws"], - "@hcengineering/communication-cockroach": ["@hcengineering/communication-cockroach@workspace:packages/cockroach"], - "@hcengineering/communication-examples": ["@hcengineering/communication-examples@workspace:packages/examples"], - "@hcengineering/communication-query": ["@hcengineering/communication-query@workspace:packages/query"], + "@hcengineering/communication-rest-client": ["@hcengineering/communication-rest-client@workspace:packages/rest-client"], + "@hcengineering/communication-sdk-types": ["@hcengineering/communication-sdk-types@workspace:packages/sdk-types"], "@hcengineering/communication-server": ["@hcengineering/communication-server@workspace:packages/server"], "@hcengineering/communication-shared": ["@hcengineering/communication-shared@workspace:packages/shared"], - "@hcengineering/communication-sqlite-wasm": ["@hcengineering/communication-sqlite-wasm@workspace:packages/sqlite-wasm"], - "@hcengineering/communication-types": ["@hcengineering/communication-types@workspace:packages/types"], - "@hcengineering/core": ["@hcengineering/core@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/core/0.7.15/785cb20f9adc4a3a3c2179a0ae38316f4b4f933f", { "dependencies": { "@hcengineering/analytics": "^0.7.15", "@hcengineering/platform": "^0.7.15", "fast-equals": "^5.2.2" } }, "sha512-YhRRdtcSuNSQUdh4TO5u3u/w/vE7fcildc51wnL7Erw7VZ1rVANEx9a3mScnViVH89xw0oAcfDAm/AbH5+EZcQ=="], + "@hcengineering/communication-yaml": ["@hcengineering/communication-yaml@workspace:packages/yaml"], - "@hcengineering/platform": ["@hcengineering/platform@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/platform/0.7.15/3e6c41c4abfd6425be49d9b31afef627dcafa4cb", { "dependencies": { "intl-messageformat": "^10.7.14" } }, "sha512-zv6afaQVu/T0MXrWch8JaxdVjLSUHDpIx3jgY1JE5xSy+FN8bjC30FR/RGavuMtm4z25rFsZQRPfr4qmZYZirQ=="], + "@hcengineering/core": ["@hcengineering/core@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/core/0.7.28/43b84c63262e2b8fe5554c1967812e2ce6bb56e1", { "dependencies": { "@hcengineering/analytics": "^0.7.28", "@hcengineering/platform": "^0.7.28", "fast-equals": "^5.2.2" } }, "sha512-qv4QE7mkect6JYlGMljW5CQ2uU80hFGq0E17lZ5rO79h1boWEKLJpINv+w3J2BdR/nNG7taIsdZg3yfl9fY4bg=="], - "@hcengineering/server-token": ["@hcengineering/server-token@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/server-token/0.7.15/5443096926f96919ed8ed748e90e3474137519ec", { "dependencies": { "@hcengineering/core": "^0.7.15", "@hcengineering/platform": "^0.7.15", "jwt-simple": "^0.5.6" } }, "sha512-XEh0nDSSxMn34a9AHeXIgRh6iO025hKmGNXLZlXnm4kRIYODzMLhigds2H/RvmUjznfUTm9Bpy3EFLq9WFmzWg=="], + "@hcengineering/platform": ["@hcengineering/platform@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/platform/0.7.28/2126ff97284b679ceab3c86f24febd73a3a28712", { "dependencies": { "intl-messageformat": "^10.7.14" } }, "sha512-0zEifwsLdOs/M6TZ9xnPrQTTlhwW28sooiyL4ZfQsiC2AJUy05vLP5/W8V9eDLta+EiR5bjtlulGsg1GfPjUoQ=="], - "@hcengineering/theme": ["@hcengineering/theme@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/theme/0.7.15/f02e70ea8421f0cd3e5b52c34ee69488d361b3e6", { "dependencies": { "@hcengineering/analytics": "^0.7.15", "@hcengineering/platform": "^0.7.15", "svelte": "^4.2.19" } }, "sha512-d+rX4Jp4J4YELBi763M5Zfe+3x5gyQjV1NcuP1GIvSCe9tXNboKEp5X2SPdqFlP1sBfE5sgVECIAtmZkhMAqcQ=="], + "@hcengineering/server-token": ["@hcengineering/server-token@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/server-token/0.7.28/b1cffcb628aa0c6ac73171ef340c8ccf0f85ab07", { "dependencies": { "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "jwt-simple": "^0.5.6" } }, "sha512-TaHwzxuoFKN6jnWHIZk0UcRmB+UQ1QE1HZzUh5doPSFUYOAtTrSb32iJDA8e8oEADcJpOp9RGIUP/EP/VED7BQ=="], - "@hcengineering/ui": ["@hcengineering/ui@0.7.15", "https://npm.pkg.github.com/download/@hcengineering/ui/0.7.15/8462766246888da93d3f51e19e0b11d7c40312f6", { "dependencies": { "@hcengineering/analytics": "^0.7.15", "@hcengineering/core": "^0.7.15", "@hcengineering/platform": "^0.7.15", "@hcengineering/theme": "^0.7.15", "autolinker": "4.0.0", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "dompurify": "^3.1.6", "emojibase": "^16.0.0", "fast-equals": "^5.2.2", "hls.js": "^1.5.20", "svelte": "^4.2.19" } }, "sha512-OkllPX5wH3eJ3WSEzr+PJ0H8H2ZyXWGNHdUmwQSPW6UTZqAYia9WtCVO3sxR7SMtZtjAnKPitRycSG4Q/z8GVA=="], + "@hcengineering/theme": ["@hcengineering/theme@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/theme/0.7.28/557640a7ba2ffaa4d6e5411a54577f60dc528300", { "dependencies": { "@hcengineering/analytics": "^0.7.28", "@hcengineering/platform": "^0.7.28", "svelte": "^4.2.19" } }, "sha512-Hv3wQKHr327TC7wMHCBiG4bMtg8vSlb7+UFwdKI8ztEs3359meqkXu+exqi7Nl5334voXIz5ZFxjwZwOMzNSFA=="], + + "@hcengineering/ui": ["@hcengineering/ui@0.7.28", "https://npm.pkg.github.com/download/@hcengineering/ui/0.7.28/82025fc775702ea8c8569611356662bfbf28eb74", { "dependencies": { "@hcengineering/analytics": "^0.7.28", "@hcengineering/core": "^0.7.28", "@hcengineering/platform": "^0.7.28", "@hcengineering/theme": "^0.7.28", "autolinker": "4.0.0", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "dompurify": "^3.1.6", "emojibase": "^16.0.0", "fast-equals": "^5.2.2", "hls.js": "^1.5.20", "plyr": "^3.7.8", "svelte": "^4.2.19" } }, "sha512-QsUpO0+Idu1n2dC6KtorPJ6SSp30It8FHSuG3Y+Awv4td/ftVocOSFqhskbk+us9Yw9KqOYgkqhGYEHbfRfw4Q=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -276,21 +247,17 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], - "@msgpack/msgpack": ["@msgpack/msgpack@3.0.1", "", {}, "sha512-9qysoVTITLcOFIIJeXbdtUgvvY25ojUp+WWfLc0O4H4KKWeamUNAqkjS5mej/PnVDnH70llWKNa7pzv5U4TqVQ=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@pkgr/core": ["@pkgr/core@0.1.1", "", {}, "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA=="], - - "@sqlite.org/sqlite-wasm": ["@sqlite.org/sqlite-wasm@3.47.1-build1", "", { "bin": { "sqlite-wasm": "bin/index.js" } }, "sha512-3qnVGab3sjJ8ov0ce1rQGZrMkglYEQ/q5fnq1s1BpRWFMYsiJVaLD1zKepcI9E9r3qx7929+2G27Hfsasvrm6Q=="], + "@pkgr/core": ["@pkgr/core@0.2.0", "", {}, "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ=="], "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="], - "@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="], + "@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], @@ -312,7 +279,7 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], - "@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="], + "@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], "@types/qs": ["@types/qs@6.9.18", "", {}, "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="], @@ -322,27 +289,29 @@ "@types/serve-static": ["@types/serve-static@1.15.7", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw=="], + "@types/snappyjs": ["@types/snappyjs@0.7.1", "", {}, "sha512-OxjzJ6cQZstysMh6PEwZWmK9qlKZyezHJKOkcUkZDooSFuog2votUEKkxMaTq51UQF3cJkXKQ+XGlj4FSl8JQQ=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], - "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + "@types/ws": ["@types/ws@8.18.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.25.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/type-utils": "8.25.0", "@typescript-eslint/utils": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.27.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.27.0", "@typescript-eslint/type-utils": "8.27.0", "@typescript-eslint/utils": "8.27.0", "@typescript-eslint/visitor-keys": "8.27.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.25.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/types": "8.25.0", "@typescript-eslint/typescript-estree": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.27.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.27.0", "@typescript-eslint/types": "8.27.0", "@typescript-eslint/typescript-estree": "8.27.0", "@typescript-eslint/visitor-keys": "8.27.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0" } }, "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.27.0", "", { "dependencies": { "@typescript-eslint/types": "8.27.0", "@typescript-eslint/visitor-keys": "8.27.0" } }, "sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.25.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.25.0", "@typescript-eslint/utils": "8.25.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.27.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.27.0", "@typescript-eslint/utils": "8.27.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-wVArTVcz1oJOIEJxui/nRhV0TXzD/zMSOYi/ggCfNq78EIszddXcJb7r4RCp/oBrjt8n9A0BSxRMKxHftpDxDA=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.25.0", "", {}, "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.27.0", "", {}, "sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.27.0", "", { "dependencies": { "@typescript-eslint/types": "8.27.0", "@typescript-eslint/visitor-keys": "8.27.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.25.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/types": "8.25.0", "@typescript-eslint/typescript-estree": "8.25.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.27.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.27.0", "@typescript-eslint/types": "8.27.0", "@typescript-eslint/typescript-estree": "8.27.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.27.0", "", { "dependencies": { "@typescript-eslint/types": "8.27.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ=="], - "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -364,7 +333,7 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="], + "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], @@ -378,10 +347,14 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "core-js": ["core-js@3.41.0", "", {}, "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="], + "custom-event-polyfill": ["custom-event-polyfill@1.0.7", "", {}, "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w=="], + "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], "date-fns-tz": ["date-fns-tz@2.0.1", "", { "peerDependencies": { "date-fns": "2.x" } }, "sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA=="], @@ -398,13 +371,13 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.21.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg=="], + "eslint": ["eslint@9.23.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.23.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw=="], "eslint-config-prettier": ["eslint-config-prettier@9.1.0", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw=="], - "eslint-plugin-prettier": ["eslint-plugin-prettier@5.2.3", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.9.1" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw=="], + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.2.4", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.10.2" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-SFtuYmnhwYCtuCDTKPoK+CEzCnEgKTU2qTLwoCxvrC0MFBTIXo1i6hDYOI4cwHaE5GZtlWmTN3YfucYi7KJwPw=="], - "eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="], + "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="], "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], @@ -432,7 +405,7 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="], + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], @@ -460,8 +433,6 @@ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - "inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="], - "intl-messageformat": ["intl-messageformat@10.7.15", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.3", "@formatjs/fast-memoize": "2.2.6", "@formatjs/icu-messageformat-parser": "2.11.1", "tslib": "2" } }, "sha512-LRyExsEsefQSBjU2p47oAheoKz+EOJxSLDdjOaEjdriajfHsMXOmV/EhMvYSg9bAgCUHasuAC+mcUBe/95PfIg=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -488,6 +459,8 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "loadjs": ["loadjs@4.3.0", "", {}, "sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ=="], + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -516,8 +489,6 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - "path": ["path@0.12.7", "", { "dependencies": { "process": "^0.11.1", "util": "^0.10.3" } }, "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -526,6 +497,8 @@ "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "plyr": ["plyr@3.7.8", "", { "dependencies": { "core-js": "^3.26.1", "custom-event-polyfill": "^1.0.7", "loadjs": "^4.2.0", "rangetouch": "^2.0.1", "url-polyfill": "^1.1.12" } }, "sha512-yG/EHDobwbB/uP+4Bm6eUpJ93f8xxHjjk2dYcD1Oqpe1EcuQl5tzzw9Oq+uVAzd2lkM11qZfydSiyIpiB8pgdA=="], + "postgres": ["postgres@3.4.5", "", {}, "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], @@ -534,17 +507,17 @@ "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], - "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "rangetouch": ["rangetouch@2.0.1", "", {}, "sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA=="], + "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -554,6 +527,8 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "snappyjs": ["snappyjs@0.7.0", "", {}, "sha512-u5iEEXkMe2EInQio6Wv9LWHOQYRDbD2O9hzS27GpT/lwfIQhTCnHCTqedqHIHe9ZcvQo+9au6vngQayipz1NYw=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], @@ -562,7 +537,7 @@ "svelte": ["svelte@4.2.19", "", { "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/trace-mapping": "^0.3.18", "@types/estree": "^1.0.1", "acorn": "^8.9.0", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "code-red": "^1.0.3", "css-tree": "^2.3.1", "estree-walker": "^3.0.3", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", "periscopic": "^3.1.0" } }, "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw=="], - "synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw=="], + "synckit": ["synckit@0.10.2", "", { "dependencies": { "@pkgr/core": "^0.2.0", "tslib": "^2.8.1" } }, "sha512-cSGiaCPhFzeFIQY8KKEacv46LclENY4d60jgkwCrKomvRkIjtMyss1dPkHLp/62c1leuOjEedB1+lWcwqTJSvA=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -572,17 +547,17 @@ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], - "typescript-eslint": ["typescript-eslint@8.25.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.25.0", "@typescript-eslint/parser": "8.25.0", "@typescript-eslint/utils": "8.25.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q=="], + "typescript-eslint": ["typescript-eslint@8.27.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.27.0", "@typescript-eslint/parser": "8.27.0", "@typescript-eslint/utils": "8.27.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-ZZ/8+Y0rRUMuW1gJaPtLWe4ryHbsPLzzibk5Sq+IFa2aOH1Vo0gPr1fbA6pOnzBke7zC2Da4w8AyCgxKXo3lqA=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "util": ["util@0.10.4", "", { "dependencies": { "inherits": "2.0.3" } }, "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A=="], + "url-polyfill": ["url-polyfill@1.1.13", "", {}, "sha512-tXzkojrv2SujumYthZ/WjF7jaSfNhSXlYMpE5AYdL2I3D7DCeo+mch8KtW2rUuKjDg+3VXODXHVgipt8yGY/eQ=="], - "uuid": ["uuid@11.0.5", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA=="], + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -596,6 +571,8 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "bun-types/@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], diff --git a/package.json b/package.json index 7e3a9002f7..5e298dc5c5 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,13 @@ "format": "prettier --write packages/**/src/*.ts && bun run lint" }, "devDependencies": { - "@eslint/js": "^9.21.0", - "@types/bun": "^1.2.4", - "bun-types": "^1.2.4", - "eslint": "^9.21.0", + "@eslint/js": "^9.23.0", + "@types/bun": "^1.2.5", + "bun-types": "^1.2.5", + "eslint": "^9.23.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-prettier": "^5.2.4", "prettier": "^3.5.3", - "typescript-eslint": "^8.25.0" + "typescript-eslint": "^8.27.0" } } diff --git a/packages/client-query/package.json b/packages/client-query/package.json index 853d4cd8a9..430773b9b1 100644 --- a/packages/client-query/package.json +++ b/packages/client-query/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/communication-client-query", - "version": "0.1.61", + "version": "0.1.147", "main": "dist/index.js", "module": "dist/index.js", "types": "./types/index.d.ts", diff --git a/packages/client-query/src/index.ts b/packages/client-query/src/index.ts index ee5d04972e..5ebcd40b15 100644 --- a/packages/client-query/src/index.ts +++ b/packages/client-query/src/index.ts @@ -13,30 +13,39 @@ // limitations under the License. // -import { LiveQueries } from '@hcengineering/communication-query' -import type { QueryClient } from '@hcengineering/communication-sdk-types' +import { LiveQueries, type QueryClient } from '@hcengineering/communication-query' import type { WorkspaceID } from '@hcengineering/communication-types' -import { MessagesQuery, NotificationsQuery } from './query' +import { MessagesQuery, NotificationContextsQuery, NotificationsQuery } from './query' let lq: LiveQueries +let onDestroy: (fn: () => void) => void = () => {} export function createMessagesQuery(): MessagesQuery { - return new MessagesQuery(lq) + return new MessagesQuery(lq, onDestroy) } export function createNotificationsQuery(): NotificationsQuery { - return new NotificationsQuery(lq) + return new NotificationsQuery(lq, onDestroy) } -export function initLiveQueries(client: QueryClient, workspace: WorkspaceID, filesUrl: string): void { +export function createNotificationContextsQuery(): NotificationContextsQuery { + return new NotificationContextsQuery(lq, onDestroy) +} + +export function initLiveQueries( + client: QueryClient, + workspace: WorkspaceID, + filesUrl: string, + destroyFn?: (fn: () => void) => void +): void { if (lq != null) { lq.close() } - lq = new LiveQueries(client, workspace, filesUrl) - - client.onEvent = (event) => { - void lq.onEvent(event) + if (destroyFn != null) { + onDestroy = destroyFn } + + lq = new LiveQueries(client, workspace, filesUrl) } diff --git a/packages/client-query/src/query.ts b/packages/client-query/src/query.ts index 76a7df614e..45331d3155 100644 --- a/packages/client-query/src/query.ts +++ b/packages/client-query/src/query.ts @@ -19,14 +19,26 @@ import type { NotificationsQueryCallback, QueryCallback } from '@hcengineering/communication-sdk-types' -import { type FindMessagesParams, type FindNotificationsParams } from '@hcengineering/communication-types' +import { + type FindMessagesParams, + type FindNotificationContextParams, + type FindNotificationsParams, + type NotificationContext +} from '@hcengineering/communication-types' import { deepEqual } from 'fast-equals' class BaseQuery

, C extends QueryCallback> { private oldQuery: P | undefined - private oldCallback: QueryCallback | undefined + private oldCallback: C | undefined - constructor(protected readonly lq: LiveQueries) {} + constructor( + protected readonly lq: LiveQueries, + onDestroy: (fn: () => void) => void + ) { + onDestroy(() => { + this.unsubscribe() + }) + } unsubscribe: () => void = () => {} @@ -82,3 +94,17 @@ export class NotificationsQuery extends BaseQuery +> { + override createQuery( + params: FindNotificationContextParams, + callback: QueryCallback + ): { + unsubscribe: () => void + } { + return this.lq.queryNotificationContexts(params, callback) + } +} diff --git a/packages/client-sqlite/package.json b/packages/client-sqlite/package.json deleted file mode 100644 index fb27de5dd5..0000000000 --- a/packages/client-sqlite/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@hcengineering/communication-client-sqlite", - "version": "0.1.61", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "./types/index.d.ts", - "files": [ - "dist/index.js", - "dist/index.cjs", - "types/**/*.d.ts" - ], - "scripts": { - "bundle": "bun run bundle:browser", - "bundle:browser": "bun build src/index.ts --outdir dist --target browser" - }, - "devDependencies": { - "@types/bun": "^1.1.14" - }, - "dependencies": { - "@hcengineering/communication-types": "workspace:*", - "@hcengineering/communication-sdk-types": "workspace:*", - "@hcengineering/communication-sqlite-wasm": "workspace:*", - "fast-equals": "^5.0.1" - }, - "peerDependencies": { - "typescript": "^5.6.3" - }, - "publishConfig": { - "registry": "https://npm.pkg.github.com" - } -} diff --git a/packages/client-sqlite/src/client.ts b/packages/client-sqlite/src/client.ts deleted file mode 100644 index 7884b77a4d..0000000000 --- a/packages/client-sqlite/src/client.ts +++ /dev/null @@ -1,215 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type CardID, - type Message, - type FindMessagesParams, - type MessageID, - type RichText, - type SocialID, - type ContextID, - type NotificationContextUpdate, - type FindNotificationContextParams, - type NotificationContext, - type FindNotificationsParams, - type Notification, - type Attachment, - type Reaction, - type WorkspaceID, - type FindMessagesGroupsParams, - type MessagesGroup, - PatchType -} from '@hcengineering/communication-types' -import { - type Client, - type MessageCreatedEvent, - type DbAdapter, - type ResponseEvent, - ResponseEventType -} from '@hcengineering/communication-sdk-types' -import { createDbAdapter as createSqliteDbAdapter } from '@hcengineering/communication-sqlite-wasm' - -//TODO: FIXME -class DbClient { - onEvent: (event: ResponseEvent) => void = () => {} - - constructor( - private readonly db: DbAdapter, - private readonly workspace: WorkspaceID, - private readonly personalWorkspace: WorkspaceID - ) {} - - async createMessage(card: CardID, content: RichText, creator: SocialID): Promise { - const created = new Date() - const id = await this.db.createMessage(card, content, creator, created) - - const event: MessageCreatedEvent = { - type: ResponseEventType.MessageCreated, - message: { - id, - card, - content, - creator, - created, - edited: created, - reactions: [], - attachments: [] - } - } - - this.onEvent(event) - - return id - } - - async removeMessage(card: CardID, message: MessageID) { - await this.db.removeMessage(card, message) - this.onEvent({ type: ResponseEventType.MessageRemoved, message, card }) - } - - async updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise { - const created = new Date() - await this.db.createPatch(card, message, PatchType.update, content, creator, created) - this.onEvent({ - type: ResponseEventType.PatchCreated, - card, - patch: { message, type: PatchType.update, content, creator, created } - }) - } - - async createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise { - const created = new Date() - await this.db.createReaction(card, message, reaction, creator, created) - this.onEvent({ type: ResponseEventType.ReactionCreated, card, reaction: { message, reaction, creator, created } }) - } - - async removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise { - await this.db.removeReaction(card, message, reaction, creator) - this.onEvent({ type: ResponseEventType.ReactionRemoved, card, message, reaction, creator }) - } - - async createAttachment(card: CardID, message: MessageID, attachment: CardID, creator: SocialID): Promise { - const created = new Date() - await this.db.createAttachment(message, card, creator, created) - this.onEvent({ - type: ResponseEventType.AttachmentCreated, - card, - attachment: { message, card: attachment, creator, created } - }) - } - - async removeAttachment(card: CardID, message: MessageID, attachment: CardID): Promise { - await this.db.removeAttachment(message, card) - this.onEvent({ type: ResponseEventType.AttachmentRemoved, message, card, attachment }) - } - - async findMessages(params: FindMessagesParams): Promise { - const rawMessages = await this.db.findMessages(params) - return rawMessages.map((it) => this.toMessage(it)) - } - - async findMessagesGroups(params: FindMessagesGroupsParams): Promise { - return await this.db.findMessagesGroups(params) - } - - async findMessage(params: FindMessagesParams): Promise { - return (await this.findMessages({ ...params, limit: 1 }))[0] - } - - toMessage(raw: any): Message { - return { - id: raw.id, - card: raw.card, - content: raw.content, - creator: raw.creator, - created: new Date(raw.created), - edited: new Date(raw.edited), - reactions: raw.reactions.map((it: any) => this.toReaction(it)), - attachments: raw.attachments.map((it: any) => this.toAttachment(it)) - } - } - - toAttachment(raw: any): Attachment { - return { - message: raw.message, - card: raw.card, - creator: raw.creator, - created: new Date(raw.created) - } - } - - toReaction(raw: any): Reaction { - return { - message: raw.message, - reaction: raw.reaction, - creator: raw.creator, - created: new Date(raw.created) - } - } - - async createNotification(message: MessageID, context: ContextID): Promise { - await this.db.createNotification(message, context) - } - - async removeNotification(message: MessageID, context: ContextID): Promise { - await this.db.removeNotification(message, context) - } - - async createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise { - return await this.db.createContext(this.personalWorkspace, card, lastView, lastUpdate) - } - - async updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise { - await this.db.updateContext(context, update) - } - - async removeNotificationContext(context: ContextID): Promise { - await this.db.removeContext(context) - } - - async findNotificationContexts(params: FindNotificationContextParams): Promise { - //TODO: should we filter by workspace? - return await this.db.findContexts(params, [this.personalWorkspace]) - } - - //eslint-disable-next-line @typescript-eslint/no-unused-vars - async findNotifications(params: FindNotificationsParams): Promise { - //TODO: should we filter by workspace? - return await this.db.findNotifications(params, this.personalWorkspace) - } - - //eslint-disable-next-line @typescript-eslint/no-unused-vars - async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise { - //TODO: implement - } - - async unsubscribeQuery() { - //ignore - } - - close() { - this.db.close() - } -} - -export async function getSqliteClient( - workspace: WorkspaceID, - personalWorkspace: WorkspaceID, - dbUrl = 'file:communication.sqlite3?vfs=opfs' -): Promise { - const db = await createSqliteDbAdapter(dbUrl) - return new DbClient(db, workspace, personalWorkspace) as unknown as Client -} diff --git a/packages/client-sqlite/tsconfig.json b/packages/client-sqlite/tsconfig.json deleted file mode 100644 index 3ae07cd3fa..0000000000 --- a/packages/client-sqlite/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "jsx": "react-jsx", - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src"] -} diff --git a/packages/client-ws/src/client.ts b/packages/client-ws/src/client.ts deleted file mode 100644 index 3fa8e0101d..0000000000 --- a/packages/client-ws/src/client.ts +++ /dev/null @@ -1,240 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type CardID, - type ContextID, - type FindMessagesGroupsParams, - type FindMessagesParams, - type FindNotificationContextParams, - type FindNotificationsParams, - type Message, - type MessageID, - type MessagesGroup, - type Notification, - type NotificationContext, - type NotificationContextUpdate, - type RichText, - type SocialID, - type WorkspaceID -} from '@hcengineering/communication-types' -import { - RequestEventType, - type Client, - type CreateAttachmentEvent, - type CreateMessageEvent, - type CreateMessageResult, - type CreateNotificationContextEvent, - type CreateNotificationContextResult, - type CreateNotificationEvent, - type CreatePatchEvent, - type CreateReactionEvent, - type EventResult, - type RemoveAttachmentEvent, - type RemoveMessageEvent, - type RemoveNotificationContextEvent, - type RemoveNotificationEvent, - type RemoveReactionEvent, - type RequestEvent, - type ResponseEvent, - type UpdateNotificationContextEvent -} from '@hcengineering/communication-sdk-types' -import { initLiveQueries } from '@hcengineering/communication-client-query' - -import { WebSocketConnection } from './connection' - -class WsClient implements Client { - private readonly ws: WebSocketConnection - - onEvent: (event: ResponseEvent) => void = () => {} - - constructor( - private readonly url: string, - private readonly token: string, - private readonly binary: boolean = false - ) { - const connectionUrl = this.url + '?token=' + this.token - this.ws = new WebSocketConnection(connectionUrl, this.binary) - this.ws.onEvent = (event) => { - void this.onEvent(event) - } - } - - async createMessage(card: CardID, content: RichText, creator: SocialID): Promise { - const event: CreateMessageEvent = { - type: RequestEventType.CreateMessage, - card, - content, - creator - } - const result = await this.sendEvent(event) - return (result as CreateMessageResult).id - } - - async removeMessage(card: CardID, message: MessageID): Promise { - const event: RemoveMessageEvent = { - type: RequestEventType.RemoveMessage, - card, - message - } - await this.sendEvent(event) - } - - async updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise { - const event: CreatePatchEvent = { - type: RequestEventType.CreatePatch, - card, - message, - content, - creator - } - await this.sendEvent(event) - } - - async createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise { - const event: CreateReactionEvent = { - type: RequestEventType.CreateReaction, - card, - message, - reaction, - creator - } - await this.sendEvent(event) - } - - async removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise { - const event: RemoveReactionEvent = { - type: RequestEventType.RemoveReaction, - card, - message, - reaction, - creator - } - await this.sendEvent(event) - } - - async createAttachment(card: CardID, message: MessageID, attachment: CardID, creator: SocialID): Promise { - const event: CreateAttachmentEvent = { - type: RequestEventType.CreateAttachment, - card, - message, - attachment, - creator - } - await this.sendEvent(event) - } - - async removeAttachment(card: CardID, message: MessageID, attachment: CardID): Promise { - const event: RemoveAttachmentEvent = { - type: RequestEventType.RemoveAttachment, - card, - message, - attachment - } - await this.sendEvent(event) - } - - async findMessages(params: FindMessagesParams, queryId?: number): Promise { - return await this.ws.send('findMessages', [params, queryId]) - } - - async findMessagesGroups(params: FindMessagesGroupsParams): Promise { - return await this.ws.send('findMessagesGroups', [params]) - } - - async createNotification(message: MessageID, context: ContextID): Promise { - const event: CreateNotificationEvent = { - type: RequestEventType.CreateNotification, - message, - context - } - await this.sendEvent(event) - } - - async removeNotification(message: MessageID, context: ContextID): Promise { - const event: RemoveNotificationEvent = { - type: RequestEventType.RemoveNotification, - message, - context - } - await this.sendEvent(event) - } - - async createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise { - const event: CreateNotificationContextEvent = { - type: RequestEventType.CreateNotificationContext, - card, - lastView, - lastUpdate - } - const result = await this.sendEvent(event) - return (result as CreateNotificationContextResult).id - } - - async removeNotificationContext(context: ContextID): Promise { - const event: RemoveNotificationContextEvent = { - type: RequestEventType.RemoveNotificationContext, - context - } - await this.sendEvent(event) - } - - async updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise { - const event: UpdateNotificationContextEvent = { - type: RequestEventType.UpdateNotificationContext, - context, - update - } - await this.sendEvent(event) - } - - async findNotificationContexts( - params: FindNotificationContextParams, - queryId?: number - ): Promise { - return await this.ws.send('findNotificationContexts', [params, queryId]) - } - - async findNotifications(params: FindNotificationsParams, queryId?: number): Promise { - return await this.ws.send('findNotifications', [params, queryId]) - } - - async unsubscribeQuery(id: number): Promise { - await this.ws.send('unsubscribeQuery', [id]) - } - - private async sendEvent(event: RequestEvent): Promise { - return await this.ws.send('event', [event]) - } - //eslint-disable-next-line @typescript-eslint/no-unused-vars - async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise { - //TODO: implement - } - - close() { - void this.ws.close() - } -} - -export async function getWebsocketClient( - url: string, - token: string, - workspace: WorkspaceID, - filesUrl: string -): Promise { - const client = new WsClient(url, token) - initLiveQueries(client, workspace, filesUrl) - return client -} diff --git a/packages/client-ws/src/connection.ts b/packages/client-ws/src/connection.ts deleted file mode 100644 index 9d0a8a8054..0000000000 --- a/packages/client-ws/src/connection.ts +++ /dev/null @@ -1,156 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { ResponseEvent } from '@hcengineering/communication-sdk-types' -import { encode, decode } from '@msgpack/msgpack' - -const PING_TIMEOUT = 10000 -const RECONNECT_TIMEOUT = 1000 - -export type RequestId = string - -export interface Response { - id?: RequestId - result?: any - error?: string -} - -export interface Request { - id?: RequestId - method: string - params: any[] -} - -export interface HelloRequest extends Request { - binary?: boolean -} - -export class WebSocketConnection { - private ws!: WebSocket | Promise - private requests: { [key: RequestId]: { resolve: (response: any) => void; reject: (reason: any) => void } } = {} - private lastId: number = 0 - - private pingInterval: any - private reconnectTimeout: any - - onEvent: (event: ResponseEvent) => void = () => {} - - constructor( - private url: string, - private readonly binary: boolean = false - ) { - this.connect() - } - - private connect(): void { - const ws = new WebSocket(this.url) - - ws.onmessage = (event: MessageEvent) => { - const response = deserializeResponse(event.data, this.binary) - if (response.id !== undefined) { - const handlers = this.requests[response.id] - if (handlers === undefined) return - delete this.requests[response.id] - if (response.error !== undefined) { - console.error('Websocket error', response.error) - handlers.reject(response.error) - } else { - handlers.resolve(response.result) - } - } else { - if (response.error !== undefined) { - console.error('Websocket error', response.error) - } else { - const event = response.result as ResponseEvent - this.onEvent(event) - } - } - } - - ws.onclose = () => { - clearInterval(this.pingInterval) - this.handleReconnect() - } - - this.ws = new Promise((resolve, reject) => { - ws.onopen = () => { - const request: HelloRequest = { id: 'hello', method: 'hello', params: [], binary: this.binary } - ws.send(serializeRequest(request, this.binary)) - clearInterval(this.pingInterval) - this.pingInterval = setInterval(() => { - void this.sendRequest({ method: 'ping', params: [] }) - }, PING_TIMEOUT) - resolve(ws) - } - ws.onerror = (event: any) => { - console.error('Websocket error', event) - reject(new Error('Websocket error')) - } - }) - } - - private handleReconnect() { - clearTimeout(this.reconnectTimeout) - this.reconnectTimeout = setTimeout(() => { - this.connect() - }, RECONNECT_TIMEOUT) - } - - async waitWs(): Promise { - if (this.ws instanceof Promise) { - this.ws = await this.ws - } - return this.ws - } - - async send(method: string, params: any[]): Promise { - const id = ++this.lastId - return await this.sendRequest({ id: id.toString(), method, params }) - } - - private async sendRequest(request: Request): Promise { - const ws = await this.waitWs() - - return new Promise((resolve, reject) => { - if (request.id !== undefined) { - this.requests[request.id] = { resolve, reject } - } - ws.send(serializeRequest(request, this.binary)) - }) - } - - async close(): Promise { - clearInterval(this.pingInterval) - clearTimeout(this.reconnectTimeout) - const ws = await this.waitWs() - ws.close() - } -} - -function serializeRequest(request: Request, binary: boolean): any { - if (binary) { - return encode(request) - } else { - return JSON.stringify(request) - } -} - -function deserializeResponse(data: any, binary: boolean): Response { - if (binary) { - return decode(data) as Response - } else { - return JSON.parse(data.toString()) - } -} diff --git a/packages/cockroach/migrations/01_message.sql b/packages/cockroach/migrations/01_message.sql index d113fe2fd3..37f45dbef7 100644 --- a/packages/cockroach/migrations/01_message.sql +++ b/packages/cockroach/migrations/01_message.sql @@ -8,6 +8,10 @@ CREATE TABLE IF NOT EXISTS communication.messages creator VARCHAR(255) NOT NULL, created TIMESTAMPTZ NOT NULL, + type VARCHAR(255) NOT NULL, + data JSONB NOT NULL DEFAULT '{}', + + PRIMARY KEY (workspace_id, card_id, id) ); @@ -20,10 +24,8 @@ CREATE TABLE IF NOT EXISTS communication.messages_groups card_id VARCHAR(255) NOT NULL, blob_id UUID NOT NULL, - from_date TIMESTAMPTZ NOT NULL, - to_date TIMESTAMPTZ NOT NULL, - from_id INT8 NOT NULL, - to_id INT8 NOT NULL, + from_sec TIMESTAMPTZ(0) NOT NULL, + to_sec TIMESTAMPTZ(0) NOT NULL, count INT NOT NULL, PRIMARY KEY (workspace_id, card_id, blob_id) diff --git a/packages/cockroach/migrations/02_patch.sql b/packages/cockroach/migrations/02_patch.sql index 527df14bf0..1c2037b046 100644 --- a/packages/cockroach/migrations/02_patch.sql +++ b/packages/cockroach/migrations/02_patch.sql @@ -1,13 +1,14 @@ CREATE TABLE IF NOT EXISTS communication.patch ( - id INT8 NOT NULL DEFAULT unique_rowid(), - workspace_id UUID NOT NULL, - card_id VARCHAR(255) NOT NULL, - message_id INT8 NOT NULL, - type VARCHAR(255) NOT NULL, - content TEXT NOT NULL, - creator VARCHAR(255) NOT NULL, - created TIMESTAMPTZ NOT NULL, + id INT8 NOT NULL DEFAULT unique_rowid(), + workspace_id UUID NOT NULL, + card_id VARCHAR(255) NOT NULL, + message_id INT8 NOT NULL, + type VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + creator VARCHAR(255) NOT NULL, + created TIMESTAMPTZ NOT NULL, + message_created_sec TIMESTAMPTZ(0) NOT NULL, PRIMARY KEY (id) ); diff --git a/packages/cockroach/migrations/03_attachment.sql b/packages/cockroach/migrations/03_attachment.sql deleted file mode 100644 index 780002f335..0000000000 --- a/packages/cockroach/migrations/03_attachment.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE IF NOT EXISTS communication.attachments -( - message_id INT8 NOT NULL, - card_id VARCHAR(255) NOT NULL, - creator VARCHAR(255) NOT NULL, - created TIMESTAMPTZ NOT NULL DEFAULT now(), - - PRIMARY KEY (card_id, message_id) -); - -CREATE INDEX IF NOT EXISTS attachment_message_idx ON communication.attachments (message_id); diff --git a/packages/cockroach/migrations/03_files.sql b/packages/cockroach/migrations/03_files.sql new file mode 100644 index 0000000000..0689245041 --- /dev/null +++ b/packages/cockroach/migrations/03_files.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS communication.files +( + workspace_id UUID NOT NULL, + card_id VARCHAR(255) NOT NULL, + message_id INT8 NOT NULL, + + blob_id UUID NOT NULL, + filename VARCHAR(255) NOT NULL, + type VARCHAR(255) NOT NULL, + size INT8 NOT NULL, + + creator VARCHAR(255) NOT NULL, + created TIMESTAMPTZ NOT NULL DEFAULT now(), + + message_created_sec TIMESTAMPTZ(0) NOT NULL, + + PRIMARY KEY (workspace_id, card_id, message_id, blob_id) +); + +CREATE INDEX IF NOT EXISTS files_workspace_card_message_idx ON communication.files (workspace_id, card_id, message_id); diff --git a/packages/cockroach/migrations/06_notification.sql b/packages/cockroach/migrations/06_notification.sql index df565632a1..88d60cd21f 100644 --- a/packages/cockroach/migrations/06_notification.sql +++ b/packages/cockroach/migrations/06_notification.sql @@ -1,27 +1,38 @@ CREATE TABLE IF NOT EXISTS communication.notification_context ( - id UUID NOT NULL DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL, - card_id VARCHAR(255) NOT NULL, + id INT8 NOT NULL DEFAULT unique_rowid(), - personal_workspace UUID NOT NULL, + workspace_id UUID NOT NULL, + card_id VARCHAR(255) NOT NULL, + account UUID NOT NULL, - archived_from TIMESTAMPTZ, - last_view TIMESTAMPTZ, - last_update TIMESTAMPTZ, + last_view TIMESTAMPTZ NOT NULL DEFAULT now(), + last_update TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (id), - UNIQUE (workspace_id, card_id, personal_workspace) + UNIQUE (workspace_id, card_id, account) ); - CREATE TABLE IF NOT EXISTS communication.notifications ( - message_id UUID NOT NULL, - context UUID NOT NULL, + id INT8 NOT NULL DEFAULT unique_rowid(), + context_id INT8 NOT NULL, + message_id INT8, + created TIMESTAMPTZ NOT NULL, + content JSONB NOT NULL DEFAULT '{}', - PRIMARY KEY (message_id, context), - FOREIGN KEY (context) REFERENCES communication.notification_context (id) ON DELETE CASCADE + PRIMARY KEY (id), + FOREIGN KEY (context_id) REFERENCES communication.notification_context (id) ON DELETE CASCADE ); -CREATE INDEX IF NOT EXISTS notification_context_idx ON communication.notifications (context); \ No newline at end of file +CREATE INDEX IF NOT EXISTS notification_context_idx ON communication.notifications (context_id); + +CREATE TABLE IF NOT EXISTS communication.collaborators +( + workspace_id UUID NOT NULL, + card_id VARCHAR(255) NOT NULL, + account UUID NOT NULL, + date TIMESTAMPTZ NOT NULL DEFAULT now(), + + PRIMARY KEY (workspace_id, card_id, account) +); \ No newline at end of file diff --git a/packages/cockroach/package.json b/packages/cockroach/package.json index 095bdf4c0d..4971913475 100644 --- a/packages/cockroach/package.json +++ b/packages/cockroach/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/communication-cockroach", - "version": "0.1.61", + "version": "0.1.147", "main": "dist/index.js", "module": "dist/index.js", "types": "./types/index.d.ts", diff --git a/packages/cockroach/src/adapter.ts b/packages/cockroach/src/adapter.ts index f5d9a846a6..c471d49093 100644 --- a/packages/cockroach/src/adapter.ts +++ b/packages/cockroach/src/adapter.ts @@ -13,11 +13,13 @@ // limitations under the License. // -import type { ParameterOrJSON, Row } from 'postgres' import type postgres from 'postgres' import { + type FindCollaboratorsParams, + type AccountID, type BlobID, type CardID, + type Collaborator, type ContextID, type FindMessagesGroupsParams, type FindMessagesParams, @@ -26,14 +28,15 @@ import { type Message, type MessageID, type MessagesGroup, + type MessageType, type Notification, type NotificationContext, - type NotificationContextUpdate, type PatchType, type RichText, type SocialID, + type Thread, type WorkspaceID, - type Thread + type NotificationID } from '@hcengineering/communication-types' import type { DbAdapter } from '@hcengineering/communication-sdk-types' import { retry } from '@hcengineering/communication-shared' @@ -41,8 +44,8 @@ import { retry } from '@hcengineering/communication-shared' import { MessagesDb } from './db/message' import { NotificationsDb } from './db/notification' import { connect, type PostgresClientReference } from './connection' -import { type Options, type Logger, type SqlClient } from './types' -import { injectVars } from './utils.ts' +import { type Logger, type Options, type SqlClient, type SqlParams, type SqlRow } from './types' +import { injectVars } from './utils' export class CockroachAdapter implements DbAdapter { private readonly message: MessagesDb @@ -58,16 +61,19 @@ export class CockroachAdapter implements DbAdapter { this.notification = new NotificationsDb(this.sql, this.workspace, logger, options) } - async createMessage(card: CardID, content: RichText, creator: SocialID, created: Date): Promise { - return await this.message.createMessage(card, content, creator, created) + async createMessage( + card: CardID, + type: MessageType, + content: RichText, + creator: SocialID, + created: Date, + data?: any + ): Promise { + return await this.message.createMessage(card, type, content, creator, created, data) } - async removeMessage(card: CardID, message: MessageID, socialIds?: SocialID[]): Promise { - await this.message.removeMessage(card, message, socialIds) - } - - async removeMessages(card: CardID, fromId: MessageID, toId: MessageID): Promise { - await this.message.removeMessages(card, fromId, toId) + async removeMessages(card: CardID, messages: MessageID[], socialIds?: SocialID[]): Promise { + return await this.message.removeMessages(card, messages, socialIds) } async createPatch( @@ -81,20 +87,8 @@ export class CockroachAdapter implements DbAdapter { await this.message.createPatch(card, message, type, content, creator, created) } - async removePatches(card: CardID, fromId: MessageID, toId: MessageID): Promise { - await this.message.removePatches(card, fromId, toId) - } - - async createMessagesGroup( - card: CardID, - blobId: BlobID, - fromDate: Date, - toDate: Date, - fromId: MessageID, - toId: MessageID, - count: number - ): Promise { - await this.message.createMessagesGroup(card, blobId, fromDate, toDate, fromId, toId, count) + async createMessagesGroup(card: CardID, blobId: BlobID, fromDate: Date, toDate: Date, count: number): Promise { + await this.message.createMessagesGroup(card, blobId, fromDate, toDate, count) } async removeMessagesGroup(card: CardID, blobId: BlobID): Promise { @@ -115,48 +109,31 @@ export class CockroachAdapter implements DbAdapter { await this.message.removeReaction(card, message, reaction, creator, new Date()) } - async createAttachment(message: MessageID, attachment: CardID, creator: SocialID, created: Date): Promise { - await this.message.createAttachment(message, attachment, creator, created) + async createFile( + card: CardID, + message: MessageID, + blobId: BlobID, + fileType: string, + filename: string, + size: number, + creator: SocialID, + created: Date + ): Promise { + await this.message.createFile(card, message, blobId, fileType, filename, size, creator, created) } - async removeAttachment(message: MessageID, attachment: CardID): Promise { - await this.message.removeAttachment(message, attachment) + async removeFile(card: CardID, message: MessageID, blobId: BlobID): Promise { + await this.message.removeFile(card, message, blobId) } async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise { await this.message.createThread(card, message, thread, created) } - async updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise { - await this.message.updateThread(thread, lastReply, op) + async updateThread(thread: CardID, op: 'increment' | 'decrement', lastReply?: Date): Promise { + await this.message.updateThread(thread, op, lastReply) } - async createNotification(message: MessageID, context: ContextID): Promise { - await this.notification.createNotification(message, context) - } - - async removeNotification(message: MessageID, context: ContextID): Promise { - await this.notification.removeNotification(message, context) - } - - async createContext( - personalWorkspace: WorkspaceID, - card: CardID, - lastView?: Date, - lastUpdate?: Date - ): Promise { - return await this.notification.createContext(personalWorkspace, card, lastView, lastUpdate) - } - - async updateContext(context: ContextID, update: NotificationContextUpdate): Promise { - await this.notification.updateContext(context, update) - } - - async removeContext(context: ContextID): Promise { - await this.notification.removeContext(context) - } - - // Finds async findMessages(params: FindMessagesParams): Promise { return await this.message.find(params) } @@ -169,25 +146,53 @@ export class CockroachAdapter implements DbAdapter { return await this.message.findThread(thread) } - async findContexts( - params: FindNotificationContextParams, - personalWorkspaces: WorkspaceID[], - workspace?: WorkspaceID - ): Promise { - return await this.notification.findContexts(params, personalWorkspaces, workspace) + async addCollaborators(card: CardID, collaborators: AccountID[], date?: Date): Promise { + await this.notification.addCollaborators(card, collaborators, date) } - async findNotifications( - params: FindNotificationsParams, - personalWorkspace: WorkspaceID, - workspace?: WorkspaceID - ): Promise { - return await this.notification.findNotifications(params, personalWorkspace, workspace) + async removeCollaborators(card: CardID, collaborators: AccountID[]): Promise { + await this.notification.removeCollaborators(card, collaborators) + } + + async createNotification(context: ContextID, message: MessageID, messageCreated: Date): Promise { + return await this.notification.createNotification(context, message, messageCreated) + } + + async removeNotification(context: ContextID, account: AccountID, untilDate: Date): Promise { + await this.notification.removeNotifications(context, account, untilDate) + } + + async createContext(account: AccountID, card: CardID, lastUpdate: Date, lastView: Date): Promise { + return await this.notification.createContext(account, card, lastUpdate, lastView) + } + + async updateContext(context: ContextID, account: AccountID, lastUpdate?: Date, lastView?: Date): Promise { + await this.notification.updateContext(context, account, lastUpdate, lastView) + } + + async removeContext(context: ContextID, account: AccountID): Promise { + await this.notification.removeContext(context, account) + } + + async findContexts(params: FindNotificationContextParams): Promise { + return await this.notification.findContexts(params) + } + + async findNotifications(params: FindNotificationsParams): Promise { + return await this.notification.findNotifications(params) + } + + async findCollaborators(params: FindCollaboratorsParams): Promise { + return await this.notification.findCollaborators(params) } close(): void { this.sql.close() } + + getCollaboratorsCursor(card: CardID, date: Date, size?: number): AsyncIterable { + return this.notification.getCollaboratorsCursor(card, date, size) + } } export async function createDbAdapter( @@ -197,12 +202,13 @@ export async function createDbAdapter( options?: Options ): Promise { const greenUrl = process.env.GREEN_URL ?? '' + const connection = connect(connectionString) + const sql = await connection.getClient() + if (greenUrl !== '') { - const client = new GreenClient(greenUrl) + const client = new GreenClient(greenUrl, sql) return new CockroachAdapter(client, workspace, logger, options) } else { - const connection = connect(connectionString) - const sql = await connection.getClient() const client = new CockroachClient(connection, sql) return new CockroachAdapter(client, workspace, logger, options) @@ -212,8 +218,11 @@ export async function createDbAdapter( class GreenClient implements SqlClient { private readonly url: string private readonly token: string - constructor(endpoint: string) { - const url = new URL(endpoint) + constructor( + private readonly endpoint: string, + private readonly sql: postgres.Sql + ) { + const url = new URL(this.endpoint) this.token = url.searchParams.get('token') ?? 'secret' const compression = url.searchParams.get('compression') ?? '' @@ -229,14 +238,21 @@ class GreenClient implements SqlClient { this.url = `${url.protocol}//${newHost}${newPathname}${newSearchParams.size > 0 ? '?' + newSearchParams.toString() : ''}` } - async execute)[]>(query: string, params?: ParameterOrJSON[]): Promise { - return await retry(() => this.fetch(query, params), { retries: 5 }) + async execute(query: string, params?: SqlParams): Promise { + return await retry(() => this.fetch(query, params), { retries: 5 }) } - private async fetch)[]>( - query: string, - params?: ParameterOrJSON[] - ): Promise { + cursor(query: string, params?: SqlParams, size?: number): AsyncIterable[]> { + const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query + + return this.sql.unsafe(sql).cursor(size) + } + + close(): void { + // do nothing + } + + private async fetch(query: string, params?: SqlParams): Promise { const url = this.url.endsWith('/') ? this.url + 'api/v1/sql' : this.url + '/api/v1/sql' const response = await fetch(url, { @@ -255,10 +271,6 @@ class GreenClient implements SqlClient { return await response.json() } - - close(): void { - // do nothing - } } class CockroachClient implements SqlClient { @@ -267,9 +279,15 @@ class CockroachClient implements SqlClient { private readonly sql: postgres.Sql ) {} - async execute)[]>(query: string, params?: ParameterOrJSON[]): Promise { + async execute(query: string, params?: SqlParams): Promise { const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query - return await this.sql.unsafe(sql) + return await this.sql.unsafe(sql) + } + + cursor(query: string, params?: SqlParams, size?: number): AsyncIterable[]> { + const sql = params !== undefined && params.length > 0 ? injectVars(query, params) : query + + return this.sql.unsafe(sql).cursor(size) } close(): void { diff --git a/packages/cockroach/src/connection.ts b/packages/cockroach/src/connection.ts index f7223da5a7..7480097f54 100644 --- a/packages/cockroach/src/connection.ts +++ b/packages/cockroach/src/connection.ts @@ -103,7 +103,7 @@ export function connect(connectionString: string, database?: string): PostgresCl database, max: 5, fetch_types: false, - prepare: true, + prepare: false, ...extraOptions }) diff --git a/packages/cockroach/src/db/base.ts b/packages/cockroach/src/db/base.ts index 572c5c3e43..5f63b6b0e7 100644 --- a/packages/cockroach/src/db/base.ts +++ b/packages/cockroach/src/db/base.ts @@ -16,7 +16,7 @@ import { type ParameterOrJSON, type Row } from 'postgres' import type { WorkspaceID } from '@hcengineering/communication-types' -import { type SqlClient, type Logger, type Options } from '../types' +import { type Logger, type Options, type SqlClient } from '../types' export class BaseDb { constructor ( @@ -26,11 +26,7 @@ export class BaseDb { readonly options?: Options ) {} - async execute)[]>( - sql: string, - params?: ParameterOrJSON[], - name?: string - ): Promise { + async execute>(sql: string, params?: ParameterOrJSON[], name?: string): Promise { if (this.options?.withLogs === true && this.logger !== undefined) { return await this.executeWithLogs(name ?? 'execute sql', this.logger, sql, params) } @@ -38,16 +34,16 @@ export class BaseDb { return await this.client.execute(sql, params) } - private async executeWithLogs)[]>( + private async executeWithLogs>( name: string, logger: Logger, sql: string, params?: ParameterOrJSON[] - ): Promise { + ): Promise { const start = performance.now() try { - return await this.client.execute(sql, params) + return await this.client.execute(sql, params) } finally { const time = performance.now() - start logger.info(name, { time }) diff --git a/packages/cockroach/src/db/mapping.ts b/packages/cockroach/src/db/mapping.ts new file mode 100644 index 0000000000..ee43477dc4 --- /dev/null +++ b/packages/cockroach/src/db/mapping.ts @@ -0,0 +1,251 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + type File, + type BlobID, + type CardID, + type Collaborator, + type ContextID, + type Message, + type MessageID, + type MessagesGroup, + type MessageType, + type Notification, + type NotificationContext, + type NotificationID, + type Patch, + PatchType, + type Reaction, + type RichText, + type SocialID, + type Thread, + type MessageData +} from '@hcengineering/communication-types' + +import { + type FileDb, + type CollaboratorDb, + type ContextDb, + type MessageDb, + type MessagesGroupDb, + type NotificationDb, + type PatchDb, + type ReactionDb, + type ThreadDb +} from './schema' + +interface RawMessage extends MessageDb { + thread_id?: CardID + replies_count?: number + last_reply?: Date + patches?: PatchDb[] + files?: FileDb[] + reactions?: ReactionDb[] +} + +interface RawNotification extends NotificationDb { + message_id: MessageID + message_type?: MessageType + message_content?: RichText + message_creator?: SocialID + message_data?: MessageData + message_created?: Date + message_group_blob_id?: BlobID + message_group_from_sec?: Date + message_group_to_sec?: Date + message_group_count?: number + message_patches?: { + patch_type: PatchType + patch_content: RichText + patch_creator: SocialID + patch_created: Date + }[] +} + +type RawContext = ContextDb & { id: ContextID } & { + notifications?: RawNotification[] +} + +export function toMessage (raw: RawMessage): Message { + const lastPatch = raw.patches?.[0] + + return { + id: String(raw.id) as MessageID, + type: raw.type, + card: raw.card_id, + content: lastPatch?.content ?? raw.content, + creator: raw.creator, + created: raw.created, + data: raw.data, + edited: lastPatch?.created ?? undefined, + thread: + raw.thread_id != null + ? { + card: raw.card_id, + message: String(raw.id) as MessageID, + thread: raw.thread_id, + repliesCount: raw.replies_count ?? 0, + lastReply: raw.last_reply ?? new Date() + } + : undefined, + reactions: (raw.reactions ?? []).map(toReaction), + files: (raw.files ?? []).map(toFile) + } +} + +export function toReaction (raw: ReactionDb): Reaction { + return { + message: String(raw.message_id) as MessageID, + reaction: raw.reaction, + creator: raw.creator, + created: raw.created + } +} + +export function toFile (raw: FileDb): File { + return { + card: raw.card_id, + message: String(raw.message_id) as MessageID, + blobId: raw.blob_id, + type: raw.type, + filename: raw.filename, + size: parseInt(raw.size as any), + creator: raw.creator, + created: raw.created + } +} + +export function toMessagesGroup (raw: MessagesGroupDb): MessagesGroup { + return { + card: raw.card_id, + blobId: raw.blob_id, + fromSec: raw.from_sec, + toSec: raw.to_sec, + count: raw.count, + patches: raw.patches == null ? [] : raw.patches.filter((it: any) => it.message_id != null).map(toPatch) + } +} + +export function toPatch (raw: PatchDb): Patch { + return { + type: raw.type, + message: String(raw.message_id) as MessageID, + content: raw.content, + creator: raw.creator, + created: new Date(raw.created) + } +} + +export function toThread (raw: ThreadDb): Thread { + return { + card: raw.card_id, + message: String(raw.message_id) as MessageID, + thread: raw.thread_id, + repliesCount: raw.replies_count, + lastReply: raw.last_reply + } +} + +export function toNotificationContext (raw: RawContext): NotificationContext { + const lastView = new Date(raw.last_view) + return { + id: String(raw.id) as ContextID, + card: raw.card_id, + account: raw.account, + lastView, + lastUpdate: new Date(raw.last_update), + notifications: (raw.notifications ?? []) + .filter((it) => it.id != null) + .map((it) => toNotificationRaw(raw.id, raw.card_id, lastView, it)) + } +} + +function toNotificationRaw ( + id: ContextID, + card: CardID, + lastView: Date | undefined, + raw: RawNotification +): Notification { + const created = new Date(raw.created) + const read = lastView != null && lastView >= created + + let message: Message | undefined + + if ( + raw.message_content != null && + raw.message_creator != null && + raw.message_created != null && + raw.message_type != null + ) { + const lastPatch = (raw.message_patches ?? []).find((it) => it.patch_type === PatchType.update) + message = { + id: String(raw.message_id) as MessageID, + type: raw.message_type, + card, + content: lastPatch?.patch_content ?? raw.message_content, + data: raw.message_data, + creator: raw.message_creator, + created: new Date(raw.message_created), + edited: lastPatch?.patch_created != null ? new Date(lastPatch.patch_created) : undefined, + reactions: [], + files: [] + } + } + + if (message != null) { + return { + id: String(raw.id) as NotificationID, + read, + messageId: String(raw.message_id) as MessageID, + created, + context: String(id) as ContextID, + message + } + } + + let messageGroup: MessagesGroup | undefined + + if (raw.message_group_blob_id != null && raw.message_group_from_sec != null && raw.message_group_to_sec != null) { + messageGroup = { + card, + blobId: raw.message_group_blob_id, + fromSec: new Date(raw.message_group_from_sec), + toSec: new Date(raw.message_group_to_sec), + count: raw.message_group_count ?? 0 + } + } + + return { + id: String(raw.id) as NotificationID, + read, + messageId: raw.message_id, + created, + context: String(id) as ContextID, + messageGroup + } +} + +export function toNotification (raw: RawNotification & { card_id: CardID, last_view?: Date }): Notification { + const lastView = raw.last_view != null ? new Date(raw.last_view) : undefined + + return toNotificationRaw(raw.context_id, raw.card_id, lastView, raw) +} + +export function toCollaborator (raw: CollaboratorDb): Collaborator { + return { + account: raw.account + } +} diff --git a/packages/cockroach/src/db/message.ts b/packages/cockroach/src/db/message.ts index e94d089650..5d020122b9 100644 --- a/packages/cockroach/src/db/message.ts +++ b/packages/cockroach/src/db/message.ts @@ -14,401 +14,487 @@ // import { - type Message, - type MessageID, - type CardID, - type FindMessagesParams, - type SocialID, - type RichText, - SortingOrder, - PatchType, - type Thread, - type BlobID, - type FindMessagesGroupsParams, - type MessagesGroup + type BlobID, + type CardID, + type FindMessagesGroupsParams, + type FindMessagesParams, + type Message, + type MessageID, + type MessagesGroup, + type MessageType, + PatchType, + type RichText, + type SocialID, + SortingOrder, + type Thread } from '@hcengineering/communication-types' -import {generateMessageId} from '@hcengineering/communication-shared' +import { generateMessageId, parseMessageId } from '@hcengineering/communication-shared' -import {BaseDb} from './base' -import { - TableName, - type MessageDb, - type AttachmentDb, - type ReactionDb, - type PatchDb, - toMessage, - type ThreadDb, - toThread, - type MessagesGroupDb, - toMessagesGroup -} from './schema' -import {getCondition} from './utils' +import { BaseDb } from './base' +import {type FileDb, type MessageDb, type MessagesGroupDb, type PatchDb, type ReactionDb, TableName, type ThreadDb } from './schema' +import { getCondition } from './utils' +import { toMessage, toMessagesGroup, toThread } from './mapping' export class MessagesDb extends BaseDb { - // Message - async createMessage(card: CardID, content: RichText, creator: SocialID, created: Date): Promise { - const id = generateMessageId() - const db: MessageDb = { - id, - workspace_id: this.workspace, - card_id: card, - content, - creator, - created - } - - const sql = `INSERT INTO ${TableName.Message} (workspace_id, card_id, id, content, creator, created) - VALUES ($1::uuid, $2::varchar, $3::bigint, $4::text, $5::varchar, $6::timestamptz)` - - await this.execute(sql, [db.workspace_id, db.card_id, db.id, db.content, db.creator, db.created], 'insert message') - - return id + // Message + async createMessage ( + card: CardID, + type: MessageType, + content: RichText, + creator: SocialID, + created: Date, + data?: any + ): Promise { + const id = generateMessageId() + const db: MessageDb = { + id, + type, + workspace_id: this.workspace, + card_id: card, + content, + creator, + created, + data } - async removeMessage(card: CardID, message: MessageID, socialIds?: SocialID[]): Promise { - if (socialIds === undefined || socialIds.length === 0) { - const sql = `DELETE - FROM ${TableName.Message} - WHERE workspace_id = $1::uuid - AND card_id = $2::varchar - AND id = $2::bigint;` - await this.execute(sql, [this.workspace, card, message], 'remove message') - } else if (socialIds.length === 1) { - const sql = `DELETE - FROM ${TableName.Message} - WHERE workspace_id = $1::uuid - AND card_id = $2::varchar - AND id = $2::bigint - AND creator = $3::varchar;` - await this.execute(sql, [this.workspace, card, message, socialIds[0]], 'remove message') - } else { - const sql = `DELETE - FROM ${TableName.Message} - WHERE workspace_id = $1::uuid - AND card_id = $2::varchar - AND id = $2::bigint - AND creator = ANY ($3::varchar[]);` + const sql = `INSERT INTO ${TableName.Message} (workspace_id, card_id, id, content, creator, created, type, data) + VALUES ($1::uuid, $2::varchar, $3::bigint, $4::text, $5::varchar, $6::timestamptz, $7::varchar, $8::jsonb)` - await this.execute(sql, [this.workspace, card, message, socialIds], 'remove message') - } + await this.execute( + sql, + [db.workspace_id, db.card_id, db.id, db.content, db.creator, db.created, db.type, db.data ?? {}], + 'insert message' + ) + + return id + } + + async removeMessages (card: CardID, messages: MessageID[], socialIds?: SocialID[]): Promise { + if (messages.length === 0) return [] + + const where: string[] = ['workspace_id = $1::uuid', 'card_id = $2::varchar'] + const values: any[] = [this.workspace, card] + + let index = values.length + 1 + + if (socialIds?.length === 1) { + where.push(`creator = $${index++}::varchar`) + values.push(socialIds[0]) } - async removeMessages(card: CardID, fromId: MessageID, toId: MessageID): Promise { - const sql = `DELETE - FROM ${TableName.Message} - WHERE workspace_id = $1::uuid - AND card_id = $2::varchar - AND id >= $3::bigint - AND id <= $4::bigint;` - - await this.execute(sql, [this.workspace, card, BigInt(fromId), BigInt(toId)], 'remove messages') + if (socialIds != null && socialIds.length > 1) { + where.push(`creator = ANY($${index++}::varchar[])`) + values.push(socialIds) } - async createPatch( - card: CardID, - message: MessageID, - type: PatchType, - content: string, - creator: SocialID, - created: Date - ): Promise { - const db: PatchDb = { - workspace_id: this.workspace, - card_id: card, - message_id: message, - type, - content, - creator, - created - } - - const sql = `INSERT INTO ${TableName.Patch} (workspace_id, card_id, message_id, type, content, creator, created) - VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::text, $6::varchar, $7::timestamptz)` - - await this.execute( - sql, - [db.workspace_id, db.card_id, db.message_id, db.type, db.content, db.creator, db.created], - 'insert patch' - ) + if (messages.length === 1) { + where.push(`id = $${index++}::bigint`) + values.push(messages[0]) + } else { + where.push(`id = ANY($${index++}::bigint[])`) + values.push(messages) } - async removePatches(card: CardID, fromId: MessageID, toId: MessageID): Promise { - const sql = `DELETE - FROM ${TableName.Patch} - WHERE workspace_id = $1::uuid - AND card_id = $2::varchar - AND message_id >= $3::bigint - AND message_id <= $4::bigint;` + const sql = `DELETE FROM ${TableName.Message} WHERE ${where.join(' AND ')} RETURNING id` - await this.execute(sql, [this.workspace, card, BigInt(fromId), BigInt(toId)], 'remove patches') + const result = await this.execute(sql, values, 'remove messages') + + return result.map((row: any) => row.id) + } + + async createPatch ( + card: CardID, + message: MessageID, + type: PatchType, + content: string, + creator: SocialID, + created: Date + ): Promise { + const db: PatchDb = { + workspace_id: this.workspace, + card_id: card, + message_id: message, + type, + content, + creator, + created, + message_created_sec: parseMessageId(message) } - // Attachment - async createAttachment(message: MessageID, card: CardID, creator: SocialID, created: Date): Promise { - const db: AttachmentDb = { - message_id: message, - card_id: card, - creator, - created - } - const sql = `INSERT INTO ${TableName.Attachment} (message_id, card_id, creator, created) - VALUES ($1::bigint, $2::varchar, $3::varchar, $4::timestamptz)` + const sql = `INSERT INTO ${TableName.Patch} (workspace_id, card_id, message_id, type, content, creator, created, message_created_sec) + VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::text, $6::varchar, $7::timestamptz, $8::timestamptz)` - await this.execute(sql, [db.message_id, db.card_id, db.creator, db.created], 'insert attachment') + await this.execute( + sql, + [db.workspace_id, db.card_id, db.message_id, db.type, db.content, db.creator, db.created, db.message_created_sec], + 'insert patch' + ) + } + + // File + async createFile (card: CardID, message: MessageID, blobId: BlobID, fileType: string, filename: string, size: number,creator: SocialID, created: Date): Promise { + const db: FileDb = { + workspace_id: this.workspace, + card_id: card, + message_id: message, + blob_id: blobId, + type: fileType, + filename, + size, + creator, + created, + message_created_sec: parseMessageId(message) } + const sql = `INSERT INTO ${TableName.File} (workspace_id, card_id, message_id, blob_id, type, filename, creator, created, message_created_sec, size) + VALUES ($1::uuid, $2::varchar, $3::int8, $4::uuid, $5::varchar, $6::varchar, $7::varchar, $8::timestamptz, $9::timestamptz, $10::int8)` - async removeAttachment(message: MessageID, card: CardID): Promise { - const sql = `DELETE - FROM ${TableName.Attachment} - WHERE message_id = $1::bigint - AND card_id = $2::varchar` - await this.execute(sql, [message, card], 'remove attachment') - } + await this.execute(sql, [db.workspace_id, db.card_id, db.message_id, db.blob_id, db.type, db.filename, db.creator, db.created, db.message_created_sec, db.size], 'insert file') + } - // Reaction - async createReaction( - card: CardID, - message: MessageID, - reaction: string, - creator: SocialID, - created: Date - ): Promise { - const select = `SELECT m.id + async removeFile (card: CardID, message: MessageID, blobId: BlobID): Promise { + const sql = `DELETE + FROM ${TableName.File} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND message_id = $3::bigint + AND blob_id = $4::uuid` + await this.execute(sql, [this.workspace, card, message, blobId], 'remove file') + } + + // Reaction + async createReaction ( + card: CardID, + message: MessageID, + reaction: string, + creator: SocialID, + created: Date + ): Promise { + const select = `SELECT m.id FROM ${TableName.Message} m WHERE m.id = $1::bigint` - const messageDb = await this.execute(select, [message], 'select message') + const messageDb = await this.execute(select, [message], 'select message') - if (messageDb.length > 0) { - const db: ReactionDb = { - workspace_id: this.workspace, - card_id: card, - message_id: message, - reaction, - creator, - created - } - const sql = `INSERT INTO ${TableName.Reaction} (workspace_id, card_id, message_id, reaction, creator, created) + if (messageDb.length > 0) { + const db: ReactionDb = { + workspace_id: this.workspace, + card_id: card, + message_id: message, + reaction, + creator, + created + } + const sql = `INSERT INTO ${TableName.Reaction} (workspace_id, card_id, message_id, reaction, creator, created) VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::varchar, $6::timestamptz)` - await this.execute( - sql, - [db.workspace_id, db.card_id, db.message_id, db.reaction, db.creator, db.created], - 'insert reaction' - ) - } else { - await this.createPatch(card, message, PatchType.addReaction, reaction, creator, created) - } + await this.execute( + sql, + [db.workspace_id, db.card_id, db.message_id, db.reaction, db.creator, db.created], + 'insert reaction' + ) + } else { + await this.createPatch(card, message, PatchType.addReaction, reaction, creator, created) } + } - async removeReaction( - card: CardID, - message: MessageID, - reaction: string, - creator: SocialID, - created: Date - ): Promise { - const select = `SELECT m.id + async removeReaction ( + card: CardID, + message: MessageID, + reaction: string, + creator: SocialID, + created: Date + ): Promise { + const select = `SELECT m.id FROM ${TableName.Message} m WHERE m.id = $1::bigint` - const messageDb = await this.execute(select, [message], 'select message') + const messageDb = await this.execute(select, [message], 'select message') - if (messageDb.length > 0) { - const sql = `DELETE + if (messageDb.length > 0) { + const sql = `DELETE FROM ${TableName.Reaction} WHERE workspace_id = $1::uuid AND card_id = $2::varchar AND message_id = $3::bigint AND reaction = $4::varchar AND creator = $5::varchar` - await this.execute(sql, [this.workspace, card, message, reaction, creator], 'remove reaction') - } else { - await this.createPatch(card, message, PatchType.removeReaction, reaction, creator, created) - } + await this.execute(sql, [this.workspace, card, message, reaction, creator], 'remove reaction') + } else { + await this.createPatch(card, message, PatchType.removeReaction, reaction, creator, created) } + } - // Thread - async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise { - const db: ThreadDb = { - workspace_id: this.workspace, - card_id: card, - message_id: message, - thread_id: thread, - replies_count: 0, - last_reply: created - } - const sql = `INSERT INTO ${TableName.Thread} (workspace_id, card_id, message_id, thread_id, replies_count, + // Thread + async createThread (card: CardID, message: MessageID, thread: CardID, created: Date): Promise { + const db: ThreadDb = { + workspace_id: this.workspace, + card_id: card, + message_id: message, + thread_id: thread, + replies_count: 0, + last_reply: created + } + const sql = `INSERT INTO ${TableName.Thread} (workspace_id, card_id, message_id, thread_id, replies_count, last_reply) VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::int, $6::timestamptz)` - await this.execute( - sql, - [db.workspace_id, db.card_id, db.message_id, db.thread_id, db.replies_count, db.last_reply], - 'insert thread' - ) + await this.execute( + sql, + [db.workspace_id, db.card_id, db.message_id, db.thread_id, db.replies_count, db.last_reply], + 'insert thread' + ) + } + + async updateThread (thread: CardID, op: 'increment' | 'decrement', lastReply?: Date): Promise { + const set: string[] = [] + const values: any[] = [] + + if (lastReply != null) { + set.push('last_reply = $3::timestamptz') + values.push(lastReply) } - async updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise { - if (op === 'increment') { - const sql = `UPDATE ${TableName.Thread} - SET replies_count = replies_count + 1, - last_reply = $3::timestamptz - WHERE workspace_id = $1::uuid - AND thread_id = $2::varchar` - await this.execute(sql, [this.workspace, thread, lastReply], 'update thread') - } else if (op === 'decrement') { - const sql = `UPDATE ${TableName.Thread} - SET replies_count = GREATEST(replies_count - 1, 0) - WHERE workspace_id = $1::uuid - AND thread_id = $2::varchar` - await this.execute(sql, [this.workspace, thread], 'update thread') - } + if (op === 'increment') { + set.push('replies_count = replies_count + 1') + } else if (op === 'decrement') { + set.push('replies_count = GREATEST(replies_count - 1, 0)') } - // MessagesGroup - async createMessagesGroup( - card: CardID, - blobId: BlobID, - fromDate: Date, - toDate: Date, - fromId: MessageID, - toId: MessageID, - count: number - ): Promise { - const db: MessagesGroupDb = { - workspace_id: this.workspace, - card_id: card, - blob_id: blobId, - from_date: fromDate, - to_date: toDate, - from_id: fromId, - to_id: toId, - count - } + const update = `UPDATE ${TableName.Thread}` + const setSql = 'SET ' + set.join(', ') + const where = 'WHERE workspace_id = $1::uuid AND thread_id = $2::varchar' + const sql = [update, setSql, where].join(' ') + await this.execute(sql, [this.workspace, thread, ...values], 'update thread') + } - const sql = `INSERT INTO ${TableName.MessagesGroup} (workspace_id, card_id, blob_id, from_date, to_date, - from_id, - to_id, count) - VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz, $6::bigint, $7::bigint, - $8::int)` - await this.execute( - sql, - [db.workspace_id, db.card_id, db.blob_id, db.from_date, db.to_date, db.from_id, db.to_id, db.count], - 'insert messages group' - ) + // MessagesGroup + async createMessagesGroup (card: CardID, blobId: BlobID, fromSec: Date, toSec: Date, count: number): Promise { + const db: MessagesGroupDb = { + workspace_id: this.workspace, + card_id: card, + blob_id: blobId, + from_sec: fromSec, + to_sec: toSec, + count } - async removeMessagesGroup(card: CardID, blobId: BlobID): Promise { - const sql = `DELETE + const sql = `INSERT INTO ${TableName.MessagesGroup} (workspace_id, card_id, blob_id, from_sec, to_sec, count) + VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz, $6::int)` + await this.execute( + sql, + [db.workspace_id, db.card_id, db.blob_id, db.from_sec, db.to_sec, db.count], + 'insert messages group' + ) + } + + async removeMessagesGroup (card: CardID, blobId: BlobID): Promise { + const sql = `DELETE FROM ${TableName.MessagesGroup} WHERE workspace_id = $1::uuid AND card_id = $2::varchar AND blob_id = $3::uuid` - await this.execute(sql, [this.workspace, card, blobId], 'remove messages group') + await this.execute(sql, [this.workspace, card, blobId], 'remove messages group') + } + + async find(params: FindMessagesParams): Promise { + const { where, values } = this.buildMessageWhere(params); + const orderBy = this.buildOrderBy(params); + const limit = this.buildLimit(params); + + const sql = ` + WITH + ${this.buildCteLimitedMessages(where, orderBy, limit)} + ${this.buildCteAggregatedFiles(params)} + ${this.buildCteAggregatedReactions(params)} + ${this.buildCteAggregatedPatches()} + ${this.buildMainSelect(params)} + `; + + const result = await this.execute(sql, values, 'find messages'); + return result.map((it: any) => toMessage(it)); + } + + private buildOrderBy(params: FindMessagesParams): string { + return params.order != null + ? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` + : ''; + } + + private buildLimit(params: FindMessagesParams): string { + return params.limit != null ? `LIMIT ${params.limit}` : ''; + } + + private buildCteLimitedMessages(where: string, orderBy: string, limit: string): string { + return ` + limited_messages AS ( + SELECT * + FROM ${TableName.Message} m + ${where} + ${orderBy} + ${limit} + ) + `; + } + + private buildCteAggregatedFiles(params: FindMessagesParams): string { + if (!params.files) return ''; + return `, + agg_files AS ( + SELECT + f.workspace_id, + f.card_id, + f.message_id, + jsonb_agg(jsonb_build_object( + 'card_id', f.card_id, + 'message_id', f.message_id, + 'blob_id', f.blob_id, + 'type', f.type, + 'filename', f.filename, + 'creator', f.creator, + 'created', f.created + )) AS files + FROM ${TableName.File} f + INNER JOIN limited_messages m + ON m.workspace_id = f.workspace_id + AND m.card_id = f.card_id + AND m.id = f.message_id + GROUP BY f.workspace_id, f.card_id, f.message_id + ) + `; + } + + private buildCteAggregatedReactions(params: FindMessagesParams): string { + if (!params.reactions) return ''; + return `, + agg_reactions AS ( + SELECT + r.workspace_id, + r.card_id, + r.message_id, + jsonb_agg(jsonb_build_object( + 'message_id', r.message_id, + 'reaction', r.reaction, + 'creator', r.creator, + 'created', r.created + )) AS reactions + FROM ${TableName.Reaction} r + INNER JOIN limited_messages m + ON m.workspace_id = r.workspace_id + AND m.card_id = r.card_id + AND m.id = r.message_id + GROUP BY r.workspace_id, r.card_id, r.message_id + ) + `; + } + + private buildCteAggregatedPatches(): string { + return `, + agg_patches AS ( + SELECT + p.workspace_id, + p.card_id, + p.message_id, + jsonb_agg( + jsonb_build_object( + 'content', p.content, + 'creator', p.creator, + 'created', p.created + ) ORDER BY p.created DESC + ) AS patches + FROM ${TableName.Patch} p + INNER JOIN limited_messages m + ON m.workspace_id = p.workspace_id + AND m.card_id = p.card_id + AND m.id = p.message_id + WHERE p.type = 'update' + GROUP BY p.workspace_id, p.card_id, p.message_id + ) + `; + } + + private buildMainSelect(params: FindMessagesParams): string { + const orderBy = this.buildOrderBy(params); + const selectReplies = params.replies + ? `t.thread_id as thread_id, t.replies_count as replies_count, t.last_reply as last_reply,` + : ''; + + const selectFiles = params.files + ? `COALESCE(f.files, '[]'::jsonb) AS files,` + : `'[]'::jsonb AS files,`; + + const selectReactions = params.reactions + ? `COALESCE(r.reactions, '[]'::jsonb) AS reactions,` + : `'[]'::jsonb AS reactions,`; + + const joinFiles = params.files ? ` + LEFT JOIN agg_files f + ON f.workspace_id = m.workspace_id + AND f.card_id = m.card_id + AND f.message_id = m.id` : ''; + + const joinReactions = params.reactions ? ` + LEFT JOIN agg_reactions r + ON r.workspace_id = m.workspace_id + AND r.card_id = m.card_id + AND r.message_id = m.id` : ''; + + return ` + SELECT + m.id, + m.card_id, + m.type, + m.content, + m.creator, + m.created, + m.data, + ${selectReplies} + ${selectFiles} + ${selectReactions} + COALESCE(p.patches, '[]'::jsonb) AS patches + FROM limited_messages m + LEFT JOIN ${TableName.Thread} t + ON t.workspace_id = m.workspace_id + AND t.card_id = m.card_id + AND t.message_id = m.id + ${joinFiles} + ${joinReactions} + LEFT JOIN agg_patches p + ON p.workspace_id = m.workspace_id + AND p.card_id = m.card_id + AND p.message_id = m.id + ${orderBy} + `; + } + + buildMessageWhere (params: FindMessagesParams): { where: string, values: any[] } { + const where: string[] = ['m.workspace_id = $1::uuid'] + const values: any[] = [this.workspace] + + let index = 2 + + if (params.id != null) { + where.push(`m.id = $${index++}::bigint`) + values.push(params.id) } - // Find messages - async find(params: FindMessagesParams): Promise { - // TODO: experiment with select to improve performance - const select = `SELECT m.id, - m.card_id, - m.content, - m.creator, - m.created, - t.thread_id as thread_id, - t.replies_count as replies_count, - t.last_reply as last_reply, - ${this.subSelectPatches()}, - ${this.subSelectReactions()} - FROM ${TableName.Message} m - LEFT JOIN ${TableName.Thread} t - ON t.workspace_id = m.workspace_id AND t.card_id = m.card_id AND - t.message_id = m.id` - - const {where, values} = this.buildMessageWhere(params) - const orderBy = - params.order != null ? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : '' - const limit = params.limit != null ? ` LIMIT ${params.limit}` : '' - const sql = [select, where, orderBy, limit].join(' ') - - const result = await this.execute(sql, values, 'find messages') - - return result.map((it: any) => toMessage(it)) + if (params.card != null) { + where.push(`m.card_id = $${index++}::varchar`) + values.push(params.card) } - buildMessageWhere(params: FindMessagesParams): { where: string, values: any[] } { - const where: string[] = ['m.workspace_id = $1::uuid'] - const values: any[] = [this.workspace] + const createdCondition = getCondition('m', 'created', index, params.created, 'timestamptz') - let index = 2 - - if (params.id != null) { - where.push(`m.id = $${index++}::bigint`) - values.push(params.id) - } - - if (params.card != null) { - where.push(`m.card_id = $${index++}::varchar`) - values.push(params.card) - } - - const createdCondition = getCondition('m', 'created', index, params.created, 'timestamptz') - - if (createdCondition != null) { - where.push(createdCondition.where) - values.push(createdCondition.value) - index++ - } - - return {where: `WHERE ${where.join(' AND ')}`, values} + if (createdCondition != null) { + where.push(createdCondition.where) + values.push(...createdCondition.values) + index = createdCondition.index } - subSelectPatches(): string { - return `COALESCE( - (SELECT jsonb_agg(jsonb_build_object( - 'content', p.content, - 'creator', p.creator, - 'created', p.created - ) ORDER BY p.created DESC) - FROM ${TableName.Patch} p - WHERE p.message_id = m.id - AND p.workspace_id = m.workspace_id - AND p.card_id = m.card_id - AND p.type = 'update' - ), '[]'::jsonb) AS patches` - } + return { where: `WHERE ${where.join(' AND ')}`, values } + } - subSelectAttachments(): string { - return `COALESCE( - (SELECT jsonb_agg(jsonb_build_object( - 'card_id', a.card_id, - 'message_id', a.message_id, - 'creator', a.creator, - 'created', a.created - )) - FROM ${TableName.Attachment} a - WHERE a.message_id = m.id - ), '[]'::jsonb) AS attachments` - } - - subSelectReactions(): string { - return `COALESCE( - (SELECT jsonb_agg(jsonb_build_object( - 'message_id', r.message_id, - 'reaction', r.reaction, - 'creator', r.creator, - 'created', r.created - )) - FROM ${TableName.Reaction} r - WHERE r.workspace_id = m.workspace_id - AND r.card_id = m.card_id - AND r.message_id = m.id - ), '[]'::jsonb) AS reactions` - } - - // Find thread - async findThread(thread: CardID): Promise { - const sql = `SELECT t.card_id, + // Find thread + async findThread (thread: CardID): Promise { + const sql = `SELECT t.card_id, t.message_id, t.thread_id, t.replies_count, @@ -418,19 +504,17 @@ export class MessagesDb extends BaseDb { AND t.thread_id = $2::varchar LIMIT 1;` - const result = await this.execute(sql, [this.workspace, thread], 'find thread') - return result.map((it: any) => toThread(it))[0] - } + const result = await this.execute(sql, [this.workspace, thread], 'find thread') + return result.map((it: any) => toThread(it))[0] + } - // Find messages groups - async findMessagesGroups(params: FindMessagesGroupsParams): Promise { - const select = ` + // Find messages groups + async findMessagesGroups (params: FindMessagesGroupsParams): Promise { + const select = ` SELECT mg.card_id, mg.blob_id, - mg.from_date, - mg.to_date, - mg.from_id, - mg.to_id, + mg.from_sec, + mg.to_sec, mg.count, patches FROM ${TableName.MessagesGroup} mg @@ -445,57 +529,57 @@ export class MessagesDb extends BaseDb { FROM ${TableName.Patch} p WHERE p.workspace_id = mg.workspace_id AND p.card_id = mg.card_id - AND p.message_id BETWEEN mg.from_id AND mg.to_id + AND p.message_created_sec BETWEEN mg.from_sec AND mg.to_sec ) sub` - const {where, values} = this.buildMessagesGroupWhere(params) - const orderBy = - params.orderBy === 'toDate' - ? `ORDER BY mg.to_date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` - : `ORDER BY mg.from_date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` - const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''; + const { where, values } = this.buildMessagesGroupWhere(params) + const orderBy = + params.orderBy === 'toSec' + ? `ORDER BY mg.to_sec ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` + : `ORDER BY mg.from_sec ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` + const limit = params.limit != null ? ` LIMIT ${params.limit}` : '' - const sql = [select, where, orderBy, limit].join(' ') - const result = await this.execute(sql, values, 'find messages groups') + const sql = [select, where, orderBy, limit].join(' ') + const result = await this.execute(sql, values, 'find messages groups') - return result.map((it: any) => toMessagesGroup(it)) + return result.map((it: any) => toMessagesGroup(it)) + } + + buildMessagesGroupWhere (params: FindMessagesGroupsParams): { + where: string + values: any[] + } { + const where: string[] = ['mg.workspace_id = $1::uuid'] + const values: any[] = [this.workspace] + + let index = 2 + + where.push(`mg.card_id = $${index++}::varchar`) + values.push(params.card) + + if (params.blobId != null) { + where.push(`mg.blob_id = $${index++}`) + values.push(params.blobId) } - buildMessagesGroupWhere(params: FindMessagesGroupsParams): { - where: string - values: any[] - } { - const where: string[] = ['mg.workspace_id = $1::uuid'] - const values: any[] = [this.workspace] - - let index = 2 - - where.push(`mg.card_id = $${index++}::varchar`) - values.push(params.card) - - if (params.blobId != null) { - where.push(`mg.blob_id = $${index++}`) - values.push(params.blobId) - } - - const fromDateCondition = getCondition('mg', 'from_date', index, params.fromDate, 'timestamptz') - if (fromDateCondition != null) { - where.push(fromDateCondition.where) - values.push(fromDateCondition.value) - index++ - } - - const toDateCondition = getCondition('mg', 'to_date', index, params.toDate, 'timestamptz') - if (toDateCondition != null) { - where.push(toDateCondition.where) - values.push(toDateCondition.value) - index++ - } - - if (params.withPatches === true) { - where.push(`sub.patches IS NOT NULL`) - } - - return {where: `WHERE ${where.join(' AND ')}`, values} + const fromDateCondition = getCondition('mg', 'from_sec', index, params.fromSec, 'timestamptz') + if (fromDateCondition != null) { + where.push(fromDateCondition.where) + values.push(...fromDateCondition.values) + index = fromDateCondition.index } + + const toDateCondition = getCondition('mg', 'to_sec', index, params.toSec, 'timestamptz') + if (toDateCondition != null) { + where.push(toDateCondition.where) + values.push(...toDateCondition.values) + index = toDateCondition.index + } + + if (params.patches === true) { + where.push('sub.patches IS NOT NULL') + } + + return { where: `WHERE ${where.join(' AND ')}`, values } + } } diff --git a/packages/cockroach/src/db/notification.ts b/packages/cockroach/src/db/notification.ts index 5f6a0e2dc5..94258d5927 100644 --- a/packages/cockroach/src/db/notification.ts +++ b/packages/cockroach/src/db/notification.ts @@ -14,82 +14,140 @@ // import { - type MessageID, - type ContextID, + type AccountID, type CardID, - type NotificationContext, + type Collaborator, + type ContextID, + type FindCollaboratorsParams, type FindNotificationContextParams, - SortingOrder, type FindNotificationsParams, + type MessageID, type Notification, - type NotificationContextUpdate, - type WorkspaceID + type NotificationContext, + SortingOrder, + type NotificationID } from '@hcengineering/communication-types' import { BaseDb } from './base' -import { TableName, type ContextDb, type NotificationDb } from './schema' +import { type ContextDb, type NotificationDb, TableName } from './schema' +import { getCondition } from './utils' +import { toCollaborator, toNotification, toNotificationContext } from './mapping' export class NotificationsDb extends BaseDb { - async createNotification (message: MessageID, context: ContextID): Promise { - const db: NotificationDb = { - message_id: message, - context + async addCollaborators (card: CardID, collaborators: AccountID[], date?: Date): Promise { + if (collaborators.length === 0) return + const values: any[] = [] + + const sqlValues = collaborators + .map((account, index) => { + const i = index * 3 + values.push(this.workspace, card, account, date ?? new Date()) + return `($${i + 1}::uuid, $${i + 2}::varchar, $${i + 3}::uuid, $${i + 4}::timestamptz)` + }) + .join(', ') + + const sql = `INSERT INTO ${TableName.Collaborators} (workspace_id, card_id, account, date) VALUES ${sqlValues} ON CONFLICT DO NOTHING` + + await this.execute(sql, values, 'insert collaborators') + } + + async removeCollaborators (card: CardID, collaborators: AccountID[]): Promise { + if (collaborators.length === 0) return + + if (collaborators.length === 1) { + const sql = `DELETE + FROM ${TableName.Collaborators} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND account = $3::uuid` + await this.execute(sql, [this.workspace, card, collaborators[0]], 'remove collaborator') + } else { + const inValues = collaborators.map((_, index) => `$${index + 3}`).join(', ') + const sql = `DELETE + FROM ${TableName.Collaborators} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND account IN (${inValues})` + + await this.execute(sql, [this.workspace, card, ...collaborators], 'remove collaborators') } - const sql = `INSERT INTO ${TableName.Notification} (message_id, context_id) - VALUES ($1::bigint, $2::uuid)` - await this.execute(sql, [db.message_id, db.context], 'insert notification') } - async removeNotification (message: MessageID, context: ContextID): Promise { - // TODO: do we need to remove notifications? - const sql = `DELETE - FROM ${TableName.NotificationContext} - WHERE id = $1::uuid` - await this.execute(sql, [context], 'remove notification') - } - - async createContext ( - personalWorkspace: WorkspaceID, + getCollaboratorsCursor ( card: CardID, - lastView?: Date, - lastUpdate?: Date - ): Promise { + date: Date, + size?: number + ): AsyncIterable[]> { + const sql = ` + SELECT account + FROM ${TableName.Collaborators} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND date <= $3::timestamptz + ORDER BY date ASC ` + + return this.client.cursor(sql, [this.workspace, card, date], size) + } + + async createNotification (context: ContextID, message: MessageID, created: Date): Promise { + const db: Omit = { + message_id: message, + context_id: context, + created + } + const sql = `INSERT INTO ${TableName.Notification} (message_id, context_id, created) + VALUES ($1::bigint, $2::int8, $3::timestamptz) + RETURNING id` + const result = await this.execute(sql, [db.message_id, db.context_id, db.created], 'insert notification') + return result[0].id as NotificationID + } + + async removeNotifications (context: ContextID, account: AccountID, untilDate: Date): Promise { + const sql = ` + DELETE FROM ${TableName.Notification} n + USING ${TableName.NotificationContext} nc + WHERE n.context_id = $1::int8 + AND nc.id = n.context_id + AND nc.account = $2::uuid + AND n.created < $3::timestamptz + ` + await this.execute(sql, [context, account, untilDate], 'remove notification') + } + + async createContext (account: AccountID, card: CardID, lastUpdate: Date, lastView: Date): Promise { const db: ContextDb = { workspace_id: this.workspace, card_id: card, - personal_workspace: personalWorkspace, + account, last_view: lastView, last_update: lastUpdate } - const sql = `INSERT INTO ${TableName.NotificationContext} (workspace_id, card_id, personal_workspace, last_view, last_update) - VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz) - RETURNING id` + const sql = `INSERT INTO ${TableName.NotificationContext} (workspace_id, card_id, account, last_view, last_update) + VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz) + RETURNING id` const result = await this.execute( sql, - [db.workspace_id, db.card_id, db.personal_workspace, db.last_view, db.last_update], + [db.workspace_id, db.card_id, db.account, db.last_view, db.last_update], 'insert notification context' ) return result[0].id as ContextID } - async removeContext (context: ContextID): Promise { + async removeContext (context: ContextID, account: AccountID): Promise { const sql = `DELETE - FROM ${TableName.Notification} - WHERE context = $1::uuid` - await this.execute(sql, [context], 'remove notification context') + FROM ${TableName.Notification} + WHERE context = $1::int8 AND account = $2::uuid` + await this.execute(sql, [context, account], 'remove notification context') } - async updateContext (context: ContextID, update: NotificationContextUpdate): Promise { + async updateContext (context: ContextID, account: AccountID, lastUpdate?: Date, lastView?: Date): Promise { const dbData: Partial = {} - if (update.archivedFrom != null) { - dbData.archived_from = update.archivedFrom + if (lastView != null) { + dbData.last_view = lastView } - if (update.lastView != null) { - dbData.last_view = update.lastView - } - if (update.lastUpdate != null) { - dbData.last_update = update.lastUpdate + if (lastUpdate != null) { + dbData.last_update = lastUpdate } if (Object.keys(dbData).length === 0) { @@ -100,91 +158,236 @@ export class NotificationsDb extends BaseDb { const values = Object.values(dbData) const sql = `UPDATE ${TableName.NotificationContext} - SET ${keys.map((k, idx) => `"${k}" = $${idx + 1}::timestamptz`).join(', ')} - WHERE id = $${keys.length + 1}::uuid;` - await this.execute(sql, [values, context], 'update notification context') + SET ${keys.map((k, idx) => `"${k}" = $${idx + 3}::timestamptz`).join(', ')} + WHERE id = $1::int8 AND account = $2::uuid;` + + await this.execute(sql, [context, account, ...values], 'update notification context') } - async findContexts ( - params: FindNotificationContextParams, - personalWorkspaces: WorkspaceID[], - workspace?: WorkspaceID - ): Promise { - const select = ` - SELECT nc.id, nc.card_id, nc.archived_from, nc.last_view, nc.last_update - FROM ${TableName.NotificationContext} nc` - const { where, values } = this.buildContextWhere(params, personalWorkspaces, workspace) - // const orderSql = `ORDER BY nc.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` - const limit = params.limit != null ? ` LIMIT ${params.limit}` : '' - const sql = [select, where, limit].join(' ') + async findContexts (params: FindNotificationContextParams): Promise { + const withNotifications = params.notifications != null + const withMessages = params.notifications?.message === true - const result = await this.execute(sql, values, 'find notification contexts') - - return result.map((it) => this.toNotificationContext(it)) - } - - async findNotifications ( - params: FindNotificationsParams, - personalWorkspace: WorkspaceID, - workspace?: WorkspaceID - ): Promise { - // TODO: experiment with select to improve performance, should join with attachments and reactions? - const select = ` - SELECT n.message_id, - n.context, - m.card_id AS message_card, - m.content AS message_content, - m.creator AS message_creator, - m.created AS message_created, - nc.card_id, - nc.archived_from, - nc.last_view, - nc.last_update, - (SELECT json_agg( - jsonb_build_object( - 'id', p.id, - 'content', p.content, - 'creator', p.creator, - 'created', p.created - ) - ) - FROM ${TableName.Patch} p - WHERE p.message_id = m.id) AS patches - FROM ${TableName.Notification} n - JOIN ${TableName.NotificationContext} nc ON n.context = nc.id - JOIN ${TableName.Message} m ON n.message_id = m.id - ` - const { where, values } = this.buildNotificationWhere(params, personalWorkspace, workspace) + const { where, values, index } = this.buildContextWhere(params) + const limit = params.limit != null ? `LIMIT ${Number(params.limit)}` : '' const orderBy = - params.order != null ? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : '' - const limit = params.limit != null ? ` LIMIT ${params.limit}` : '' - const sql = [select, where, orderBy, limit].join(' ') + params.order != null ? `ORDER BY nc.last_update ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : '' + + let joinMessages = '' + let buildObject = ` + JSONB_BUILD_OBJECT( + 'id', n.id, + 'created', n.created, + 'message_id', n.message_id + )` + + if (withMessages) { + joinMessages = ` + LEFT JOIN ${TableName.Message} m + ON n.message_id = m.id + AND nc.workspace_id = m.workspace_id + AND nc.card_id = m.card_id + LEFT JOIN ${TableName.MessagesGroup} mg + ON nc.workspace_id = mg.workspace_id + AND nc.card_id = mg.card_id + AND n.created BETWEEN mg.from_sec AND mg.to_sec` + + buildObject = ` + JSONB_BUILD_OBJECT( + 'id', n.id, + 'created', n.created, + 'message_id', n.message_id, + 'message_type', m.type, + 'message_content', m.content, + 'message_data', m.data, + 'message_creator', m.creator, + 'message_created', m.created, + 'message_group_blob_id', mg.blob_id, + 'message_group_from_sec', mg.from_sec, + 'message_group_to_sec', mg.to_sec, + 'message_group_count', mg.count, + 'message_patches', ( + SELECT COALESCE( + JSON_AGG( + JSONB_BUILD_OBJECT( + 'patch_type', p.type, + 'patch_content', p.content, + 'patch_creator', p.creator, + 'patch_created', p.created + ) ORDER BY p.created DESC + ), + '[]'::JSONB + ) + FROM ${TableName.Patch} p + WHERE p.message_id = n.message_id + ) + )` + } + + let joinNotifications = '' + let notificationsSelect = '' + let groupBy = '' + + if (withNotifications) { + const { where: whereNotifications, values: valuesNotifications } = this.buildNotificationWhere( + { read: params.notifications?.read }, + index, + true + ) + + values.push(...valuesNotifications) + + joinNotifications = ` + LEFT JOIN LATERAL ( + SELECT + n.*, + ROW_NUMBER() OVER ( + PARTITION BY n.context_id + ORDER BY n.created ${params.notifications?.order === SortingOrder.Ascending ? 'ASC' : 'DESC'} + ) AS rn + FROM ${TableName.Notification} n + ${whereNotifications} ${whereNotifications.length > 1 ? 'AND n.context_id = nc.id' : 'WHERE n.context_id = nc.id'} + ) n ON n.rn <= ${params.notifications?.limit ?? 1}` + + notificationsSelect = `, + COALESCE( + JSON_AGG( + ${buildObject} + ORDER BY n.created ${params.notifications?.order === SortingOrder.Ascending ? 'ASC' : 'DESC'} + ), + '[]'::JSONB + ) AS notifications` + + groupBy = 'GROUP BY nc.id' + } + + const sql = ` + SELECT nc.id, + nc.card_id, + nc.account, + nc.last_view, + nc.last_update + ${notificationsSelect} + FROM ${TableName.NotificationContext} nc + ${joinNotifications} + ${joinMessages} + ${where} + ${groupBy} + ${orderBy} + ${limit}; + ` + + const result = await this.execute(sql, values, 'find contexts') + + return result.map((it: any) => toNotificationContext(it)) + } + + async findNotifications (params: FindNotificationsParams): Promise { + const withMessage = params.message === true + + let select = 'SELECT n.id, n.created,n.message_id, n.context_id, nc.last_view ' + + let joinMessages = '' + + if (withMessage) { + select += `, + m.card_id AS card_id, + m.type AS message_type, + m.content AS message_content, + m.creator AS message_creator, + m.created AS message_created, + m.data AS message_data, + mg.blob_id AS message_group_blob_id, + mg.from_sec AS message_group_from_sec, + mg.to_sec AS message_group_to_sec, + mg.count AS message_group_count, + (SELECT json_agg( + jsonb_build_object( + 'id', p.id, + 'content', p.content, + 'creator', p.creator, + 'created', p.created + ) + ) + FROM ${TableName.Patch} p + WHERE p.message_id = m.id) AS message_patches + ` + + joinMessages = ` + LEFT JOIN ${TableName.Message} m + ON n.message_id = m.id + AND nc.workspace_id = m.workspace_id + AND nc.card_id = m.card_id + LEFT JOIN ${TableName.MessagesGroup} mg + ON nc.workspace_id = mg.workspace_id + AND nc.card_id = mg.card_id + AND n.created BETWEEN mg.from_sec AND mg.to_sec + ` + } + + select += ` FROM ${TableName.Notification} n + JOIN ${TableName.NotificationContext} nc ON n.context_id = nc.id` + + const { where, values } = this.buildNotificationWhere(params) + const orderBy = + params.order != null ? `ORDER BY n.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : '' + const limit = params.limit != null ? `LIMIT ${params.limit}` : '' + + const sql = [select, joinMessages, where, orderBy, limit].join(' ') const result = await this.execute(sql, values, 'find notifications') - return result.map((it) => this.toNotification(it)) + return result.map((it: any) => toNotification(it)) } - buildContextWhere ( - params: FindNotificationContextParams, - personalWorkspaces: WorkspaceID[], - workspace?: WorkspaceID - ): { - where: string - values: any[] - } { - const where: string[] = [] - const values: any[] = [] - let index = 1 + async findCollaborators (params: FindCollaboratorsParams): Promise { + const { where, values } = this.buildCollaboratorsWhere(params) + const select = ` + SELECT c.account + FROM ${TableName.Collaborators} c + ` - if (workspace != null) { - where.push(`nc.workspace_id = $${index++}::uuid`) - values.push(workspace) + const limit = params.limit != null ? ` LIMIT ${params.limit}` : '' + const orderBy = + params.order != null ? `ORDER BY c.date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : '' + + const sql = [select, where, orderBy, limit].join(' ') + const result = await this.execute(sql, values, 'find collaborators') + + return result.map((it: any) => toCollaborator(it)) + } + + private buildCollaboratorsWhere (params: FindCollaboratorsParams): { where: string, values: any[] } { + const where: string[] = ['c.workspace_id = $1::uuid', 'c.card_id = $2::varchar'] + const values: any[] = [this.workspace, params.card] + let index = values.length + 1 + + if (params.account != null) { + const accounts = Array.isArray(params.account) ? params.account : [params.account] + if (accounts.length === 1) { + where.push(`c.account = $${index++}::uuid`) + values.push(accounts[0]) + } else if (accounts.length > 1) { + where.push(`c.account = ANY($${index++}::uuid[])`) + values.push(accounts) + } } - if (personalWorkspaces.length > 0) { - where.push(`nc.personal_workspace IN (${personalWorkspaces.map((it) => `$${index++}::uuid`).join(', ')})`) - values.push(...personalWorkspaces) + return { where: `WHERE ${where.join(' AND ')}`, values } + } + + private buildContextWhere (params: FindNotificationContextParams): { + where: string + values: any[] + index: number + } { + const where: string[] = ['nc.workspace_id = $1::uuid'] + const values: any[] = [this.workspace] + let index = 2 + + if (params.id != null) { + where.push(`nc.id = $${index++}::int8`) + values.push(params.id) } if (params.card != null) { @@ -192,82 +395,64 @@ export class NotificationsDb extends BaseDb { values.push(params.card) } - return { where: `WHERE ${where.join(' AND ')}`, values } + if (params.account != null) { + const accounts = Array.isArray(params.account) ? params.account : [params.account] + if (accounts.length === 1) { + where.push(`nc.account = $${index++}::uuid`) + values.push(accounts[0]) + } else if (accounts.length > 1) { + where.push(`nc.account IN (${accounts.map((it) => `$${index++}::uuid`).join(', ')})`) + values.push(...accounts) + } + } + + const lastUpdateCondition = getCondition('nc', 'last_update', index, params.lastUpdate, 'timestamptz') + + if (lastUpdateCondition != null) { + where.push(lastUpdateCondition.where) + values.push(...lastUpdateCondition.values) + index= lastUpdateCondition.index + } + + return { where: `WHERE ${where.join(' AND ')}`, values, index } } - buildNotificationWhere ( + private buildNotificationWhere ( params: FindNotificationsParams, - personalWorkspace: WorkspaceID, - workspace?: WorkspaceID + initialIndex?: number, + skipWorkspace?: boolean ): { where: string values: any[] } { - const where: string[] = ['nc.personal_workspace = $1::uuid'] - const values: any[] = [personalWorkspace] - let index = 2 - - if (workspace != null) { - where.push(`nc.workspace_id = $${index++}::uuid`) - values.push(workspace) - } + const where: string[] = skipWorkspace === true ? [] : ['nc.workspace_id = $1::uuid'] + const values: any[] = skipWorkspace === true ? [] : [this.workspace] + let index = (initialIndex ?? 0) + values.length + 1 if (params.context != null) { - where.push(`n.context = $${index++}::uuid`) + where.push(`n.context_id = $${index++}::int8`) values.push(params.context) } + if (params.account != null) { + const accounts = Array.isArray(params.account) ? params.account : [params.account] + if (accounts.length === 1) { + where.push(`nc.account = $${index++}::uuid`) + values.push(accounts[0]) + } else if (accounts.length > 1) { + where.push(`nc.account = ANY ($${index++}::uuid[])`) + values.push(accounts) + } + } + if (params.read === true) { - where.push('nc.last_view IS NOT NULL AND nc.last_view >= m.created') + where.push('nc.last_view IS NOT NULL AND nc.last_view >= n.created') } if (params.read === false) { - where.push('(nc.last_view IS NULL OR nc.last_view > m.created)') + where.push('(nc.last_view IS NULL OR nc.last_view < n.created)') } - if (params.archived === true) { - where.push('nc.archived_from IS NOT NULL AND nc.archived_from >= m.created') - } - - if (params.archived === false) { - where.push('(nc.archived_from IS NULL OR nc.archived_from > m.created)') - } - - return { where: `WHERE ${where.join(' AND ')}`, values } - } - - toNotificationContext (row: any): NotificationContext { - return { - id: row.id, - card: row.card_id, - workspace: row.workspace_id, - personalWorkspace: row.personal_workspace, - archivedFrom: row.archived_from != null ? new Date(row.archived_from) : undefined, - lastView: row.last_view != null ? new Date(row.last_view) : undefined, - lastUpdate: row.last_update != null ? new Date(row.last_update) : undefined - } - } - - toNotification (row: any): Notification { - const lastPatch = row.patches?.[0] - const lastView = row.last_view != null ? new Date(row.last_view) : undefined - const archivedFrom = row.archived_from != null ? new Date(row.archived_from) : undefined - const created = new Date(row.message_created) - - return { - message: { - id: row.id, - card: row.message_card, - content: lastPatch?.content ?? row.message_content, - creator: row.message_creator, - created, - edited: new Date(lastPatch?.created ?? row.message_created), - reactions: row.reactions ?? [], - attachments: row.attachments ?? [] - }, - context: row.context, - read: lastView != null && lastView >= created, - archived: archivedFrom != null && archivedFrom >= created - } + return { where: where.length > 0 ? `WHERE ${where.join(' AND ')}` : '', values } } } diff --git a/packages/cockroach/src/db/schema.ts b/packages/cockroach/src/db/schema.ts index 298a35ba9d..b09e608c0e 100644 --- a/packages/cockroach/src/db/schema.ts +++ b/packages/cockroach/src/db/schema.ts @@ -14,53 +14,48 @@ // import { + type AccountID, + type BlobID, + type CardID, type ContextID, type MessageID, + type MessageType, + type PatchType, type RichText, type SocialID, - type CardID, - type BlobID, - type Message, - type Reaction, - type Attachment, - type MessagesGroup, type WorkspaceID, - type PatchType, - type Patch, - type Thread + type NotificationID } from '@hcengineering/communication-types' export enum TableName { - Attachment = 'communication.attachments', + File = 'communication.files', Message = 'communication.messages', MessagesGroup = 'communication.messages_groups', Notification = 'communication.notifications', NotificationContext = 'communication.notification_context', Patch = 'communication.patch', Reaction = 'communication.reactions', - Thread = 'communication.thread' + Thread = 'communication.thread', + Collaborators = 'communication.collaborators' } export interface MessageDb { id: MessageID + type: MessageType workspace_id: WorkspaceID card_id: CardID content: RichText creator: SocialID created: Date - thread_id?: CardID - replies_count?: number - last_reply?: Date + data?: any } export interface MessagesGroupDb { workspace_id: WorkspaceID card_id: CardID blob_id: BlobID - from_date: Date - to_date: Date - from_id: MessageID - to_id: MessageID + from_sec: Date + to_sec: Date count: number patches?: PatchDb[] } @@ -73,6 +68,7 @@ export interface PatchDb { content: RichText creator: SocialID created: Date + message_created_sec: Date } export interface ReactionDb { @@ -84,11 +80,17 @@ export interface ReactionDb { created: Date } -export interface AttachmentDb { - message_id: MessageID +export interface FileDb { + workspace_id: WorkspaceID card_id: CardID + message_id: MessageID + blob_id: BlobID + filename: string + size: number + type: string creator: SocialID created: Date + message_created_sec: Date } export interface ThreadDb { @@ -101,98 +103,23 @@ export interface ThreadDb { } export interface NotificationDb { - message_id: MessageID - context: ContextID + id: NotificationID + message_id: MessageID | null + context_id: ContextID + created: Date } export interface ContextDb { workspace_id: WorkspaceID card_id: CardID - personal_workspace: WorkspaceID - - archived_from?: Date - last_view?: Date - last_update?: Date + account: AccountID + last_update: Date + last_view: Date } -interface RawMessage extends MessageDb { - patches?: PatchDb[] - attachments?: AttachmentDb[] - reactions?: ReactionDb[] -} - -export function toMessage (raw: RawMessage): Message { - const lastPatch = raw.patches?.[0] - - return { - id: String(raw.id) as MessageID, - card: raw.card_id, - content: lastPatch?.content ?? raw.content, - creator: raw.creator, - created: raw.created, - edited: lastPatch?.created ?? undefined, - thread: - raw.thread_id != null - ? { - card: raw.card_id, - message: String(raw.id) as MessageID, - thread: raw.thread_id, - repliesCount: raw.replies_count ?? 0, - lastReply: raw.last_reply ?? new Date() - } - : undefined, - reactions: (raw.reactions ?? []).map(toReaction), - attachments: (raw.attachments ?? []).map(toAttachment) - } -} - -export function toReaction (raw: ReactionDb): Reaction { - return { - message: String(raw.message_id) as MessageID, - reaction: raw.reaction, - creator: raw.creator, - created: raw.created - } -} - -export function toAttachment (raw: AttachmentDb): Attachment { - return { - message: String(raw.message_id) as MessageID, - card: raw.card_id, - creator: raw.creator, - created: raw.created - } -} - -export function toMessagesGroup (raw: MessagesGroupDb): MessagesGroup { - return { - card: raw.card_id, - blobId: raw.blob_id, - fromDate: raw.from_date, - toDate: raw.to_date, - fromId: String(raw.from_id) as MessageID, - toId: String(raw.to_id) as MessageID, - count: raw.count, - patches: raw.patches == null ? [] : raw.patches.filter((it: any) => it.message_id != null).map(toPatch) - } -} - -export function toPatch (raw: PatchDb): Patch { - return { - type: raw.type, - message: String(raw.message_id) as MessageID, - content: raw.content, - creator: raw.creator, - created: new Date(raw.created) - } -} - -export function toThread (raw: ThreadDb): Thread { - return { - card: raw.card_id, - message: String(raw.message_id) as MessageID, - thread: raw.thread_id, - repliesCount: raw.replies_count, - lastReply: raw.last_reply - } +export interface CollaboratorDb { + workspace_id: WorkspaceID + card_id: CardID + account: AccountID + date: Date } diff --git a/packages/cockroach/src/db/utils.ts b/packages/cockroach/src/db/utils.ts index 950ef1bb5a..17a6fb9265 100644 --- a/packages/cockroach/src/db/utils.ts +++ b/packages/cockroach/src/db/utils.ts @@ -13,31 +13,47 @@ // limitations under the License. // -export function getCondition ( +export function getCondition( table: string, dbField: string, - index: number, + startIndex: number, param: any, type: string -): { where: string, value: any } | undefined { - if (typeof param === 'object') { - if (param.less != null) { - return { where: `${table}.${dbField} < $${index}::${type}`, value: param.less } +): { where: string, values: any[], index: number } | undefined { + const conditions: string[] = []; + const values: any[] = []; + let index = startIndex; + + if (param !== null && typeof param === 'object') { + if (param.less !== undefined) { + conditions.push(`${table}.${dbField} < $${index}::${type}`); + values.push(param.less); + index++; } - if (param.lessOrEqual != null) { - return { where: `${table}.${dbField} <= $${index}::${type}`, value: param.lessOrEqual } + if (param.lessOrEqual !== undefined) { + conditions.push(`${table}.${dbField} <= $${index}::${type}`); + values.push(param.lessOrEqual); + index++; } - if (param.greater != null) { - return { where: `${table}.${dbField} > $${index}::${type}`, value: param.greater } + if (param.greater !== undefined) { + conditions.push(`${table}.${dbField} > $${index}::${type}`); + values.push(param.greater); + index++; } - if (param.greaterOrEqual != null) { - return { where: `${table}.${dbField} >= $${index}::${type}`, value: param.greaterOrEqual } + if (param.greaterOrEqual !== undefined) { + conditions.push(`${table}.${dbField} >= $${index}::${type}`); + values.push(param.greaterOrEqual); + index++; } } - if (param != null) { - return { where: `${table}.${dbField} = $${index}::${type}`, value: param } + if (param != null && conditions.length === 0) { + conditions.push(`${table}.${dbField} = $${index}::${type}`); + values.push(param); + index++; } - return undefined -} + if (conditions.length === 0) return undefined; + + return { where: conditions.join(' AND '), values, index }; +} \ No newline at end of file diff --git a/packages/cockroach/src/types.ts b/packages/cockroach/src/types.ts index f8545d4129..1e7a5280e9 100644 --- a/packages/cockroach/src/types.ts +++ b/packages/cockroach/src/types.ts @@ -15,6 +15,9 @@ import { type ParameterOrJSON, type Row } from 'postgres' +export type SqlRow = Row & Iterable +export type SqlParams = ParameterOrJSON[] + export interface Logger { info: (message: string, data?: Record) => void warn: (message: string, data?: Record) => void @@ -26,6 +29,7 @@ export interface Options { } export interface SqlClient { - execute: )[]>(query: string, params?: ParameterOrJSON[]) => Promise + execute: (query: string, params?: SqlParams) => Promise + cursor: (query: string, params?: SqlParams, size?: number) => AsyncIterable[]> close: () => void } diff --git a/packages/cockroach/src/utils.ts b/packages/cockroach/src/utils.ts index 6ffc522d3f..213f6d7f3f 100644 --- a/packages/cockroach/src/utils.ts +++ b/packages/cockroach/src/utils.ts @@ -44,6 +44,8 @@ function escape(value: any): string { return `'${value.replace(/'/g, "''")}'` case 'bigint': return value.toString() + case 'object': + return `'${JSON.stringify(value)}'` default: throw new Error(`Unsupported value type: ${typeof value}`) } diff --git a/packages/examples/package.json b/packages/examples/package.json deleted file mode 100644 index c83e4869f4..0000000000 --- a/packages/examples/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@hcengineering/communication-examples", - "version": "0.1.61", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "./types/index.d.ts", - "files": [ - "dist/index.js", - "dist/index.cjs", - "types/**/*.d.ts" - ], - "scripts": { - "bundle": "bun run bundle:browser", - "bundle:browser": "bun build src/index.ts --outdir dist --target browser" - }, - "devDependencies": { - "@types/bun": "^1.1.14" - }, - "dependencies": { - "@hcengineering/communication-client-ws": "workspace:*", - "@hcengineering/communication-types": "workspace:*" - }, - "peerDependencies": { - "typescript": "^5.6.3" - } -} diff --git a/packages/examples/src/httpServer.ts b/packages/examples/src/httpServer.ts deleted file mode 100644 index 4320997f2b..0000000000 --- a/packages/examples/src/httpServer.ts +++ /dev/null @@ -1,70 +0,0 @@ -Bun.serve({ - port: 3003, - - fetch(req) { - const url = new URL(req.url) - const headers = { - 'Cross-Origin-Opener-Policy': 'same-origin', - 'Cross-Origin-Embedder-Policy': 'require-corp' - } - if (url.pathname === '/') { - return new Response(Bun.file('./index.html'), { headers }) - } - - if (url.pathname.endsWith('index.js')) { - return new Response(Bun.file('../dist/index.js'), { headers }) - } - - if (url.pathname.endsWith('sqlite3-opfs-async-proxy.js')) { - return new Response( - Bun.file('../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js'), - { - headers - } - ) - } - - if (url.pathname.endsWith('index.mjs')) { - return new Response(Bun.file('../../../node_modules/@sqlite.org/sqlite-wasm/index.mjs'), { - headers - }) - } - - if (url.pathname.endsWith('sqlite3-worker1-promiser.mjs')) { - return new Response( - Bun.file('../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-worker1-promiser.mjs'), - { - headers - } - ) - } - - if (url.pathname.endsWith('sqlite3-worker1-bundler-friendly.mjs')) { - return new Response( - Bun.file( - '../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-worker1-bundler-friendly.mjs' - ), - { - headers - } - ) - } - - if (url.pathname.endsWith('sqlite3.js')) { - return new Response(Bun.file('../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.js'), { - headers - }) - } - - if (url.pathname.endsWith('.wasm')) { - return new Response(Bun.file('../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm'), { - headers: { - ...headers, - ContentType: 'application/wasm' - } - }) - } - - return new Response('Not Found', { status: 404, headers }) - } -}) diff --git a/packages/examples/src/index.html b/packages/examples/src/index.html deleted file mode 100644 index 1eae9ec030..0000000000 --- a/packages/examples/src/index.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - -

-
-
-
- - -
- - - - - - - \ No newline at end of file diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts deleted file mode 100644 index d72db680f7..0000000000 --- a/packages/examples/src/index.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { - type Message, - type SocialID, - SortingOrder, - type CardID, - type Window, - type WorkspaceID -} from '@hcengineering/communication-types' -import { getWebsocketClient, createMessagesQuery } from '@hcengineering/communication-client-ws' - -const card = 'cd0aba36-1c4f-4170-95f2-27a12a5415f6' as CardID -const workspace = 'cd0aba36-1c4f-4170-95f2-27a12a5415f6' as WorkspaceID -// const personalWorkspace = 'cd0aba36-1c4f-4170-95f2-27a12a5415f5' as WorkspaceID -const creator1 = 'email:vasya@huly.com' as SocialID - -export async function example() { - const platformUrl = 'ws://localhost:8090' - const token = 'token' - const client = await getWebsocketClient( - platformUrl, - token, - workspace, - 'http://localhost:4022/blob/:workspace/:blobId/:filename' - ) - - const query1 = createMessagesQuery() - - let window: Window | undefined = undefined - - query1.query({ card, order: SortingOrder.Descending }, (res) => { - window = res - const r = window.getResult() - r.reverse() - showMessages(r) - }) - - document.getElementById('forward-button')?.addEventListener('click', async () => { - if (window == null) return - await window.loadNextPage() - }) - - document.getElementById('backward-button')?.addEventListener('click', async () => { - if (window == null) return - await window.loadPrevPage() - }) - - async function editMessage(message: Message) { - await client.updateMessage(card, message.id, message.content + '_1_', creator1) - } - - async function deleteMessage(message: Message) { - await client.removeMessage(card, message.id) - } - - async function addReaction(message: Message) { - await client.createReaction(card, message.id, '👍', creator1) - } - - async function removeReaction(message: Message) { - await client.removeReaction(card, message.id, '👍', creator1) - } - - function scrollToBottom() { - const el = document.getElementById('chat') - if (el == null) return - el.scrollTo(0, el.scrollHeight) - } - - async function showMessages(messages: ReadonlyArray) { - const el = document.getElementById('messages') - if (el == null) return - el.innerHTML = '' - for (const message of messages) { - const div = el.appendChild(document.createElement('div')) - div.className = 'message' - - const messageContent = document.createElement('span') - messageContent.textContent = message.content + ' ' + message.reactions.map((it) => it.reaction).join(' ') - // + ' ' + messages.created.getTime() - div.appendChild(messageContent) - - const buttonsDiv = document.createElement('div') - buttonsDiv.className = 'buttons' - - const editButton = document.createElement('button') - editButton.textContent = 'Edit' - editButton.className = 'edit-button' - editButton.addEventListener('click', () => editMessage(message)) - buttonsDiv.appendChild(editButton) - - const deleteButton = document.createElement('button') - deleteButton.textContent = 'Remove' - deleteButton.className = 'delete-button' - deleteButton.addEventListener('click', () => deleteMessage(message)) - buttonsDiv.appendChild(deleteButton) - - const addReactionButton = document.createElement('button') - addReactionButton.textContent = '+R' - addReactionButton.className = 'add-reaction-button' - addReactionButton.addEventListener('click', () => addReaction(message)) - buttonsDiv.appendChild(addReactionButton) - - const removeReactionButton = document.createElement('button') - removeReactionButton.textContent = '-R' - removeReactionButton.className = 'remove-reaction-button' - removeReactionButton.addEventListener('click', () => removeReaction(message)) - buttonsDiv.appendChild(removeReactionButton) - - div.appendChild(buttonsDiv) - } - scrollToBottom() - } - - document.getElementById('form')?.addEventListener('submit', async (event) => { - event.preventDefault() - // @ts-expect-error error - const el = event.target?.getElementsByTagName('input')[0] as HTMLInputElement - if (el.value == '' || el.value == null) return - - await client.createMessage(card, el.value, creator1) - - el.value = '' - }) -} - -void example() diff --git a/packages/query/package.json b/packages/query/package.json index d2f5783f9c..0afa492501 100644 --- a/packages/query/package.json +++ b/packages/query/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/communication-query", - "version": "0.1.61", + "version": "0.1.147", "main": "dist/index.js", "module": "dist/index.js", "types": "./types/index.d.ts", @@ -21,6 +21,7 @@ "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-shared": "workspace:*", "@hcengineering/communication-types": "workspace:*", + "@hcengineering/communication-yaml": "workspace:*", "fast-equals": "^5.0.1" }, "peerDependencies": { diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index 45ba576822..d5362634f0 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -13,4 +13,5 @@ // limitations under the License. // -export * from './lq.ts' +export * from './lq' +export type { QueryClient } from './types' diff --git a/packages/query/src/lq.ts b/packages/query/src/lq.ts index 5ace34515d..e1b333a1cd 100644 --- a/packages/query/src/lq.ts +++ b/packages/query/src/lq.ts @@ -15,26 +15,33 @@ import { type FindMessagesParams, + type FindNotificationContextParams, type FindNotificationsParams, - type WorkspaceID + type Message, + type WorkspaceID, + type Notification, + type NotificationContext } from '@hcengineering/communication-types' import { deepEqual } from 'fast-equals' import type { MessagesQueryCallback, NotificationsQueryCallback, ResponseEvent, - QueryClient + QueryCallback, + RequestEvent, + EventResult } from '@hcengineering/communication-sdk-types' -import type { FindParams, PagedQuery, QueryId } from './types' +import type { PagedQuery, FindParams, QueryId, QueryClient } from './types' import { MessagesQuery } from './messages/query' import { NotificationQuery } from './notifications/query' +import { NotificationContextsQuery } from './notification-contexts/query' interface CreateQueryResult { unsubscribe: () => void } -const maxQueriesCache = 10 +const maxQueriesCache = 20 export class LiveQueries { private readonly queries = new Map() @@ -45,27 +52,61 @@ export class LiveQueries { private readonly client: QueryClient, private readonly workspace: WorkspaceID, private readonly filesUrl: string - ) {} + ) { + this.client.onEvent = (event) => { + void this.onEvent(event) + } + this.client.onRequest = (event, promise) => { + void this.onRequest(event, promise) + } + } async onEvent(event: ResponseEvent): Promise { for (const q of this.queries.values()) { - await q.onEvent(event) + void q.onEvent(event) + } + } + + async onRequest(event: RequestEvent, promise: Promise): Promise { + for (const q of this.queries.values()) { + void q.onRequest(event, promise) } } queryMessages(params: FindMessagesParams, callback: MessagesQueryCallback): CreateQueryResult { - const query = this.createMessagesQuery(params, callback) - this.queries.set(query.id, query) - - return { - unsubscribe: () => { - this.unsubscribeQuery(query) - } - } + return this.createAndStoreQuery( + params, + callback, + MessagesQuery, + (params) => this.findMessagesQuery(params) + ) } queryNotifications(params: FindNotificationsParams, callback: NotificationsQueryCallback): CreateQueryResult { - const query = this.createNotificationQuery(params, callback) + return this.createAndStoreQuery( + params, + callback, + NotificationQuery, + (params) => this.findNotificationQuery(params) + ) + } + + queryNotificationContexts(params: FindNotificationContextParams, callback: any): CreateQueryResult { + return this.createAndStoreQuery( + params, + callback, + NotificationContextsQuery, + (params) => this.findNotificationContextsQuery(params) + ) + } + + private createAndStoreQuery>( + params: P, + callback: QueryCallback, + QueryClass: new (...args: any[]) => Q, + finder: (params: P) => Q | undefined + ): CreateQueryResult { + const query = this.createQuery(params, callback, QueryClass, finder) this.queries.set(query.id, query) return { @@ -75,68 +116,51 @@ export class LiveQueries { } } - private createMessagesQuery(params: FindMessagesParams, callback: MessagesQueryCallback): MessagesQuery { + private createQuery>( + params: P, + callback: any, + QueryClass: new (...args: any[]) => Q, + finder: (params: P) => Q | undefined + ): Q { const id = ++this.counter - const exists = this.findMessagesQuery(params) + const exists = finder(params) if (exists !== undefined) { - if (this.unsubscribed.has(id)) { - this.unsubscribed.delete(id) + if (this.unsubscribed.has(exists.id)) { + this.unsubscribed.delete(exists.id) exists.setCallback(callback) return exists } else { const result = exists.copyResult() - return new MessagesQuery(this.client, this.workspace, this.filesUrl, id, params, callback, result) + return new QueryClass(this.client, this.workspace, this.filesUrl, id, params, callback, result) } } - return new MessagesQuery(this.client, this.workspace, this.filesUrl, id, params, callback) + return new QueryClass(this.client, this.workspace, this.filesUrl, id, params, callback) } - private createNotificationQuery( - params: FindNotificationsParams, - callback: NotificationsQueryCallback - ): NotificationQuery { - const id = ++this.counter - const exists = this.findNotificationQuery(params) - - if (exists !== undefined) { - if (this.unsubscribed.has(id)) { - this.unsubscribed.delete(id) - exists.setCallback(callback) - return exists - } else { - const result = exists.copyResult() - return new NotificationQuery(this.client, id, params, callback, result) + private findQuery(params: FindParams, QueryClass: new (...args: any[]) => T): T | undefined { + for (const query of this.queries.values()) { + if (query instanceof QueryClass && this.queryCompare(params, query.params)) { + return query as T } } - - return new NotificationQuery(this.client, id, params, callback) } private findMessagesQuery(params: FindMessagesParams): MessagesQuery | undefined { - for (const query of this.queries.values()) { - if (query instanceof MessagesQuery) { - if (!this.queryCompare(params, query.params)) continue - return query - } - } + return this.findQuery(params, MessagesQuery) } private findNotificationQuery(params: FindNotificationsParams): NotificationQuery | undefined { - for (const query of this.queries.values()) { - if (query instanceof NotificationQuery) { - if (!this.queryCompare(params, query.params)) continue - return query - } - } + return this.findQuery(params, NotificationQuery) + } + + private findNotificationContextsQuery(params: FindNotificationContextParams): NotificationContextsQuery | undefined { + return this.findQuery(params, NotificationContextsQuery) } private queryCompare(q1: FindParams, q2: FindParams): boolean { - if (Object.keys(q1).length !== Object.keys(q2).length) { - return false - } - return deepEqual(q1, q2) + return Object.keys(q1).length === Object.keys(q2).length && deepEqual(q1, q2) } private removeOldQueries(): void { @@ -166,5 +190,6 @@ export class LiveQueries { close(): void { this.queries.clear() + this.unsubscribed.clear() } } diff --git a/packages/query/src/messages/query.ts b/packages/query/src/messages/query.ts index 877fe3363e..8097f79a34 100644 --- a/packages/query/src/messages/query.ts +++ b/packages/query/src/messages/query.ts @@ -16,40 +16,48 @@ import { type FindMessagesParams, type Message, - type MessagesGroup, - type WorkspaceID, type MessageID, - SortingOrder, - type Patch, + type MessagesGroup, type ParsedFile, + type Patch, PatchType, - type CardID + type Reaction, + type SocialID, + SortingOrder, + type WorkspaceID, + type File, + type BlobID, + MessageType } from '@hcengineering/communication-types' import { - ResponseEventType, - type AttachmentCreatedEvent, - type AttachmentRemovedEvent, + type FileCreatedEvent, + type FileRemovedEvent, type MessageCreatedEvent, - type MessageRemovedEvent, + type MessagesRemovedEvent, type PatchCreatedEvent, type QueryCallback, - type QueryClient, type ReactionCreatedEvent, type ReactionRemovedEvent, + type CreateMessageEvent, + type RequestEvent, + RequestEventType, type ResponseEvent, - type ThreadCreatedEvent + ResponseEventType, + type ThreadCreatedEvent, + type EventResult, + type CreateMessageResult } from '@hcengineering/communication-sdk-types' -import { loadGroupFile, parseMessageId } from '@hcengineering/communication-shared' +import { applyPatch, applyPatches, generateMessageId, parseMessageId } from '@hcengineering/communication-shared' +import { loadGroupFile } from '@hcengineering/communication-yaml' import { QueryResult } from '../result' -import { defaultQueryParams, Direction, type QueryId, type PagedQuery } from '../types' +import { defaultQueryParams, Direction, type PagedQuery, type QueryId, type QueryClient } from '../types' import { WindowImpl } from '../window' -import { addReaction, addReply, removeReaction, removeReply } from './utils' const GROUPS_LIMIT = 20 export class MessagesQuery implements PagedQuery { - protected result: Promise> | QueryResult + private result: Promise> | QueryResult private messagesFromFiles: Message[] = [] @@ -70,8 +78,10 @@ export class MessagesQuery implements PagedQuery { hasGroups: true } + private tmpMessages: Map = new Map() + constructor ( - protected readonly client: QueryClient, + private readonly client: QueryClient, private readonly workspace: WorkspaceID, private readonly filesUrl: string, public readonly id: QueryId, @@ -107,22 +117,132 @@ export class MessagesQuery implements PagedQuery { this.result = new QueryResult([] as Message[], (x) => x.id) if (this.isInitLoadingForward()) { - this.result.setHead(true) + this.result.setHead(this.params.created == null) void this.requestLoadNextPage() } else { - this.result.setTail(true) + this.result.setTail(this.params.created == null) void this.requestLoadPrevPage() } } } - setCallback (callback: QueryCallback): void { - this.callback = callback - void this.notify() + async onEvent (event: ResponseEvent): Promise { + switch (event.type) { + case ResponseEventType.MessageCreated: { + await this.onMessageCreatedEvent(event) + break + } + case ResponseEventType.MessagesRemoved: { + await this.onMessagesRemovedEvent(event) + break + } + case ResponseEventType.PatchCreated: { + await this.onPatchCreatedEvent(event) + break + } + case ResponseEventType.ReactionCreated: { + await this.onReactionCreatedEvent(event) + break + } + case ResponseEventType.ReactionRemoved: { + await this.onReactionRemovedEvent(event) + break + } + case ResponseEventType.FileCreated: { + await this.onFileCreatedEvent(event) + break + } + case ResponseEventType.FileRemoved: { + await this.onFileRemovedEvent(event) + break + } + case ResponseEventType.ThreadCreated: { + await this.onThreadCreatedEvent(event) + } + } } - removeCallback (): void { - this.callback = () => {} + async onRequest (event: RequestEvent, promise: Promise): Promise { + switch (event.type) { + case RequestEventType.CreateMessage: { + await this.onCreateMessageRequest(event, promise as Promise) + break + } + } + } + + async onCreateMessageRequest( + event: CreateMessageEvent, + promise: Promise + ): Promise { + if (this.params.card !== event.card) return; + const eventId = event._id + if(eventId == null) return + + const tmpId = generateMessageId(); + let resultId: MessageID | undefined; + const tmpMessage: Message = { + id: tmpId, + type: MessageType.Message, + card: event.card, + content: event.content, + creator: event.creator, + created: new Date(), + data: event.data, + edited: undefined, + thread: undefined, + reactions: [], + files: [] + }; + + if (!this.match(tmpMessage)) return; + + promise + .then(async (result) => { + this.tmpMessages.delete(eventId) + resultId = result.id; + if (this.result instanceof Promise) this.result = await this.result + + if (this.result.get(resultId)) { + if (this.result.delete(tmpId)) { + await this.notify(); + } + } else { + const updatedMessage = { ...tmpMessage, id: resultId }; + this.result.delete(tmpId); + + this.insertMessage(this.result, updatedMessage); + + void this.notify(); + } + }) + .catch(async () => { + if (this.result instanceof Promise) this.result = await this.result + this.tmpMessages.delete(eventId) + if (this.result.delete(tmpId)) { + void this.notify(); + } + }); + + if (this.result instanceof Promise) this.result = await this.result; + + if (resultId === undefined && this.result.isTail()) { + this.tmpMessages.set(eventId, tmpId) + this.insertMessage(this.result, tmpMessage); + void this.notify(); + } + } + + private insertMessage(result: QueryResult, message: Message): void { + if (this.params.order === SortingOrder.Ascending) { + result.push(message); + } else { + result.unshift(message); + } + } + + async unsubscribe (): Promise { + await this.client.unsubscribeQuery(this.id) } async requestLoadNextPage (): Promise { @@ -152,22 +272,31 @@ export class MessagesQuery implements PagedQuery { } } - private isInitLoadingForward (): boolean { - const { order, created, id } = this.params + removeCallback (): void { + this.callback = () => {} + } - if (id != null) { - return false + setCallback (callback: QueryCallback): void { + this.callback = callback + void this.notify() + } + + copyResult (): QueryResult | undefined { + if (this.result instanceof Promise) { + return undefined } - if (created == null) return order === SortingOrder.Ascending - if (created instanceof Date) return order === SortingOrder.Ascending - // TODO: fix me - if (created.less != null) return order !== SortingOrder.Ascending - if (created.lessOrEqual != null) return order !== SortingOrder.Ascending - if (created.greater != null) return order === SortingOrder.Ascending - if (created.greaterOrEqual != null) return order === SortingOrder.Ascending + return this.result.copy() + } - return false + private isInitLoadingForward (): boolean { + const { order, id } = this.params + + if (id != null) { + return true + } + + return order === SortingOrder.Ascending } private async loadPage (direction: Direction, result: QueryResult): Promise> { @@ -190,7 +319,11 @@ export class MessagesQuery implements PagedQuery { } } - result.append(messages) + if (this.params.order === SortingOrder.Ascending && direction === Direction.Backward) { + result.prepend(messages.reverse()) + } else { + result.append(messages) + } return result } @@ -229,9 +362,9 @@ export class MessagesQuery implements PagedQuery { created: last != null ? { - greater: last?.created - } - : undefined, + greater: last.created + } + : this.params.created, limit, order: SortingOrder.Ascending }) @@ -271,16 +404,16 @@ export class MessagesQuery implements PagedQuery { private async findPrevMessages (limit: number, result: QueryResult): Promise { if (!this.prev.hasMessages || result.isHead()) return [] - const first = result.getLast() + const first = this.params.order === SortingOrder.Ascending ? result.getFirst() : result.getLast() return await this.find({ ...this.params, created: first != null ? { - less: first?.created - } - : undefined, + less: first?.created + } + : this.params.created, limit, order: SortingOrder.Descending }) @@ -302,7 +435,7 @@ export class MessagesQuery implements PagedQuery { ? currentGroups : await this.findGroups( direction, - direction === Direction.Forward ? this.lastGroup?.fromDate : this.firstGroup?.fromDate + direction === Direction.Forward ? this.lastGroup?.fromSec : this.firstGroup?.fromSec ) if (currentGroups.length === 0) { @@ -380,7 +513,6 @@ export class MessagesQuery implements PagedQuery { private async loadMessagesFromFiles (group: MessagesGroup): Promise { const parsedFile = await loadGroupFile(this.workspace, this.filesUrl, group, { retries: 5 }) - const patches = group.patches ?? [] const patchesMap = new Map() @@ -392,36 +524,24 @@ export class MessagesQuery implements PagedQuery { metadata: parsedFile.metadata, messages: patches.length > 0 - ? parsedFile.messages.map((message) => this.applyPatches(message, patchesMap.get(message.id) ?? [])) + ? parsedFile.messages.map((message) => applyPatches(message, patchesMap.get(message.id) ?? [], this.allowedPatches())) : parsedFile.messages } } - private async findGroupByMessage (id: MessageID): Promise { - const date = parseMessageId(id) - const group1 = (await this.client.findMessagesGroups({ - card: this.params.card, - limit: 1, - toDate: { - greaterOrEqual: date - }, - order: SortingOrder.Descending, - orderBy: 'fromDate' - }))[0] + private async findGroupByMessage(id: MessageID): Promise { + const date = parseMessageId(id); - if (group1 !== undefined) { - return group1 - } - - return (await this.client.findMessagesGroups({ + const groups = await this.client.findMessagesGroups({ card: this.params.card, + fromSec: { lessOrEqual: date }, + toSec: { greaterOrEqual: date }, limit: 1, - fromDate: { - lessOrEqual: date - }, order: SortingOrder.Ascending, - orderBy: 'fromDate' - }))[0] + orderBy: 'fromSec' + }); + + return groups[0]; } private async findGroups (direction: Direction, fromDate?: Date): Promise { @@ -435,7 +555,7 @@ export class MessagesQuery implements PagedQuery { card: this.params.card, limit: GROUPS_LIMIT, order: direction === Direction.Forward ? SortingOrder.Ascending : SortingOrder.Descending, - orderBy: 'fromDate' + orderBy: 'fromSec' }) } @@ -443,15 +563,15 @@ export class MessagesQuery implements PagedQuery { card: this.params.card, limit: GROUPS_LIMIT, order: direction === Direction.Forward ? SortingOrder.Ascending : SortingOrder.Descending, - orderBy: 'fromDate', - fromDate: + orderBy: 'fromSec', + fromSec: direction === Direction.Forward ? { - greater: fromDate - } + greater: fromDate + } : { - less: fromDate - } + less: fromDate + } }) } @@ -466,47 +586,18 @@ export class MessagesQuery implements PagedQuery { this.callback(new WindowImpl(result, this.result.isTail(), this.result.isHead(), this)) } - async unsubscribe (): Promise { - await this.client.unsubscribeQuery(this.id) - } - - async onEvent (event: ResponseEvent): Promise { - switch (event.type) { - case ResponseEventType.MessageCreated: { - await this.onCreateMessageEvent(event) - return - } - case ResponseEventType.MessageRemoved: { - await this.onRemoveMessageEvent(event) - return - } - case ResponseEventType.PatchCreated: { - await this.onCreatePatchEvent(event) - return - } - case ResponseEventType.ReactionCreated: { - await this.onCreateReactionEvent(event) - return - } - case ResponseEventType.ReactionRemoved: { - await this.onRemoveReactionEvent(event) - return - } - case ResponseEventType.AttachmentCreated: { - await this.onCreateAttachmentEvent(event) - return - } - case ResponseEventType.AttachmentRemoved: { - await this.onRemoveAttachmentEvent(event) - return - } - case ResponseEventType.ThreadCreated: { - await this.onCreateThreadEvent(event) - } + private match (message: Message): boolean { + if (this.params.id != null && this.params.id !== message.id) { + return false } + if (this.params.card !== message.card) { + return false + } + return true } - async onCreateThreadEvent (event: ThreadCreatedEvent): Promise { + private async onThreadCreatedEvent (event: ThreadCreatedEvent): Promise { + if(this.params.replies !== true) return if (this.params.card !== event.thread.card) return if (this.result instanceof Promise) this.result = await this.result @@ -532,7 +623,7 @@ export class MessagesQuery implements PagedQuery { }) } - async onCreateMessageEvent (event: MessageCreatedEvent): Promise { + private async onMessageCreatedEvent (event: MessageCreatedEvent): Promise { if (this.result instanceof Promise) this.result = await this.result if (this.params.card !== event.message.card) return const { message } = event @@ -542,6 +633,12 @@ export class MessagesQuery implements PagedQuery { if (!this.match(message)) return if (this.result.isTail()) { + const eventId = event._id + if(eventId != null) { + const tmp = this.tmpMessages.get(eventId) + if (tmp) this.result.delete(tmp) + this.tmpMessages.delete(eventId) + } if (this.params.order === SortingOrder.Ascending) { this.result.push(message) } else { @@ -551,51 +648,50 @@ export class MessagesQuery implements PagedQuery { } } - private match (message: Message): boolean { - if (this.params.id != null && this.params.id !== message.id) { - return false - } - if (this.params.card !== message.card) { - return false - } - return true - } - - private async onCreatePatchEvent (event: PatchCreatedEvent): Promise { + private async onPatchCreatedEvent (event: PatchCreatedEvent): Promise { if (this.params.card !== event.card) return + if (!this.isAllowedPatch(event.patch.type)) return if (this.result instanceof Promise) this.result = await this.result const { patch } = event - const messageId = BigInt(patch.message) - const group = this.groupsBuffer.find((it) => BigInt(it.fromId) <= messageId && BigInt(it.toId) >= messageId) + const created = parseMessageId(patch.message) + const groups = this.groupsBuffer.filter((it) => it.fromSec <= created && it.toSec >= created) - if (group != null && group.patches != null) { - group.patches.push(patch) + for (const group of groups) { + if (group.patches != null) { + group.patches.push(patch) + } } const message = this.result.get(patch.message) if (message === undefined) return if (message.created < patch.created) { - this.result.update(this.applyPatch(message, patch)) + this.result.update(applyPatch(message, patch, this.allowedPatches())) await this.notify() } } - private async onRemoveMessageEvent (event: MessageRemovedEvent): Promise { + private async onMessagesRemovedEvent (event: MessagesRemovedEvent): Promise { if (this.params.card !== event.card) return if (this.result instanceof Promise) this.result = await this.result - const deleted = this.result.delete(event.message) + let isDeleted = false - if (deleted !== undefined) { + for (const message of event.messages) { + const deleted = this.result.delete(message) + isDeleted = isDeleted || deleted !== undefined + } + + if (isDeleted) { void this.notify() } - this.messagesFromFiles = this.messagesFromFiles.filter((it) => it.id !== event.message) + this.messagesFromFiles = this.messagesFromFiles.filter((it) => !event.messages.includes(it.id)) } - private async onCreateReactionEvent (event: ReactionCreatedEvent): Promise { + private async onReactionCreatedEvent (event: ReactionCreatedEvent): Promise { + if(this.params.reactions !== true) return if (this.result instanceof Promise) this.result = await this.result if (this.params.card !== event.card) return @@ -616,7 +712,8 @@ export class MessagesQuery implements PagedQuery { } } - private async onRemoveReactionEvent (event: ReactionRemovedEvent): Promise { + private async onReactionRemovedEvent (event: ReactionRemovedEvent): Promise { + 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 { ) } - private async onCreateAttachmentEvent (event: AttachmentCreatedEvent): Promise { + private async onFileCreatedEvent (event: FileCreatedEvent): Promise { + 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 { + private async onFileRemovedEvent (event: FileRemovedEvent): Promise { + 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 | 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 } } diff --git a/packages/query/src/messages/utils.ts b/packages/query/src/messages/utils.ts deleted file mode 100644 index 41a9ce4c7e..0000000000 --- a/packages/query/src/messages/utils.ts +++ /dev/null @@ -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 - } - } -} diff --git a/packages/query/src/notification-contexts/query.ts b/packages/query/src/notification-contexts/query.ts new file mode 100644 index 0000000000..a8dd93fb27 --- /dev/null +++ b/packages/query/src/notification-contexts/query.ts @@ -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 { + private result: QueryResult | Promise> + private forward: Promise | NotificationContext[] = [] + private backward: Promise | 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, + initialResult?: QueryResult + ) { + 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 { + 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 {} + + async unsubscribe (): Promise { + await this.client.unsubscribeQuery(this.id) + } + + async requestLoadNextPage (): Promise { + 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 { + 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): void { + this.callback = callback + void this.notify() + } + + copyResult (): QueryResult | undefined { + if (this.result instanceof Promise) { + return undefined + } + + return this.result.copy() + } + + private async find (params: FindNotificationContextParams): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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) + } +} diff --git a/packages/query/src/notifications/query.ts b/packages/query/src/notifications/query.ts index 33a2952537..128ef28c04 100644 --- a/packages/query/src/notifications/query.ts +++ b/packages/query/src/notifications/query.ts @@ -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 { - override async find (params: FindNotificationsParams): Promise { - return await this.client.findNotifications(params, this.id) +export class NotificationQuery implements PagedQuery { + private result: QueryResult | Promise> + + constructor ( + private readonly client: QueryClient, + private readonly workspace: WorkspaceID, + private readonly filesUrl: string, + public readonly id: QueryId, + public readonly params: FindNotificationsParams, + private callback?: QueryCallback, + initialResult?: QueryResult + ) { + 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> { + 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 { + async onEvent (event: ResponseEvent): Promise { 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 { - if (this.result instanceof Promise) { - this.result = await this.result + async onRequest (event: RequestEvent): Promise {} + + async unsubscribe (): Promise { + await this.client.unsubscribeQuery(this.id) + } + + async requestLoadNextPage (): Promise { + if (this.result instanceof Promise) this.result = await this.result + + await this.loadPage(SortingOrder.Ascending, this.result.getLast()?.created) + } + + async requestLoadPrevPage (): Promise { + 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 { + 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): void { + this.callback = callback + void this.notify() + } + + copyResult (): QueryResult | undefined { + return this.result instanceof Promise ? undefined : this.result.copy() + } + + private async find (params: FindNotificationsParams): Promise { + 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 { + 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 { - 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 { - if (this.result instanceof Promise) { - this.result = await this.result + private async onRemoveNotificationsEvent (event: NotificationsRemovedEvent): Promise { + 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 { - 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 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 { + 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 { + 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 + }) + } } diff --git a/packages/query/src/query.ts b/packages/query/src/query.ts deleted file mode 100644 index 084f2af846..0000000000 --- a/packages/query/src/query.ts +++ /dev/null @@ -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 implements PagedQuery { - protected result: QueryResult | Promise> - private forward: Promise | T[] = [] - private backward: Promise | T[] = [] - - constructor( - protected readonly client: QueryClient, - public readonly id: QueryId, - public readonly params: P, - private callback?: QueryCallback, - initialResult?: QueryResult - ) { - 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 { - /* 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 { - /* Implement in subclass */ - } - - setCallback(callback: QueryCallback): void { - this.callback = callback - void this.notify() - } - - removeCallback(): void { - this.callback = () => {} - } - - protected async notify(): Promise { - 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 { - 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 { - 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 | undefined { - if (this.result instanceof Promise) { - return undefined - } - - return this.result.copy() - } - - async unsubscribe(): Promise { - await this.client.unsubscribeQuery(this.id) - } -} diff --git a/packages/query/src/result.ts b/packages/query/src/result.ts index 31a45ccb81..a896cfab3a 100644 --- a/packages/query/src/result.ts +++ b/packages/query/src/result.ts @@ -74,6 +74,14 @@ export class QueryResult { 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 { } } + 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 { const copy = new QueryResult(Array.from(this.objectById.values()), this.getId) diff --git a/packages/query/src/types.ts b/packages/query/src/types.ts index 95d32a1763..ca8d8dc246 100644 --- a/packages/query/src/types.ts +++ b/packages/query/src/types.ts @@ -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 { readonly params: P onEvent: (event: ResponseEvent) => Promise + onRequest: (event: RequestEvent, promise: Promise) => Promise + + unsubscribe: () => Promise requestLoadNextPage: () => Promise requestLoadPrevPage: () => Promise - unsubscribe: () => Promise - - setCallback: (callback: (window: Window) => void) => void removeCallback: () => void + setCallback: (callback: (window: Window) => void) => void copyResult: () => QueryResult | undefined } + +export interface QueryClient { + onEvent(event: ResponseEvent): void + + onRequest(event: RequestEvent, promise: Promise): void + + findMessages(params: FindMessagesParams, queryId?: number): Promise + + findMessagesGroups(params: FindMessagesGroupsParams): Promise + + findNotificationContexts(params: FindNotificationContextParams, queryId?: number): Promise + + findNotifications(params: FindNotificationsParams, queryId?: number): Promise + + unsubscribeQuery(id: number): Promise +} diff --git a/packages/query/src/utils.ts b/packages/query/src/utils.ts new file mode 100644 index 0000000000..42c5910dcb --- /dev/null +++ b/packages/query/src/utils.ts @@ -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 { + 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) +} diff --git a/packages/rest-client/package.json b/packages/rest-client/package.json new file mode 100644 index 0000000000..0ece05e977 --- /dev/null +++ b/packages/rest-client/package.json @@ -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" + } +} diff --git a/packages/client-sqlite/src/index.ts b/packages/rest-client/src/index.ts similarity index 86% rename from packages/client-sqlite/src/index.ts rename to packages/rest-client/src/index.ts index 9ad2a8e4ba..d371f8c07e 100644 --- a/packages/client-sqlite/src/index.ts +++ b/packages/rest-client/src/index.ts @@ -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' diff --git a/packages/rest-client/src/rest.ts b/packages/rest-client/src/rest.ts new file mode 100644 index 0000000000..7d6e450d39 --- /dev/null +++ b/packages/rest-client/src/rest.ts @@ -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 { + 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 { + 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 { + 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(response) + }, + { retries } + ) + } + + async findMessagesGroups(params: FindMessagesGroupsParams): Promise { + 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(response) + }, + { retries } + ) + } + + async findNotificationContexts(params: FindNotificationContextParams): Promise { + 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(response) + }, + { retries } + ) + } + + async findNotifications(params: FindNotificationsParams): Promise { + 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(response) + }, + { retries } + ) + } +} diff --git a/packages/rest-client/src/types.ts b/packages/rest-client/src/types.ts new file mode 100644 index 0000000000..c16cfe12e1 --- /dev/null +++ b/packages/rest-client/src/types.ts @@ -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 + findMessagesGroups: (params: FindMessagesGroupsParams) => Promise + findNotificationContexts: (params: FindNotificationContextParams) => Promise + findNotifications: (params: FindNotificationsParams) => Promise + + event: (event: RequestEvent) => Promise +} diff --git a/packages/rest-client/src/utils.ts b/packages/rest-client/src/utils.ts new file mode 100644 index 0000000000..02c1f39168 --- /dev/null +++ b/packages/rest-client/src/utils.ts @@ -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(response: Response): Promise { + 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 +} diff --git a/packages/client-ws/tsconfig.json b/packages/rest-client/tsconfig.json similarity index 100% rename from packages/client-ws/tsconfig.json rename to packages/rest-client/tsconfig.json diff --git a/packages/sdk-types/package.json b/packages/sdk-types/package.json index df292a920b..e43b1fd963 100644 --- a/packages/sdk-types/package.json +++ b/packages/sdk-types/package.json @@ -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" diff --git a/packages/sdk-types/src/client.ts b/packages/sdk-types/src/client.ts index 75de33b38c..a5b1536afa 100644 --- a/packages/sdk-types/src/client.ts +++ b/packages/sdk-types/src/client.ts @@ -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 removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise - createAttachment(card: CardID, message: MessageID, attachment: CardID, creator: SocialID): Promise - removeAttachment(card: CardID, message: MessageID, attachment: CardID): Promise + createFile( + card: CardID, + message: MessageID, + blobId: BlobID, + fileType: string, + filename: string, + creator: SocialID + ): Promise + removeFile(card: CardID, message: MessageID, blobId: BlobID): Promise createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise createNotification(message: MessageID, context: ContextID): Promise removeNotification(message: MessageID, context: ContextID): Promise - createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise + createNotificationContext(card: CardID, lastView: Date, lastUpdate: Date): Promise removeNotificationContext(context: ContextID): Promise - updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise + updateNotificationContext(context: ContextID, lastView?: Date, lastUpdate?: Date): Promise onEvent(event: ResponseEvent): void diff --git a/packages/sdk-types/src/db.ts b/packages/sdk-types/src/db.ts index a19e0d0ec1..10fd390b37 100644 --- a/packages/sdk-types/src/db.ts +++ b/packages/sdk-types/src/db.ts @@ -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 - removeMessage(card: CardID, id: MessageID, socialIds?: SocialID[]): Promise - removeMessages(card: CardID, fromId: MessageID, toId: MessageID): Promise + createMessage( + card: CardID, + type: MessageType, + content: RichText, + creator: SocialID, + created: Date, + data?: MessageData + ): Promise + removeMessages(card: CardID, ids: MessageID[], socialIds?: SocialID[]): Promise createPatch( card: CardID, @@ -47,49 +57,47 @@ export interface DbAdapter { creator: SocialID, created: Date ): Promise - removePatches(card: CardID, fromId: MessageID, toId: MessageID): Promise - createMessagesGroup( - card: CardID, - blobId: BlobID, - fromDate: Date, - toDate: Date, - fromID: MessageID, - toID: MessageID, - count: number - ): Promise + createMessagesGroup(card: CardID, blobId: BlobID, fromSec: Date, toSec: Date, count: number): Promise removeMessagesGroup(card: CardID, blobId: BlobID): Promise createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID, created: Date): Promise removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise - createAttachment(message: MessageID, attachment: CardID, creator: SocialID, created: Date): Promise - removeAttachment(message: MessageID, attachment: CardID): Promise + createFile( + card: CardID, + message: MessageID, + blobId: BlobID, + fileType: string, + filename: string, + size: number, + creator: SocialID, + created: Date + ): Promise + removeFile(card: CardID, message: MessageID, blobId: BlobID): Promise createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise - updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise + updateThread(thread: CardID, op: 'increment' | 'decrement', lastReply?: Date): Promise findMessages(params: FindMessagesParams): Promise findMessagesGroups(params: FindMessagesGroupsParams): Promise findThread(thread: CardID): Promise - createNotification(message: MessageID, context: ContextID): Promise - removeNotification(message: MessageID, context: ContextID): Promise + addCollaborators(card: CardID, collaborators: AccountID[], date?: Date): Promise + removeCollaborators(card: CardID, collaborators: AccountID[]): Promise + getCollaboratorsCursor(card: CardID, date: Date, size?: number): AsyncIterable - createContext(personalWorkspace: WorkspaceID, card: CardID, lastView?: Date, lastUpdate?: Date): Promise - updateContext(context: ContextID, update: NotificationContextUpdate): Promise - removeContext(context: ContextID): Promise + findCollaborators(params: FindCollaboratorsParams): Promise - findContexts( - params: FindNotificationContextParams, - personalWorkspaces: WorkspaceID[], - workspace?: WorkspaceID - ): Promise - findNotifications( - params: FindNotificationsParams, - personalWorkspace: WorkspaceID, - workspace?: WorkspaceID - ): Promise + createNotification(context: ContextID, message: MessageID, created: Date): Promise + removeNotification(context: ContextID, account: AccountID, untilDate: Date): Promise + + createContext(account: AccountID, card: CardID, lastUpdate: Date, lastView: Date): Promise + updateContext(context: ContextID, account: AccountID, lastUpdate?: Date, lastView?: Date): Promise + removeContext(context: ContextID, account: AccountID): Promise + + findContexts(params: FindNotificationContextParams): Promise + findNotifications(params: FindNotificationsParams): Promise close(): void } diff --git a/packages/sdk-types/src/query.ts b/packages/sdk-types/src/query.ts index 60c479a024..1d8c11ec1c 100644 --- a/packages/sdk-types/src/query.ts +++ b/packages/sdk-types/src/query.ts @@ -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 = (window: Window) => void export type MessagesQueryCallback = QueryCallback diff --git a/packages/sdk-types/src/requestEvent.ts b/packages/sdk-types/src/requestEvent.ts index 9b8b9cc03b..4642c8a24b 100644 --- a/packages/sdk-types/src/requestEvent.ts +++ b/packages/sdk-types/src/requestEvent.ts @@ -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 { diff --git a/packages/sdk-types/src/responseEvent.ts b/packages/sdk-types/src/responseEvent.ts index 32ebcf0392..1fdf3a5f4a 100644 --- a/packages/sdk-types/src/responseEvent.ts +++ b/packages/sdk-types/src/responseEvent.ts @@ -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 } diff --git a/packages/sdk-types/src/serverApi.ts b/packages/sdk-types/src/serverApi.ts index a62d38c3bd..05425d649f 100644 --- a/packages/sdk-types/src/serverApi.ts +++ b/packages/sdk-types/src/serverApi.ts @@ -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 + findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: number | string): Promise findMessagesGroups(info: ConnectionInfo, params: FindMessagesGroupsParams): Promise + findNotificationContexts( + info: ConnectionInfo, + params: FindNotificationContextParams, + queryId?: number | string + ): Promise + findNotifications( + info: ConnectionInfo, + params: FindNotificationsParams, + queryId?: number | string + ): Promise event(info: ConnectionInfo, event: RequestEvent): Promise diff --git a/packages/server/package.json b/packages/server/package.json index d7196acfc9..684fb03b52 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -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" diff --git a/packages/server/src/eventProcessor.ts b/packages/server/src/eventProcessor.ts index 13ed0e7149..681e646b6c 100644 --- a/packages/server/src/eventProcessor.ts +++ b/packages/server/src/eventProcessor.ts @@ -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 { 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 { - this.checkCreator(info.account, event.creator) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private async updateThread(event: UpdateThreadEvent, _: ConnectionInfo): Promise { + 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 { + 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 { + 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 { 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 { - this.checkCreator(info.account, event.creator) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private async createPatch(event: CreatePatchEvent, _: ConnectionInfo): Promise { 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 { - 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 { - 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 { - 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 { - this.checkCreator(info.account, event.creator) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private async createReaction(event: CreateReactionEvent, _: ConnectionInfo): Promise { 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 { - this.checkCreator(info.account, event.creator) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private async removeReaction(event: RemoveReactionEvent, _: ConnectionInfo): Promise { 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 { - 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 { - await this.db.removeAttachment(event.message, event.card) - const responseEvent: AttachmentRemovedEvent = { - type: ResponseEventType.AttachmentRemoved, + private async createFile(event: CreateFileEvent, _: ConnectionInfo): Promise { + 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 { + 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 { - 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 { - await this.db.removeNotification(event.message, event.context) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private async removeNotifications(event: RemoveNotificationsEvent, _: ConnectionInfo): Promise { + 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 { - // 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 { + 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 { - 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 { - await this.db.updateContext(event.context, event.update) + async updateNotificationContext(event: UpdateNotificationContextEvent, _: ConnectionInfo): Promise { + 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 { - 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 { + 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 { - if (systemAccountUuid !== info.account.uuid) { - throw new Error('Forbidden') - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async removeMessagesGroup(event: RemoveMessagesGroupEvent, _: ConnectionInfo): Promise { 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 } } } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 183580da18..95fa81eb95 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -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 { - 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 { + async findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: QueryId): Promise { 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 { + 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 { + 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 { this.manager.unsubscribeQuery(info, id) } diff --git a/packages/server/src/manager.ts b/packages/server/src/manager.ts index d1248f72ee..b62fb2df74 100644 --- a/packages/server/src/manager.ts +++ b/packages/server/src/manager.ts @@ -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 - notificationQueries: Map - contextQueries: Map + contextQueries: Map> } export class Manager { private readonly dataBySessionId = new Map() 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 { + async findMessages(info: ConnectionInfo, params: FindMessagesParams, queryId?: QueryId): Promise { + 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 { + async findMessagesGroups(info: ConnectionInfo, params: FindMessagesGroupsParams): Promise { + this.createSession(info) return await this.db.findMessagesGroups(params) } + async findNotificationContexts( + info: ConnectionInfo, + params: FindNotificationContextParams, + queryId?: QueryId + ): Promise { + 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 { + this.createSession(info) + return await this.db.findNotifications(params) + } + async event(info: ConnectionInfo, event: RequestEvent): Promise { + 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): 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): 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 { - 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 { + 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 { + private async execute(info: ConnectionInfo, request: RequestEvent): Promise { + 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 { + 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 { 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 + ): 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() - } } diff --git a/packages/server/src/metadata.ts b/packages/server/src/metadata.ts index bb261412e2..7a4a0f3fe2 100644 --- a/packages/server/src/metadata.ts +++ b/packages/server/src/metadata.ts @@ -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 ?? '' } } diff --git a/packages/server/src/notification/notification.ts b/packages/server/src/notification/notification.ts new file mode 100644 index 0000000000..8a8b5e3fa3 --- /dev/null +++ b/packages/server/src/notification/notification.ts @@ -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 { + switch (event.type) { + case ResponseEventType.MessageCreated: { + return await notifyMessage(ctx, event.message) + } + } + + return [] +} + +async function notifyMessage (ctx: TriggerCtx, message: Message): Promise { + 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 { + 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 { + 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 +} diff --git a/packages/server/src/permissions.ts b/packages/server/src/permissions.ts new file mode 100644 index 0000000000..3a27ccce8c --- /dev/null +++ b/packages/server/src/permissions.ts @@ -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') + } + } +} diff --git a/packages/server/src/triggers.ts b/packages/server/src/triggers.ts index c82947a272..d944ba2a70 100644 --- a/packages/server/src/triggers.ts +++ b/packages/server/src/triggers.ts @@ -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 = new Set() + private readonly registeredCards = new Set() constructor( + private readonly ctx: MeasureContext, private readonly metadata: Metadata, private readonly db: DbAdapter, private readonly workspace: WorkspaceID ) {} - async process(event: ResponseEvent, info: ConnectionInfo): Promise { - 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, + execute: (event: RequestEvent) => Promise + ): Promise { + 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 { + private async applySyncTriggers( + ctx: TriggerCtx, + event: ResponseEvent, + apply: (events: RequestEvent[]) => Promise + ): Promise { + 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 { + return await notify(ctx, event) + } + + private async onMessagesGroupCreated(_: TriggerCtx, event: MessagesGroupCreatedEvent): Promise { this.registeredCards.delete(event.group.card) return [] } - async onMessageRemoved(event: MessageRemovedEvent, info: ConnectionInfo): Promise { + private async onMessagesRemoved(ctx: TriggerCtx, event: MessagesRemovedEvent): Promise { 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 { - return (await Promise.all([this.registerCard(event.message.card), this.updateThread(event)])).flat() + private async onMessageCreated(ctx: TriggerCtx, event: MessageCreatedEvent): Promise { + 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 { - return this.registerCard(event.card) + private async addCollaborators(ctx: TriggerCtx, event: MessageCreatedEvent): Promise { + 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 { + private async onPatchCreated(_: TriggerCtx, event: PatchCreatedEvent): Promise { + void this.registerCard(event.card) + return [] + } + + private async addThreadReply(event: MessageCreatedEvent): Promise { 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 { - if (this.registeredCards.has(card) || this.metadata.msg2fileUrl === '') return [] + private async onFileCreated(_: TriggerCtx, event: FileCreatedEvent): Promise { + 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 = { + 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 { + 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 { + 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 [] } } diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts new file mode 100644 index 0000000000..e191c9220c --- /dev/null +++ b/packages/server/src/types.ts @@ -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 +} + +export type QueryId = string | number diff --git a/packages/server/src/utils.ts b/packages/server/src/utils.ts new file mode 100644 index 0000000000..def6f26140 --- /dev/null +++ b/packages/server/src/utils.ts @@ -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 { + 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 { + const body = { + method: 'findPersonBySocialId' as const, + params: { socialId, requireAccount: true } + } + const request: RequestInit = { + keepalive: true, + headers: { + ...(token === undefined + ? {} + : { + Authorization: 'Bearer ' + token + }) + } + } + + const response = await fetch(url, { + ...request, + headers: { + ...request.headers, + 'Content-Type': 'application/json' + }, + method: 'POST', + body: JSON.stringify(body) + }) + + const result = await response.json() + if (result.error != null) { + throw Error(result.error) + } + + return result.result as AccountID | undefined +} diff --git a/packages/shared/package.json b/packages/shared/package.json index 7f96a8d7c3..6ec1fa97bc 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -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", diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index bd41801a46..f9d4d66a36 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -15,4 +15,4 @@ export * from './message.ts' export * from './retry.ts' -export * from './files.ts' +export * from './patch.ts' diff --git a/packages/shared/src/message.ts b/packages/shared/src/message.ts index 0dac007bc2..8e5b28522c 100644 --- a/packages/shared/src/message.ts +++ b/packages/shared/src/message.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) } diff --git a/packages/shared/src/patch.ts b/packages/shared/src/patch.ts new file mode 100644 index 0000000000..6c1897714b --- /dev/null +++ b/packages/shared/src/patch.ts @@ -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 + +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 + } + } +} diff --git a/packages/sqlite-wasm/package.json b/packages/sqlite-wasm/package.json deleted file mode 100644 index d938150b23..0000000000 --- a/packages/sqlite-wasm/package.json +++ /dev/null @@ -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" - } -} diff --git a/packages/sqlite-wasm/src/adapter.ts b/packages/sqlite-wasm/src/adapter.ts deleted file mode 100644 index 628a435443..0000000000 --- a/packages/sqlite-wasm/src/adapter.ts +++ /dev/null @@ -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 { - 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 { - //TODO: FIXME - return await this.message.createPatch(message, content, creator, created) - } - - async removeMessage(workspace: string, card: CardID, id: MessageID): Promise { - await this.message.removeMessage(id) - return id - } - - async removeMessages(workspace: string, card: CardID, ids: MessageID[]): Promise { - //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 { - //TODO: implement - } - - async findMessagesGroups(workspace: string, params: FindMessagesGroupsParams): Promise { - //TODO: implement - return [] - } - - async createReaction( - workspace: string, - card: CardID, - message: MessageID, - reaction: string, - creator: SocialID, - created: Date - ): Promise { - //TODO: FIXME - return await this.message.createReaction(message, reaction, creator, created) - } - - async removeReaction( - workspace: string, - card: CardID, - message: MessageID, - reaction: string, - creator: SocialID - ): Promise { - //TODO: FIXME - return await this.message.removeReaction(message, reaction, creator) - } - - async createAttachment(message: MessageID, attachment: CardID, creator: SocialID, created: Date): Promise { - return await this.message.createAttachment(message, attachment, creator, created) - } - - async removeAttachment(message: MessageID, attachment: CardID): Promise { - return await this.message.removeAttachment(message, attachment) - } - - async findMessages(workspace: string, params: FindMessagesParams): Promise { - return await this.message.find(workspace, params) - } - - async createNotification(message: MessageID, context: ContextID): Promise { - return await this.notification.createNotification(message, context) - } - async removeNotification(message: MessageID, context: ContextID): Promise { - return await this.notification.removeNotification(message, context) - } - - async createContext( - workspace: string, - card: CardID, - personalWorkspace: string, - lastView?: Date, - lastUpdate?: Date - ): Promise { - return await this.notification.createContext(workspace, card, personalWorkspace, lastView, lastUpdate) - } - - async removeContext(context: ContextID): Promise { - return await this.notification.removeContext(context) - } - - async updateContext(context: ContextID, update: NotificationContextUpdate): Promise { - return await this.notification.updateContext(context, update) - } - - async findContexts( - params: FindNotificationContextParams, - personalWorkspaces: string[], - workspace?: string - ): Promise { - return await this.notification.findContexts(params, personalWorkspaces, workspace) - } - - async findNotifications( - params: FindNotificationsParams, - personalWorkspace: string, - workspace?: string - ): Promise { - return await this.notification.findNotifications(params, personalWorkspace, workspace) - } - - close(): void { - void this.worker('close') - } -} - -export async function createDbAdapter(connectionString: string): Promise { - const { worker, dbId } = await initializeSQLite(connectionString) - - await applyMigrations(worker, dbId) - //TODO: FIXME - return new SqliteAdapter(worker, dbId) as unknown as DbAdapter -} diff --git a/packages/sqlite-wasm/src/connection.ts b/packages/sqlite-wasm/src/connection.ts deleted file mode 100644 index 281b7f9c78..0000000000 --- a/packages/sqlite-wasm/src/connection.ts +++ /dev/null @@ -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 - - (command: 'close'): Promise -} - -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 } -} diff --git a/packages/sqlite-wasm/src/db/base.ts b/packages/sqlite-wasm/src/db/base.ts deleted file mode 100644 index be348e544b..0000000000 --- a/packages/sqlite-wasm/src/db/base.ts +++ /dev/null @@ -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): Promise { - 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): Promise { - 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[]> { - return new Promise(async (resolve) => { - const rows: Record[] = []; - - 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 = {}; - res.columnNames.forEach((columnName, index) => { - rowObject[columnName] = res.row?.[index] ?? undefined - }); - - rows.push(rowObject); - } - }, - }); - }); - } -} \ No newline at end of file diff --git a/packages/sqlite-wasm/src/db/message.ts b/packages/sqlite-wasm/src/db/message.ts deleted file mode 100644 index 44c820224b..0000000000 --- a/packages/sqlite-wasm/src/db/message.ts +++ /dev/null @@ -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 { - 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 { - await this.remove(TableName.Message, {id: message}) - } - - async createPatch(message: MessageID, content: RichText, creator: SocialID, created: Date): Promise { - 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 { - 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 { - await this.remove(TableName.Attachment, { - message_id: message, - card_id: card - }) - } - - //Reaction - async createReaction(message: MessageID, reaction: string, creator: SocialID, created: Date): Promise { - 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 { - await this.remove(TableName.Reaction, { - message_id: message, - reaction: reaction, - creator: creator - }) - } - - //Find messages - async find(workspace: string, params: FindMessagesParams): Promise { - 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) - } - } -} - diff --git a/packages/sqlite-wasm/src/db/notification.ts b/packages/sqlite-wasm/src/db/notification.ts deleted file mode 100644 index b3215a2e5a..0000000000 --- a/packages/sqlite-wasm/src/db/notification.ts +++ /dev/null @@ -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 { - const dbData: NotificationDb = { - message_id: message, - context_id: context - } - await this.insert(TableName.Notification, dbData) - } - - async removeNotification(message: MessageID, context: ContextID): Promise { - await this.remove(TableName.Notification, { - message_id: message, - context - }) - } - - async createContext(workspace: string, card: CardID, personalWorkspace: string, lastView?: Date, lastUpdate?: Date): Promise { - 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 { - await this.remove(TableName.NotificationContext, { - id: context - }) - } - - async updateContext(context: ContextID, update: NotificationContextUpdate): Promise { - const dbData: Partial = {} - - 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 { - 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 { - //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 - } - } -} - diff --git a/packages/sqlite-wasm/src/db/types.ts b/packages/sqlite-wasm/src/db/types.ts deleted file mode 100644 index edde495119..0000000000 --- a/packages/sqlite-wasm/src/db/types.ts +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/packages/sqlite-wasm/src/migrations.ts b/packages/sqlite-wasm/src/migrations.ts deleted file mode 100644 index 20db224d33..0000000000 --- a/packages/sqlite-wasm/src/migrations.ts +++ /dev/null @@ -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 { - await migrationV1(worker, dbId) -} - -async function migrationV1(worker: Sqlite3Worker1Promiser, dbId: string): Promise { - 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 - ); - ` - }) -} diff --git a/packages/sqlite-wasm/tsconfig.json b/packages/sqlite-wasm/tsconfig.json deleted file mode 100644 index e7a6cb178d..0000000000 --- a/packages/sqlite-wasm/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "declarationDir": "./types", - "emitDeclarationOnly": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src"] -} diff --git a/packages/types/package.json b/packages/types/package.json index cac7fc4471..1501b2565d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -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", diff --git a/packages/client-ws/src/index.ts b/packages/types/src/core.ts similarity index 62% rename from packages/client-ws/src/index.ts rename to packages/types/src/core.ts index a4b18e3dca..4d5175102e 100644 --- a/packages/client-ws/src/index.ts +++ b/packages/types/src/core.ts @@ -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 +export type CardID = Ref +export type SocialID = PersonId +export type WorkspaceID = WorkspaceUuid +export type RichText = string +export type AccountID = AccountUuid + +export type ID = string diff --git a/packages/types/src/file.ts b/packages/types/src/file.ts index 5068805588..7b8da10cf3 100644 --- a/packages/types/src/file.ts +++ b/packages/types/src/file.ts @@ -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 { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b95c2f6b61..3cd71d8829 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -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' diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index f9c3a7a760..514f23eff1 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.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 -export type CardID = Ref -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 + action: 'add' | 'remove' +} + +type AttributeValue = string | number | null + +export interface ActivityAttributeUpdate { + type: ActivityUpdateType.Attribute + attrKey: string + attrClass: Ref>> + mixin?: Ref> + 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 } diff --git a/packages/types/src/notification.ts b/packages/types/src/notification.ts index c860d2e24b..be4ee0fa5e 100644 --- a/packages/types/src/notification.ts +++ b/packages/types/src/notification.ts @@ -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[] } diff --git a/packages/types/src/query.ts b/packages/types/src/query.ts index 474ef6b286..3ea86cb1f0 100644 --- a/packages/types/src/query.ts +++ b/packages/types/src/query.ts @@ -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 = { - [K in keyof T]: Record & Partial, never>> -}[keyof T] - export interface Window { getResult(): T[] loadNextPage(): Promise + loadPrevPage(): Promise hasNextPage(): boolean + hasPrevPage(): boolean } @@ -44,27 +43,43 @@ interface FindParams { export interface FindMessagesParams extends FindParams { id?: MessageID card: CardID - created?: Exclusive> | Date -} - -export interface FindNotificationsParams extends FindParams { - context?: ContextID - message?: MessageID - read?: boolean - archived?: boolean - created?: Exclusive> | Date -} - -export interface FindNotificationContextParams extends FindParams { - id?: ContextID - card?: CardID + files?: boolean + reactions?: boolean + replies?: boolean + created?: Partial> | Date } export interface FindMessagesGroupsParams extends FindParams { card: CardID blobId?: BlobID - withPatches?: boolean - fromDate?: Partial> | Date - toDate?: Partial> | Date - orderBy?: 'fromDate' | 'toDate' + patches?: boolean + fromSec?: Partial> | Date + toSec?: Partial> | Date + orderBy?: 'fromSec' | 'toSec' +} + +export interface FindNotificationContextParams extends FindParams { + id?: ContextID + card?: CardID + lastUpdate?: Partial> | Date + account?: AccountID | AccountID[] + notifications?: { + message?: boolean + limit: number + order: SortingOrder + read?: boolean + } +} + +export interface FindNotificationsParams extends FindParams { + context?: ContextID + read?: boolean + created?: Partial> | Date + account?: AccountID | AccountID[] + message?: boolean +} + +export interface FindCollaboratorsParams extends FindParams { + card: CardID + account?: AccountID | AccountID[] } diff --git a/packages/client-ws/package.json b/packages/yaml/package.json similarity index 58% rename from packages/client-ws/package.json rename to packages/yaml/package.json index 542e6b52fe..c97729c12c 100644 --- a/packages/client-ws/package.json +++ b/packages/yaml/package.json @@ -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" diff --git a/packages/yaml/src/deserialize.ts b/packages/yaml/src/deserialize.ts new file mode 100644 index 0000000000..93171636f8 --- /dev/null +++ b/packages/yaml/src/deserialize.ts @@ -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 + })) + } +} diff --git a/packages/sqlite-wasm/src/index.ts b/packages/yaml/src/index.ts similarity index 91% rename from packages/sqlite-wasm/src/index.ts rename to packages/yaml/src/index.ts index 566be6a066..86f8e444af 100644 --- a/packages/sqlite-wasm/src/index.ts +++ b/packages/yaml/src/index.ts @@ -13,4 +13,5 @@ // limitations under the License. // -export * from './adapter.ts' +export * from './parse.ts' +export * from './deserialize.ts' diff --git a/packages/shared/src/files.ts b/packages/yaml/src/parse.ts similarity index 90% rename from packages/shared/src/files.ts rename to packages/yaml/src/parse.ts index 8159556901..a08fd688ba 100644 --- a/packages/shared/src/files.ts +++ b/packages/yaml/src/parse.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, diff --git a/packages/examples/tsconfig.json b/packages/yaml/tsconfig.json similarity index 100% rename from packages/examples/tsconfig.json rename to packages/yaml/tsconfig.json