diff --git a/bun.lock b/bun.lock index 098028ab1c..238a096925 100644 --- a/bun.lock +++ b/bun.lock @@ -4,19 +4,19 @@ "": { "name": "@hcengineering/communication", "devDependencies": { - "@eslint/js": "^9.15.0", - "@types/bun": "^1.1.14", - "bun-types": "^1.1.38", - "eslint": "^9.15.0", + "@eslint/js": "^9.21.0", + "@types/bun": "^1.2.4", + "bun-types": "^1.2.4", + "eslint": "^9.21.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.1", - "prettier": "^3.3.3", - "typescript-eslint": "^8.15.0", + "eslint-plugin-prettier": "^5.2.3", + "prettier": "^3.5.2", + "typescript-eslint": "^8.25.0", }, }, "packages/client-query": { "name": "@hcengineering/communication-client-query", - "version": "0.1.9", + "version": "0.1.47", "dependencies": { "@hcengineering/communication-query": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", @@ -32,7 +32,7 @@ }, "packages/client-sqlite": { "name": "@hcengineering/communication-client-sqlite", - "version": "0.1.9", + "version": "0.1.47", "dependencies": { "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-sqlite-wasm": "workspace:*", @@ -48,8 +48,9 @@ }, "packages/client-ws": { "name": "@hcengineering/communication-client-ws", - "version": "0.1.9", + "version": "0.1.47", "dependencies": { + "@hcengineering/communication-client-query": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-types": "workspace:*", "@msgpack/msgpack": "^3.0.0-beta2", @@ -63,12 +64,11 @@ }, "packages/cockroach": { "name": "@hcengineering/communication-cockroach", - "version": "0.1.9", + "version": "0.1.47", "dependencies": { - "@hcengineering/communication-core": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", + "@hcengineering/communication-shared": "workspace:*", "@hcengineering/communication-types": "workspace:*", - "pg": "8.12.0", "postgres": "^3.4.4", "uuid": "^11.0.3", }, @@ -79,25 +79,10 @@ "typescript": "^5.6.3", }, }, - "packages/core": { - "name": "@hcengineering/communication-core", - "version": "0.1.9", - "dependencies": { - "@hcengineering/communication-types": "workspace:*", - }, - "devDependencies": { - "@types/bun": "^1.1.14", - }, - "peerDependencies": { - "typescript": "^5.6.3", - }, - }, "packages/examples": { "name": "@hcengineering/communication-examples", - "version": "0.1.9", + "version": "0.1.47", "dependencies": { - "@hcengineering/communication-client-query": "workspace:*", - "@hcengineering/communication-client-sqlite": "workspace:*", "@hcengineering/communication-client-ws": "workspace:*", "@hcengineering/communication-types": "workspace:*", }, @@ -110,9 +95,10 @@ }, "packages/query": { "name": "@hcengineering/communication-query", - "version": "0.1.9", + "version": "0.1.47", "dependencies": { "@hcengineering/communication-sdk-types": "workspace:*", + "@hcengineering/communication-shared": "workspace:*", "@hcengineering/communication-types": "workspace:*", "fast-equals": "^5.0.1", }, @@ -126,7 +112,7 @@ }, "packages/sdk-types": { "name": "@hcengineering/communication-sdk-types", - "version": "0.1.9", + "version": "0.1.47", "dependencies": { "@hcengineering/communication-types": "workspace:*", }, @@ -139,7 +125,7 @@ }, "packages/server": { "name": "@hcengineering/communication-server", - "version": "0.1.9", + "version": "0.1.47", "dependencies": { "@hcengineering/communication-cockroach": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", @@ -156,9 +142,24 @@ "typescript": "^5.6.3", }, }, + "packages/shared": { + "name": "@hcengineering/communication-shared", + "version": "0.1.47", + "dependencies": { + "@hcengineering/communication-types": "workspace:*", + "js-yaml": "^4.1.0", + }, + "devDependencies": { + "@types/bun": "^1.1.14", + "@types/js-yaml": "^4.0.9", + }, + "peerDependencies": { + "typescript": "^5.6.3", + }, + }, "packages/sqlite-wasm": { "name": "@hcengineering/communication-sqlite-wasm", - "version": "0.1.9", + "version": "0.1.47", "dependencies": { "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-types": "workspace:*", @@ -175,7 +176,7 @@ }, "packages/types": { "name": "@hcengineering/communication-types", - "version": "0.1.9", + "version": "0.1.47", "dependencies": { "@hcengineering/card": "0.7.2", "@hcengineering/core": "0.7.2", @@ -199,15 +200,15 @@ "@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="], - "@eslint/core": ["@eslint/core@0.11.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA=="], + "@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.2.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w=="], + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ=="], - "@eslint/js": ["@eslint/js@9.20.0", "", {}, "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ=="], + "@eslint/js": ["@eslint/js@9.21.0", "", {}, "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="], + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="], "@formatjs/ecma402-abstract": ["@formatjs/ecma402-abstract@2.3.3", "", { "dependencies": { "@formatjs/fast-memoize": "2.2.6", "@formatjs/intl-localematcher": "0.6.0", "decimal.js": "10", "tslib": "2" } }, "sha512-pJT1OkhplSmvvr6i3CWTPvC/FGC06MbN5TNBfRO6Ox62AEz90eMq+dVvtX9Bl3jxCEkS0tATzDarRZuOLw7oFg=="], @@ -231,8 +232,6 @@ "@hcengineering/communication-cockroach": ["@hcengineering/communication-cockroach@workspace:packages/cockroach"], - "@hcengineering/communication-core": ["@hcengineering/communication-core@workspace:packages/core"], - "@hcengineering/communication-examples": ["@hcengineering/communication-examples@workspace:packages/examples"], "@hcengineering/communication-query": ["@hcengineering/communication-query@workspace:packages/query"], @@ -241,6 +240,8 @@ "@hcengineering/communication-server": ["@hcengineering/communication-server@workspace:packages/server"], + "@hcengineering/communication-shared": ["@hcengineering/communication-shared@workspace:packages/shared"], + "@hcengineering/communication-sqlite-wasm": ["@hcengineering/communication-sqlite-wasm@workspace:packages/sqlite-wasm"], "@hcengineering/communication-types": ["@hcengineering/communication-types@workspace:packages/types"], @@ -259,7 +260,7 @@ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.1", "", {}, "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA=="], + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], @@ -285,7 +286,7 @@ "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="], - "@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="], + "@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], @@ -301,6 +302,8 @@ "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="], + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], @@ -319,21 +322,21 @@ "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.24.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/type-utils": "8.24.1", "@typescript-eslint/utils": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.25.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/type-utils": "8.25.0", "@typescript-eslint/utils": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.24.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.25.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/types": "8.25.0", "@typescript-eslint/typescript-estree": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1" } }, "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0" } }, "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.24.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/utils": "8.24.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.25.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.25.0", "@typescript-eslint/utils": "8.25.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.24.1", "", {}, "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.25.0", "", {}, "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.25.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/types": "8.25.0", "@typescript-eslint/typescript-estree": "8.25.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ=="], "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], @@ -357,7 +360,7 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="], + "bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], @@ -391,7 +394,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.20.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g=="], + "eslint": ["eslint@9.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.21.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg=="], "eslint-config-prettier": ["eslint-config-prettier@9.1.0", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw=="], @@ -513,37 +516,13 @@ "periscopic": ["periscopic@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", "is-reference": "^3.0.0" } }, "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw=="], - "pg": ["pg@8.12.0", "", { "dependencies": { "pg-connection-string": "^2.6.4", "pg-pool": "^3.6.2", "pg-protocol": "^1.6.1", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.1.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ=="], - - "pg-cloudflare": ["pg-cloudflare@1.1.1", "", {}, "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q=="], - - "pg-connection-string": ["pg-connection-string@2.7.0", "", {}, "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA=="], - - "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - - "pg-pool": ["pg-pool@3.7.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw=="], - - "pg-protocol": ["pg-protocol@1.7.1", "", {}, "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ=="], - - "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], - - "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "postgres": ["postgres@3.4.5", "", {}, "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg=="], - "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], - - "postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="], - - "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], - - "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.5.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw=="], + "prettier": ["prettier@3.5.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg=="], "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], @@ -569,8 +548,6 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -589,7 +566,7 @@ "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], - "typescript-eslint": ["typescript-eslint@8.24.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/parser": "8.24.1", "@typescript-eslint/utils": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA=="], + "typescript-eslint": ["typescript-eslint@8.25.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.25.0", "@typescript-eslint/parser": "8.25.0", "@typescript-eslint/utils": "8.25.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], @@ -603,14 +580,10 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.10.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw=="], - "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], diff --git a/package.json b/package.json index 92c68b7cd1..1d53a75e85 100644 --- a/package.json +++ b/package.json @@ -3,18 +3,19 @@ "type": "module", "workspaces": ["packages/*"], "scripts": { + "publish": "sh scripts/publish.sh", "build": "sh scripts/build.sh && tsc --noEmit", "lint": "eslint packages/**/src/*.ts", "format": "prettier --write packages/**/src/*.ts && bun run lint" }, "devDependencies": { - "@eslint/js": "^9.15.0", - "@types/bun": "^1.1.14", - "bun-types": "^1.1.38", - "eslint": "^9.15.0", + "@eslint/js": "^9.21.0", + "@types/bun": "^1.2.4", + "bun-types": "^1.2.4", + "eslint": "^9.21.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.1", - "prettier": "^3.3.3", - "typescript-eslint": "^8.15.0" + "eslint-plugin-prettier": "^5.2.3", + "prettier": "^3.5.2", + "typescript-eslint": "^8.25.0" } } diff --git a/packages/client-query/package.json b/packages/client-query/package.json index 79c0faedee..df4ca1bc4c 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.9", + "version": "0.1.47", "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 c48319513f..b1df62dcd8 100644 --- a/packages/client-query/src/index.ts +++ b/packages/client-query/src/index.ts @@ -1,5 +1,6 @@ import { LiveQueries } from '@hcengineering/communication-query' import type { QueryClient } from '@hcengineering/communication-sdk-types' +import type { WorkspaceID } from '@hcengineering/communication-types' import { MessagesQuery, NotificationsQuery } from './query' @@ -13,12 +14,12 @@ export function createNotificationsQuery(): NotificationsQuery { return new NotificationsQuery(lq) } -export function initLiveQueries(client: QueryClient) { +export function initLiveQueries(client: QueryClient, workspace: WorkspaceID, filesUrl: string): void { if (lq != null) { lq.close() } - lq = new LiveQueries(client) + lq = new LiveQueries(client, workspace, filesUrl) client.onEvent = (event) => { void lq.onEvent(event) diff --git a/packages/client-query/src/query.ts b/packages/client-query/src/query.ts index 55b2840c90..400e1e4d72 100644 --- a/packages/client-query/src/query.ts +++ b/packages/client-query/src/query.ts @@ -44,7 +44,7 @@ class BaseQuery

, C extends QueryCallback> { } } - private needUpdate(params: FindMessagesParams, callback: MessagesQueryCallback): boolean { + private needUpdate(params: P, callback: C): boolean { if (!deepEqual(params, this.oldQuery)) return true if (!deepEqual(callback.toString(), this.oldCallback?.toString())) return true return false diff --git a/packages/client-sqlite/package.json b/packages/client-sqlite/package.json index 97743c2e9b..ee0fa824cb 100644 --- a/packages/client-sqlite/package.json +++ b/packages/client-sqlite/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/communication-client-sqlite", - "version": "0.1.9", + "version": "0.1.47", "main": "dist/index.js", "module": "dist/index.js", "types": "./types/index.d.ts", diff --git a/packages/client-sqlite/src/client.ts b/packages/client-sqlite/src/client.ts index 2dc53627e9..2393490f9f 100644 --- a/packages/client-sqlite/src/client.ts +++ b/packages/client-sqlite/src/client.ts @@ -13,7 +13,10 @@ import { type Notification, type Attachment, type Reaction, - type WorkspaceID + type WorkspaceID, + type FindMessagesGroupsParams, + type MessagesGroup, + PatchType } from '@hcengineering/communication-types' import { type Client, @@ -24,7 +27,8 @@ import { } from '@hcengineering/communication-sdk-types' import { createDbAdapter as createSqliteDbAdapter } from '@hcengineering/communication-sqlite-wasm' -class DbClient implements Client { +//TODO: FIXME +class DbClient { onEvent: (event: ResponseEvent) => void = () => {} constructor( @@ -61,10 +65,14 @@ class DbClient implements Client { this.onEvent({ type: ResponseEventType.MessageRemoved, message, card }) } - async createPatch(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise { + async updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise { const created = new Date() - await this.db.createPatch(card, message, content, creator, created) - this.onEvent({ type: ResponseEventType.PatchCreated, card, patch: { message, content, creator, created } }) + await this.db.createPatch(card, message, PatchType.update, content, creator, created) + this.onEvent({ + type: ResponseEventType.PatchCreated, + card, + patch: { message, type: PatchType.update, content, creator, created } + }) } async createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise { @@ -98,6 +106,10 @@ class DbClient implements Client { 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] } @@ -158,11 +170,17 @@ class DbClient implements Client { return await this.db.findContexts(params, [this.personalWorkspace]) } + //eslint-disable-next-line @typescript-eslint/no-unused-vars async findNotifications(params: FindNotificationsParams): Promise { //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 } @@ -178,5 +196,5 @@ export async function getSqliteClient( dbUrl = 'file:communication.sqlite3?vfs=opfs' ): Promise { const db = await createSqliteDbAdapter(dbUrl) - return new DbClient(db, workspace, personalWorkspace) + return new DbClient(db, workspace, personalWorkspace) as unknown as Client } diff --git a/packages/client-ws/package.json b/packages/client-ws/package.json index e9c75bc5f9..86e4dadcb1 100644 --- a/packages/client-ws/package.json +++ b/packages/client-ws/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/communication-client-ws", - "version": "0.1.9", + "version": "0.1.47", "main": "dist/index.js", "module": "dist/index.js", "types": "./types/index.d.ts", @@ -14,6 +14,7 @@ }, "dependencies": { "@hcengineering/communication-types": "workspace:*", + "@hcengineering/communication-client-query": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", "@msgpack/msgpack": "^3.0.0-beta2" }, diff --git a/packages/client-ws/src/client.ts b/packages/client-ws/src/client.ts index 74d2828437..d11674f83b 100644 --- a/packages/client-ws/src/client.ts +++ b/packages/client-ws/src/client.ts @@ -1,18 +1,19 @@ import { - type Attachment, type CardID, type ContextID, + type FindMessagesGroupsParams, type FindMessagesParams, type FindNotificationContextParams, type FindNotificationsParams, type Message, type MessageID, + type MessagesGroup, type Notification, type NotificationContext, type NotificationContextUpdate, - type Reaction, type RichText, - type SocialID + type SocialID, + type WorkspaceID } from '@hcengineering/communication-types' import { RequestEventType, @@ -35,6 +36,7 @@ import { type ResponseEvent, type UpdateNotificationContextEvent } from '@hcengineering/communication-sdk-types' +import { initLiveQueries } from '@hcengineering/communication-client-query' import { WebSocketConnection } from './connection' @@ -75,7 +77,7 @@ class WsClient implements Client { await this.sendEvent(event) } - async createPatch(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise { + async updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise { const event: CreatePatchEvent = { type: RequestEventType.CreatePatch, card, @@ -130,39 +132,11 @@ class WsClient implements Client { } async findMessages(params: FindMessagesParams, queryId?: number): Promise { - const rawMessages = await this.ws.send('findMessages', [params, queryId]) - return rawMessages.map((it: any) => this.toMessage(it)) + return await this.ws.send('findMessages', [params, queryId]) } - toMessage(raw: any): Message { - return { - id: raw.id, - card: raw.card, - content: raw.content, - creator: raw.creator, - created: new Date(raw.created), - edited: new Date(raw.edited), - reactions: raw.reactions.map((it: any) => this.toReaction(it)), - attachments: raw.attachments.map((it: any) => this.toAttachment(it)) - } - } - - toAttachment(raw: any): Attachment { - return { - message: raw.message, - card: raw.card, - creator: raw.creator, - created: new Date(raw.created) - } - } - - toReaction(raw: any): Reaction { - return { - message: raw.message, - reaction: raw.reaction, - creator: raw.creator, - created: new Date(raw.created) - } + async findMessagesGroups(params: FindMessagesGroupsParams): Promise { + return await this.ws.send('findMessagesGroups', [params]) } async createNotification(message: MessageID, context: ContextID): Promise { @@ -229,12 +203,23 @@ class WsClient implements Client { 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): Promise { - return new WsClient(url, token) +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/index.ts b/packages/client-ws/src/index.ts index 83dae7638c..226fb61584 100644 --- a/packages/client-ws/src/index.ts +++ b/packages/client-ws/src/index.ts @@ -1 +1,4 @@ +import { createMessagesQuery, createNotificationsQuery } from '@hcengineering/communication-client-query' + export * from './client' +export { createMessagesQuery, createNotificationsQuery } diff --git a/packages/cockroach/migrations/00_schema.sql b/packages/cockroach/migrations/00_schema.sql index 0f19be60a3..ed4ea1dc0c 100644 --- a/packages/cockroach/migrations/00_schema.sql +++ b/packages/cockroach/migrations/00_schema.sql @@ -1,2 +1 @@ -DROP SCHEMA IF EXISTS communication CASCADE; CREATE SCHEMA IF NOT EXISTS communication; diff --git a/packages/cockroach/migrations/01_message.sql b/packages/cockroach/migrations/01_message.sql index 65ee7b4aab..d113fe2fd3 100644 --- a/packages/cockroach/migrations/01_message.sql +++ b/packages/cockroach/migrations/01_message.sql @@ -8,9 +8,11 @@ CREATE TABLE IF NOT EXISTS communication.messages creator VARCHAR(255) NOT NULL, created TIMESTAMPTZ NOT NULL, - PRIMARY KEY (id, card_id, workspace_id) + PRIMARY KEY (workspace_id, card_id, id) ); +CREATE INDEX IF NOT EXISTS idx_messages_workspace_card ON communication.messages (workspace_id, card_id); +CREATE INDEX IF NOT EXISTS idx_messages_workspace_card_id ON communication.messages (workspace_id, card_id, id); CREATE TABLE IF NOT EXISTS communication.messages_groups ( @@ -18,11 +20,14 @@ CREATE TABLE IF NOT EXISTS communication.messages_groups card_id VARCHAR(255) NOT NULL, blob_id UUID NOT NULL, - from_id INT8 NOT NULL, - to_id INT8 NOT NULL, from_date TIMESTAMPTZ NOT NULL, to_date TIMESTAMPTZ NOT NULL, + from_id INT8 NOT NULL, + to_id INT8 NOT NULL, count INT NOT NULL, PRIMARY KEY (workspace_id, card_id, blob_id) ); + + +CREATE INDEX IF NOT EXISTS idx_messages_groups_workspace_card ON communication.messages_groups (workspace_id, card_id); \ No newline at end of file diff --git a/packages/cockroach/migrations/02_patch.sql b/packages/cockroach/migrations/02_patch.sql index 0513090d97..527df14bf0 100644 --- a/packages/cockroach/migrations/02_patch.sql +++ b/packages/cockroach/migrations/02_patch.sql @@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS communication.patch workspace_id UUID NOT NULL, card_id VARCHAR(255) NOT NULL, message_id INT8 NOT NULL, + type VARCHAR(255) NOT NULL, content TEXT NOT NULL, creator VARCHAR(255) NOT NULL, created TIMESTAMPTZ NOT NULL, @@ -11,4 +12,4 @@ CREATE TABLE IF NOT EXISTS communication.patch PRIMARY KEY (id) ); -CREATE INDEX idx_patch_message_id ON communication.patch (message_id); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_patch_workspace_card_message ON communication.patch (workspace_id, card_id, message_id); \ No newline at end of file diff --git a/packages/cockroach/migrations/03_attachment.sql b/packages/cockroach/migrations/03_attachment.sql index 3a45d1fcb9..780002f335 100644 --- a/packages/cockroach/migrations/03_attachment.sql +++ b/packages/cockroach/migrations/03_attachment.sql @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS communication.attachments creator VARCHAR(255) NOT NULL, created TIMESTAMPTZ NOT NULL DEFAULT now(), - PRIMARY KEY (message_id, card_id) + PRIMARY KEY (card_id, message_id) ); CREATE INDEX IF NOT EXISTS attachment_message_idx ON communication.attachments (message_id); diff --git a/packages/cockroach/migrations/04_reaction.sql b/packages/cockroach/migrations/04_reaction.sql index a8b4fc9496..30e8ce02a7 100644 --- a/packages/cockroach/migrations/04_reaction.sql +++ b/packages/cockroach/migrations/04_reaction.sql @@ -7,7 +7,8 @@ CREATE TABLE IF NOT EXISTS communication.reactions creator VARCHAR(255) NOT NULL, created TIMESTAMPTZ NOT NULL DEFAULT now(), + FOREIGN KEY (workspace_id, card_id, message_id) REFERENCES communication.messages (workspace_id, card_id, id) ON DELETE CASCADE, PRIMARY KEY (workspace_id, card_id, message_id, creator, reaction) ); -CREATE INDEX IF NOT EXISTS reaction_message_idx ON communication.reactions (message_id); +CREATE INDEX IF NOT EXISTS idx_reactions_workspace_card_message ON communication.reactions (workspace_id, card_id, message_id); \ No newline at end of file diff --git a/packages/cockroach/migrations/05_notification_context.sql b/packages/cockroach/migrations/05_notification_context.sql deleted file mode 100644 index 33fc386649..0000000000 --- a/packages/cockroach/migrations/05_notification_context.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE IF NOT EXISTS communication.notification_context -( - id UUID NOT NULL DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL, - card_id VARCHAR(255) NOT NULL, - - personal_workspace UUID NOT NULL, - - archived_from TIMESTAMPTZ, - last_view TIMESTAMPTZ, - last_update TIMESTAMPTZ, - - PRIMARY KEY (id), - UNIQUE (workspace_id, card_id, personal_workspace) -); diff --git a/packages/cockroach/migrations/05_thread.sql b/packages/cockroach/migrations/05_thread.sql new file mode 100644 index 0000000000..73d6e50120 --- /dev/null +++ b/packages/cockroach/migrations/05_thread.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS communication.thread +( + workspace_id UUID NOT NULL, + card_id VARCHAR(255) NOT NULL, + message_id INT8 NOT NULL, + thread_id VARCHAR(255) NOT NULL, + replies_count INT NOT NULL, + last_reply TIMESTAMPTZ NOT NULL, + + PRIMARY KEY (workspace_id, thread_id), + UNIQUE (workspace_id, card_id, message_id) +); + +CREATE INDEX IF NOT EXISTS idx_thread_workspace_card_message ON communication.thread (workspace_id, thread_id); +CREATE INDEX IF NOT EXISTS idx_thread_workspace_card_message ON communication.thread (workspace_id, card_id, message_id); \ No newline at end of file diff --git a/packages/cockroach/migrations/06_notification.sql b/packages/cockroach/migrations/06_notification.sql index 0bb7b2c946..df565632a1 100644 --- a/packages/cockroach/migrations/06_notification.sql +++ b/packages/cockroach/migrations/06_notification.sql @@ -1,3 +1,20 @@ +CREATE TABLE IF NOT EXISTS communication.notification_context +( + id UUID NOT NULL DEFAULT gen_random_uuid(), + workspace_id UUID NOT NULL, + card_id VARCHAR(255) NOT NULL, + + personal_workspace UUID NOT NULL, + + archived_from TIMESTAMPTZ, + last_view TIMESTAMPTZ, + last_update TIMESTAMPTZ, + + PRIMARY KEY (id), + UNIQUE (workspace_id, card_id, personal_workspace) +); + + CREATE TABLE IF NOT EXISTS communication.notifications ( message_id UUID NOT NULL, diff --git a/packages/cockroach/package.json b/packages/cockroach/package.json index 619b223cf4..b34725c8ed 100644 --- a/packages/cockroach/package.json +++ b/packages/cockroach/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/communication-cockroach", - "version": "0.1.9", + "version": "0.1.47", "main": "dist/index.js", "module": "dist/index.js", "types": "./types/index.d.ts", @@ -13,10 +13,9 @@ "@types/bun": "^1.1.14" }, "dependencies": { - "@hcengineering/communication-core": "workspace:*", + "@hcengineering/communication-shared": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", "@hcengineering/communication-types": "workspace:*", - "pg": "8.12.0", "postgres": "^3.4.4", "uuid": "^11.0.3" }, diff --git a/packages/cockroach/src/adapter.ts b/packages/cockroach/src/adapter.ts index 0a805aab03..0ca7ef20b1 100644 --- a/packages/cockroach/src/adapter.ts +++ b/packages/cockroach/src/adapter.ts @@ -1,73 +1,88 @@ +import type { ParameterOrJSON, Row } from 'postgres' import type postgres from 'postgres' import { - type Message, - type FindMessagesParams, + type BlobID, type CardID, + type ContextID, + type FindMessagesGroupsParams, + type FindMessagesParams, + type FindNotificationContextParams, + type FindNotificationsParams, + type Message, + type MessageID, + type MessagesGroup, + type Notification, + type NotificationContext, + type NotificationContextUpdate, + type PatchType, type RichText, type SocialID, - type MessageID, - type ContextID, - type NotificationContextUpdate, - type FindNotificationContextParams, - type NotificationContext, - type FindNotificationsParams, - type Notification, - type BlobID, - type MessagesGroup, - type FindMessagesGroupsParams, - type WorkspaceID + type WorkspaceID, + type Thread } from '@hcengineering/communication-types' import type { DbAdapter } from '@hcengineering/communication-sdk-types' +import { retry } from '@hcengineering/communication-shared' import { MessagesDb } from './db/message' import { NotificationsDb } from './db/notification' import { connect, type PostgresClientReference } from './connection' +import { type Options, type Logger, type SqlClient } from './types' export class CockroachAdapter implements DbAdapter { private readonly message: MessagesDb private readonly notification: NotificationsDb constructor( - private readonly db: PostgresClientReference, - private readonly sqlClient: postgres.Sql, - private readonly workspace: WorkspaceID + private readonly sql: SqlClient, + private readonly workspace: WorkspaceID, + private readonly logger?: Logger, + private readonly options?: Options ) { - this.message = new MessagesDb(this.sqlClient, this.workspace) - this.notification = new NotificationsDb(this.sqlClient, this.workspace) + this.message = new MessagesDb(this.sql, this.workspace, logger, options) + this.notification = new NotificationsDb(this.sql, this.workspace, logger, options) } async createMessage(card: CardID, content: RichText, creator: SocialID, created: Date): Promise { return await this.message.createMessage(card, content, creator, created) } + 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 createPatch( card: CardID, message: MessageID, + type: PatchType, content: RichText, creator: SocialID, created: Date ): Promise { - return await this.message.createPatch(card, message, content, creator, created) + await this.message.createPatch(card, message, type, content, creator, created) } - async removeMessage(card: CardID, message: MessageID): Promise { - return await this.message.removeMessage(card, message) - } - - async removeMessages(card: CardID, ids: MessageID[]): Promise { - return await this.message.removeMessages(card, ids) + async removePatches(card: CardID, fromId: MessageID, toId: MessageID): Promise { + await this.message.removePatches(card, fromId, toId) } async createMessagesGroup( card: CardID, blobId: BlobID, - from_id: MessageID, - to_id: MessageID, - from_date: Date, - to_date: Date, + fromDate: Date, + toDate: Date, + fromId: MessageID, + toId: MessageID, count: number ): Promise { - return await this.message.createMessagesGroup(card, blobId, from_id, to_id, from_date, to_date, count) + await this.message.createMessagesGroup(card, blobId, fromDate, toDate, fromId, toId, count) + } + + async removeMessagesGroup(card: CardID, blobId: BlobID): Promise { + await this.message.removeMessagesGroup(card, blobId) } async createReaction( @@ -77,35 +92,35 @@ export class CockroachAdapter implements DbAdapter { creator: SocialID, created: Date ): Promise { - return await this.message.createReaction(card, message, reaction, creator, created) + await this.message.createReaction(card, message, reaction, creator, created) } async removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise { - return await this.message.removeReaction(card, message, reaction, creator) + await this.message.removeReaction(card, message, reaction, creator, new Date()) } async createAttachment(message: MessageID, attachment: CardID, creator: SocialID, created: Date): Promise { - return await this.message.createAttachment(message, attachment, creator, created) + await this.message.createAttachment(message, attachment, creator, created) } async removeAttachment(message: MessageID, attachment: CardID): Promise { - return await this.message.removeAttachment(message, attachment) + await this.message.removeAttachment(message, attachment) } - async findMessages(params: FindMessagesParams): Promise { - return await this.message.find(params) + async createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise { + await this.message.createThread(card, message, thread, created) } - async findMessagesGroups(params: FindMessagesGroupsParams): Promise { - return await this.message.findGroups(params) + async updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise { + await this.message.updateThread(thread, lastReply, op) } async createNotification(message: MessageID, context: ContextID): Promise { - return await this.notification.createNotification(message, context) + await this.notification.createNotification(message, context) } async removeNotification(message: MessageID, context: ContextID): Promise { - return await this.notification.removeNotification(message, context) + await this.notification.removeNotification(message, context) } async createContext( @@ -118,11 +133,24 @@ export class CockroachAdapter implements DbAdapter { } async updateContext(context: ContextID, update: NotificationContextUpdate): Promise { - return await this.notification.updateContext(context, update) + await this.notification.updateContext(context, update) } async removeContext(context: ContextID): Promise { - return await this.notification.removeContext(context) + await this.notification.removeContext(context) + } + + // Finds + async findMessages(params: FindMessagesParams): Promise { + return await this.message.find(params) + } + + async findMessagesGroups(params: FindMessagesGroupsParams): Promise { + return await this.message.findMessagesGroups(params) + } + + async findThread(thread: CardID): Promise { + return await this.message.findThread(thread) } async findContexts( @@ -142,13 +170,92 @@ export class CockroachAdapter implements DbAdapter { } close(): void { - this.db.close() + this.sql.close() } } -export async function createDbAdapter(connectionString: string, workspace: WorkspaceID): Promise { - const db = connect(connectionString) - const sqlClient = await db.getClient() +export async function createDbAdapter( + connectionString: string, + workspace: WorkspaceID, + logger?: Logger, + options?: Options +): Promise { + const greenUrl = process.env.GREEN_URL ?? '' + if (greenUrl !== '') { + const client = new GreenClient(greenUrl) + return new CockroachAdapter(client, workspace, logger, options) + } else { + const connection = connect(connectionString) + const sql = await connection.getClient() + const client = new CockroachClient(connection, sql) - return new CockroachAdapter(db, sqlClient, workspace) + return new CockroachAdapter(client, workspace, logger, options) + } +} + +class GreenClient implements SqlClient { + private readonly url: string + private readonly token: string + constructor(endpoint: string) { + const url = new URL(endpoint) + this.token = url.searchParams.get('token') ?? 'secret' + + const compression = url.searchParams.get('compression') ?? '' + + const newHost = url.host + const newPathname = url.pathname + const newSearchParams = new URLSearchParams() + + if (compression !== '') { + newSearchParams.set('compression', compression) + } + + this.url = `${url.protocol}//${newHost}${newPathname}${newSearchParams.size > 0 ? '?' + newSearchParams.toString() : ''}` + } + + async execute)[]>(query: string, params?: ParameterOrJSON[]): Promise { + return await retry(() => this.fetch(query, params), { retries: 5 }) + } + + private async fetch)[]>( + query: string, + params?: ParameterOrJSON[] + ): Promise { + const url = this.url.endsWith('/') ? this.url + 'api/v1/sql' : this.url + '/api/v1/sql' + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.token, + Connection: 'keep-alive' + }, + body: JSON.stringify({ query, params }, (_, value) => (typeof value === 'bigint' ? value.toString() : value)) + }) + + if (!response.ok) { + throw new Error(`Failed to execute sql: ${response.status} ${response.statusText}`) + } + + return await response.json() + } + + close(): void { + // do nothing + } +} + +class CockroachClient implements SqlClient { + constructor( + private readonly db: PostgresClientReference, + private readonly sql: postgres.Sql + ) {} + + async execute)[]>(query: string, params?: ParameterOrJSON[]): Promise { + return await this.sql.unsafe(query, params) + } + + close(): void { + this.db.close() + } } diff --git a/packages/cockroach/src/connection.ts b/packages/cockroach/src/connection.ts index a9aa16e74b..b14941e516 100644 --- a/packages/cockroach/src/connection.ts +++ b/packages/cockroach/src/connection.ts @@ -1,4 +1,4 @@ -//Full copy from @hcengineering/postgres +// Full copy from @hcengineering/postgres import postgres from 'postgres' import { v4 as uuid } from 'uuid' @@ -86,10 +86,9 @@ export function connect(connectionString: string, database?: string): PostgresCl application_name: 'communication' }, database, - max: 10, - transform: { - undefined: null - }, + max: 5, + fetch_types: false, + prepare: true, ...extraOptions }) diff --git a/packages/cockroach/src/db/base.ts b/packages/cockroach/src/db/base.ts index 1c71ecb734..e81dad2d21 100644 --- a/packages/cockroach/src/db/base.ts +++ b/packages/cockroach/src/db/base.ts @@ -1,79 +1,41 @@ -import type postgres from 'postgres' -import type {WorkspaceID} from "@hcengineering/communication-types"; +import { type ParameterOrJSON, type Row } from 'postgres' +import type { WorkspaceID } from '@hcengineering/communication-types' + +import { type SqlClient, type Logger, type Options } from '../types' export class BaseDb { - constructor( - readonly client: postgres.Sql, - readonly workspace: WorkspaceID - ) { + constructor ( + readonly client: SqlClient, + readonly workspace: WorkspaceID, + readonly logger?: Logger, + readonly options?: Options + ) {} + + async execute)[]>( + 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) } - 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 (${keys.map((_, idx) => `$${idx + 1}`).join(', ')}); - ` - await this.client.unsafe(sql, values) - } - - async insertWithReturn(table: string, data: Record, returnField: string): Promise { - const keys = Object.keys(data) - const values = Object.values(data) - const sql = ` - INSERT INTO ${table} (${keys.map((k) => `"${k}"`).join(', ')}) - VALUES (${keys.map((_, idx) => `$${idx + 1}`).join(', ')}) - RETURNING ${returnField};` - const result = await this.client.unsafe(sql, values) - - return result[0][returnField] - } - - async remove(table: string, where: Record): Promise { - const keys = Object.keys(where) - const values = Object.values(where) - - if (keys.length === 0) { - throw new Error("WHERE condition cannot be empty"); - } - - const sql = ` - DELETE - FROM ${table} - WHERE ${keys.map((k, idx) => `"${k}" = $${idx + 1}`).join(' AND ')};` - - await this.client.unsafe(sql, values) - } - - async removeWithReturn(table: string, where: Record, returnField: string): Promise { - const keys = Object.keys(where); - const values: any[] = []; - - if (keys.length === 0) { - throw new Error("WHERE condition cannot be empty"); - } - - const whereClause = keys.map((key) => { - const value = where[key]; - if (Array.isArray(value)) { - const placeholders = value.map((_, i) => `$${values.length + i + 1}`).join(", "); - values.push(...value); - return `"${key}" IN (${placeholders})`; - } else { - values.push(value); - return `"${key}" = $${values.length}`; - } - }).join(" AND "); - - const sql = ` - DELETE FROM ${table} - WHERE ${whereClause} - RETURNING ${returnField}; - `; - - const result = await this.client.unsafe(sql, values); - - return result.map((it: any) => it[returnField]); + return await this.client.execute(sql, params) + } + + private async executeWithLogs)[]>( + name: string, + logger: Logger, + sql: string, + params?: ParameterOrJSON[] + ): Promise { + const start = performance.now() + + try { + return await this.client.execute(sql, params) + } finally { + const time = performance.now() - start + logger.info(name, { time }) } + } } diff --git a/packages/cockroach/src/db/message.ts b/packages/cockroach/src/db/message.ts index 79462c4372..1ddad0af65 100644 --- a/packages/cockroach/src/db/message.ts +++ b/packages/cockroach/src/db/message.ts @@ -1,302 +1,481 @@ import { - type Message, - type MessageID, - type CardID, - type FindMessagesParams, - SortOrder, - type SocialID, - type RichText, - Direction, - type BlobID, - type MessagesGroup, - type FindMessagesGroupsParams + type Message, + type MessageID, + type CardID, + type FindMessagesParams, + type SocialID, + type RichText, + SortingOrder, + PatchType, + type Thread, + type BlobID, + type FindMessagesGroupsParams, + type MessagesGroup } from '@hcengineering/communication-types' -import {generateMessageId} from "@hcengineering/communication-core"; +import { generateMessageId } from '@hcengineering/communication-shared' -import {BaseDb} from './base.ts' +import { BaseDb } from './base' import { - TableName, - type MessageDb, - type AttachmentDb, - type ReactionDb, - type PatchDb, - type MessagesGroupDb, - toMessage, - toMessagesGroup -} from './schema.ts' -import {getCondition} from './utils.ts'; - + TableName, + type MessageDb, + type AttachmentDb, + type ReactionDb, + type PatchDb, + toMessage, + type ThreadDb, + toThread, + type MessagesGroupDb, + toMessagesGroup +} from './schema' +import { getCondition } from './utils' export class MessagesDb extends BaseDb { - //Message - async createMessage(card: CardID, content: RichText, creator: SocialID, created: Date): Promise { - const dbData: MessageDb = { - id: generateMessageId(), - workspace_id: this.workspace, - card_id: card, - content: content, - creator: creator, - created: created, - } - - await this.insert(TableName.Message, dbData) - - return dbData.id as MessageID + // Message + async createMessage (card: CardID, content: RichText, creator: SocialID, created: Date): Promise { + const id = generateMessageId() + const db: MessageDb = { + id, + workspace_id: this.workspace, + card_id: card, + content, + creator, + created } - async removeMessage(card: CardID, message: MessageID): Promise { - const result = await this.removeWithReturn(TableName.Message, {id: message, workspace_id: this.workspace, card_id: card}, "id") - return result[0] as MessageID | undefined + const sql = `INSERT INTO ${TableName.Message} (workspace_id, card_id, id, content, creator, created) + VALUES ($1::uuid, $2::varchar, $3::bigint, $4::text, $5::varchar, $6::timestamptz)` + + await this.execute(sql, [db.workspace_id, db.card_id, db.id, db.content, db.creator, db.created], 'insert message') + + return id + } + + async removeMessage (card: CardID, message: MessageID, socialIds?: SocialID[]): Promise { + if (socialIds === undefined || socialIds.length === 0) { + const sql = `DELETE + FROM ${TableName.Message} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND id = $2::bigint;` + await this.execute(sql, [this.workspace, card, message], 'remove message') + } else if (socialIds.length === 1) { + const sql = `DELETE + FROM ${TableName.Message} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND id = $2::bigint + AND creator = $3::varchar;` + await this.execute(sql, [this.workspace, card, message, socialIds[0]], 'remove message') + } else { + const sql = `DELETE + FROM ${TableName.Message} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND id = $2::bigint + AND creator = ANY ($3::varchar[]);` + + await this.execute(sql, [this.workspace, card, message, socialIds], 'remove message') + } + } + + async removeMessages (card: CardID, fromId: MessageID, toId: MessageID): Promise { + const sql = `DELETE + FROM ${TableName.Message} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND id >= $3::bigint + AND id <= $4::bigint;` + + await this.execute(sql, [this.workspace, card, BigInt(fromId), BigInt(toId)], 'remove messages') + } + + async createPatch ( + card: CardID, + message: MessageID, + type: PatchType, + content: string, + creator: SocialID, + created: Date + ): Promise { + const db: PatchDb = { + workspace_id: this.workspace, + card_id: card, + message_id: message, + type, + content, + creator, + created } - async removeMessages(card: CardID, ids: MessageID[]): Promise { - const result = await this.removeWithReturn(TableName.Message, { - workspace_id: this.workspace, - card_id: card, - id: ids - }, "id") - return result.map((it: any) => it.id) + const sql = `INSERT INTO ${TableName.Patch} (workspace_id, card_id, message_id, type, content, creator, created) + VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::text, $6::varchar, $7::timestamptz)` + + await this.execute( + sql, + [db.workspace_id, db.card_id, db.message_id, db.type, db.content, db.creator, db.created], + 'insert patch' + ) + } + + async removePatches (card: CardID, fromId: MessageID, toId: MessageID): Promise { + const sql = `DELETE + FROM ${TableName.Patch} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND message_id >= $3::bigint + AND message_id <= $4::bigint;` + + await this.execute(sql, [this.workspace, card, BigInt(fromId), BigInt(toId)], 'remove patches') + } + + // Attachment + async createAttachment (message: MessageID, card: CardID, creator: SocialID, created: Date): Promise { + const db: AttachmentDb = { + message_id: message, + card_id: card, + creator, + created + } + const sql = `INSERT INTO ${TableName.Attachment} (message_id, card_id, creator, created) + VALUES ($1::bigint, $2::varchar, $3::varchar, $4::timestamptz)` + + await this.execute(sql, [db.message_id, db.card_id, db.creator, db.created], 'insert attachment') + } + + async removeAttachment (message: MessageID, card: CardID): Promise { + const sql = `DELETE + FROM ${TableName.Attachment} + WHERE message_id = $1::bigint + AND card_id = $2::varchar` + await this.execute(sql, [message, card], 'remove attachment') + } + + // Reaction + async createReaction ( + card: CardID, + message: MessageID, + reaction: string, + creator: SocialID, + created: Date + ): Promise { + const select = `SELECT m.id + FROM ${TableName.Message} m + WHERE m.id = $1::bigint` + + const messageDb = await this.execute(select, [message], 'select message') + + if (messageDb.length > 0) { + const db: ReactionDb = { + workspace_id: this.workspace, + card_id: card, + message_id: message, + reaction, + creator, + created + } + const sql = `INSERT INTO ${TableName.Reaction} (workspace_id, card_id, message_id, reaction, creator, created) + VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::varchar, $6::timestamptz)` + + await this.execute( + sql, + [db.workspace_id, db.card_id, db.message_id, db.reaction, db.creator, db.created], + 'insert reaction' + ) + } else { + await this.createPatch(card, message, PatchType.addReaction, reaction, creator, created) + } + } + + async removeReaction ( + card: CardID, + message: MessageID, + reaction: string, + creator: SocialID, + created: Date + ): Promise { + const select = `SELECT m.id + FROM ${TableName.Message} m + WHERE m.id = $1::bigint` + + const messageDb = await this.execute(select, [message], 'select message') + + if (messageDb.length > 0) { + const sql = `DELETE + FROM ${TableName.Reaction} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND message_id = $3::bigint + AND reaction = $4::varchar + AND creator = $5::varchar` + await this.execute(sql, [this.workspace, card, message, reaction, creator], 'remove reaction') + } else { + await this.createPatch(card, message, PatchType.removeReaction, reaction, creator, created) + } + } + + // Thread + async createThread (card: CardID, message: MessageID, thread: CardID, created: Date): Promise { + const db: ThreadDb = { + workspace_id: this.workspace, + card_id: card, + message_id: message, + thread_id: thread, + replies_count: 0, + last_reply: created + } + const sql = `INSERT INTO ${TableName.Thread} (workspace_id, card_id, message_id, thread_id, replies_count, + last_reply) + VALUES ($1::uuid, $2::varchar, $3::bigint, $4::varchar, $5::int, $6::timestamptz)` + await this.execute( + sql, + [db.workspace_id, db.card_id, db.message_id, db.thread_id, db.replies_count, db.last_reply], + 'insert thread' + ) + } + + async updateThread (thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise { + if (op === 'increment') { + const sql = `UPDATE ${TableName.Thread} + SET replies_count = replies_count + 1, + last_reply = $3::timestamptz + WHERE workspace_id = $1::uuid + AND thread_id = $2::varchar` + await this.execute(sql, [this.workspace, thread, lastReply], 'update thread') + } else if (op === 'decrement') { + const sql = `UPDATE ${TableName.Thread} + SET replies_count = GREATEST(replies_count - 1, 0) + WHERE workspace_id = $1::uuid + AND thread_id = $2::varchar` + await this.execute(sql, [this.workspace, thread], 'update thread') + } + } + + // MessagesGroup + async createMessagesGroup ( + card: CardID, + blobId: BlobID, + fromDate: Date, + toDate: Date, + fromId: MessageID, + toId: MessageID, + count: number + ): Promise { + const db: MessagesGroupDb = { + workspace_id: this.workspace, + card_id: card, + blob_id: blobId, + from_date: fromDate, + to_date: toDate, + from_id: fromId, + to_id: toId, + count } - async createPatch(card: CardID, message: MessageID, content: RichText, creator: SocialID, created: Date): Promise { - const dbData: PatchDb = { - workspace_id: this.workspace, - card_id: card, - message_id: message, - content: content, - creator: creator, - created: created - } + const sql = `INSERT INTO ${TableName.MessagesGroup} (workspace_id, card_id, blob_id, from_date, to_date, from_id, + to_id, count) + VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz, $6::bigint, $7::bigint, + $8::int)` + await this.execute( + sql, + [db.workspace_id, db.card_id, db.blob_id, db.from_date, db.to_date, db.from_id, db.to_id, db.count], + 'insert messages group' + ) + } - await this.insert(TableName.Patch, dbData) + async removeMessagesGroup (card: CardID, blobId: BlobID): Promise { + const sql = `DELETE + FROM ${TableName.MessagesGroup} + WHERE workspace_id = $1::uuid + AND card_id = $2::varchar + AND blob_id = $3::uuid` + await this.execute(sql, [this.workspace, card, blobId], 'remove messages group') + } + + // Find messages + async find (params: FindMessagesParams): Promise { + // TODO: experiment with select to improve performance + const select = `SELECT m.id, + m.card_id, + m.content, + m.creator, + m.created, + t.thread_id as thread_id, + t.replies_count as replies_count, + t.last_reply as last_reply, + ${this.subSelectPatches()}, + ${this.subSelectReactions()} + FROM ${TableName.Message} m + LEFT JOIN ${TableName.Thread} t + ON t.workspace_id = m.workspace_id AND t.card_id = m.card_id AND + t.message_id = m.id` + + const { where, values } = this.buildMessageWhere(params) + const orderBy = + params.order != null ? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : '' + const limit = params.limit != null ? ` LIMIT ${params.limit}` : '' + const sql = [select, where, orderBy, limit].join(' ') + + const result = await this.execute(sql, values, 'find messages') + + return result.map((it: any) => toMessage(it)) + } + + buildMessageWhere (params: FindMessagesParams): { where: string, values: any[] } { + const where: string[] = ['m.workspace_id = $1::uuid'] + const values: any[] = [this.workspace] + + let index = 2 + + if (params.id != null) { + where.push(`m.id = $${index++}::bigint`) + values.push(params.id) } - //MessagesGroup - async createMessagesGroup(card: CardID, blobId: BlobID, from_id: MessageID, to_id: MessageID, from_date: Date, to_date: Date, count: number): Promise { - const dbData: MessagesGroupDb = { - workspace_id: this.workspace, - card_id: card, - blob_id: blobId, - from_id, - to_id, - from_date, - to_date, - count - } - await this.insert(TableName.MessagesGroup, dbData) + if (params.card != null) { + where.push(`m.card_id = $${index++}::varchar`) + values.push(params.card) } - //Attachment - async createAttachment(message: MessageID, card: CardID, creator: SocialID, created: Date): Promise { - const dbData: AttachmentDb = { - message_id: message, - card_id: card, - creator: creator, - created: created - } - await this.insert(TableName.Attachment, dbData) + const createdCondition = getCondition('m', 'created', index, params.created, 'timestamptz') + + if (createdCondition != null) { + where.push(createdCondition.where) + values.push(createdCondition.value) + index++ } - async removeAttachment(message: MessageID, card: CardID): Promise { - await this.remove(TableName.Attachment, { - message_id: message, - card_id: card - }) + return { where: `WHERE ${where.join(' AND ')}`, values } + } + + subSelectPatches (): string { + return `COALESCE( + (SELECT jsonb_agg(jsonb_build_object( + 'content', p.content, + 'creator', p.creator, + 'created', p.created + ) ORDER BY p.created DESC) + FROM ${TableName.Patch} p + WHERE p.message_id = m.id + AND p.workspace_id = m.workspace_id + AND p.card_id = m.card_id + AND p.type = 'update' + ), '[]'::jsonb) AS patches` + } + + subSelectAttachments (): string { + return `COALESCE( + (SELECT jsonb_agg(jsonb_build_object( + 'card_id', a.card_id, + 'message_id', a.message_id, + 'creator', a.creator, + 'created', a.created + )) + FROM ${TableName.Attachment} a + WHERE a.message_id = m.id + ), '[]'::jsonb) AS attachments` + } + + subSelectReactions (): string { + return `COALESCE( + (SELECT jsonb_agg(jsonb_build_object( + 'message_id', r.message_id, + 'reaction', r.reaction, + 'creator', r.creator, + 'created', r.created + )) + FROM ${TableName.Reaction} r + WHERE r.workspace_id = m.workspace_id + AND r.card_id = m.card_id + AND r.message_id = m.id + ), '[]'::jsonb) AS reactions` + } + + // Find thread + async findThread (thread: CardID): Promise { + const sql = `SELECT t.card_id, + t.message_id, + t.thread_id, + t.replies_count, + t.last_reply + FROM ${TableName.Thread} t + WHERE t.workspace_id = $1::uuid + AND t.thread_id = $2::varchar + LIMIT 1;` + + const result = await this.execute(sql, [this.workspace, thread], 'find thread') + return result.map((it: any) => toThread(it))[0] + } + + // Find messages groups + async findMessagesGroups (params: FindMessagesGroupsParams): Promise { + const select = `SELECT mg.card_id, + mg.blob_id, + mg.from_date, + mg.to_date, + mg.from_id, + mg.to_id, + mg.count, + jsonb_agg(jsonb_build_object( + 'message_id', p.message_id, + 'type', p.type, + 'content', p.content, + 'creator', p.creator, + 'created', p.created + ) ORDER BY p.created) AS patches + FROM ${TableName.MessagesGroup} mg + LEFT JOIN ${TableName.Patch} p + ON p.workspace_id = mg.workspace_id + AND p.card_id = mg.card_id + AND p.message_id BETWEEN mg.from_id AND mg.to_id` + + const { where, values } = this.buildMessagesGroupWhere(params) + const orderBy = + params.orderBy === 'toDate' + ? `ORDER BY mg.to_date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` + : `ORDER BY mg.from_date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` + const groupBy = 'GROUP BY mg.card_id, mg.blob_id, mg.from_date, mg.to_date, mg.from_id, mg.to_id, mg.count' + + const limit = params.limit != null ? ` LIMIT ${params.limit}` : '' + const sql = [select, where, groupBy, orderBy, limit].join(' ') + + const result = await this.execute(sql, values, 'find messages groups') + + return result.map((it: any) => toMessagesGroup(it)) + } + + buildMessagesGroupWhere (params: FindMessagesGroupsParams): { + where: string + values: any[] + } { + const where: string[] = ['mg.workspace_id = $1::uuid'] + const values: any[] = [this.workspace] + + let index = 2 + + if (params.card != null) { + where.push(`mg.card_id = $${index++}::varchar`) + values.push(params.card) } - //Reaction - async createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID, created: Date): Promise { - const dbData: ReactionDb = { - workspace_id: this.workspace, - card_id: card, - message_id: message, - reaction: reaction, - creator: creator, - created: created - } - await this.insert(TableName.Reaction, dbData) + if (params.blobId != null) { + where.push(`mg.blob_id = $${index++}`) + values.push(params.blobId) } - async removeReaction( card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise { - await this.remove(TableName.Reaction, { - workspace_id: this.workspace, - card_id: card, - message_id: message, - reaction: reaction, - creator: creator - }) + const fromDateCondition = getCondition('mg', 'from_date', index, params.fromDate, 'timestamptz') + if (fromDateCondition != null) { + where.push(fromDateCondition.where) + values.push(fromDateCondition.value) + index++ } - //Find messages - async find(params: FindMessagesParams): Promise { - //TODO: experiment with select to improve performance - const select = `SELECT m.id, - m.card_id, - m.content, - m.creator, - m.created, - ${this.subSelectPatches()}, - ${this.subSelectAttachments()}, - ${this.subSelectReactions()} - FROM ${TableName.Message} m` - - const {where, values} = this.buildMessageWhere( params) - const orderBy = params.sort ? `ORDER BY m.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` : '' - const limit = params.limit ? ` LIMIT ${params.limit}` : '' - const sql = [select, where, orderBy, limit].join(' ') - - const result = await this.client.unsafe(sql, values) - - return result.map((it: any) => toMessage(it)) + const toDateCondition = getCondition('mg', 'to_date', index, params.toDate, 'timestamptz') + if (toDateCondition != null) { + where.push(toDateCondition.where) + values.push(toDateCondition.value) + index++ } - buildMessageWhere(params: FindMessagesParams): { where: string, values: any[] } { - const where: string[] = ['m.workspace_id = $1'] - const values: any[] = [this.workspace] - - let index = 2 - - if (params.id != null) { - where.push(`m.id = $${index++}`) - values.push(params.id) - } - - if (params.card != null) { - where.push(`m.card_id = $${index++}`) - values.push(params.card) - } - - if (params.from != null) { - const exclude = params.excluded ?? false - const direction = params.direction ?? Direction.Forward - const getOperator = () => { - if (exclude) { - return direction === Direction.Forward ? '>' : '<' - } else { - return direction === Direction.Forward ? '>=' : '<=' - } - } - - where.push(`m.created ${getOperator()} $${index++}`) - values.push(params.from) - } - - - return {where: `WHERE ${where.join(' AND ')}`, values} - } - - subSelectPatches(): string { - return `array( - SELECT jsonb_build_object( - 'content', p.content, - 'creator', p.creator, - 'created', p.created - ) - FROM ${TableName.Patch} p - WHERE p.message_id = m.id AND p.workspace_id = m.workspace_id AND p.card_id = m.card_id - ) AS patches` - } - - subSelectAttachments(): string { - return `array( - SELECT jsonb_build_object( - 'card_id', a.card_id, - 'message_id', a.message_id, - 'creator', a.creator, - 'created', a.created - ) - FROM ${TableName.Attachment} a - WHERE a.message_id = m.id - ) AS attachments` - } - - subSelectReactions(): string { - return `array( - SELECT jsonb_build_object( - 'message_id', r.message_id, - 'reaction', r.reaction, - 'creator', r.creator, - 'created', r.created - ) - FROM ${TableName.Reaction} r - WHERE r.message_id = m.id AND r.workspace_id = m.workspace_id AND r.card_id = m.card_id - ) AS reactions` - } - - - //Find messages groups - async findGroups(params: FindMessagesGroupsParams): Promise { - const select = `SELECT mg.card_id, - mg.blob_id, - mg.from_id, - mg.to_id, - mg.count - FROM ${TableName.MessagesGroup} mg` - - const {where, values, index} = this.buildMessagesGroupWhere(this.workspace, params) - const orderBy = params.sortBy ? `ORDER BY ${index} ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` : '' - if(params.sortBy) { - values.push(params.sortBy) - } - const limit = params.limit ? ` LIMIT ${params.limit}` : '' - const sql = [select, where, orderBy, limit].join(' ') - - const result = await this.client.unsafe(sql, values) - - return result.map((it: any) => toMessagesGroup(it)) - } - - buildMessagesGroupWhere(workspace: string, params: FindMessagesGroupsParams): { - where: string, - values: any[], - index: number - } { - const where: string[] = ['mg.workspace_id = $1'] - const values: any[] = [workspace] - - let index = 2 - - if (params.card != null) { - where.push(`mg.card_id = $${index++}`) - values.push(params.card) - } - - if (params.blobId != null) { - where.push(`mg.blob_id = $${index++}`) - values.push(params.blobId) - } - - const fromIdCondition = getCondition("mg", "from_id", index, params.fromId); - if (fromIdCondition != null) { - where.push(fromIdCondition.where); - values.push(fromIdCondition.value); - index++; - } - - const toIdCondition = getCondition("mg", "to_id", index, params.toId); - - if (toIdCondition != null) { - where.push(toIdCondition.where); - values.push(toIdCondition.value); - index++; - } - - const fromDateCondition = getCondition("mg", "from_date", index, params.fromDate); - if (fromDateCondition != null) { - where.push(fromDateCondition.where); - values.push(fromDateCondition.value); - index++; - } - - const toDateCondition = getCondition("mg", "to_date", index, params.toDate); - if (toDateCondition != null) { - where.push(toDateCondition.where); - values.push(toDateCondition.value); - index++; - } - - - return {where: `WHERE ${where.join(' AND ')}`, values, index} - } + return { where: `WHERE ${where.join(' AND ')}`, values } + } } - diff --git a/packages/cockroach/src/db/notification.ts b/packages/cockroach/src/db/notification.ts index ba43fa769e..705edc24f7 100644 --- a/packages/cockroach/src/db/notification.ts +++ b/packages/cockroach/src/db/notification.ts @@ -1,95 +1,120 @@ import { - type MessageID, - type ContextID, - type CardID, - type NotificationContext, - type FindNotificationContextParams, SortOrder, - type FindNotificationsParams, type Notification, - type NotificationContextUpdate, - type WorkspaceID + type MessageID, + type ContextID, + type CardID, + type NotificationContext, + type FindNotificationContextParams, + SortingOrder, + type FindNotificationsParams, + type Notification, + type NotificationContextUpdate, + type WorkspaceID } from '@hcengineering/communication-types' -import {BaseDb} from './base.ts' -import {TableName, type ContextDb, type NotificationDb} from './schema.ts' +import { BaseDb } from './base' +import { TableName, type ContextDb, type NotificationDb } from './schema' export class NotificationsDb extends BaseDb { - async createNotification(message: MessageID, context: ContextID): Promise { - const dbData: NotificationDb = { - message_id: message, - context - } - await this.insert(TableName.Notification, dbData) + async createNotification (message: MessageID, context: ContextID): Promise { + const db: NotificationDb = { + message_id: message, + context + } + const sql = `INSERT INTO ${TableName.Notification} (message_id, context_id) + VALUES ($1::bigint, $2::uuid)` + await this.execute(sql, [db.message_id, db.context], 'insert notification') + } + + async removeNotification (message: MessageID, context: ContextID): Promise { + // TODO: do we need to remove notifications? + const sql = `DELETE + FROM ${TableName.NotificationContext} + WHERE id = $1::uuid` + await this.execute(sql, [context], 'remove notification') + } + + async createContext ( + personalWorkspace: WorkspaceID, + card: CardID, + lastView?: Date, + lastUpdate?: Date + ): Promise { + const db: ContextDb = { + workspace_id: this.workspace, + card_id: card, + personal_workspace: personalWorkspace, + last_view: lastView, + last_update: lastUpdate + } + const sql = `INSERT INTO ${TableName.NotificationContext} (workspace_id, card_id, personal_workspace, last_view, last_update) + VALUES ($1::uuid, $2::varchar, $3::uuid, $4::timestamptz, $5::timestamptz) + RETURNING id` + const result = await this.execute( + sql, + [db.workspace_id, db.card_id, db.personal_workspace, db.last_view, db.last_update], + 'insert notification context' + ) + return result[0].id as ContextID + } + + async removeContext (context: ContextID): Promise { + const sql = `DELETE + FROM ${TableName.Notification} + WHERE context = $1::uuid` + await this.execute(sql, [context], 'remove notification context') + } + + async updateContext (context: ContextID, update: NotificationContextUpdate): Promise { + 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 } - async removeNotification(message: MessageID, context: ContextID): Promise { - await this.remove(TableName.Notification, { - message_id: message, - context - }) + if (Object.keys(dbData).length === 0) { + return } - async createContext(personalWorkspace: WorkspaceID,card: CardID, lastView?: Date, lastUpdate?: Date): Promise { - const dbData: ContextDb = { - workspace_id: this.workspace, - card_id: card, - personal_workspace: personalWorkspace, - last_view: lastView, - last_update: lastUpdate - } - return await this.insertWithReturn(TableName.NotificationContext, dbData, 'id') as ContextID - } + const keys = Object.keys(dbData) + const values = Object.values(dbData) - async removeContext(context: ContextID): Promise { - await this.remove(TableName.NotificationContext, { - id: context - }) - } + const sql = `UPDATE ${TableName.NotificationContext} + SET ${keys.map((k, idx) => `"${k}" = $${idx + 1}::timestamptz`).join(', ')} + WHERE id = $${keys.length + 1}::uuid;` + await this.execute(sql, [values, context], 'update notification context') + } - async updateContext(context: ContextID, update: NotificationContextUpdate): Promise { - 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}" = $${idx + 1}`).join(', ')} - WHERE id = $${keys.length + 1}` - - await this.client.unsafe(sql, [values, context]) - } - - async findContexts(params: FindNotificationContextParams, personalWorkspaces: WorkspaceID[], workspace?: WorkspaceID,): Promise { - const select = ` + 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 ? ` LIMIT ${params.limit}` : '' - const sql = [select, where, limit].join(' ') + FROM ${TableName.NotificationContext} nc` + const { where, values } = this.buildContextWhere(params, personalWorkspaces, workspace) + // const orderSql = `ORDER BY nc.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` + const limit = params.limit != null ? ` LIMIT ${params.limit}` : '' + const sql = [select, where, limit].join(' ') - const result = await this.client.unsafe(sql, values); + const result = await this.execute(sql, values, 'find notification contexts') - return result.map(this.toNotificationContext); - } + return result.map((it) => this.toNotificationContext(it)) + } - - async findNotifications(params: FindNotificationsParams, personalWorkspace: WorkspaceID, workspace?: WorkspaceID): Promise { - //TODO: experiment with select to improve performance, should join with attachments and reactions? - const select = ` + 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, @@ -113,115 +138,121 @@ export class NotificationsDb extends BaseDb { FROM ${TableName.Notification} n JOIN ${TableName.NotificationContext} nc ON n.context = nc.id JOIN ${TableName.Message} m ON n.message_id = m.id - `; - const {where, values} = this.buildNotificationWhere(params, personalWorkspace, workspace) - const orderBy = params.sort ? `ORDER BY m.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` : '' - const limit = params.limit ? ` LIMIT ${params.limit}` : '' - const sql = [select, where, orderBy, limit].join(' ') + ` + const { where, values } = this.buildNotificationWhere(params, personalWorkspace, workspace) + const orderBy = + params.order != null ? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : '' + const limit = params.limit != null ? ` LIMIT ${params.limit}` : '' + const sql = [select, where, orderBy, limit].join(' ') - const result = await this.client.unsafe(sql, values); + const result = await this.execute(sql, values, 'find notifications') - return result.map(this.toNotification); - } + return result.map((it) => this.toNotification(it)) + } - buildContextWhere(params: FindNotificationContextParams, personalWorkspaces: WorkspaceID[], workspace?: WorkspaceID): { - where: string, - values: any[] + buildContextWhere ( + params: FindNotificationContextParams, + personalWorkspaces: WorkspaceID[], + workspace?: WorkspaceID + ): { + where: string + values: any[] } { - const where: string[] = [] - const values: any[] = [] - let index = 1 + const where: string[] = [] + const values: any[] = [] + let index = 1 - if (workspace != null) { - where.push(`nc.workspace_id = $${index++}`) - values.push(workspace) - } - - if (personalWorkspaces.length > 0) { - where.push(`nc.personal_workspace IN (${personalWorkspaces.map((it) => `$${index++}`).join(', ')})`) - values.push(...personalWorkspaces) - } - - if (params.card != null) { - where.push(`nc.card_id = $${index++}`) - values.push(params.card) - } - - return {where: `WHERE ${where.join(' AND ')}`, values} + if (workspace != null) { + where.push(`nc.workspace_id = $${index++}::uuid`) + values.push(workspace) } - buildNotificationWhere(params: FindNotificationsParams, personalWorkspace: WorkspaceID, workspace?: WorkspaceID): { - where: string, - values: any[] + if (personalWorkspaces.length > 0) { + where.push(`nc.personal_workspace IN (${personalWorkspaces.map((it) => `$${index++}::uuid`).join(', ')})`) + values.push(...personalWorkspaces) + } + + if (params.card != null) { + where.push(`nc.card_id = $${index++}::varchar`) + values.push(params.card) + } + + return { where: `WHERE ${where.join(' AND ')}`, values } + } + + buildNotificationWhere ( + params: FindNotificationsParams, + personalWorkspace: WorkspaceID, + workspace?: WorkspaceID + ): { + where: string + values: any[] } { - const where: string[] = ['nc.personal_workspace = $1'] - const values: any[] = [personalWorkspace] - let index = 2 + const where: string[] = ['nc.personal_workspace = $1::uuid'] + const values: any[] = [personalWorkspace] + let index = 2 - if (workspace != null) { - where.push(`nc.workspace_id = $${index++}`) - values.push(workspace) - } - - if (params.context != null) { - where.push(`n.context = $${index++}`) - values.push(params.context) - } - - - if (params.read === true) { - where.push(`nc.last_view IS NOT NULL AND nc.last_view >= m.created`) - } - - if (params.read === false) { - where.push(`(nc.last_view IS NULL OR nc.last_view > m.created)`) - } - - if (params.archived === true) { - where.push(`nc.archived_from IS NOT NULL AND nc.archived_from >= m.created`) - } - - if (params.archived === false) { - where.push(`(nc.archived_from IS NULL OR nc.archived_from > m.created)`) - } - - - return {where: `WHERE ${where.join(' AND ')}`, values} + if (workspace != null) { + where.push(`nc.workspace_id = $${index++}::uuid`) + values.push(workspace) } - toNotificationContext(row: any): NotificationContext { - return { - id: row.id, - card: row.card_id, - workspace: row.workspace_id, - personalWorkspace: row.personal_workspace, - archivedFrom: row.archived_from ? new Date(row.archived_from) : undefined, - lastView: row.last_view ? new Date(row.last_view) : undefined, - lastUpdate: row.last_update ? new Date(row.last_update) : undefined - } + if (params.context != null) { + where.push(`n.context = $${index++}::uuid`) + values.push(params.context) } - toNotification(row: any): Notification { - const lastPatch = row.patches?.[0] - const lastView = row.last_view ? new Date(row.last_view) : undefined - const archivedFrom = row.archived_from ? new Date(row.archived_from) : undefined - const created = new Date(row.message_created) - - return { - message: { - id: row.id, - card: row.message_card, - content: lastPatch?.content ?? row.message_content, - creator: row.message_creator, - created, - edited: new Date(lastPatch?.created ?? row.message_created), - reactions: row.reactions ?? [], - attachments: row.attachments ?? [] - }, - context: row.context, - read: lastView != null && lastView >= created, - archived: archivedFrom != null && archivedFrom >= created - } + if (params.read === true) { + where.push('nc.last_view IS NOT NULL AND nc.last_view >= m.created') } + + if (params.read === false) { + where.push('(nc.last_view IS NULL OR nc.last_view > m.created)') + } + + if (params.archived === true) { + where.push('nc.archived_from IS NOT NULL AND nc.archived_from >= m.created') + } + + if (params.archived === false) { + where.push('(nc.archived_from IS NULL OR nc.archived_from > m.created)') + } + + return { where: `WHERE ${where.join(' AND ')}`, values } + } + + toNotificationContext (row: any): NotificationContext { + return { + id: row.id, + card: row.card_id, + workspace: row.workspace_id, + personalWorkspace: row.personal_workspace, + archivedFrom: row.archived_from != null ? new Date(row.archived_from) : undefined, + lastView: row.last_view != null ? new Date(row.last_view) : undefined, + lastUpdate: row.last_update != null ? new Date(row.last_update) : undefined + } + } + + toNotification (row: any): Notification { + const lastPatch = row.patches?.[0] + const lastView = row.last_view != null ? new Date(row.last_view) : undefined + const archivedFrom = row.archived_from != null ? new Date(row.archived_from) : undefined + const created = new Date(row.message_created) + + return { + message: { + id: row.id, + card: row.message_card, + content: lastPatch?.content ?? row.message_content, + creator: row.message_creator, + created, + edited: new Date(lastPatch?.created ?? row.message_created), + reactions: row.reactions ?? [], + attachments: row.attachments ?? [] + }, + context: row.context, + read: lastView != null && lastView >= created, + archived: archivedFrom != null && archivedFrom >= created + } + } } - diff --git a/packages/cockroach/src/db/schema.ts b/packages/cockroach/src/db/schema.ts index 755cadc9e8..1dfc6578c2 100644 --- a/packages/cockroach/src/db/schema.ts +++ b/packages/cockroach/src/db/schema.ts @@ -1,122 +1,183 @@ -import type {ContextID, MessageID, RichText, SocialID, CardID, BlobID, Message, Reaction, Attachment, MessagesGroup, WorkspaceID } from "@hcengineering/communication-types" +import { + type ContextID, + type MessageID, + type RichText, + type SocialID, + type CardID, + type BlobID, + type Message, + type Reaction, + type Attachment, + type MessagesGroup, + type WorkspaceID, + type PatchType, + type Patch, + type Thread +} from '@hcengineering/communication-types' export enum TableName { - Message = 'communication.messages', - MessagesGroup = 'communication.messages_groups', - Patch = 'communication.patch', - Attachment = 'communication.attachments', - Reaction = 'communication.reactions', - Notification = 'communication.notifications', - NotificationContext = 'communication.notification_context' + Attachment = 'communication.attachments', + Message = 'communication.messages', + MessagesGroup = 'communication.messages_groups', + Notification = 'communication.notifications', + NotificationContext = 'communication.notification_context', + Patch = 'communication.patch', + Reaction = 'communication.reactions', + Thread = 'communication.thread' } export interface MessageDb { - id: MessageID, - workspace_id: WorkspaceID, - card_id: CardID, - content: RichText, - creator: SocialID, - created: Date, + id: MessageID + workspace_id: WorkspaceID + card_id: CardID + content: RichText + creator: SocialID + created: Date + thread_id?: CardID + replies_count?: number + last_reply?: Date } export interface MessagesGroupDb { - workspace_id: WorkspaceID, - card_id: CardID, - blob_id: BlobID, - from_id: MessageID, - to_id: MessageID, - from_date: Date, - to_date: Date, - count: number + workspace_id: WorkspaceID + card_id: CardID + blob_id: BlobID + from_date: Date + to_date: Date + from_id: MessageID + to_id: MessageID + count: number + patches?: PatchDb[] } export interface PatchDb { - workspace_id: WorkspaceID, - card_id: CardID, - message_id: MessageID, - content: RichText, - creator: SocialID, - created: Date, + workspace_id: WorkspaceID + card_id: CardID + message_id: MessageID + type: PatchType + content: RichText + creator: SocialID + created: Date } export interface ReactionDb { - workspace_id: WorkspaceID, - card_id: CardID, - message_id: MessageID, - reaction: string, - creator: SocialID - created: Date + workspace_id: WorkspaceID + card_id: CardID + message_id: MessageID + reaction: string + creator: SocialID + created: Date } export interface AttachmentDb { - message_id: MessageID, - card_id: CardID, - creator: SocialID - created: Date + message_id: MessageID + card_id: CardID + creator: SocialID + created: Date +} + +export interface ThreadDb { + workspace_id: WorkspaceID + card_id: CardID + message_id: MessageID + thread_id: CardID + replies_count: number + last_reply: Date } export interface NotificationDb { - message_id: MessageID, - context: ContextID + message_id: MessageID + context: ContextID } export interface ContextDb { - workspace_id: WorkspaceID - card_id: CardID - personal_workspace: WorkspaceID + workspace_id: WorkspaceID + card_id: CardID + personal_workspace: WorkspaceID - archived_from?: Date - last_view?: Date - last_update?: Date + archived_from?: Date + last_view?: Date + last_update?: Date } - interface RawMessage extends MessageDb { - patches?: PatchDb[] - attachments?: AttachmentDb[] - reactions?: ReactionDb[] + patches?: PatchDb[] + attachments?: AttachmentDb[] + reactions?: ReactionDb[] } -export function toMessage(raw: RawMessage): Message { - const lastPatch = raw.patches?.[0] - return { - id: raw.id, - card: raw.card_id, - content: lastPatch?.content ?? raw.content, - creator: raw.creator, - created: new Date(raw.created), - edited: lastPatch?.created ? new Date(lastPatch.created) : undefined, - reactions: (raw.reactions ?? []).map(toReaction), - attachments: (raw.attachments ?? []).map(toAttachment) - } +export function toMessage (raw: RawMessage): Message { + const lastPatch = raw.patches?.[0] + + return { + id: String(raw.id) as MessageID, + card: raw.card_id, + content: lastPatch?.content ?? raw.content, + creator: raw.creator, + created: raw.created, + edited: lastPatch?.created ?? undefined, + thread: + raw.thread_id != null + ? { + card: raw.card_id, + message: String(raw.id) as MessageID, + thread: raw.thread_id, + repliesCount: raw.replies_count ?? 0, + lastReply: raw.last_reply ?? new Date() + } + : undefined, + reactions: (raw.reactions ?? []).map(toReaction), + attachments: (raw.attachments ?? []).map(toAttachment) + } } -export function toReaction(raw: ReactionDb): Reaction { - return { - message: raw.message_id, - reaction: raw.reaction, - creator: raw.creator, - created: new Date(raw.created) - } +export function toReaction (raw: ReactionDb): Reaction { + return { + message: String(raw.message_id) as MessageID, + reaction: raw.reaction, + creator: raw.creator, + created: raw.created + } } -export function toAttachment(raw: AttachmentDb): Attachment { - return { - message: raw.message_id, - card: raw.card_id, - creator: raw.creator, - created: new Date(raw.created) - } +export function toAttachment (raw: AttachmentDb): Attachment { + return { + message: String(raw.message_id) as MessageID, + card: raw.card_id, + creator: raw.creator, + created: raw.created + } } -export function toMessagesGroup(raw: MessagesGroupDb): MessagesGroup { - return { - card: raw.card_id, - blobId: raw.blob_id, - fromId: raw.from_id, - toId: raw.to_id, - fromDate: new Date(raw.from_date), - toDate: new Date(raw.to_date), - count: raw.count - } -} \ No newline at end of file +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 + } +} diff --git a/packages/cockroach/src/db/utils.ts b/packages/cockroach/src/db/utils.ts index 4b56ad3f86..9f4ac0a1b1 100644 --- a/packages/cockroach/src/db/utils.ts +++ b/packages/cockroach/src/db/utils.ts @@ -1,22 +1,28 @@ -export function getCondition(table: string, dbField: string, index: number, param: any): { where: string, value: any } | undefined { - if (typeof param === 'object') { - if (param.less != null) { - return {where: `${table}.${dbField} < $${index}`, value: param.less}; - } - if (param.lessOrEqual != null) { - return {where: `${table}.${dbField} <= $${index}`, value: param.lessOrEqual}; - } - if (param.greater != null) { - return {where: `${table}.${dbField} > $${index}`, value: param.greater}; - } - if (param.greaterOrEqual != null) { - return {where: `${table}.${dbField} >= $${index}`, value: param.greaterOrEqual}; - } +export function getCondition ( + table: string, + dbField: string, + index: number, + param: any, + type: string +): { where: string, value: any } | undefined { + if (typeof param === 'object') { + if (param.less != null) { + return { where: `${table}.${dbField} < $${index}::${type}`, value: param.less } } - - if(param != null) { - return {where: `${table}.${dbField} = $${index}`, value: param}; + if (param.lessOrEqual != null) { + return { where: `${table}.${dbField} <= $${index}::${type}`, value: param.lessOrEqual } } + if (param.greater != null) { + return { where: `${table}.${dbField} > $${index}::${type}`, value: param.greater } + } + if (param.greaterOrEqual != null) { + return { where: `${table}.${dbField} >= $${index}::${type}`, value: param.greaterOrEqual } + } + } - return undefined -} \ No newline at end of file + if (param != null) { + return { where: `${table}.${dbField} = $${index}::${type}`, value: param } + } + + return undefined +} diff --git a/packages/cockroach/src/index.ts b/packages/cockroach/src/index.ts index 03eeab5ffa..32544c6dfa 100644 --- a/packages/cockroach/src/index.ts +++ b/packages/cockroach/src/index.ts @@ -1 +1 @@ -export * from './adapter.ts' +export * from './adapter' diff --git a/packages/cockroach/src/types.ts b/packages/cockroach/src/types.ts new file mode 100644 index 0000000000..d15e572af3 --- /dev/null +++ b/packages/cockroach/src/types.ts @@ -0,0 +1,16 @@ +import { type ParameterOrJSON, type Row } from 'postgres' + +export interface Logger { + info: (message: string, data?: Record) => void + warn: (message: string, data?: Record) => void + error: (message: string, data?: Record) => void +} + +export interface Options { + withLogs?: boolean +} + +export interface SqlClient { + execute: )[]>(query: string, params?: ParameterOrJSON[]) => Promise + close: () => void +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts deleted file mode 100644 index 0560e4bd55..0000000000 --- a/packages/core/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './message.ts' diff --git a/packages/core/src/message.ts b/packages/core/src/message.ts deleted file mode 100644 index 48a43227a5..0000000000 --- a/packages/core/src/message.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { MessageID } from '@hcengineering/communication-types' - -let lastTimestamp = Math.floor(Date.now() / 1000) -let counter = 0 - -export function generateMessageId(): MessageID { - const timestamp = Math.floor(Date.now() / 1000) - - if (timestamp !== lastTimestamp) { - lastTimestamp = timestamp - counter = 0 - } - - counter++ - - return ((timestamp << 24) | counter) as MessageID -} - -export function parseMessageId(id: MessageID): { timestamp: number; counter: number } { - const timestamp = id >> 24 - const counter = id & 0xffffff - - return { timestamp, counter } -} diff --git a/packages/examples/package.json b/packages/examples/package.json index 9dfa63f469..e79ec57aa2 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/communication-examples", - "version": "0.1.9", + "version": "0.1.47", "main": "dist/index.js", "module": "dist/index.js", "types": "./types/index.d.ts", @@ -13,9 +13,7 @@ "@types/bun": "^1.1.14" }, "dependencies": { - "@hcengineering/communication-client-query": "workspace:*", "@hcengineering/communication-client-ws": "workspace:*", - "@hcengineering/communication-client-sqlite": "workspace:*", "@hcengineering/communication-types": "workspace:*" }, "peerDependencies": { diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index 414362ed14..d72db680f7 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -1,39 +1,33 @@ import { type Message, type SocialID, - SortOrder, + SortingOrder, type CardID, type Window, type WorkspaceID } from '@hcengineering/communication-types' -import { getWebsocketClient } from '@hcengineering/communication-client-ws' -import { getSqliteClient } from '@hcengineering/communication-client-sqlite' -import { createMessagesQuery, initLiveQueries } from '@hcengineering/communication-client-query' +import { getWebsocketClient, createMessagesQuery } from '@hcengineering/communication-client-ws' const card = 'cd0aba36-1c4f-4170-95f2-27a12a5415f6' as CardID const workspace = 'cd0aba36-1c4f-4170-95f2-27a12a5415f6' as WorkspaceID -const personalWorkspace = 'cd0aba36-1c4f-4170-95f2-27a12a5415f5' as WorkspaceID +// const personalWorkspace = 'cd0aba36-1c4f-4170-95f2-27a12a5415f5' as WorkspaceID const creator1 = 'email:vasya@huly.com' as SocialID -async function getClient(type: 'ws' | 'sqlite') { - if (type === 'ws') { - const platformUrl = 'ws://localhost:8090' - const token = 'token' - return await getWebsocketClient(platformUrl, token) - } - - return await getSqliteClient(workspace, personalWorkspace) -} - export async function example() { - const client = await getClient('sqlite') - initLiveQueries(client) + const platformUrl = 'ws://localhost:8090' + const token = 'token' + const client = await getWebsocketClient( + platformUrl, + token, + workspace, + 'http://localhost:4022/blob/:workspace/:blobId/:filename' + ) const query1 = createMessagesQuery() let window: Window | undefined = undefined - query1.query({ card, sort: SortOrder.Desc }, (res) => { + query1.query({ card, order: SortingOrder.Descending }, (res) => { window = res const r = window.getResult() r.reverse() @@ -51,7 +45,7 @@ export async function example() { }) async function editMessage(message: Message) { - await client.createPatch(card, message.id, message.content + '_1_', creator1) + await client.updateMessage(card, message.id, message.content + '_1_', creator1) } async function deleteMessage(message: Message) { diff --git a/packages/query/package.json b/packages/query/package.json index 1a3669fcb8..87dbd672de 100644 --- a/packages/query/package.json +++ b/packages/query/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/communication-query", - "version": "0.1.9", + "version": "0.1.47", "main": "dist/index.js", "module": "dist/index.js", "types": "./types/index.d.ts", @@ -11,11 +11,12 @@ ], "devDependencies": { "@types/bun": "^1.1.14", - "@types/crypto-js": "^4.2.2" + "@types/crypto-js": "^4.2.2" }, "dependencies": { - "@hcengineering/communication-types": "workspace:*", "@hcengineering/communication-sdk-types": "workspace:*", + "@hcengineering/communication-shared": "workspace:*", + "@hcengineering/communication-types": "workspace:*", "fast-equals": "^5.0.1" }, "peerDependencies": { diff --git a/packages/query/src/lq.ts b/packages/query/src/lq.ts index 9dbae0845b..44563787ec 100644 --- a/packages/query/src/lq.ts +++ b/packages/query/src/lq.ts @@ -1,4 +1,8 @@ -import { type FindMessagesParams, type FindNotificationsParams } from '@hcengineering/communication-types' +import { + type FindMessagesParams, + type FindNotificationsParams, + type WorkspaceID +} from '@hcengineering/communication-types' import { deepEqual } from 'fast-equals' import type { MessagesQueryCallback, @@ -7,7 +11,7 @@ import type { QueryClient } from '@hcengineering/communication-sdk-types' -import type { Query, QueryId } from './types' +import type { FindParams, PagedQuery, QueryId } from './types' import { MessagesQuery } from './messages/query' import { NotificationQuery } from './notifications/query' @@ -18,14 +22,15 @@ interface CreateQueryResult { const maxQueriesCache = 10 export class LiveQueries { - private readonly client: QueryClient - private readonly queries = new Map() + private readonly queries = new Map() private readonly unsubscribed = new Set() private counter: number = 0 - constructor(client: QueryClient) { - this.client = client - } + constructor( + private readonly client: QueryClient, + private readonly workspace: WorkspaceID, + private readonly filesUrl: string + ) {} async onEvent(event: ResponseEvent): Promise { for (const q of this.queries.values()) { @@ -66,11 +71,11 @@ export class LiveQueries { return exists } else { const result = exists.copyResult() - return new MessagesQuery(this.client, id, params, callback, result) + return new MessagesQuery(this.client, this.workspace, this.filesUrl, id, params, callback, result) } } - return new MessagesQuery(this.client, id, params, callback) + return new MessagesQuery(this.client, this.workspace, this.filesUrl, id, params, callback) } private createNotificationQuery( @@ -103,7 +108,7 @@ export class LiveQueries { } } - private findNotificationQuery(params: FindMessagesParams): NotificationQuery | undefined { + private findNotificationQuery(params: FindNotificationsParams): NotificationQuery | undefined { for (const query of this.queries.values()) { if (query instanceof NotificationQuery) { if (!this.queryCompare(params, query.params)) continue @@ -112,7 +117,7 @@ export class LiveQueries { } } - private queryCompare(q1: FindMessagesParams, q2: FindMessagesParams): boolean { + private queryCompare(q1: FindParams, q2: FindParams): boolean { if (Object.keys(q1).length !== Object.keys(q2).length) { return false } @@ -136,7 +141,7 @@ export class LiveQueries { this.unsubscribed.delete(id) } - private unsubscribeQuery(query: Query): void { + private unsubscribeQuery(query: PagedQuery): void { this.unsubscribed.add(query.id) query.removeCallback() if (this.unsubscribed.size > maxQueriesCache) { diff --git a/packages/query/src/messages/query.ts b/packages/query/src/messages/query.ts index 08279eeaaf..073527450b 100644 --- a/packages/query/src/messages/query.ts +++ b/packages/query/src/messages/query.ts @@ -1,201 +1,693 @@ import { - type FindMessagesParams, - type MessageID, - type Message, - type Patch, - SortOrder + type FindMessagesParams, + type Message, + type MessagesGroup, + type WorkspaceID, + type MessageID, + SortingOrder, + type Patch, + type ParsedFile, + PatchType, + type CardID } from '@hcengineering/communication-types' import { - type AttachmentCreatedEvent, - type MessageCreatedEvent, - type PatchCreatedEvent, - type ReactionCreatedEvent, - type ResponseEvent, - ResponseEventType, - type AttachmentRemovedEvent, - type MessageRemovedEvent, - type ReactionRemovedEvent + ResponseEventType, + type AttachmentCreatedEvent, + type AttachmentRemovedEvent, + type MessageCreatedEvent, + type MessageRemovedEvent, + type PatchCreatedEvent, + type QueryCallback, + type QueryClient, + type ReactionCreatedEvent, + type ReactionRemovedEvent, + type ResponseEvent, + type ThreadCreatedEvent } from '@hcengineering/communication-sdk-types' +import { loadGroupFile, parseMessageId } from '@hcengineering/communication-shared' -import { BaseQuery } from '../query' +import { QueryResult } from '../result' +import { defaultQueryParams, Direction, type QueryId, type PagedQuery } from '../types' +import { WindowImpl } from '../window' +import { addReaction, addReply, removeReaction, removeReply } from './utils' -export class MessagesQuery extends BaseQuery { - override async find(params: FindMessagesParams): Promise { - return this.client.findMessages(params, this.id) +const GROUPS_LIMIT = 20 + +export class MessagesQuery implements PagedQuery { + protected result: Promise> | QueryResult + + private messagesFromFiles: Message[] = [] + + private readonly groupsBuffer: MessagesGroup[] = [] + + private firstGroup?: MessagesGroup + private lastGroup?: MessagesGroup + + private readonly limit: number + + private readonly next = { + hasMessages: true, + hasGroups: true + } + + private readonly prev = { + hasMessages: true, + hasGroups: true + } + + constructor ( + protected readonly client: QueryClient, + private readonly workspace: WorkspaceID, + private readonly filesUrl: string, + public readonly id: QueryId, + public readonly params: FindMessagesParams, + private callback?: QueryCallback, + initialResult?: QueryResult + ) { + const baseLimit = params.id != null ? 1 : this.params.limit ?? defaultQueryParams.limit + this.limit = baseLimit + 1 + this.params = { + ...params, + order: params.order ?? defaultQueryParams.order } + if (initialResult !== undefined) { + const messages = initialResult.getResult() + const count = messages.length - override getObjectId(object: Message): MessageID { - return object.id - } - - override getObjectDate(object: Message): Date { - return object.created - } - - override async onEvent(event: ResponseEvent): Promise { - switch (event.type) { - case ResponseEventType.MessageCreated: - return await this.onCreateMessageEvent(event) - case ResponseEventType.MessageRemoved: - return await this.onRemoveMessageEvent(event) - case ResponseEventType.PatchCreated: - return await this.onCreatePatchEvent(event) - case ResponseEventType.ReactionCreated: - return await this.onCreateReactionEvent(event) - case ResponseEventType.ReactionRemoved: - return await this.onRemoveReactionEvent(event) - case ResponseEventType.AttachmentCreated: - return await this.onCreateAttachmentEvent(event) - case ResponseEventType.AttachmentRemoved: - return await this.onRemoveAttachmentEvent(event) + if (count < this.limit) { + this.result = initialResult + } else { + if (this.params.order === SortingOrder.Ascending) { + this.result = new QueryResult(messages.slice(0, baseLimit), (x) => x.id) + this.result.setHead(true) + this.result.setTail(false) + } else { + this.result = new QueryResult(messages.slice(0, baseLimit), (x) => x.id) + this.result.setHead(false) + this.result.setTail(true) } + } + void this.notify() + } else { + this.result = new QueryResult([] as Message[], (x) => x.id) + + if (this.isInitLoadingForward()) { + this.result.setHead(true) + void this.requestLoadNextPage() + } else { + this.result.setTail(true) + void this.requestLoadPrevPage() + } + } + } + + setCallback (callback: QueryCallback): void { + this.callback = callback + void this.notify() + } + + removeCallback (): void { + this.callback = () => {} + } + + async requestLoadNextPage (): Promise { + if (this.result instanceof Promise) this.result = await this.result + + if (!this.result.isTail()) { + this.result = this.loadPage(Direction.Forward, this.result) + void this.result + .then(() => this.notify()) + .catch((error) => { + console.error('Failed to load messages', error) + }) + } + } + + async requestLoadPrevPage (): Promise { + if (this.result instanceof Promise) this.result = await this.result + if (!this.result.isHead()) { + this.result = this.loadPage(Direction.Backward, this.result) + void this.result + .then(() => this.notify()) + .catch((error) => { + console.error('Failed to load messages', error) + }) + } + } + + private isInitLoadingForward (): boolean { + const { order, created, id } = this.params + + if (id != null) { + return false } - async onCreateMessageEvent(event: MessageCreatedEvent): Promise { - if (this.result instanceof Promise) { - this.result = await this.result - } + if (created == null) return order === SortingOrder.Ascending + if (created instanceof Date) return order === SortingOrder.Ascending + // TODO: fix me + if (created.less != null) return order !== SortingOrder.Ascending + if (created.lessOrEqual != null) return order !== SortingOrder.Ascending + if (created.greater != null) return order === SortingOrder.Ascending + if (created.greaterOrEqual != null) return order === SortingOrder.Ascending - const message = { - ...event.message, - edited: event.message.edited, - created: event.message.created - } - const exists = this.result.get(message.id) + return false + } - if (exists !== undefined) return - if (!this.match(message)) return + private async loadPage (direction: Direction, result: QueryResult): Promise> { + const { messages, fromDb } = + direction === Direction.Forward ? await this.loadNextMessages(result) : await this.loadPrevMessages(result) - if (this.result.isTail()) { - if (this.params.sort === SortOrder.Asc) { - this.result.push(message) - } else { - this.result.unshift(message) + if (!result.isHead() && direction === Direction.Backward) { + result.setHead(messages.length < this.limit) + } + if (!result.isTail() && direction === Direction.Forward) { + result.setTail(messages.length < this.limit) + } + + if (messages.length === this.limit && this.limit > 1) { + const lastMessage = messages.pop() + if (lastMessage != null && !fromDb) { + direction === Direction.Forward + ? this.messagesFromFiles.unshift(lastMessage) + : this.messagesFromFiles.push(lastMessage) + } + } + + result.append(messages) + + return result + } + + // Load next + private async loadNextMessages (result: QueryResult): Promise<{ messages: Message[], fromDb: boolean }> { + const messages: Message[] = this.messagesFromFiles.splice(0, this.limit) + + if (messages.length >= this.limit) return { messages, fromDb: false } + + while (this.next.hasGroups || this.groupsBuffer.length > 0) { + await this.loadGroups(Direction.Forward, result) + + messages.push(...this.messagesFromFiles.splice(0, this.limit - messages.length)) + + if (messages.length >= this.limit) return { messages, fromDb: false } + } + + const dbMessages = await this.findNextMessages(this.limit - messages.length, result) + this.next.hasMessages = dbMessages.length > 0 + messages.push(...dbMessages) + return { messages, fromDb: dbMessages.length > 0 } + } + + private async findNextMessages (limit: number, result: QueryResult): Promise { + if (this.next.hasGroups) { + return [] + } + + if (result.isTail()) return [] + + const last = result.getLast() + + return await this.find({ + ...this.params, + created: + last != null + ? { + greater: last?.created } - await this.notify() - } + : undefined, + limit, + order: SortingOrder.Ascending + }) + } + + // Load prev + private async loadPrevMessages (result: QueryResult): Promise<{ messages: Message[], fromDb: boolean }> { + const messages: Message[] = [] + + if (this.prev.hasMessages) { + const prevMessages = await this.findPrevMessages(this.limit, result) + this.prev.hasMessages = prevMessages.length > 0 + messages.push(...prevMessages) } - private match(message: Message): boolean { - if (this.params.id != null && this.params.id !== message.id) { - return false - } - if (this.params.card != null && this.params.card !== message.card) { - return false - } - return true + if (messages.length >= this.limit) return { messages, fromDb: true } + + const restLimit = this.limit - messages.length + const fromBuffer = this.messagesFromFiles.splice(-restLimit, restLimit).reverse() + messages.push(...fromBuffer) + + if (messages.length >= this.limit) return { messages, fromDb: false } + + while (this.prev.hasGroups || this.groupsBuffer.length > 0) { + await this.loadGroups(Direction.Backward, result) + + const rest = this.limit - messages.length + const fromBuffer2 = this.messagesFromFiles.splice(-rest, rest).reverse() + + messages.push(...fromBuffer2) + if (messages.length >= this.limit) return { messages, fromDb: false } } - private async onCreatePatchEvent(event: PatchCreatedEvent): Promise { - if (this.result instanceof Promise) { - this.result = await this.result + return { messages, fromDb: false } + } + + private async findPrevMessages (limit: number, result: QueryResult): Promise { + if (!this.prev.hasMessages || result.isHead()) return [] + + const first = result.getLast() + + return await this.find({ + ...this.params, + created: + first != null + ? { + less: first?.created + } + : undefined, + limit, + order: SortingOrder.Descending + }) + } + + private async loadGroups (direction: Direction, result: QueryResult): Promise { + let messagesCount = 0 + const lastResult = result.getLast() + const toLoad: MessagesGroup[] = [] + const toBuffer: MessagesGroup[] = [] + + while (messagesCount < this.limit) { + const currentGroups = this.groupsBuffer.splice(direction === Direction.Forward ? 0 : -GROUPS_LIMIT, GROUPS_LIMIT) + const hasGroups = direction === Direction.Forward ? this.next.hasGroups : this.prev.hasGroups + if (currentGroups.length === 0 && !hasGroups) break + + const groups = + currentGroups.length > 0 + ? currentGroups + : await this.findGroups( + direction, + direction === Direction.Forward ? this.lastGroup?.fromDate : this.firstGroup?.fromDate + ) + + if (currentGroups.length === 0) { + this.firstGroup = direction === Direction.Forward ? this.firstGroup ?? groups[0] : groups[groups.length - 1] + this.lastGroup = + direction === Direction.Forward ? groups[groups.length - 1] ?? this.lastGroup : this.lastGroup ?? groups[0] + + if (direction === Direction.Forward) { + this.next.hasGroups = groups.length >= GROUPS_LIMIT + } else { + this.prev.hasGroups = groups.length >= GROUPS_LIMIT } - - const patch = { - ...event.patch, - created: event.patch.created + if (this.params.id != null) { + this.next.hasGroups = false + this.prev.hasGroups = false } + } - const message = this.result.get(patch.message) + const orderedGroups = direction === Direction.Forward ? groups : groups.reverse() + while (messagesCount < this.limit && orderedGroups.length > 0) { + const group = direction === Direction.Forward ? orderedGroups.shift() : orderedGroups.pop() + if (group == null) break + toLoad.push(group) + messagesCount += group.count + } - if (message === undefined) return - - if (message.created < patch.created) { - this.result.update(this.applyPatch(message, patch)) - await this.notify() - } + while (orderedGroups.length > 0) { + const group = direction === Direction.Forward ? orderedGroups.shift() : orderedGroups.pop() + if (group == null) break + toBuffer.push(group) + messagesCount += group.count + } } - private async onRemoveMessageEvent(event: MessageRemovedEvent): Promise { - if (this.result instanceof Promise) { - this.result = await this.result - } - - const deleted = this.result.delete(event.message) - - if (deleted !== undefined) { - await this.notify() - } + if (direction === Direction.Forward) { + this.groupsBuffer.push(...toBuffer) + } else { + this.groupsBuffer.unshift(...toBuffer) } - private async onCreateReactionEvent(event: ReactionCreatedEvent): Promise { - if (this.result instanceof Promise) { - this.result = await this.result - } + const parsedFiles = await Promise.all(toLoad.map((group) => this.loadMessagesFromFiles(group))) - const reaction = { - ...event.reaction, - created: event.reaction.created - } - const message = this.result.get(reaction.message) - if (message === undefined) return + for (const file of parsedFiles) { + if (file.messages.length === 0) continue + if (direction === Direction.Forward) { + const firstInFile = file.messages[0] + const queryDate = + lastResult != null && firstInFile.created < lastResult?.created ? lastResult?.created : undefined + this.messagesFromFiles.push(...this.matchFileMessages(file, queryDate)) + } else { + const lastInFile = file.messages[file.messages.length - 1] + const queryDate = + lastResult != null && lastInFile.created > lastResult?.created ? lastResult?.created : undefined + this.messagesFromFiles.unshift(...this.matchFileMessages(file, queryDate)) + } + } + } - message.reactions.push(reaction) - this.result.update(message) - await this.notify() + private matchFileMessages (file: ParsedFile, created?: Date): Message[] { + let result: Message[] = file.messages + if (this.params.id != null) { + const msg = file.messages.find((it) => it.id === this.params.id) + result = msg != null ? [msg] : [] } - private async onRemoveReactionEvent(event: ReactionRemovedEvent): Promise { - if (this.result instanceof Promise) { - this.result = await this.result - } - - const message = this.result.get(event.message) - if (message === undefined) return - - const reactions = message.reactions.filter((it) => it.reaction !== event.reaction && it.creator !== event.creator) - if (reactions.length === message.reactions.length) return - - const updated = { - ...message, - reactions - } - this.result.update(updated) - await this.notify() + if (created != null) { + result = + this.params.order === SortingOrder.Ascending + ? result.filter((it) => it.created > created) + : result.filter((it) => it.created < created) } - private async onCreateAttachmentEvent(event: AttachmentCreatedEvent): Promise { - if (this.result instanceof Promise) { - this.result = await this.result - } + return result + } - const attachment = { - ...event.attachment, - created: event.attachment.created - } - const message = this.result.get(attachment.message) - if (message === undefined) return + private async loadMessagesFromFiles (group: MessagesGroup): Promise { + const parsedFile = await loadGroupFile(this.workspace, this.filesUrl, group, { retries: 5 }) - message.attachments.push(attachment) - this.result.update(message) - await this.notify() + const patches = group.patches ?? [] + + const patchesMap = new Map() + for (const patch of patches) { + patchesMap.set(patch.message, [...(patchesMap.get(patch.message) ?? []), patch]) } - private async onRemoveAttachmentEvent(event: AttachmentRemovedEvent): Promise { - if (this.result instanceof Promise) { - this.result = await this.result - } + return { + metadata: parsedFile.metadata, + messages: + patches.length > 0 + ? parsedFile.messages.map((message) => this.applyPatches(message, patchesMap.get(message.id) ?? [])) + : parsedFile.messages + } + } - const message = this.result.get(event.message) - if (message === undefined) return + private async findGroupByMessage (id: MessageID): Promise { + const date = parseMessageId(id) + const group1 = (await this.client.findMessagesGroups({ + card: this.params.card, + limit: 1, + toDate: { + greaterOrEqual: date + }, + order: SortingOrder.Descending, + orderBy: 'fromDate' + }))[0] - const attachments = message.attachments.filter((it) => it.card !== event.card) - if (attachments.length === message.attachments.length) return - - const updated = { - ...message, - attachments - } - this.result.update(updated) - await this.notify() + if (group1 !== undefined) { + return group1 } - private applyPatch(message: Message, patch: Patch): Message { + return (await this.client.findMessagesGroups({ + card: this.params.card, + limit: 1, + fromDate: { + lessOrEqual: date + }, + order: SortingOrder.Ascending, + orderBy: 'fromDate' + }))[0] + } + + private async findGroups (direction: Direction, fromDate?: Date): Promise { + if (this.params.id != null) { + const group = await this.findGroupByMessage(this.params.id) + return group !== undefined ? [group] : [] + } + + if (fromDate == null) { + return await this.client.findMessagesGroups({ + card: this.params.card, + limit: GROUPS_LIMIT, + order: direction === Direction.Forward ? SortingOrder.Ascending : SortingOrder.Descending, + orderBy: 'fromDate' + }) + } + + return await this.client.findMessagesGroups({ + card: this.params.card, + limit: GROUPS_LIMIT, + order: direction === Direction.Forward ? SortingOrder.Ascending : SortingOrder.Descending, + orderBy: 'fromDate', + fromDate: + direction === Direction.Forward + ? { + greater: fromDate + } + : { + less: fromDate + } + }) + } + + private async find (params: FindMessagesParams): Promise { + return await this.client.findMessages(params, this.id) + } + + private async notify (): Promise { + if (this.callback == null) return + if (this.result instanceof Promise) this.result = await this.result + const result = this.result.getResult() + this.callback(new WindowImpl(result, this.result.isTail(), this.result.isHead(), this)) + } + + async unsubscribe (): Promise { + 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) + } + } + } + + async onCreateThreadEvent (event: ThreadCreatedEvent): Promise { + if (this.params.card !== event.thread.card) return + if (this.result instanceof Promise) this.result = await this.result + + const message = this.result.get(event.thread.message) + if (message !== undefined) { + const updated: Message = { + ...message, + thread: event.thread + } + + this.result.update(updated) + void this.notify() + } + + this.messagesFromFiles = this.messagesFromFiles.map((it) => { + if (it.id === event.thread.message) { return { - ...message, - content: patch.content, - creator: patch.creator, - created: patch.created + ...it, + thread: event.thread } + } + return it + }) + } + + async onCreateMessageEvent (event: MessageCreatedEvent): Promise { + if (this.result instanceof Promise) this.result = await this.result + if (this.params.card !== event.message.card) return + const { message } = event + const exists = this.result.get(message.id) + + if (exists !== undefined) return + if (!this.match(message)) return + + if (this.result.isTail()) { + if (this.params.order === SortingOrder.Ascending) { + this.result.push(message) + } else { + this.result.unshift(message) + } + await this.notify() } + } + + private match (message: Message): boolean { + if (this.params.id != null && this.params.id !== message.id) { + return false + } + if (this.params.card !== message.card) { + return false + } + return true + } + + private async onCreatePatchEvent (event: PatchCreatedEvent): Promise { + if (this.params.card !== event.card) return + if (this.result instanceof Promise) this.result = await this.result + + const { patch } = event + const messageId = BigInt(patch.message) + const group = this.groupsBuffer.find((it) => BigInt(it.fromId) <= messageId && BigInt(it.toId) >= messageId) + + if (group != null) { + group.patches.push(patch) + } + + const message = this.result.get(patch.message) + if (message === undefined) return + + if (message.created < patch.created) { + this.result.update(this.applyPatch(message, patch)) + await this.notify() + } + } + + private async onRemoveMessageEvent (event: MessageRemovedEvent): Promise { + if (this.params.card !== event.card) return + if (this.result instanceof Promise) this.result = await this.result + + const deleted = this.result.delete(event.message) + + if (deleted !== undefined) { + void this.notify() + } + + this.messagesFromFiles = this.messagesFromFiles.filter((it) => it.id !== event.message) + } + + private async onCreateReactionEvent (event: ReactionCreatedEvent): Promise { + if (this.result instanceof Promise) this.result = await this.result + if (this.params.card !== event.card) return + + const reaction = { + ...event.reaction, + created: event.reaction.created + } + + const message = this.result.get(reaction.message) + if (message !== undefined) { + this.result.update(addReaction(message, reaction)) + void this.notify() + } + + const fromBuffer = this.messagesFromFiles.find((it) => it.id === reaction.message) + if (fromBuffer !== undefined) { + addReaction(fromBuffer, reaction) + } + } + + private async onRemoveReactionEvent (event: ReactionRemovedEvent): Promise { + if (this.result instanceof Promise) this.result = await this.result + if (this.params.card !== event.card) return + + const message = this.result.get(event.message) + if (message !== undefined) { + const updated = removeReaction(message, event.reaction, event.creator) + if (updated.reactions.length !== message.reactions.length) { + this.result.update(updated) + void this.notify() + } + } + this.messagesFromFiles = this.messagesFromFiles.map((it) => + it.id === event.message ? removeReaction(it, event.reaction, event.creator) : it + ) + } + + private async onCreateAttachmentEvent (event: AttachmentCreatedEvent): Promise { + if (this.result instanceof Promise) this.result = await this.result + + const attachment = { + ...event.attachment, + created: event.attachment.created + } + const message = this.result.get(attachment.message) + if (message === undefined) return + + message.attachments.push(attachment) + this.result.update(message) + await this.notify() + } + + private async onRemoveAttachmentEvent (event: AttachmentRemovedEvent): Promise { + if (this.result instanceof Promise) this.result = await this.result + + const message = this.result.get(event.message) + if (message === undefined) return + + const attachments = message.attachments.filter((it) => it.card !== event.card) + if (attachments.length === message.attachments.length) return + + const updated = { + ...message, + attachments + } + this.result.update(updated) + await this.notify() + } + + private applyPatch (message: Message, patch: Patch): Message { + switch (patch.type) { + case PatchType.update: + return { + ...message, + edited: patch.created, + content: patch.content + } + case PatchType.addReaction: + return addReaction(message, { + message: message.id, + reaction: patch.content, + creator: patch.creator, + created: patch.created + }) + case PatchType.removeReaction: + return removeReaction(message, patch.content, patch.creator) + case PatchType.addReply: + return addReply(message, patch.content as CardID, patch.created) + case PatchType.removeReply: + return removeReply(message, patch.content as CardID) + } + + return message + } + + private applyPatches (message: Message, patches: Patch[]): Message { + if (patches.length === 0) return message + + for (const p of patches) { + message = this.applyPatch(message, p) + } + return message + } + + copyResult (): QueryResult | undefined { + if (this.result instanceof Promise) { + return undefined + } + + return this.result.copy() + } } diff --git a/packages/query/src/messages/utils.ts b/packages/query/src/messages/utils.ts new file mode 100644 index 0000000000..1db6a1260d --- /dev/null +++ b/packages/query/src/messages/utils.ts @@ -0,0 +1,54 @@ +import type { CardID, Message, Reaction, SocialID } from '@hcengineering/communication-types' + +export function addReaction (message: Message, reaction: Reaction): Message { + message.reactions.push(reaction) + return message +} + +export function removeReaction (message: Message, emoji: string, creator: SocialID): Message { + const reactions = message.reactions.filter((it) => it.reaction !== emoji || it.creator !== creator) + if (reactions.length === message.reactions.length) return message + + return { + ...message, + reactions + } +} + +export function addReply (message: Message, thread: CardID, created: Date): Message { + if (message.thread === undefined) { + return { + ...message, + thread: { + card: message.card, + message: message.id, + thread, + repliesCount: 1, + lastReply: created + } + } + } + + if (message.thread.thread !== thread) return message + + return { + ...message, + thread: { + ...message.thread, + repliesCount: message.thread.repliesCount + 1, + lastReply: created + } + } +} + +export function removeReply (message: Message, thread: CardID): Message { + if (message.thread === undefined || message.thread.thread !== thread) return message + + return { + ...message, + thread: { + ...message.thread, + repliesCount: message.thread.repliesCount - 1 + } + } +} diff --git a/packages/query/src/notifications/query.ts b/packages/query/src/notifications/query.ts index e318a01660..7b0d5b6a50 100644 --- a/packages/query/src/notifications/query.ts +++ b/packages/query/src/notifications/query.ts @@ -1,129 +1,138 @@ import { - type FindNotificationsParams, - SortOrder, - type Notification, - type MessageID, + type FindNotificationsParams, + SortingOrder, + type Notification, + type MessageID } from '@hcengineering/communication-types' import { - type NotificationCreatedEvent, - ResponseEventType, - type ResponseEvent, - type NotificationContextRemovedEvent, - type NotificationRemovedEvent, - type NotificationContextUpdatedEvent, + type NotificationCreatedEvent, + ResponseEventType, + type ResponseEvent, + type NotificationContextRemovedEvent, + type NotificationRemovedEvent, + type NotificationContextUpdatedEvent } from '@hcengineering/communication-sdk-types' -import {BaseQuery} from '../query.ts'; +import { BaseQuery } from '../query' export class NotificationQuery extends BaseQuery { - override async find(params: FindNotificationsParams): Promise { - return this.client.findNotifications(params, this.id) + override async find (params: FindNotificationsParams): Promise { + return await this.client.findNotifications(params, this.id) + } + + override getObjectId (object: Notification): MessageID { + return object.message.id + } + + override getObjectDate (object: Notification): Date { + return object.message.created + } + + override async onEvent (event: ResponseEvent): Promise { + switch (event.type) { + case ResponseEventType.NotificationCreated: { + await this.onCreateNotificationEvent(event) + return + } + case ResponseEventType.NotificationRemoved: { + await this.onRemoveNotificationEvent(event) + return + } + case ResponseEventType.NotificationContextUpdated: { + await this.onUpdateNotificationContextEvent(event) + return + } + case ResponseEventType.NotificationContextRemoved: { + await this.onRemoveNotificationContextEvent(event) + } + } + } + + async onCreateNotificationEvent (event: NotificationCreatedEvent): Promise { + if (this.result instanceof Promise) { + this.result = await this.result } - override getObjectId(object: Notification): MessageID { - return object.message.id + const exists = this.result.get(event.notification.message.id) + if (exists !== undefined) return + + if (this.params.message != null && this.params.message !== event.notification.message.id) return + if (this.params.context != null && this.params.context !== event.notification.context) return + + if (this.result.isTail()) { + if (this.params.order === SortingOrder.Ascending) { + this.result.push(event.notification) + } else { + this.result.unshift(event.notification) + } + await this.notify() + } + } + + private async onUpdateNotificationContextEvent (event: NotificationContextUpdatedEvent): Promise { + if (this.result instanceof Promise) { + this.result = await this.result } - override getObjectDate(object: Notification): Date { - return object.message.created - } + if (this.params.context != null && this.params.context !== event.context) return + if (event.update.lastView === undefined && event.update.archivedFrom === undefined) return - override async onEvent(event: ResponseEvent): Promise { - switch (event.type) { - case ResponseEventType.NotificationCreated: - return await this.onCreateNotificationEvent(event) - case ResponseEventType.NotificationRemoved: - return await this.onRemoveNotificationEvent(event) - case ResponseEventType.NotificationContextUpdated: - return await this.onUpdateNotificationContextEvent(event) - case ResponseEventType.NotificationContextRemoved: - return await this.onRemoveNotificationContextEvent(event) - } - } + const toUpdate = + this.params.context === event.context + ? this.result.getResult() + : this.result.getResult().filter((it) => it.context === event.context) + if (toUpdate.length === 0) return - async onCreateNotificationEvent(event: NotificationCreatedEvent): Promise { - if (this.result instanceof Promise) { - this.result = await this.result - } - - const exists = this.result.get(event.notification.message.id) - if (exists !== undefined) return - - if (this.params.message != null && this.params.message !== event.notification.message.id) return - if (this.params.context != null && this.params.context !== event.notification.context) return - - if (this.result.isTail()) { - if (this.params.sort === SortOrder.Asc) { - this.result.push(event.notification) - } else { - this.result.unshift(event.notification) + for (const notification of toUpdate) { + this.result.update({ + ...notification, + ...(event.update.lastView !== undefined + ? { + read: event.update.lastView < notification.message.created } - await this.notify() - } - } - - - private async onUpdateNotificationContextEvent(event: NotificationContextUpdatedEvent): Promise { - if (this.result instanceof Promise) { - this.result = await this.result - } - - if (this.params.context != null && this.params.context !== event.context) return - if (event.update.lastView === undefined && event.update.archivedFrom === undefined) return - - const toUpdate = this.params.context === event.context ? - this.result.getResult() - : this.result.getResult().filter(it => it.context === event.context) - if (toUpdate.length === 0) return - - for (const notification of toUpdate) { - this.result.update({ - ...notification, - ...event.update.lastView !== undefined ? { - read: event.update.lastView < notification.message.created - } : {}, - ...event.update.archivedFrom !== undefined ? { - archived: event.update.archivedFrom < notification.message.created - } : {} - }) - } - } - - private async onRemoveNotificationEvent(event: NotificationRemovedEvent): Promise { - if (this.result instanceof Promise) { - this.result = await this.result - } - - const deleted = this.result.delete(event.message) - - if (deleted !== undefined) { - await this.notify() - } - } - - private async onRemoveNotificationContextEvent(event: NotificationContextRemovedEvent): Promise { - if (this.result instanceof Promise) { - this.result = await this.result - } - - if (this.params.context != null && this.params.context !== event.context) return - - if (event.context === this.params.context) { - if (this.result.length === 0) return - this.result.deleteAll() - this.result.setHead(true) - this.result.setTail(true) - await this.notify() - } else { - const toRemove = this.result.getResult().filter(it => it.context === event.context) - if (toRemove.length === 0) return - - for (const notification of toRemove) { - this.result.delete(notification.message.id) + : {}), + ...(event.update.archivedFrom !== undefined + ? { + archived: event.update.archivedFrom < notification.message.created } - await this.notify() - } + : {}) + }) + } + } + private async onRemoveNotificationEvent (event: NotificationRemovedEvent): Promise { + if (this.result instanceof Promise) { + this.result = await this.result } + const deleted = this.result.delete(event.message) + + if (deleted !== undefined) { + await this.notify() + } + } + + private async onRemoveNotificationContextEvent (event: NotificationContextRemovedEvent): Promise { + if (this.result instanceof Promise) { + this.result = await this.result + } + + if (this.params.context != null && this.params.context !== event.context) return + + if (event.context === this.params.context) { + if (this.result.length === 0) return + this.result.deleteAll() + this.result.setHead(true) + this.result.setTail(true) + await this.notify() + } else { + const toRemove = this.result.getResult().filter((it) => it.context === event.context) + if (toRemove.length === 0) return + + for (const notification of toRemove) { + this.result.delete(notification.message.id) + } + await this.notify() + } + } } diff --git a/packages/query/src/query.ts b/packages/query/src/query.ts index fd95c80cf3..a770f1b0ef 100644 --- a/packages/query/src/query.ts +++ b/packages/query/src/query.ts @@ -1,11 +1,11 @@ -import { Direction, type ID, SortOrder } from '@hcengineering/communication-types' +import { SortingOrder, type ID } from '@hcengineering/communication-types' import { type ResponseEvent, type QueryCallback, type QueryClient } from '@hcengineering/communication-sdk-types' import { QueryResult } from './result' -import { defaultQueryParams, type FindParams, type Query, type QueryId } from './types' +import { defaultQueryParams, type FindParams, type PagedQuery, type QueryId } from './types' import { WindowImpl } from './window' -export class BaseQuery implements Query { +export class BaseQuery implements PagedQuery { protected result: QueryResult | Promise> private forward: Promise | T[] = [] private backward: Promise | T[] = [] @@ -24,20 +24,18 @@ export class BaseQuery implements Query { const limit = this.params.limit ?? defaultQueryParams.limit const findParams = { ...this.params, - excluded: this.params.excluded ?? defaultQueryParams.excluded, - direction: this.params.direction ?? defaultQueryParams.direction, - sort: this.params.sort ?? defaultQueryParams.sort, + sort: this.params.order ?? defaultQueryParams.order, limit: limit + 1 } const findPromise = this.find(findParams) this.result = findPromise.then((res) => { - const isTail = params.from ? res.length <= limit : params.sort === SortOrder.Desc - const isHead = params.from === undefined && params.sort === SortOrder.Asc + const isTail = false + const isHead = false if (!isTail) { res.pop() } - const qResult = new QueryResult(res, this.getObjectId) + const qResult = new QueryResult(res, (x) => this.getObjectId(x)) qResult.setTail(isTail) qResult.setHead(isHead) @@ -55,25 +53,25 @@ export class BaseQuery implements Query { // eslint-disable-next-line @typescript-eslint/no-unused-vars protected async find(params: FindParams): Promise { - /*Implement in subclass*/ + /* Implement in subclass */ return [] as T[] } // eslint-disable-next-line @typescript-eslint/no-unused-vars protected getObjectId(object: T): ID { - /*Implement in subclass*/ + /* Implement in subclass */ return '' as ID } // eslint-disable-next-line @typescript-eslint/no-unused-vars protected getObjectDate(object: T): Date { - /*Implement in subclass*/ - return new Date(0) as Date + /* Implement in subclass */ + return new Date(0) } // eslint-disable-next-line @typescript-eslint/no-unused-vars async onEvent(event: ResponseEvent): Promise { - /*Implement in subclass*/ + /* Implement in subclass */ } setCallback(callback: QueryCallback): void { @@ -99,7 +97,7 @@ export class BaseQuery implements Query { this.callback(window) } - async loadForward() { + async requestLoadNextPage(): Promise { if (this.result instanceof Promise) { this.result = await this.result } @@ -117,9 +115,8 @@ export class BaseQuery implements Query { ...this.params, from: this.getObjectDate(last), excluded: true, - direction: Direction.Forward, limit: limit + 1, - sort: SortOrder.Asc + order: SortingOrder.Ascending } const forward = this.find(findParams) @@ -139,7 +136,7 @@ export class BaseQuery implements Query { }) } - async loadBackward() { + async requestLoadPrevPage(): Promise { if (this.result instanceof Promise) { this.result = await this.result } @@ -149,17 +146,15 @@ export class BaseQuery implements Query { if (this.result.isHead()) return - const first = this.params.sort === SortOrder.Asc ? this.result.getFirst() : this.result.getLast() + const first = this.params.order === SortingOrder.Ascending ? this.result.getFirst() : this.result.getLast() if (first === undefined) return const limit = this.params.limit ?? defaultQueryParams.limit const findParams: FindParams = { ...this.params, from: this.getObjectDate(first), - excluded: true, - direction: Direction.Backward, limit: limit + 1, - sort: SortOrder.Desc + order: SortingOrder.Descending } const backward = this.find(findParams) @@ -172,7 +167,7 @@ export class BaseQuery implements Query { res.pop() } - if (this.params.sort === SortOrder.Asc) { + if (this.params.order === SortingOrder.Ascending) { const reversed = res.reverse() this.result.prepend(reversed) } else { diff --git a/packages/query/src/result.ts b/packages/query/src/result.ts index 58e288c967..5186ccebd1 100644 --- a/packages/query/src/result.ts +++ b/packages/query/src/result.ts @@ -25,11 +25,11 @@ export class QueryResult { return this.head } - setHead(head: boolean) { + setHead(head: boolean): void { this.head = head } - setTail(tail: boolean) { + setTail(tail: boolean): void { this.tail = tail } @@ -47,7 +47,7 @@ export class QueryResult { return object } - deleteAll() { + deleteAll(): void { this.objectById.clear() } @@ -71,12 +71,12 @@ export class QueryResult { return Array.from(this.objectById.values())[this.objectById.size - 1] } - prepend(objects: T[]) { + prepend(objects: T[]): void { const current = Array.from(this.objectById.entries()) this.objectById = new Map([...objects.map<[ID, T]>((object) => [this.getId(object), object]), ...current]) } - append(objects: T[]) { + append(objects: T[]): void { for (const object of objects) { this.objectById.set(this.getId(object), object) } diff --git a/packages/query/src/types.ts b/packages/query/src/types.ts index 31ddd46f95..cf4a6b8e4f 100644 --- a/packages/query/src/types.ts +++ b/packages/query/src/types.ts @@ -1,33 +1,34 @@ import { type ResponseEvent } from '@hcengineering/communication-sdk-types' -import { Direction, SortOrder, type Window } from '@hcengineering/communication-types' +import { SortingOrder, type Window } from '@hcengineering/communication-types' -import { QueryResult } from './result.ts' +import { QueryResult } from './result' export type QueryId = number export const defaultQueryParams = { limit: 50, - excluded: false, - direction: Direction.Forward, - sort: SortOrder.Desc + order: SortingOrder.Ascending } -export type FindParams = Partial & { - from?: Date +export enum Direction { + Forward = 1, + Backward = -1 } -export interface Query { +export type FindParams = Partial + +export interface PagedQuery { readonly id: QueryId readonly params: P - onEvent(event: ResponseEvent): Promise + onEvent: (event: ResponseEvent) => Promise - loadForward(): Promise - loadBackward(): Promise + requestLoadNextPage: () => Promise + requestLoadPrevPage: () => Promise - unsubscribe(): Promise + unsubscribe: () => Promise - setCallback(callback: (window: Window) => void): void - removeCallback(): void - copyResult(): QueryResult | undefined + setCallback: (callback: (window: Window) => void) => void + removeCallback: () => void + copyResult: () => QueryResult | undefined } diff --git a/packages/query/src/window.ts b/packages/query/src/window.ts index 824df8615a..a31d7026cb 100644 --- a/packages/query/src/window.ts +++ b/packages/query/src/window.ts @@ -1,13 +1,13 @@ import type { Window } from '@hcengineering/communication-types' -import type { Query } from './types' +import type { PagedQuery } from './types' export class WindowImpl implements Window { constructor( private readonly result: T[], private readonly isTail: boolean, private readonly isHead: boolean, - private readonly query: Query + private readonly query: PagedQuery ) {} getResult(): T[] { @@ -16,12 +16,12 @@ export class WindowImpl implements Window { async loadNextPage(): Promise { if (!this.hasNextPage()) return - await this.query.loadForward() + await this.query.requestLoadNextPage() } async loadPrevPage(): Promise { if (!this.hasPrevPage()) return - await this.query.loadBackward() + await this.query.requestLoadPrevPage() } hasNextPage(): boolean { diff --git a/packages/sdk-types/package.json b/packages/sdk-types/package.json index 4295ff7149..084cb2176c 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.9", + "version": "0.1.47", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./types/index.d.ts", @@ -20,7 +20,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/hcengineering/communication.git", + "url": "git+https://github.com/hcengineering/communication.git" }, "publishConfig": { "registry": "https://npm.pkg.github.com" diff --git a/packages/sdk-types/src/client.ts b/packages/sdk-types/src/client.ts index 8d03ea2e70..36415edfb6 100644 --- a/packages/sdk-types/src/client.ts +++ b/packages/sdk-types/src/client.ts @@ -9,7 +9,9 @@ import type { NotificationContextUpdate, RichText, SocialID, - Notification + Notification, + FindMessagesGroupsParams, + MessagesGroup } from '@hcengineering/communication-types' import type { FindMessagesParams } from '@hcengineering/communication-types' @@ -18,7 +20,7 @@ import type { ResponseEvent } from './responseEvent.ts' export interface Client { createMessage(card: CardID, content: RichText, creator: SocialID): Promise removeMessage(card: CardID, id: MessageID): Promise - createPatch(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise + updateMessage(card: CardID, message: MessageID, content: RichText, creator: SocialID): Promise createReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise removeReaction(card: CardID, message: MessageID, reaction: string, creator: SocialID): Promise @@ -26,6 +28,8 @@ export interface Client { createAttachment(card: CardID, message: MessageID, attachment: CardID, creator: SocialID): Promise removeAttachment(card: CardID, message: MessageID, attachment: CardID): Promise + createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise + createNotification(message: MessageID, context: ContextID): Promise removeNotification(message: MessageID, context: ContextID): Promise @@ -36,6 +40,7 @@ export interface Client { onEvent(event: ResponseEvent): void findMessages(params: FindMessagesParams, queryId?: number): Promise + findMessagesGroups(params: FindMessagesGroupsParams): Promise findNotificationContexts(params: FindNotificationContextParams, queryId?: number): Promise findNotifications(params: FindNotificationsParams, queryId?: number): Promise diff --git a/packages/sdk-types/src/db.ts b/packages/sdk-types/src/db.ts index 24d9a1cb85..fecd247840 100644 --- a/packages/sdk-types/src/db.ts +++ b/packages/sdk-types/src/db.ts @@ -14,47 +14,55 @@ import type { BlobID, FindMessagesGroupsParams, MessagesGroup, - WorkspaceID + WorkspaceID, + PatchType, + Thread } from '@hcengineering/communication-types' export interface DbAdapter { createMessage(card: CardID, content: RichText, creator: SocialID, created: Date): Promise + removeMessage(card: CardID, id: MessageID, socialIds?: SocialID[]): Promise + removeMessages(card: CardID, fromId: MessageID, toId: MessageID): Promise - removeMessage(card: CardID, id: MessageID): Promise - removeMessages(card: CardID, ids: MessageID[]): Promise - - createPatch(card: CardID, message: MessageID, content: RichText, creator: SocialID, created: Date): Promise + createPatch( + card: CardID, + message: MessageID, + type: PatchType, + content: RichText, + creator: SocialID, + created: Date + ): Promise + removePatches(card: CardID, fromId: MessageID, toId: MessageID): Promise createMessagesGroup( card: CardID, blobId: BlobID, - from_id: MessageID, - to_id: MessageID, - from_date: Date, - to_date: Date, + fromDate: Date, + toDate: Date, + fromID: MessageID, + toID: MessageID, count: number ): Promise + 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 - findMessages(query: FindMessagesParams): Promise + createThread(card: CardID, message: MessageID, thread: CardID, created: Date): Promise + updateThread(thread: CardID, lastReply: Date, op: 'increment' | 'decrement'): Promise - findMessagesGroups(query: FindMessagesGroupsParams): 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 createContext(personalWorkspace: WorkspaceID, card: CardID, lastView?: Date, lastUpdate?: Date): Promise - updateContext(context: ContextID, update: NotificationContextUpdate): Promise - removeContext(context: ContextID): Promise findContexts( @@ -62,7 +70,6 @@ export interface DbAdapter { personalWorkspaces: WorkspaceID[], workspace?: WorkspaceID ): Promise - findNotifications( params: FindNotificationsParams, personalWorkspace: WorkspaceID, diff --git a/packages/sdk-types/src/query.ts b/packages/sdk-types/src/query.ts index 29fbe8eb9e..156a1ef67d 100644 --- a/packages/sdk-types/src/query.ts +++ b/packages/sdk-types/src/query.ts @@ -4,7 +4,13 @@ import type { Client } from './client' export type QueryClient = Pick< Client, - 'onEvent' | 'findMessages' | 'findNotificationContexts' | 'findNotifications' | 'unsubscribeQuery' | 'close' + | 'onEvent' + | 'findMessages' + | 'findMessagesGroups' + | 'findNotificationContexts' + | 'findNotifications' + | 'unsubscribeQuery' + | 'close' > export type QueryCallback = (window: Window) => void diff --git a/packages/sdk-types/src/requestEvent.ts b/packages/sdk-types/src/requestEvent.ts index d2f50edc5f..ee305b6888 100644 --- a/packages/sdk-types/src/requestEvent.ts +++ b/packages/sdk-types/src/requestEvent.ts @@ -5,24 +5,35 @@ import type { NotificationContextUpdate, RichText, SocialID, - MessagesGroup + MessagesGroup, + BlobID } from '@hcengineering/communication-types' export enum RequestEventType { CreateMessage = 'createMessage', RemoveMessage = 'removeMessage', RemoveMessages = 'removeMessages', + CreatePatch = 'createPatch', + RemovePatches = 'removePatches', + CreateReaction = 'createReaction', RemoveReaction = 'removeReaction', + CreateAttachment = 'createAttachment', RemoveAttachment = 'removeAttachment', + + CreateThread = 'createThread', + + CreateMessagesGroup = 'createMessagesGroup', + RemoveMessagesGroup = 'removeMessagesGroup', + CreateNotification = 'createNotification', RemoveNotification = 'removeNotification', + CreateNotificationContext = 'createNotificationContext', RemoveNotificationContext = 'removeNotificationContext', - UpdateNotificationContext = 'updateNotificationContext', - CreateMessagesGroup = 'createMessagesGroup' + UpdateNotificationContext = 'updateNotificationContext' } export type RequestEvent = @@ -30,6 +41,7 @@ export type RequestEvent = | RemoveMessageEvent | RemoveMessagesEvent | CreatePatchEvent + | RemovePatchesEvent | CreateReactionEvent | RemoveReactionEvent | CreateAttachmentEvent @@ -40,6 +52,8 @@ export type RequestEvent = | RemoveNotificationContextEvent | UpdateNotificationContextEvent | CreateMessagesGroupEvent + | RemoveMessagesGroupEvent + | CreateThreadEvent export interface CreateMessageEvent { type: RequestEventType.CreateMessage @@ -57,8 +71,8 @@ export interface RemoveMessageEvent { export interface RemoveMessagesEvent { type: RequestEventType.RemoveMessages card: CardID - messages: MessageID[] - silent?: boolean + fromId: MessageID + toId: MessageID } export interface CreatePatchEvent { @@ -69,6 +83,13 @@ export interface CreatePatchEvent { creator: SocialID } +export interface RemovePatchesEvent { + type: RequestEventType.RemovePatches + card: CardID + fromId: MessageID + toId: MessageID +} + export interface CreateReactionEvent { type: RequestEventType.CreateReaction card: CardID @@ -100,6 +121,13 @@ export interface RemoveAttachmentEvent { attachment: CardID } +export interface CreateThreadEvent { + type: RequestEventType.CreateThread + card: CardID + message: MessageID + thread: CardID +} + export interface CreateNotificationEvent { type: RequestEventType.CreateNotification message: MessageID @@ -135,6 +163,12 @@ export interface CreateMessagesGroupEvent { group: MessagesGroup } +export interface RemoveMessagesGroupEvent { + type: RequestEventType.RemoveMessagesGroup + card: CardID + blobId: BlobID +} + export type EventResult = CreateMessageResult | CreateNotificationContextResult | {} export interface CreateMessageResult { diff --git a/packages/sdk-types/src/responseEvent.ts b/packages/sdk-types/src/responseEvent.ts index f6b41d09fa..b1fd2cf2fb 100644 --- a/packages/sdk-types/src/responseEvent.ts +++ b/packages/sdk-types/src/responseEvent.ts @@ -9,18 +9,19 @@ import type { Patch, Reaction, SocialID, - Notification + Notification, + Thread } from '@hcengineering/communication-types' export enum ResponseEventType { MessageCreated = 'messageCreated', MessageRemoved = 'messageRemoved', - MessagesRemoved = 'messagesRemoved', PatchCreated = 'patchCreated', ReactionCreated = 'reactionCreated', ReactionRemoved = 'reactionRemoved', AttachmentCreated = 'attachmentCreated', AttachmentRemoved = 'attachmentRemoved', + ThreadCreated = 'threadCreated', NotificationCreated = 'notificationCreated', NotificationRemoved = 'notificationRemoved', NotificationContextCreated = 'notificationContextCreated', @@ -31,7 +32,6 @@ export enum ResponseEventType { export type ResponseEvent = | MessageCreatedEvent | MessageRemovedEvent - | MessagesRemovedEvent | PatchCreatedEvent | ReactionCreatedEvent | ReactionRemovedEvent @@ -42,6 +42,7 @@ export type ResponseEvent = | NotificationContextCreatedEvent | NotificationContextRemovedEvent | NotificationContextUpdatedEvent + | ThreadCreatedEvent export interface MessageCreatedEvent { type: ResponseEventType.MessageCreated @@ -54,12 +55,6 @@ export interface MessageRemovedEvent { message: MessageID } -export interface MessagesRemovedEvent { - type: ResponseEventType.MessagesRemoved - card: CardID - messages: MessageID[] -} - export interface PatchCreatedEvent { type: ResponseEventType.PatchCreated card: CardID @@ -93,6 +88,11 @@ export interface AttachmentRemovedEvent { attachment: CardID } +export interface ThreadCreatedEvent { + type: ResponseEventType.ThreadCreated + thread: Thread +} + export interface NotificationCreatedEvent { type: ResponseEventType.NotificationCreated personalWorkspace: string diff --git a/packages/sdk-types/src/serverApi.ts b/packages/sdk-types/src/serverApi.ts index 4a9d1f918d..a1d4217488 100644 --- a/packages/sdk-types/src/serverApi.ts +++ b/packages/sdk-types/src/serverApi.ts @@ -13,6 +13,7 @@ export interface ConnectionInfo { sessionId: string personalWorkspace: WorkspaceID socialIds: SocialID[] + isSystem: boolean } export interface ServerApi { diff --git a/packages/server/package.json b/packages/server/package.json index e215f29720..bd98818cb8 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/communication-server", - "version": "0.1.9", + "version": "0.1.47", "main": "dist/index.js", "module": "dist/index.js", "types": "./types/index.d.ts", diff --git a/packages/server/src/eventProcessor.ts b/packages/server/src/eventProcessor.ts index 5e37920c49..473a2d7ec4 100644 --- a/packages/server/src/eventProcessor.ts +++ b/packages/server/src/eventProcessor.ts @@ -4,7 +4,8 @@ import { type Reaction, type Attachment, type SocialID, - type WorkspaceID + type WorkspaceID, + PatchType } from '@hcengineering/communication-types' import { type CreateAttachmentEvent, @@ -38,62 +39,65 @@ import { ResponseEventType, type CreateMessagesGroupEvent, type RemoveMessagesEvent, - type MessagesRemovedEvent + type ThreadCreatedEvent, + type CreateThreadEvent, + type ConnectionInfo, + type RemovePatchesEvent, + type RemoveMessagesGroupEvent } from '@hcengineering/communication-sdk-types' -export type Result = { +export interface Result { responseEvent?: ResponseEvent result: EventResult } -export type UserInfo = { - personalWorkspace: WorkspaceID - socialIds: SocialID[] -} - export class EventProcessor { constructor( private readonly db: DbAdapter, private readonly workspace: WorkspaceID ) {} - async process(user: UserInfo, event: RequestEvent): Promise { + async process(info: ConnectionInfo, event: RequestEvent): Promise { switch (event.type) { case RequestEventType.CreateMessage: - return await this.createMessage(event, user) + return await this.createMessage(event, info) case RequestEventType.RemoveMessage: - return await this.removeMessage(event, user) + return await this.removeMessage(event, info) case RequestEventType.RemoveMessages: - return await this.removeMessages(event, user) + return await this.removeMessages(event, info) case RequestEventType.CreatePatch: - return await this.createPatch(event, user) + return await this.createPatch(event, info) case RequestEventType.CreateReaction: - return await this.createReaction(event, user) + return await this.createReaction(event, info) case RequestEventType.RemoveReaction: - return await this.removeReaction(event, user) + return await this.removeReaction(event, info) case RequestEventType.CreateAttachment: - return await this.createAttachment(event, user) + return await this.createAttachment(event, info) case RequestEventType.RemoveAttachment: - return await this.removeAttachment(event, user) + return await this.removeAttachment(event, info) case RequestEventType.CreateNotification: - return await this.createNotification(event, user) + return await this.createNotification(event, info) case RequestEventType.RemoveNotification: - return await this.removeNotification(event, user) + return await this.removeNotification(event, info) case RequestEventType.CreateNotificationContext: - return await this.createNotificationContext(event, user) + return await this.createNotificationContext(event, info) case RequestEventType.RemoveNotificationContext: - return await this.removeNotificationContext(event, user) + return await this.removeNotificationContext(event, info) case RequestEventType.UpdateNotificationContext: - return await this.updateNotificationContext(event, user) + return await this.updateNotificationContext(event, info) case RequestEventType.CreateMessagesGroup: - return await this.createMessagesGroup(event, user) + return await this.createMessagesGroup(event, info) + case RequestEventType.CreateThread: + return await this.createThread(event, info) + case RequestEventType.RemovePatches: + return await this.removePatches(event, info) + case RequestEventType.RemoveMessagesGroup: + return await this.removeMessagesGroup(event, info) } } - private async createMessage(event: CreateMessageEvent, user: UserInfo): Promise { - if (!user.socialIds.includes(event.creator)) { - throw new Error('Forbidden') - } + private async createMessage(event: CreateMessageEvent, info: ConnectionInfo): Promise { + this.checkCreator(info, event.creator) const created = new Date() const id = await this.db.createMessage(event.card, event.content, event.creator, created) @@ -102,8 +106,7 @@ export class EventProcessor { card: event.card, content: event.content, creator: event.creator, - created: created, - edited: created, + created, reactions: [], attachments: [] } @@ -117,18 +120,17 @@ export class EventProcessor { } } - private async createPatch(event: CreatePatchEvent, user: UserInfo): Promise { - if (!user.socialIds.includes(event.creator)) { - throw new Error('Forbidden') - } + private async createPatch(event: CreatePatchEvent, info: ConnectionInfo): Promise { + this.checkCreator(info, event.creator) const created = new Date() - await this.db.createPatch(event.card, event.message, event.content, event.creator, created) + await this.db.createPatch(event.card, event.message, PatchType.update, event.content, event.creator, created) const patch: Patch = { + type: PatchType.update, message: event.message, content: event.content, creator: event.creator, - created: created + created } const responseEvent: PatchCreatedEvent = { type: ResponseEventType.PatchCreated, @@ -141,16 +143,9 @@ export class EventProcessor { } } - //eslint-disable-next-line @typescript-eslint/no-unused-vars - private async removeMessage(event: RemoveMessageEvent, _: UserInfo): Promise { - const res = await this.db.removeMessage(event.card, event.message) - - if (res === undefined) { - return { - responseEvent: undefined, - result: { id: res } - } - } + private async removeMessage(event: RemoveMessageEvent, info: ConnectionInfo): Promise { + const socialIds = info.isSystem ? undefined : info.socialIds + await this.db.removeMessage(event.card, event.message, socialIds) const responseEvent: MessageRemovedEvent = { type: ResponseEventType.MessageRemoved, @@ -160,37 +155,34 @@ export class EventProcessor { return { responseEvent, - result: { id: res } + result: {} } } - //eslint-disable-next-line @typescript-eslint/no-unused-vars - private async removeMessages(event: RemoveMessagesEvent, _: UserInfo): Promise { - const ids = await this.db.removeMessages(event.card, event.messages) - - if (event.silent === true) { - return { - responseEvent: undefined, - result: { ids } - } - } - - const responseEvent: MessagesRemovedEvent = { - type: ResponseEventType.MessagesRemoved, - card: event.card, - messages: ids - } - - return { - responseEvent, - result: { ids } - } - } - - private async createReaction(event: CreateReactionEvent, user: UserInfo): Promise { - if (!user.socialIds.includes(event.creator)) { + private async removeMessages(event: RemoveMessagesEvent, info: ConnectionInfo): Promise { + if (!info.isSystem) { throw new Error('Forbidden') } + await this.db.removeMessages(event.card, event.fromId, event.toId) + + return { + result: {} + } + } + + private async removePatches(event: RemovePatchesEvent, info: ConnectionInfo): Promise { + if (!info.isSystem) { + throw new Error('Forbidden') + } + await this.db.removePatches(event.card, event.fromId, event.toId) + + return { + result: {} + } + } + + private async createReaction(event: CreateReactionEvent, info: ConnectionInfo): Promise { + this.checkCreator(info, event.creator) const created = new Date() await this.db.createReaction(event.card, event.message, event.reaction, event.creator, created) @@ -198,7 +190,7 @@ export class EventProcessor { message: event.message, reaction: event.reaction, creator: event.creator, - created: created + created } const responseEvent: ReactionCreatedEvent = { type: ResponseEventType.ReactionCreated, @@ -211,10 +203,8 @@ export class EventProcessor { } } - private async removeReaction(event: RemoveReactionEvent, user: UserInfo): Promise { - if (!user.socialIds.includes(event.creator)) { - throw new Error('Forbidden') - } + private async removeReaction(event: RemoveReactionEvent, info: ConnectionInfo): Promise { + this.checkCreator(info, event.creator) await this.db.removeReaction(event.card, event.message, event.reaction, event.creator) const responseEvent: ReactionRemovedEvent = { type: ResponseEventType.ReactionRemoved, @@ -229,8 +219,8 @@ export class EventProcessor { } } - //eslint-disable-next-line @typescript-eslint/no-unused-vars - private async createAttachment(event: CreateAttachmentEvent, _: UserInfo): Promise { + private async createAttachment(event: CreateAttachmentEvent, info: ConnectionInfo): Promise { + this.checkCreator(info, event.creator) const created = new Date() await this.db.createAttachment(event.message, event.card, event.creator, created) @@ -238,7 +228,7 @@ export class EventProcessor { message: event.message, card: event.card, creator: event.creator, - created: created + created } const responseEvent: AttachmentCreatedEvent = { type: ResponseEventType.AttachmentCreated, @@ -252,8 +242,8 @@ export class EventProcessor { } } - //eslint-disable-next-line @typescript-eslint/no-unused-vars - private async removeAttachment(event: RemoveAttachmentEvent, _: UserInfo): Promise { + // 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, @@ -267,8 +257,8 @@ export class EventProcessor { } } - //eslint-disable-next-line @typescript-eslint/no-unused-vars - private async createNotification(event: CreateNotificationEvent, _: UserInfo): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private async createNotification(event: CreateNotificationEvent, _: ConnectionInfo): Promise { await this.db.createNotification(event.message, event.context) return { @@ -276,12 +266,12 @@ export class EventProcessor { } } - private async removeNotification(event: RemoveNotificationEvent, user: UserInfo): Promise { + private async removeNotification(event: RemoveNotificationEvent, info: ConnectionInfo): Promise { await this.db.removeNotification(event.message, event.context) const responseEvent: NotificationRemovedEvent = { type: ResponseEventType.NotificationRemoved, - personalWorkspace: user.personalWorkspace, + personalWorkspace: info.personalWorkspace, message: event.message, context: event.context } @@ -291,14 +281,17 @@ export class EventProcessor { } } - private async createNotificationContext(event: CreateNotificationContextEvent, user: UserInfo): Promise { - const id = await this.db.createContext(user.personalWorkspace, event.card, event.lastView, event.lastUpdate) + private async createNotificationContext( + event: CreateNotificationContextEvent, + info: ConnectionInfo + ): Promise { + const id = await this.db.createContext(info.personalWorkspace, event.card, event.lastView, event.lastUpdate) const responseEvent: NotificationContextCreatedEvent = { type: ResponseEventType.NotificationContextCreated, context: { id, workspace: this.workspace, - personalWorkspace: user.personalWorkspace, + personalWorkspace: info.personalWorkspace, card: event.card, lastView: event.lastView, lastUpdate: event.lastUpdate @@ -310,11 +303,14 @@ export class EventProcessor { } } - private async removeNotificationContext(event: RemoveNotificationContextEvent, user: UserInfo): Promise { + private async removeNotificationContext( + event: RemoveNotificationContextEvent, + info: ConnectionInfo + ): Promise { await this.db.removeContext(event.context) const responseEvent: NotificationContextRemovedEvent = { type: ResponseEventType.NotificationContextRemoved, - personalWorkspace: user.personalWorkspace, + personalWorkspace: info.personalWorkspace, context: event.context } return { @@ -323,12 +319,12 @@ export class EventProcessor { } } - async updateNotificationContext(event: UpdateNotificationContextEvent, user: UserInfo): Promise { + async updateNotificationContext(event: UpdateNotificationContextEvent, info: ConnectionInfo): Promise { await this.db.updateContext(event.context, event.update) const responseEvent: NotificationContextUpdatedEvent = { type: ResponseEventType.NotificationContextUpdated, - personalWorkspace: user.personalWorkspace, + personalWorkspace: info.personalWorkspace, context: event.context, update: event.update } @@ -338,14 +334,54 @@ export class EventProcessor { } } - //eslint-disable-next-line @typescript-eslint/no-unused-vars - async createMessagesGroup(event: CreateMessagesGroupEvent, _: UserInfo): Promise { - const { fromId, toId, fromDate, toDate, count } = event.group - await this.db.createMessagesGroup(event.group.card, event.group.blobId, fromId, toId, fromDate, toDate, count) + async createMessagesGroup(event: CreateMessagesGroupEvent, info: ConnectionInfo): Promise { + if (!info.isSystem) { + throw new Error('Forbidden') + } + const { fromDate, toDate, count, fromId, toId, card, blobId } = event.group + await this.db.createMessagesGroup(card, blobId, fromDate, toDate, fromId, toId, count) return { responseEvent: undefined, result: {} } } + + async removeMessagesGroup(event: RemoveMessagesGroupEvent, info: ConnectionInfo): Promise { + if (!info.isSystem) { + throw new Error('Forbidden') + } + await this.db.removeMessagesGroup(event.card, event.blobId) + + return { + responseEvent: undefined, + result: {} + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private async createThread(event: CreateThreadEvent, _: ConnectionInfo): Promise { + const date = new Date() + await this.db.createThread(event.card, event.message, event.thread, date) + const responseEvent: ThreadCreatedEvent = { + type: ResponseEventType.ThreadCreated, + thread: { + card: event.card, + thread: event.thread, + message: event.message, + repliesCount: 0, + lastReply: date + } + } + return { + responseEvent, + result: {} + } + } + + private checkCreator(info: ConnectionInfo, creator: SocialID): void { + if (!info.socialIds.includes(creator) && !info.isSystem) { + throw new Error('Forbidden') + } + } } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 8e1c99d13b..586d47b99b 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -15,7 +15,7 @@ import type { ServerApi } from '@hcengineering/communication-sdk-types' -import { Manager, type BroadcastSessionsFunc } from './manager.ts' +import { Manager, type BroadcastSessionsFunc } from './manager' export class Api implements ServerApi { private readonly manager: Manager @@ -26,7 +26,7 @@ export class Api implements ServerApi { private readonly db: DbAdapter, private readonly broadcast: BroadcastSessionsFunc ) { - this.manager = new Manager(this.ctx, db, this.workspace, broadcast) + this.manager = new Manager(this.ctx, this.db, this.workspace, this.broadcast) } static async create( @@ -35,7 +35,7 @@ export class Api implements ServerApi { dbUrl: string, broadcast: BroadcastSessionsFunc ): Promise { - const db = await createDbAdapter(dbUrl, workspace) + const db = await createDbAdapter(dbUrl, workspace, ctx, { withLogs: true }) return new Api(ctx, workspace, db, broadcast) } diff --git a/packages/server/src/manager.ts b/packages/server/src/manager.ts index b77c01c741..08ac1a523e 100644 --- a/packages/server/src/manager.ts +++ b/packages/server/src/manager.ts @@ -18,17 +18,17 @@ import type { MessagesGroup, WorkspaceID } from '@hcengineering/communication-types' - -import { Triggers } from './triggers.ts' -import { EventProcessor } from './eventProcessor.ts' import type { MeasureContext } from '@hcengineering/core' +import { Triggers } from './triggers' +import { EventProcessor } from './eventProcessor' + type QueryId = number | string type QueryType = 'message' | 'notification' | 'context' export type BroadcastSessionsFunc = (ctx: MeasureContext, sessionIds: string[], result: any) => void -type SessionInfo = { +interface SessionInfo { personalWorkspace: string messageQueries: Map notificationQueries: Map @@ -36,8 +36,8 @@ type SessionInfo = { } export class Manager { - private dataBySessionId: Map = new Map() - private triggers: Triggers + private readonly dataBySessionId = new Map() + private readonly triggers: Triggers private readonly eventProcessor: EventProcessor constructor( @@ -58,17 +58,16 @@ export class Manager { return result } - async findMessagesGroups(info: ConnectionInfo, params: FindMessagesGroupsParams): Promise { + async findMessagesGroups(_: ConnectionInfo, params: FindMessagesGroupsParams): Promise { return await this.db.findMessagesGroups(params) } async event(info: ConnectionInfo, event: RequestEvent): Promise { - const { result, responseEvent } = await this.eventProcessor.process( - { personalWorkspace: info.personalWorkspace, socialIds: info.socialIds }, - event - ) + const eventResult = await this.eventProcessor.process(info, event) + + const { result, responseEvent } = eventResult if (responseEvent !== undefined) { - void this.next(responseEvent) + void this.next(info, responseEvent) } return result } @@ -86,7 +85,7 @@ export class Manager { } if (type === 'message') { - data.messageQueries.set(queryId, params) + data.messageQueries.set(queryId, params as FindMessagesParams) } else if (type === 'notification') { data.notificationQueries.set(queryId, params) } else if (type === 'context') { @@ -107,10 +106,10 @@ export class Manager { this.dataBySessionId.delete(sessionId) } - async next(event: ResponseEvent): Promise { + async next(info: ConnectionInfo, event: ResponseEvent): Promise { await this.responseEvent(event) - const derived = await this.triggers.process(event) - await Promise.all(derived.map((it) => this.next(it))) + const derived = await this.triggers.process(event, info) + await Promise.all(derived.map((it) => this.next(info, it))) } private async responseEvent(event: ResponseEvent): Promise { @@ -143,8 +142,6 @@ export class Manager { { card: event.card, id: event.message }, Array.from(info.messageQueries.values()) ) - case ResponseEventType.MessagesRemoved: - return this.matchMessagesQuery({ card: event.card }, Array.from(info.messageQueries.values())) case ResponseEventType.ReactionCreated: return this.matchMessagesQuery( { card: event.card, id: event.reaction.message }, @@ -165,6 +162,11 @@ export class Manager { { card: event.card, id: event.message }, Array.from(info.messageQueries.values()) ) + case ResponseEventType.ThreadCreated: + return this.matchMessagesQuery( + { card: event.thread.card, id: event.thread.message }, + Array.from(info.messageQueries.values()) + ) case ResponseEventType.NotificationCreated: return ( info.personalWorkspace === event.personalWorkspace && diff --git a/packages/server/src/triggers.ts b/packages/server/src/triggers.ts index c4f61b597a..80c8b1d9f3 100644 --- a/packages/server/src/triggers.ts +++ b/packages/server/src/triggers.ts @@ -1,12 +1,12 @@ import { - ResponseEventType, - type DbAdapter, type MessageCreatedEvent, - type NotificationContextCreatedEvent, - type NotificationCreatedEvent, - type ResponseEvent + type DbAdapter, + type ResponseEvent, + ResponseEventType, + type MessageRemovedEvent, + type ConnectionInfo } from '@hcengineering/communication-sdk-types' -import type { NotificationContext, ContextID, CardID, WorkspaceID } from '@hcengineering/communication-types' +import { type WorkspaceID, PatchType, type Patch } from '@hcengineering/communication-types' export class Triggers { constructor( @@ -14,103 +14,65 @@ export class Triggers { private readonly workspace: WorkspaceID ) {} - async process(event: ResponseEvent): Promise { + async process(event: ResponseEvent, info: ConnectionInfo): Promise { switch (event.type) { case ResponseEventType.MessageCreated: - return this.createNotifications(event) + return await this.onMessageCreated(event) + case ResponseEventType.MessageRemoved: + return await this.onMessageRemoved(event, info) } - return [] } - private async createNotifications(event: MessageCreatedEvent): Promise { - const card = event.message.card as any as CardID - const subscribedPersonalWorkspaces = [ - 'cd0aba36-1c4f-4170-95f2-27a12a5415f7', - 'cd0aba36-1c4f-4170-95f2-27a12a5415f8' - ] as WorkspaceID[] + async onMessageRemoved(event: MessageRemovedEvent, info: ConnectionInfo): Promise { + const { card } = event + const thread = await this.db.findThread(card) + if (thread === undefined) return [] - const res: ResponseEvent[] = [] - const contexts = await this.db.findContexts({ card }, [], this.workspace) + const date = new Date() + const socialId = info.socialIds[0] - res.push(...(await this.updateNotificationContexts(event.message.created, contexts))) - - for (const personalWorkspace of subscribedPersonalWorkspaces) { - const existsContext = contexts.find( - (it) => it.card === card && it.personalWorkspace === personalWorkspace && this.workspace === it.workspace - ) - const contextId = await this.getOrCreateContextId( - personalWorkspace, - card, - res, - event.message.created, - existsContext - ) - - await this.db.createNotification(event.message.id, contextId) - - const resultEvent: NotificationCreatedEvent = { - type: ResponseEventType.NotificationCreated, - personalWorkspace, - notification: { - context: contextId, - message: event.message, - read: false, - archived: false - } - } - res.push(resultEvent) + const patch: Patch = { + message: thread.message, + type: PatchType.removeReply, + content: thread.thread, + creator: socialId, + created: date } + await this.db.updateThread(thread.thread, date, 'decrement') + await this.db.createPatch(thread.card, patch.message, patch.type, patch.content, patch.creator, patch.created) - return res + return [ + { + type: ResponseEventType.PatchCreated, + card: thread.card, + patch + } + ] } - private async getOrCreateContextId( - personalWorkspace: WorkspaceID, - card: CardID, - res: ResponseEvent[], - lastUpdate: Date, - context?: NotificationContext - ): Promise { - if (context !== undefined) { - return context.id - } else { - const contextId = await this.db.createContext(personalWorkspace, card, undefined, lastUpdate) - const newContext = { - id: contextId, - card, - workspace: this.workspace, - personalWorkspace - } - const resultEvent: NotificationContextCreatedEvent = { - type: ResponseEventType.NotificationContextCreated, - context: newContext - } + async onMessageCreated(event: MessageCreatedEvent): Promise { + const { message } = event + const thread = await this.db.findThread(message.card) + if (thread === undefined) return [] - res.push(resultEvent) - - return contextId + const date = new Date() + const patch: Patch = { + message: thread.message, + type: PatchType.addReply, + content: thread.thread, + creator: message.creator, + created: date } - } + await this.db.updateThread(thread.thread, date, 'increment') + await this.db.createPatch(thread.card, patch.message, patch.type, patch.content, patch.creator, patch.created) - private async updateNotificationContexts( - lastUpdate: Date, - contexts: NotificationContext[] - ): Promise { - const res: ResponseEvent[] = [] - for (const context of contexts) { - if (context.lastUpdate === undefined || context.lastUpdate < lastUpdate) { - await this.db.updateContext(context.id, { lastUpdate }) - res.push({ - type: ResponseEventType.NotificationContextUpdated, - personalWorkspace: context.personalWorkspace, - context: context.id, - update: { - lastUpdate - } - }) + return [ + { + type: ResponseEventType.PatchCreated, + card: thread.card, + patch } - } - return res + ] } } diff --git a/packages/core/package.json b/packages/shared/package.json similarity index 68% rename from packages/core/package.json rename to packages/shared/package.json index ba3abbb372..ac4216412d 100644 --- a/packages/core/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { - "name": "@hcengineering/communication-core", - "version": "0.1.9", + "name": "@hcengineering/communication-shared", + "version": "0.1.47", "main": "dist/index.js", "module": "dist/index.js", "types": "./types/index.d.ts", @@ -10,13 +10,15 @@ "types/**/*.d.ts" ], "devDependencies": { - "@types/bun": "^1.1.14" + "@types/bun": "^1.1.14", + "@types/js-yaml": "^4.0.9" }, "peerDependencies": { "typescript": "^5.6.3" }, "dependencies": { - "@hcengineering/communication-types": "workspace:*" + "@hcengineering/communication-types": "workspace:*", + "js-yaml": "^4.1.0" }, "repository": { "type": "git", diff --git a/packages/shared/src/files.ts b/packages/shared/src/files.ts new file mode 100644 index 0000000000..0d73c08104 --- /dev/null +++ b/packages/shared/src/files.ts @@ -0,0 +1,58 @@ +import type { + FileMessage, + FileMetadata, + MessagesGroup, + ParsedFile, + WorkspaceID +} from '@hcengineering/communication-types' +import yaml from 'js-yaml' + +import { retry, type RetryOptions } from './retry' + +export async function loadGroupFile( + workspace: WorkspaceID, + filesUrl: string, + group: MessagesGroup, + options: RetryOptions +): Promise { + const url = getFileUrl(workspace, filesUrl, group.blobId) + + const file = await retry(() => fetchFile(url), options) + const [metadata, messages] = yaml.loadAll(file) as [FileMetadata, FileMessage[]] + + return { + metadata, + messages: messages.map((message) => ({ + id: message.id, + card: metadata.card, + content: message.content, + edited: message.edited, + creator: message.creator, + created: message.created, + attachments: [], + reactions: message.reactions.map((reaction) => ({ + message: message.id, + reaction: reaction.reaction, + creator: reaction.creator, + created: reaction.created + })) + })) + } +} + +async function fetchFile(url: string): Promise { + const res = await fetch(url) + + if (!res.ok) { + throw new Error(`Failed to fetch file: ${res.statusText}`) + } + + return await res.text() +} + +function getFileUrl(workspace: WorkspaceID, urlTemplate: string, file: string): string { + return urlTemplate + .replaceAll(':filename', encodeURIComponent(file)) + .replaceAll(':workspace', encodeURIComponent(workspace)) + .replaceAll(':blobId', encodeURIComponent(file)) +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000000..4cb8058974 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,3 @@ +export * from './message.ts' +export * from './retry.ts' +export * from './files.ts' diff --git a/packages/shared/src/message.ts b/packages/shared/src/message.ts new file mode 100644 index 0000000000..394acc4f22 --- /dev/null +++ b/packages/shared/src/message.ts @@ -0,0 +1,24 @@ +import type { MessageID } from '@hcengineering/communication-types' + +let lastTimestamp = 0 +let counter = 0n + +export function generateMessageId(): MessageID { + const timestamp = Math.floor(Date.now() / 1000) + + if (timestamp !== lastTimestamp) { + lastTimestamp = timestamp + counter = 0n + } + + const id = (BigInt(timestamp) << 20n) | counter + counter++ + + return id.toString() as MessageID +} + +export function parseMessageId(messageId: MessageID): Date { + const timestamp = Number(BigInt(messageId) >> 20n) + + return new Date(timestamp * 1000) +} diff --git a/packages/shared/src/retry.ts b/packages/shared/src/retry.ts new file mode 100644 index 0000000000..eb533dca69 --- /dev/null +++ b/packages/shared/src/retry.ts @@ -0,0 +1,20 @@ +export interface RetryOptions { + retries: number + delay?: number +} + +export async function retry(op: () => Promise, { retries, delay }: RetryOptions): Promise { + let error: any + while (retries > 0) { + retries-- + try { + return await op() + } catch (err: any) { + error = err + if (retries !== 0 && delay !== undefined && delay > 0) { + await new Promise((resolve) => setTimeout(resolve, delay)) + } + } + } + throw error +} diff --git a/packages/core/tsconfig.json b/packages/shared/tsconfig.json similarity index 100% rename from packages/core/tsconfig.json rename to packages/shared/tsconfig.json diff --git a/packages/sqlite-wasm/package.json b/packages/sqlite-wasm/package.json index 991c97f1db..4f2d9621c2 100644 --- a/packages/sqlite-wasm/package.json +++ b/packages/sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/communication-sqlite-wasm", - "version": "0.1.9", + "version": "0.1.47", "main": "dist/index.js", "module": "dist/index.js", "types": "./types/index.d.ts", diff --git a/packages/sqlite-wasm/src/adapter.ts b/packages/sqlite-wasm/src/adapter.ts index c5a7ed5eb8..75620ee250 100644 --- a/packages/sqlite-wasm/src/adapter.ts +++ b/packages/sqlite-wasm/src/adapter.ts @@ -73,8 +73,6 @@ export class SqliteAdapter { workspace: string, card: CardID, blobId: BlobID, - from_id: MessageID, - to_id: MessageID, from_date: Date, to_date: Date, count: number diff --git a/packages/sqlite-wasm/src/db/message.ts b/packages/sqlite-wasm/src/db/message.ts index f02d0b33ab..28b1b03100 100644 --- a/packages/sqlite-wasm/src/db/message.ts +++ b/packages/sqlite-wasm/src/db/message.ts @@ -3,10 +3,10 @@ import { type MessageID, type CardID, type FindMessagesParams, - SortOrder, + SortingOrder, type SocialID, type RichText, - Direction, type Reaction, type Attachment + type Reaction, type Attachment } from '@hcengineering/communication-types' import {BaseDb} from './base.ts' @@ -124,7 +124,7 @@ export class MessagesDb extends BaseDb { const where = this.buildMessageWhere(workspace, params) const groupBy = `GROUP BY m.id` - const orderBy = params.sort ? `ORDER BY m.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` : '' + const orderBy = params.order ? `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : '' const limit = params.limit ? ` LIMIT ${params.limit}` : '' const sql = [select, where, groupBy, orderBy, limit].join(' ') @@ -143,19 +143,20 @@ export class MessagesDb extends BaseDb { where.push(`m.id = '${params.id}'`) } - if (params.from != null) { - const exclude = params.excluded ?? false - const direction = params.direction ?? Direction.Forward - const getOperator = () => { - if (exclude) { - return direction === Direction.Forward ? '>' : '<' - } else { - return direction === Direction.Forward ? '>=' : '<=' - } - } - - where.push(`m.created ${getOperator()} ${params.from}`) - } + //TODO: FIX ME + // if (params.from != null) { + // const exclude = params.excluded ?? false + // const direction = params.direction ?? Direction.Forward + // const getOperator = () => { + // if (exclude) { + // return direction === Direction.Forward ? '>' : '<' + // } else { + // return direction === Direction.Forward ? '>=' : '<=' + // } + // } + // + // where.push(`m.created ${getOperator()} ${params.from}`) + // } return `WHERE ${where.join(' AND ')}` } diff --git a/packages/sqlite-wasm/src/db/notification.ts b/packages/sqlite-wasm/src/db/notification.ts index d1daf8ce17..9decaa5675 100644 --- a/packages/sqlite-wasm/src/db/notification.ts +++ b/packages/sqlite-wasm/src/db/notification.ts @@ -3,7 +3,7 @@ import { type ContextID, type CardID, type NotificationContext, - type FindNotificationContextParams, SortOrder, + type FindNotificationContextParams, SortingOrder, type FindNotificationsParams, type Notification, type NotificationContextUpdate } from '@hcengineering/communication-types' @@ -130,7 +130,7 @@ export class NotificationsDb extends BaseDb { `; const where = this.buildNotificationWhere(params, personalWorkspace, workspace) const groupBy = `GROUP BY n.message_id, n.context_id, m.id, nc.card_id, nc.archived_from, nc.last_view, nc.last_update`; - const orderBy = `ORDER BY m.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` + const orderBy = `ORDER BY m.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` const limit = params.limit ? ` LIMIT ${params.limit}` : '' const sql = [select, where, groupBy, orderBy, limit].join(' ') diff --git a/packages/types/package.json b/packages/types/package.json index 988ac8491d..2f07b5b9cf 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/communication-types", - "version": "0.1.9", + "version": "0.1.47", "main": "dist/index.js", "module": "dist/index.js", "types": "./types/index.d.ts", diff --git a/packages/types/src/file.ts b/packages/types/src/file.ts new file mode 100644 index 0000000000..7cc1276064 --- /dev/null +++ b/packages/types/src/file.ts @@ -0,0 +1,36 @@ +import type { CardID, Message, MessageID, RichText, SocialID } from './message' + +export interface FileMetadata { + card: CardID + title: string + fromDate: Date + toDate: Date +} + +export interface FileMessage { + id: MessageID + content: RichText + edited?: Date + creator: SocialID + created: Date + reactions: FileReaction[] + thread?: FileThread +} + +export interface FileReaction { + reaction: string + creator: SocialID + created: Date +} + +export interface FileThread { + thread: CardID + repliesCount: number + lastReply: Date + replied: SocialID[] +} + +export interface ParsedFile { + metadata: FileMetadata + messages: Message[] +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0d1134f9e2..d8fd44fd1c 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,3 +1,4 @@ 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 e848df14a7..d299ae7fe9 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -7,19 +7,18 @@ export type SocialID = PersonId export type WorkspaceID = WorkspaceUuid export type RichText = string -export type ID = string | number -export type MessageID = number & { message: true } +export type ID = string +export type MessageID = string & { message: true } -interface Object { - creator: SocialID - created: Date -} - -export interface Message extends Object { +export interface Message { id: MessageID card: CardID content: RichText + creator: SocialID + created: Date + edited?: Date + thread?: Thread reactions: Reaction[] attachments: Attachment[] } @@ -32,19 +31,43 @@ export interface MessagesGroup { fromDate: Date toDate: Date count: number + patches: Patch[] } -export interface Patch extends Object { +export interface Patch { message: MessageID - content: RichText + type: PatchType + content: string + creator: SocialID + created: Date } -export interface Reaction extends Object { +export enum PatchType { + update = 'update', + addReaction = 'addReaction', + removeReaction = 'removeReaction', + addReply = 'addReply', + removeReply = 'removeReply' +} + +export interface Reaction { message: MessageID reaction: string + creator: SocialID + created: Date } -export interface Attachment extends Object { +export interface Attachment { message: MessageID card: CardID + creator: SocialID + created: Date +} + +export interface Thread { + card: CardID + message: MessageID + thread: CardID + repliesCount: number + lastReply: Date } diff --git a/packages/types/src/query.ts b/packages/types/src/query.ts index 63e08dca8b..34dce7a0ce 100644 --- a/packages/types/src/query.ts +++ b/packages/types/src/query.ts @@ -1,15 +1,15 @@ +import { SortingOrder } from '@hcengineering/core' + import type { BlobID, CardID, MessageID } from './message' import type { ContextID } from './notification' -export enum SortOrder { - Asc = 1, - Desc = -1 -} +export { SortingOrder } -export enum Direction { - Backward = 1, - Forward = -1 -} +export type ComparisonOperator = 'less' | 'lessOrEqual' | 'greater' | 'greaterOrEqual' | 'notEqual' + +type Exclusive = { + [K in keyof T]: Record & Partial, never>> +}[keyof T] export interface Window { getResult(): T[] @@ -22,16 +22,14 @@ export interface Window { } interface FindParams { - from?: Date - excluded?: boolean - direction?: Direction - sort?: SortOrder + order?: SortingOrder limit?: number } export interface FindMessagesParams extends FindParams { id?: MessageID - card?: CardID + card: CardID + created?: Exclusive> | Date } export interface FindNotificationsParams extends FindParams { @@ -39,6 +37,7 @@ export interface FindNotificationsParams extends FindParams { message?: MessageID read?: boolean archived?: boolean + created?: Exclusive> | Date } export interface FindNotificationContextParams extends FindParams { @@ -46,20 +45,10 @@ export interface FindNotificationContextParams extends FindParams { card?: CardID } -export type ComparisonOperator = 'less' | 'lessOrEqual' | 'greater' | 'greaterOrEqual' | 'notEqual' - -type Exclusive = { - [K in keyof T]: Record & Partial, never>> -}[keyof T] - -export interface FindMessagesGroupsParams { +export interface FindMessagesGroupsParams extends FindParams { card?: CardID blobId?: BlobID - fromId?: Exclusive> | MessageID - toId?: Exclusive> | MessageID - fromDate?: Exclusive> | Date - toDate?: Exclusive> | Date - limit?: number - sortBy?: 'fromId' | 'toId' | 'fromDate' | 'toDate' - sort?: SortOrder + fromDate?: Partial> | Date + toDate?: Partial> | Date + orderBy?: 'fromDate' | 'toDate' }